source: trunk/src_apollo/org/apollo/io/AudioIO.java@ 315

Last change on this file since 315 was 315, checked in by bjn8, 16 years ago

Apollo spin-off added

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