package 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 paulscode.sound.ICodec;
import paulscode.sound.SoundBuffer;
import paulscode.sound.SoundSystemConfig;
import 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 );
}
}