/ proof-of-concept / soundbox
Sign in
proof-of-concept / soundbox
soundbox / src / studio / session.ts
288 lines · 9.3 KBCodeBlameHistory
2 * One running scene: the air grid, every object's grid, the generated (or
3 * hand-edited) .m files compiled to GPU plans, the timestep, the microphone.
4 *
5 * Everything that is not rendering. The app and the tests both go through
6 * this, so there is one place that decides how a SceneSpec becomes something
7 * running on the GPU — and nothing about it is browser-specific beyond
8 * needing a GPUDevice.
9 *
10 * The order of construction matters and is worth spelling out. The air grid
11 * fixes h; with the speed of air that fixes a provisional timestep (the CFL
12 * condition); the generator then sizes every object's grid at that dt and
13 * instantiates the ops. The scene .m is evaluated next — normally it keeps
14 * the medium at c0 and the provisional dt stands, but an edited scene that
15 * puts a faster medium somewhere lowers dt, and the object grids are resized
16 * once at the final value. Objects have no say in the timestep; each chooses
17 * the finest node spacing that is stable at the air's dt for anything its
18 * sliders can reach (see src/grids.ts), which is why moving a slider never
19 * recompiles.
20 */
21import { makeAirGrid, stableDt, type AirGrid, type ObjectGrid } from '../grids.ts';
22import { OpPlan } from '../mgpu/ops.ts';
23import { GpuModel, type ModelParams } from '../mgpu/model.ts';
24import { Scene } from '../scene/scene.ts';
25import { Recorder } from '../audio/recorder.ts';
26import { C_AIR, CFL, DOMAIN_X } from '../units.ts';
27import { generate, suffixedParams, type GeneratedScene } from './codegen.ts';
28import { templateOf } from './objects.ts';
29import { BODY_TEMPLATES } from './bodies.ts';
30import { defaultsOf, type Params, type SceneSpec } from './types.ts';
32export interface StudioSessionOptions {
33 device: GPUDevice;
34 spec: SceneSpec;
35 /** Model parameter values by suffixed key; defaults fill the gaps. */
36 params?: Params;
37 /** Scene parameter values (placement + body) by key; defaults fill gaps. */
38 sceneParams?: Params;
39 /** Override the generated sources — the Lab editor's working copies. */
40 modelSource?: string;
41 sceneSource?: string;
42 /** Air grid points along x; y and z get half each. */
43 nx: number;
44 /** Domain length along x, metres. */
45 Lx?: number;
e18e57eRender scenes outside the browserJeremy Magland 46 /** Samples the microphone's trace holds; defaults to about 9 seconds at
47 * the browser's default grid. The headless renderer sizes it from the
48 * requested duration. */
49 traceCapacity?: number;
761af16soundbox: a scene-composition studio over the dulcimer engineJeremy Magland 50 /** Grid fields one kernel may read, overriding what the device allows.
51 * Only for tests. */
52 operandBudget?: number;
55/** Model + scene parameter defaults for a spec, suffixed, spec values kept. */
56export function specParams(spec: SceneSpec): { params: Params; sceneParams: Params } {
57 const params: Params = {};
58 const sceneParams: Params = {};
59 for (const o of spec.objects) {
60 const t = templateOf(o);
61 Object.assign(params, suffixedParams(t.paramSpecs, o.params, o.id));
62 Object.assign(sceneParams, suffixedParams(t.placeSpecs, o.place, o.id));
63 }
64 Object.assign(
65 sceneParams,
66 defaultsOf(BODY_TEMPLATES[spec.body.type].paramSpecs),
67 spec.body.params,
68 );
69 return { params, sceneParams };
72export class StudioSession {
73 readonly device: GPUDevice;
74 readonly spec: SceneSpec;
75 readonly air: AirGrid;
76 readonly gpu: GpuModel;
77 readonly generated: GeneratedScene;
78 /** The sources actually compiled (generated, or the editor's overrides). */
79 readonly modelSource: string;
80 readonly sceneSource: string;
81 /** The microphone: the pressure at one point, every timestep. */
82 readonly recorder: Recorder;
84 /** Model time and step count since the last excite. */
85 t = 0;
86 steps = 0;
88 #scene: Scene;
89 #params: Params;
90 #sceneParams: Params;
91 #dt: number;
93 private constructor(init: {
94 device: GPUDevice;
95 spec: SceneSpec;
96 air: AirGrid;
97 gpu: GpuModel;
98 generated: GeneratedScene;
99 modelSource: string;
100 sceneSource: string;
101 recorder: Recorder;
102 scene: Scene;
103 params: Params;
104 sceneParams: Params;
105 dt: number;
106 }) {
107 this.device = init.device;
108 this.spec = init.spec;
109 this.air = init.air;
110 this.gpu = init.gpu;
111 this.generated = init.generated;
112 this.modelSource = init.modelSource;
113 this.sceneSource = init.sceneSource;
114 this.recorder = init.recorder;
115 this.#scene = init.scene;
116 this.#params = init.params;
117 this.#sceneParams = init.sceneParams;
118 this.#dt = init.dt;
119 }
121 static async create(opts: StudioSessionOptions): Promise<StudioSession> {
122 const { device, spec } = opts;
123 const air = makeAirGrid(opts.nx, opts.Lx ?? DOMAIN_X);
125 const defaults = specParams(spec);
126 const params = { ...defaults.params, ...opts.params };
127 const sceneParams = { ...defaults.sceneParams, ...opts.sceneParams };
129 // A provisional timestep from the speed of air; the scene is evaluated
130 // against it, and only an edited scene with a faster medium changes it.
131 let dt = stableDt(air.h, C_AIR, CFL);
132 let generated = generate(spec, air, dt);
133 const sceneSource = opts.sceneSource ?? generated.sceneSource;
135 const buildScene = (): Scene =>
136 Scene.create({
137 air,
138 source: sceneSource,
139 outputs: generated.sceneOutputs,
140 consts: generated.sceneConsts,
141 paramNames: generated.sceneParamNames,
142 params: sceneParams,
143 });
144 let scene = buildScene();
145 const dtFinal = stableDt(air.h, Math.max(scene.cmax, C_AIR), CFL);
146 if (dtFinal < dt) {
147 // A faster medium tightens the CFL bound; resize the object grids once
148 // at the final timestep. The scene's fields do not depend on dt.
149 dt = dtFinal;
150 generated = generate(spec, air, dt);
151 }
153 const modelSource = opts.modelSource ?? generated.modelSource;
154 const ops = new OpPlan(device, generated.ops);
156 // The scene's evaluated fields replace the generator's placeholders.
157 const fields = { ...generated.modelFields, ...scene.fields };
159 const gpu = await GpuModel.create({
160 device,
161 ops,
162 consts: generated.modelConsts,
163 fields,
164 source: modelSource,
165 paramNames: generated.modelParamNames,
166 state: generated.state,
167 operandBudget: opts.operandBudget,
168 });
169 gpu.setDt(dt);
171 // The microphone listens to the host-owned pressure buffer, which is
172 // where both `init` and `step` leave their result.
173 const recorder = await Recorder.create({
174 device,
175 field: gpu.stateBuffer('p')!,
176 nx: air.nx,
177 ny: air.ny,
178 nz: air.nz,
e18e57eRender scenes outside the browserJeremy Magland 179 capacity: opts.traceCapacity,
182 const session = new StudioSession({
183 device, spec, air, gpu, generated, modelSource, sceneSource,
184 recorder, scene, params, sceneParams, dt,
185 });
186 gpu.setParams(params);
187 return session;
188 }
190 get scene(): Scene {
191 return this.#scene;
192 }
194 get dt(): number {
195 return this.#dt;
196 }
198 get params(): Params {
199 return this.#params;
200 }
202 get sceneParams(): Params {
203 return this.#sceneParams;
204 }
206 /** Each object's grid, by id. */
207 get grids(): Record<string, ObjectGrid> {
208 return this.generated.grids;
209 }
211 /**
212 * Re-evaluate the scene under a running model — a body edit, an object
213 * moved. It is data, not code, so this needs no recompile. The timestep is
214 * deliberately left alone: it was set from max(cmax, c_air) at build time,
215 * and a scene edit that *raises* the fastest speed above that needs a
216 * rebuild anyway (the caller compares `dtWanted` and rebuilds when they
217 * disagree).
218 */
219 setScene(sceneParams: Params, source?: string): void {
220 const scene = Scene.create({
221 air: this.air,
222 source: source ?? this.sceneSource,
223 outputs: this.generated.sceneOutputs,
224 consts: this.generated.sceneConsts,
225 paramNames: this.generated.sceneParamNames,
226 params: sceneParams,
227 });
228 this.#scene = scene;
229 this.#sceneParams = sceneParams;
230 this.gpu.uploadFields(scene.fields);
231 }
233 /** The timestep the current scene would ask for. Differs from `dt` only
234 * when a scene edit changed the fastest speed, which calls for a rebuild. */
235 get dtWanted(): number {
236 return stableDt(this.air.h, Math.max(this.#scene.cmax, C_AIR), CFL);
237 }
239 setParams(params: ModelParams): void {
240 this.#params = params;
241 this.gpu.setParams(params);
242 }
244 /** Run `init`: every object drawn into its excitation, silent air, t = 0. */
245 excite(): void {
246 this.gpu.init();
247 this.recorder.clear();
248 this.t = 0;
249 this.steps = 0;
250 }
252 /** Put the microphone at the grid point nearest (x, y, z), metres. */
253 setMic(x: number, y: number, z: number): void {
254 const { Lx, Ly, Lz, h } = this.air;
255 this.recorder.setProbe(
256 (x + Lx / 2) / h - 0.5,
257 (y + Ly / 2) / h - 0.5,
258 (z + Lz / 2) / h - 0.5,
259 );
260 }
262 /** Advance `n` steps. Synchronous: records and submits, nothing read back.
263 * The microphone samples inside the same submission, once per step. */
264 step(n = 1): void {
265 this.gpu.step(n, (enc) => this.recorder.encode(enc));
266 this.t += n * this.#dt;
267 this.steps += n;
268 }
270 /** Wait for the submitted steps to finish, without reading anything back. */
271 sync(): Promise<undefined> {
272 return this.device.queue.onSubmittedWorkDone();
273 }
275 /** Read a named field back to the CPU. */
276 read(name: string): Promise<Float32Array> {
277 return this.gpu.read(name);
278 }
280 describe(): { init: string[]; step: string[] } {
281 return this.gpu.describe();
282 }
284 destroy(): void {
285 this.recorder.destroy();
286 this.gpu.destroy();
287 }
↑↓ move↵openescclose