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}