    001package paulscode.sound.codecs;
    002
    003import java.io.DataInputStream;
    004import java.io.InputStream;
    005import java.io.IOException;
    006import java.net.URL;
    007import java.nio.ByteBuffer;
    008import java.nio.ByteOrder;
    009import java.nio.ShortBuffer;
    010import javax.sound.sampled.AudioFormat;
    011
    012import paulscode.sound.ICodec;
    013import paulscode.sound.SoundBuffer;
    014import paulscode.sound.SoundSystemConfig;
    015import paulscode.sound.SoundSystemLogger;
    016
    017import ibxm.FastTracker2;
    018import ibxm.IBXM;
    019import ibxm.Module;
    020import ibxm.ProTracker;
    021import ibxm.ScreamTracker3;
    022
    023/**
    024 * The CodecIBXM class provides an ICodec interface for reading from MOD/S3M/XM
    025 * files via the IBXM library.
    026 *<b><i>   SoundSystem CodecIBXM Class License:</b></i><br><b><br>
    027 *    You are free to use this class for any purpose, commercial or otherwise.
    028 *    You may modify this class or source code, and distribute it any way you
    029 *    like, provided the following conditions are met:
    030 *<br>
    031 *    1) You may not falsely claim to be the author of this class or any
    032 *    unmodified portion of it.
    033 *<br>
    034 *    2) You may not copyright this class or a modified version of it and then
    035 *    sue me for copyright infringement.
    036 *<br>
    037 *    3) If you modify the source code, you must clearly document the changes
    038 *    made before redistributing the modified source code, so other users know
    039 *    it is not the original code.
    040 *<br>
    041 *    4) You are not required to give me credit for this class in any derived
    042 *    work, but if you do, you must also mention my website:
    043 *    http://www.paulscode.com
    044 *<br>
    045 *    5) I the author will not be responsible for any damages (physical,
    046 *    financial, or otherwise) caused by the use if this class or any portion
    047 *    of it.
    048 *<br>
    049 *    6) I the author do not guarantee, warrant, or make any representations,
    050 *    either expressed or implied, regarding the use of this class or any
    051 *    portion of it.
    052 * <br><br>
    053 *    Author: Paul Lamb
    054 * <br>
    055 *    http://www.paulscode.com
    056 *</b><br><br>
    057 *<b>
    058 *    This software is based on or using the IBXM library available from
    059 *    http://www.geocities.com/sunet2000/
    060 *</b><br><br>
    061 *<br><b>
    062 * IBXM is copyright (c) 2007, Martin Cameron, and is licensed under the BSD
    063 * License.
    064 *<br><br>
    065 * All rights reserved.
    066 *<br><br>
    067 * Redistribution and use in source and binary forms, with or without
    068 * modification, are permitted provided that the following conditions are met:
    069 *<br><br>
    070 * Redistributions of source code must retain the above copyright notice, this
    071 * list of conditions and the following disclaimer.  Redistributions in binary
    072 * form must reproduce the above copyright notice, this list of conditions and
    073 * the following disclaimer in the documentation and/or other materials
    074 * provided with the distribution.  Neither the name of mumart nor the names of
    075 * its contributors may be used to endorse or promote products derived from
    076 * this software without specific prior written permission.
    077 * <br><br>
    078 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    079 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    080 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    081 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    082 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    083 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    084 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    085 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    086 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    087 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    088 * POSSIBILITY OF SUCH DAMAGE.
    089 * <br><br><br></b>
    090 */
    091public class CodecIBXM implements ICodec
    092{
    093/**
    094 * Used to return a current value from one of the synchronized
    095 * boolean-interface methods.
    096 */
    097    private static final boolean GET = false;
    098
    099/**
    100 * Used to set the value in one of the synchronized boolean-interface methods.
    101 */
    102    private static final boolean SET = true;
    103
    104/**
    105 * Used when a parameter for one of the synchronized boolean-interface methods
    106 * is not aplicable.
    107 */
    108    private static final boolean XXX = false;
    109
    110/**
    111 * True if there is no more data to read in.
    112 */
    113    private boolean endOfStream = false;
    114
    115/**
    116 * True if the stream has finished initializing.
    117 */
    118    private boolean initialized = false;
    119
    120/**
    121 * Format the converted audio will be in.
    122 */
    123    private AudioFormat myAudioFormat = null;
    124
    125/**
    126 * True if the using library requires data read by this codec to be
    127 * reverse-ordered before returning it from methods read() and readAll().
    128 */
    129    private boolean reverseBytes = false;
    130
    131/**
    132 * IBXM decoder.
    133 */
    134    private IBXM ibxm;
    135
    136/**
    137 * Module instance to be played.
    138 */
    139    private Module module;
    140
    141/**
    142 * Duration of the audio (in frames).
    143 */
    144    private int songDuration;
    145
    146/**
    147 * Audio read position (in frames).
    148 */
    149    private int playPosition;
    150
    151/**
    152 * Processes status messages, warnings, and error messages.
    153 */
    154    private SoundSystemLogger logger;
    155
    156/**
    157 * Constructor:  Grabs a handle to the logger.
    158 */
    159    public CodecIBXM()
    160    {
    161        logger = SoundSystemConfig.getLogger();
    162    }
    163
    164/**
    165 * Tells this codec when it will need to reverse the byte order of
    166 * the data before returning it in the read() and readAll() methods.  The
    167 * IBXM library produces audio data in a format that some external audio
    168 * libraries require to be reversed.  Derivatives of the Library and Source
    169 * classes for audio libraries which require this type of data to be reversed
    170 * will call the reverseByteOrder() method.
    171 * @param b True if the calling audio library requires byte-reversal.
    172 */
    173    public void reverseByteOrder( boolean b )
    174    {
    175        reverseBytes = b;
    176    }
    177
    178/**
    179 * Prepares an audio stream to read from.  If another stream is already opened,
    180 * it will be closed and a new audio stream opened in its place.
    181 * @param url URL to an audio file to stream from.
    182 * @return False if an error occurred or if end of stream was reached.
    183 */
    184    public boolean initialize( URL url )
    185    {
    186        initialized( SET, false );
    187        cleanup();
    188
    189        if( url == null )
    190        {
    191            errorMessage( "url null in method 'initialize'" );
    192            cleanup();
    193            return false;
    194        }
    195
    196        InputStream is = null;
    197
    198        try
    199        {
    200            is = url.openStream();
    201        }
    202        catch( IOException ioe )
    203        {
    204            errorMessage( "Unable to open stream in method 'initialize'" );
    205            printStackTrace( ioe );
    206            return false;
    207        }
    208
    209        if( ibxm == null )
    210            ibxm = new IBXM( 48000 );
    211        if( myAudioFormat == null )
    212            myAudioFormat = new AudioFormat( 48000, 16, 2, true, true );
    213
    214        try
    215        {
    216            setModule( loadModule( is ) );
    217        }
    218        catch( IllegalArgumentException iae )
    219        {
    220            errorMessage( "Illegal argument in method 'initialize'" );
    221            printStackTrace( iae );
    222            if( is != null )
    223            {
    224                try
    225                {
    226                    is.close();
    227                }
    228                catch( IOException ioe )
    229                {}
    230            }
    231            return false;
    232        }
    233        catch( IOException ioe )
    234        {
    235            errorMessage( "Error loading module in method 'initialize'" );
    236            printStackTrace( ioe );
    237            if( is != null )
    238            {
    239                try
    240                {
    241                    is.close();
    242                }
    243                catch( IOException ioe2 )
    244                {}
    245            }
    246            return false;
    247        }
    248
    249        if( is != null )
    250        {
    251            try
    252            {
    253                is.close();
    254            }
    255            catch( IOException ioe )
    256            {}
    257        }
    258
    259        endOfStream( SET, false );
    260        initialized( SET, true );
    261        return true;
    262    }
    263
    264/**
    265 * Returns false if the stream is busy initializing.
    266 * @return True if steam is initialized.
    267 */
    268    public boolean initialized()
    269    {
    270        return initialized( GET, XXX );
    271    }
    272
    273/**
    274 * Reads in one stream buffer worth of audio data.  See
    275 * {@link paulscode.sound.SoundSystemConfig SoundSystemConfig} for more
    276 * information about accessing and changing default settings.
    277 * @return The audio data wrapped into a SoundBuffer context.
    278 */
    279    public SoundBuffer read()
    280    {
    281        if( endOfStream( GET, XXX ) )
    282            return null;
    283
    284        if( module == null )
    285        {
    286            errorMessage( "Module null in method 'read'" );
    287            return null;
    288        }
    289
    290        // Check to make sure there is an audio format:
    291        if( myAudioFormat == null )
    292        {
    293            errorMessage( "Audio Format null in method 'read'" );
    294            return null;
    295        }
    296
    297        int bufferFrameSize = (int) SoundSystemConfig.getStreamingBufferSize()
    298                                    / 4;
    299
    300        int frames = songDuration - playPosition;
    301        if( frames > bufferFrameSize )
    302            frames = bufferFrameSize;
    303
    304        if( frames <= 0 )
    305        {
    306            endOfStream( SET, true );
    307            return null;
    308        }
    309        byte[] outputBuffer = new byte[ frames * 4 ];
    310
    311        ibxm.get_audio( outputBuffer, frames );
    312
    313        playPosition += frames;
    314        if( playPosition >= songDuration )
    315        {
    316            endOfStream( SET, true );
    317        }
    318
    319        // Reverse the byte order if necessary:
    320        if( reverseBytes )
    321            reverseBytes( outputBuffer, 0, frames * 4 );
    322
    323        // Wrap the data into a SoundBuffer:
    324        SoundBuffer buffer = new SoundBuffer( outputBuffer, myAudioFormat );
    325
    326        return buffer;
    327    }
    328
    329/**
    330 * Reads in all the audio data from the stream (up to the default
    331 * "maximum file size".  See
    332 * {@link paulscode.sound.SoundSystemConfig SoundSystemConfig} for more
    333 * information about accessing and changing default settings.
    334 * @return the audio data wrapped into a SoundBuffer context.
    335 */
    336    public SoundBuffer readAll()
    337    {
    338        if( module == null )
    339        {
    340            errorMessage( "Module null in method 'readAll'" );
    341            return null;
    342        }
    343
    344        // Check to make sure there is an audio format:
    345        if( myAudioFormat == null )
    346        {
    347            errorMessage( "Audio Format null in method 'readAll'" );
    348            return null;
    349        }
    350
    351        int bufferFrameSize = (int) SoundSystemConfig.getFileChunkSize()
    352                                    / 4;
    353
    354        byte[] outputBuffer = new byte[ bufferFrameSize * 4 ];
    355
    356        // Buffer to contain the audio data:
    357        byte[] fullBuffer = null;
    358        // frames of audio data:
    359        int frames;
    360        // bytes of audio data:
    361        int totalBytes = 0;
    362
    363        while( (!endOfStream(GET, XXX)) &&
    364               (totalBytes < SoundSystemConfig.getMaxFileSize()) )
    365        {
    366            frames = songDuration - playPosition;
    367            if( frames > bufferFrameSize )
    368                frames = bufferFrameSize;
    369            ibxm.get_audio( outputBuffer, frames );
    370            totalBytes += (frames * 4);
    371
    372            fullBuffer = appendByteArrays( fullBuffer, outputBuffer,
    373                                           frames * 4 );
    374
    375            playPosition += frames;
    376            if( playPosition >= songDuration )
    377            {
    378                endOfStream( SET, true );
    379            }
    380        }
    381
    382        // Reverse the byte order if necessary:
    383        if( reverseBytes )
    384            reverseBytes( fullBuffer, 0, totalBytes );
    385
    386        // Wrap the data into a SoundBuffer:
    387        SoundBuffer buffer = new SoundBuffer( fullBuffer, myAudioFormat );
    388
    389        return buffer;
    390    }
    391
    392/**
    393 * Returns false if there is still more data available to be read in.
    394 * @return True if end of stream was reached.
    395 */
    396    public boolean endOfStream()
    397    {
    398        return endOfStream( GET, XXX );
    399    }
    400
    401/**
    402 * Closes the audio stream and remove references to all instantiated objects.
    403 */
    404    public void cleanup()
    405    {
    406//        if( ibxm != null )
    407//            ibxm.seek( 0 );
    408        playPosition = 0;
    409    }
    410
    411/**
    412 * Returns the audio format of the data being returned by the read() and
    413 * readAll() methods.
    414 * @return Information wrapped into an AudioFormat context.
    415 */
    416    public AudioFormat getAudioFormat()
    417    {
    418        return myAudioFormat;
    419    }
    420
    421/**
    422 * Decodes the data in the specified InputStream into an instance of
    423 * ibxm.Module.
    424 * @param input an InputStream containing the module file to be decoded.
    425 * @throws IllegalArgumentException if the data is not recognised as a module file.
    426 */
    427    private static Module loadModule( InputStream input )
    428                                    throws IllegalArgumentException, IOException
    429    {
    430        DataInputStream data_input_stream = new DataInputStream( input );
    431
    432        // Check if data is in XM format:
    433        byte[] xm_header = new byte[ 60 ];
    434        data_input_stream.readFully( xm_header );
    435        if( FastTracker2.is_xm( xm_header ) )
    436            return FastTracker2.load_xm( xm_header, data_input_stream );
    437
    438        // Check if data is in ScreamTracker 3 format:
    439        byte[] s3m_header = new byte[ 96 ];
    440        System.arraycopy( xm_header, 0, s3m_header, 0, 60 );
    441        data_input_stream.readFully( s3m_header, 60, 36 );
    442        if( ScreamTracker3.is_s3m( s3m_header ) )
    443            return ScreamTracker3.load_s3m( s3m_header, data_input_stream );
    444
    445        // Check if data is in ProTracker format:
    446        byte[] mod_header = new byte[ 1084 ];
    447        System.arraycopy( s3m_header, 0, mod_header, 0, 96 );
    448        data_input_stream.readFully( mod_header, 96, 988 );
    449        return ProTracker.load_mod( mod_header, data_input_stream );
    450    }
    451
    452/**
    453 * Sets the Module instance to be played.
    454 */
    455    private void setModule( Module m )
    456    {
    457        if( m != null )
    458            module = m;
    459        ibxm.set_module( module );
    460        songDuration = ibxm.calculate_song_duration();
    461    }
    462
    463/**
    464 * Internal method for synchronizing access to the boolean 'initialized'.
    465 * @param action GET or SET.
    466 * @param value New value if action == SET, or XXX if action == GET.
    467 * @return True if steam is initialized.
    468 */
    469    private synchronized boolean initialized( boolean action, boolean value )
    470    {
    471        if( action == SET )
    472            initialized = value;
    473        return initialized;
    474    }
    475
    476/**
    477 * Internal method for synchronizing access to the boolean 'endOfStream'.
    478 * @param action GET or SET.
    479 * @param value New value if action == SET, or XXX if action == GET.
    480 * @return True if end of stream was reached.
    481 */
    482    private synchronized boolean endOfStream( boolean action, boolean value )
    483    {
    484        if( action == SET )
    485            endOfStream = value;
    486        return endOfStream;
    487    }
    488
    489/**
    490 * Trims down the size of the array if it is larger than the specified
    491 * maximum length.
    492 * @param array Array containing audio data.
    493 * @param maxLength Maximum size this array may be.
    494 * @return New array.
    495 */
    496    private static byte[] trimArray( byte[] array, int maxLength )
    497    {
    498        byte[] trimmedArray = null;
    499        if( array != null && array.length > maxLength )
    500        {
    501            trimmedArray = new byte[maxLength];
    502            System.arraycopy( array, 0, trimmedArray, 0, maxLength );
    503        }
    504        return trimmedArray;
    505    }
    506
    507/**
    508 * Reverse-orders all bytes contained in the specified array.
    509 * @param buffer Array containing audio data.
    510 */
    511    public static void reverseBytes( byte[] buffer )
    512    {
    513        reverseBytes( buffer, 0, buffer.length );
    514    }
    515
    516/**
    517 * Reverse-orders the specified range of bytes contained in the specified array.
    518 * @param buffer Array containing audio data.
    519 * @param offset Array index to begin.
    520 * @param size number of bytes to reverse-order.
    521 */
    522    public static void reverseBytes( byte[] buffer, int offset, int size )
    523    {
    524
    525        byte b;
    526        for( int i = offset; i < ( offset + size ); i += 2 )
    527        {
    528            b = buffer[i];
    529            buffer[i] = buffer[i + 1];
    530            buffer[i + 1] = b;
    531        }
    532    }
    533
    534/**
    535 * Converts sound bytes to little-endian format.
    536 * @param audio_bytes The original wave data
    537 * @param two_bytes_data For stereo sounds.
    538 * @return byte array containing the converted data.
    539 */
    540    private static byte[] convertAudioBytes( byte[] audio_bytes,
    541                                             boolean two_bytes_data )
    542    {
    543        ByteBuffer dest = ByteBuffer.allocateDirect( audio_bytes.length );
    544        dest.order( ByteOrder.nativeOrder() );
    545        ByteBuffer src = ByteBuffer.wrap( audio_bytes );
    546        src.order( ByteOrder.LITTLE_ENDIAN );
    547        if( two_bytes_data )
    548        {
    549            ShortBuffer dest_short = dest.asShortBuffer();
    550            ShortBuffer src_short = src.asShortBuffer();
    551            while( src_short.hasRemaining() )
    552            {
    553                dest_short.put(src_short.get());
    554            }
    555        }
    556        else
    557        {
    558            while( src.hasRemaining() )
    559            {
    560                dest.put( src.get() );
    561            }
    562        }
    563        dest.rewind();
    564
    565        if( !dest.hasArray() )
    566        {
    567            byte[] arrayBackedBuffer = new byte[dest.capacity()];
    568            dest.get( arrayBackedBuffer );
    569            dest.clear();
    570
    571            return arrayBackedBuffer;
    572        }
    573
    574        return dest.array();
    575    }
    576
    577/**
    578 * Creates a new array with the second array appended to the end of the first
    579 * array.
    580 * @param arrayOne The first array.
    581 * @param arrayTwo The second array.
    582 * @param length How many bytes to append from the second array.
    583 * @return Byte array containing information from both arrays.
    584 */
    585    private static byte[] appendByteArrays( byte[] arrayOne, byte[] arrayTwo,
    586                                            int length )
    587    {
    588        byte[] newArray;
    589        if( arrayOne == null && arrayTwo == null )
    590        {
    591            // no data, just return
    592            return null;
    593        }
    594        else if( arrayOne == null )
    595        {
    596            // create the new array, same length as arrayTwo:
    597            newArray = new byte[ length ];
    598            // fill the new array with the contents of arrayTwo:
    599            System.arraycopy( arrayTwo, 0, newArray, 0, length );
    600            arrayTwo = null;
    601        }
    602        else if( arrayTwo == null )
    603        {
    604            // create the new array, same length as arrayOne:
    605            newArray = new byte[ arrayOne.length ];
    606            // fill the new array with the contents of arrayOne:
    607            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
    608            arrayOne = null;
    609        }
    610        else
    611        {
    612            // create the new array large enough to hold both arrays:
    613            newArray = new byte[ arrayOne.length + length ];
    614            System.arraycopy( arrayOne, 0, newArray, 0, arrayOne.length );
    615            // fill the new array with the contents of both arrays:
    616            System.arraycopy( arrayTwo, 0, newArray, arrayOne.length,
    617                              length );
    618            arrayOne = null;
    619            arrayTwo = null;
    620        }
    621
    622        return newArray;
    623    }
    624
    625/**
    626 * Prints an error message.
    627 * @param message Message to print.
    628 */
    629    private void errorMessage( String message )
    630    {
    631        logger.errorMessage( "CodecWav", message, 0 );
    632    }
    633
    634/**
    635 * Prints an exception's error message followed by the stack trace.
    636 * @param e Exception containing the information to print.
    637 */
    638    private void printStackTrace( Exception e )
    639    {
    640        logger.printStackTrace( e, 1 );
    641    }
    642}