UNPKG

9.19 kBJavaScriptView Raw
1const ArrayBufferStream = require('./ArrayBufferStream');
2const log = require('./log');
3
4/**
5 * Data used by the decompression algorithm
6 * @type {Array}
7 */
8const STEP_TABLE = [
9 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
10 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230,
11 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963,
12 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327,
13 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487,
14 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
15];
16
17/**
18 * Data used by the decompression algorithm
19 * @type {Array}
20 */
21const INDEX_TABLE = [
22 -1, -1, -1, -1, 2, 4, 6, 8,
23 -1, -1, -1, -1, 2, 4, 6, 8
24];
25
26let _deltaTable = null;
27
28/**
29 * Build a table of deltas from the 89 possible steps and 16 codes.
30 * @return {Array<number>} computed delta values
31 */
32const deltaTable = function () {
33 if (_deltaTable === null) {
34 const NUM_STEPS = STEP_TABLE.length;
35 const NUM_INDICES = INDEX_TABLE.length;
36 _deltaTable = new Array(NUM_STEPS * NUM_INDICES).fill(0);
37 let i = 0;
38
39 for (let index = 0; index < NUM_STEPS; index++) {
40 for (let code = 0; code < NUM_INDICES; code++) {
41 const step = STEP_TABLE[index];
42
43 let delta = 0;
44 if (code & 4) delta += step;
45 if (code & 2) delta += step >> 1;
46 if (code & 1) delta += step >> 2;
47 delta += step >> 3;
48 _deltaTable[i++] = (code & 8) ? -delta : delta;
49 }
50 }
51 }
52
53 return _deltaTable;
54};
55
56/**
57 * Decode wav audio files that have been compressed with the ADPCM format.
58 * This is necessary because, while web browsers have native decoders for many audio
59 * formats, ADPCM is a non-standard format used by Scratch since its early days.
60 * This decoder is based on code from Scratch-Flash:
61 * https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as
62 */
63class ADPCMSoundDecoder {
64 /**
65 * @param {AudioContext} audioContext - a webAudio context
66 * @constructor
67 */
68 constructor (audioContext) {
69 this.audioContext = audioContext;
70 }
71
72 /**
73 * Data used by the decompression algorithm
74 * @type {Array}
75 */
76 static get STEP_TABLE () {
77 return STEP_TABLE;
78 }
79
80 /**
81 * Data used by the decompression algorithm
82 * @type {Array}
83 */
84 static get INDEX_TABLE () {
85 return INDEX_TABLE;
86 }
87
88 /**
89 * Decode an ADPCM sound stored in an ArrayBuffer and return a promise
90 * with the decoded audio buffer.
91 * @param {ArrayBuffer} audioData - containing ADPCM encoded wav audio
92 * @return {Promise.<AudioBuffer>} the decoded audio buffer
93 */
94 decode (audioData) {
95
96 return new Promise((resolve, reject) => {
97 const stream = new ArrayBufferStream(audioData);
98
99 const riffStr = stream.readUint8String(4);
100 if (riffStr !== 'RIFF') {
101 log.warn('incorrect adpcm wav header');
102 reject(new Error('incorrect adpcm wav header'));
103 }
104
105 const lengthInHeader = stream.readInt32();
106 if ((lengthInHeader + 8) !== audioData.byteLength) {
107 log.warn(`adpcm wav length in header: ${lengthInHeader} is incorrect`);
108 }
109
110 const wavStr = stream.readUint8String(4);
111 if (wavStr !== 'WAVE') {
112 log.warn('incorrect adpcm wav header');
113 reject(new Error('incorrect adpcm wav header'));
114 }
115
116 const formatChunk = this.extractChunk('fmt ', stream);
117 this.encoding = formatChunk.readUint16();
118 this.channels = formatChunk.readUint16();
119 this.samplesPerSecond = formatChunk.readUint32();
120 this.bytesPerSecond = formatChunk.readUint32();
121 this.blockAlignment = formatChunk.readUint16();
122 this.bitsPerSample = formatChunk.readUint16();
123 formatChunk.position += 2; // skip extra header byte count
124 this.samplesPerBlock = formatChunk.readUint16();
125 this.adpcmBlockSize = ((this.samplesPerBlock - 1) / 2) + 4; // block size in bytes
126
127 const compressedData = this.extractChunk('data', stream);
128 const sampleCount = this.numberOfSamples(compressedData, this.adpcmBlockSize);
129
130 const buffer = this.audioContext.createBuffer(1, sampleCount, this.samplesPerSecond);
131 this.imaDecompress(compressedData, this.adpcmBlockSize, buffer.getChannelData(0));
132
133 resolve(buffer);
134 });
135 }
136
137 /**
138 * Extract a chunk of audio data from the stream, consisting of a set of audio data bytes
139 * @param {string} chunkType - the type of chunk to extract. 'data' or 'fmt' (format)
140 * @param {ArrayBufferStream} stream - an stream containing the audio data
141 * @return {ArrayBufferStream} a stream containing the desired chunk
142 */
143 extractChunk (chunkType, stream) {
144 stream.position = 12;
145 while (stream.position < (stream.getLength() - 8)) {
146 const typeStr = stream.readUint8String(4);
147 const chunkSize = stream.readInt32();
148 if (typeStr === chunkType) {
149 const chunk = stream.extract(chunkSize);
150 return chunk;
151 }
152 stream.position += chunkSize;
153
154 }
155 }
156
157 /**
158 * Count the exact number of samples in the compressed data.
159 * @param {ArrayBufferStream} compressedData - the compressed data
160 * @param {number} blockSize - size of each block in the data in bytes
161 * @return {number} number of samples in the compressed data
162 */
163 numberOfSamples (compressedData, blockSize) {
164 if (!compressedData) return 0;
165
166 compressedData.position = 0;
167
168 const available = compressedData.getBytesAvailable();
169 const blocks = (available / blockSize) | 0;
170 // Number of samples in full blocks.
171 const fullBlocks = (blocks * (2 * (blockSize - 4))) + 1;
172 // Number of samples in the last incomplete block. 0 if the last block
173 // is full.
174 const subBlock = Math.max((available % blockSize) - 4, 0) * 2;
175 // 1 if the last block is incomplete. 0 if it is complete.
176 const incompleteBlock = Math.min(available % blockSize, 1);
177 return fullBlocks + subBlock + incompleteBlock;
178 }
179
180 /**
181 * Decompress sample data using the IMA ADPCM algorithm.
182 * Note: Handles only one channel, 4-bits per sample.
183 * @param {ArrayBufferStream} compressedData - a stream of compressed audio samples
184 * @param {number} blockSize - the number of bytes in the stream
185 * @param {Float32Array} out - the uncompressed audio samples
186 */
187 imaDecompress (compressedData, blockSize, out) {
188 let sample;
189 let code;
190 let delta;
191 let index = 0;
192 let lastByte = -1; // -1 indicates that there is no saved lastByte
193
194 // Bail and return no samples if we have no data
195 if (!compressedData) return;
196
197 compressedData.position = 0;
198
199 const size = out.length;
200 const samplesAfterBlockHeader = (blockSize - 4) * 2;
201
202 const DELTA_TABLE = deltaTable();
203
204 let i = 0;
205 while (i < size) {
206 // read block header
207 sample = compressedData.readInt16();
208 index = compressedData.readUint8();
209 compressedData.position++; // skip extra header byte
210 if (index > 88) index = 88;
211 out[i++] = sample / 32768;
212
213 const blockLength = Math.min(samplesAfterBlockHeader, size - i);
214 const blockStart = i;
215 while (i - blockStart < blockLength) {
216 // read 4-bit code and compute delta from previous sample
217 lastByte = compressedData.readUint8();
218 code = lastByte & 0xF;
219 delta = DELTA_TABLE[(index * 16) + code];
220 // compute next index
221 index += INDEX_TABLE[code];
222 if (index > 88) index = 88;
223 else if (index < 0) index = 0;
224 // compute and output sample
225 sample += delta;
226 if (sample > 32767) sample = 32767;
227 else if (sample < -32768) sample = -32768;
228 out[i++] = sample / 32768;
229
230 // use 4-bit code from lastByte and compute delta from previous
231 // sample
232 code = (lastByte >> 4) & 0xF;
233 delta = DELTA_TABLE[(index * 16) + code];
234 // compute next index
235 index += INDEX_TABLE[code];
236 if (index > 88) index = 88;
237 else if (index < 0) index = 0;
238 // compute and output sample
239 sample += delta;
240 if (sample > 32767) sample = 32767;
241 else if (sample < -32768) sample = -32768;
242 out[i++] = sample / 32768;
243 }
244 }
245 }
246}
247
248module.exports = ADPCMSoundDecoder;