paulscode.sound.codecs.CodecJOrbis Maven / Gradle / Ivy
package paulscode.sound.codecs;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownServiceException;
import javax.sound.sampled.AudioFormat;
import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Block;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Info;
import paulscode.sound.ICodec;
import paulscode.sound.SoundBuffer;
import paulscode.sound.SoundSystemConfig;
import paulscode.sound.SoundSystemLogger;
// From the JOrbis library, http://www.jcraft.com/jorbis/
/**
* The CodecJOrbis class provides an ICodec interface to the external JOrbis
* library.
* SoundSystem CodecJOrbis Class License:
* 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:
*
* 1) You may not falsely claim to be the author of this class or any
* unmodified portion of it.
*
* 2) You may not copyright this class or a modified version of it and then
* sue me for copyright infringement.
*
* 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.
*
* 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
*
* 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.
*
* 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.
*
* Author: Paul Lamb
*
* http://www.paulscode.com
*
*
* This software is based on or using the JOrbis library available from
* http://www.jcraft.com/jorbis/
*
*
*
* Author: Paul Lamb
*
* http://www.paulscode.com
*
*/
public class CodecJOrbis 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;
/**
* URL to the audio file to stream from.
*/
private URL url;
/**
* Used for connecting to the URL.
*/
private URLConnection urlConnection = null;
/**
* InputStream context for reading data from the file.
*/
private InputStream inputStream;
/**
* Format in which the converted audio data is stored.
*/
private AudioFormat audioFormat;
/**
* 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;
/**
* Used to hold the data as it is read in.
*/
private byte[] buffer = null;
/**
* Amount of data to read in at one time.
*/
private int bufferSize;
/**
* Number of bytes read.
*/
private int count = 0;
/**
* Location within the data.
*/
private int index = 0;
/**
* Size of the data after it has been converted into pcm.
*/
private int convertedBufferSize;
/**
* Linear buffer to hold the data after it has been converted into pcm.
*/
private byte[] convertedBuffer = null;
/**
* Nonlinear pcm data.
*/
private float[][][] pcmInfo;
/**
* Location within the data.
*/
private int[] pcmIndex;
/**
* Data packet.
*/
private Packet joggPacket = new Packet();
/**
* Data Page.
*/
private Page joggPage = new Page();
/**
* Stream state.
*/
private StreamState joggStreamState = new StreamState();
/**
* Packet streaming layer.
*/
private SyncState joggSyncState = new SyncState();
/**
* Internal data storage.
*/
private DspState jorbisDspState = new DspState();
/**
* Block of stored JOrbis data.
*/
private Block jorbisBlock = new Block(jorbisDspState);
/**
* Comment fields.
*/
private Comment jorbisComment = new Comment();
/**
* Info about the data.
*/
private Info jorbisInfo = new Info();
/**
* Processes status messages, warnings, and error messages.
*/
private SoundSystemLogger logger;
/**
* Constructor: Grabs a handle to the logger.
*/
public CodecJOrbis()
{
logger = SoundSystemConfig.getLogger();
}
/**
* This method is ignored by CodecJOrbis, 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 input stream to read from. If another stream is already opened,
* it will be closed and a new input stream opened in its place.
* @param url URL to an ogg 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 );
if( joggStreamState != null )
joggStreamState.clear();
if( jorbisBlock != null )
jorbisBlock.clear();
if( jorbisDspState != null )
jorbisDspState.clear();
if( jorbisInfo != null )
jorbisInfo.clear();
if( joggSyncState != null )
joggSyncState.clear();
if( inputStream != null )
{
try
{
inputStream.close();
}
catch( IOException ioe )
{}
}
this.url = url;
//this.bufferSize = SoundSystemConfig.getStreamingBufferSize() / 2;
this.bufferSize = 4096*2;
buffer = null;
count = 0;
index = 0;
joggStreamState = new StreamState();
jorbisBlock = new Block(jorbisDspState);
jorbisDspState = new DspState();
jorbisInfo = new Info();
joggSyncState = new SyncState();
try
{
urlConnection = url.openConnection();
}
catch( UnknownServiceException use )
{
errorMessage( "Unable to create a UrlConnection in method " +
"'initialize'." );
printStackTrace( use );
cleanup();
return false;
}
catch( IOException ioe )
{
errorMessage( "Unable to create a UrlConnection in method " +
"'initialize'." );
printStackTrace( ioe );
cleanup();
return false;
}
if( urlConnection != null )
{
try
{
inputStream = urlConnection.getInputStream();
}
catch( IOException ioe )
{
errorMessage( "Unable to acquire inputstream in method " +
"'initialize'." );
printStackTrace( ioe );
cleanup();
return false;
}
}
endOfStream( SET, false );
joggSyncState.init();
joggSyncState.buffer( bufferSize );
buffer = joggSyncState.data;
try
{
if( !readHeader() )
{
errorMessage( "Error reading the header" );
return false;
}
}
catch( IOException ioe )
{
errorMessage( "Error reading the header" );
return false;
}
convertedBufferSize = bufferSize * 2;
jorbisDspState.synthesis_init( jorbisInfo );
jorbisBlock.init( jorbisDspState );
int channels = jorbisInfo.channels;
int rate = jorbisInfo.rate;
audioFormat = new AudioFormat( (float) rate, 16, channels, true,
false );
pcmInfo = new float[1][][];
pcmIndex = new int[ jorbisInfo.channels ];
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 SoundSystemConfig SoundSystemConfig} for more
* information about accessing and changing default settings.
* @return The audio data wrapped into a SoundBuffer context.
*/
public SoundBuffer read()
{
byte[] returnBuffer = null;
while( !endOfStream( GET, XXX ) && ( returnBuffer == null ||
returnBuffer.length < SoundSystemConfig.getStreamingBufferSize() ) )
{
if( returnBuffer == null )
returnBuffer = readBytes();
else
returnBuffer = appendByteArrays( returnBuffer, readBytes() );
}
if( returnBuffer == null )
return null;
return new SoundBuffer( returnBuffer, audioFormat );
}
/**
* Reads in all the audio data from the stream (up to the default
* "maximum file size". See
* {@link SoundSystemConfig SoundSystemConfig} for more
* information about accessing and changing default settings.
* @return the audio data wrapped into a SoundBuffer context.
*/
public SoundBuffer readAll()
{
byte[] returnBuffer = null;
while( !endOfStream( GET, XXX ) )
{
if( returnBuffer == null )
returnBuffer = readBytes();
else
returnBuffer = appendByteArrays( returnBuffer, readBytes() );
}
if( returnBuffer == null )
return null;
return new SoundBuffer( returnBuffer, audioFormat );
}
/**
* 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 input stream and remove references to all instantiated objects.
*/
public void cleanup()
{
joggStreamState.clear();
jorbisBlock.clear();
jorbisDspState.clear();
jorbisInfo.clear();
joggSyncState.clear();
if( inputStream != null )
{
try
{
inputStream.close();
}
catch( IOException ioe )
{}
}
joggStreamState = null;
jorbisBlock = null;
jorbisDspState = null;
jorbisInfo = null;
joggSyncState = null;
inputStream = 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 audioFormat;
}
/**
* Reads in the header information for the ogg file, which is contained in the
* first three packets of data.
* @return True if successful.
*/
private boolean readHeader() throws IOException
{
// Update up JOrbis internal buffer:
index = joggSyncState.buffer( bufferSize );
// Read in a buffer of data:
int bytes = inputStream.read( joggSyncState.data, index, bufferSize );
if( bytes < 0 )
bytes = 0;
// Let JOrbis know how many bytes we got:
joggSyncState.wrote( bytes );
if( joggSyncState.pageout( joggPage ) != 1 )
{
// Finished reading the entire file:
if( bytes < bufferSize )
return true;
errorMessage( "Ogg header not recognized in method 'readHeader'." );
return false;
}
// Initialize JOrbis:
joggStreamState.init( joggPage.serialno() );
jorbisInfo.init();
jorbisComment.init();
if( joggStreamState.pagein( joggPage ) < 0 )
{
errorMessage( "Problem with first Ogg header page in method " +
"'readHeader'." );
return false;
}
if( joggStreamState.packetout( joggPacket ) != 1 )
{
errorMessage( "Problem with first Ogg header packet in method " +
"'readHeader'." );
return false;
}
if( jorbisInfo.synthesis_headerin( jorbisComment, joggPacket ) < 0 )
{
errorMessage( "File does not contain Vorbis header in method " +
"'readHeader'." );
return false;
}
int i = 0;
while( i < 2 )
{
while( i < 2 )
{
int result = joggSyncState.pageout( joggPage );
if( result == 0 )
break;
if( result == 1 )
{
joggStreamState.pagein( joggPage );
while( i < 2 )
{
result = joggStreamState.packetout( joggPacket );
if( result == 0 )
break;
if( result == -1 )
{
errorMessage( "Secondary Ogg header corrupt in " +
"method 'readHeader'." );
return false;
}
jorbisInfo.synthesis_headerin( jorbisComment,
joggPacket );
i++;
}
}
}
index = joggSyncState.buffer( bufferSize );
bytes = inputStream.read( joggSyncState.data, index, bufferSize );
if( bytes < 0 )
bytes = 0;
if( bytes == 0 && i < 2 )
{
errorMessage( "End of file reached before finished reading" +
"Ogg header in method 'readHeader'" );
return false;
}
joggSyncState.wrote( bytes );
}
index = joggSyncState.buffer( bufferSize );
buffer = joggSyncState.data;
return true;
}
/**
* Reads and decodes a chunk of data of length "convertedBufferSize".
* @return Array containing the converted audio data.
*/
private byte[] readBytes()
{
if( !initialized( GET, XXX ) )
return null;
if( endOfStream( GET, XXX ) )
return null;
if( convertedBuffer == null )
convertedBuffer = new byte[ convertedBufferSize ];
byte[] returnBuffer = null;
float[][] pcmf;
int samples, bout, ptr, mono, val, i, j;
switch( joggSyncState.pageout( joggPage ) )
{
case( 0 ):
case( -1 ):
break;
default:
{
joggStreamState.pagein( joggPage );
if( joggPage.granulepos() == 0 )
{
endOfStream( SET, true );
return null;
}
processPackets: while( true )
{
switch( joggStreamState.packetout( joggPacket ) )
{
case( 0 ):
break processPackets;
case( -1 ):
break;
default:
{
if( jorbisBlock.synthesis( joggPacket ) == 0 )
jorbisDspState.synthesis_blockin( jorbisBlock );
while( ( samples=jorbisDspState.synthesis_pcmout(
pcmInfo, pcmIndex ) ) > 0 )
{
pcmf = pcmInfo[0];
bout = ( samples < convertedBufferSize ?
samples : convertedBufferSize );
for( i = 0; i < jorbisInfo.channels; i++ )
{
ptr = i * 2;
mono = pcmIndex[i];
for( j = 0; j < bout; j++ )
{
val = (int) ( pcmf[i][mono + j] *
32767. );
if( val > 32767 )
val = 32767;
if( val < -32768 )
val = -32768;
if( val < 0 )
val = val | 0x8000;
convertedBuffer[ptr] = (byte) (val);
convertedBuffer[ptr+1] =
(byte) (val>>>8);
ptr += 2 * (jorbisInfo.channels);
}
}
jorbisDspState.synthesis_read( bout );
returnBuffer = appendByteArrays( returnBuffer,
convertedBuffer,
2 * jorbisInfo.channels * bout );
}
}
}
}
if( joggPage.eos() != 0 )
endOfStream( SET, true );
}
}
if( !endOfStream( GET, XXX ) )
{
index = joggSyncState.buffer( bufferSize );
buffer = joggSyncState.data;
try
{
count = inputStream.read( buffer, index, bufferSize );
}
catch( Exception e )
{
printStackTrace( e );
return null;
}
if( count == -1 )
return returnBuffer;
joggSyncState.wrote( count );
if( count==0 )
endOfStream( SET, true );
}
return returnBuffer;
}
/**
* 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;
}
/**
* 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 arrayTwoBytes The number of bytes to append from the second array.
* @return Byte array containing information from both arrays.
*/
private static byte[] appendByteArrays( byte[] arrayOne, byte[] arrayTwo,
int arrayTwoBytes )
{
byte[] newArray;
int bytes = arrayTwoBytes;
// Make sure we aren't trying to append more than is there:
if( arrayTwo == null || arrayTwo.length == 0 )
bytes = 0;
else if( arrayTwo.length < arrayTwoBytes )
bytes = arrayTwo.length;
if( arrayOne == null && (arrayTwo == null || bytes <= 0) )
{
// no data, just return
return null;
}
else if( arrayOne == null )
{
// create the new array, same length as arrayTwo:
newArray = new byte[ bytes ];
// fill the new array with the contents of arrayTwo:
System.arraycopy( arrayTwo, 0, newArray, 0, bytes );
arrayTwo = null;
}
else if( arrayTwo == null || bytes <= 0 )
{
// 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 + bytes ];
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,
bytes );
arrayOne = null;
arrayTwo = null;
}
return newArray;
}
/**
* 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.
* @return Byte array containing information from both arrays.
*/
private static byte[] appendByteArrays( byte[] arrayOne, byte[] arrayTwo )
{
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[ arrayTwo.length ];
// fill the new array with the contents of arrayTwo:
System.arraycopy( arrayTwo, 0, newArray, 0, arrayTwo.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 + arrayTwo.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,
arrayTwo.length );
arrayOne = null;
arrayTwo = null;
}
return newArray;
}
/**
* Prints an error message.
* @param message Message to print.
*/
private void errorMessage( String message )
{
logger.errorMessage( "CodecJOrbis", 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 );
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy