/** * A rendered trace as .wav bytes: resampled from the solver's rate (1/dt, * around 114 kHz at the default grid) to 48 kHz by linear interpolation, * which is harmless here because the content is band-limited far below * either rate. Normalized to peak and faded a few milliseconds at each end * so the file does not open and close with a click. */ export const WAV_RATE = 48000; export function traceToWavBytes(trace: Float32Array, dt: number): Uint8Array { const n = Math.max(1, Math.round(trace.length * dt * WAV_RATE)); let peak = 0; for (const v of trace) peak = Math.max(peak, Math.abs(v)); const gain = peak > 0 ? 0.9 / peak : 0; const samples = new Float32Array(n); for (let i = 0; i < n; i++) { const s = i / (WAV_RATE * dt); const k = Math.min(trace.length - 2, Math.floor(s)); const f = Math.min(1, s - k); samples[i] = gain * ((1 - f) * trace[k] + f * trace[k + 1]); } const fade = Math.min(Math.round(0.005 * WAV_RATE), Math.floor(n / 2)); for (let i = 0; i < fade; i++) { const w = i / fade; samples[i] *= w; samples[n - 1 - i] *= w; } const bytes = 44 + 2 * n; const buf = new ArrayBuffer(bytes); const view = new DataView(buf); const str = (off: number, s: string): void => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); }; str(0, 'RIFF'); view.setUint32(4, bytes - 8, true); str(8, 'WAVE'); str(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, WAV_RATE, true); view.setUint32(28, 2 * WAV_RATE, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); str(36, 'data'); view.setUint32(40, 2 * n, true); for (let i = 0; i < n; i++) { const v = Math.max(-1, Math.min(1, samples[i])); view.setInt16(44 + 2 * i, Math.round(v * 32767), true); } return new Uint8Array(buf); }