1// The hit log and its export as a cymsim configuration.
2import type { Exciter, SimulationConfig } from "./config/schema.ts";
3import { cloneConfig, configToYaml } from "./config/yaml.ts";
5export interface LoggedHit {
6 id: number;
7 time: number; // audio-context seconds at release (filled in when the worklet reports it)
8 r: number;
9 phi: number;
10 velocity: number;
11 exciter: Exciter;
12 source: string; // key or "click"
13 peakForce?: number;
14 contactTime?: number;
15 rebound?: number;
16}
18export interface ExportOptions {
19 leadIn: number; // seconds before the first hit
20 tail: number; // seconds after the last hit
21 prefix: string;
22}
24/** The base configuration with the recorded hits, times shifted so the first hit is at leadIn. */
25export function exportConfig(base: SimulationConfig, hits: LoggedHit[], opts: ExportOptions): SimulationConfig {
26 const cfg = cloneConfig(base);
27 const sorted = hits.filter((h) => h.time >= 0).slice().sort((a, b) => a.time - b.time);
28 const t0 = sorted.length ? sorted[0].time : 0;
29 cfg.hits = sorted.map((h) => ({
30 time: round(h.time - t0 + opts.leadIn, 5),
31 r: round(h.r, 5),
32 phi: round(h.phi, 5),
33 velocity: round(h.velocity, 3),
34 exciter: structuredClone(h.exciter),
35 }));
36 const last = cfg.hits.length ? cfg.hits[cfg.hits.length - 1].time : 0;
37 cfg.numerics.duration = round(last + opts.tail, 3);
38 cfg.output.prefix = opts.prefix;
39 return cfg;
40}
42export function exportYaml(base: SimulationConfig, hits: LoggedHit[], opts: ExportOptions): string {
43 const header =
44 `# Recorded in cymplay (linear modal playback) on ${new Date().toISOString()}.\n` +
45 `# ${hits.length} hit(s); render the full nonlinear model with: cymsim simulate <this file>\n`;
46 return header + configToYaml(exportConfig(base, hits, opts));
47}
49function round(x: number, digits: number): number {
50 const f = Math.pow(10, digits);
51 return Math.round(x * f) / f;
52}
54/** 16-bit PCM mono WAV of a float signal, peak-normalised to 0.9 full scale when ``normalize``. */
55export function encodeWav(samples: Float32Array, sampleRate: number, normalize = true): Blob {
56 let peak = 0;
57 for (const s of samples) peak = Math.max(peak, Math.abs(s));
58 const scale = normalize ? (peak > 0 ? 0.9 / peak : 1) : 0.05;
59 const buf = new ArrayBuffer(44 + 2 * samples.length);
60 const v = new DataView(buf);
61 const str = (o: number, s: string) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); };
62 str(0, "RIFF"); v.setUint32(4, 36 + 2 * samples.length, true); str(8, "WAVE");
63 str(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true);
64 v.setUint32(24, sampleRate, true); v.setUint32(28, sampleRate * 2, true); v.setUint16(32, 2, true); v.setUint16(34, 16, true);
65 str(36, "data"); v.setUint32(40, 2 * samples.length, true);
66 for (let i = 0; i < samples.length; i++) {
67 const x = Math.max(-1, Math.min(1, samples[i] * scale));
68 v.setInt16(44 + 2 * i, Math.round(x * 32767), true);
69 }
70 return new Blob([buf], { type: "audio/wav" });
71}