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 }