1 package org.daisy.pipeline.audio;
2
3 import java.io.BufferedInputStream;
4 import java.io.ByteArrayInputStream;
5 import java.io.InputStream;
6 import java.io.IOException;
7 import java.nio.ByteBuffer;
8 import java.nio.ByteOrder;
9 import java.time.Duration;
10 import java.util.Arrays;
11 import java.util.Collections;
12 import java.util.Iterator;
13 import java.util.NoSuchElementException;
14
15 import com.google.common.collect.AbstractIterator;
16 import com.google.common.io.ByteStreams;
17
18 import javax.sound.sampled.AudioFormat;
19 import javax.sound.sampled.AudioFormat.Encoding;
20 import javax.sound.sampled.AudioInputStream;
21 import javax.sound.sampled.AudioSystem;
22 import javax.sound.sampled.UnsupportedAudioFileException;
23
24 public final class AudioUtils {
25
26 private AudioUtils() {}
27
28
29
30
31
32
33 public static AudioInputStream createAudioStream(AudioFormat format, byte[] data) {
34 return createAudioStream(format, new ByteArrayInputStream(data));
35 }
36
37 public static AudioInputStream createAudioStream(AudioFormat format, ByteArrayInputStream data) {
38 if (format.getFrameSize() == AudioSystem.NOT_SPECIFIED)
39 throw new IllegalArgumentException("unknown frame size");
40 return new AudioInputStream(data,
41 format,
42
43
44 data.available() / format.getFrameSize());
45 }
46
47
48
49
50
51
52
53
54 public static AudioInputStream createAudioStream(InputStream stream)
55 throws UnsupportedAudioFileException, IOException {
56 AudioInputStream audio = AudioSystem.getAudioInputStream(new BufferedInputStream(stream));
57 AudioFormat format = audio.getFormat();
58 if (format.getFrameSize() != AudioSystem.NOT_SPECIFIED) {
59 byte[] data = ByteStreams.toByteArray(audio);
60 audio.close();
61 audio = createAudioStream(format, data);
62 }
63 return audio;
64 }
65
66
67
68
69
70
71 public static Duration getDuration(AudioInputStream stream) {
72 return getDurationFromFrames(PCMAudioFormat.of(stream.getFormat()), stream.getFrameLength());
73 }
74
75
76
77
78
79 public static Duration getDuration(AudioFormat format, int bytes) {
80 format = PCMAudioFormat.of(format);
81 return getDurationFromFrames((PCMAudioFormat)format, (long)(bytes / format.getFrameSize()));
82 }
83
84 private static Duration getDurationFromFrames(PCMAudioFormat format, Long frames) {
85 return Duration.ofMillis((long)(frames.floatValue() / (format.getFrameRate() / 1000)));
86 }
87
88
89
90
91 public static long getLengthInFrames(PCMAudioFormat format, Duration duration) {
92
93 return (long)Math.ceil((double)(format.getFrameRate() / 1000) * duration.toMillis());
94 }
95
96
97
98
99
100
101 public static AudioInputStream concat(Iterable<AudioInputStream> streams) {
102 int count = 0;
103 long _totalLength = 0;
104 PCMAudioFormat format = null;
105 for (AudioInputStream s : streams) {
106 count++;
107 _totalLength += s.getFrameLength();
108 if (format == null) {
109 try {
110 format = PCMAudioFormat.of(s.getFormat());
111 } catch (IllegalArgumentException e) {
112 throw new IllegalArgumentException("AudioInputStream must be PCM encoded, but got: "+ s.getFormat());
113 }
114 } else if (!format.matches(s.getFormat()))
115 throw new IllegalArgumentException("Can not concatenate AudioInputStream with different audio formats");
116 }
117 if (count == 0)
118 throw new IllegalArgumentException("At least one AudioInputStream expected");
119 if (count == 1)
120 return streams.iterator().next();
121 long totalLength = _totalLength;
122 int frameSize = format.getFrameSize();
123 return new AudioInputStream(
124 new InputStream() {
125 Iterator<AudioInputStream> nextStreams = streams.iterator();
126 AudioInputStream stream = null;
127 byte[] frame = new byte[frameSize];
128
129 long availableFrames = totalLength;
130 int availableInFrame = 0;
131 @Override
132 public int read() throws IOException {
133 if (availableFrames == 0 && availableInFrame == 0)
134 return -1;
135 if (availableInFrame > 0)
136 return frame[frameSize - (availableInFrame--)] & 0xFF;
137 if (stream != null) {
138 availableInFrame = stream.read(frame);
139 if (availableInFrame > 0) {
140 availableFrames--;
141 return frame[frameSize - (availableInFrame--)] & 0xFF;
142 }
143 }
144 try {
145 if (stream != null)
146 stream.close();
147 stream = nextStreams.next();
148 } catch (NoSuchElementException e) {
149 return -1;
150 }
151 return read();
152 }
153 @Override
154 public int read(byte[] b, int off, int len) throws IOException {
155 if (off < 0 || len < 0 || b.length - off < len)
156 throw new IndexOutOfBoundsException();
157 if (len == 0)
158 return 0;
159 if (availableFrames == 0 && availableInFrame == 0)
160 return -1;
161 int read = 0;
162 while (true) {
163 if (availableInFrame > 0) {
164 System.arraycopy(frame, frameSize - availableInFrame, b, off, Math.min(len, availableInFrame));
165 if (availableInFrame >= len) {
166 availableInFrame -= len;
167 read += len;
168 len = 0;
169 } else {
170 len -= availableInFrame;
171 off += availableInFrame;
172 read += availableInFrame;
173 availableInFrame = 0;
174 }
175 }
176 if (len < frameSize)
177 return read;
178
179 if (stream != null) {
180 int l = stream.read(b, off, len);
181
182
183 if (l > 0) {
184 if (l % frameSize > 0)
185 throw new IllegalStateException();
186 len -= l;
187 off += l;
188 read += l;
189 availableFrames -= (l / frameSize);
190 continue;
191 }
192 }
193 try {
194 if (stream != null)
195 stream.close();
196 stream = nextStreams.next();
197 } catch (NoSuchElementException e) {
198 return read;
199 }
200 }
201 }
202 @Override
203 public int available() {
204 try {
205 return Math.toIntExact(Math.multiplyExact(availableFrames, frameSize)) + availableInFrame;
206 } catch (ArithmeticException e) {
207 return Integer.MAX_VALUE;
208 }
209 }
210 @Override
211 public void close() throws IOException {
212 if (stream != null)
213 stream.close();
214 while (nextStreams.hasNext())
215 nextStreams.next().close();
216 }
217 },
218 format,
219 totalLength);
220 }
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237 public static Iterable<AudioInputStream> split(AudioInputStream stream, Duration... splitPoints) {
238 long[] splitPointsInFrames = new long[splitPoints.length];
239 PCMAudioFormat format = PCMAudioFormat.of(stream.getFormat());
240 for (int i = 0; i < splitPoints.length; i++)
241 splitPointsInFrames[i] = getLengthInFrames(format, splitPoints[i]);
242 return split(stream, splitPointsInFrames);
243 }
244
245 public static Iterable<AudioInputStream> split(AudioInputStream stream, long... splitPoints) {
246 if (splitPoints.length == 0)
247 return Collections.singleton(stream);
248 PCMAudioFormat format = PCMAudioFormat.of(stream.getFormat());
249 long totalFrames = stream.getFrameLength();
250 {
251 long begin = 0;
252 for (long end : splitPoints) {
253 if (end < begin)
254 throw new IllegalArgumentException();
255 begin = end; }
256 if (totalFrames < begin)
257
258 if (totalFrames + 1 < begin)
259 throw new IllegalArgumentException(
260 "invalid split points: " + Arrays.toString(splitPoints)
261 + " (total number of frames: " + totalFrames + ")");
262 }
263 int frameSize = format.getFrameSize();
264 return new Iterable<AudioInputStream>() {
265 public Iterator<AudioInputStream> iterator() {
266 return new AbstractIterator<AudioInputStream>() {
267 long framesAvailable = totalFrames;
268 long framesConsumed = 0;
269 int i = 0;
270 protected AudioInputStream computeNext() {
271 if (i > splitPoints.length)
272 return endOfData();
273 long frames = i == splitPoints.length
274 ? framesAvailable
275 : splitPoints[i] - framesConsumed;
276 if (frames > framesAvailable) {
277 if (i == splitPoints.length - 1 && frames == framesAvailable + 1)
278 frames--;
279 else
280 throw new IllegalStateException("coding error");
281 }
282
283
284
285
286
287
288 byte[] bytes = new byte[Math.toIntExact(frames * frameSize)];
289 if (frames > 0) {
290 try {
291 if (stream.read(bytes) < bytes.length)
292
293
294 throw new IllegalStateException("coding error");
295 } catch (IOException e) {
296 throw new RuntimeException(e);
297 }
298 framesConsumed += frames;
299 framesAvailable -= frames;
300 }
301 i++;
302 return createAudioStream(format, bytes);
303 }
304 };
305 }
306 };
307 }
308
309
310
311
312
313 public static AudioInputStream createSilence(PCMAudioFormat format, Duration duration) {
314 if (duration.compareTo(Duration.ZERO) < 0)
315 throw new IllegalArgumentException();
316 long frames = getLengthInFrames(format, duration);
317 if (frames == 0)
318 return null;
319 else
320 return createSilence(format, frames);
321 }
322
323
324
325
326
327
328 public static AudioInputStream createSilence(AudioFormat format, long frames) {
329 format = PCMAudioFormat.of(format);
330 if (frames < 0)
331 throw new IllegalArgumentException();
332 if (frames == 0)
333 return null;
334 byte[] data = new byte[Math.multiplyExact(Math.toIntExact(frames), format.getFrameSize())];
335 Encoding encoding = format.getEncoding();
336 if (encoding == Encoding.PCM_SIGNED)
337 return createAudioStream(format, data);
338 else if (encoding == Encoding.PCM_UNSIGNED)
339 return convertAudioStream(
340 format,
341 createAudioStream(new AudioFormat(Encoding.PCM_SIGNED,
342 format.getSampleRate(),
343 format.getSampleSizeInBits(),
344 format.getChannels(),
345 format.getFrameSize(),
346 format.getFrameRate(),
347 format.isBigEndian()),
348 data));
349 else
350 throw new UnsupportedOperationException("Not implemented");
351 }
352
353
354
355
356 public static AudioInputStream convertAudioStream(AudioFormat newFormat, AudioInputStream stream) {
357 AudioFormat format = stream.getFormat();
358 if (format.getSampleSizeInBits() > 8
359 && newFormat.getSampleSizeInBits() > 8
360 && !format.isBigEndian()
361 && !newFormat.isBigEndian()
362 && ((format.getEncoding() == Encoding.PCM_UNSIGNED
363 && newFormat.getEncoding() == Encoding.PCM_SIGNED)
364 || (format.getEncoding() == Encoding.PCM_SIGNED
365 && newFormat.getEncoding() == Encoding.PCM_UNSIGNED))) {
366
367
368
369
370 return convertAudioStream(
371 newFormat,
372 convertAudioStream(
373 new AudioFormat(format.getSampleRate(),
374 format.getSampleSizeInBits(),
375 format.getChannels(),
376 format.getEncoding() == Encoding.PCM_SIGNED,
377 true),
378 stream));
379 } else if (format.getEncoding() == Encoding.PCM_FLOAT
380 && newFormat.getEncoding() == Encoding.PCM_FLOAT
381 && format.getSampleSizeInBits() == 64
382 && newFormat.getSampleSizeInBits() == 64
383 && format.isBigEndian() != newFormat.isBigEndian()) {
384
385
386
387 AudioFormat otherEndian = new AudioFormat(format.getEncoding(),
388 format.getSampleRate(),
389 format.getSampleSizeInBits(),
390 format.getChannels(),
391 format.getFrameSize(),
392 format.getFrameRate(),
393 !format.isBigEndian());
394 ByteBuffer bytes; {
395 try {
396 bytes = ByteBuffer.wrap(ByteStreams.toByteArray(stream));
397 } catch (IOException e) {
398 throw new RuntimeException(e);
399 }
400 }
401 ByteBuffer reorderedBytes = ByteBuffer.wrap(new byte[bytes.array().length]);
402 reorderedBytes = reorderedBytes.order(ByteOrder.LITTLE_ENDIAN);
403 while (bytes.hasRemaining())
404 reorderedBytes.putLong(bytes.getLong());
405 stream = createAudioStream(otherEndian, reorderedBytes.array());
406 if (otherEndian.matches(newFormat))
407 return stream;
408 else
409 return convertAudioStream(newFormat, stream);
410 }
411 return AudioSystem.getAudioInputStream(newFormat, stream);
412 }
413
414 public static boolean isPCM(AudioFormat format) {
415 Encoding encoding = format.getEncoding();
416 return encoding == Encoding.PCM_SIGNED
417 || encoding == Encoding.PCM_UNSIGNED
418 || encoding == Encoding.PCM_FLOAT;
419 }
420 }