package paulscode.sound.codecs; import java.io.DataInputStream; import java.io.InputStream; 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 paulscode.sound.ICodec; import paulscode.sound.SoundBuffer; import paulscode.sound.SoundSystemConfig; import paulscode.sound.SoundSystemLogger; import ibxm.FastTracker2; import ibxm.IBXM; import ibxm.Module; import ibxm.ProTracker; import ibxm.ScreamTracker3; /** * The CodecIBXM class provides an ICodec interface for reading from MOD/S3M/XM * files via the IBXM library. *<b><i> SoundSystem CodecIBXM 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 IBXM library available from * http://www.geocities.com/sunet2000/ *</b><br><br> *<br><b> * IBXM is copyright (c) 2007, Martin Cameron, and is licensed under the BSD * License. *<br><br> * All rights reserved. *<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. 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 mumart 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 COPYRIGHT HOLDER 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><br><br></b> */ public class CodecIBXM 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; /** * True if the using library requires data read by this codec to be * reverse-ordered before returning it from methods read() and readAll(). */ private boolean reverseBytes = false; /** * IBXM decoder. */ private IBXM ibxm; /** * Module instance to be played. */ private Module module; /** * Duration of the audio (in frames). */ private int songDuration; /** * Audio read position (in frames). */ private int playPosition; /** * Processes status messages, warnings, and error messages. */ private SoundSystemLogger logger; /** * Constructor: Grabs a handle to the logger. */ public CodecIBXM() { logger = SoundSystemConfig.getLogger(); } /** * Tells this codec when it will need to reverse the byte order of * the data before returning it in the read() and readAll() methods. The * IBXM library produces audio data in a format that some external audio * libraries require to be reversed. Derivatives of the Library and Source * classes for audio libraries which require this type of data to be reversed * will call the reverseByteOrder() method. * @param b True if the calling audio library requires byte-reversal. */ @Override public void reverseByteOrder( boolean b ) { reverseBytes = 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. */ @Override public boolean initialize( URL url ) { initialized( SET, false ); cleanup(); if( url == null ) { errorMessage( "url null in method 'initialize'" ); cleanup(); return false; } InputStream is = null; try { is = url.openStream(); } catch( IOException ioe ) { errorMessage( "Unable to open stream in method 'initialize'" ); printStackTrace( ioe ); return false; } if( ibxm == null ) ibxm = new IBXM( 48000 ); if( myAudioFormat == null ) myAudioFormat = new AudioFormat( 48000, 16, 2, true, true ); try { setModule( loadModule( is ) ); } catch( IllegalArgumentException iae ) { errorMessage( "Illegal argument in method 'initialize'" ); printStackTrace( iae ); if( is != null ) { try { is.close(); } catch( IOException ioe ) {} } return false; } catch( IOException ioe ) { errorMessage( "Error loading module in method 'initialize'" ); printStackTrace( ioe ); if( is != null ) { try { is.close(); } catch( IOException ioe2 ) {} } return false; } if( is != null ) { try { is.close(); } catch( IOException ioe ) {} } endOfStream( SET, false ); initialized( SET, true ); return true; } /** * Returns false if the stream is busy initializing. * @return True if steam is initialized. */ @Override 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. */ @Override public SoundBuffer read() { if( endOfStream( GET, XXX ) ) return null; if( module == null ) { errorMessage( "Module null in method 'read'" ); return null; } // Check to make sure there is an audio format: if( myAudioFormat == null ) { errorMessage( "Audio Format null in method 'read'" ); return null; } int bufferFrameSize = (int) SoundSystemConfig.getStreamingBufferSize() / 4; int frames = songDuration - playPosition; if( frames > bufferFrameSize ) frames = bufferFrameSize; if( frames <= 0 ) { endOfStream( SET, true ); return null; } byte[] outputBuffer = new byte[ frames * 4 ]; ibxm.get_audio( outputBuffer, frames ); playPosition += frames; if( playPosition >= songDuration ) { endOfStream( SET, true ); } // Reverse the byte order if necessary: if( reverseBytes ) reverseBytes( outputBuffer, 0, frames * 4 ); // Wrap the data into a SoundBuffer: SoundBuffer buffer = new SoundBuffer( outputBuffer, myAudioFormat ); 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. */ @Override public SoundBuffer readAll() { if( module == null ) { errorMessage( "Module null in method 'readAll'" ); return null; } // Check to make sure there is an audio format: if( myAudioFormat == null ) { errorMessage( "Audio Format null in method 'readAll'" ); return null; } int bufferFrameSize = (int) SoundSystemConfig.getFileChunkSize() / 4; byte[] outputBuffer = new byte[ bufferFrameSize * 4 ]; // Buffer to contain the audio data: byte[] fullBuffer = null; // frames of audio data: int frames; // bytes of audio data: int totalBytes = 0; while( (!endOfStream(GET, XXX)) && (totalBytes < SoundSystemConfig.getMaxFileSize()) ) { frames = songDuration - playPosition; if( frames > bufferFrameSize ) frames = bufferFrameSize; ibxm.get_audio( outputBuffer, frames ); totalBytes += (frames * 4); fullBuffer = appendByteArrays( fullBuffer, outputBuffer, frames * 4 ); playPosition += frames; if( playPosition >= songDuration ) { endOfStream( SET, true ); } } // Reverse the byte order if necessary: if( reverseBytes ) reverseBytes( fullBuffer, 0, totalBytes ); // Wrap the data into a SoundBuffer: SoundBuffer buffer = new SoundBuffer( fullBuffer, myAudioFormat ); return buffer; } /** * Returns false if there is still more data available to be read in. * @return True if end of stream was reached. */ @Override public boolean endOfStream() { return endOfStream( GET, XXX ); } /** * Closes the audio stream and remove references to all instantiated objects. */ @Override public void cleanup() { // if( ibxm != null ) // ibxm.seek( 0 ); playPosition = 0; } /** * Returns the audio format of the data being returned by the read() and * readAll() methods. * @return Information wrapped into an AudioFormat context. */ @Override public AudioFormat getAudioFormat() { return myAudioFormat; } /** * Decodes the data in the specified InputStream into an instance of * ibxm.Module. * @param input an InputStream containing the module file to be decoded. * @throws IllegalArgumentException if the data is not recognised as a module file. */ private static Module loadModule( InputStream input ) throws IllegalArgumentException, IOException { DataInputStream data_input_stream = new DataInputStream( input ); // Check if data is in XM format: byte[] xm_header = new byte[ 60 ]; data_input_stream.readFully( xm_header ); if( FastTracker2.is_xm( xm_header ) ) return FastTracker2.load_xm( xm_header, data_input_stream ); // Check if data is in ScreamTracker 3 format: byte[] s3m_header = new byte[ 96 ]; System.arraycopy( xm_header, 0, s3m_header, 0, 60 ); data_input_stream.readFully( s3m_header, 60, 36 ); if( ScreamTracker3.is_s3m( s3m_header ) ) return ScreamTracker3.load_s3m( s3m_header, data_input_stream ); // Check if data is in ProTracker format: byte[] mod_header = new byte[ 1084 ]; System.arraycopy( s3m_header, 0, mod_header, 0, 96 ); data_input_stream.readFully( mod_header, 96, 988 ); return ProTracker.load_mod( mod_header, data_input_stream ); } /** * Sets the Module instance to be played. */ private void setModule( Module m ) { if( m != null ) module = m; ibxm.set_module( module ); songDuration = ibxm.calculate_song_duration(); } /** * 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; } /** * Reverse-orders all bytes contained in the specified array. * @param buffer Array containing audio data. */ public static void reverseBytes( byte[] buffer ) { reverseBytes( buffer, 0, buffer.length ); } /** * Reverse-orders the specified range of bytes contained in the specified array. * @param buffer Array containing audio data. * @param offset Array index to begin. * @param size number of bytes to reverse-order. */ public static void reverseBytes( byte[] buffer, int offset, int size ) { byte b; for( int i = offset; i < ( offset + size ); i += 2 ) { b = buffer[i]; buffer[i] = buffer[i + 1]; buffer[i + 1] = b; } } /** * 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 ); } }