2 * A rendered trace as .wav bytes: resampled from the solver's rate (1/dt,
3 * around 114 kHz at the default grid) to 48 kHz by linear interpolation,
4 * which is harmless here because the content is band-limited far below
5 * either rate. Normalized to peak and faded a few milliseconds at each end
6 * so the file does not open and close with a click.
7 */
8export const WAV_RATE = 48000;
10export function traceToWavBytes(trace: Float32Array, dt: number): Uint8Array {
11 const n = Math.max(1, Math.round(trace.length * dt * WAV_RATE));
12 let peak = 0;
13 for (const v of trace) peak = Math.max(peak, Math.abs(v));
14 const gain = peak > 0 ? 0.9 / peak : 0;
16 const samples = new Float32Array(n);
17 for (let i = 0; i < n; i++) {
18 const s = i / (WAV_RATE * dt);
19 const k = Math.min(trace.length - 2, Math.floor(s));
20 const f = Math.min(1, s - k);
21 samples[i] = gain * ((1 - f) * trace[k] + f * trace[k + 1]);
22 }
23 const fade = Math.min(Math.round(0.005 * WAV_RATE), Math.floor(n / 2));
24 for (let i = 0; i < fade; i++) {
25 const w = i / fade;
26 samples[i] *= w;
27 samples[n - 1 - i] *= w;
28 }
30 const bytes = 44 + 2 * n;
31 const buf = new ArrayBuffer(bytes);
32 const view = new DataView(buf);
33 const str = (off: number, s: string): void => {
34 for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
35 };
36 str(0, 'RIFF');
37 view.setUint32(4, bytes - 8, true);
38 str(8, 'WAVE');
39 str(12, 'fmt ');
40 view.setUint32(16, 16, true);
41 view.setUint16(20, 1, true);
42 view.setUint16(22, 1, true);
43 view.setUint32(24, WAV_RATE, true);
44 view.setUint32(28, 2 * WAV_RATE, true);
45 view.setUint16(32, 2, true);
46 view.setUint16(34, 16, true);
47 str(36, 'data');
48 view.setUint32(40, 2 * n, true);
49 for (let i = 0; i < n; i++) {
50 const v = Math.max(-1, Math.min(1, samples[i]));
51 view.setInt16(44 + 2 * i, Math.round(v * 32767), true);
52 }
53 return new Uint8Array(buf);
54}