Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADPCM Loop Optimizations #105

Merged
merged 3 commits into from
Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 124 additions & 55 deletions src/ADPCMSoundDecoder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
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
];

let _deltaTable = null;

/**
* Build a table of deltas from the 89 possible steps and 16 codes.
* @return {Array<number>} computed delta values
*/
const deltaTable = function () {
if (_deltaTable === null) {
const NUM_STEPS = STEP_TABLE.length;
const NUM_INDICES = INDEX_TABLE.length;
_deltaTable = new Array(NUM_STEPS * NUM_INDICES).fill(0);

This comment was marked as abuse.

This comment was marked as abuse.

let i = 0;

for (let index = 0; index < NUM_STEPS; index++) {
for (let code = 0; code < NUM_INDICES; code++) {
const step = STEP_TABLE[index];

let delta = 0;
if (code & 4) delta += step;
if (code & 2) delta += step >> 1;
if (code & 1) delta += step >> 2;
delta += step >> 3;
_deltaTable[i++] = (code & 8) ? -delta : delta;
}
}
}

return _deltaTable;
};

/**
* 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
Expand All @@ -16,30 +68,21 @@ 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;
}

/**
* Data used by the decompression algorithm
* @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;
}

/**
Expand Down Expand Up @@ -81,14 +124,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);
});
Expand All @@ -114,65 +154,94 @@ 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
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);
} else {
const size = out.length;
const samplesAfterBlockHeader = (blockSize - 4) * 2;

const DELTA_TABLE = deltaTable();

let i = 0;
while (i < size) {
// read block header
sample = compressedData.readInt16();
index = compressedData.readUint8();
compressedData.position++; // skip extra header byte
if (index > 88) index = 88;
out[i++] = sample / 32768;

const blockLength = Math.min(samplesAfterBlockHeader, size - i);
const blockStart = i;
while (i - blockStart < blockLength) {
// read 4-bit code and compute delta from previous sample
if (lastByte < 0) {
if (compressedData.getBytesAvailable() === 0) break;
lastByte = compressedData.readUint8();
code = lastByte & 0xF;
} else {
code = (lastByte >> 4) & 0xF;
lastByte = -1;
}
step = ADPCMSoundDecoder.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;
lastByte = compressedData.readUint8();
code = lastByte & 0xF;
delta = DELTA_TABLE[index * 16 + code];
// compute next index
index += INDEX_TABLE[code];
if (index > 88) index = 88;
else if (index < 0) index = 0;
// compute and output sample
sample += delta;
if (sample > 32767) sample = 32767;
else if (sample < -32768) sample = -32768;
out[i++] = sample / 32768;

// use 4-bit code from lastByte and compute delta from previous
// sample
code = (lastByte >> 4) & 0xF;
delta = DELTA_TABLE[index * 16 + code];
// compute next index
index += ADPCMSoundDecoder.INDEX_TABLE[code];
index += INDEX_TABLE[code];
if (index > 88) index = 88;
if (index < 0) index = 0;
else if (index < 0) index = 0;
// compute and output sample
sample += (code & 8) ? -delta : delta;
sample += delta;
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
out.push(sample);
else if (sample < -32768) sample = -32768;
out[i++] = sample / 32768;
}
}
const samples = Int16Array.from(out);
return samples;
}
}

Expand Down
Loading