/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / src / modal / pool.ts
208 lines · 7.8 KBCodeBlameHistory
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 1// A pool of the workers of worker.ts. It runs the eigenvalue jobs of
2// modes.ts through a JobRunner, and splits the modes of a model between the
3// workers for the synthesis: the main thread steps the contact (which
4// couples all modes and is cheap), each worker forms the parts of its
5// modes, and the main thread sums them.
7import { type LinearSystem } from "../model/assemble.ts";
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 8import { type Plate, type SimParams } from "../params.ts";
9import { type DampingPlate } from "./damping.ts";
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 10import { type Job, type JobResult, type JobRunner, type ModeSet, computeModes } from "./modes.ts";
11import {
12 type ContactResult,
13 type ModalModel,
14 type Synthesis,
15 attackPressure,
16 listenerWeights,
17 modalRates,
18 simulateContact,
19 sliceContact,
20 sliceModel,
21 strikeGains,
22 tailStart,
23} from "./synth.ts";
24import { type WorkerRequest, type WorkerResponse } from "./worker.ts";
26interface Pending {
27 resolve: (r: WorkerResponse) => void;
28 reject: (e: Error) => void;
31class PoolWorker {
32 worker: Worker;
33 busy = false;
34 pending = new Map<number, Pending>();
35 ready: Pending | null = null;
36 constructor() {
37 this.worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
38 this.worker.onmessage = (ev: MessageEvent<WorkerResponse>) => {
39 const r = ev.data;
40 if (r.type === "ready") { this.ready?.resolve(r); this.ready = null; return; }
41 const p = this.pending.get(r.id);
42 if (!p) return;
43 this.pending.delete(r.id);
44 if (r.type === "error") p.reject(new Error(r.message)); else p.resolve(r);
45 };
46 this.worker.onerror = (e) => {
47 for (const p of this.pending.values()) p.reject(new Error(e.message));
48 this.pending.clear();
49 };
50 }
51 send(msg: WorkerRequest, transfer: ArrayBuffer[] = []): Promise<WorkerResponse> {
52 return new Promise((resolve, reject) => {
53 if (msg.type === "system" || msg.type === "model") this.ready = { resolve, reject };
54 else this.pending.set(msg.id, { resolve, reject });
55 this.worker.postMessage(msg, transfer);
56 });
57 }
60export class WorkerPool {
61 private workers: PoolWorker[] = [];
62 private queue: (() => void)[] = [];
63 private modelSplit: { from: number; to: number }[] = [];
64 private nextId = 1;
66 constructor(public readonly size: number) {
67 for (let i = 0; i < size; i++) this.workers.push(new PoolWorker());
68 }
70 static defaultSize(): number {
71 const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4;
72 return Math.max(2, Math.min(12, hw));
73 }
75 /** Waits for an idle worker. */
76 private acquire(): Promise<PoolWorker> {
77 return new Promise((resolve) => {
78 const tryTake = (): boolean => {
79 const w = this.workers.find((w) => !w.busy);
80 if (!w) return false;
81 w.busy = true;
82 resolve(w);
83 return true;
84 };
85 if (!tryTake()) this.queue.push(() => tryTake());
86 });
87 }
88 private release(w: PoolWorker): void {
89 w.busy = false;
90 const next = this.queue.shift();
91 if (next) next();
92 }
94 /** Sends the linear system to every worker. */
95 async setSystem(sys: LinearSystem): Promise<void> {
96 await Promise.all(this.workers.map((w) => w.send({ type: "system", n: sys.n, bw: sys.bw, band: sys.band, mass: sys.mass })));
97 }
99 runner(): JobRunner {
100 return {
101 run: async (job: Job): Promise<JobResult> => {
102 const w = await this.acquire();
103 try {
104 const r = await w.send({ type: "job", id: this.nextId++, job });
105 return (r as { result: JobResult }).result;
106 } finally {
107 this.release(w);
108 }
109 },
110 close: () => {},
111 };
112 }
114 /** The modes of the system, computed by the workers. */
115 async modes(sys: LinearSystem, fMax: number, onProgress?: (done: number, total: number) => void): Promise<ModeSet> {
116 await this.setSystem(sys);
76b7843Slice size follows the bandwidth (about bw/3.5); aluminum square presetJeremy Magland 117 return computeModes(sys, fMax, this.runner(), { onProgress });
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 120 /** Splits the modes of the model between the workers, with the plate when the per-mode losses are still to be computed. */
121 async setModel(model: ModalModel, plate: DampingPlate | null): Promise<void> {
123 this.modelSplit = [];
124 const per = Math.ceil(model.count / n);
125 await Promise.all(
126 this.workers.map((w, i) => {
127 const from = Math.min(model.count, i * per), to = Math.min(model.count, (i + 1) * per);
128 this.modelSplit.push({ from, to });
129 const slice = sliceModel(model, from, to);
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 130 return w.send({ type: "model", model: slice, plate });
132 );
133 }
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 135 /** The radiation and thermoelastic rates of the model's modes, computed by the workers on their slices and written into the model. */
136 async rates(model: ModalModel): Promise<void> {
137 const parts = await Promise.all(
138 this.workers.map((w, i) => {
139 const { from, to } = this.modelSplit[i];
140 if (to <= from) return Promise.resolve(null);
141 return w.send({ type: "rates", id: this.nextId++ });
142 }),
143 );
144 parts.forEach((r, i) => {
145 if (!r || r.type !== "rates") return;
146 const { from } = this.modelSplit[i];
147 model.sigmaRad.set(r.rad, from);
148 model.sigmaTe.set(r.te, from);
149 });
150 // The workers' slices were sent before the rates existed; refresh them so the synthesis sees the same model.
151 await this.setModel(model, null);
152 }
155 * Synthesizes a strike with the model last set. The audio comes first
156 * (through `onAudio`), then the movie frames; the returned promise
157 * resolves when both are done.
158 */
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 159 async synthesize(model: ModalModel, pl: Plate, p: SimParams, onAudio?: (audio: Float32Array, contact: ContactResult) => void, withMovie = true): Promise<Synthesis> {
160 const sigma = modalRates(model);
162 const contact = simulateContact(model, sigma, g, p, p.fps);
163 const { weight, delay, maxDelay } = listenerWeights(pl, p);
164 const tStart = tailStart(contact, maxDelay);
165 const len = Math.min(contact.tableLen, Math.ceil(tStart * contact.tableRate) + 2);
166 const nSamples = Math.round(p.duration * p.fs);
167 const nFrames = withMovie ? Math.round(p.duration * p.fps) : 0;
168 const parts = await Promise.all(
169 this.workers.map((w, i) => {
170 const { from, to } = this.modelSplit[i];
171 if (to <= from) return Promise.resolve(null);
172 return w.send({
173 type: "synth", id: this.nextId++, sigma: sigma.slice(from, to), contact: sliceContact(contact, from, to),
174 weight, delay, tStart, len, fs: p.fs, nSamples, audio: true, movie: null,
175 });
176 }),
177 );
178 const audio = new Float32Array(nSamples);
179 const table = new Float32Array(model.nRows * len);
180 for (const r of parts) {
181 if (!r || r.type !== "synth") continue;
182 if (r.audio) for (let i = 0; i < nSamples; i++) audio[i] += r.audio[i];
183 if (r.table) for (let i = 0; i < table.length; i++) table[i] += r.table[i];
184 }
185 attackPressure(model, table, len, contact.tableRate, weight, delay, p.fs, tStart, audio);
186 onAudio?.(audio, contact);
187 const frames = new Float32Array(nFrames * model.nGrid);
188 if (withMovie) {
189 const mparts = await Promise.all(
190 this.workers.map((w, i) => {
191 const { from, to } = this.modelSplit[i];
192 if (to <= from) return Promise.resolve(null);
193 return w.send({
194 type: "synth", id: this.nextId++, sigma: sigma.slice(from, to), contact: sliceContact(contact, from, to),
195 weight, delay, tStart, len, fs: p.fs, nSamples, audio: false, movie: { fps: p.fps, nFrames },
196 });
197 }),
198 );
199 for (const r of mparts) if (r && r.type === "synth" && r.frames) for (let i = 0; i < frames.length; i++) frames[i] += r.frames[i];
200 }
201 return { audio, frames, nFrames, contact };
202 }
204 terminate(): void {
205 for (const w of this.workers) w.worker.terminate();
206 this.workers = [];
207 }
↑↓ move↵openescclose