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