98 lines
4.2 KiB
TypeScript
98 lines
4.2 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";
|
|
|
|
const sinc = (x: number) => (x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x));
|
|
|
|
/** Windowed-sinc (Blackman) resampler; low-passes at the lower Nyquist when downsampling. */
|
|
export function resample(x: Float32Array, from: number, to: number): Float32Array {
|
|
if (from === to) return x.slice();
|
|
const ratio = to / from;
|
|
const outLen = Math.round(x.length * ratio);
|
|
const out = new Float32Array(outLen);
|
|
const cutoff = Math.min(1, ratio); // in units of input Nyquist
|
|
const half = 16 / cutoff; // kernel half-width in input samples (16 zero-crossings)
|
|
for (let j = 0; j < outLen; j++) {
|
|
const t = j / ratio;
|
|
const lo = Math.max(0, Math.ceil(t - half)), hi = Math.min(x.length - 1, Math.floor(t + half));
|
|
let acc = 0, wsum = 0;
|
|
for (let i = lo; i <= hi; i++) {
|
|
const d = i - t;
|
|
const w = 0.42 + 0.5 * Math.cos((Math.PI * d) / half) + 0.08 * Math.cos((2 * Math.PI * d) / half);
|
|
const k = cutoff * sinc(cutoff * d) * w;
|
|
acc += x[i]! * k;
|
|
wsum += k;
|
|
}
|
|
// Normalise to unity DC gain so edges and non-integer ratios stay calibrated.
|
|
out[j] = wsum !== 0 ? acc / wsum : 0;
|
|
}
|
|
return out;
|
|
}
|