73 lines
3.1 KiB
TypeScript
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";
|
|
|