concept-collection / dulcimer
dulcimer / src / audio / wav.ts
59 lines · 2.1 KBBlameHistoryRaw
1/**
2 * A recorded trace as a .wav file.
3 *
4 * The trace holds one sample per timestep, 1/dt of them a second — around
5 * 150 kHz at the default grid, which is a legal but eccentric rate for a
6 * .wav. It is resampled to 48 kHz by linear interpolation, which is harmless
7 * here: the content is band-limited far below either rate (the grid stops
8 * resolving sound around 5 kHz). Normalized to peak, like playback, and
9 * faded a few milliseconds at each end so the file does not open and close
10 * with a click.
11 */
13export const WAV_RATE = 48000;
15export function traceToWav(trace: Float32Array, dt: number): Blob {
16 const n = Math.max(1, Math.round(trace.length * dt * WAV_RATE));
17 let peak = 0;
18 for (const v of trace) peak = Math.max(peak, Math.abs(v));
19 const gain = peak > 0 ? 0.9 / peak : 0;
21 const samples = new Float32Array(n);
22 for (let i = 0; i < n; i++) {
23 const s = i / (WAV_RATE * dt);
24 const k = Math.min(trace.length - 2, Math.floor(s));
25 const f = Math.min(1, s - k);
26 samples[i] = gain * ((1 - f) * trace[k] + f * trace[k + 1]);
27 }
28 const fade = Math.min(Math.round(0.005 * WAV_RATE), Math.floor(n / 2));
29 for (let i = 0; i < fade; i++) {
30 const w = i / fade;
31 samples[i] *= w;
32 samples[n - 1 - i] *= w;
33 }
35 const bytes = 44 + 2 * n;
36 const buf = new ArrayBuffer(bytes);
37 const view = new DataView(buf);
38 const str = (off: number, s: string): void => {
39 for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
40 };
41 str(0, 'RIFF');
42 view.setUint32(4, bytes - 8, true);
43 str(8, 'WAVE');
44 str(12, 'fmt ');
45 view.setUint32(16, 16, true); // PCM chunk size
46 view.setUint16(20, 1, true); // PCM
47 view.setUint16(22, 1, true); // mono
48 view.setUint32(24, WAV_RATE, true);
49 view.setUint32(28, 2 * WAV_RATE, true); // byte rate
50 view.setUint16(32, 2, true); // block align
51 view.setUint16(34, 16, true); // bits per sample
52 str(36, 'data');
53 view.setUint32(40, 2 * n, true);
54 for (let i = 0; i < n; i++) {
55 const v = Math.max(-1, Math.min(1, samples[i]));
56 view.setInt16(44 + 2 * i, Math.round(v * 32767), true);
57 }
58 return new Blob([buf], { type: 'audio/wav' });