scratch-html5/js/sound/SoundDecoder.js

192 lines
7 KiB
JavaScript
Raw Normal View History

2013-10-28 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04:00
/*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
];
2013-11-01 22:44:51 -04:00
SoundDecoder.prototype.getSampleADPCM = function() {
2013-10-28 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04: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 16:00:20 -04:00
if (this.sample > 32767) this.sample = 32767;
if (this.sample < -32768) this.sample = -32768;
return this.sample;
}
}