/** * Encode a mono signal as a 16-bit PCM RIFF/WAVE file. The input is expected * to be peak-normalized already (see `prepareAudio`); values outside [-1, 1] * are clipped rather than wrapped. Non-finite samples are written as silence. */ export function encodeWav(samples: ArrayLike, fs: number): Uint8Array { const n = samples.length; const bytesPerSample = 2; const dataBytes = n * bytesPerSample; const buf = new ArrayBuffer(44 + dataBytes); const v = new DataView(buf); const ascii = (off: number, s: string) => { for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i)); }; ascii(0, "RIFF"); v.setUint32(4, 36 + dataBytes, true); ascii(8, "WAVE"); ascii(12, "fmt "); v.setUint32(16, 16, true); // fmt chunk size v.setUint16(20, 1, true); // PCM v.setUint16(22, 1, true); // mono v.setUint32(24, fs, true); v.setUint32(28, fs * bytesPerSample, true); // byte rate v.setUint16(32, bytesPerSample, true); // block align v.setUint16(34, 16, true); // bits per sample ascii(36, "data"); v.setUint32(40, dataBytes, true); let off = 44; for (let i = 0; i < n; i++, off += 2) { const x = samples[i]; const c = Number.isFinite(x) ? Math.max(-1, Math.min(1, x)) : 0; v.setInt16(off, Math.round(c < 0 ? c * 32768 : c * 32767), true); } return new Uint8Array(buf); }