scratch-audio
Advanced tools
Comparing version 0.1.0-prerelease.20181023163139 to 0.1.0-prerelease.20181023202904
{ | ||
"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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
586801
4554