source: trunk/src/org/apollo/io/AudioIO.java@ 1007

Last change on this file since 1007 was 1007, checked in by davidb, 8 years ago

Generalization of audio support to allow playback/mixer to be stereo, plus some edits to comments

File size: 18.7 KB
Line 
1package org.apollo.io;
2
3import java.io.ByteArrayInputStream;
4import java.io.ByteArrayOutputStream;
5import java.io.File;
6import java.io.FileOutputStream;
7import java.io.IOException;
8import java.util.LinkedList;
9
10import javax.sound.sampled.AudioFileFormat;
11import javax.sound.sampled.AudioFormat;
12import javax.sound.sampled.AudioInputStream;
13import javax.sound.sampled.AudioSystem;
14import javax.sound.sampled.UnsupportedAudioFileException;
15import javax.sound.sampled.AudioFileFormat.Type;
16
17import org.apollo.audio.ApolloSubjectChangedEvent;
18import org.apollo.audio.SampledAudioManager;
19import org.apollo.mvc.AbstractSubject;
20import org.apollo.mvc.Observer;
21import org.apollo.mvc.SubjectChangedEvent;
22import org.apollo.util.AudioMath;
23
24
25/**
26 * Provides audio IO and conversion operations.
27 *
28 * @author Brook Novak
29 *
30 */
31public class AudioIO {
32
33 private AudioIO () {}
34 private static AudioIO innerclassCreator = new AudioIO();
35
36 /**
37 * Determines the running length of a audio file.
38 *
39 * @param path
40 * The path to the audio file.
41 *
42 * @return
43 * The running time of the sound file in milliseconds.
44 * -1 if the audio frame count is not specified due to the encoding method.
45 *
46 * @throws IOException
47 * If failed to create file for saving, or an error occurred while writing audio bytes.
48 *
49 * @throws UnsupportedAudioFileException
50 * If audio format is unsupported or the encoding is not PCM.
51 *
52 * @throws NullPointerException
53 * If path is null.
54 *
55 */
56 public static long getRunningTime(String path) throws IOException, UnsupportedAudioFileException {
57 if (path == null) throw new NullPointerException("path");
58
59 AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(new File(path));
60
61 if (fileFormat.getFrameLength() == AudioSystem.NOT_SPECIFIED)
62 return -1;
63
64 return AudioMath.framesToMilliseconds(fileFormat.getFrameLength(), fileFormat.getFormat());
65 }
66
67
68 /**
69 * Determines whether or not a file can be imported. Depending on the
70 * file exists and is an audio file with a supported format (or conversion).
71 *
72 * @param f
73 * The file to check.
74 *
75 * @return
76 * True if the file can be imported. Otherwise false.
77 */
78 public static boolean canImportFile(File f) {
79
80 if (f == null || !f.exists() || !f.canRead()) return false;
81
82 AudioFileFormat fileFormat;
83 try {
84 fileFormat = AudioSystem.getAudioFileFormat(f);
85 } catch (UnsupportedAudioFileException e) {
86 return false;
87 } catch (IOException e) {
88 return false;
89 }
90
91 return (SampledAudioManager.getInstance().isFormatSupportedForPlayback(fileFormat.getFormat()) ||
92 AudioSystem.isConversionSupported(
93 SampledAudioManager.getInstance().getDefaultPlaybackFormat(),
94 fileFormat.getFormat()));
95 }
96
97 /**
98 * Converts audio bytes into a target format. ...If needs to.
99 *
100 * @param sourceBytes
101 * The bytes to convert. Must not be null.
102 *
103 * @param sourceFormat
104 * The format of the bytes that will be converted. Must not be null.
105 *
106 * @param targetFormat
107 * The format to convert the bytes to. Must not be null.
108 *
109 * @return
110 * The converted bytes. If the source and target formats match then sourceBytes is returned.
111 *
112 * @throws IOException
113 * If an error occured while converting
114 *
115 * @throws NullPointerException
116 * If any of the arguments are null.
117 *
118 * @throws IllegalArgumentException
119 * If the conversion is not supported.
120 *
121 */
122 public static byte[] convertAudioBytes(byte[] sourceBytes, AudioFormat sourceFormat, AudioFormat targetFormat)
123 throws IOException {
124
125 if (sourceBytes == null) throw new NullPointerException("sourceBytes");
126 if (sourceFormat == null) throw new NullPointerException("sourceFormat");
127 if (targetFormat == null) throw new NullPointerException("targetFormat");
128 if (sourceBytes.length == 0) throw new IllegalArgumentException("sourceBytes length is zero");
129
130 if (!AudioSystem.isConversionSupported(targetFormat, sourceFormat))
131 throw new IllegalArgumentException("Conversion not supported");
132
133 // Need converting?
134 if (targetFormat.equals(sourceFormat)) return sourceBytes;
135
136 int frameCount = sourceBytes.length / sourceFormat.getFrameSize();
137
138 // Create AudioInputStream to stream source bytes
139 AudioInputStream aisOriginal = new AudioInputStream(
140 new ByteArrayInputStream(sourceBytes),
141 sourceFormat,
142 frameCount);
143
144 // Chain stream with a series of conversion streams. Note that you cannot just
145 // use one conversion stream to process the audio all at once.
146 AudioInputStream aisConverter = aisOriginal;
147
148 // First convert to the correct PCM format so can carry out appropriate conversions
149 //if (!sourceFormat.getEncoding().equals(targetFormat.getEncoding())) {
150// AudioFormat.Encoding conversionEncoding = null;
151//
152// if (targetFormat.getEncoding().toString().startsWith("PCM")) // convert signed/unsigned if need to
153// conversionEncoding = targetFormat.getEncoding();
154//
155// else if (!targetFormat.getEncoding().toString().startsWith("PCM"))
156// conversionEncoding =
157// (sourceFormat.getSampleSizeInBits() == 8) ?
158// AudioFormat.Encoding.PCM_UNSIGNED :
159// AudioFormat.Encoding.PCM_SIGNED;
160//
161// if (conversionEncoding != null) {
162// aisConverter = AudioSystem.getAudioInputStream(conversionEncoding, aisConverter);
163// }
164//
165// }
166
167 if (!sourceFormat.getEncoding().toString().startsWith("PCM")) {
168
169 AudioFormat.Encoding conversionEncoding =
170 (sourceFormat.getSampleSizeInBits() == 8) ?
171 AudioFormat.Encoding.PCM_UNSIGNED :
172 AudioFormat.Encoding.PCM_SIGNED;
173
174 aisConverter = AudioSystem.getAudioInputStream(conversionEncoding, aisConverter);
175
176 }
177
178 // Next convert number of channels
179 if (sourceFormat.getChannels() != targetFormat.getChannels()) {
180
181 AudioFormat chainedFormat = aisConverter.getFormat();
182
183 aisConverter = AudioSystem.getAudioInputStream(
184 new AudioFormat(
185 chainedFormat.getEncoding(),
186 chainedFormat.getSampleRate(),
187 chainedFormat.getSampleSizeInBits(),
188 targetFormat.getChannels(),
189 targetFormat.getChannels() * ((chainedFormat.getSampleSizeInBits() + 7) / 8),
190 chainedFormat.getFrameRate(),
191 chainedFormat.isBigEndian()),
192 aisConverter);
193 }
194
195
196 // Next convert sample size AND endianess.
197 if ((aisConverter.getFormat().getSampleSizeInBits() != targetFormat.getSampleSizeInBits())
198 || (aisConverter.getFormat().isBigEndian() != targetFormat.isBigEndian())) {
199
200 AudioFormat chainedFormat = aisConverter.getFormat();
201
202 aisConverter = AudioSystem.getAudioInputStream(
203 new AudioFormat(
204 chainedFormat.getEncoding(),
205 chainedFormat.getSampleRate(),
206 targetFormat.getSampleSizeInBits(),
207 chainedFormat.getChannels(),
208 chainedFormat.getChannels() * ((targetFormat.getSampleSizeInBits() + 7) / 8),
209 chainedFormat.getFrameRate(),
210 targetFormat.isBigEndian()),
211 aisConverter);
212
213 }
214
215 // convert sample rate - this relies on a plugin. I.E. tritonous' PCM2PCM SPI
216 final float DELTA = 1E-9F;
217 if (Math.abs(aisConverter.getFormat().getSampleRate() - targetFormat.getSampleRate()) >= DELTA) {
218
219 AudioFormat chainedFormat = aisConverter.getFormat();
220
221 aisConverter = AudioSystem.getAudioInputStream(
222 new AudioFormat(
223 chainedFormat.getEncoding(),
224 targetFormat.getSampleRate(),
225 chainedFormat.getSampleSizeInBits(),
226 chainedFormat.getChannels(),
227 chainedFormat.getFrameSize(),
228 targetFormat.getFrameRate(),
229 chainedFormat.isBigEndian()),
230 aisConverter);
231
232 }
233
234 // convert to back to non-PCM encoding, or to different PCM encoding, if need to.
235 if (!targetFormat.getEncoding().equals(aisConverter.getFormat().getEncoding())) {
236 aisConverter = AudioSystem.getAudioInputStream(targetFormat.getEncoding(), aisConverter);
237 }
238
239 LinkedList<byte[]> convertedBytes = new LinkedList<byte[]>();
240 int BUFFERSIZE = 50000 * sourceFormat.getFrameSize();
241
242 // Read the bytes and convert
243 int bytesRead = 0;
244
245 while(bytesRead != -1) {
246
247 int bytePosition = 0;
248 bytesRead = 0;
249 byte[] buffer = new byte[BUFFERSIZE];
250
251 while (bytePosition < BUFFERSIZE) {
252
253 bytesRead = aisConverter.read(buffer, bytePosition, buffer.length - bytePosition);
254
255 if (bytesRead == -1) break;
256
257 bytePosition += bytesRead;
258
259 }
260
261 if (bytePosition > 0 && bytesRead > 0) {
262
263 convertedBytes.add(buffer);
264
265 } else if (bytePosition > 0) { // last chunk
266 byte[] finalChunk = new byte[bytePosition];
267 System.arraycopy(buffer, 0, finalChunk, 0, bytePosition);
268 convertedBytes.add(finalChunk);
269 }
270
271 }
272
273 int size = 0;
274
275 for (byte[] ba : convertedBytes) size += ba.length;
276
277 byte[] converted = new byte[size];
278 int pos = 0;
279
280 // Assemble
281 for (byte[] ba : convertedBytes) {
282 System.arraycopy(
283 ba,
284 0,
285 converted,
286 pos,
287 ba.length);
288
289 pos += ba.length;
290 }
291
292
293 return converted;
294 }
295
296 /**
297 * Writes PCM encoded audio bytes as a wave file.
298 *
299 * @param path
300 * The path of the wave file to write to. This will be overriden.
301 *
302 * @param bytes
303 * The PCM encoded audio bytes to write
304 *
305 * @param format
306 * The format of the audio bytes. Must be in PCM.
307 *
308 * @throws IOException
309 * If failed to create file for saving, or an error occured while writing audio bytes.
310 *
311 * @throws UnsupportedAudioFileException
312 * If audio format is unsupported or the encoding is not PCM.
313 *
314 * @throws NullPointerException
315 * If path, bytes or format is null.
316 */
317 public static synchronized void savePCMAudioToWaveFile(String path, byte[] bytes, AudioFormat format)
318 throws IOException, UnsupportedAudioFileException {
319
320 if (bytes == null) throw new NullPointerException("bytes");
321 if (format == null) throw new NullPointerException("format");
322
323 if (!format.getEncoding().toString().startsWith("PCM"))
324 throw new UnsupportedAudioFileException();
325
326 saveAudioToFile(path, bytes, format, Type.WAVE, bytes.length / format.getFrameSize());
327 }
328
329 /**
330 * Writes audio bytes to file.
331 *
332 * @param path
333 * The path of the audio file to write to. This will be overriden.
334 *
335 * @param bytes
336 * The audio bytes to write
337 *
338 * @param format
339 * The format of the audio bytes.
340 *
341 * @param fileType
342 * The type of file to output to.
343 *
344 * @param length
345 * The amount of audio bytes to write
346 *
347 * @throws IOException
348 * If failed to create file for saving, or an error occured while writing audio bytes.
349 *
350 * @throws UnsupportedAudioFileException
351 * If audio format is unsupported
352 *
353 * @throws NullPointerException
354 * If path, bytes, format or fileType is null.
355 */
356 public static synchronized void saveAudioToFile(String path, byte[] bytes,
357 AudioFormat format, Type fileType, long length)
358 throws IOException, UnsupportedAudioFileException {
359
360 if (path == null) throw new NullPointerException("path");
361 if (bytes == null) throw new NullPointerException("bytes");
362 if (format == null) throw new NullPointerException("format");
363 if (fileType == null) throw new NullPointerException("fileType");
364
365 // Create file out stream
366 File f = new File(path);
367 FileOutputStream out = new FileOutputStream(f);
368
369 // Create audio input stream
370 AudioInputStream aistream = new AudioInputStream(
371 new ByteArrayInputStream(bytes),
372 format,
373 length);
374
375 // Write audio file
376 AudioSystem.write(aistream, fileType, out);
377
378 }
379
380 /**
381 * Loads an audio file into memory. The loaded bytes are converted if they need to be
382 * in order to be used in apollos.
383 *
384 * This operation can be cancelled via cancelLoad.
385 *
386 * @param file
387 * The file to load. must not be null and the file must exist.
388 *
389 * @param statusObserver
390 * If given (can be null), then the statusObserver will be notified with
391 * LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
392 * The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
393 * The subject source will be this instance.
394 *
395 * @return
396 * The loaded audio - in a format that can be used in apollos.
397 * Null if cancelled.
398 *
399 * @throws IOException
400 * If an exception occured while loade audio bytes.
401 *
402 * @throws UnsupportedAudioFileException
403 * ` if the File does not point to valid audio file data recognized by the system.
404 *
405 */
406 public static synchronized LoadedAudioData loadAudioFile(File file, Observer statusObserver)
407 throws IOException, UnsupportedAudioFileException {
408
409 return (innerclassCreator.new AudioFileLoader()).loadAudioFile(file, statusObserver);
410
411 }
412
413
414
415 /**
416 * Loads audio files into memory - converts to format ready to be used in apollos.
417 *
418 * This is also a subject - it optionally reports the load status while loading.
419 * The loading operatoin is also cancellable.
420 *
421 * @author Brook Novak
422 *
423 */
424 public class AudioFileLoader extends AbstractSubject {
425
426 /**
427 * Util constructor
428 */
429 private AudioFileLoader() {}
430
431 private boolean cancelLoad = false;
432
433 /**
434 * Cancels current load operation if any.
435 */
436 public void cancelLoad() {
437 cancelLoad = true;
438 }
439
440 /**
441 * Loads an audio file into memory. The loaded bytes are converted if they need to be
442 * in order to be used in apollos.
443 *
444 * This operation can be cancelled via cancelLoad.
445 *
446 * @param file
447 * The file to load. must not be null and the file must exist.
448 *
449 * @param statusObserver
450 * If given (an be null), then the statusObserver will be notified with
451 * LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
452 * The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
453 * The subject source will be this instance.
454 *
455 * @return
456 * The loaded audio - in a format that can be used in apollos.
457 * Null if cancelled.
458 *
459 * @throws IOException
460 * If an exception occured while loade audio bytes.
461 *
462 * @throws UnsupportedAudioFileException
463 * ` if the File does not point to valid audio file data recognized by the system.
464 *
465 */
466 public LoadedAudioData loadAudioFile(File file, Observer statusObserver) throws IOException, UnsupportedAudioFileException {
467 assert(file != null && file.exists());
468
469 cancelLoad = false;
470
471 AudioInputStream fileStream = null; // stream directly from file
472 AudioInputStream decodeStream = null; // stream decoding fileStream
473
474 try {
475
476 // Get the audio form information
477 AudioFileFormat fformat = AudioSystem.getAudioFileFormat(file);
478
479 // Get the file stream
480 fileStream = AudioSystem.getAudioInputStream(file);
481
482 AudioInputStream sampleStream;
483 AudioFormat sampleFormat;
484 boolean isConverting;
485
486 // Check if file needs to convert
487 if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(fformat.getFormat())) {
488
489 sampleFormat = SampledAudioManager.getInstance().getDefaultPlaybackFormat();
490
491 if (!AudioSystem.isConversionSupported(sampleFormat, fformat.getFormat())) {
492 throw new UnsupportedAudioFileException("Audio not supported");
493 }
494
495 decodeStream = AudioSystem.getAudioInputStream(sampleFormat, fileStream);
496 sampleStream = decodeStream;
497 isConverting = true;
498
499
500 } else { // otherwise read bytes directly from file stream
501 sampleStream = fileStream;
502 sampleFormat = fformat.getFormat();
503 isConverting = false;
504 }
505
506 assert (SampledAudioManager.getInstance().isFormatSupportedForPlayback(sampleFormat));
507
508 // Initialize the ByteBuffer - and size if possible (not possible for variable frame size encoding)
509 ByteArrayOutputStream loadedBytes = (fformat.getFrameLength() != AudioSystem.NOT_SPECIFIED) ?
510 new ByteArrayOutputStream(fformat.getFrameLength() * sampleFormat.getFrameSize()) :
511 new ByteArrayOutputStream();
512
513 byte[] buffer = new byte[sampleFormat.getFrameSize() * (int)sampleFormat.getFrameRate()];
514
515 int bytesRead = 0;
516 int totalBytesRead = 0;
517 float percent = 0.0f;
518
519 while (!cancelLoad) { // keep loading until cancelled
520
521 // Report current percent to given observer (if any)
522 if (statusObserver != null) {
523 statusObserver.modelChanged(this, new SubjectChangedEvent(
524 ApolloSubjectChangedEvent.LOAD_STATUS_REPORT, new Float(percent)));
525 }
526
527 // Read bytes from the stream into memory
528 bytesRead = sampleStream.read(buffer, 0, buffer.length);
529
530 if (bytesRead == -1) break; // complete
531
532 loadedBytes.write(buffer, 0, bytesRead);
533
534 // Update percent complete
535 totalBytesRead += bytesRead;
536 percent = (float)totalBytesRead / (float)fformat.getByteLength();
537 }
538
539 loadedBytes.close();
540
541 // if incomplete - then cancelled.
542 if (bytesRead != -1) {
543 assert(cancelLoad);
544 return null;
545 }
546
547 // otherwise return the loaded audio
548 return new LoadedAudioData(loadedBytes.toByteArray(), sampleFormat, isConverting);
549
550 } finally { // Always ensure that decoder and file streams are closed
551
552 if (decodeStream != null) {
553 decodeStream.close();
554 }
555
556 if (fileStream != null) {
557 fileStream.close();
558 }
559
560 }
561 }
562
563 }
564
565 /**
566 * For manual testing purposes
567 *
568 * @param args
569 * 1 Arg - the file to convert
570 */
571 public static void main(String[] args) {
572
573 String args_zero = null;
574
575 if (args.length==0) {
576 // Hardwire to whatever is a meaningful example for you!
577 args_zero = "C:\\Temp\\GoodTime_Preview.mp3";
578 }
579 else if (args.length==1) {
580 args_zero = args[0];
581
582 }
583
584 System.out.println("Testing with " + args_zero);
585
586 try {
587 LoadedAudioData loaded = loadAudioFile(new File(args_zero), null);
588
589 if (loaded != null && loaded.getAudioBytes() != null) {
590
591 System.out.println("Loaded file into internal format: " + loaded.getAudioFormat());
592
593 if (loaded.wasConverted()) {
594 savePCMAudioToWaveFile(
595 args_zero + "_pre-converted.wav",
596 loaded.getAudioBytes(),
597 loaded.getAudioFormat());
598 }
599
600
601 AudioFormat lowQualFormat = new AudioFormat(
602 22050,
603 8, // 8-bit
604 1, // mono
605 loaded.getAudioFormat().getEncoding().toString().endsWith("SIGNED"),
606 true); // Big endian
607
608 byte[] stdData = AudioIO.convertAudioBytes(
609 loaded.getAudioBytes(),
610 loaded.getAudioFormat(),
611 lowQualFormat);
612
613 if (stdData != null) {
614
615 System.out.println("Saving file into low-quality format: " + lowQualFormat);
616
617 savePCMAudioToWaveFile(
618 args_zero + "_converted.wav",
619 stdData,
620 lowQualFormat);
621
622 System.out.println("Conversion succeeded");
623 return;
624
625
626 }
627
628
629 }
630
631 } catch (IOException e) {
632 e.printStackTrace();
633 } catch (UnsupportedAudioFileException e) {
634 e.printStackTrace();
635 }
636
637 System.err.println("Conversion failed");
638 return;
639
640
641 //System.err.println("Must supply 1 argument: the file path to convert");
642
643
644
645 }
646
647}
Note: See TracBrowser for help on using the repository browser.