package com.paulscode.sound.codecs;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import javax.sound.sampled.AudioFormat;
import com.paulscode.sound.ICodec;
import com.paulscode.sound.SoundBuffer;
import com.paulscode.sound.SoundSystemConfig;
import com.paulscode.sound.SoundSystemLogger;
import org.xiph.speex.OggCrc;
import org.xiph.speex.SpeexDecoder;
/**
* The CodecJSpeex class provides an ICodec interface for reading from
* files encoded by Speex.
*<b><i> SoundSystem CodecJSpeex Class 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><br><br>
*<b>
* This software is based on or using the JSpeex library available from
* http://jspeex.sourceforge.net/index.php
* <br>
* <br>
* JSpeex is a Java port of the Speex speech codec. For more information,
* visit http://www.speex.org/
*</b><br><br>
*<br><b>
* JSpeex License:
* <br><br>
* Copyright (c) 1999-2003 Wimba S.A., All Rights Reserved.
*<br><br>
* COPYRIGHT:
* This software is the property of Wimba S.A.
* This software is redistributed under the Xiph.org variant of
* the BSD license.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* - Neither the name of Wimba, the Xiph.org Foundation nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*<br><br>
* WARRANTIES:
* This software is made available by the authors in the hope
* that it will be useful, but without any warranty.
* Wimba S.A. is not liable for any consequence related to the
* use of the provided software.
*<br><br>
* Date: 22nd April 2003
*<br><br>
*<br>
*<br>
* Copyright (C) 2002 Jean-Marc Valin
*<br><br>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*<br><br>
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*<br><br>
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*<br><br>
* - Neither the name of the Xiph.org Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*<br><br>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*<br></b>
*/
public class CodecJSpeex 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;
/**
* Format the converted audio will be in.
*/
private AudioFormat myAudioFormat = null;
/**
* Global identifier for when the container speex file is ogg.
*/
public static final int CONTAINER_FORMAT_OGG = 1;
/**
* Global identifier for when the container speex file is wav.
*/
public static final int CONTAINER_FORMAT_WAV = 2;
/**
* Defines the file format that the speex file is wrapped into.
*/
private static int sourceFormat = CONTAINER_FORMAT_OGG;
/**
* Speex Decoder
*/
private SpeexDecoder speexDecoder;
/**
* Whether the speex data is "enhanced"
*/
private boolean enhanced = true;
/**
* Indicates the bandwidth of the encoded audio
*/
private int mode = 0;
/**
* Number of data frames
*/
private int nframes = 1;
/**
* Audio sample-rate
*/
private int sampleRate = -1;
/**
* Audio channels (mono or stereo)
*/
private int channels = 1;
/**
* Input from the URL
*/
private InputStream is = null;
/**
* For reading data from 'is'
*/
private DataInputStream dis = null;
// Header info used by the speex library
//---------------------------------------
private final int WAV_HEADERSIZE = 8;
private final short WAVE_FORMAT_SPEEX = (short) 0xa109;
private final String RIFF = "RIFF";
private final String WAVE = "WAVE";
private final String FORMAT = "fmt ";
private final String DATA = "data";
private final int OGG_HEADERSIZE = 27;
private final int OGG_SEGOFFSET = 26;
private final String OGGID = "OggS";
//---------------------------------------
// Buffers for reading in data
//---------------------------------------
private byte[] header = new byte[2048];
private byte[] payload = new byte[65536];
private byte[] decdat = new byte[SoundSystemConfig.getStreamingBufferSize()];
//---------------------------------------
// Varriables used by the speex library for reading
//---------------------------------------
private int origchksum = 0;
private int chksum = 0;
private int curseg = 0;
private int segments = 0;
private int packetNo = 0;
private int bodybytes = 0;
private int decsize = 0;
//---------------------------------------
/**
* Processes status messages, warnings, and error messages.
*/
private SoundSystemLogger logger;
/**
* Constructor: Grabs a handle to the logger.
*/
public CodecJSpeex()
{
logger = SoundSystemConfig.getLogger();
}
/**
* This method is ignored by CodecJSpeex, because it produces "nice" data.
* @param b True if the calling audio library requires byte-reversal from certain codecs
*/
public void reverseByteOrder( boolean b )
{}
/**
* 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
{
is = url.openStream();
dis = new DataInputStream( url.openStream() );
}
catch( IOException ioe )
{
errorMessage( "Unable to open stream in method 'initialize'" );
printStackTrace( ioe );
return false;
}
speexDecoder = new SpeexDecoder();
if( !processHeader() )
return false;
myAudioFormat = new AudioFormat( (float) sampleRate, 16, channels,
true, 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()
{
return readBytes( SoundSystemConfig.getStreamingBufferSize() );
}
/**
* 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()
{
return readBytes( SoundSystemConfig.getMaxFileSize() );
}
/**
* Reads in APPROXIMATELY the specified number of bytes (will be slightly over)
* @param maxBytes Target size in bytes to read.
* @return The audio data wrapped into a SoundBuffer context.
*/
private SoundBuffer readBytes( int maxBytes )
{
if( endOfStream( GET, XXX ) )
return null;
// Check to make sure there is an audio format:
if( myAudioFormat == null )
{
errorMessage( "Audio Format null in method 'read'" );
return null;
}
byte[] fullBuffer = null;
int totalBytes = 0;
try
{
if( getSourceFormat() == CONTAINER_FORMAT_OGG )
{
for( ; curseg < segments && totalBytes < maxBytes; curseg++ )
{
bodybytes = header[OGG_HEADERSIZE + curseg] & 0xFF;
if( bodybytes==255 )
{
errorMessage( "Unable to handle ogg body size 255 " +
"in method 'readAll'" );
return null;
}
dis.readFully( payload, 0, bodybytes );
chksum=OggCrc.checksum( chksum, payload, 0, bodybytes );
if( packetNo == 1 )
{
packetNo++;
}
else
{
speexDecoder.processData( payload, 0, bodybytes );
for( int i = 1; i < nframes; i++ )
{
speexDecoder.processData( false );
}
if( ( decsize = speexDecoder.getProcessedData( decdat,
0 ) ) > 0 )
{
fullBuffer = appendByteArrays( fullBuffer, decdat,
decsize );
totalBytes += decsize;
}
packetNo++;
}
}
while( !endOfStream( GET, XXX ) && totalBytes < maxBytes )
{
// read the OGG header
dis.readFully( header, 0, OGG_HEADERSIZE );
origchksum = readInt( header, 22 );
header[22] = 0;
header[23] = 0;
header[24] = 0;
header[25] = 0;
chksum=OggCrc.checksum( 0, header, 0, OGG_HEADERSIZE );
// make sure its a OGG header
if( !OGGID.equals( new String( header, 0, 4 ) ) )
{
errorMessage( "missing ogg id in method 'readAll'" );
return null;
}
segments = header[OGG_SEGOFFSET] & 0xFF;
dis.readFully( header, OGG_HEADERSIZE, segments );
chksum=OggCrc.checksum( chksum, header, OGG_HEADERSIZE, segments );
for( curseg=0; curseg < segments && totalBytes < maxBytes;
curseg++ )
{
bodybytes = header[OGG_HEADERSIZE + curseg] & 0xFF;
if( bodybytes==255 )
{
errorMessage( "Unable to handle ogg body size " +
"255 in method 'readAll'" );
return null;
}
dis.readFully( payload, 0, bodybytes );
chksum=OggCrc.checksum( chksum, payload, 0, bodybytes );
speexDecoder.processData( payload, 0, bodybytes );
for( int i = 1; i < nframes; i++ )
{
speexDecoder.processData( false );
}
if( ( decsize = speexDecoder.getProcessedData( decdat, 0 ) ) > 0 )
{
fullBuffer = appendByteArrays( fullBuffer, decdat,
decsize );
totalBytes += decsize;
}
packetNo++;
}
}
}
else
{
while( !endOfStream( GET, XXX ) && totalBytes < maxBytes )
{
dis.readFully( payload, 0, bodybytes );
speexDecoder.processData( payload, 0, bodybytes );
for( int i = 1; i < nframes; i++ )
{
speexDecoder.processData( false );
}
if( ( decsize = speexDecoder.getProcessedData( decdat, 0 ) )
> 0 )
{
fullBuffer = appendByteArrays( fullBuffer, decdat,
decsize );
totalBytes += decsize;
}
packetNo++;
}
}
}
catch( EOFException eof )
{
endOfStream( SET, true );
}
catch( IOException ioe )
{
printStackTrace( ioe );
return null;
}
// Wrap the data into a SoundBuffer:
SoundBuffer buffer = new SoundBuffer( fullBuffer, myAudioFormat );
return buffer;
}
/**
* Processes the speex header from the container file (ogg or wav).
* @return False if there was an error.
*/
private boolean processHeader()
{
origchksum = 0;
chksum = 0;
curseg = 0;
segments = 0;
packetNo = 0;
bodybytes = 0;
decsize = 0;
if( getSourceFormat() == CONTAINER_FORMAT_OGG )
{
try
{
dis.readFully( header, 0, OGG_HEADERSIZE );
}
catch( IOException ioe )
{
errorMessage( "Unable to read first segment of ogg header " +
"in method 'processHeader'" );
printStackTrace( ioe );
return false;
}
origchksum = readInt( header, 22 );
header[22] = 0;
header[23] = 0;
header[24] = 0;
header[25] = 0;
chksum=OggCrc.checksum( 0, header, 0, OGG_HEADERSIZE );
// make sure its a OGG header
if( !OGGID.equals( new String( header, 0, 4 ) ) )
{
errorMessage( "Ogg id missing in method 'processHeader'" );
return false;
}
segments = header[OGG_SEGOFFSET] & 0xFF;
try
{
dis.readFully( header, OGG_HEADERSIZE, segments );
}
catch( IOException ioe )
{
errorMessage( "Unable to read second segment of ogg header " +
"in method 'processHeader'" );
printStackTrace( ioe );
return false;
}
chksum=OggCrc.checksum( chksum, header, OGG_HEADERSIZE, segments );
for( curseg = 0; packetNo == 0; curseg++ )
{
bodybytes = header[OGG_HEADERSIZE + curseg] & 0xFF;
if( bodybytes == 255 )
{
errorMessage( "Unable to handle ogg body size 255 in " +
"method 'processHeader'" );
return false;
}
try
{
dis.readFully( payload, 0, bodybytes );
}
catch( IOException ioe )
{
errorMessage( "Unable to read segment " + curseg +
" of the ogg body in method " +
"'processHeader'" );
printStackTrace( ioe );
return false;
}
chksum = OggCrc.checksum( chksum, payload, 0, bodybytes );
if( readSpeexHeader( payload, 0, bodybytes ) )
{
sampleRate = speexDecoder.getSampleRate();
channels = speexDecoder.getChannels();
packetNo++;
}
else
{
packetNo = 0;
}
}
}
else
{
try
{
dis.readFully( header, 0, WAV_HEADERSIZE + 4 );
}
catch( IOException ioe )
{
errorMessage( "Error reading first segment of wav header " +
"in method 'processHeader'" );
printStackTrace( ioe );
return false;
}
if( !RIFF.equals( new String( header, 0, 4 ) ) &&
!WAVE.equals( new String( header, 8, 4 ) ) )
{
errorMessage( "Containing file not in the wav format in " +
"method 'processHeader'" );
return false;
}
try
{
dis.readFully( header, 0, WAV_HEADERSIZE );
}
catch( IOException ioe )
{
errorMessage( "Error reading second segment of wav header " +
"in method 'processHeader'" );
printStackTrace( ioe );
return false;
}
String chunk = new String( header, 0, 4 );
int size = readInt( header, 4 );
while( !chunk.equals( DATA ) )
{
try
{
dis.readFully( header, 0, size );
}
catch( IOException ioe )
{
errorMessage( "Error reading segment '" + chunk +
"' of wav header in method " +
"'processHeader'" );
printStackTrace( ioe );
return false;
}
if( chunk.equals( FORMAT ) )
{
if( readShort( header, 0 ) != WAVE_FORMAT_SPEEX )
{
errorMessage( "File is not a 'Wave Speex' file in " +
"method 'processHeader'" );
return false;
}
channels = readShort( header, 2 );
sampleRate = readInt( header, 4 );
bodybytes = readShort( header, 12 );
if( readShort( header, 16 ) < 82 )
{
errorMessage( "Possibly corrupt Speex Wave file in " +
"method 'processHeader'" );
return false;
}
readSpeexHeader( header, 20, 80 );
}
try
{
dis.readFully( header, 0, WAV_HEADERSIZE );
}
catch( IOException ioe )
{
errorMessage( "Error reading title of wav header " +
"segment after segment '" + chunk +
"' in method 'processHeader'" );
printStackTrace( ioe );
return false;
}
chunk = new String( header, 0, 4 );
size = readInt( header, 4 );
}
packetNo++;
}
return true;
}
/**
* 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( dis != null )
{
try
{
dis.close();
}
catch( IOException ioe )
{}
dis = null;
}
if( is != null )
{
try
{
is.close();
}
catch( IOException ioe )
{}
is = null;
}
speexDecoder = 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()
{
return myAudioFormat;
}
/**
* Returns the version of JSpeex being used.
* @return String indicating the version.
*/
public static String version()
{
return SpeexDecoder.VERSION;
}
/**
* Reads the header packet.
* @param packet Packet contents.
* @param offset The offset from which to start reading.
* @param bytes The number of bytes to read (should be 80).
* @return False if there was an error.
*/
private boolean readSpeexHeader( final byte[] packet, final int offset,
final int bytes )
{
if( bytes != 80 )
{
errorMessage( "Header byte size not 80 in method " +
"'readSpeexHeader'" );
return false;
}
if( !"Speex ".equals( new String( packet, offset, 8 ) ) )
{
return false;
}
mode = packet[40+offset] & 0xFF;
sampleRate = readInt( packet, offset + 36 );
channels = readInt( packet, offset + 48 );
nframes = readInt( packet, offset + 64 );
return speexDecoder.init( mode, sampleRate, channels, enhanced );
}
/**
* Converts Little Endian (Windows) bytes to an int (Java uses Big Endian).
* @param data the data to read.
* @param offset the offset from which to start reading.
* @return the integer value of the reassembled bytes.
*/
private static int readInt( final byte[] data, final int offset )
{
return ( data[offset] & 0xff ) |
( ( data[offset + 1] & 0xff ) << 8 ) |
( ( data[offset + 2] & 0xff ) << 16 ) |
( data[offset + 3] << 24 );
}
/**
* Converts Little Endian (Windows) bytes to a short (Java uses Big Endian).
* @param data the data to read.
* @param offset the offset from which to start reading.
* @return the integer value of the reassembled bytes.
*/
private static int readShort( final byte[] data, final int offset )
{
return ( data[offset] & 0xff ) |
( data[offset + 1] << 8 );
}
/**
* Tells the codec what format of the containing file is in (ogg or wav).
* @param format Global format identifier.
*/
public static void setSourceFormat( int format )
{
sourceFormat( SET, format );
}
/**
* Indicates the format of the containing file (ogg or wav).
* @return Global format identifier.
*/
public static int getSourceFormat()
{
return sourceFormat( GET, -1 );
}
/**
* Sets or returns the format of the containing file (ogg or wav).
* @return Global format identifier.
* @param action GET or SET.
* @param format Global format identifier.
*/
private static synchronized int sourceFormat( boolean action, int format )
{
if( action == SET )
sourceFormat = format;
return sourceFormat;
}
/**
* 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;
}
/**
* 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 );
}
}