scratch-audio
Advanced tools
Comparing version
{ | ||
"name": "scratch-audio", | ||
"version": "0.1.0-prerelease.20181023163139", | ||
"version": "0.1.0-prerelease.20181023202904", | ||
"description": "audio engine for scratch 3.0", | ||
@@ -5,0 +5,0 @@ "main": "dist.js", |
@@ -5,2 +5,54 @@ const ArrayBufferStream = require('./ArrayBufferStream'); | ||
/** | ||
* 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. | ||
@@ -20,2 +72,3 @@ * This is necessary because, while web browsers have native decoders for many audio | ||
} | ||
/** | ||
@@ -26,10 +79,3 @@ * Data used by the decompression algorithm | ||
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; | ||
} | ||
@@ -42,6 +88,3 @@ | ||
static get INDEX_TABLE () { | ||
return [ | ||
-1, -1, -1, -1, 2, 4, 6, 8, | ||
-1, -1, -1, -1, 2, 4, 6, 8 | ||
]; | ||
return INDEX_TABLE; | ||
} | ||
@@ -88,11 +131,8 @@ | ||
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); | ||
const buffer = this.audioContext.createBuffer(1, sampleCount, this.samplesPerSecond); | ||
this.imaDecompress(compressedData, this.adpcmBlockSize, buffer.getChannelData(0)); | ||
// @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; | ||
} | ||
resolve(buffer); | ||
@@ -123,2 +163,25 @@ }); | ||
/** | ||
* 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. | ||
@@ -128,7 +191,6 @@ * Note: Handles only one channel, 4-bits per sample. | ||
* @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; | ||
@@ -138,47 +200,54 @@ let delta; | ||
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 += 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; | ||
// 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 += 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; | ||
} | ||
} | ||
const samples = Int16Array.from(out); | ||
return samples; | ||
} | ||
@@ -185,0 +254,0 @@ } |
@@ -10,7 +10,49 @@ class ArrayBufferStream { | ||
* @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; | ||
} | ||
@@ -24,5 +66,3 @@ | ||
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); | ||
} | ||
@@ -34,3 +74,3 @@ | ||
getLength () { | ||
return this.arrayBuffer.byteLength; | ||
return this.end - this.start; | ||
} | ||
@@ -42,6 +82,25 @@ | ||
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; | ||
} | ||
/** | ||
* Read an unsigned 8 bit integer from the stream | ||
@@ -51,4 +110,4 @@ * @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; | ||
@@ -64,8 +123,9 @@ } | ||
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; | ||
@@ -79,4 +139,4 @@ } | ||
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; | ||
@@ -90,4 +150,4 @@ } | ||
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; | ||
@@ -101,4 +161,4 @@ } | ||
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; | ||
@@ -112,4 +172,4 @@ } | ||
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; | ||
@@ -116,0 +176,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
586801
2.02%4554
5%