// 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; 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 this.sample = 0; this.index = 0; this.lastByte = -1; // -1 indicates that there is no saved lastByte 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; } } }; SoundDecoder.prototype.noteFinished = function() { // Called by subclasses to force ending condition to be true in writeSampleData() this.bytePosition = this.endOffset; }; // 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; } }; // 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; }; // 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; } var out = this.fraction == 0 ? this.thisSample : this.thisSample + this.fraction * (this.nextSample - this.thisSample); return out / 32768.0; }; // 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; }; // 8-bit samples, uncompressed SoundDecoder.prototype.getSample8Uncompressed = function() { if (this.bytePosition >= this.info.sampleDataSize) return null; return (this.soundData[this.bytePosition++] - 128) << 8; }; /*SoundDecoder.prototype.updateVolume = function() { 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 ]; SoundDecoder.prototype.getSampleADPCM = function() { // Decompress sample data using the IMA ADPCM algorithm. // Note: Handles only one channel, 4-bits/sample. var step = 0, code = 0, delta = 0; if (this.bytePosition % this.adpcmBlockSize == 0 && this.lastByte < 0) { // read block header if (this.bytePosition > this.info.sampleDataSize - 4) return null; 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 this.sample += code & 8 ? -delta : delta; if (this.sample > 32767) this.sample = 32767; if (this.sample < -32768) this.sample = -32768; return this.sample; } }