Merge pull request #104 from mzgoddard/js-optimizations

ADPCM JS Optimizations
This commit is contained in:
Eric Rosenbaum 2018-10-23 16:24:55 -04:00 committed by GitHub
commit 468aa9bf6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 143 additions and 51 deletions

View file

@ -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;
}
}

View file

@ -7,11 +7,53 @@ class ArrayBufferStream {
* The available types to read include:
* Uint8, Uint8String, Int16, Uint16, Int32, Uint32
* @param {ArrayBuffer} arrayBuffer - array to use as a stream
* @param {number} start - the start position in the raw buffer. position
* will be relative to the start value.
* @param {number} end - the end position in the raw buffer. length and
* bytes available will be relative to the end value.
* @param {ArrayBufferStream} parent - if passed reuses the parent's
* internal objects
* @constructor
*/
constructor (arrayBuffer) {
constructor (
arrayBuffer, start = 0, end = arrayBuffer.byteLength,
{
_uint8View = new Uint8Array(arrayBuffer)
} = {}
) {
/**
* Raw data buffer for stream to read.
* @type {ArrayBufferStream}
*/
this.arrayBuffer = arrayBuffer;
this.position = 0;
/**
* Start position in arrayBuffer. Read values are relative to the start
* in the arrayBuffer.
* @type {number}
*/
this.start = start;
/**
* End position in arrayBuffer. Length and bytes available are relative
* to the start, end, and _position in the arrayBuffer;
* @type {number};
*/
this.end = end;
/**
* Cached Uint8Array view of the arrayBuffer. Heavily used for reading
* Uint8 values and Strings from the stream.
* @type {Uint8Array}
*/
this._uint8View = _uint8View;
/**
* Raw position in the arrayBuffer relative to the beginning of the
* arrayBuffer.
* @type {number}
*/
this._position = start;
}
/**
@ -20,23 +62,40 @@ class ArrayBufferStream {
* @return {ArrayBufferStream} the extracted stream
*/
extract (length) {
const slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position + length);
const newStream = new ArrayBufferStream(slicedArrayBuffer);
return newStream;
return new ArrayBufferStream(this.arrayBuffer, this._position, this._position + length, this);
}
/**
* @return {number} the length of the stream in bytes
*/
getLength () {
return this.arrayBuffer.byteLength;
return this.end - this.start;
}
/**
* @return {number} the number of bytes available after the current position in the stream
*/
getBytesAvailable () {
return (this.arrayBuffer.byteLength - this.position);
return this.end - this._position;
}
/**
* Position relative to the start value in the arrayBuffer of this
* ArrayBufferStream.
* @type {number}
*/
get position () {
return this._position - this.start;
}
/**
* Set the position to read from in the arrayBuffer.
* @type {number}
* @param {number} value - new value to set position to
*/
set position (value) {
this._position = value + this.start;
return value;
}
/**
@ -44,8 +103,8 @@ class ArrayBufferStream {
* @return {number} the next 8 bit integer in the stream
*/
readUint8 () {
const val = new Uint8Array(this.arrayBuffer, this.position, 1)[0];
this.position += 1;
const val = this._uint8View[this._position];
this._position += 1;
return val;
}
@ -56,12 +115,13 @@ class ArrayBufferStream {
* @return {string} a String made by concatenating the chars in the input
*/
readUint8String (length) {
const arr = new Uint8Array(this.arrayBuffer, this.position, length);
this.position += length;
const arr = this._uint8View;
let str = '';
for (let i = 0; i < arr.length; i++) {
const end = this._position + length;
for (let i = this._position; i < end; i++) {
str += String.fromCharCode(arr[i]);
}
this._position += length;
return str;
}
@ -70,8 +130,8 @@ class ArrayBufferStream {
* @return {number} the next 16 bit integer in the stream
*/
readInt16 () {
const val = new Int16Array(this.arrayBuffer, this.position, 1)[0];
this.position += 2; // one 16 bit int is 2 bytes
const val = new Int16Array(this.arrayBuffer, this._position, 1)[0];
this._position += 2; // one 16 bit int is 2 bytes
return val;
}
@ -80,8 +140,8 @@ class ArrayBufferStream {
* @return {number} the next unsigned 16 bit integer in the stream
*/
readUint16 () {
const val = new Uint16Array(this.arrayBuffer, this.position, 1)[0];
this.position += 2; // one 16 bit int is 2 bytes
const val = new Uint16Array(this.arrayBuffer, this._position, 1)[0];
this._position += 2; // one 16 bit int is 2 bytes
return val;
}
@ -90,8 +150,8 @@ class ArrayBufferStream {
* @return {number} the next 32 bit integer in the stream
*/
readInt32 () {
const val = new Int32Array(this.arrayBuffer, this.position, 1)[0];
this.position += 4; // one 32 bit int is 4 bytes
const val = new Int32Array(this.arrayBuffer, this._position, 1)[0];
this._position += 4; // one 32 bit int is 4 bytes
return val;
}
@ -100,8 +160,8 @@ class ArrayBufferStream {
* @return {number} the next unsigned 32 bit integer in the stream
*/
readUint32 () {
const val = new Uint32Array(this.arrayBuffer, this.position, 1)[0];
this.position += 4; // one 32 bit int is 4 bytes
const val = new Uint32Array(this.arrayBuffer, this._position, 1)[0];
this._position += 4; // one 32 bit int is 4 bytes
return val;
}
}