wip on loading adpcm wav files

This commit is contained in:
Eric Rosenbaum 2016-11-16 17:06:34 -05:00
parent d400de1b4c
commit 85d8db5d14
3 changed files with 220 additions and 57 deletions

145
src/ADPCMSoundLoader.js Normal file
View file

@ -0,0 +1,145 @@
/*
ADPCMSoundLoader loads wav files that have been compressed with the ADPCM format
based on code from Scratch-Flash:
https://github.com/LLK/scratch-flash/blob/master/src/sound/WAVFile.as
*/
var ArrayBufferStream = require('./ArrayBufferStream');
var Tone = require('tone');
var log = require('./log');
function ADPCMSoundLoader (url) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function () {
var audioData = request.response;
var stream = new ArrayBufferStream(audioData);
var riffStr = stream.readUint8String(4);
if (riffStr != 'RIFF') {
log.warn('incorrect adpcm wav header');
}
var lengthInHeader = stream.readInt32();
if ((lengthInHeader + 8) != audioData.byteLength) {
log.warn('adpcm wav length in header: ' + length + 'is incorrect');
}
var wavStr = stream.readUint8String(4);
if (wavStr != 'WAVE') {
log.warn('incorrect adpcm wav header');
}
var 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
var samples = this.imaDecompress(this.extractChunk('data', stream), this.adpcmBlockSize);
var buffer = Tone.context.createBuffer(1, samples.length, this.samplesPerSecond);
// todo: optimize this?
for (var i=0; i<samples.length; i++) {
buffer.getChannelData(0)[i] = samples[i] / 32768;
}
var source = Tone.context.createBufferSource();
source.buffer = buffer;
source.connect(Tone.Master);
source.start();
}.bind(this);
request.send();
this.stepTable = [
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];
this.indexTable = [
-1, -1, -1, -1, 2, 4, 6, 8,
-1, -1, -1, -1, 2, 4, 6, 8];
}
ADPCMSoundLoader.prototype.extractChunk = function (chunkType, stream) {
stream.position = 12;
while (stream.position < (stream.getLength() - 8)) {
var typeStr = stream.readUint8String(4);
var chunkSize = stream.readInt32();
if (typeStr == chunkType) {
var chunk = stream.extract(chunkSize);
return chunk;
} else {
stream.position += chunkSize;
}
}
};
ADPCMSoundLoader.prototype.imaDecompress = function (compressedData, blockSize) {
// Decompress sample data using the IMA ADPCM algorithm.
// Note: Handles only one channel, 4-bits/sample.
var sample, step, code, delta;
var index = 0;
var lastByte = -1; // -1 indicates that there is no saved lastByte
var out = [];
// Bail and return no samples if we have no data
if (!compressedData) return out;
compressedData.position = 0;
var a = 0;
while (a==0) {
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 {
// 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 = this.stepTable[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 += this.indexTable[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);
}
}
var samples = Int16Array.from(out);
return samples;
};
module.exports = ADPCMSoundLoader;

71
src/ArrayBufferStream.js Normal file
View file

