157 lines
4.7 KiB
TypeScript
157 lines
4.7 KiB
TypeScript
/**
|
|
* Programmatic WAV fixture builders used by the test suite. No binary files
|
|
* are checked in — every fixture is generated deterministically from
|
|
* primitives at test time. Each builder returns a `Uint8Array` ready to be
|
|
* fed to `decodeWav` or `createWavStreamDecoder`.
|
|
*/
|
|
|
|
export interface BuildWavInput {
|
|
readonly sampleRate: number;
|
|
readonly channels: number;
|
|
readonly bitsPerSample: number;
|
|
readonly samplesPerChannel: number;
|
|
/** `1` for integer PCM, `3` for IEEE float PCM. */
|
|
readonly formatCode: 1 | 3;
|
|
/** Per-channel sample generator. Defaults to silence. */
|
|
readonly sampler?: (channel: number, index: number) => number;
|
|
}
|
|
|
|
/** Build a well-formed RIFF/WAVE byte buffer. */
|
|
export function buildWav(input: BuildWavInput): Uint8Array {
|
|
const {
|
|
sampleRate,
|
|
channels,
|
|
bitsPerSample,
|
|
samplesPerChannel,
|
|
formatCode,
|
|
sampler = () => 0,
|
|
} = input;
|
|
const bytesPerSample = bitsPerSample / 8;
|
|
const dataBytes = samplesPerChannel * channels * bytesPerSample;
|
|
const headerBytes = 44;
|
|
const buffer = new Uint8Array(headerBytes + dataBytes);
|
|
const dv = new DataView(buffer.buffer);
|
|
writeAscii(buffer, 0, "RIFF");
|
|
dv.setUint32(4, headerBytes - 8 + dataBytes, true);
|
|
writeAscii(buffer, 8, "WAVE");
|
|
writeAscii(buffer, 12, "fmt ");
|
|
dv.setUint32(16, 16, true);
|
|
dv.setUint16(20, formatCode, true);
|
|
dv.setUint16(22, channels, true);
|
|
dv.setUint32(24, sampleRate, true);
|
|
dv.setUint32(28, sampleRate * channels * bytesPerSample, true);
|
|
dv.setUint16(32, channels * bytesPerSample, true);
|
|
dv.setUint16(34, bitsPerSample, true);
|
|
writeAscii(buffer, 36, "data");
|
|
dv.setUint32(40, dataBytes, true);
|
|
let p = headerBytes;
|
|
for (let i = 0; i < samplesPerChannel; i++) {
|
|
for (let c = 0; c < channels; c++) {
|
|
const v = sampler(c, i);
|
|
p += writeSample(dv, p, v, bitsPerSample, formatCode);
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
function writeAscii(b: Uint8Array, off: number, s: string): void {
|
|
for (let i = 0; i < s.length; i++) b[off + i] = s.charCodeAt(i);
|
|
}
|
|
|
|
function writeSample(
|
|
dv: DataView,
|
|
pos: number,
|
|
value: number,
|
|
bits: number,
|
|
fmt: 1 | 3
|
|
): number {
|
|
if (fmt === 3) {
|
|
if (bits === 32) {
|
|
dv.setFloat32(pos, value, true);
|
|
return 4;
|
|
}
|
|
dv.setFloat64(pos, value, true);
|
|
return 8;
|
|
}
|
|
if (bits === 16) {
|
|
const clipped = clip(value);
|
|
const v = Math.round(clipped < 0 ? clipped * 32768 : clipped * 32767);
|
|
dv.setInt16(pos, v, true);
|
|
return 2;
|
|
}
|
|
if (bits === 24) {
|
|
const clipped = clip(value);
|
|
const v = Math.round(
|
|
clipped < 0 ? clipped * 8388608 : clipped * 8388607
|
|
);
|
|
dv.setUint8(pos, v & 0xff);
|
|
dv.setUint8(pos + 1, (v >> 8) & 0xff);
|
|
dv.setUint8(pos + 2, (v >> 16) & 0xff);
|
|
return 3;
|
|
}
|
|
const clipped = clip(value);
|
|
const v = Math.round(
|
|
clipped < 0 ? clipped * 2147483648 : clipped * 2147483647
|
|
);
|
|
dv.setInt32(pos, v, true);
|
|
return 4;
|
|
}
|
|
|
|
function clip(v: number): number {
|
|
if (v > 1) return 1;
|
|
if (v < -1) return -1;
|
|
return v;
|
|
}
|
|
|
|
/** Build an A-law WAV header — the body is silence; we only verify rejection. */
|
|
export function buildAlawWav(): Uint8Array {
|
|
const dataBytes = 32;
|
|
const buffer = new Uint8Array(44 + dataBytes);
|
|
const dv = new DataView(buffer.buffer);
|
|
writeAscii(buffer, 0, "RIFF");
|
|
dv.setUint32(4, 36 + dataBytes, true);
|
|
writeAscii(buffer, 8, "WAVE");
|
|
writeAscii(buffer, 12, "fmt ");
|
|
dv.setUint32(16, 16, true);
|
|
dv.setUint16(20, 6, true); // A-law format code
|
|
dv.setUint16(22, 1, true);
|
|
dv.setUint32(24, 8000, true);
|
|
dv.setUint32(28, 8000, true);
|
|
dv.setUint16(32, 1, true);
|
|
dv.setUint16(34, 8, true);
|
|
writeAscii(buffer, 36, "data");
|
|
dv.setUint32(40, dataBytes, true);
|
|
return buffer;
|
|
}
|
|
|
|
/** Build a WAV whose `data` chunk declares double the bytes present. */
|
|
export function buildTruncatedWav(): Uint8Array {
|
|
const real = buildWav({
|
|
sampleRate: 8000,
|
|
channels: 1,
|
|
bitsPerSample: 16,
|
|
samplesPerChannel: 100,
|
|
formatCode: 1,
|
|
});
|
|
const dv = new DataView(real.buffer);
|
|
const declared = dv.getUint32(40, true);
|
|
dv.setUint32(40, declared * 2, true);
|
|
return real;
|
|
}
|
|
|
|
/** Build a buffer whose first 4 bytes are zeroed (no RIFF magic). */
|
|
export function buildCorruptHeaderWav(): Uint8Array {
|
|
const real = buildWav({
|
|
sampleRate: 8000,
|
|
channels: 1,
|
|
bitsPerSample: 16,
|
|
samplesPerChannel: 8,
|
|
formatCode: 1,
|
|
});
|
|
real[0] = 0;
|
|
real[1] = 0;
|
|
real[2] = 0;
|
|
real[3] = 0;
|
|
return real;
|
|
}
|