From c12bf02b33fb13e0a344c536c5b9c3e05ffdd0f3 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 22 Oct 2018 16:01:41 -0400 Subject: [PATCH] write ADPCM samples directly to the Audio Buffer's channel data The number of samples in the ADPCM can be known once the data chunk is extracted and the block size is known. From there the audio buffer can be created and its channel data passed to the decompress method. A lot of time is saved by writing to the channel data directly instead of writing to one array, copying that to another array, and then finally copying to the channel data. A surprising amount of time is saved by using one getChannelData call instead of calling to store each sample. --- src/ADPCMSoundDecoder.js | 94 +++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/ADPCMSoundDecoder.js b/src/ADPCMSoundDecoder.js index b970d76..ac0e171 100644 --- a/src/ADPCMSoundDecoder.js +++ b/src/ADPCMSoundDecoder.js @@ -1,6 +1,28 @@ const ArrayBufferStream = require('./ArrayBufferStream'); const log = require('./log'); +/** + * Data used by the decompression algorithm + * @type {Array} + */ +const STEP_TABLE = [ + 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 +]; + +/** + * Data used by the decompression algorithm + * @type {Array} + */ +const INDEX_TABLE = [ + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 +]; + /** * Decode wav audio files that have been compressed with the ADPCM format. * This is necessary because, while web browsers have native decoders for many audio @@ -16,19 +38,13 @@ class ADPCMSoundDecoder { constructor (audioContext) { this.audioContext = audioContext; } + /** * Data used by the decompression algorithm * @type {Array} */ static get STEP_TABLE () { - return [ - 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 - ]; + return STEP_TABLE; } /** @@ -36,10 +52,7 @@ class ADPCMSoundDecoder { * @type {Array} */ static get INDEX_TABLE () { - return [ - -1, -1, -1, -1, 2, 4, 6, 8, - -1, -1, -1, -1, 2, 4, 6, 8 - ]; + return INDEX_TABLE; } /** @@ -81,14 +94,11 @@ class ADPCMSoundDecoder { this.samplesPerBlock = formatChunk.readUint16(); this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes - const samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize); + const compressedData = this.extractChunk('data', stream); + const sampleCount = this.numberOfSamples(compressedData, this.adpcmBlockSize); - const buffer = this.audioContext.createBuffer(1, samples.length, this.samplesPerSecond); - - // @todo optimize this? e.g. replace the divide by storing 1/32768 and multiply? - for (let i = 0; i < samples.length; i++) { - buffer.getChannelData(0)[i] = samples[i] / 32768; - } + const buffer = this.audioContext.createBuffer(1, sampleCount, this.samplesPerSecond); + this.imaDecompress(compressedData, this.adpcmBlockSize, buffer.getChannelData(0)); resolve(buffer); }); @@ -114,36 +124,60 @@ class ADPCMSoundDecoder { } } + /** + * Count the exact number of samples in the compressed data. + * @param {ArrayBufferStream} compressedData - the compressed data + * @param {number} blockSize - size of each block in the data in bytes + * @return {number} number of samples in the compressed data + */ + numberOfSamples (compressedData, blockSize) { + if (!compressedData) return 0; + + compressedData.position = 0; + + const available = compressedData.getBytesAvailable(); + const blocks = (available / blockSize) | 0; + // Number of samples in full blocks. + const fullBlocks = blocks * (2 * (blockSize - 4)) + 1; + // Number of samples in the last incomplete block. 0 if the last block + // is full. + const subBlock = Math.max((available % blockSize) - 4, 0) * 2; + // 1 if the last block is incomplete. 0 if it is complete. + const incompleteBlock = Math.min(available % blockSize, 1); + return fullBlocks + subBlock + incompleteBlock; + } + /** * Decompress sample data using the IMA ADPCM algorithm. * Note: Handles only one channel, 4-bits per sample. * @param {ArrayBufferStream} compressedData - a stream of compressed audio samples * @param {number} blockSize - the number of bytes in the stream - * @return {Int16Array} the uncompressed audio samples + * @param {Float32Array} out - the uncompressed audio samples */ - imaDecompress (compressedData, blockSize) { + imaDecompress (compressedData, blockSize, out) { let sample; let step; let code; let delta; let index = 0; let lastByte = -1; // -1 indicates that there is no saved lastByte - const out = []; // Bail and return no samples if we have no data - if (!compressedData) return out; + if (!compressedData) return; compressedData.position = 0; - // @todo Update this loop ported from Scratch 2.0 to use a condition or a for loop. - while (true) { // eslint-disable-line no-constant-condition + const size = out.length; + + let i = 0; + while (i < size) { if (((compressedData.position % blockSize) === 0) && (lastByte < 0)) { // read block header if (compressedData.getBytesAvailable() === 0) break; sample = compressedData.readInt16(); index = compressedData.readUint8(); compressedData.position++; // skip extra header byte if (index > 88) index = 88; - out.push(sample); + out[i++] = sample / 32768; } else { // read 4-bit code and compute delta from previous sample if (lastByte < 0) { @@ -154,25 +188,23 @@ class ADPCMSoundDecoder { code = (lastByte >> 4) & 0xF; lastByte = -1; } - step = ADPCMSoundDecoder.STEP_TABLE[index]; + step = STEP_TABLE[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 - index += ADPCMSoundDecoder.INDEX_TABLE[code]; + index += INDEX_TABLE[code]; if (index > 88) index = 88; if (index < 0) index = 0; // compute and output sample sample += (code & 8) ? -delta : delta; if (sample > 32767) sample = 32767; if (sample < -32768) sample = -32768; - out.push(sample); + out[i++] = sample / 32768; } } - const samples = Int16Array.from(out); - return samples; } }