1/**
2 * Simulated instruments (../simulated_instruments) behind the generic
3 * Instrument interface: physically based MATLAB models executed by numbl
4 * in a worker, one render per distinct note, mixed per track. What each
5 * instrument exposes (entry point, pitch and velocity mapping, curated
6 * knobs, preview overrides) comes from its own instrument.json manifest;
7 * nothing here is instrument-specific. There is no live noteOn voice yet,
8 * so monitoring while recording falls back to the player's beep.
9 */
10import type { Instrument, RenderRequest } from '../types.ts';
11import { registerInstrument } from '../registry.ts';
12import { loadCatalog } from './catalog.ts';
13import type { SimManifest } from './manifest.ts';
14import type { WorkerRenderMsg, WorkerResponse } from './worker.ts';
16type StatusFn = (msg: string) => void;
17let onStatus: StatusFn = () => {};
18export function setSimulatedStatus(cb: StatusFn): void {
19 onStatus = cb;
20}
22let worker: Worker | null = null;
23let nextId = 1;
24const pending = new Map<number, { resolve: (v: Float32Array) => void; reject: (e: Error) => void }>();
26function ensureWorker(): Worker {
27 if (worker) return worker;
28 worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
29 worker.onmessage = (ev: MessageEvent<WorkerResponse>) => {
30 const msg = ev.data;
31 if (msg.kind === 'status') {
32 onStatus(msg.text);
33 return;
34 }
35 const p = pending.get(msg.id);
36 if (!p) return;
37 pending.delete(msg.id);
38 if (msg.kind === 'result') p.resolve(msg.samples);
39 else p.reject(new Error(msg.message));
40 };
41 worker.onerror = (ev) => {
42 const err = new Error(`simulated-instrument worker failed: ${ev.message}`);
43 for (const p of pending.values()) p.reject(err);
44 pending.clear();
45 };
46 return worker;
47}
49class SimInstrument implements Instrument {
50 readonly id: string;
51 readonly name: string;
52 readonly knobs;
54 constructor(private readonly manifest: SimManifest) {
55 this.id = `sim-${manifest.name}`;
56 this.name = `${manifest.label} (sim)`;
57 this.knobs = manifest.knobs;
58 }
60 render(req: RenderRequest): Promise<Float32Array> {
61 if (req.notes.length === 0) return Promise.resolve(new Float32Array(req.lengthSamples));
62 const w = ensureWorker();
63 const id = nextId++;
64 const msg: WorkerRenderMsg = {
65 kind: 'render',
66 id,
67 name: this.manifest.name,
68 notes: req.notes.map((n) => ({ pitch: n.pitch, velocity: n.velocity, durationSec: n.durationSec, timeSec: n.timeSec })),
69 sampleRate: req.sampleRate,
70 lengthSamples: req.lengthSamples,
71 quality: req.quality ?? 'preview',
72 settings: req.settings ?? {},
73 };
74 return new Promise<Float32Array>((resolve, reject) => {
75 pending.set(id, { resolve, reject });
76 w.postMessage(msg);
77 });
78 }
79}
81export function registerSimulatedInstruments(): number {
82 let n = 0;
83 for (const def of loadCatalog()) {
84 registerInstrument(new SimInstrument(def.manifest));
85 n++;
86 }
87 return n;
88}