203 lines
6.1 KiB
JavaScript
203 lines
6.1 KiB
JavaScript
//This is adapted from https://github.com/Snack-X/node-ms-adpcm
|
|
//I tried to find a fast decoder for MSADPCM in nodejs and came up short.
|
|
//Maybe I didn't look hard enough.
|
|
//With some work, this did the job well for me.
|
|
|
|
const ADAPTATION_TABLE = [
|
|
230, 230, 230, 230, 307, 409, 512, 614,
|
|
768, 614, 512, 409, 307, 230, 230, 230,
|
|
];
|
|
|
|
function clamp(val, min, max) {
|
|
if(val < min) return min;
|
|
else if(val > max) return max;
|
|
else return val;
|
|
}
|
|
|
|
function expandNibble(nibble, state, channel) {
|
|
const signed = 8 <= nibble ? nibble - 16 : nibble;
|
|
|
|
let predictor = ((
|
|
state.sample1[channel] * state.coeff1[channel] +
|
|
state.sample2[channel] * state.coeff2[channel]
|
|
) >> 8) + (signed * state.delta[channel]);
|
|
|
|
predictor = clamp(predictor, -0x8000, 0x7fff);
|
|
|
|
state.sample2[channel] = state.sample1[channel];
|
|
state.sample1[channel] = predictor;
|
|
|
|
state.delta[channel] = Math.floor(ADAPTATION_TABLE[nibble] * state.delta[channel] / 256);
|
|
if(state.delta[channel] < 16) state.delta[channel] = 16;
|
|
|
|
return predictor;
|
|
}
|
|
|
|
/**
|
|
* Decode a block of MS-ADPCM data
|
|
* @param {Buffer} buf one block of MS-ADPCM data
|
|
* @param {number} channels number of channels (usually 1 or 2, never tested on upper values)
|
|
* @param {number[]} coefficient1 array of 7 UInt8 coefficient values
|
|
* usually, [ 256, 512, 0, 192, 240, 460, 392 ]
|
|
* @param {number[]} coefficient2 array of 7 UInt8 coefficient values
|
|
* usually, [ 0, -256, 0, 64, 0, -208, -232 ]
|
|
* @return {Buffer[]} array of decoded PCM buffer for each channels
|
|
*/
|
|
function decode(buf, channels, coefficient1, coefficient2) {
|
|
const state = {
|
|
coefficient: [ coefficient1, coefficient2 ],
|
|
coeff1: [],
|
|
coeff2: [],
|
|
delta: [],
|
|
sample1: [],
|
|
sample2: [],
|
|
};
|
|
|
|
let offset = 0;
|
|
|
|
// Read MS-ADPCM header
|
|
for(let i = 0 ; i < channels ; i++) {
|
|
const predictor = clamp(buf.readUInt8(offset), 0, 6);
|
|
offset += 1;
|
|
|
|
state.coeff1[i] = state.coefficient[0][predictor];
|
|
state.coeff2[i] = state.coefficient[1][predictor];
|
|
}
|
|
|
|
for(let i = 0 ; i < channels ; i++) { state.delta.push(buf.readInt16LE(offset)); offset += 2; }
|
|
for(let i = 0 ; i < channels ; i++) { state.sample1.push(buf.readInt16LE(offset)); offset += 2; }
|
|
for(let i = 0 ; i < channels ; i++) { state.sample2.push(buf.readInt16LE(offset)); offset += 2; }
|
|
|
|
// Decode
|
|
const output = [];
|
|
|
|
for(let i = 0 ; i < channels ; i++)
|
|
output[i] = [ state.sample2[i], state.sample1[i] ];
|
|
|
|
let channel = 0;
|
|
while(offset < buf.length) {
|
|
const byte = buf.readUInt8(offset);
|
|
offset += 1;
|
|
|
|
output[channel].push(expandNibble(byte >> 4, state, channel));
|
|
channel = (channel + 1) % channels;
|
|
|
|
output[channel].push(expandNibble(byte & 0xf, state, channel));
|
|
channel = (channel + 1) % channels;
|
|
}
|
|
|
|
//Converting all sound to stereo since it'll be easier later on.
|
|
if (channels == 1) {
|
|
output.push(output[0]);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function readWav(buf) {
|
|
|
|
let offset = 0;
|
|
|
|
// 'RIFF'
|
|
const magic = buf.readUInt32BE(offset); offset += 4;
|
|
if(magic !== 0x52494646) {
|
|
console.log(magic);
|
|
throw "0x0000:0x0004 != 52:49:46:46";
|
|
}
|
|
|
|
const dataSize = buf.readUInt32LE(offset); offset += 4;
|
|
|
|
// 'WAVE'
|
|
const format = buf.readUInt32BE(offset); offset += 4;
|
|
if(format !== 0x57415645) throw "0x0008:0x000B != 57:41:56:45";
|
|
|
|
let wavFormat, wavData;
|
|
|
|
while(offset < buf.length) {
|
|
const name = buf.readUInt32BE(offset); offset += 4;
|
|
const blockSize = buf.readUInt32LE(offset); offset += 4;
|
|
|
|
// 'fmt '
|
|
if(name === 0x666D7420) {
|
|
wavFormat = {
|
|
format: buf.readUInt16LE(offset + 0),
|
|
channels: buf.readUInt16LE(offset + 2),
|
|
sampleRate: buf.readUInt32LE(offset + 4),
|
|
byteRate: buf.readUInt32LE(offset + 8),
|
|
blockAlign: buf.readUInt16LE(offset + 12),
|
|
bitsPerSample: buf.readUInt16LE(offset + 14),
|
|
};
|
|
|
|
offset += 16;
|
|
|
|
if(wavFormat.format === 0x01) {
|
|
// console.log(`${filename} is PCM file`);
|
|
continue;
|
|
}
|
|
else if(wavFormat.format === 0x02) {
|
|
// console.log(`${filename} is MS-ADPCM file`);
|
|
|
|
const extraSize = buf.readUInt16LE(offset); offset += 2;
|
|
wavFormat.extraSize = extraSize;
|
|
wavFormat.extra = {
|
|
samplesPerBlock: buf.readUInt16LE(offset + 0),
|
|
coefficientCount: buf.readUInt16LE(offset + 2),
|
|
coefficient: [ [], [] ],
|
|
};
|
|
|
|
offset += 4;
|
|
|
|
for(let i = 0 ; i < wavFormat.extra.coefficientCount ; i++) {
|
|
wavFormat.extra.coefficient[0].push(buf.readInt16LE(offset + 0));
|
|
wavFormat.extra.coefficient[1].push(buf.readInt16LE(offset + 2));
|
|
offset += 4;
|
|
}
|
|
}
|
|
else throw `WAVE format ${wavFormat.format} is unknown`;
|
|
}
|
|
// 'data'
|
|
else if(name === 0x64617461) {
|
|
wavData = buf.slice(offset, offset + blockSize);
|
|
offset += blockSize;
|
|
}
|
|
else {
|
|
offset += blockSize;
|
|
}
|
|
}
|
|
|
|
if(wavFormat && wavData) return { format: wavFormat, data: wavData };
|
|
else throw "'fmt ' or/and 'data' block not found";
|
|
}
|
|
|
|
exports.decodeKeysoundOut = (buff, vol) => {
|
|
const adpcmData = readWav(buff);
|
|
const blockSize = adpcmData.format.blockAlign;
|
|
|
|
let totalBuff = Buffer.alloc(1);
|
|
const totalBlocks = adpcmData.data.length / blockSize;
|
|
let totalOffset = 0;
|
|
|
|
for(let i = 0 ; i < adpcmData.data.length ; i += blockSize) {
|
|
const adpcmBlock = adpcmData.data.slice(i, i + blockSize);
|
|
const decoded = decode(
|
|
adpcmBlock,
|
|
adpcmData.format.channels,
|
|
adpcmData.format.extra.coefficient[0],
|
|
adpcmData.format.extra.coefficient[1]
|
|
);
|
|
|
|
const pcmBlockSize = decoded[0].length * 2;
|
|
if (totalBuff.length == 1) {
|
|
totalBuff = Buffer.alloc(pcmBlockSize * totalBlocks * 2);
|
|
}
|
|
|
|
for(let s = 0 ; s < pcmBlockSize/2; s++) {
|
|
for(let c = 0 ; c < decoded.length ; c++) {
|
|
totalBuff.writeInt16LE(decoded[c][s], totalOffset);
|
|
totalOffset += 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {data: totalBuff, channels: adpcmData.format.channels, samplingRate: adpcmData.format.sampleRate, volume: vol};
|
|
} |