/** * Simulated instruments (../simulated_instruments) behind the generic * Instrument interface: physically based MATLAB models executed by numbl * in a worker, one render per distinct note, mixed per track. What each * instrument exposes (entry point, pitch and velocity mapping, curated * knobs, preview overrides) comes from its own instrument.json manifest; * nothing here is instrument-specific. There is no live noteOn voice yet, * so monitoring while recording falls back to the player's beep. */ import type { Instrument, RenderRequest } from '../types.ts'; import { registerInstrument } from '../registry.ts'; import { loadCatalog } from './catalog.ts'; import type { SimManifest } from './manifest.ts'; import type { WorkerRenderMsg, WorkerResponse } from './worker.ts'; type StatusFn = (msg: string) => void; let onStatus: StatusFn = () => {}; export function setSimulatedStatus(cb: StatusFn): void { onStatus = cb; } let worker: Worker | null = null; let nextId = 1; const pending = new Map void; reject: (e: Error) => void }>(); function ensureWorker(): Worker { if (worker) return worker; worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); worker.onmessage = (ev: MessageEvent) => { const msg = ev.data; if (msg.kind === 'status') { onStatus(msg.text); return; } const p = pending.get(msg.id); if (!p) return; pending.delete(msg.id); if (msg.kind === 'result') p.resolve(msg.samples); else p.reject(new Error(msg.message)); }; worker.onerror = (ev) => { const err = new Error(`simulated-instrument worker failed: ${ev.message}`); for (const p of pending.values()) p.reject(err); pending.clear(); }; return worker; } class SimInstrument implements Instrument { readonly id: string; readonly name: string; readonly knobs; constructor(private readonly manifest: SimManifest) { this.id = `sim-${manifest.name}`; this.name = `${manifest.label} (sim)`; this.knobs = manifest.knobs; } render(req: RenderRequest): Promise { if (req.notes.length === 0) return Promise.resolve(new Float32Array(req.lengthSamples)); const w = ensureWorker(); const id = nextId++; const msg: WorkerRenderMsg = { kind: 'render', id, name: this.manifest.name, notes: req.notes.map((n) => ({ pitch: n.pitch, velocity: n.velocity, durationSec: n.durationSec, timeSec: n.timeSec })), sampleRate: req.sampleRate, lengthSamples: req.lengthSamples, quality: req.quality ?? 'preview', settings: req.settings ?? {}, }; return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); w.postMessage(msg); }); } } export function registerSimulatedInstruments(): number { let n = 0; for (const def of loadCatalog()) { registerInstrument(new SimInstrument(def.manifest)); n++; } return n; }