Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ npm-*

/dist.js
/dist.js.map

yarn-debug.log*
yarn-error.log*
package-lock.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@
"tap": "^12.0.1",
"web-audio-test-api": "^0.5.2",
"webpack": "^4.8.0",
"webpack-cli": "^2.0.15"
"webpack-cli": "^3.1.2"
}
}
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);
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