import { open } from "node:fs/promises"; // Reading a window out of an episode without reading the episode. // // The whole reason the old triage page could not widen a clip is that the // generator sliced +/-0.12s at BUILD time and inlined that as base64. Serving // audio makes any window reachable -- but only if a window is genuinely cheap. // A wav48 episode is 700-830s of 48 kHz 16-bit mono: ~70-80 MB. Decoding that // per request to hand back two seconds would trade one bottleneck for another. // // So: parse the header, seek, and read only the bytes the window covers. export type WavInfo = { sampleRate: number; channels: number; bitsPerSample: number; dataOffset: number; dataBytes: number; frames: number; duration: number; }; // RIFF chunk walk over the first few KB. Real wav48 files put `data` last, but // LIST/INFO chunks do turn up ahead of it, so walk rather than assume offset 44. function parseHeader(head: Buffer, fileSize: number): WavInfo { if (head.toString("latin1", 0, 4) !== "RIFF" || head.toString("latin1", 8, 12) !== "WAVE") { throw new Error("not a RIFF/WAVE file"); } let p = 12; let channels = 0; let sampleRate = 0; let bitsPerSample = 0; while (p + 8 <= head.length) { const id = head.toString("latin1", p, p + 4); const size = head.readUInt32LE(p + 4); const body = p + 8; if (id === "fmt ") { channels = head.readUInt16LE(body + 2); sampleRate = head.readUInt32LE(body + 4); bitsPerSample = head.readUInt16LE(body + 14); } if (id === "data") { // A size of 0xFFFFFFFF (or one that overruns the file) means the writer // never went back to patch the header -- trust the file length instead. const declared = size; const available = fileSize - body; const dataBytes = declared === 0 || declared > available ? available : declared; if (!channels || !sampleRate || !bitsPerSample) throw new Error("wav: data before fmt"); const bytesPerFrame = channels * (bitsPerSample / 8); const frames = Math.floor(dataBytes / bytesPerFrame); return { sampleRate, channels, bitsPerSample, dataOffset: body, dataBytes, frames, duration: frames / sampleRate, }; } p = body + size + (size & 1); } throw new Error("wav: no data chunk"); } const headerCache = new Map(); export async function wavInfo(file: string): Promise { const hit = headerCache.get(file); if (hit) return hit; const fh = await open(file, "r"); try { const stat = await fh.stat(); const head = Buffer.alloc(Math.min(65536, stat.size)); await fh.read(head, 0, head.length, 0); const info = parseHeader(head, stat.size); headerCache.set(file, info); return info; } finally { await fh.close(); } } export type WavWindow = { x: Float32Array; sampleRate: number; /** Absolute episode seconds of x[0] -- the window may be clamped at either end. */ from: number; to: number; duration: number; }; /** * Read [from, to] in absolute episode seconds as mono float samples, reading * only those bytes. Clamps to the file rather than failing, so dragging a handle * past the start of an episode gives you what exists. */ export async function readWavWindow(file: string, from: number, to: number): Promise { const info = await wavInfo(file); if (info.bitsPerSample !== 16) throw new Error(`wav: expected 16-bit, got ${info.bitsPerSample}`); const bytesPerFrame = info.channels * 2; const f0 = Math.max(0, Math.min(info.frames, Math.floor(from * info.sampleRate))); const f1 = Math.max(f0, Math.min(info.frames, Math.ceil(to * info.sampleRate))); const n = f1 - f0; const x = new Float32Array(n); if (n > 0) { const buf = Buffer.alloc(n * bytesPerFrame); const fh = await open(file, "r"); try { await fh.read(buf, 0, buf.length, info.dataOffset + f0 * bytesPerFrame); } finally { await fh.close(); } // Mono-ise by taking channel 0. Every wav48 file here is already mono; a // stereo source would otherwise silently play at double speed. for (let i = 0; i < n; i += 1) x[i] = buf.readInt16LE(i * bytesPerFrame) / 32768; } return { x, sampleRate: info.sampleRate, from: f0 / info.sampleRate, to: f1 / info.sampleRate, duration: info.duration, }; } /** Peak absolute sample over an absolute-seconds range, read the same cheap way. */ export async function peakOver(file: string, from: number, to: number): Promise { const { x } = await readWavWindow(file, from, to); let peak = 0; for (let i = 0; i < x.length; i += 1) { const v = x[i] < 0 ? -x[i] : x[i]; if (v > peak) peak = v; } return peak; } /** 16-bit mono RIFF, for handing a window straight to an