2017-04-17 11:22:16 -04:00
|
|
|
const ArrayBufferStream = require('./ArrayBufferStream');
|
|
|
|
const log = require('./log');
|
2016-11-16 17:06:34 -05:00
|
|
|
|
2018-10-22 16:01:41 -04:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
];
|
|
|
|
|
2017-02-02 15:48:26 -05:00
|
|
|
/**
|
2017-03-22 18:08:44 -04:00
|
|
|
* Decode wav audio files that have been compressed with the ADPCM format.
|
2017-02-02 14:53:17 -05:00
|
|
|
* This is necessary because, while web browsers have native decoders for many audio
|
|
|
|
* formats, ADPCM is a non-standard format used by Scratch since its early days.
|
|
|
|
* This decoder is based on code from Scratch-Flash:
|
|
|
|
* https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as
|
|
|
|
*/
|
2017-04-17 12:55:09 -04:00
|
|
|
class ADPCMSoundDecoder {
|
2017-06-22 10:51:57 -04:00
|
|
|
/**
|
|
|
|
* @param {AudioContext} audioContext - a webAudio context
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
constructor (audioContext) {
|
|
|
|
this.audioContext = audioContext;
|
2017-06-20 16:50:02 -04:00
|
|
|
}
|
2018-10-22 16:01:41 -04:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
/**
|
|
|
|
* Data used by the decompression algorithm
|
|
|
|
* @type {Array}
|
|
|
|
*/
|
2017-04-18 17:59:37 -04:00
|
|
|
static get STEP_TABLE () {
|
2018-10-22 16:01:41 -04:00
|
|
|
return STEP_TABLE;
|
2017-04-17 12:55:09 -04:00
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
/**
|
|
|
|
* Data used by the decompression algorithm
|
|
|
|
* @type {Array}
|
|
|
|
*/
|
2017-04-18 17:59:37 -04:00
|
|
|
static get INDEX_TABLE () {
|
2018-10-22 16:01:41 -04:00
|
|
|
return INDEX_TABLE;
|
2017-04-17 12:55:09 -04:00
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
/**
|
|
|
|
* Decode an ADPCM sound stored in an ArrayBuffer and return a promise
|
|
|
|
* with the decoded audio buffer.
|
|
|
|
* @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio
|
2017-06-20 16:50:02 -04:00
|
|
|
* @return {AudioBuffer} the decoded audio buffer
|
2017-04-17 12:55:09 -04:00
|
|
|
*/
|
|
|
|
decode (audioData) {
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const stream = new ArrayBufferStream(audioData);
|
|
|
|
|
|
|
|
const riffStr = stream.readUint8String(4);
|
|
|
|
if (riffStr !== 'RIFF') {
|
|
|
|
log.warn('incorrect adpcm wav header');
|
|
|
|
reject();
|
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
const lengthInHeader = stream.readInt32();
|
|
|
|
if ((lengthInHeader + 8) !== audioData.byteLength) {
|
|
|
|
log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`);
|
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
const wavStr = stream.readUint8String(4);
|
|
|
|
if (wavStr !== 'WAVE') {
|
|
|
|
log.warn('incorrect adpcm wav header');
|
|
|
|
reject();
|
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
const formatChunk = this.extractChunk('fmt ', stream);
|
|
|
|
this.encoding = formatChunk.readUint16();
|
|
|
|
this.channels = formatChunk.readUint16();
|
|
|
|
this.samplesPerSecond = formatChunk.readUint32();
|
|
|
|
this.bytesPerSecond = formatChunk.readUint32();
|
|
|
|
this.blockAlignment = formatChunk.readUint16();
|
|
|
|
this.bitsPerSample = formatChunk.readUint16();
|
|
|
|
formatChunk.position += 2; // skip extra header byte count
|
|
|
|
this.samplesPerBlock = formatChunk.readUint16();
|
|
|
|
this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes
|
|
|
|
|
2018-10-22 16:01:41 -04:00
|
|
|
const compressedData = this.extractChunk('data', stream);
|
|
|
|
const sampleCount = this.numberOfSamples(compressedData, this.adpcmBlockSize);
|
2017-04-17 12:55:09 -04:00
|
|
|
|
2018-10-22 16:01:41 -04:00
|
|
|
const buffer = this.audioContext.createBuffer(1, sampleCount, this.samplesPerSecond);
|
|
|
|
this.imaDecompress(compressedData, this.adpcmBlockSize, buffer.getChannelData(0));
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
resolve(buffer);
|
|
|
|
});
|
|
|
|
}
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
/**
|
|
|
|
* Extract a chunk of audio data from the stream, consisting of a set of audio data bytes
|
|
|
|
* @param {string} chunkType - the type of chunk to extract. 'data' or 'fmt' (format)
|
|
|
|
* @param {ArrayBufferStream} stream - an stream containing the audio data
|
|
|
|
* @return {ArrayBufferStream} a stream containing the desired chunk
|
|
|
|
*/
|
|
|
|
extractChunk (chunkType, stream) {
|
|
|
|
stream.position = 12;
|
|
|
|
while (stream.position < (stream.getLength() - 8)) {
|
|
|
|
const typeStr = stream.readUint8String(4);
|
|
|
|
const chunkSize = stream.readInt32();
|
|
|
|
if (typeStr === chunkType) {
|
|
|
|
const chunk = stream.extract(chunkSize);
|
|
|
|
return chunk;
|
|
|
|
}
|
|
|
|
stream.position += chunkSize;
|
2016-11-19 14:17:43 -05:00
|
|
|
|
2016-11-16 17:06:34 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-22 16:01:41 -04:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2017-04-17 12:55:09 -04:00
|
|
|
/**
|
|
|
|
* 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
|
2018-10-22 16:01:41 -04:00
|
|
|
* @param {Float32Array} out - the uncompressed audio samples
|
2017-04-17 12:55:09 -04:00
|
|
|
*/
|
2018-10-22 16:01:41 -04:00
|
|
|
imaDecompress (compressedData, blockSize, out) {
|
2017-04-17 12:55:09 -04:00
|
|
|
let sample;
|
|
|
|
let step;
|
|
|
|
let code;
|
|
|
|
let delta;
|
|
|
|
let index = 0;
|
|
|
|
let lastByte = -1; // -1 indicates that there is no saved lastByte
|
|
|
|
|
|
|
|
// Bail and return no samples if we have no data
|
2018-10-22 16:01:41 -04:00
|
|
|
if (!compressedData) return;
|
2017-04-17 12:55:09 -04:00
|
|
|
|
|
|
|
compressedData.position = 0;
|
2017-04-18 18:05:36 -04:00
|
|
|
|
2018-10-22 16:01:41 -04:00
|
|
|
const size = out.length;
|
|
|
|
|
|
|
|
let i = 0;
|
|
|
|
while (i < size) {
|
2017-04-17 12:55:09 -04:00
|
|
|
if (((compressedData.position % blockSize) === 0) && (lastByte < 0)) { // read block header
|
2017-04-17 11:44:51 -04:00
|
|
|
if (compressedData.getBytesAvailable() === 0) break;
|
2017-04-17 12:55:09 -04:00
|
|
|
sample = compressedData.readInt16();
|
|
|
|
index = compressedData.readUint8();
|
|
|
|
compressedData.position++; // skip extra header byte
|
|
|
|
if (index > 88) index = 88;
|
2018-10-22 16:01:41 -04:00
|
|
|
out[i++] = sample / 32768;
|
2016-11-16 17:06:34 -05:00
|
|
|
} else {
|
2017-04-17 12:55:09 -04:00
|
|
|
// 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;
|
|
|
|
}
|
2018-10-22 16:01:41 -04:00
|
|
|
step = STEP_TABLE[index];
|
2017-04-17 12:55:09 -04:00
|
|
|
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
|
2018-10-22 16:01:41 -04:00
|
|
|
index += INDEX_TABLE[code];
|
2017-04-17 12:55:09 -04:00
|
|
|
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;
|
2018-10-22 16:01:41 -04:00
|
|
|
out[i++] = sample / 32768;
|
2016-11-16 17:06:34 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-04-17 12:55:09 -04:00
|
|
|
}
|
2016-11-16 17:06:34 -05:00
|
|
|
|
2017-03-22 18:08:44 -04:00
|
|
|
module.exports = ADPCMSoundDecoder;
|