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 /** The Sample object contains the raw sample data needed by the instrument.
  9  * 
 10  * @constructor
 11  * @param {weasel.Instrument} oInstrument = The Instrument object this sample will be associated with,
 12  * @param {weasel.UltimateSoundTracker121|weasel.UltimateSoundTracker18|weasel.DOCSoundTracker9|weasel.DOCSoundTracker22|weasel.TJCSoundTracker2|weasel.DefJamSoundTracker3|weasel.SpreadpointSoundTracker23|weasel.SpreadpointSoundTracker25|weasel.NoiseTracker11|weasel.NoiseTracker20|weasel.ProTrackerMK|weasel.FSTModule} oModule = The module containing the sample data.
 13  * @param {int} iSampleNumber = The number of the sample that will be used/extracted.
 14  * @param {weasel.Sample.prototype.SampleScannerMode} iSampleScannerMode = Scan for IFF Header corruption residue?.
 15  * 
 16  * @author Warren Willmey 2011.
 17  */
 18 weasel.Sample = function( oInstrument, oModule, iSampleNumber, iSampleScannerMode ){
 19 	this.iLength = 0;
 20 	this.bLooped = false;
 21 	this.iLoopStart = 0;
 22 	this.iLoopLength = 0;
 23 	this.bIFFHeaderCleaned = false;
 24 	this.aSampleData = [ 0.0 ];
 25 	this.iBarrelLoopSize = 0;
 26 	this.iFineTuning = 0;
 27 	this.bNoisetrackerLoopQuirkMode = false;
 28 	this.bSampleLoopedYet = false;
 29 
 30 	this.__extractSampleFromMod( oInstrument, oModule, iSampleNumber, iSampleScannerMode );
 31 };
 32 
 33 // ---------------------------------------------------------------------------
 34 /** Different modes for dealing with scanning for IFF header corruption residue,
 35  * display names where '$' is converted to '-' and '_' becomes ' '.
 36  * @const
 37  * @enum {int}
 38  */
 39 weasel.Sample.prototype.SampleScannerMode = {
 40 		  Do_Not_Scan			: 1
 41 		, Clear_IFF_Headers		: 2
 42 		, Remove_IFF_Headers	: 3
 43 };
 44 
 45 // ---------------------------------------------------------------------------
 46 /** The default size of a looped sample's barrel loop (used to increase the 
 47  * performance [speed] of play a looped sample at the cost of memory). Does not
 48  * apply to non-looped samples.
 49  *
 50  * @const
 51  * @enum {int}
 52  */
 53 weasel.Sample.prototype.BarrelLoopSize = 1024;
 54 
 55 //---------------------------------------------------------------------------
 56 /**
 57  * Get list of supported Scanner Modes, used for populating a drop down list for user selection.
 58  * 
 59  * @return {weasel.Sample.prototype.SampleScannerMode} = The list of supported Scanner Modes.
 60  */
 61 weasel.Sample.prototype.getSampleScannerModes = function()
 62 {
 63 	return weasel.Sample.prototype.SampleScannerMode;
 64 };
 65 
 66 // ---------------------------------------------------------------------------
 67 /**
 68  * Extract the sample needed by the Instrument from the module data into this Sample object.
 69  * 
 70  * @param {weasel.Instrument} oInstrument = The Instrument needing this Sample object.
 71  * @param {weasel.UltimateSoundTracker121|weasel.UltimateSoundTracker18|weasel.DOCSoundTracker9|weasel.DOCSoundTracker22|weasel.TJCSoundTracker2|weasel.DefJamSoundTracker3|weasel.SpreadpointSoundTracker23|weasel.SpreadpointSoundTracker25|weasel.NoiseTracker11|weasel.NoiseTracker20|weasel.ProTrackerMK|weasel.FSTModule} oModule = The Ultimate Soundtracker 1.21 Module Object.
 72  * @param {int} iSampleNumber = The number of the sample to extract.
 73  * @param {weasel.Sample.prototype.SampleScannerMode} iSampleScannerMode = Scan for IFF Header corruption residue?.
 74  * 
 75  * @private
 76  */
 77 weasel.Sample.prototype.__extractSampleFromMod = function( oInstrument, oModule, iSampleNumber, iSampleScannerMode )
 78 {
 79 	if( 0 == iSampleNumber )
 80 	{
 81 		return;
 82 	}
 83 
 84 	this.iLength	= oInstrument.getLengthInWords() * 2;
 85 	this.bLooped	= oInstrument.getLoopLengthInWords() > 1;
 86 	this.iLoopStart	= oInstrument.getLoopOffsetInBytes();
 87 	this.iLoopLength= oInstrument.getLoopLengthInWords() * 2;
 88 	this.iFineTuning= oInstrument.getFineTuning();
 89 
 90 	var aModuleData = oModule.getModuleData();
 91 	var iSampleStartingOffsetInMod = oModule.FormatModuleHeaderSize() + (oModule.numberOfUniquePatternsInSong() * oModule.getPatternSizeInBytes() );
 92 
 93 	for( var iAddEachSampleLength = 1; iAddEachSampleLength < iSampleNumber; iAddEachSampleLength++ )
 94 	{
 95 		iSampleStartingOffsetInMod += oModule.getInstrument( iAddEachSampleLength ).getLengthInWords() * 2;
 96 	}
 97 
 98 	var aSampleAsBytes = window.Uint8Array && aModuleData instanceof Uint8Array ? aModuleData.subarray( iSampleStartingOffsetInMod, iSampleStartingOffsetInMod + this.getLength() ) : aModuleData.slice( iSampleStartingOffsetInMod, iSampleStartingOffsetInMod + this.getLength() );
 99 
100 	if( weasel.Sample.prototype.SampleScannerMode.Clear_IFF_Headers == iSampleScannerMode || weasel.Sample.prototype.SampleScannerMode.Remove_IFF_Headers == iSampleScannerMode )
101 	{
102 		this.__scanForIFFHeaders( aSampleAsBytes, iSampleScannerMode );
103 	}
104 
105 	var iNewSampleDataSize = this.getLength() + 4;
106 
107 	if( this.bLooped )
108 	{
109 		this.iBarrelLoopSize = weasel.Sample.prototype.BarrelLoopSize;
110 		iNewSampleDataSize += this.iBarrelLoopSize;
111 
112 		// Noisetracker & Protracker have an additional loop mode where
113 		// the entire sample is played first and then the loop is played
114 		// but this only happens if the loop start point is set at zero.
115 		// Handle this by copying the loop to the end of the sample data if the 
116 		// loop length is not the whole sample.
117 		//
118 		if( oInstrument.isNoisetrackerInstrument() && oInstrument.useNoisetrackerLoopQuirk() && this.iLoopStart == 0 && this.iLoopLength < this.iLength )
119 		{
120 			this.bNoisetrackerLoopQuirkMode = true;
121 			iNewSampleDataSize += this.iLoopLength;
122 		}
123 	}
124 
125 	this.aSampleData = weasel.Helper.getFloat32Array( iNewSampleDataSize );
126 	
127 	try
128 	{
129 		for( var iLength = this.getLength(), aSample = this.aSampleData, iIndex = 0; --iLength >= 0; iIndex++ )
130 		{
131 			var f32BitSample = weasel.Helper.twosComplement( weasel.Helper.getByte( aSampleAsBytes, iIndex ) ) / 128.0;
132 			aSample[ iIndex ] = f32BitSample;
133 		}
134 	}catch( oException )
135 	{
136 		// Most like sample length is incorrect.
137 	}
138 
139 
140 	if( this.bLooped )
141 	{
142 		// Noisetracker & Protracker have an additional loop mode where
143 		// the entire sample is played first and then the loop is played
144 		// but this only happens if the loop start point is set at zero.
145 		// Handle this by copying the loop to the end of the sample data if the 
146 		// loop length is not the whole sample.
147 		//
148 		if( this.bNoisetrackerLoopQuirkMode )
149 		{
150 			// Copy loop to end of sample.
151 			//
152 			for( var aSample = this.aSampleData, iEndOfSample = this.iLength, iLoopStart = this.iLoopStart, iLoopLength = this.iLoopLength; --iLoopLength >= 0; )
153 			{
154 				aSample[ iEndOfSample++ ] = aSample[ iLoopStart++ ];
155 			}
156 
157 			// Relocate loop start to end of sample,
158 			// and adjust sample length to reflect this.
159 			//
160 			this.iLoopStart	 = this.iLength;
161 			this.iLength	+= this.iLoopLength;
162 		}
163 	}
164 
165 	// Copy last sample to whole of barrel loop, or copy sample loop until end of sample.
166 	//
167 	// Don't forget sound routine unfortunately access the next sample past the end of the sample, however it does not use it.
168 	// This would cause a warning (not an error so cant be caught with a try/catch block) in Firefox Strict Mode.
169 	//
170 	var iEndOffset = this.getLength();
171 	var iStartOffset = iEndOffset -1;
172 	if( iStartOffset < 0 )
173 	{
174 		iStartOffset = 0;
175 		iEndOffset = 1;
176 		this.aSampleData[ 0 ] = 0.0;
177 	}
178 
179 	if( this.isLooped() )
180 	{
181 		iStartOffset = this.getLoopStart();
182 		iEndOffset = iStartOffset + this.getLoopLength();
183 	}
184 
185 	for( var aSample = this.aSampleData, iEnd = aSample.length; iEndOffset < iEnd; )
186 	{
187 		aSample[ iEndOffset++ ] = aSample[ iStartOffset++ ];
188 	}
189 
190 	
191 	
192 };
193 
194 // ---------------------------------------------------------------------------
195 /** Scan the sample data (prior to use) for any residue of a IFF header that may
196  * have been left over accidentally.
197  * 
198  * @param {Array|Uint8Array} aSample = The sample data.
199  * @param {weasel.Sample.prototype.SampleScannerMode} iIFFRemovalMode = The type of removal to use if a IFF header is found (truncate or clean basically).
200  * 
201  * @private
202  */
203 weasel.Sample.prototype.__scanForIFFHeaders = function( aSample, iIFFRemovalMode )
204 {
205 	var iSize = aSample.length;
206 	
207 	var bFound = true;
208 
209 	var iBeginScan = 0;
210 	while( bFound )
211 	{
212 		var iHeaderFound = weasel.Helper.searchArrayForString( aSample, iBeginScan, iSize, '8SVXVHDR' );
213 
214 		if( -1 == iHeaderFound )
215 		{
216 			bFound = false;
217 		}
218 		else
219 		{
220 			// Find end of "headers", with a IFF file you are supposed to walk through the file,
221 			// each header having a 32 bit length following it.
222 			// It would be simpler to just search for "BODY", but the gazillion to one chance
223 			// that the name of the sample is BODY or the BODY chunk is not present make life more painful.
224 			//
225 
226 			// Technically the iff sample chunk header is "8SVX" and the "VHDR" 
227 			// is the actual voice chunk header, so step over the "8SVX" part,
228 			// as somewhat strangely "8SVX" does not have a length attribute.
229 			//
230 			var iNextHeader = iHeaderFound + 4;	
231 			var iHeaderSize = -1;
232 			var iZapLength = 4;
233 			var bBodyChunkFound = false;
234 			
235 			// Don't expect header to be more than 200 bytes.
236 			//
237 			while( iNextHeader < iHeaderFound + 200 )
238 			{
239 				try
240 				{
241 					// Chunk ID ("VHDR" etc).
242 					//
243 					iNextHeader += 4;
244 					iZapLength  += 4;
245 					iHeaderSize = weasel.Helper.getLong( aSample, iNextHeader );
246 
247 					// Chunk Length (4 bytes).
248 					//
249 					iNextHeader += iHeaderSize + 4;
250 					iZapLength +=  iHeaderSize + 4;
251 				}catch( oException )
252 				{
253 					// Looks like header is at end of sample, zap what we got.
254 					//
255 					// What would be the Chunk Length (4 bytes).
256 					//
257 					iNextHeader += 4;
258 					iZapLength  += 4;
259 					break;
260 				}
261 
262 				var iBodyHeader = weasel.Helper.searchArrayForString( aSample, iNextHeader, iNextHeader + 4, 'BODY' );
263 
264 
265 				if( iBodyHeader >= 0 )
266 				{
267 					// Found.
268 					//
269 					bBodyChunkFound = true;
270 					break;
271 				}
272 				
273 			}
274 
275 			if( bBodyChunkFound )
276 			{
277 				// Zap the "BODY" and its length attribute.
278 				//
279 				iZapLength += 8;
280 			}
281 			
282 			// Check to see if the "FORM" chunk length is intact, its before the "8SVX".
283 			try
284 			{
285 				if( weasel.Helper.getLong( aSample, iHeaderFound - 4 ) <= 65535 )
286 				{
287 					iHeaderFound -= 4;
288 					iZapLength += 4;
289 					
290 					if( 0 == weasel.Helper.searchArrayForString( aSample, iHeaderFound - 4, iHeaderFound, 'FORM' ) )
291 					{
292 						// Looks like the entire .iff header is actually present.
293 						//
294 						iHeaderFound -= 4;
295 						iZapLength += 4;
296 					}
297 				}
298 			}catch( oException )
299 			{
300 			}
301 
302 			if( weasel.Sample.prototype.SampleScannerMode.Remove_IFF_Headers == iIFFRemovalMode )
303 			{
304 				this.__removeIFFHeader(aSample, iHeaderFound, iZapLength );
305 			}
306 			else
307 			{
308 				this.__clearIFFHeader(aSample, iHeaderFound, iZapLength );
309 			}
310 			
311 			this.bIFFHeaderCleaned = true;
312 
313 			iBeginScan = iHeaderFound;
314 		}
315 	}
316 	
317 };
318 
319 // ---------------------------------------------------------------------------
320 /** Remove an area of sample data (used to zero out IFF sample headers that may 
321  * accidentally still be in the sample data), any space gained at the end of the
322  * sample is filled with the previous sample.
323  * 
324  * @param {Array|Uint8Array} aSample = The sample data.
325  * @param {int} iOffset = The offset into aSample at which to remove the data.
326  * @param {int} iRemoveLength = The number of samples to remove.
327  * 
328  * @private
329  */
330 weasel.Sample.prototype.__removeIFFHeader = function( aSample, iOffset, iRemoveLength )
331 {
332 	// Copy sample.
333 	//
334 	for( var iFrom = iOffset + iRemoveLength, iSize = aSample.length; iFrom < iSize; )
335 	{
336 		aSample[ iOffset++ ] = aSample[ iFrom++ ];
337 	}
338 
339 	// Fill rest of sample with last byte.
340 	//
341 	for( var iLastByte = iOffset == 0 ? aSample[ iOffset ] : aSample[ iOffset - 1], iSize = aSample.length; iOffset < iSize; )
342 	{
343 		aSample[ iOffset++ ] = iLastByte;
344 	}
345 };
346 
347 // ---------------------------------------------------------------------------
348 /** Clear an area of sample data (used to zero out IFF sample headers that may 
349  * accidentally still be in the sample data).
350  * 
351  * @param {Array|Uint8Array} aSample = The sample data.
352  * @param {int} iOffset = The offset into aSample at which to zero out the data.
353  * @param {int} iRemoveLength = The number of samples to zero out.
354  * 
355  * @private
356  */
357 weasel.Sample.prototype.__clearIFFHeader = function( aSample, iOffset, iRemoveLength )
358 {
359 	for( var iSize = aSample.length; --iRemoveLength >= 0 &&  iOffset < iSize; )
360 	{
361 		aSample[ iOffset++ ] = 0;
362 	}
363 };
364 
365 // ---------------------------------------------------------------------------
366 /** Get the length of the sample in bytes/samples.
367  * 
368  * @return {int} The length of the sample in bytes.
369  */
370 weasel.Sample.prototype.getLength = function()
371 {
372 	return this.iLength;
373 };
374 
375 // ---------------------------------------------------------------------------
376 /** Get the loop starting position in bytes/samples.
377  * 
378  * @return {int} The starting loop position offset in bytes.
379  */
380 weasel.Sample.prototype.getLoopStart = function()
381 {
382 	return this.iLoopStart;
383 };
384 
385 // ---------------------------------------------------------------------------
386 /** Get the length of the loop in bytes/samples.
387  * 
388  * @return {int} Get the loop length in bytes/samples.
389  */
390 weasel.Sample.prototype.getLoopLength = function()
391 {
392 	return this.iLoopLength;
393 };
394 
395 // ---------------------------------------------------------------------------
396 /** Check to see if this sample is looped.
397  * 
398  * @return {boolean} True if sample is looped.
399  */
400 weasel.Sample.prototype.isLooped = function()
401 {
402 	return this.bLooped;
403 };
404 
405 // ---------------------------------------------------------------------------
406 /** Get a sample value at a given offset.
407  * 
408  * @param {int} iOffset = The offset into the sample to fetch, this value is currently not bound checked.
409  * 
410  * @return {float} The sample at the requested offset, in the range of -1.0 to 1.0.
411  */
412 weasel.Sample.prototype.getSample = function( iOffset )
413 {
414 	return this.aSampleData[ iOffset ];
415 };
416 
417 // ---------------------------------------------------------------------------
418 /** Get an array containing the entire sample.
419  * 
420  * @return {Array|Float32Array} The array containing the sample in floating point format ( -1.0 to 1.0 range).
421  */
422 weasel.Sample.prototype.getSampleArray = function( )
423 {
424 	return this.aSampleData;
425 };
426 
427 
428 //---------------------------------------------------------------------------
429 /** Was sample data cleaned of IFF Headers, if any where found and 
430  * assuming the Scanner was active in the first place.
431  * 
432  * @return {boolean} True if sample has been cleaned.
433  */
434 weasel.Sample.prototype.corruptionCleaned = function()
435 {
436 	return this.bIFFHeaderCleaned;
437 };
438 
439 
440 //---------------------------------------------------------------------------
441 /** Get the size of the barrel loop attached to the end of the sample or the 
442  * end of loop (used to increase performance by avoiding regular checks for 
443  * the sample loop end).
444  * 
445  * @return {int} The size of the barrel loop in samples.
446  */
447 weasel.Sample.prototype.getBarrelLoopSize = function()
448 {
449 	return this.iBarrelLoopSize;
450 };
451 
452 // ---------------------------------------------------------------------------
453 /** Get the fine tuning value of this sample.
454  * 
455  * @return {int} The fine tuning value [ 0-15  in two complement form e.g. 8-15 are the NEGATIVE VALUES!].
456  */
457 weasel.Sample.prototype.getFineTuning = function()
458 {
459 	return this.iFineTuning;
460 };
461 
462 //---------------------------------------------------------------------------
463 /** Noisetracker introduced an odd sample looping mode, when the sample loop start
464  * is set to zero then the ENTIRE sample is played before the sample starts to loop
465  * EVEN when the loop end is not set at the end of the sample (see Mike Clarkes 
466  * Last Ninja 2 loading tune and various 4-Mat chiptunes).
467 * 
468 * @return {boolean} TRUE: this sample is affected by the Noisetracker Loop Quirk, FALSE : sample not affected.
469 */
470 weasel.Sample.prototype.getNoisetrackerLoopQuirkMode = function()
471 {
472 	return this.bNoisetrackerLoopQuirkMode;
473 };
474 
475 // ---------------------------------------------------------------------------
476 /** Invert a sample at a given offset.
477  * 
478  * @param {int} iOffset = The offset into the sample to invert.
479  * 
480  */
481 weasel.Sample.prototype.invertSample = function( iOffset )
482 {
483 	if( iOffset < 0 || iOffset >= this.aSampleData.length )
484 	{
485 		return;
486 	}
487 
488 	if( this.bLooped && (iOffset >= this.iLength || ( this.bNoisetrackerLoopQuirkMode && iOffset >= this.iLoopStart )) )
489 	{
490 		return;
491 	}
492 
493 	this.aSampleData[ iOffset ] = -this.aSampleData[ iOffset ];
494 
495 	if( this.bLooped && this.iBarrelLoopSize != 0 && ((!this.bNoisetrackerLoopQuirkMode && iOffset >= this.iLoopStart && iOffset < this.iLoopStart + this.iLoopLength) || (this.bNoisetrackerLoopQuirkMode && iOffset < this.iLoopLength) ) )
496 	{
497 		// Mirror changes in barrel loop.
498 		//
499 		var fSample = this.aSampleData[ iOffset ];
500 		var iDestination = 0;
501 		if( this.bNoisetrackerLoopQuirkMode )
502 		{
503 			// The loop start has been relocated to the end of the sample in Quirks Mode.
504 			//
505 			iDestination = iOffset + this.iLoopStart;
506 		}
507 		else
508 		{
509 			iDestination = this.iLength + (iOffset - this.iLoopStart);
510 		}
511 
512 		for( var iStep = this.iLoopLength, iSize = this.aSampleData.length; iDestination < iSize; iDestination += iStep )
513 		{
514 			this.aSampleData[ iDestination ] = fSample;
515 		}
516 	}
517 };
518 
519 
520 //---------------------------------------------------------------------------
521 /** Set the first play of the sample, if the sample is a Loop Quirk.this flag
522  * is used to indicate that the ENTIRE sample must play first before looping.
523  * 
524  * @param {boolean} bLooped = FALSE : sample has not looped yet, TRUE : sample has looped (played until the end of th sample in NT Loop Quirk mode).
525  *
526  */
527 weasel.Sample.prototype.setLoopedYet = function( bLooped )
528 {
529 	this.bSampleLoopedYet = bLooped;
530 };
531 
532 
533 //---------------------------------------------------------------------------
534 /** Has this sample looped yet?
535 */
536 weasel.Sample.prototype.getLoopedYet = function()
537 {
538 	return this.bSampleLoopedYet;
539 };
540