Files
STSheets/src/audio/wav.ts
T

73 lines
3.1 KiB
TypeScript

import type { Audio } from "../core/types.ts";
/** Read a PCM16 / PCM24 / float32 WAV file into float channels. */
export async function readWav(path: string): Promise<Audio> {
const buf = new Uint8Array(await Bun.file(path).arrayBuffer());
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const tag = (o: number) => String.fromCharCode(buf[o]!, buf[o + 1]!, buf[o + 2]!, buf[o + 3]!);
if (tag(0) !== "RIFF" || tag(8) !== "WAVE") throw new Error(`${path}: not a RIFF/WAVE file`);
let fmt = 0, nch = 0, sampleRate = 0, bits = 0;
let dataOff = -1, dataLen = 0;
for (let o = 12; o + 8 <= buf.length; ) {
const id = tag(o);
const len = dv.getUint32(o + 4, true);
if (id === "fmt ") {
fmt = dv.getUint16(o + 8, true);
nch = dv.getUint16(o + 10, true);
sampleRate = dv.getUint32(o + 12, true);
bits = dv.getUint16(o + 22, true);
if (fmt === 0xfffe) fmt = dv.getUint16(o + 32, true); // WAVE_FORMAT_EXTENSIBLE sub-format
} else if (id === "data") {
dataOff = o + 8;
dataLen = Math.min(len, buf.length - dataOff);
break;
}
o += 8 + len + (len & 1);
}
if (dataOff < 0 || nch < 1) throw new Error(`${path}: missing fmt/data chunk`);
const bytes = bits / 8;
const frames = Math.floor(dataLen / (bytes * nch));
const channels = Array.from({ length: nch }, () => new Float32Array(frames));
for (let i = 0; i < frames; i++) {
for (let c = 0; c < nch; c++) {
const o = dataOff + (i * nch + c) * bytes;
let v: number;
if (fmt === 3 && bits === 32) v = dv.getFloat32(o, true);
else if (fmt === 1 && bits === 16) v = dv.getInt16(o, true) / 32768;
else if (fmt === 1 && bits === 24) v = ((dv.getUint8(o) | (dv.getUint8(o + 1) << 8) | (dv.getInt8(o + 2) << 16)) as number) / 8388608;
else if (fmt === 1 && bits === 32) v = dv.getInt32(o, true) / 2147483648;
else throw new Error(`${path}: unsupported WAV format ${fmt}/${bits}-bit`);
channels[c]![i] = v;
}
}
return { sampleRate, channels };
}
/** Write 16-bit PCM WAV (clipped to [-1, 1]). */
export async function writeWav(path: string, a: Audio): Promise<void> {
const nch = a.channels.length;
const frames = a.channels[0]?.length ?? 0;
const dataLen = frames * nch * 2;
const out = new Uint8Array(44 + dataLen);
const dv = new DataView(out.buffer);
const put = (o: number, s: string) => { for (let i = 0; i < 4; i++) out[o + i] = s.charCodeAt(i); };
put(0, "RIFF"); dv.setUint32(4, 36 + dataLen, true); put(8, "WAVE");
put(12, "fmt "); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, nch, true);
dv.setUint32(24, a.sampleRate, true); dv.setUint32(28, a.sampleRate * nch * 2, true);
dv.setUint16(32, nch * 2, true); dv.setUint16(34, 16, true);
put(36, "data"); dv.setUint32(40, dataLen, true);
let o = 44;
for (let i = 0; i < frames; i++) {
for (let c = 0; c < nch; c++) {
const s = Math.max(-1, Math.min(1, a.channels[c]![i]!));
dv.setInt16(o, Math.round(s < 0 ? s * 32768 : s * 32767), true);
o += 2;
}
}
await Bun.write(path, out);
}
export { toMono } from "../dsp/mono.ts";
export { resample } from "../dsp/resample.ts";