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 /** Create a TakeTracker/FastTracker Module (a 2-32 channel M.K. module from TakeTracker/FastTracker/DigiBoost) format out of the provided data (which has already passed the module sniffer test).
  9  * 
 10  * @constructor
 11  * @extends weasel.ProTrackerMK
 12  * 
 13  * @param {Array|Uint8Array} aModuleData = The TakeTracker/FastTracker Module module as a byte array that MUST have passed the module sniffer test.
 14  * @param {int} iPlaybackFrequency = The playback frequency in hertz to use (e.g. 44100 ).
 15  * @param {weasel.Sample.prototype.SampleScannerMode} iSampleScannerMode = Scan for IFF Header corruption residue?.
 16  * 
 17  * @author Warren Willmey 2013
 18  */
 19 weasel.FSTModule = function( aModuleData, iPlaybackFrequency, iSampleScannerMode )
 20 {
 21 	this.parent = weasel.ProTrackerMK;
 22 
 23 	// Needed for prototype Inheritance.
 24 	//
 25 	if( aModuleData === undefined || !(( aModuleData instanceof Array ) || ( window.Uint8Array && aModuleData instanceof Uint8Array )) )
 26 		return;
 27 	
 28 	// Convert Module finger print into number of channels.
 29 	//
 30 	this.iNumberOfChannels = 4;
 31 
 32 	if( -1 != weasel.Helper.searchArrayForString( aModuleData, weasel.FormatSpreadpointSoundTracker23.MKFingerPrint + 1, weasel.FormatSpreadpointSoundTracker23.MKFingerPrint + 4, 'CHN' )  )
 33 	{
 34 		this.iNumberOfChannels = weasel.Helper.getByte( aModuleData, weasel.FormatSpreadpointSoundTracker23.MKFingerPrint ) - 48;
 35 	}
 36 	else
 37 	{
 38 		this.iNumberOfChannels = ((weasel.Helper.getByte( aModuleData, weasel.FormatSpreadpointSoundTracker23.MKFingerPrint ) - 48) * 10) + ( weasel.Helper.getByte( aModuleData, weasel.FormatSpreadpointSoundTracker23.MKFingerPrint + 1 ) - 48);
 39 	}
 40 
 41 	this.bNoiseTrackerLoopQuirk = false;
 42 	this.bFSTClearSampleOffsetAfterUse = true;
 43 
 44 	this.parent( aModuleData, iPlaybackFrequency, iSampleScannerMode );
 45 	this.setProtracker3SampleOffsetMode( true );
 46 	this.setProtrackerTremoloSawtoothBugMode( true );
 47 	this.bDontStopDMAQuirk = false;
 48 
 49 	this.sModuleType = weasel.ModuleSniffer.prototype.SupportedModules.FSTModule;
 50 };
 51 
 52 weasel.FSTModule.prototype = new weasel.ProTrackerMK;
 53 
 54 
 55 // ---------------------------------------------------------------------------
 56 /** Get the number of channels in this module.
 57  * 
 58  * @return {int} The number of channels in this module.
 59  * 
 60  * @override
 61  */
 62 weasel.FSTModule.prototype.getNumberOfChannels = function( )
 63 {
 64 	return this.iNumberOfChannels;
 65 };
 66 
 67 // ---------------------------------------------------------------------------
 68 /** Get the pattern size in bytes of this module.
 69  * 
 70  * @return {int} The size of each pattern in bytes for this module type.
 71  * 
 72  * @override
 73  */
 74 weasel.FSTModule.prototype.getPatternSizeInBytes = function( )
 75 {
 76 	return this.getNumberOfChannels() * weasel.FormatUltimateSoundTracker121.BytesPerRowCell * weasel.FormatUltimateSoundTracker121.NumberOfRowsPerPattern;
 77 };
 78 
 79 // ---------------------------------------------------------------------------
 80 /** During pitch bends TakeTracker/FastTracker Modules clamps the values to the ~8 octave range,
 81  * Max note period in a 12 bit value.
 82  * TakeTracker has a 6 Octave range so could (will) potentially be different.
 83  * FT2 allows you to use 13 bit note period values when the sample number is > 15 though..
 84  * 
 85  * @param {int} iNotePeriod = The note period to clamp.
 86  * 
 87  * @return {int} The clamped period value.
 88  * 
 89  * @private
 90  */
 91 weasel.FSTModule.prototype._clampNotePeriod = function( iNotePeriod )
 92 {
 93 	if( iNotePeriod > 4064 )
 94 	{
 95 		return 4064;
 96 	}
 97 	else if( iNotePeriod < 28 )
 98 	{
 99 		return 28;
100 	}
101 
102 	return iNotePeriod;
103 };
104 
105 // ---------------------------------------------------------------------------
106 /** Taketracker/Fasttracker allow Protracker systle fine tuning but have a larger number
107  * of octaves to cover.
108  * As period table for 8 octaves range with finetune would be huge use a function
109  * instead. Lars Hamre defines the Protracker Finetune as -8 to +7 * 1/8th of a semi-tone (a note) 
110  * 		Note Period * ( 2^( -FineTune / 12 / 8 ) ).
111  *
112  * @param {weasel.Channel} oChannel = The channel object to start its pending sample.
113  * @param {int} iNotePeriod = The note period yet to be set for this oChannel object.
114  *
115  * @return {int} =	The note period corrected for fine tuning.
116  * 
117  * @protected
118  * @override
119  */
120 weasel.FSTModule.prototype._fineTune = function( oChannel, iNotePeriod )
121 {
122 	var iFineTune = oChannel.getFineTune();
123 
124 	if( 0 == iFineTune )
125 	{
126 		return iNotePeriod;
127 	}
128 
129 	if( 0 == iNotePeriod )
130 	{
131 		return 0;
132 	}
133 
134 	// Clamp Fine Tuning, just in case.
135 	//
136 	if( iFineTune < 0 )
137 	{
138 		iFineTune = 0;
139 	}
140 	else if( iFineTune > 15 )
141 	{
142 		iFineTune = 15;
143 	}
144 
145 	// Convert to two-complement, correct to -8 to 7 range.
146 	//
147 	if( iFineTune > 7 )
148 	{
149 		iFineTune = -16 + iFineTune;
150 	}
151 
152 	var iNotePeriod = Math.round( iNotePeriod * Math.pow( 2.0, ( -iFineTune / 12.0 / 8.0 ) ) );
153 
154 	var aPeriodTable = weasel.FormatFSTModule.FSTPeriodTable;
155 	if( iNotePeriod < aPeriodTable[ aPeriodTable.length -1 ] )
156 	{
157 		iNotePeriod = aPeriodTable[ aPeriodTable.length -1 ];
158 	}
159 
160 	if( iNotePeriod > aPeriodTable[ 0 ] )
161 	{
162 		iNotePeriod = aPeriodTable[ 0 ];
163 	}
164 
165 	return iNotePeriod
166 };
167 
168 
169 //---------------------------------------------------------------------------
170 /** Taketracker/Fasttracker get the note period value with fine tuning applied.
171 *
172 * @param {weasel.Channel} oChannel = The channel object to start its pending sample.
173 * @param {int} iNotePeriod = The note period yet to be set for this oChannel object.
174 *
175 * @return {int} = The note period corrected for fine tuning.
176 * 
177 * @public
178 */
179 weasel.FSTModule.prototype.calcFineTune = function( oChannel, iNotePeriod )
180 {
181 	return this._fineTune( oChannel, iNotePeriod );
182 };
183 
184 
185 // ---------------------------------------------------------------------------
186 /** Process a channel's effects that occur on Tick Zero.
187  * 
188  * @param {weasel.Channel} oChannel = The Channel to process for effects.
189  * 
190  * @protected
191  * @override
192  */
193 weasel.FSTModule.prototype._processChannelTick0Effect = function( oChannel )
194 {
195 	var iEffectParameter = oChannel.getEffectParameter();
196 
197 	switch( oChannel.getEffectNumber() )
198 	{
199 		case weasel.FormatFSTModule.Effects.Panning:
200 
201 			// Panning Position.
202 			//
203 			var iPanning = iEffectParameter;
204 			if( this.getFSTPanningMode() )
205 			{
206 				// 7 Bit Panning module in use, double value.
207 				//
208 				iPanning *= 2;
209 			}
210 
211 			oChannel.setPanningPosition( iPanning );
212 		break;
213 
214 		case weasel.FormatDOCSoundTracker22.Effects.SequencePositionJump :
215 
216 				// If Pattern Break command occurs in any other channel, this command
217 				// turns it into a Sequence Position Jump command instead.
218 				//
219 				var iSequenceJump = iEffectParameter;
220 
221 				this.bPatternBreak = true;
222 				this.iPatternBreakToRow = 0;
223 				this.iSequencePositionJump = iSequenceJump;
224 			break;
225 
226 		case weasel.FormatDOCSoundTracker9.Effects.Volume :
227 
228 				var iVolume = iEffectParameter;
229 
230 				if( iVolume > 64 )
231 				{
232 					iVolume = 64;
233 				}
234 
235 				oChannel.setVolume( iVolume );
236 
237 			break;
238 
239 		case weasel.FormatDOCSoundTracker22.Effects.PatternBreak :
240 
241 				// Pattern break.
242 				//
243 				var iRowJump = ((iEffectParameter >>> 4) * 10) + (iEffectParameter & 0xf );
244 
245 				if( iRowJump > 63 )
246 				{
247 					iRowJump = 0;
248 				}
249 
250 				this.bPatternBreak = true;
251 				this.iPatternBreakToRow = iRowJump;
252 			break;
253 
254 		case weasel.FormatProTrackerMK.Effects.ExtendedCommands :
255 
256 				this._processChannelTick0ExtendedEffect( oChannel );
257 			break;
258 
259 		case weasel.FormatDOCSoundTracker9.Effects.TickSpeed :
260 
261 				if( this.getTimingOverride() == this.TimingOverrides.UseBPM )
262 				{
263 					// BPM/CIA replay mode.
264 					//
265 					if( iEffectParameter < 32 )
266 					{
267 						// Change Tick Speed.
268 						//
269 						this.setTickSpeed( iEffectParameter );
270 					}
271 					else
272 					{
273 						// Change tempo/BPM speed.
274 						//
275 						this.setSongSpeed( iEffectParameter );
276 					}
277 				}
278 				else
279 				{
280 					// VBL replay mode.
281 					// There is an undocumented side of Protracker in VBL mode
282 					// which allows setting the Tick Speed in the range 1-255!
283 					// 0 is ignored.
284 					if( iEffectParameter > 0 )
285 					{
286 						// Change Tick Speed to 1-255 range.
287 						//
288 						this.setTickSpeed( iEffectParameter );
289 					}
290 				}
291 			break;
292 
293 		default :
294 				// Restore the (shadow) note period for all other effects which are not processed on this tick.
295 				//
296 				oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
297 			break;
298 	}
299 };
300 
301 // ---------------------------------------------------------------------------
302 /** Process a channel's Extended Effect Commands that occur on Tick Zero.
303  * 
304  * @param {weasel.Channel} oChannel = The Channel to process for effects.
305  * 
306  * @protected
307  */
308 weasel.FSTModule.prototype._processChannelTick0ExtendedEffect = function( oChannel )
309 {
310 
311 	var iExtendedEffectParameter = oChannel.getEffectParameter() & 0xf;
312 
313 	switch( oChannel.getEffectParameter() & 0xf0 )
314 	{
315 		case weasel.FormatProTrackerMK.Effects.Filter:
316 				// Ignore Filter.
317 			break;
318 
319 			case weasel.FormatProTrackerMK.Effects.FineSlideUp:
320 				if( this.iRowDelay == 0 )
321 				{
322 					// FT2 only applies FineSlideUp once if row is delayed (not like Protracker).
323 					//
324 					oChannel.pitchBend( 1, 0, iExtendedEffectParameter );
325 					oChannel.setShadowNotePeriod( this._clampNotePeriod( oChannel.getShadowNotePeriod() ) );
326 					oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
327 				}
328 			break;
329 
330 			case weasel.FormatProTrackerMK.Effects.FineSlideDown:
331 				if( this.iRowDelay == 0 )
332 				{
333 					// FT2 only applies FineSlideDown once if row is delayed (not like Protracker).
334 					//
335 					oChannel.pitchBend( 1, iExtendedEffectParameter, 0 );
336 					oChannel.setShadowNotePeriod( this._clampNotePeriod( oChannel.getShadowNotePeriod() ) );
337 					oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
338 				}
339 			break;
340 
341 			case weasel.FormatProTrackerMK.Effects.GlissandoControl:
342 				oChannel.setGlissando( iExtendedEffectParameter != 0 );
343 			break;
344 
345 			case weasel.FormatProTrackerMK.Effects.SetVibratoWaveform:
346 				var bContinueVibratoWaveform = (iExtendedEffectParameter & 0x4) != 0 ? true : false;
347 				var iVibratoWaveformType = 0;
348 
349 				switch( iExtendedEffectParameter & 0x3 )
350 				{
351 					case weasel.Channel.prototype.ProtrackerVibratoWaveform.SineWave:
352 						iVibratoWaveformType = weasel.Channel.prototype.ProtrackerVibratoWaveform.SineWave;
353 					break;
354 
355 					case weasel.Channel.prototype.ProtrackerVibratoWaveform.RampDownSawTooth:
356 						iVibratoWaveformType = weasel.Channel.prototype.ProtrackerVibratoWaveform.RampDownSawTooth;
357 					break;
358 
359 					default:
360 						iVibratoWaveformType = weasel.Channel.prototype.ProtrackerVibratoWaveform.Square;
361 					break;
362 
363 				}
364 
365 				oChannel.setProtrackerContinueVibratoWaveform( bContinueVibratoWaveform );
366 				oChannel.setProtrackerVibratoWaveformType( iVibratoWaveformType );
367 			break;
368 
369 			case weasel.FormatProTrackerMK.Effects.PatternLoop:
370 				if( 0 == iExtendedEffectParameter )
371 				{
372 					// Set the pattern start loop point.
373 					//
374 					oChannel.setPatternRowLoopStart( this.getCurrentPatternRowPosition() );
375 				}
376 				else
377 				{
378 					if( oChannel.getPatternRowLoopCounter() <= 0 )
379 					{
380 						oChannel.setPatternRowLoopCounter( iExtendedEffectParameter );
381 					}
382 					else
383 					{
384 						oChannel.decPatternRowLoopCounter();
385 
386 						if( oChannel.getPatternRowLoopCounter() <= 0 )
387 						{
388 							// Pattern Row Loop ended, continue pattern as normal.
389 							//
390 
391 							return;
392 						}
393 					}
394 
395 					// Loop to row in current pattern.
396 					//
397 					this.bPatternBreak = true;
398 					this.iPatternBreakToRow = oChannel.getPatternRowLoopStart();
399 					this.iSequencePositionJump = this.getCurrentSequencePosition();
400 				}
401 			break;
402 
403 			case weasel.FormatProTrackerMK.Effects.SetTremoloWaveform:
404 				var bContinueTremoloWaveform = (iExtendedEffectParameter & 0x4) != 0 ? true : false;
405 				var iTremoloWaveformType = 0;
406 
407 				switch( iExtendedEffectParameter & 0x3 )
408 				{
409 					case weasel.Channel.prototype.ProtrackerTremoloWaveform.SineWave:
410 						iTremoloWaveformType = weasel.Channel.prototype.ProtrackerTremoloWaveform.SineWave;
411 					break;
412 
413 					case weasel.Channel.prototype.ProtrackerTremoloWaveform.RampDownSawTooth:
414 						iTremoloWaveformType = weasel.Channel.prototype.ProtrackerTremoloWaveform.RampDownSawTooth;
415 					break;
416 
417 					default:
418 						iTremoloWaveformType = weasel.Channel.prototype.ProtrackerTremoloWaveform.Square;
419 					break;
420 
421 				}
422 
423 				oChannel.setProtrackerContinueTremoloWaveform( bContinueTremoloWaveform );
424 				oChannel.setProtrackerTremoloWaveformType( iTremoloWaveformType );
425 			break;
426 
427 			case weasel.FormatFSTModule.Effects.CoarsePanning:
428 				// Panning Position.
429 				// Gravis Ultrasound has 4bit panning, 0 = full left, 15 = full right
430 				// BUT converting 4 bit to 8bit cause wierdness, 15 * 16 = 240, not 255.
431 				// So use: 4bit panning * (256/15).
432 				//
433 				var iPanning = (iExtendedEffectParameter * ( 256 / 15 )) |0;
434 				oChannel.setPanningPosition( iPanning );
435 			break;
436 
437 			case weasel.FormatProTrackerMK.Effects.RetriggerNote:
438 				// Re-trigger Note can also occur on Tick 0, but only if there is NO note!
439 				//
440 				if( 0 == oChannel.getCurrentPatternCell().getNotePeriod() )
441 				{
442 					oChannel.retriggerNote( 0, iExtendedEffectParameter, 0 );
443 				}
444 			break;
445 
446 			case weasel.FormatProTrackerMK.Effects.FineVolumeSlideUp:
447 				if( this.iRowDelay == 0 )
448 				{
449 					// FT2 only applies FineVolumeSlideUp once if row is delayed (not like Protracker).
450 					//
451 					var iVolume = oChannel.getShadowVolume()+ iExtendedEffectParameter;
452 
453 					if( iVolume > 64 )
454 					{
455 						iVolume = 64;
456 					}
457 					oChannel.setVolume( iVolume );
458 				}
459 			break;
460 
461 			case weasel.FormatProTrackerMK.Effects.FineVolumeSlideDown:
462 				if( this.iRowDelay == 0 )
463 				{
464 					// FT2 only applies FineVolumeSlideDown once if row is delayed (not like Protracker).
465 					//
466 					var iVolume = oChannel.getShadowVolume() - iExtendedEffectParameter;
467 
468 					if( iVolume < 0 )
469 					{
470 						iVolume = 0;
471 					}
472 					oChannel.setVolume( iVolume );
473 				}
474 			break;
475 
476 			case weasel.FormatProTrackerMK.Effects.NoteCut:
477 				// Note Cut can also occur on Tick 0.
478 				//
479 				if( this.getCurrentTick() == iExtendedEffectParameter )
480 				{
481 					oChannel.setVolume( 0 );
482 				}
483 			break;
484 
485 			case weasel.FormatProTrackerMK.Effects.PatternDelay:
486 				if( this.iRowDelay == 0 )
487 				{
488 					this.bProtrackerRowDelayQuirk = true;
489 					this.iRowDelay = iExtendedEffectParameter + 1;
490 				}
491 			break;
492 
493 			case weasel.FormatProTrackerMK.Effects.InvertLoop:
494 
495 				oChannel.setProtrackerInvertLoopSpeed( iExtendedEffectParameter );
496 
497 				// Notice that on Tick Zero updateProtrackerInvertLoop() can get
498 				// called twice IF the Extended Command is present in the current cell.
499 				// BUT on further inspection it transpires that the Protracker (2.3a)
500 				// Editor does not do this. There appears to be a difference between
501 				// the Editor code and the Stand Alone Replay source code, with the
502 				// Stand Alone Replay code calls updateProtrackerInvertLoop() twice.
503 				// So decided to go with the Editor.
504 				//
505 				oChannel.updateProtrackerInvertLoop();
506 			break;
507 
508 	}
509 };
510 
511 //---------------------------------------------------------------------------
512 /** Arpeggio effect for Fasttracker is slightly different from Protracker as
513  * there is no zero note period between finetune tables so no note delay bug effect possible. 
514  * 
515  * @param {weasel.Channel} oChannel = The Channel to process.
516  * 
517  * @protected
518  * @override
519  */
520 weasel.FSTModule.prototype._arpeggio = function( oChannel )
521 {
522 	if( oChannel.getEffectParameter() == 0 )
523 	{
524 		// Restore the (shadow) note period when no effect occurs.
525 		//
526 		oChannel.setNotePeriod( oChannel.getShadowNotePeriod() );
527 		return;
528 	}
529 
530 	oChannel.arpeggio( this.iCurrentTick, weasel.Channel.prototype.ArpeggioMode.FSTModule );
531 };
532 
533 //---------------------------------------------------------------------------
534 /**
535  * Get the note and octave from the period value (which is stored in the pattern)
536  * Taketracker/Fastracker has an additional 3 octaves over Ultimate Soundtracker.
537  * 
538  * @param {int} iNotePeriod = The Period value of the note.
539  * 
540  * @return {String} = The note and octave (in the format note octave e.g. 'C-2' or 'G#1'), or '???' if not found.
541  * 
542  * @TODO Should we be returning the nearest note if not found??
543  */
544 weasel.FSTModule.prototype.getNoteFromPeriod = function( iNotePeriod )
545 {
546 	if( iNotePeriod < 28 || iNotePeriod > 6848 )
547 		return '???';
548 
549 	if( iNotePeriod in weasel.FormatFSTModule.NoteTable )
550 	{
551 		return weasel.FormatFSTModule.NoteTable[ iNotePeriod ];
552 	}
553 
554 	// Should we be returning the nearest note if not found??
555 	//
556 	return '???';
557 };
558 
559 
560 //---------------------------------------------------------------------------
561 /**
562  * Play this Fasttracker/Taketracker module into the given AudioBuffer object.
563  * 
564  * @param {weasel.AudioBuffer} oAudioBuffer = The AudioBuffer object to render to.
565  * @param {bool} bIgnoreFilter = Not used in Fasttracker/Taketracker so is always ignored.
566  * @param {int} iSamples = [optional] The number of samples to make this frame, usually the remaining samples to fill in the oAudioBuffer object.
567  */
568 weasel.FSTModule.prototype.play = function( oAudioBuffer, bIgnoreFilter, iSamples )
569 {
570 	var iSamplesToFill = oAudioBuffer.samplesToFill();
571 	var iNumberOfChannels = this.getNumberOfChannels();
572 	var oChannel1 = this.getChannel( 0 );
573 	var iChannelBufferLength = oChannel1.getCircularAudioBuffer().length;
574 
575 	if( undefined !== iSamples )
576 	{
577 		if( iSamples < iSamplesToFill && iSamples >= 0 )
578 		{
579 			iSamplesToFill = iSamples;
580 		}
581 	}
582 
583 	var iStartingCircularBufferOffset = oChannel1.getCircularBufferPosition();
584 
585 	for( ; iSamplesToFill > 0; )
586 	{
587 		var iSamplesTillPatternProcess = this.iSamplesRemaining;
588 	
589 		if( iSamplesTillPatternProcess > iSamplesToFill )
590 		{
591 			iSamplesTillPatternProcess = iSamplesToFill;
592 		}
593 
594 		var iCurrentCircularBufferOffset = oChannel1.getCircularBufferPosition();
595 		for( var iChannel = 0; iChannel < iNumberOfChannels; iChannel++ )
596 		{
597 			this.getChannel( iChannel ).make( iSamplesTillPatternProcess );
598 		}
599 
600 		var iSamplesMade = ((oChannel1.getCircularBufferPosition() + iChannelBufferLength ) - iCurrentCircularBufferOffset) % iChannelBufferLength;
601 
602 		// TakeTracker/FastTracker module mix channels one at a time.
603 		//
604 		oAudioBuffer.clear( iSamplesMade );
605 
606 		for( var iChannel = 0; iChannel < iNumberOfChannels; iChannel++ )
607 		{
608 			oAudioBuffer.mixIn( iSamplesMade, this.getChannel( iChannel ).getCircularAudioBuffer(), iCurrentCircularBufferOffset, this.getChannel( iChannel ).getPanningPosition() );
609 		}
610 
611 		// Clipping sample range to -1 to +1 is also handled by the browser...
612 		// So were are just replicating and wasting processor time.
613 		// There is bound to be a Browser that doesn't do this though!
614 		//
615 		if( oAudioBuffer.SupportedMixerTypes.RLM$D_Fat == oAudioBuffer.getMixerType() )
616 		{
617 			oAudioBuffer.clipRLMDFat( iSamplesMade );
618 		}
619 		else
620 		{
621 			oAudioBuffer.clip( iSamplesMade );
622 		}
623 
624 		this.iSamplesRemaining -= iSamplesTillPatternProcess;
625 		iSamplesToFill -= iSamplesTillPatternProcess;
626 
627 		// Time to process Pattern data?
628 		//
629 		if( this.iSamplesRemaining <= 0 )
630 		{
631 			this.processPattern();
632 			this.iSamplesRemaining = this.iSamplesPerTick;
633 		}
634 	}
635 };
636