Files
feedBack/tests/plugins/tuner/js/helpers/wav-parser.js
T
2026-06-16 18:47:13 +02:00

81 lines
3.1 KiB
JavaScript

'use strict';
/**
* Minimal PCM WAV parser for Node.js test environments.
* Supports: mono/stereo, 16-bit and 24-bit signed PCM (audio format 1).
* Mixes all channels down to mono and returns a normalized Float32Array in [-1, 1].
*/
function parseWav(nodeBuffer) {
// Node.js Buffer.buffer is a pooled ArrayBuffer; slice to get an exact view.
const ab = nodeBuffer.buffer.slice(
nodeBuffer.byteOffset,
nodeBuffer.byteOffset + nodeBuffer.byteLength
);
const view = new DataView(ab);
const bytes = new Uint8Array(ab);
function fourCC(offset) {
return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
}
if (fourCC(0) !== 'RIFF' || fourCC(8) !== 'WAVE') {
throw new Error('Not a valid WAV file');
}
let audioFormat = 0, numChannels = 0, sampleRate = 0, bitsPerSample = 0;
let dataStart = -1, dataSize = 0;
let offset = 12;
while (offset + 8 <= ab.byteLength) {
const id = fourCC(offset);
const size = view.getUint32(offset + 4, true);
if (id === 'fmt ') {
audioFormat = view.getUint16(offset + 8, true);
numChannels = view.getUint16(offset + 10, true);
sampleRate = view.getUint32(offset + 12, true);
bitsPerSample = view.getUint16(offset + 22, true);
} else if (id === 'data') {
dataStart = offset + 8;
dataSize = size;
break;
}
offset += 8 + size + (size & 1); // chunks are word-aligned
}
if (dataStart === -1) throw new Error('No data chunk found in WAV file');
if (audioFormat !== 1) throw new Error(`Only PCM WAV supported (audio format ${audioFormat})`);
if (bitsPerSample !== 16 && bitsPerSample !== 24) {
throw new Error(`Only 16-bit and 24-bit PCM supported (got ${bitsPerSample}-bit)`);
}
const bytesPerSample = bitsPerSample >> 3;
const bytesPerFrame = numChannels * bytesPerSample;
const nFrames = Math.floor(dataSize / bytesPerFrame);
if (dataStart + nFrames * bytesPerFrame > ab.byteLength) {
throw new Error('WAV data chunk extends beyond end of file');
}
const samples = new Float32Array(nFrames);
for (let i = 0; i < nFrames; i++) {
let sum = 0;
for (let ch = 0; ch < numChannels; ch++) {
const byteOffset = dataStart + i * bytesPerFrame + ch * bytesPerSample;
if (bitsPerSample === 16) {
sum += view.getInt16(byteOffset, true);
} else {
// 24-bit: read 3 bytes little-endian, sign-extend from bit 23.
const lo = bytes[byteOffset];
const mi = bytes[byteOffset + 1];
const hi = bytes[byteOffset + 2];
const raw = (lo | (mi << 8) | (hi << 16));
sum += raw & 0x800000 ? raw - 0x1000000 : raw;
}
}
const scale = bitsPerSample === 16 ? 32768.0 : 8388608.0;
samples[i] = (sum / numChannels) / scale;
}
return { samples, sampleRate };
}
module.exports = { parseWav };