mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2025-03-13 16:33:33 -04:00
Merge pull request #104 from mzgoddard/js-optimizations
ADPCM JS Optimizations
This commit is contained in:
commit
468aa9bf6f
2 changed files with 143 additions and 51 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue