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    }