/ proof-of-concept / cymplay
Sign in
proof-of-concept / cymplay
cymplay / tests / worklet_node.ts
78 lines · 4.2 KBBlameHistoryRaw
1// Run the AudioWorklet processor in node with stubbed globals: one mallet hit and one stick hit,
2// half a second of output. Writes a raw float32 signal, a WAV and a JSON of the hit reports.
3// node tests/worklet_node.ts <modes.bin> <out prefix> [seconds]
4import { readFileSync, writeFileSync } from "node:fs";
5import { parseModes } from "../src/modes.ts";
6import { derive } from "../src/derived.ts";
7import { parseConfigYaml } from "../src/config/yaml.ts";
8import { buildHitPatch } from "../src/patch.ts";
9import { defaultMallet, defaultStick } from "../src/config/schema.ts";
10import { encodeWav } from "../src/hits.ts";
12const g = globalThis as unknown as Record<string, unknown>;
13const messages: unknown[] = [];
14g.sampleRate = 44100;
15g.currentFrame = 0;
16let Proc: new () => { port: { onmessage: ((ev: { data: unknown }) => void) | null }; process(i: Float32Array[][], o: Float32Array[][]): boolean };
17g.AudioWorkletProcessor = class { port = { onmessage: null, postMessage: (m: unknown) => messages.push(m) }; };
18g.registerProcessor = (_name: string, ctor: typeof Proc) => { Proc = ctor; };
19await import("../src/worklet.ts");
21const modes = parseModes(readFileSync(process.argv[2]).buffer as ArrayBuffer);
22const prefix = process.argv[3], seconds = Number(process.argv[4] ?? "0.5");
23const proc = new Proc!();
24const gain = new Float32Array(2 * modes.nModes);
25for (let i = 0; i < gain.length; i++) gain[i] = modes.gainFront[i] - modes.gainBack[i];
26const cfg = parseConfigYaml(JSON.stringify(modes.meta.config)).config!;
27const der = derive(modes, cfg);
28proc.port.onmessage!({ data: { type: "init", freq: der.freq, sigma: der.sigma, gain } });
30const hitsToPlay = [
31 { t: 0.0, kind: "mallet", r: 0.13716, phi: 0.3, v: 8, substeps: 2 },
32 { t: 0.25, kind: "stick", r: 0.194, phi: 2.0, v: 7, substeps: 6 },
33];
34if (seconds > 0.6) hitsToPlay.push(
35 { t: 0.5, kind: "mallet", r: 0.13716, phi: 1.3, v: 6, substeps: 2 },
36 { t: 0.75, kind: "stick", r: 0.194, phi: 3.0, v: 5, substeps: 6 },
37 { t: 1.0, kind: "mallet", r: 0.10, phi: -1.0, v: 9, substeps: 2 },
38 { t: 1.25, kind: "stick", r: 0.18, phi: 0.5, v: 8, substeps: 6 },
39);
40const fs = 44100, B = 128, nBlocks = Math.ceil(seconds * fs / B);
41const out = new Float32Array(nBlocks * B);
42let id = 1;
43const t0 = performance.now();
44let maxBlockMs = 0;
45let slow: string[] = [];
46const runs = Number(process.env.RUNS ?? "1");
47for (let run = 0; run < runs; run++) {
48if (run > 0) { const cfg = parseConfigYaml(JSON.stringify(modes.meta.config)).config!;
49const der = derive(modes, cfg);
50proc.port.onmessage!({ data: { type: "init", freq: der.freq, sigma: der.sigma, gain } }); slow = []; maxBlockMs = 0; }
51for (let b = 0; b < nBlocks; b++) {
52 for (const h of hitsToPlay) {
53 if (Math.round(h.t * fs / B) === b) {
54 const ex = h.kind === "stick" ? defaultStick() : defaultMallet();
55 const patch = buildHitPatch(modes, ex, h.r, h.phi, h.v, h.kind === "mallet" ? Number(process.env.MALLET_POINTS ?? "1") : 7)!;
56 proc.port.onmessage!({ data: { type: "hit", id: id++, phi: patch.phi, offsets: patch.offsets, kPoint: patch.kPoint, alpha: patch.alpha, mass: patch.mass, gap: patch.gap, velocity: h.v } });
57 }
58 }
59 const buf = [[new Float32Array(B)]];
60 const tb = performance.now();
61 proc.process([], buf);
62 const cost = performance.now() - tb;
63 if (cost > 2.9) slow.push(`block ${b} (t=${(b * B / fs * 1e3).toFixed(1)} ms): ${cost.toFixed(1)} ms`);
64 maxBlockMs = Math.max(maxBlockMs, cost);
65 out.set(buf[0][0], b * B);
66 g.currentFrame = (b + 1) * B;
69const wall = (performance.now() - t0) / 1000;
70let peak = 0, finite = true;
71for (const x of out) { if (!Number.isFinite(x)) finite = false; peak = Math.max(peak, Math.abs(x)); }
72console.log(`rendered ${seconds} s in ${wall.toFixed(2)} s (${(100 * wall / seconds).toFixed(0)} % of real time), worst block ${maxBlockMs.toFixed(2)} ms of ${(1000 * B / fs).toFixed(2)} ms; finite ${finite}; peak ${peak.toFixed(3)} Pa`);
73console.log("slow blocks:", slow.join("; ") || "none");
74for (const m of messages) if ((m as { type: string }).type !== "load") console.log(JSON.stringify(m));
75writeFileSync(prefix + ".f32", Buffer.from(out.buffer));
76const wav = encodeWav(out, fs);
77writeFileSync(prefix + ".wav", Buffer.from(await wav.arrayBuffer()));
78writeFileSync(prefix + "_hits.json", JSON.stringify(hitsToPlay));
↑↓ move↵openescclose