package com.paulscode.sound.codecs; import java.io.BufferedInputStream; import java.io.IOException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.UnsupportedAudioFileException; import com.paulscode.sound.ICodec; import com.paulscode.sound.SoundBuffer; import com.paulscode.sound.SoundSystemConfig; import com.paulscode.sound.SoundSystemLogger; /** * The CodecWav class provides an ICodec interface for reading from .wav files. <br> * <br> * <b><i> SoundSystem CodecWav License:</b></i><br> * <b><br> * You are free to use this class for any purpose, commercial or otherwise. You * may modify this class or source code, and distribute it any way you like, * provided the following conditions are met: <br> * 1) You may not falsely claim to be the author of this class or any unmodified * portion of it. <br> * 2) You may not copyright this class or a modified version of it and then sue * me for copyright infringement. <br> * 3) If you modify the source code, you must clearly document the changes made * before redistributing the modified source code, so other users know it is not * the original code. <br> * 4) You are not required to give me credit for this class in any derived work, * but if you do, you must also mention my website: http://www.paulscode.com <br> * 5) I the author will not be responsible for any damages (physical, financial, * or otherwise) caused by the use if this class or any portion of it. <br> * 6) I the author do not guarantee, warrant, or make any representations, * either expressed or implied, regarding the use of this class or any portion * of it. <br> * <br> * Author: Paul Lamb <br> * http://www.paulscode.com </b> */ public class CodecWav implements ICodec { /** * Used to return a current value from one of the synchronized * boolean-interface methods. */ private static final boolean GET = false; /** * Used to set the value in one of the synchronized boolean-interface * methods. */ private static final boolean SET = true; /** * Used when a parameter for one of the synchronized boolean-interface * methods is not aplicable. */ private static final boolean XXX = false; /** * True if there is no more data to read in. */ private boolean endOfStream = false; /** * True if the stream has finished initializing. */ private boolean initialized = false; /** * Input stream to use for reading in pcm data. */ private AudioInputStream myAudioInputStream = null; /** * This method is ignored by CodecWav, because it produces "nice" data. * * @param b * True if the calling audio library requires byte-reversal from * certain codecs */ public void reverseByteOrder(boolean b) { } /** * Processes status messages, warnings, and error messages. */ private SoundSystemLogger logger; /** * Constructor: Grabs a handle to the logger. */ public CodecWav() { logger = SoundSystemConfig.getLogger(); } /** * Prepares an audio stream to read from. If another stream is already * opened, it will be closed and a new audio stream opened in its place. * * @param url * URL to an audio file to stream from. * @return False if an error occurred or if end of stream was reached. */ public boolean initialize(URL url) { initialized(SET, false); cleanup(); if (url == null) { errorMessage("url null in method 'initialize'"); cleanup(); return false; } try { myAudioInputStream = AudioSystem .getAudioInputStream(new BufferedInputStream(url .openStream())); } catch (UnsupportedAudioFileException uafe) { errorMessage("Unsupported audio format in method 'initialize'"); printStackTrace(uafe); return false; } catch (IOException ioe) { errorMessage("Error setting up audio input stream in method " + "'initialize'"); printStackTrace(ioe); return false; } endOfStream(SET, false); initialized(SET, true); return true; } /** * Returns false if the stream is busy initializing. * * @return True if steam is initialized. */ public boolean initialized() { return initialized(GET, XXX); } /** * Reads in one stream buffer worth of audio data. See * {@link paulscode.sound.SoundSystemConfig SoundSystemConfig} for more * information about accessing and changing default settings. * * @return The audio data wrapped into a SoundBuffer context. */ public SoundBuffer read() { if (myAudioInputStream == null) return null; // Get the format for the audio data: AudioFormat audioFormat = myAudioInputStream.getFormat(); // Check to make sure there is an audio format: if (audioFormat == null) { errorMessage("Audio Format null in method 'read'"); return null; } // Varriables used when reading from the audio input stream: int bytesRead = 0, cnt = 0; // Allocate memory for the audio data: byte[] streamBuffer = new byte[SoundSystemConfig .getStreamingBufferSize()]; try { // Read until buffer is full or end of stream is reached: while ((!endOfStream(GET, XXX)) && (bytesRead < streamBuffer.length)) { if ((cnt = myAudioInputStream.read(streamBuffer, bytesRead, streamBuffer.length - bytesRead)) <= 0) { endOfStream(SET, true); break; } // keep track of how many bytes were read: bytesRead += cnt; } } catch (IOException ioe) { // TODO: See if setting endOfStream is needed here endOfStream(SET, true); return null; } // Return null if no data was read: if (bytesRead <= 0) return null; // If we didn't fill the stream buffer entirely, trim it down to size: if (bytesRead < streamBuffer.length) streamBuffer = trimArray(streamBuffer, bytesRead); // Insert the converted data into a ByteBuffer: byte[] data = convertAudioBytes(streamBuffer, audioFormat.getSampleSizeInBits() == 16); // Wrap the data into a SoundBuffer: SoundBuffer buffer = new SoundBuffer(data, audioFormat); // Return the result: return buffer; } /** * Reads in all the audio data from the stream (up to the default * "maximum file size". See {@link paulscode.sound.SoundSystemConfig * SoundSystemConfig} for more information about accessing and changing * default settings. * * @return the audio data wrapped into a SoundBuffer context. */ public SoundBuffer readAll() { // Check to make sure there is an audio format: if (myAudioInputStream == null) { errorMessage("Audio input stream null in method 'readAll'"); return null; } AudioFormat myAudioFormat = myAudioInputStream.getFormat(); // Check to make sure there is an audio format: if (myAudioFormat == null) { errorMessage("Audio Format null in method 'readAll'"); return null; } // Array to contain the audio data: byte[] fullBuffer = null; // Determine how much data will be read in: int fileSize = myAudioFormat.getChannels() * (int) myAudioInputStream.getFrameLength() * myAudioFormat.getSampleSizeInBits() / 8; if (fileSize > 0) { // Allocate memory for the audio data: fullBuffer = new byte[myAudioFormat.getChannels() * (int) myAudioInputStream.getFrameLength() * myAudioFormat.getSampleSizeInBits() / 8]; int read = 0, total = 0; try { // Read until the end of the stream is reached: while ((read = myAudioInputStream.read(fullBuffer, total, fullBuffer.length - total)) != -1 && total < fullBuffer.length) { total += read; } } catch (IOException e) { errorMessage("Exception thrown while reading from the " + "AudioInputStream (location #1)."); printStackTrace(e); return null; } } else { // Total file size unknown. // Varriables used when reading from the audio input stream: int totalBytes = 0, bytesRead = 0, cnt = 0; byte[] smallBuffer = null; // Allocate memory for a chunk of data: smallBuffer = new byte[SoundSystemConfig.getFileChunkSize()]; // Read until end of file or maximum file size is reached: while ((!endOfStream(GET, XXX)) && (totalBytes < SoundSystemConfig.getMaxFileSize())) { bytesRead = 0; cnt = 0; try { // Read until small buffer is filled or end of file reached: while (bytesRead < smallBuffer.length) { if ((cnt = myAudioInputStream.read(smallBuffer, bytesRead, smallBuffer.length - bytesRead)) <= 0) { endOfStream(SET, true); break; } bytesRead += cnt; } } catch (IOException e) { errorMessage("Exception thrown while reading from the " + "AudioInputStream (location #2)."); printStackTrace(e); return null; } // Keep track of the total number of bytes read: totalBytes += bytesRead; // Append the small buffer to the full buffer: fullBuffer = appendByteArrays(fullBuffer, smallBuffer, bytesRead); } } // Insert the converted data into a ByteBuffer byte[] data = convertAudioBytes(fullBuffer, myAudioFormat.getSampleSizeInBits() == 16); // Wrap the data into an SoundBuffer: SoundBuffer soundBuffer = new SoundBuffer(data, myAudioFormat); // Close the audio input stream try { myAudioInputStream.close(); } catch (IOException e) { } // Return the result: return soundBuffer; } /** * Returns false if there is still more data available to be read in. * * @return True if end of stream was reached. */ public boolean endOfStream() { return endOfStream(GET, XXX); } /** * Closes the audio stream and remove references to all instantiated * objects. */ public void cleanup() { if (myAudioInputStream != null) try { myAudioInputStream.close(); } catch (Exception e) { } myAudioInputStream = null; } /** * Returns the audio format of the data being returned by the read() and * readAll() methods. * * @return Information wrapped into an AudioFormat context. */ public AudioFormat getAudioFormat() { if (myAudioInputStream == null) return null; return myAudioInputStream.getFormat(); } /** * Internal method for synchronizing access to the boolean 'initialized'. * * @param action * GET or SET. * @param value * New value if action == SET, or XXX if action == GET. * @return True if steam is initialized. */ private synchronized boolean initialized(boolean action, boolean value) { if (action == SET) initialized = value; return initialized; } /** * Internal method for synchronizing access to the boolean 'endOfStream'. * * @param action * GET or SET. * @param value * New value if action == SET, or XXX if action == GET. * @return True if end of stream was reached. */ private synchronized boolean endOfStream(boolean action, boolean value) { if (action == SET) endOfStream = value; return endOfStream; } /** * Trims down the size of the array if it is larger than the specified * maximum length. * * @param array * Array containing audio data. * @param maxLength * Maximum size this array may be. * @return New array. */ private static byte[] trimArray(byte[] array, int maxLength) { byte[] trimmedArray = null; if (array != null && array.length > maxLength) { trimmedArray = new byte[maxLength]; System.arraycopy(array, 0, trimmedArray, 0, maxLength); } return trimmedArray; } /** * Converts sound bytes to little-endian format. * * @param audio_bytes * The original wave data * @param two_bytes_data * For stereo sounds. * @return byte array containing the converted data. */ private static byte[] convertAudioBytes(byte[] audio_bytes, boolean two_bytes_data) { ByteBuffer dest = ByteBuffer.allocateDirect(audio_bytes.length); dest.order(ByteOrder.nativeOrder()); ByteBuffer src = ByteBuffer.wrap(audio_bytes); src.order(ByteOrder.LITTLE_ENDIAN); if (two_bytes_data) { ShortBuffer dest_short = dest.asShortBuffer(); ShortBuffer src_short = src.asShortBuffer(); while (src_short.hasRemaining()) { dest_short.put(src_short.get()); } } else { while (src.hasRemaining()) { dest.put(src.get()); } } dest.rewind(); if (!dest.hasArray()) { byte[] arrayBackedBuffer = new byte[dest.capacity()]; dest.get(arrayBackedBuffer); dest.clear(); return arrayBackedBuffer; } return dest.array(); } /** * Creates a new array with the second array appended to the end of the * first array. * * @param arrayOne * The first array. * @param arrayTwo * The second array. * @param length * How many bytes to append from the second array. * @return Byte array containing information from both arrays. */ private static byte[] appendByteArrays(byte[] arrayOne, byte[] arrayTwo, int length) { byte[] newArray; if (arrayOne == null && arrayTwo == null) { // no data, just return return null; } else if (arrayOne == null) { // create the new array, same length as arrayTwo: newArray = new byte[length]; // fill the new array with the contents of arrayTwo: System.arraycopy(arrayTwo, 0, newArray, 0, length); arrayTwo = null; } else if (arrayTwo == null) { // create the new array, same length as arrayOne: newArray = new byte[arrayOne.length]; // fill the new array with the contents of arrayOne: System.arraycopy(arrayOne, 0, newArray, 0, arrayOne.length); arrayOne = null; } else { // create the new array large enough to hold both arrays: newArray = new byte[arrayOne.length + length]; System.arraycopy(arrayOne, 0, newArray, 0, arrayOne.length); // fill the new array with the contents of both arrays: System.arraycopy(arrayTwo, 0, newArray, arrayOne.length, length); arrayOne = null; arrayTwo = null; } return newArray; } /** * Prints an error message. * * @param message * Message to print. */ private void errorMessage(String message) { logger.errorMessage("CodecWav", message, 0); } /** * Prints an exception's error message followed by the stack trace. * * @param e * Exception containing the information to print. */ private void printStackTrace(Exception e) { logger.printStackTrace(e, 1); } }