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