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 /** Object used to store Audio data before it gets sent to the Browser, 9 * responsible for mixing the created Channels together into stereo. 10 * 11 * @constructor 12 * @param {int} iPlaybackFrequency = The playback frequency in hertz to use. 13 * @param {int} iSoundChannels = The number of sound channels (2 = stereo, anything else will fail). 14 * @param {int} iBufferSizeInSamples = The size of the audio buffer to use in samples (so this value is the same no matter the number of sound channels). 15 * 16 * @author Warren Willmey 2011 17 */ 18 weasel.AudioBuffer = function( iPlaybackFrequency, iSoundChannels, iBufferSizeInSamples ) 19 { 20 this.iPlaybackFrequency = iPlaybackFrequency; 21 this.iSoundChannels = iSoundChannels; 22 this.aBuffer = weasel.Helper.getFloat32Array( iBufferSizeInSamples * iSoundChannels ); 23 this.aCircularAudioBuffer = weasel.Helper.getFloat32Array( iPlaybackFrequency * iSoundChannels ); 24 this.iBufferSizeInSamples = (this.aBuffer.length / iSoundChannels) | 0; 25 this.lWrittenSamples = 0; 26 this.iBufferNotFullOffset = 0; 27 this.bIgnoreCircularBuffer = true; 28 29 this.clearAudioBuffers(); 30 31 this.iCircularBufferPosition = 0; 32 33 this.iMixerType = 1; 34 this.fPanningLawDB = -4.5; // Typical panning law values are -3db (considered analogue mixing desk standard), -4.5db, -6.0db. 35 this.fPeakLeft = 0.0; 36 this.fPeakRight = 0.0; 37 /* 38 * TODO Auto gain is completely missing, currently just a low value constant to stop the majority of clipping from occurring. 39 */ 40 this.fAutoGain = 0.3; 41 42 this.bPreviousFilterState = false; 43 this.aFilterBufferLeft = weasel.Helper.getFloat32Array( 128 ); 44 this.aFilterBufferRight = weasel.Helper.getFloat32Array( 128 ); 45 this.iFilterBufferPosition = 0; 46 for( var iLength = this.aFilterBufferLeft.length, aFilterLeft = this.aFilterBufferLeft, aFilterRight = this.aFilterBufferRight; --iLength >= 0; ) 47 { 48 aFilterLeft[ iLength ] = 0.0; 49 aFilterRight[ iLength ]= 0.0; 50 } 51 }; 52 53 // --------------------------------------------------------------------------- 54 /** Supported mixer types (that may change in future), also used for 55 * display names where '$' is converted to '-' and '_' becomes ' '.. 56 * @enum 57 * @type {string} 58 */ 59 weasel.AudioBuffer.prototype.SupportedMixerTypes = { 60 Linear : 0 61 , RLM$D_Fat : 1 62 }; 63 64 // --------------------------------------------------------------------------- 65 /** 66 * Get list of supported mixer type, used for populating a drop down list for user selection. 67 * 68 * @return {weasel.AudioBuffer.prototype.SupportedMixerTypes} = The list of supported mixer types. 69 */ 70 weasel.AudioBuffer.prototype.getSupportedMixerTypes = function() 71 { 72 return weasel.AudioBuffer.prototype.SupportedMixerTypes; 73 }; 74 75 // --------------------------------------------------------------------------- 76 /** 77 * Get the number of written samples, which is the number of samples that have 78 * left this buffer and been given to the actual audio sub system (the Audio object in most cases). 79 * This does not include the samples remaining in this buffer, as they have NOT been 80 * passed to the audio sub system yet. 81 * 82 * @return {long} = The number of Samples written/passed to the audio sub system. 83 */ 84 weasel.AudioBuffer.prototype.getWrittenSamples = function() 85 { 86 return this.lWrittenSamples; 87 }; 88 89 // --------------------------------------------------------------------------- 90 /** 91 * Get the number of sound channels used in this audio buffer. 92 * 93 * @return {int} = The number of sound channels used in this buffer (1 = mono, 2 = stereo). 94 */ 95 weasel.AudioBuffer.prototype.getSoundChannels = function() 96 { 97 return this.iSoundChannels; 98 }; 99 100 // --------------------------------------------------------------------------- 101 /** 102 * Get the mixer type. 103 * 104 * @return {int} = The ID of the mixer, 0 = Linear, 1 = RLM D-Fat. 105 */ 106 weasel.AudioBuffer.prototype.getMixerType = function() 107 { 108 return this.iMixerType; 109 }; 110 111 // --------------------------------------------------------------------------- 112 /** 113 * Set the mixer type. 114 * 115 * @param {int} iMixerType = The ID of the mixer to use when mixing the 4 sound channels down to 2 stereo channels, 0 = Linear (quick), 1 = RLM D-Fat (slower). 116 */ 117 weasel.AudioBuffer.prototype.setMixerType = function( iMixerType ) 118 { 119 // Currently only 2 mixer types. 120 // 121 if( iMixerType != 1 ) 122 { 123 iMixerType = 0; 124 } 125 126 this.iMixerType = iMixerType; 127 }; 128 129 // --------------------------------------------------------------------------- 130 /** 131 * Set Ignore Circular Buffer, which records 1 seconds worth of output, only useful for 132 * oscilloscopes and (surprisingly) can be the difference between working and not on slow CPUs. 133 * 134 * @param {bool} bIgnore = true : Dont fill the circular buffer. 135 */ 136 weasel.AudioBuffer.prototype.setIgnoreCircularBuffer = function( bIgnore ) 137 { 138 this.bIgnoreCircularBuffer = bIgnore == true ? true : false; 139 }; 140 141 // --------------------------------------------------------------------------- 142 /** 143 * get Ignore Circular Buffer. 144 * 145 * @return {bool} = true : the circular buffer is not being filled. 146 */ 147 weasel.AudioBuffer.prototype.getIgnoreCircularBuffer = function() 148 { 149 return this.bIgnoreCircularBuffer; 150 }; 151 152 153 //--------------------------------------------------------------------------- 154 /** 155 * Clear the audio buffers. 156 * 157 */ 158 weasel.AudioBuffer.prototype.clearAudioBuffers = function() 159 { 160 for( var iLength = this.aBuffer.length, aBuffer = this.aBuffer; --iLength >= 0; ) 161 { 162 aBuffer[ iLength ] = 0.0; 163 } 164 165 for( var iLength = this.aCircularAudioBuffer.length, aCircularAudioBuffer = this.aCircularAudioBuffer; --iLength >= 0; ) 166 { 167 aCircularAudioBuffer[ iLength ] = 0.0; 168 } 169 }; 170 171 // --------------------------------------------------------------------------- 172 /** 173 * Inform AudioBuffer that samples were added to the audio sub system and should 174 * be removed from this AudioBuffer object. 175 * 176 * @param {long} lSamplesToRemove = The number of samples passed to the audio sub system 177 * (that is the number of samples that can now be removed), this value is in mono (ignore stereo or multiple channels). 178 */ 179 weasel.AudioBuffer.prototype.removeSamples = function( lSamplesToRemove ) 180 { 181 if( 0 == lSamplesToRemove ) 182 return; 183 184 var iDestination = 0; 185 var iBufferLengthInSamples = this.getBufferLengthInSamples(); 186 187 if( lSamplesToRemove > iBufferLengthInSamples ) 188 { 189 lSamplesToRemove = iBufferLengthInSamples; 190 } 191 192 if( !this.bIgnoreCircularBuffer ) 193 { 194 var iCircularBuffer = this.iCircularBufferPosition; 195 196 var aSource = this.aBuffer; 197 var aDest = this.aCircularAudioBuffer; 198 199 for( var iLength = lSamplesToRemove * this.iSoundChannels, iSource = 0, iMod = aDest.length; iLength > 0 ; ) 200 { 201 var iMaxLengthCopy = iCircularBuffer + iLength > iMod ? iMod - iCircularBuffer : iLength; 202 203 iLength -= iMaxLengthCopy; 204 205 for( var iRemainder = iMaxLengthCopy & 3; --iRemainder >= 0; ) 206 { 207 aDest[ iCircularBuffer++ ] = aSource[ iSource++ ]; 208 } 209 210 for( var iBlockCopy = iMaxLengthCopy >>> 2; --iBlockCopy >= 0; ) 211 { 212 aDest[ iCircularBuffer++ ] = aSource[ iSource++ ]; 213 aDest[ iCircularBuffer++ ] = aSource[ iSource++ ]; 214 aDest[ iCircularBuffer++ ] = aSource[ iSource++ ]; 215 aDest[ iCircularBuffer++ ] = aSource[ iSource++ ]; 216 } 217 218 iCircularBuffer %= iMod; 219 } 220 this.iCircularBufferPosition = iCircularBuffer; 221 } 222 223 for( var iLength = (iBufferLengthInSamples - lSamplesToRemove - this.samplesToFill() ) * this.iSoundChannels, iSource = lSamplesToRemove * this.iSoundChannels, aBuffer = this.aBuffer; iLength > 0; ) 224 { 225 // aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 226 227 for( var iRemainder = iLength & 3; --iRemainder >= 0; ) 228 { 229 aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 230 } 231 232 for( var iBlockCopy = iLength >>> 2; --iBlockCopy >= 0; ) 233 { 234 aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 235 aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 236 aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 237 aBuffer[ iDestination++ ] = aBuffer[ iSource++ ]; 238 } 239 240 iLength -= iLength; 241 } 242 243 this.iBufferNotFullOffset = iDestination; 244 this.lWrittenSamples += lSamplesToRemove; 245 246 }; 247 248 // --------------------------------------------------------------------------- 249 /** 250 * Get the length of the audio buffer in samples (number of channels is ignored, so same value is returned for stereo and mono). 251 * 252 * @return {int} = The Audio buffer size in samples. 253 */ 254 weasel.AudioBuffer.prototype.getBufferLengthInSamples = function() 255 { 256 return this.iBufferSizeInSamples; 257 }; 258 259 // --------------------------------------------------------------------------- 260 /** 261 * Get the actual audio buffer directly, is used because feeding samples with appendSampleToBuffer() is far too slow. 262 * 263 * @return {Array} = The Audio buffer array (Audio Channels are interleaved, e.g. left-0, right-0, left-1, right-2, left-3, right-3 etc. 264 */ 265 weasel.AudioBuffer.prototype.getBuffer = function() 266 { 267 return this.aBuffer; 268 }; 269 270 // --------------------------------------------------------------------------- 271 /** 272 * Get the position in the audio buffer directly, is used because feeding samples with appendSampleToBuffer() is far too slow. 273 * 274 * @return {int} = The offset into the Audio buffer array (beware of stereo etc). 275 */ 276 weasel.AudioBuffer.prototype.getBufferPosition = function() 277 { 278 return this.iBufferNotFullOffset; 279 }; 280 281 // --------------------------------------------------------------------------- 282 /** 283 * Get the samples to fill, the number of sample slots available for use within this audio buffer (ignores the number of channels). 284 * 285 * @return {int} = Number of sample slots available. 286 */ 287 weasel.AudioBuffer.prototype.samplesToFill = function() 288 { 289 return this.getBufferLengthInSamples() - ((this.iBufferNotFullOffset / this.iSoundChannels) | 0); 290 }; 291 292 // --------------------------------------------------------------------------- 293 /** 294 * Append a sample to this Audio Sample buffer, incredibly slow, so is not recommended for large call volume. 295 * 296 * @param {float} fSample = The Sample to add. 297 * 298 * @exception Is thrown if audio buffer is already full. 299 */ 300 weasel.AudioBuffer.prototype.appendSampleToBuffer = function( fSample ) 301 { 302 if( this.iBufferNotFullOffset < this.aBuffer.length ) 303 { 304 this.aBuffer[ this.iBufferNotFullOffset++ ] = fSample; 305 } 306 else 307 { 308 throw( 'Audio Buffer is already full, sample is ignored.( Buffer Length: ' + this.aBuffer.length + ', Buffer offset: ' + this.iBufferNotFullOffset + ').' ); 309 } 310 }; 311 312 // --------------------------------------------------------------------------- 313 /** 314 * Get the circular audio buffer containing the rendered sample that is passed to the Audio sub system (its in interleaved stereo format). 315 * 316 * @return {Array} = The circular array containing the rendered sample data. 317 */ 318 weasel.AudioBuffer.prototype.getCircularAudioBuffer = function( ) 319 { 320 return this.aCircularAudioBuffer; 321 }; 322 323 // --------------------------------------------------------------------------- 324 /** 325 * Get the current position in the circular array (rendered sample data is behind this offset). 326 * 327 * @return {int} = The current position in the circular array containing the rendered sample data. 328 */ 329 weasel.AudioBuffer.prototype.getCircularBufferPosition = function( ) 330 { 331 return this.iCircularBufferPosition; 332 }; 333 334 // --------------------------------------------------------------------------- 335 /** 336 * RLM-D FAT Compander, which mixes the individual channels together to make a stereo output. 337 * They are mixed using a inlined case optimised version of RLM-D FAT (no master 338 * volume control, no overflow clamping, specifically for 2 channels). (a rather 339 * nice [personal opinion!] routine I came up with on the Atari ST in the 90ies 340 * which keeps the samples nice and loud with minimal distortion), 341 * Yes the name is wrong (its only half a compander!) I was experimenting with 342 * sample compression (via bit reduction) at the time and wondered what it sounded 343 * like without the expander!. 344 * 345 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 346 * @param {Array} aSamples1 = The rendered samples for channel 1. 347 * @param {Array} aSamples2 = The rendered samples for channel 2. 348 * @param {Array} aSamples3 = The rendered samples for channel 3. 349 * @param {Array} aSamples4 = The rendered samples for channel 4. 350 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 351 * 352 * @private 353 */ 354 weasel.AudioBuffer.prototype._companderRLMDFat = function( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ) 355 { 356 var iBufferNotFullOffset = this.iBufferNotFullOffset; 357 358 for( var iLength = iSamplesToFill, iMod = aSamples1.length, aBuffer = this.getBuffer(); iLength > 0; ) 359 { 360 var iMaxLengthFill = iCircularBufferOffset + iLength > iMod ? iMod - iCircularBufferOffset : iLength; 361 iLength -= iMaxLengthFill; 362 363 while( --iMaxLengthFill >= 0 ) 364 { 365 var fLeft = (aSamples1[ iCircularBufferOffset ] + aSamples4[ iCircularBufferOffset ]) * 0.5; 366 var fRight= (aSamples2[ iCircularBufferOffset ] + aSamples3[ iCircularBufferOffset++ ]) * 0.5; 367 368 aBuffer[ iBufferNotFullOffset++ ] = fLeft < 0.0 ? -1.0 + (( 1.0 + fLeft ) * (1.0 + fLeft ) ) : 1.0 - ( (1.0 - fLeft ) * ( 1.0 - fLeft ) ); // Left. 369 aBuffer[ iBufferNotFullOffset++ ] = fRight < 0.0 ? -1.0 + (( 1.0 + fRight ) * (1.0 + fRight ) ) : 1.0 - ( (1.0 - fRight ) * ( 1.0 - fRight ) ); // Right. 370 } 371 iCircularBufferOffset %= iMod; 372 } 373 374 this.iBufferNotFullOffset = iBufferNotFullOffset; 375 }; 376 377 // --------------------------------------------------------------------------- 378 /** 379 * Linear mixer for channels down to 2 (stereo) channels, overflow clamping. 380 * 381 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 382 * @param {Array} aSamples1 = The rendered samples for channel 1. 383 * @param {Array} aSamples2 = The rendered samples for channel 2. 384 * @param {Array} aSamples3 = The rendered samples for channel 3. 385 * @param {Array} aSamples4 = The rendered samples for channel 4. 386 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 387 * 388 * @private 389 */ 390 weasel.AudioBuffer.prototype._mixLinear = function( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ) 391 { 392 var iBufferNotFullOffset = this.iBufferNotFullOffset; 393 394 for( var iLength = iSamplesToFill, iMod = aSamples1.length, aBuffer = this.getBuffer(); iLength > 0; ) 395 { 396 var iMaxLengthFill = iCircularBufferOffset + iLength > iMod ? iMod - iCircularBufferOffset : iLength; 397 iLength -= iMaxLengthFill; 398 399 while( --iMaxLengthFill >= 0 ) 400 { 401 var fLeft = (aSamples1[ iCircularBufferOffset ] + aSamples4[ iCircularBufferOffset ]) * 0.5; 402 var fRight= (aSamples2[ iCircularBufferOffset ] + aSamples3[ iCircularBufferOffset++ ]) * 0.5; 403 404 aBuffer[ iBufferNotFullOffset++ ] = fLeft < -1.0 ? -1.0 : fLeft > 1.0 ? 1.0 : fLeft; // Left. 405 aBuffer[ iBufferNotFullOffset++ ] = fRight < -1.0 ? -1.0 : fRight > 1.0 ? 1.0 : fRight; // Right. 406 } 407 iCircularBufferOffset %= iMod; 408 } 409 410 this.iBufferNotFullOffset = iBufferNotFullOffset; 411 412 }; 413 // --------------------------------------------------------------------------- 414 /** 415 * Linear mixer for channels down to 2 (stereo) channels, overflow clamping, with 416 * simulated Amiga Filter (just an averaging filter for now). 417 * 418 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 419 * @param {Array} aSamples1 = The rendered samples for channel 1. 420 * @param {Array} aSamples2 = The rendered samples for channel 2. 421 * @param {Array} aSamples3 = The rendered samples for channel 3. 422 * @param {Array} aSamples4 = The rendered samples for channel 4. 423 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 424 * 425 * @private 426 */ 427 weasel.AudioBuffer.prototype._mixLinearFiltered = function( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ) 428 { 429 var iBufferNotFullOffset = this.iBufferNotFullOffset; 430 var iFilterBufferPosition = this.iFilterBufferPosition; 431 var iBarrelLoopMask = 0x7f; 432 433 var iSamplesPerFilter = Math.ceil( (this.iPlaybackFrequency / 7000.0) ); 434 if( iSamplesPerFilter < 2 ) 435 { 436 // If the sampling rate ever goes close the filter rate always 437 // filter by an average of 2 samples. 438 // 439 iSamplesPerFilter = 2; 440 } 441 442 if( !this.bPreviousFilterState ) 443 { 444 // Filter has just been switched on, copy some of the previously mixed samples 445 // into the filter buffer as it is out of sync (no data written to it since 446 // last time filter was on). 447 // 448 this.bPreviousFilterState = true; 449 this._updateFilterHistory( iSamplesPerFilter ); 450 } 451 452 var fReciprocalAverageDivider = 1.0 / iSamplesPerFilter; 453 var fLeftTotal = 0.0; 454 var fRightTotal = 0.0; 455 for( var iSamplesToTotal = 1; iSamplesToTotal <= iSamplesPerFilter; iSamplesToTotal++ ) 456 { 457 var iBuffPos = (iFilterBufferPosition - iSamplesToTotal) & iBarrelLoopMask; 458 459 fLeftTotal += this.aFilterBufferLeft[ iBuffPos ]; 460 fRightTotal+= this.aFilterBufferRight[ iBuffPos ]; 461 } 462 463 for( var iLength = iSamplesToFill, iMod = aSamples1.length, aBuffer = this.getBuffer(), aFilterLeft = this.aFilterBufferLeft, aFilterRight = this.aFilterBufferRight; iLength > 0; ) 464 { 465 var iMaxLengthFill = iCircularBufferOffset + iLength > iMod ? iMod - iCircularBufferOffset : iLength; 466 iLength -= iMaxLengthFill; 467 468 while( --iMaxLengthFill >= 0 ) 469 { 470 var fLeft = (aSamples1[ iCircularBufferOffset ] + aSamples4[ iCircularBufferOffset ]) * 0.5; 471 var fRight= (aSamples2[ iCircularBufferOffset ] + aSamples3[ iCircularBufferOffset++ ]) * 0.5; 472 473 aFilterLeft[ iFilterBufferPosition ] = fLeft; 474 aFilterRight[ iFilterBufferPosition ] = fRight; 475 476 fLeftTotal += fLeft - aFilterLeft[ (iFilterBufferPosition - iSamplesPerFilter) & iBarrelLoopMask ]; 477 fRightTotal += fRight - aFilterRight[ (iFilterBufferPosition - iSamplesPerFilter) & iBarrelLoopMask ]; 478 iFilterBufferPosition = ++iFilterBufferPosition & iBarrelLoopMask; 479 480 fLeft = fLeftTotal * fReciprocalAverageDivider; 481 fRight = fRightTotal * fReciprocalAverageDivider; 482 483 484 aBuffer[ iBufferNotFullOffset++ ] = fLeft < -1.0 ? -1.0 : fLeft > 1.0 ? 1.0 : fLeft; // Left. 485 aBuffer[ iBufferNotFullOffset++ ] = fRight < -1.0 ? -1.0 : fRight > 1.0 ? 1.0 : fRight; // Right. 486 } 487 iCircularBufferOffset %= iMod; 488 } 489 490 this.iBufferNotFullOffset = iBufferNotFullOffset; 491 this.iFilterBufferPosition = iFilterBufferPosition; 492 }; 493 494 // --------------------------------------------------------------------------- 495 /** 496 * 497 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 498 * @param {Array} aSamples1 = The rendered samples for channel 1. 499 * @param {Array} aSamples2 = The rendered samples for channel 2. 500 * @param {Array} aSamples3 = The rendered samples for channel 3. 501 * @param {Array} aSamples4 = The rendered samples for channel 4. 502 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 503 */ 504 weasel.AudioBuffer.prototype._companderRLMDFatFiltered = function( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ) 505 { 506 var iBufferNotFullOffset = this.iBufferNotFullOffset; 507 var iFilterBufferPosition = this.iFilterBufferPosition; 508 var iBarrelLoopMask = 0x7f; 509 510 var iSamplesPerFilter = Math.ceil( (this.iPlaybackFrequency / 7000.0) ); 511 if( iSamplesPerFilter < 2 ) 512 { 513 // If the sampling rate ever goes close the filter rate always 514 // filter by an average of 2 samples. 515 // 516 iSamplesPerFilter = 2; 517 } 518 519 if( !this.bPreviousFilterState ) 520 { 521 // Filter has just been switched on, copy some of the previously mixed samples 522 // into the filter buffer as it is out of sync (no data written to it since 523 // last time filter was on). 524 // 525 this.bPreviousFilterState = true; 526 this._updateFilterHistory( iSamplesPerFilter ); 527 } 528 529 var fReciprocalAverageDivider = 1.0 / iSamplesPerFilter; 530 var fLeftTotal = 0.0; 531 var fRightTotal = 0.0; 532 for( var iSamplesToTotal = 1; iSamplesToTotal <= iSamplesPerFilter; iSamplesToTotal++ ) 533 { 534 var iBuffPos = (iFilterBufferPosition - iSamplesToTotal) & iBarrelLoopMask; 535 536 fLeftTotal += this.aFilterBufferLeft[ iBuffPos ]; 537 fRightTotal+= this.aFilterBufferRight[ iBuffPos ]; 538 } 539 540 for( var iLength = iSamplesToFill, iMod = aSamples1.length, aBuffer = this.getBuffer(), aFilterLeft = this.aFilterBufferLeft, aFilterRight = this.aFilterBufferRight; iLength > 0; ) 541 { 542 var iMaxLengthFill = iCircularBufferOffset + iLength > iMod ? iMod - iCircularBufferOffset : iLength; 543 iLength -= iMaxLengthFill; 544 545 while( --iMaxLengthFill >= 0 ) 546 { 547 var fLeft = (aSamples1[ iCircularBufferOffset ] + aSamples4[ iCircularBufferOffset ]) * 0.5; 548 var fRight= (aSamples2[ iCircularBufferOffset ] + aSamples3[ iCircularBufferOffset++ ]) * 0.5; 549 550 aFilterLeft[ iFilterBufferPosition ] = fLeft; 551 aFilterRight[ iFilterBufferPosition ] = fRight; 552 553 fLeftTotal += fLeft - aFilterLeft[ (iFilterBufferPosition - iSamplesPerFilter) & iBarrelLoopMask ]; 554 fRightTotal += fRight - aFilterRight[ (iFilterBufferPosition - iSamplesPerFilter) & iBarrelLoopMask ]; 555 iFilterBufferPosition = ++iFilterBufferPosition & iBarrelLoopMask; 556 557 fLeft = fLeftTotal * fReciprocalAverageDivider; 558 fRight = fRightTotal * fReciprocalAverageDivider; 559 560 aBuffer[ iBufferNotFullOffset++ ] = fLeft < 0.0 ? -1.0 + (( 1.0 + fLeft ) * (1.0 + fLeft ) ) : 1.0 - ( (1.0 - fLeft ) * ( 1.0 - fLeft ) ); // Left. 561 aBuffer[ iBufferNotFullOffset++ ] = fRight < 0.0 ? -1.0 + (( 1.0 + fRight ) * (1.0 + fRight ) ) : 1.0 - ( (1.0 - fRight ) * ( 1.0 - fRight ) ); // Right. 562 } 563 iCircularBufferOffset %= iMod; 564 } 565 566 this.iBufferNotFullOffset = iBufferNotFullOffset; 567 this.iFilterBufferPosition = iFilterBufferPosition; 568 }; 569 570 // --------------------------------------------------------------------------- 571 /** When the filter switches from Off to On the filter history does not contain 572 * any sample history so copy them from the already mixed audio buffer (which 573 * conveniently contains un-filtered samples as the filter has only just been turned 574 * on). 575 * 576 * @param {int} iSamplesPerFilter = The number of samples the current filter needs. 577 * 578 * @private 579 */ 580 weasel.AudioBuffer.prototype._updateFilterHistory = function( iSamplesPerFilter ) 581 { 582 var iBarrelLoopMask = 0x7f; 583 var aMixedSamples = this.getBuffer(); 584 var iCopyMixedFrom = (this.iBufferNotFullOffset - (iSamplesPerFilter * this.iSoundChannels) + aMixedSamples.length) % aMixedSamples.length; 585 var iBuffPos = (this.iFilterBufferPosition - iSamplesPerFilter) & iBarrelLoopMask; 586 587 for( var iSamplesToCopy = 1; iSamplesToCopy <= iSamplesPerFilter; iSamplesToCopy++ ) 588 { 589 this.aFilterBufferLeft[ iBuffPos ] = aMixedSamples[ iCopyMixedFrom++ ] ; 590 this.aFilterBufferRight[ iBuffPos++ ] = aMixedSamples[ iCopyMixedFrom++ ] ; 591 iBuffPos &= iBarrelLoopMask; 592 iCopyMixedFrom %= aMixedSamples.length; 593 } 594 }; 595 596 // --------------------------------------------------------------------------- 597 /** 598 * Mix sound channels together. 599 * 600 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 601 * @param {Array} aSamples1 = The rendered samples for channel 1. 602 * @param {Array} aSamples2 = The rendered samples for channel 2. 603 * @param {Array} aSamples3 = The rendered samples for channel 3. 604 * @param {Array} aSamples4 = The rendered samples for channel 4. 605 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 606 * @param {bool} bFilterOn = Mix using the Amiga filter. 607 * 608 */ 609 weasel.AudioBuffer.prototype.mix = function( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset, bFilterOn ) 610 { 611 if( 0 == this.getMixerType() ) 612 { 613 if( !bFilterOn ) 614 { 615 this._mixLinear( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ); 616 this.bPreviousFilterState = false; 617 } 618 else 619 { 620 this._mixLinearFiltered( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ); 621 } 622 } 623 else 624 { 625 if( !bFilterOn ) 626 { 627 this._companderRLMDFat( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ); 628 this.bPreviousFilterState = false; 629 } 630 else 631 { 632 this._companderRLMDFatFiltered( iSamplesToFill, aSamples1, aSamples2, aSamples3, aSamples4, iCircularBufferOffset ); 633 } 634 } 635 }; 636 637 //--------------------------------------------------------------------------- 638 /** 639 * Convert a decibel value into a linear value. 640 * 641 * @param {float} fDecibel = The decibel value to convert to a linear modifier, e.g. -3.0, -4.5, -6.0. 642 * 643 * @return {float} = The decibel value as linear. 644 * 645 * @private 646 * @deprecated Remove upon further investigation for surround sound (5.1). 647 */ 648 weasel.AudioBuffer.prototype._decibelToLinear = function( fDecibel ) 649 { 650 return Math.pow( 10.0, fDecibel / 20.0 ); 651 }; 652 653 //--------------------------------------------------------------------------- 654 /** 655 * Panning Law for left channel, reduce the channels sound level the close 656 * it gets to the center (so that the sample volume stays approximately the same 657 * as it pans from left to right). 658 * Typical panning law values are: 659 * -3db (considered analogue mixing desk standard) 660 * -4.5db 661 * -6.0db. 662 * 663 * @param {float} fPanning = The panning position -1 = full left, 0 = center, +1 = full right. 664 * 665 * @return {float} = The output volume modifier for the left channel (0-1). 666 * 667 * @deprecated Remove upon further investigation for surround sound (5.1). 668 */ 669 weasel.AudioBuffer.prototype.panningLawLeft = function( fPanning ) 670 { 671 if( fPanning >= 1.0 ) 672 { 673 return 0.0; 674 } 675 else if( fPanning <= -1.0 ) 676 { 677 return 1.0; 678 } 679 680 var fLeftLaw = fPanning; 681 var fLeftPanning = 1.0; 682 if( fPanning > 0.0 ) 683 { 684 fLeftLaw = 0.0; 685 fLeftPanning -= fPanning; 686 } 687 688 return this._decibelToLinear( this.fPanningLawDB *( 1.0 + fLeftLaw ) ) * fLeftPanning; 689 }; 690 691 //--------------------------------------------------------------------------- 692 /** 693 * Panning Law for right channel, reduce the channels sound level the close 694 * it gets to the center (so that the sample volume stays approximately the same 695 * as it pans from left to right). 696 * Typical panning law values are: 697 * -3db (considered analogue mixing desk standard) 698 * -4.5db 699 * -6.0db. 700 * 701 * @param {float} fPanning = The panning position -1 = full left, 0 = center, +1 = full right. 702 * 703 * @return {float} = The output volume modifier for the right channel (0-1). 704 * 705 * @deprecated Remove upon further investigation for surround sound (5.1). 706 */ 707 weasel.AudioBuffer.prototype.panningLawRight = function( fPanning ) 708 { 709 if( fPanning <= -1.0 ) 710 { 711 return 0.0; 712 } 713 else if( fPanning >= 1.0 ) 714 { 715 return 1.0; 716 } 717 718 var fRightLaw = fPanning; 719 var fRightPanning = 1.0; 720 if( fPanning < 0.0 ) 721 { 722 fRightLaw = 0.0; 723 fRightPanning += fPanning; 724 } 725 726 return this._decibelToLinear( this.fPanningLawDB *( 1.0 - fRightLaw ) ) * fRightPanning; 727 }; 728 729 //--------------------------------------------------------------------------- 730 /** 731 * Fasttracker 2 Panning Law, which is (as far as I can tell) a logarithmic curve - or at 732 * least part of the curve with ~2.5db roll off for the center channel. 733 * 734 * @param {float} fPanning = The panning position -1 = full left, 0 = center, +1 = full right. 735 * 736 * @return {float} = The output volume modifier for the left channel (0-1). 737 */ 738 weasel.AudioBuffer.prototype.panningFT2Left = function( fPanning ) 739 { 740 if( fPanning >= 1.0 ) 741 { 742 return 0.0; 743 } 744 else if( fPanning <= -1.0 ) 745 { 746 return 1.0; 747 } 748 749 fPanning = ( 1.1 - ( fPanning * 0.9 ) ) * 0.5; 750 751 return 1 + ( Math.log( fPanning ) / Math.LN10 ); 752 }; 753 754 //--------------------------------------------------------------------------- 755 /** 756 * Fasttracker 2 Panning Law, which is (as far as I can tell) a logarithmic curve - or at 757 * least part of the curve with ~2.5db roll off for the center channel. 758 * 759 * @param {float} fPanning = The panning position -1 = full left, 0 = center, +1 = full right. 760 * 761 * @return {float} = The output volume modifier for the right channel (0-1). 762 */ 763 weasel.AudioBuffer.prototype.panningFT2Right = function( fPanning ) 764 { 765 if( fPanning <= -1.0 ) 766 { 767 return 0.0; 768 } 769 else if( fPanning >= 1.0 ) 770 { 771 return 1.0; 772 } 773 774 fPanning = (( fPanning * 0.9 ) + 1.1 ) * 0.5; 775 776 return 1 + ( Math.log( fPanning ) / Math.LN10 ); 777 }; 778 779 //--------------------------------------------------------------------------- 780 /** 781 * Mix sound a channel into the AudioBuffer. 782 * 783 * @param {int} iSamplesToFill = The number of samples to mix together (ignoring stereo). 784 * @param {Array} aSamples = The rendered samples for the channel. 785 * @param {int} iCircularBufferOffset = The position in the above sample circular buffers. 786 * @param {int} iPanningPosition = The panning position of the sample (0=left, 128 = middle, 255 = right). 787 */ 788 weasel.AudioBuffer.prototype.mixIn = function( iSamplesToFill, aSamples, iCircularBufferOffset, iPanningPosition ) 789 { 790 var fPanning = (iPanningPosition - 128) / 128.0; 791 var fVolumeLeft = this.panningFT2Left( fPanning ) * this.fAutoGain; 792 var fVolumeRight = this.panningFT2Right( fPanning ) * this.fAutoGain; 793 794 for( var iLength = iSamplesToFill, iMod = aSamples.length, aBuffer = this.getBuffer(), iBufferNotFullOffset = this.iBufferNotFullOffset; iLength > 0; ) 795 { 796 var iMaxLengthFill = iCircularBufferOffset + iLength > iMod ? iMod - iCircularBufferOffset : iLength; 797 iLength -= iMaxLengthFill; 798 799 while( --iMaxLengthFill >= 0 ) 800 { 801 var fSample = aSamples[ iCircularBufferOffset++ ]; 802 803 aBuffer[ iBufferNotFullOffset++ ] += fSample * fVolumeLeft; // Left. 804 aBuffer[ iBufferNotFullOffset++ ] += fSample * fVolumeRight; // Right. 805 } 806 807 iCircularBufferOffset %= iMod; 808 } 809 }; 810 811 //--------------------------------------------------------------------------- 812 /** 813 * Clear part of the audio buffer (for use with mod with more than 4 channels 814 * as the mixing process is different). 815 * 816 * @param {int} iSamplesToFill = The number of samples to clear (ignoring stereo). 817 */ 818 weasel.AudioBuffer.prototype.clear = function( iSamplesToFill ) 819 { 820 var iBufferNotFullOffset = this.iBufferNotFullOffset; 821 822 for( var iLength = iSamplesToFill, aBuffer = this.getBuffer(); iLength > 0; iLength-- ) 823 { 824 aBuffer[ iBufferNotFullOffset++ ] = 0.0; // Left. 825 aBuffer[ iBufferNotFullOffset++ ] = 0.0; // Right. 826 } 827 828 // this.iBufferNotFullOffset = iBufferNotFullOffset; 829 }; 830 831 //--------------------------------------------------------------------------- 832 /** 833 * Clip part of the audio buffer (for use with mod with more than 4 channels 834 * as the mixing process is different), clips to -1.0 to 1.0 range. 835 * 836 * @param {int} iSamplesToFill = The number of samples to clear (ignoring stereo). 837 */ 838 weasel.AudioBuffer.prototype.clip = function( iSamplesToClip ) 839 { 840 var iBufferNotFullOffset = this.iBufferNotFullOffset; 841 var fPeakLeft = 0.0; 842 var fPeakRight = 0.0; 843 844 for( var iLength = iSamplesToClip, aBuffer = this.getBuffer(); iLength > 0; iLength-- ) 845 { 846 var fLeft = aBuffer[ iBufferNotFullOffset ]; 847 var fTemp = Math.abs( fLeft ); 848 if( fTemp > fPeakLeft ) 849 { 850 fPeakLeft = fTemp; 851 } 852 853 aBuffer[ iBufferNotFullOffset++ ] = fLeft < -1.0 ? -1.0 : fLeft > 1.0 ? 1.0 : fLeft; // Left. 854 855 var fRight = aBuffer[ iBufferNotFullOffset ]; 856 var fTemp = Math.abs( fRight ); 857 if( fTemp > fPeakRight ) 858 { 859 fPeakRight = fTemp; 860 } 861 862 aBuffer[ iBufferNotFullOffset++ ] = fRight < -1.0 ? -1.0 : fRight > 1.0 ? 1.0 : fRight; // Right. 863 } 864 865 this.iBufferNotFullOffset = iBufferNotFullOffset; 866 this.fPeakLeft = fPeakLeft; 867 this.fPeakRight = fPeakRight; 868 }; 869 870 871 //--------------------------------------------------------------------------- 872 /** 873 * Clip part of the audio buffer (for use with mod with more than 4 channels 874 * as the mixing process is different), clips to -1.0 to 1.0 range. 875 * 876 * @param {int} iSamplesToFill = The number of samples to clear (ignoring stereo). 877 */ 878 weasel.AudioBuffer.prototype.clipRLMDFat = function( iSamplesToClip ) 879 { 880 var iBufferNotFullOffset = this.iBufferNotFullOffset; 881 var fPeakLeft = 0.0; 882 var fPeakRight = 0.0; 883 884 for( var iLength = iSamplesToClip, aBuffer = this.getBuffer(); iLength > 0; iLength-- ) 885 { 886 var fLeft = aBuffer[ iBufferNotFullOffset ]; 887 var fTemp = Math.abs( fLeft ); 888 if( fTemp > fPeakLeft ) 889 { 890 fPeakLeft = fTemp; 891 } 892 893 fLeft = fLeft < -1.0 ? -1.0 : fLeft > 1.0 ? 1.0 : fLeft; // Left. 894 aBuffer[ iBufferNotFullOffset++ ] = fLeft < 0.0 ? -1.0 + (( 1.0 + fLeft ) * (1.0 + fLeft ) ) : 1.0 - ( (1.0 - fLeft ) * ( 1.0 - fLeft ) ); // Left. 895 896 var fRight = aBuffer[ iBufferNotFullOffset ]; 897 var fTemp = Math.abs( fRight ); 898 if( fTemp > fPeakRight ) 899 { 900 fPeakRight = fTemp; 901 } 902 903 fRight = fRight < -1.0 ? -1.0 : fRight > 1.0 ? 1.0 : fRight; // Right. 904 aBuffer[ iBufferNotFullOffset++ ] = fRight < 0.0 ? -1.0 + (( 1.0 + fRight ) * (1.0 + fRight ) ) : 1.0 - ( (1.0 - fRight ) * ( 1.0 - fRight ) ); // Right. 905 } 906 907 this.iBufferNotFullOffset = iBufferNotFullOffset; 908 this.fPeakLeft = fPeakLeft; 909 this.fPeakRight = fPeakRight; 910 };