    001    package paulscode.sound.codecs;
    002
    003    import java.io.DataInputStream;
    004    import java.io.InputStream;
    005    import java.io.IOException;
    006    import java.net.URL;
    007    import java.nio.ByteBuffer;
    008    import java.nio.ByteOrder;
    009    import java.nio.ShortBuffer;
    010    import javax.sound.sampled.AudioFormat;
    011
    012    import paulscode.sound.ICodec;
    013    import paulscode.sound.SoundBuffer;
    014    import paulscode.sound.SoundSystemConfig;
    015    import paulscode.sound.SoundSystemLogger;
    016
    017    import ibxm.FastTracker2;
    018    import ibxm.IBXM;
    019    import ibxm.Module;
    020    import ibxm.ProTracker;
    021    import 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     */
    091    public 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    }