mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2024-12-22 14:02:29 -05:00
wip on loading adpcm wav files
This commit is contained in:
parent
d400de1b4c
commit
85d8db5d14
3 changed files with 220 additions and 57 deletions
145
src/ADPCMSoundLoader.js
Normal file
145
src/ADPCMSoundLoader.js
Normal 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
71
src/ArrayBufferStream.js
Normal 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;
|
61
src/index.js
61
src/index.js
|
@ -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');
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue