/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / instruments / simulated / worker.ts
111 lines · 3.8 KBBlameHistoryRaw
1/**
2 * The render worker: numbl runs here so a seconds-long eigensolve or jet
3 * integration never blocks the UI. One request renders one track: each
4 * note is rendered individually (velocity and duration quantized so
5 * repeats hit the cache), placed at its onset, and summed; the mixed
6 * buffer is transferred back. The per-note cache is an LRU bounded by
7 * total samples, keyed by everything that affects one note's audio.
8 */
9import { loadCatalog, type SimInstrumentDef } from './catalog.ts';
10import { bucketDuration, bucketVelocity, noteCacheKey } from './manifest.ts';
11import { renderSimNote } from './mrender.ts';
13export interface WorkerNote {
14 pitch: number;
15 velocity: number; // 0..1
16 durationSec: number;
17 timeSec: number;
20export interface WorkerRenderMsg {
21 kind: 'render';
22 id: number;
23 name: string; // manifest name
24 notes: WorkerNote[];
25 sampleRate: number;
26 lengthSamples: number;
27 quality: 'preview' | 'full';
28 settings: Record<string, number>;
31export type WorkerResponse =
32 | { kind: 'result'; id: number; samples: Float32Array }
33 | { kind: 'error'; id: number; message: string }
34 | { kind: 'status'; text: string };
36const catalog = new Map<string, SimInstrumentDef>();
37for (const def of loadCatalog()) catalog.set(def.manifest.name, def);
39const CACHE_MAX_SAMPLES = 48e6; // ~192 MB of Float32 note renders
40const cache = new Map<string, Float32Array>();
41let cacheSamples = 0;
43function cacheGet(key: string): Float32Array | undefined {
44 const v = cache.get(key);
45 if (v) {
46 cache.delete(key);
47 cache.set(key, v);
48 }
49 return v;
52function cachePut(key: string, buf: Float32Array): void {
53 cache.set(key, buf);
54 cacheSamples += buf.length;
55 while (cacheSamples > CACHE_MAX_SAMPLES && cache.size > 1) {
56 const oldest = cache.keys().next().value as string;
57 cacheSamples -= cache.get(oldest)!.length;
58 cache.delete(oldest);
59 }
62const post = (m: WorkerResponse, transfer?: Transferable[]) =>
63 (self as unknown as Worker).postMessage(m, transfer ?? []);
65self.onmessage = (ev: MessageEvent<WorkerRenderMsg>) => {
66 const msg = ev.data;
67 if (msg.kind !== 'render') return;
68 try {
69 const def = catalog.get(msg.name);
70 if (!def) throw new Error(`unknown simulated instrument '${msg.name}'`);
71 const m = def.manifest;
72 const out = new Float32Array(msg.lengthSamples);
73 // Group notes by cache key so each distinct sound renders once.
74 const groups = new Map<string, { velB: number; durB: number; pitch: number; times: number[] }>();
75 for (const n of msg.notes) {
76 const velB = bucketVelocity(n.velocity);
77 const durB = bucketDuration(n.durationSec);
78 const key = noteCacheKey(m, msg.quality, msg.sampleRate, msg.settings, n.pitch, velB, durB);
79 const g = groups.get(key);
80 if (g) g.times.push(n.timeSec);
81 else groups.set(key, { velB, durB, pitch: n.pitch, times: [n.timeSec] });
82 }
83 let done = 0;
84 for (const [key, g] of groups) {
85 let buf = cacheGet(key);
86 if (!buf) {
87 post({ kind: 'status', text: `${m.label}: rendering note ${done + 1}/${groups.size} (${msg.quality})` });
88 buf = renderSimNote(m, def.mfiles, {
89 pitch: g.pitch,
90 velocity: g.velB,
91 durationSec: g.durB,
92 sampleRate: msg.sampleRate,
93 quality: msg.quality,
94 settings: msg.settings,
95 });
96 cachePut(key, buf);
97 }
98 for (const t of g.times) {
99 const off = Math.round(t * msg.sampleRate);
100 const end = Math.min(out.length, off + buf.length);
101 for (let i = Math.max(0, off); i < end; i++) out[i] += buf[i - off];
102 }
103 done++;
104 }
105 post({ kind: 'status', text: '' });
106 post({ kind: 'result', id: msg.id, samples: out }, [out.buffer]);
107 } catch (err) {
108 post({ kind: 'status', text: '' });
109 post({ kind: 'error', id: msg.id, message: err instanceof Error ? err.message : String(err) });
110 }
111};
↑↓ move↵openescclose