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 }