/ proof-of-concept / plate-dynamics-lab-2
Sign in
proof-of-concept / plate-dynamics-lab-2
plate-dynamics-lab-2 / packages / core / src / audio / wav.ts
44 lines · 1.6 KBBlameHistoryRaw
1/**
2 * Encode a mono signal as a PCM RIFF/WAVE file, 24-bit by default (as the
3 * reference simulators write) or 16-bit. The input is expected to be
4 * peak-normalized already (see `prepareAudio`); values outside [-1, 1] are
5 * clipped rather than wrapped. Non-finite samples are written as silence.
6 */
7export function encodeWav(samples: ArrayLike<number>, fs: number, bits: 16 | 24 = 24): Uint8Array {
8 const n = samples.length;
9 const bytesPerSample = bits / 8;
10 const dataBytes = n * bytesPerSample;
11 const buf = new ArrayBuffer(44 + dataBytes);
12 const v = new DataView(buf);
13 const ascii = (off: number, s: string) => {
14 for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i));
15 };
16 ascii(0, "RIFF");
17 v.setUint32(4, 36 + dataBytes, true);
18 ascii(8, "WAVE");
19 ascii(12, "fmt ");
20 v.setUint32(16, 16, true); // fmt chunk size
21 v.setUint16(20, 1, true); // PCM
22 v.setUint16(22, 1, true); // mono
23 v.setUint32(24, fs, true);
24 v.setUint32(28, fs * bytesPerSample, true); // byte rate
25 v.setUint16(32, bytesPerSample, true); // block align
26 v.setUint16(34, bits, true); // bits per sample
27 ascii(36, "data");
28 v.setUint32(40, dataBytes, true);
29 let off = 44;
30 const full = bits === 24 ? 8388607 : 32767;
31 for (let i = 0; i < n; i++, off += bytesPerSample) {
32 const x = samples[i];
33 const c = Number.isFinite(x) ? Math.max(-1, Math.min(1, x)) : 0;
34 const q = Math.round(c * full);
35 if (bits === 24) {
36 v.setUint8(off, q & 255);
37 v.setUint8(off + 1, (q >> 8) & 255);
38 v.setUint8(off + 2, (q >> 16) & 255);
39 } else {
40 v.setInt16(off, Math.max(-32768, q), true);
41 }
42 }
43 return new Uint8Array(buf);
↑↓ move↵openescclose