@ -0,0 +1,71 @@
/*
ArrayBufferStream wraps the built-in javascript ArrayBuffer, adding the ability to access
data in it like a stream. You can request to read a value from the front of the array,
such as an 8 bit unsigned int, a 16 bit int, etc, and it will keep track of the position
within the byte array, so that successive reads are consecutive.
*/
function ArrayBufferStream (arrayBuffer) {
this.arrayBuffer = arrayBuffer;
this.position = 0;
}
// return a new ArrayBufferStream that is a slice of the existing one
ArrayBufferStream.prototype.extract = function (length) {
var slicedArrayBuffer = this.arrayBuffer.slice(this.position, this.position+length);
var newStream = new ArrayBufferStream(slicedArrayBuffer);
return newStream;
};
ArrayBufferStream.prototype.getLength = function () {
return this.arrayBuffer.byteLength;
};
ArrayBufferStream.prototype.getBytesAvailable = function () {
return (this.arrayBuffer.byteLength - this.position);
};
ArrayBufferStream.prototype.readUint8 = function () {
var val = new Uint8Array(this.arrayBuffer, this.position, 1)[0];
this.position += 1;
return val;
};
// convert a sequence of bytes of the given length to a string
// for small length strings only
ArrayBufferStream.prototype.readUint8String = function (length) {
var arr = new Uint8Array(this.arrayBuffer, this.position, length);
this.position += length;
var str = '';
for (var i=0; i<arr.length; i++) {
str += String.fromCharCode(arr[i]);
}
return str;
};
ArrayBufferStream.prototype.readInt16 = function () {
var val = new Int16Array(this.arrayBuffer, this.position, 1)[0];
this.position += 2; // one 16 bit int is 2 bytes
return val;
};
ArrayBufferStream.prototype.readUint16 = function () {
var val = new Uint16Array(this.arrayBuffer, this.position, 1)[0];
this.position += 2; // one 16 bit int is 2 bytes
return val;
};
ArrayBufferStream.prototype.readInt32 = function () {
var val = new Int32Array(this.arrayBuffer, this.position, 1)[0];
this.position += 4; // one 32 bit int is 4 bytes
return val;
};
ArrayBufferStream.prototype.readUint32 = function () {
var val = new Uint32Array(this.arrayBuffer, this.position, 1)[0];
this.position += 4; // one 32 bit int is 4 bytes
return val;
};
module.exports = ArrayBufferStream;

View file

@ -2,6 +2,7 @@ var log = require('./log');
var Tone = require('tone');
// var Soundfont = require('soundfont-player');
var Vocoder = require('./vocoder');
var ADPCMSoundLoader = require('./ADPCMSoundLoader');
function AudioEngine (sounds) {
@ -70,7 +71,9 @@ AudioEngine.prototype.loadSounds = function (sounds) {
if (sounds[i].format == 'adpcm') {
log.warn('attempting to load sound in adpcm format');
buffer = this._loadSoundADPCM(sounds[i].fileUrl);
var loader = new ADPCMSoundLoader(sounds[i].fileUrl);
// var audioBuffer = loader.getAudioBuffer();
// buffer = new Tone.Buffer(audioBuffer);
} else {
buffer = new Tone.Buffer(sounds[i].fileUrl);
}
@ -84,62 +87,6 @@ AudioEngine.prototype.loadSounds = function (sounds) {
return soundPlayers;
};
AudioEngine.prototype._loadSoundADPCM = function (url) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function () {
var audioData = request.response;
// convert a Uint8 array to a string
function Uint8ToStr (arr) {
var str = '';
for (var i=0; i<arr.length; i++) {
str += String.fromCharCode(arr[i]);
}
return str;
}
// RIFF
var riffData = new Uint8Array(audioData, 0, 4);
if (Uint8ToStr(riffData) != 'RIFF') {
log.warn('adpcm wav header missing RIFF');
}
// length
var length = new Int32Array(audioData, 4, 1)[0];
if ((length + 8) != audioData.byteLength) {
log.warn('adpcm wav length in header: ' + length + 'is incorrect');
}
// WAVE
var waveData = new Uint8Array(audioData, 8, 4);
if (Uint8ToStr(waveData) != 'WAVE') {
log.warn('adpcm wave header missing WAVE');
}
extractChunk('fmt', audioData);
function extractChunk (chunkType, arrayBuffer) {
var position = 12; // start at the 12th byte, after RIFF + length + WAVE
while (position < (arrayBuffer.byteLength - 8)) {
var type = new Uint8Array(arrayBuffer, position, 4);
var typeStr = Uint8ToStr(type);
position += 4;
var chunkSize = new Uint32Array(arrayBuffer, position, 1)[0];
position += 4;
position += chunkSize;
if (typeStr == chunkType) {
}
}
}
};
request.send();
};
// AudioEngine.prototype._soundsLoaded = function() {
// console.log('all sounds loaded');
// }