2013-10-28 20:00:20 +00:00
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// SoundDecoder.js
// Decode WAV Files (8-bit, 16-bit, and ADPCM) for playing by Sprites.
// For best performance, this should be run only once per WAV and
// the decoded buffer should be cached.
// Based almost entirely on John Maloney's AS implementation.
var SoundDecoder = function ( wavFileData ) {
this . scratchSound = null ;
2013-11-01 22:44:51 -04:00
2013-10-28 20:00:20 +00:00
this . soundData = null ;
this . startOffset = 0 ;
this . endOffset = 0 ;
this . stepSize = 0 ;
this . adpcmBlockSize = 0 ;
this . bytePosition = 0 ;
this . soundChannel = null ;
this . lastBufferTime = 0 ;
this . getSample = null ;
this . fraction = 0.0 ;
this . thisSample = 0 ;
// decoder state
2013-11-01 22:44:51 -04:00
this . sample = 0 ;
2013-10-28 20:00:20 +00:00
this . index = 0 ;
this . lastByte = - 1 ; // -1 indicates that there is no saved lastByte
2013-11-01 22:44:51 -04:00
2013-10-28 20:00:20 +00:00
this . nextSample = 0 ;
this . info = null ;
getSample = this . getSample16Uncompressed ;
if ( wavFileData != null ) {
var info = WAVFile . decode ( wavFileData ) ;
this . info = info ;
this . startOffset = info . sampleDataStart ;
this . endOffset = this . startOffset + info . sampleDataSize ;
this . soundData = new Uint8Array ( wavFileData . slice ( this . startOffset , this . endOffset ) ) ;
this . stepSize = info . samplesPerSecond / 44100.0 ;
if ( info . encoding == 17 ) {
this . adpcmBlockSize = info . adpcmBlockSize ;
this . getSample = this . getSampleADPCM ;
} else {
if ( info . bitsPerSample == 8 ) this . getSample = this . getSample8Uncompressed ;
if ( info . bitsPerSample == 16 ) this . getSample = this . getSample16Uncompressed ;
}
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
SoundDecoder . prototype . noteFinished = function ( ) {
// Called by subclasses to force ending condition to be true in writeSampleData()
this . bytePosition = this . endOffset ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// Used for Notes and Drums - Web Audio API ScriptProcessorNodes use this
// as a callback function to fill the buffers with sample data.
SoundDecoder . prototype . writeSampleData = function ( evt ) {
var i = 0 ;
var output = evt . outputBuffer . getChannelData ( 0 ) ;
//this.updateVolume();
for ( i = 0 ; i < output . length ; i ++ ) {
var n = this . interpolatedSample ( ) ;
output [ i ] = n ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// For pre-caching the samples of WAV sounds
// Return a full list of samples generated by the decoder.
SoundDecoder . prototype . getAllSamples = function ( ) {
var samples = [ ] , smp = 0 ;
smp = this . interpolatedSample ( ) ;
while ( smp != null ) {
samples . push ( smp ) ;
smp = this . interpolatedSample ( ) ;
}
return samples ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// Provide the next sample for the buffer
SoundDecoder . prototype . interpolatedSample = function ( ) {
this . fraction += this . stepSize ;
while ( this . fraction >= 1.0 ) {
this . thisSample = this . nextSample ;
this . nextSample = this . getSample ( ) ;
this . fraction -= 1.0 ;
}
if ( this . nextSample == null ) { return null ; }
2013-11-01 22:44:51 -04:00
var out = this . fraction == 0 ? this . thisSample : this . thisSample + this . fraction * ( this . nextSample - this . thisSample ) ;
return out / 32768.0 ;
} ;
2013-10-28 20:00:20 +00:00
// 16-bit samples, big-endian
SoundDecoder . prototype . getSample16Uncompressed = function ( ) {
var result = 0 ;
if ( this . bytePosition <= ( this . info . sampleDataSize - 2 ) ) {
result = ( this . soundData [ this . bytePosition + 1 ] << 8 ) + this . soundData [ this . bytePosition ] ;
if ( result > 32767 ) result -= 65536 ;
this . bytePosition += 2 ;
} else {
this . bytePosition = this . endOffset ;
result = null ;
}
return result ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// 8-bit samples, uncompressed
SoundDecoder . prototype . getSample8Uncompressed = function ( ) {
if ( this . bytePosition >= this . info . sampleDataSize ) return null ;
return ( this . soundData [ this . bytePosition ++ ] - 128 ) << 8 ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
/ * S o u n d D e c o d e r . p r o t o t y p e . u p d a t e V o l u m e = f u n c t i o n ( ) {
if ( this . client == null ) {
this . volume = 1.0 ;
return ;
}
if ( this . client . volume == this . lastClientVolume ) return ; // optimization
this . volume = Math . max ( 0.0 , Math . min ( this . client . volume / 100.0 , 1.0 ) ) ;
this . lastClientVolume = this . client . volume ;
} * /
// Decoder for IMA ADPCM compressed sounds
SoundDecoder . indexTable = [ - 1 , - 1 , - 1 , - 1 , 2 , 4 , 6 , 8 , - 1 , - 1 , - 1 , - 1 , 2 , 4 , 6 , 8 ] ;
SoundDecoder . stepTable = [
7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 16 , 17 , 19 , 21 , 23 , 25 , 28 , 31 , 34 , 37 , 41 , 45 ,
50 , 55 , 60 , 66 , 73 , 80 , 88 , 97 , 107 , 118 , 130 , 143 , 157 , 173 , 190 , 209 , 230 ,
253 , 279 , 307 , 337 , 371 , 408 , 449 , 494 , 544 , 598 , 658 , 724 , 796 , 876 , 963 ,
1060 , 1166 , 1282 , 1411 , 1552 , 1707 , 1878 , 2066 , 2272 , 2499 , 2749 , 3024 , 3327 ,
3660 , 4026 , 4428 , 4871 , 5358 , 5894 , 6484 , 7132 , 7845 , 8630 , 9493 , 10442 , 11487 ,
12635 , 13899 , 15289 , 16818 , 18500 , 20350 , 22385 , 24623 , 27086 , 29794 , 32767
] ;
2013-11-01 22:44:51 -04:00
SoundDecoder . prototype . getSampleADPCM = function ( ) {
2013-10-28 20:00:20 +00:00
// Decompress sample data using the IMA ADPCM algorithm.
2013-11-01 22:44:51 -04:00
// Note: Handles only one channel, 4-bits/sample.
2013-10-28 20:00:20 +00:00
var step = 0 , code = 0 , delta = 0 ;
2013-11-01 22:44:51 -04:00
if ( this . bytePosition % this . adpcmBlockSize == 0 && this . lastByte < 0 ) { // read block header
if ( this . bytePosition > this . info . sampleDataSize - 4 ) return null ;
2013-10-28 20:00:20 +00:00
this . sample = ( this . soundData [ this . bytePosition + 1 ] << 8 ) + this . soundData [ this . bytePosition ] ;
if ( this . sample > 32767 ) this . sample -= 65536 ;
this . index = this . soundData [ this . bytePosition + 2 ] ;
this . bytePosition += 4 ;
if ( this . index > 88 ) this . index = 88 ;
this . lastByte = - 1 ;
return this . sample ;
} else {
// read 4-bit code and compute delta
if ( this . lastByte < 0 ) {
if ( this . bytePosition >= this . info . sampleDataSize ) return null ;
this . lastByte = this . soundData [ this . bytePosition ++ ] ;
code = this . lastByte & 0xF ;
} else {
code = ( this . lastByte >> 4 ) & 0xF ;
this . lastByte = - 1 ;
}
step = SoundDecoder . stepTable [ this . index ] ;
delta = 0 ;
if ( code & 4 ) delta += step ;
if ( code & 2 ) delta += step >> 1 ;
if ( code & 1 ) delta += step >> 2 ;
delta += step >> 3 ;
// compute next index
this . index += SoundDecoder . indexTable [ code ] ;
if ( this . index > 88 ) this . index = 88 ;
if ( this . index < 0 ) this . index = 0 ;
// compute and output sample
2013-11-01 22:44:51 -04:00
this . sample += code & 8 ? - delta : delta ;
2013-10-28 20:00:20 +00:00
if ( this . sample > 32767 ) this . sample = 32767 ;
if ( this . sample < - 32768 ) this . sample = - 32768 ;
return this . sample ;
}
}