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