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

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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 = (sourceFormat.getSampleSizeInBits() == 8) ? AudioFormat.Encoding.PCM_UNSIGNED : AudioFormat.Encoding.PCM_SIGNED;
170
171 aisConverter = AudioSystem.getAudioInputStream(conversionEncoding, aisConverter);
172
173 }
174
175 // Next convert number of channels
176 if (sourceFormat.getChannels() != targetFormat.getChannels()) {
177
178 AudioFormat chainedFormat = aisConverter.getFormat();
179
180 aisConverter = AudioSystem.getAudioInputStream(
181 new AudioFormat(
182 chainedFormat.getEncoding(),
183 chainedFormat.getSampleRate(),
184 chainedFormat.getSampleSizeInBits(),
185 targetFormat.getChannels(),
186 targetFormat.getChannels() * ((chainedFormat.getSampleSizeInBits() + 7) / 8),
187 chainedFormat.getFrameRate(),
188 chainedFormat.isBigEndian()),
189 aisConverter);
190 }
191
192
193 // Next convert sample size AND endianess.
194 if ((aisConverter.getFormat().getSampleSizeInBits() != targetFormat.getSampleSizeInBits())
195 || (aisConverter.getFormat().isBigEndian() != targetFormat.isBigEndian())) {
196
197 AudioFormat chainedFormat = aisConverter.getFormat();
198
199 aisConverter = AudioSystem.getAudioInputStream(
200 new AudioFormat(
201 chainedFormat.getEncoding(),
202 chainedFormat.getSampleRate(),
203 targetFormat.getSampleSizeInBits(),
204 chainedFormat.getChannels(),
205 chainedFormat.getChannels() * ((targetFormat.getSampleSizeInBits() + 7) / 8),
206 chainedFormat.getFrameRate(),
207 targetFormat.isBigEndian()),
208 aisConverter);
209
210 }
211
212 // convert sample rate - this relies on a plugin. I.E. tritonous' PCM2PCM SPI
213 final float DELTA = 1E-9F;
214 if (Math.abs(aisConverter.getFormat().getSampleRate() - targetFormat.getSampleRate()) >= DELTA) {
215
216 AudioFormat chainedFormat = aisConverter.getFormat();
217
218 aisConverter = AudioSystem.getAudioInputStream(
219 new AudioFormat(
220 chainedFormat.getEncoding(),
221 targetFormat.getSampleRate(),
222 chainedFormat.getSampleSizeInBits(),
223 chainedFormat.getChannels(),
224 chainedFormat.getFrameSize(),
225 targetFormat.getFrameRate(),
226 chainedFormat.isBigEndian()),
227 aisConverter);
228
229 }
230
231 // convert to back to non-PCM encoding, or to different PCM encoding, if need to.
232 if (!targetFormat.getEncoding().equals(aisConverter.getFormat().getEncoding())) {
233 aisConverter = AudioSystem.getAudioInputStream(targetFormat.getEncoding(), aisConverter);
234 }
235
236 LinkedList<byte[]> convertedBytes = new LinkedList<byte[]>();
237 int BUFFERSIZE = 50000 * sourceFormat.getFrameSize();
238
239 // Read the bytes and convert
240 int bytesRead = 0;
241
242 while(bytesRead != -1) {
243
244 int bytePosition = 0;
245 bytesRead = 0;
246 byte[] buffer = new byte[BUFFERSIZE];
247
248 while (bytePosition < BUFFERSIZE) {
249
250 bytesRead = aisConverter.read(buffer, bytePosition, buffer.length - bytePosition);
251
252 if (bytesRead == -1) break;
253
254 bytePosition += bytesRead;
255
256 }
257
258 if (bytePosition > 0 && bytesRead > 0) {
259
260 convertedBytes.add(buffer);
261
262 } else if (bytePosition > 0) { // last chunk
263 byte[] finalChunk = new byte[bytePosition];
264 System.arraycopy(buffer, 0, finalChunk, 0, bytePosition);
265 convertedBytes.add(finalChunk);
266 }
267
268 }
269
270 int size = 0;
271
272 for (byte[] ba : convertedBytes) size += ba.length;
273
274 byte[] converted = new byte[size];
275 int pos = 0;
276
277 // Assemble
278 for (byte[] ba : convertedBytes) {
279 System.arraycopy(
280 ba,
281 0,
282 converted,
283 pos,
284 ba.length);
285
286 pos += ba.length;
287 }
288
289
290 return converted;
291 }
292
293 /**
294 * Writes PCM encoded audio bytes as a wave file.
295 *
296 * @param path
297 * The path of the wave file to write to. This will be overriden.
298 *
299 * @param bytes
300 * The PCM encoded audio bytes to write
301 *
302 * @param format
303 * The format of the audio bytes. Must be in PCM.
304 *
305 * @throws IOException
306 * If failed to create file for saving, or an error occured while writing audio bytes.
307 *
308 * @throws UnsupportedAudioFileException
309 * If audio format is unsupported or the encoding is not PCM.
310 *
311 * @throws NullPointerException
312 * If path, bytes or format is null.
313 */
314 public static synchronized void savePCMAudioToWaveFile(String path, byte[] bytes, AudioFormat format)
315 throws IOException, UnsupportedAudioFileException {
316
317 if (bytes == null) throw new NullPointerException("bytes");
318 if (format == null) throw new NullPointerException("format");
319
320 if (!format.getEncoding().toString().startsWith("PCM"))
321 throw new UnsupportedAudioFileException();
322
323 saveAudioToFile(path, bytes, format, Type.WAVE, bytes.length / format.getFrameSize());
324 }
325
326 /**
327 * Writes audio bytes to file.
328 *
329 * @param path
330 * The path of the audio file to write to. This will be overriden.
331 *
332 * @param bytes
333 * The audio bytes to write
334 *
335 * @param format
336 * The format of the audio bytes.
337 *
338 * @param fileType
339 * The type of file to output to.
340 *
341 * @param length
342 * The amount of audio bytes to write
343 *
344 * @throws IOException
345 * If failed to create file for saving, or an error occured while writing audio bytes.
346 *
347 * @throws UnsupportedAudioFileException
348 * If audio format is unsupported
349 *
350 * @throws NullPointerException
351 * If path, bytes, format or fileType is null.
352 */
353 public static synchronized void saveAudioToFile(String path, byte[] bytes,
354 AudioFormat format, Type fileType, long length)
355 throws IOException, UnsupportedAudioFileException {
356
357 if (path == null) throw new NullPointerException("path");
358 if (bytes == null) throw new NullPointerException("bytes");
359 if (format == null) throw new NullPointerException("format");
360 if (fileType == null) throw new NullPointerException("fileType");
361
362 // Create file out stream
363 File f = new File(path);
364 FileOutputStream out = new FileOutputStream(f);
365
366 // Create audio input stream
367 AudioInputStream aistream = new AudioInputStream(
368 new ByteArrayInputStream(bytes),
369 format,
370 length);
371
372 // Write audio file
373 AudioSystem.write(aistream, fileType, out);
374
375 }
376
377 /**
378 * Loads an audio file into memory. The loaded bytes are converted if they need to be
379 * in order to be used in apollos.
380 *
381 * This operation can be cancelled via cancelLoad.
382 *
383 * @param file
384 * The file to load. must not be null and the file must exist.
385 *
386 * @param statusObserver
387 * If given (can be null), then the statusObserver will be notified with
388 * LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
389 * The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
390 * The subject source will be this instance.
391 *
392 * @return
393 * The loaded audio - in a format that can be used in apollos.
394 * Null if cancelled.
395 *
396 * @throws IOException
397 * If an exception occured while loade audio bytes.
398 *
399 * @throws UnsupportedAudioFileException
400 * ` if the File does not point to valid audio file data recognized by the system.
401 *
402 */
403 public static synchronized LoadedAudioData loadAudioFile(File file, Observer statusObserver)
404 throws IOException, UnsupportedAudioFileException {
405
406 return (innerclassCreator.new AudioFileLoader()).loadAudioFile(file, statusObserver);
407
408 }
409
410
411
412 /**
413 * Loads audio files into memory - converts to format ready to be used in apollos.
414 *
415 * This is also a subject - it optionally reports the load status while loading.
416 * The loading operatoin is also cancellable.
417 *
418 * @author Brook Novak
419 *
420 */
421 public class AudioFileLoader extends AbstractSubject {
422
423 /**
424 * Util constructor
425 */
426 private AudioFileLoader() {}
427
428 private boolean cancelLoad = false;
429
430 /**
431 * Cancels current load operation if any.
432 */
433 public void cancelLoad() {
434 cancelLoad = true;
435 }
436
437 /**
438 * Loads an audio file into memory. The loaded bytes are converted if they need to be
439 * in order to be used in apollos.
440 *
441 * This operation can be cancelled via cancelLoad.
442 *
443 * @param file
444 * The file to load. must not be null and the file must exist.
445 *
446 * @param statusObserver
447 * If given (an be null), then the statusObserver will be notified with
448 * LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
449 * The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
450 * The subject source will be this instance.
451 *
452 * @return
453 * The loaded audio - in a format that can be used in apollos.
454 * Null if cancelled.
455 *
456 * @throws IOException
457 * If an exception occured while loade audio bytes.
458 *
459 * @throws UnsupportedAudioFileException
460 * ` if the File does not point to valid audio file data recognized by the system.
461 *
462 */
463 public LoadedAudioData loadAudioFile(File file, Observer statusObserver) throws IOException, UnsupportedAudioFileException {
464 assert(file != null && file.exists());
465
466 cancelLoad = false;
467
468 AudioInputStream fileStream = null; // stream directly from file
469 AudioInputStream decodeStream = null; // stream decoding fileStream
470
471 try {
472
473 // Get the audio form information
474 AudioFileFormat fformat = AudioSystem.getAudioFileFormat(file);
475
476 // Get the file stream
477 fileStream = AudioSystem.getAudioInputStream(file);
478
479 AudioInputStream sampleStream;
480 AudioFormat sampleFormat;
481 boolean isConverting;
482
483 // Check if file needs to convert
484 if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(fformat.getFormat())) {
485
486 sampleFormat = SampledAudioManager.getInstance().getDefaultPlaybackFormat();
487
488 if (!AudioSystem.isConversionSupported(sampleFormat, fformat.getFormat())) {
489 throw new UnsupportedAudioFileException("Audio not supported");
490 }
491
492 decodeStream = AudioSystem.getAudioInputStream(sampleFormat, fileStream);
493 sampleStream = decodeStream;
494 isConverting = true;
495
496
497 } else { // otherwise read bytes directly from file stream
498 sampleStream = fileStream;
499 sampleFormat = fformat.getFormat();
500 isConverting = false;
501 }
502
503 assert (SampledAudioManager.getInstance().isFormatSupportedForPlayback(sampleFormat));
504
505 // Initialize the ByteBuffer - and size if possible (not possible for variable frame size encoding)
506 ByteArrayOutputStream loadedBytes = (fformat.getFrameLength() != AudioSystem.NOT_SPECIFIED) ?
507 new ByteArrayOutputStream(fformat.getFrameLength() * sampleFormat.getFrameSize()) :
508 new ByteArrayOutputStream();
509
510 byte[] buffer = new byte[sampleFormat.getFrameSize() * (int)sampleFormat.getFrameRate()];
511
512 int bytesRead = 0;
513 int totalBytesRead = 0;
514 float percent = 0.0f;
515
516 while (!cancelLoad) { // keep loading until cancelled
517
518 // Report current percent to given observer (if any)
519 if (statusObserver != null) {
520 statusObserver.modelChanged(this, new SubjectChangedEvent(
521 ApolloSubjectChangedEvent.LOAD_STATUS_REPORT, new Float(percent)));
522 }
523
524 // Read bytes from the stream into memory
525 bytesRead = sampleStream.read(buffer, 0, buffer.length);
526
527 if (bytesRead == -1) break; // complete
528
529 loadedBytes.write(buffer, 0, bytesRead);
530
531 // Update percent complete
532 totalBytesRead += bytesRead;
533 percent = (float)totalBytesRead / (float)fformat.getByteLength();
534 }
535
536 loadedBytes.close();
537
538 // if incomplete - then cancelled.
539 if (bytesRead != -1) {
540 assert(cancelLoad);
541 return null;
542 }
543
544 // otherwise return the loaded audio
545 return new LoadedAudioData(loadedBytes.toByteArray(), sampleFormat, isConverting);
546
547 } finally { // Always ensure that decoder and file streams are closed
548
549 if (decodeStream != null) {
550 decodeStream.close();
551 }
552
553 if (fileStream != null) {
554 fileStream.close();
555 }
556
557 }
558 }
559
560 }
561
562 /**
563 * For manual testing purposes
564 *
565 * @param args
566 * 1 Arg - the file to convert
567 */
568 public static void main(String[] args) {
569
570 String args_zero = null;
571
572 if (args.length==0) {
573 // Hardwire to whatever is a meaningful example for you!
574 args_zero = "C:\\Temp\\GoodTime_Preview.mp3";
575 }
576 else if (args.length==1) {
577 args_zero = args[0];
578
579 }
580
581 System.out.println("Testing with " + args_zero);
582
583 try {
584 LoadedAudioData loaded = loadAudioFile(new File(args_zero), null);
585
586 if (loaded != null && loaded.getAudioBytes() != null) {
587
588 System.out.println("Loaded file into internal format: " + loaded.getAudioFormat());
589
590 if (loaded.wasConverted()) {
591 savePCMAudioToWaveFile(
592 args_zero + "_pre-converted.wav",
593 loaded.getAudioBytes(),
594 loaded.getAudioFormat());
595 }
596
597
598 AudioFormat lowQualFormat = new AudioFormat(
599 22050,
600 8, // 8-bit
601 1, // mono
602 loaded.getAudioFormat().getEncoding().toString().endsWith("SIGNED"),
603 true); // Big endian
604
605 byte[] stdData = AudioIO.convertAudioBytes(
606 loaded.getAudioBytes(),
607 loaded.getAudioFormat(),
608 lowQualFormat);
609
610 if (stdData != null) {
611
612 System.out.println("Saving file into low-quality format: " + lowQualFormat);
613
614 savePCMAudioToWaveFile(
615 args_zero + "_converted.wav",
616 stdData,
617 lowQualFormat);
618
619 System.out.println("Conversion succeeded");
620 return;
621
622
623 }
624
625
626 }
627
628 } catch (IOException e) {
629 e.printStackTrace();
630 } catch (UnsupportedAudioFileException e) {
631 e.printStackTrace();
632 }
633
634 System.err.println("Conversion failed");
635 return;
636
637
638 //System.err.println("Must supply 1 argument: the file path to convert");
639
640
641
642 }
643
644}
Note: See TracBrowser for help on using the repository browser.