1 /** 2 * This file is part of the Web Enabled Audio and Sound Enhancement Library (aka the Weasel audio library) Copyright 2011 - 2013 Warren Willmey. It is covered by the GNU General Public License version 3 as published by the Free Software Foundation, you should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. 3 */ 4 5 if( undefined == window.weasel ) window.weasel = {}; 6 7 8 // --------------------------------------------------------------------------- 9 /** Static class containing helper functions for dealing with binary data in Soundtracker Modules. 10 * 11 * @static 12 * @constructor 13 * @author Warren Willmey 2011 14 */ 15 weasel.Helper = {}; 16 17 // --------------------------------------------------------------------------- 18 /** 19 * Read a byte from byte array. 20 * 21 * @param {Array|Uint8Array} aData = The Array containing the byte to fetch. 22 * @param {int} iOffset = The offset of the byte to fetch (starting at 0, last byte = sData.length -1). 23 * 24 * @return {int} = The byte at the given offset, or throws an exception if "Index out of range". 25 * 26 * @exception {String} 'Index out of range' = the Offset is out of range of the provided data. 27 * @exception {String} 'Wrong data type' = aData is expected to be an Array. 28 * 29 * @author Warren Willmey 2011 30 */ 31 weasel.Helper.getByte = function( aData, iOffset ) 32 { 33 if( !(( aData instanceof Array ) || ( aData instanceof Uint8Array )) ) 34 throw( 'Wrong data type' ); 35 36 if( iOffset < 0 || iOffset >= aData.length ) 37 throw 'Index out of range: Data Size = ' + aData.length + ', Index = ' + iOffset; 38 39 return aData[ iOffset ] & 0xff; 40 }; 41 42 // --------------------------------------------------------------------------- 43 /** 44 * Read a word (2 bytes in big endian format [thats 680x0 number format]) from byte array. 45 * 46 * @param {Array|Uint8Array} aData = The Array containing the word to fetch. 47 * @param {int} iOffset = The offset of the word to fetch (starting at 0, last byte = aData.length -1). 48 * 49 * @return {int} = The word at the given offset, or throws an exception if "Index out of range". 50 * 51 * @exception {String} 'Index out of range' = the Offset is out of range of the provided data. 52 * @exception {String} 'Wrong data type' = sData is expected to be an Array. 53 * 54 * @author Warren Willmey 2011 55 */ 56 weasel.Helper.getWord = function( aData, iOffset ) 57 { 58 return ( weasel.Helper.getByte( aData, iOffset ) << 8 ) | weasel.Helper.getByte( aData, iOffset + 1 ); 59 }; 60 61 // --------------------------------------------------------------------------- 62 /** 63 * Read a long (4 bytes in big endian format [thats 680x0 number format]) from byte array. 64 * 65 * @param {Array|Uint8Array} aData = The Array containing the word to fetch. 66 * @param {int} iOffset = The offset of the long to fetch (starting at 0, last byte = aData.length -1). 67 * 68 * @return {int} = The long at the given offset, or throws an exception if "Index out of range". 69 * 70 * @exception {String} 'Index out of range' = the Offset is out of range of the provided data. 71 * @exception {String} 'Wrong data type' = sData is expected to be an Array. 72 * 73 * @author Warren Willmey 2011 74 */ 75 weasel.Helper.getLong = function( aData, iIndex ) 76 { 77 return (weasel.Helper.getWord( aData, iIndex ) << 16) | weasel.Helper.getWord( aData, iIndex + 2 ); 78 }; 79 80 81 // --------------------------------------------------------------------------- 82 /** 83 * Convert a String (containing binary data) into a byte array. 84 * 85 * @param {String} sData = The String containing the bytes. 86 * 87 * @return {Array|Uint8Array} = The String converted to an Array. 88 * 89 * @exception {String} 'Wrong data type' = sData is expected to be a String. 90 * 91 * @author Warren Willmey 2011 92 */ 93 weasel.Helper.convertStringToArray = function( sString ) 94 { 95 if( 'string' !== typeof sString ) 96 throw( 'Wrong data type' ); 97 98 var aArray = weasel.Helper.getUnsignedByteArray( sString.length ); 99 100 for( var iLength = sString.length, iOffset = 0; --iLength >= 0; ) 101 { 102 aArray[ iOffset ] = sString.charCodeAt( iOffset++ ) & 0xff; 103 } 104 105 return aArray; 106 }; 107 108 // --------------------------------------------------------------------------- 109 /** 110 * Convert a unsigned byte into a signed byte (ones complement into twos complement). 111 * 112 * @param {int} iUsignedByte = the Ones complement (or unsigned byte) to convert, 113 * 114 * @return {int} = The Twos complement (signed byte). 115 * 116 * @author Warren Willmey 2011 117 */ 118 weasel.Helper.twosComplement = function( iUsignedByte ) 119 { 120 if( iUsignedByte >= 128 ) 121 { 122 return iUsignedByte - 256; 123 } 124 125 return iUsignedByte; 126 }; 127 128 // --------------------------------------------------------------------------- 129 /** 130 * Get a null terminated string from a byte array. 131 * 132 * @param {Array|Uint8Array} aData = Array of bytes containing string. 133 * @param {int} iOffset = The offset into the data to the beginning of the string. 134 * @param {int} iMaxLength = The maximum length the string can be. 135 * 136 * @return {String} = The null terminated string converted to accepted ASCII range. 137 */ 138 weasel.Helper.getNullTerminatedString = function( aData, iOffset, iMaxLength ) 139 { 140 var sString = ''; 141 142 // Strings inside the mod file have a known maximum length and are expected 143 // to be NULL terminated (obviously bad rips or hacked versions may not be 144 // so dont expect them to conform). 145 // Only accept ASCII values for safety, Amiga extended charset ignored for now. 146 // 147 // @TODO: Convert Amiga characters to Unicode better. 148 // 149 try 150 { 151 for( var iEndOfString = iOffset + iMaxLength; iOffset < iEndOfString; iOffset++ ) 152 { 153 var iByte = weasel.Helper.getByte( aData, iOffset ); 154 155 if( 0 == iByte ) 156 { 157 break; 158 } 159 else if( iByte >= 32 && iByte < 127 ) 160 { 161 sString += String.fromCharCode( iByte ); 162 } 163 else 164 { 165 sString += ' '; 166 } 167 } 168 }catch( oException ){} 169 170 return sString; 171 }; 172 173 // --------------------------------------------------------------------------- 174 /** Check to see if the 32 bit floating point number arrays are supported in this browser. 175 * 176 * @return {bool} = true : Float 32 Array type available. 177 * 178 * @private 179 */ 180 weasel.Helper._detectFloat32Array = function( ) 181 { 182 return window.Float32Array ? true : false; 183 }; 184 185 // --------------------------------------------------------------------------- 186 /** Use the Float32Array data type (its quicker than checking window.Float32Array and 187 * every time you want to use it). 188 * @type {boolean} 189 * 190 * @private 191 */ 192 weasel.Helper._bFloat32ArrayAvailable = weasel.Helper._detectFloat32Array(); 193 194 195 // --------------------------------------------------------------------------- 196 /** 197 * Get a Typed Array if supported (as these are supposed to be much faster, use less memory and don't cause the Garbage Collector to go nuts). 198 * Be aware that a Float is a 32bit data type and not a Number data type (IEEE-754 Double, which is 64 bits) and so mathematical results WILL be different when using them, due to precision. 199 * 200 * @param {int} iSize = The size of the array wanted. 201 * 202 * @return {(Array|Float32Array)} = Array of iSize or Float32Array of iSize if Float32Array data type is supported. 203 */ 204 weasel.Helper.getFloat32Array = function( iSize ) 205 { 206 if( weasel.Helper._bFloat32ArrayAvailable ) 207 return new window.Float32Array( iSize ); 208 209 return new Array( iSize ); 210 }; 211 212 213 // --------------------------------------------------------------------------- 214 /** Check to see if the 8 bit unsigned byte arrays are supported in this browser. 215 * 216 * @return {bool} = true : unsigned byte Array type available. 217 * 218 * @private 219 */ 220 weasel.Helper._detectUint8Array = function( ) 221 { 222 return window.Uint8Array ? true : false; 223 }; 224 225 // --------------------------------------------------------------------------- 226 /** Use the Uint8Array data type (its quicker than checking Uint8Array and 227 * every time you want to use it). 228 * @type {boolean} 229 * 230 * @private 231 */ 232 weasel.Helper._bUint8ArrayAvailable = weasel.Helper._detectUint8Array(); 233 234 235 // --------------------------------------------------------------------------- 236 /** 237 * Get a Typed Array if supported (as these are supposed to be much faster, use less memory and don't cause the Garbage Collector to go nuts). 238 * 239 * @param {int} iSize = The size of the array wanted. 240 * 241 * @return {(Array|Uint8Array)} = Array of iSize or Uint8Array of iSize if Uint8Array data type is supported. 242 */ 243 weasel.Helper.getUnsignedByteArray = function( iSize ) 244 { 245 if( weasel.Helper._bUint8ArrayAvailable ) 246 return new window.Uint8Array( iSize ); 247 248 return new Array( iSize ); 249 }; 250 251 252 // --------------------------------------------------------------------------- 253 /** 254 * Initialise the Easy Audio player with a Ultimate Soundtracker Module. This is 255 * the easiest and simplest way to play a module, if your not too concerned about 256 * performance or the finer points of controlling the playback. Use the Helper.easyStart() 257 * to start playing the module and Helper.easyStop() to stop/pause the module. 258 * 259 * 260 * @param {Array|Uint8Array|Element|String} xModuleData = The Ultimate Soundtracker Module is various different data formats, A) In binary as an Array of bytes; B) Contained within a HTML Element as a Base64 encoded string; C).As a Base64 encoded String. IF the 3rd Party (LGPL 3) JXG decompression library is available for use then .gz and .zip modules are allowed. 261 * 262 * @return {(String|weasel.BrowserAudio)} = A string is returned if an error occurred (containing the error message), or a weasel.BrowserAudio object ready for use upon success. 263 */ 264 weasel.Helper.easyPlay = function( xModuleData ) 265 { 266 267 if( undefined == xModuleData ) 268 { 269 return '** Error, parameter xModuleData data is missing.'; 270 } 271 272 var aModuleData = null; 273 274 if( ( xModuleData instanceof Array ) || ( window.Uint8Array && xModuleData instanceof window.Uint8Array ) ) 275 { 276 aModuleData = xModuleData; 277 }else if( xModuleData instanceof Element && typeof( xModuleData ) == 'object' && xModuleData.innerHTML ) 278 { 279 aModuleData = weasel.Helper.base64Decode( xModuleData.innerHTML ); 280 281 }else if( typeof( xModuleData ) == 'string' ) 282 { 283 aModuleData = weasel.Helper.base64Decode( xModuleData ); 284 } 285 286 var bJXGLibraryPresent = weasel.Helper._detectJXGLibrary(); 287 if( bJXGLibraryPresent ) 288 { 289 aModuleData = weasel.Helper.checkForCompression( aModuleData ); 290 } 291 292 if( null == aModuleData ) 293 { 294 var sJXGLibrary = bJXGLibraryPresent ? ' The JXG decompression library has been found.' : ' The JXG decompression library has not been found.'; 295 return 'Unable to extract Module data which needs to be in one of the following formats: a) Module in an Array (already in binary format). b) A DOM Element containing the Base64 encoded Module within its innerHTML. c) Module as a Base64 encoded string.' + sJXGLibrary; 296 } 297 298 var oModuleSniffer = new weasel.ModuleSniffer(); 299 oModuleSniffer.sniff( aModuleData, false ); 300 301 if( 'ok' == oModuleSniffer.getReason() ) 302 { 303 this.iEasyPlayIntervalMsRate = 200; 304 var iReplayFrequency = 44100; 305 var iPreBufferMS = 300; 306 307 if( !this.oEasyPlayBrowserAudio ) 308 { 309 this.oEasyPlayBrowserAudio = new weasel.BrowserAudio( iReplayFrequency, this.iEasyPlayIntervalMsRate ); 310 this.oEasyPlayBrowserAudio.init(); 311 312 if( weasel.BrowserAudio.prototype.AudioType.HTML5Audio == this.oEasyPlayBrowserAudio.getAudioType() ) 313 { 314 // Switch to 16Khz playback due to HTML5 Audio being so slow (or rather slow at a time critical moment). 315 // 316 this.oEasyPlayBrowserAudio.changeReplayFrequency( 16000 ); 317 } 318 } 319 320 var oModule = oModuleSniffer.createModule( aModuleData, this.oEasyPlayBrowserAudio.getPlaybackFrequency(), weasel.Sample.prototype.SampleScannerMode.Remove_IFF_Headers ); 321 322 this.oEasyPlayBrowserAudio.setModule( oModule ); 323 this.oEasyPlayBrowserAudio.setPreBufferingInMS( iPreBufferMS ); 324 325 return this.oEasyPlayBrowserAudio; 326 } 327 328 return oModuleSniffer.getReason(); 329 }; 330 331 // --------------------------------------------------------------------------- 332 /** 333 * The audio feeder used by the Easy Audio player. 334 * 335 * @private 336 */ 337 weasel.Helper._easyPlayFeed = function( ) 338 { 339 if( weasel.Helper.oEasyPlayBrowserAudio ) 340 { 341 weasel.Helper.oEasyPlayBrowserAudio.feedAudio( ); 342 } 343 }; 344 345 // --------------------------------------------------------------------------- 346 /** 347 * Start/unpause the Easy Audio player. 348 */ 349 weasel.Helper.easyStart = function( ) 350 { 351 if( (undefined == this.iEasyPlayIntervalID || null == this.iEasyPlayIntervalID) && this.iEasyPlayIntervalMsRate ) 352 { 353 this.iEasyPlayIntervalID = setInterval( weasel.Helper._easyPlayFeed, this.iEasyPlayIntervalMsRate ); 354 355 // Re/start Audio if needed. 356 // 357 this.oEasyPlayBrowserAudio.start(); 358 } 359 }; 360 361 // --------------------------------------------------------------------------- 362 /** 363 * Stop/Pause the Easy Audio player. 364 */ 365 weasel.Helper.easyStop = function( ) 366 { 367 if( this.iEasyPlayIntervalID && this.iEasyPlayIntervalID != null ) 368 { 369 clearInterval( this.iEasyPlayIntervalID ); 370 this.iEasyPlayIntervalID = null; 371 372 this.oEasyPlayBrowserAudio.stop(); 373 } 374 375 }; 376 // --------------------------------------------------------------------------- 377 /** Decode a Base 64 encoded ASCII character code into its 6 bit value. 378 * 379 * @param {int} iCharCode = The Base64 encoded ASCII character code (65 = A, 66 = B etc). 380 * 381 * @return {int} = The decoded 6 bit value (A = 0, B = 1 etc), this also include the equals '=' character which is decoded as the value 64, which actually is 7 bits in length. 382 * 383 * @private 384 */ 385 weasel.Helper._decodeChar = function( iCharCode ) 386 { 387 if( iCharCode >= 97 ) // abcdefghijklmnopqrstuvwxyz 388 return iCharCode -71; 389 390 if( 61 == iCharCode ) // = 391 return 64; 392 393 if( 47 == iCharCode ) // / 394 return 63; 395 396 if( 43 == iCharCode ) // + 397 return 62; 398 399 if( iCharCode <= 57 ) // 0123456789 400 return 4 + iCharCode; 401 402 return iCharCode -65; // ABCDEFGHIJKLMNOPQRSTUVWXYZ 403 }; 404 405 // --------------------------------------------------------------------------- 406 /** Decode a base64 string into a binary array. This is the preferred method to 407 * decode Base64 encoded data as unfortunately window.atob() is not supported by 408 * all browsers (looking at you IE). It does have the advantage of decoding directly 409 * to an array instead of a string. 410 * 411 * @param {String} sEncodedString = The Base64 encoded string. 412 * 413 * @return {Array|Uint8Array} = Array containing the decoded byte data. 414 */ 415 weasel.Helper.base64Decode = function( sEncodedString ) 416 { 417 if( undefined == sEncodedString ) 418 { 419 return new Array(); 420 } 421 422 if( 'string' !== typeof sEncodedString ) 423 { 424 return new Array(); 425 } 426 427 // Ignore any non legal characters a la RFC 2045. (Base64 originally is on multiple lines anyway). 428 // 429 sEncodedString = sEncodedString.replace(/[^a-zA-Z0-9\+\/\=]/g, '' ); 430 431 var iDecodedLength = (sEncodedString.length >>> 2) * 3; 432 if( 61 == sEncodedString.charCodeAt( sEncodedString.length -2 ) ) 433 { 434 iDecodedLength -= 2; 435 } 436 else if( 61 == sEncodedString.charCodeAt( sEncodedString.length -1 ) ) 437 { 438 iDecodedLength--; 439 } 440 441 var aDecoded = weasel.Helper.getUnsignedByteArray( iDecodedLength ); 442 var iWriteOffset = 0; 443 var iOffset = 0; 444 var fDecodeChar = weasel.Helper._decodeChar; 445 446 for( var iLength = ((sEncodedString.length >>> 2 ) << 2) -4; iOffset < iLength; ) 447 { 448 var i24BitGroup = (fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ) << 18) | (fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ) << 12) | (fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ) << 6) | fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ); 449 aDecoded[ iWriteOffset++ ] = i24BitGroup >>> 16; 450 aDecoded[ iWriteOffset++ ] = (i24BitGroup >>> 8 ) & 0xff; 451 aDecoded[ iWriteOffset++ ] = i24BitGroup & 0xff; 452 } 453 454 // Decode last 4 characters, which may contain the '=' or '=='. 455 // 456 { 457 var iToken1 = fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ); 458 var iToken2 = fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ); 459 var iToken3 = fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ); 460 var iToken4 = fDecodeChar( sEncodedString.charCodeAt( iOffset++ ) ); 461 462 var i24BitGroup = (iToken1 << 18) | (iToken2 << 12) | (iToken3 << 6) | iToken4; 463 464 aDecoded[ iWriteOffset++ ] = i24BitGroup >>> 16; 465 466 if( 64 != iToken3 ) 467 { 468 aDecoded[ iWriteOffset++ ] = (i24BitGroup >>> 8 ) & 0xff; 469 470 if( 64 != iToken4 ) 471 { 472 aDecoded[ iWriteOffset++ ] = i24BitGroup & 0xff; 473 } 474 } 475 } 476 477 return aDecoded; 478 }; 479 480 // --------------------------------------------------------------------------- 481 /** Check to see if the High resolution timer exists in this browser. 482 * 483 * @return {bool} = true : High resolution timer available. 484 * 485 * @private 486 */ 487 weasel.Helper._detectHighResolutionTimer = function( ) 488 { 489 return window.performance ? ( window.performance.now ? true : false ) : false; 490 }; 491 492 // --------------------------------------------------------------------------- 493 /** Use the High resolution timer (its quicker than checking window.performance and 494 * window.performance.now every time you want to use it). 495 * @type {boolean} 496 * @private 497 */ 498 weasel.Helper._bHighResolutionTimerAvaiable = weasel.Helper._detectHighResolutionTimer(); 499 500 // --------------------------------------------------------------------------- 501 /** Get the value of the high resolution timer which has microsecond accuracy, if available. Returns value in 502 * milliseconds if not. 503 * 504 * @return {int|DOMHighResTimeStamp} = Current value of timer in milliseconds. 505 */ 506 weasel.Helper.getHighRezTimer = function( ) 507 { 508 if( weasel.Helper._bHighResolutionTimerAvaiable ) 509 { 510 return window.performance.now(); 511 } 512 513 return Date.now(); 514 }; 515 516 // --------------------------------------------------------------------------- 517 /** 518 * Search for a ASCII string in a byte array from a given offset, used to find 519 * IFF sample headers. 520 * 521 * @param {Array} aData = The array of bytes to search. 522 * @param {int} iStartOffset = The starting offset to search from. 523 * @param {int} iEndOffset = The ending offset to search too. 524 * @param {string} sString = The string to look for (case sensitive). 525 * 526 * @return {int} = The offset of the match, or -1 if not found. 527 */ 528 weasel.Helper.searchArrayForString = function( aData, iStartOffset, iEndOffset, sString ) 529 { 530 var aMatch = weasel.Helper.convertStringToArray( sString ); 531 var iMatchLength = aMatch.length; 532 533 if( iStartOffset < 0 ) 534 { 535 iStartOffset = 0; 536 } 537 538 if( aData.length < iEndOffset ) 539 { 540 iEndOffset = aData.length; 541 } 542 543 // Due to JavaScript 1.6 still not being available in all browsers search manually. 544 // 545 for( var iFirstMatch = aMatch[ 0 ]; iStartOffset < iEndOffset; iStartOffset++ ) 546 { 547 if( iFirstMatch == weasel.Helper.getByte( aData, iStartOffset ) ) 548 { 549 var iMatch = 1; 550 551 for( var iOffset = iStartOffset + 1; iOffset < iEndOffset && iMatch < iMatchLength; iOffset++, iMatch++ ) 552 { 553 if( aMatch[ iMatch ] != weasel.Helper.getByte( aData, iOffset ) ) 554 { 555 break; 556 } 557 } 558 559 if( iMatch == iMatchLength ) 560 { 561 return iStartOffset; 562 } 563 } 564 } 565 566 return -1; 567 }; 568 569 //--------------------------------------------------------------------------- 570 /** Detect if the JXG.Util.Unzip decompression library for .gz and .zip files is available for use. 571 * 572 * @return {bool} = True the decompression library is found, false it is not available. 573 * 574 * @private 575 */ 576 weasel.Helper._detectJXGLibrary = function( ) 577 { 578 if( undefined != window.JXG && undefined != window.JXG.Util && undefined != window.JXG.Util.Unzip ) 579 { 580 return true; 581 } 582 583 return false; 584 }; 585 586 //--------------------------------------------------------------------------- 587 /** Decompress the module if needed. Requires the JXG.Util.Unzip library, which is LGPL 3 and 588 * can be located here: http://jsxgraph.uni-bayreuth.de/wp/2009/09/29/jsxcompressor-zlib-compressed-javascript-code/ 589 * 590 * @param {Array|Uint8Array} aModuleData = The un/compressed module data. 591 * 592 * @return {Array|Uint8Array} = The decompressed module if compressed. If not the data returned is that which was passed in. 593 */ 594 weasel.Helper.checkForCompression = function( aModuleData ) 595 { 596 // Decompress module if JXG.Util.Unzip library exists. 597 // 598 if( weasel.Helper._detectJXGLibrary() ) 599 { 600 try 601 { 602 // .gz files have a MAGIC of 0x1f8b for the first two byes of the file. 603 // .zip files have a MAGIC of 0x504b0304 for the first four bytes of a file. 604 // 605 if( 0 == weasel.Helper.searchArrayForString( aModuleData, 0 ,2, '\x1f\x8b' ) || 0 == weasel.Helper.searchArrayForString( aModuleData, 0 ,4, '\x50\x4b\x03\x04' ) ) 606 { 607 // Decompress module. 608 // JXG causes lots of garbage to be GC'ed. 609 // 610 var oCompressed = new JXG.Util.Unzip( aModuleData ); 611 aModuleData = weasel.Helper.convertStringToArray( oCompressed.unzip()[0][0] ); 612 } 613 }catch( oException ) 614 { 615 } 616 } 617 618 return aModuleData; 619 }; 620