// The maraca behind the generic track-render instrument interface, for // direct import by midiboot and by the maraca web app. The MATLAB model // in ../matlab is executed by numbl in a lazily spawned worker that this // module owns; the worker is kept alive across renders so its caches (the // explorer stage tiers and finished stroke renders) persist. Aborting a // request is cooperative, checked between render stages; if the worker // does not respond within a grace period it is torn down (and the caches // with it) and respawned. import type { Instrument, TrackRenderRequest, TrackRenderResult } from './types.ts'; import { MARACA_KNOBS, type MaracaVariant } from './knobs.ts'; import type { WorkerCancelMsg, WorkerRenderMsg, WorkerResponse } from './worker.ts'; const CANCEL_GRACE_MS = 5000; interface Pending { resolve: (r: TrackRenderResult) => void; reject: (e: Error) => void; onProgress?: (p: { frac: number; stage: string }) => void; } let worker: Worker | null = null; let nextId = 1; const pending = new Map(); 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; const p = pending.get(msg.id); if (!p) return; if (msg.kind === 'progress') { p.onProgress?.({ frac: msg.frac, stage: msg.stage }); return; } pending.delete(msg.id); if (msg.kind === 'result') p.resolve({ audio: msg.audio, diag: msg.diag }); else if (msg.kind === 'cancelled') p.reject(new DOMException('render cancelled', 'AbortError')); else p.reject(Object.assign(new Error(msg.message), { file: msg.file, line: msg.line })); }; worker.onerror = (ev) => { const err = new Error(`maraca instrument worker failed: ${ev.message}`); for (const p of pending.values()) p.reject(err); pending.clear(); }; return worker; } function teardownWorker(reason: Error): void { if (worker) worker.terminate(); worker = null; for (const p of pending.values()) p.reject(reason); pending.clear(); } function renderOnWorker(variant: MaracaVariant, req: TrackRenderRequest): Promise { if (req.signal?.aborted) return Promise.reject(new DOMException('render cancelled', 'AbortError')); const w = ensureWorker(); const id = nextId++; const msg: WorkerRenderMsg = { kind: 'render', id, variant, notes: req.notes.map((n) => ({ timeSec: n.timeSec, durationSec: n.durationSec, pitch: n.pitch, velocity: n.velocity })), sampleRate: req.sampleRate, lengthSamples: req.lengthSamples, quality: req.quality, settings: req.settings ?? {}, params: req.params, mfiles: req.mfiles, wantDiagnostics: req.wantDiagnostics, }; return new Promise((resolve, reject) => { let graceTimer: ReturnType | null = null; const onAbort = () => { const cancel: WorkerCancelMsg = { kind: 'cancel', id }; worker?.postMessage(cancel); // Escalation: a stage that never yields (or a hung worker) is torn // down after the grace period, losing the caches. graceTimer = setTimeout(() => { if (pending.has(id)) teardownWorker(new DOMException('render cancelled', 'AbortError')); }, CANCEL_GRACE_MS); }; req.signal?.addEventListener('abort', onAbort, { once: true }); pending.set(id, { resolve: (r) => { req.signal?.removeEventListener('abort', onAbort); if (graceTimer !== null) clearTimeout(graceTimer); resolve(r); }, reject: (e) => { req.signal?.removeEventListener('abort', onAbort); if (graceTimer !== null) clearTimeout(graceTimer); reject(e); }, onProgress: req.onProgress, }); w.postMessage(msg); }); } class MaracaInstrument implements Instrument { readonly id = 'maraca'; readonly name = 'Maraca'; readonly knobs = MARACA_KNOBS; renderTrack(req: TrackRenderRequest): Promise { return renderOnWorker('maraca', req); } } export const maracaInstrument: Instrument = new MaracaInstrument(); export const maracaInstruments: Instrument[] = [maracaInstrument];