/ simulated_instruments / maraca
Sign in
simulated_instruments / maraca
maraca / instrument / worker.ts
120 lines · 4.1 KBCodeBlameHistory
373d415Add instrument/: the maraca behind the track-render interfaceJeremy Magland 1// The render worker: numbl runs here so a seconds-long solve never
2// blocks the UI. Requests are processed one at a time; the core yields to
3// the macrotask queue between stages, which is when queued cancel messages
4// are delivered, so cancellation is cooperative and the caches survive it.
5import { renderRequest, type MaracaVariant } from './core.ts';
6import { DEFAULT_MFILES } from './mfiles.ts';
7import type { MFile, NoteSpec } from './types.ts';
9export interface WorkerRenderMsg {
10 kind: 'render';
11 id: number;
12 variant: MaracaVariant;
13 notes: NoteSpec[];
14 sampleRate: number;
15 lengthSamples: number;
16 quality: 'preview' | 'full';
17 settings?: Record<string, number>;
18 params?: Record<string, number | boolean>;
19 mfiles?: MFile[];
20 wantDiagnostics?: boolean;
23export interface WorkerCancelMsg {
24 kind: 'cancel';
25 id: number;
28export type WorkerResponse =
29 | { kind: 'progress'; id: number; frac: number; stage: string }
30 | { kind: 'result'; id: number; audio: Float32Array; diag?: unknown }
31 | { kind: 'cancelled'; id: number }
32 | { kind: 'error'; id: number; message: string; file?: string; line?: number };
34class Cancelled extends Error {}
36const cancelled = new Set<number>();
37const queue: WorkerRenderMsg[] = [];
38let running = false;
40const post = (m: WorkerResponse, transfer?: Transferable[]) =>
41 (self as unknown as Worker).postMessage(m, transfer ?? []);
43const SKIP_WARNING = /skipping (\S+\.m) \(syntax error at line (\d+)\)/;
45async function run(msg: WorkerRenderMsg): Promise<void> {
46 // numbl reports a file it could not parse through console.warn and then
47 // skips it, so the failure surfaces at the call site as "Undefined
48 // function". Capture the warning to report the actual syntax error.
49 const warnings: string[] = [];
50 const origWarn = console.warn;
51 console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); };
52 try {
53 const result = await renderRequest(msg.variant, {
54 notes: msg.notes,
55 sampleRate: msg.sampleRate,
56 lengthSamples: msg.lengthSamples,
57 quality: msg.quality,
58 settings: msg.settings,
59 params: msg.params,
60 wantDiagnostics: msg.wantDiagnostics,
61 }, {
62 mfiles: msg.mfiles ?? DEFAULT_MFILES,
63 post: (frac, stage) => post({ kind: 'progress', id: msg.id, frac, stage }),
64 checkCancel: () => { if (cancelled.has(msg.id)) throw new Cancelled(); },
65 yieldToHost: () => new Promise((res) => setTimeout(res, 0)),
66 });
67 const transfer: Transferable[] = [result.audio.buffer];
68 if (result.diag) {
69 for (const v of Object.values(result.diag as unknown as Record<string, unknown>)) {
70 if (v instanceof Float32Array && v.buffer !== result.audio.buffer) transfer.push(v.buffer);
71 }
72 }
73 post({ kind: 'result', id: msg.id, audio: result.audio, diag: result.diag }, transfer);
74 } catch (err) {
75 if (err instanceof Cancelled) {
76 post({ kind: 'cancelled', id: msg.id });
77 } else {
78 const e = err as { message?: string; file?: string; line?: number };
79 let m: WorkerResponse = {
80 kind: 'error',
81 id: msg.id,
82 message: e?.message ? String(e.message) : String(err),
83 file: typeof e?.file === 'string' ? e.file : undefined,
84 line: typeof e?.line === 'number' ? e.line : undefined,
85 };
86 for (const w of warnings) {
87 const hit = SKIP_WARNING.exec(w);
88 if (hit) {
89 m = { kind: 'error', id: msg.id, message: 'syntax error', file: hit[1], line: Number(hit[2]) };
90 break;
91 }
92 }
93 post(m);
94 }
95 } finally {
96 console.warn = origWarn;
97 cancelled.delete(msg.id);
98 }
101async function pump(): Promise<void> {
102 if (running) return;
103 running = true;
104 while (queue.length > 0) {
105 const msg = queue.shift()!;
106 if (cancelled.has(msg.id)) { post({ kind: 'cancelled', id: msg.id }); cancelled.delete(msg.id); continue; }
107 await run(msg);
108 }
109 running = false;
112self.onmessage = (ev: MessageEvent<WorkerRenderMsg | WorkerCancelMsg>) => {
113 const msg = ev.data;
114 if (msg.kind === 'render') {
115 queue.push(msg);
116 void pump();
117 } else if (msg.kind === 'cancel') {
118 cancelled.add(msg.id);
119 }
120};
↑↓ move↵openescclose