3Instruments as files. Each instrument is one JSON document carrying a
4parameter spec in physical units and the raw WebGPU programs for two
5operations: a precompute that runs when the instrument's shape changes, and
6a render that turns tuning parameters and a list of stimuli into audio. The
7host is a generic runner that knows nothing about drums or strings; it
8allocates buffers, feeds parameters to shaders as pipeline override
9constants, walks the file's dispatch schedules, and reads the results back.
11This is a restructuring of the fast render path in
12[soundbox](../soundbox), which demonstrated the physics: for a linear,
13one-way-coupled scene, the room's response to every mode of every vibrating
14object can be recorded in a single reciprocal air run, after which pitch,
15decay, brightness and excitation changes cost about a second of synthesis
16rather than a re-simulation. A limitation of that arrangement is that the
17knowledge of what to precompute, what invalidates it, and how to synthesize
18from it lives in the application's host code, one hard-coded path per
19concern. Here we move all of it into the instrument file, so that the cost
20structure is data:
22- **geometry** parameters (sizes, body shape, placement, and the
23 microphone position, which is geometry like any other) feed the
24 precompute; changing one invalidates the bake.
25- **accuracy** parameters (grid size, room response length, mode set
26 margin) also feed the precompute; they trade fidelity against cost and
27 bake size.
28- **tuning** parameters (pitch, decay, brightness, strike position and
29 strength, coupling gains) feed only the render and are free to move.
31Three instruments are included, one per object type: a **drum** (a struck
32membrane over a cylindrical shell), a **dulcimer** (a plucked stiff string
33over a box with a sound hole, radiating along its line and driving a bridge
34patch), and a **chime** (a struck stiff bar in open air).
36## What the precompute produces
38The bake is a set of impulse responses, one per coupling pattern. For the
39drum at the default settings (a 96 x 48 x 48 air grid, timestep 8.77 µs),
40the head's sine modes are recorded up to twice what the air grid can
41spatially resolve across it, 961 patterns, each with a half-second response
42at the microphone: a 219 MB file that depends only on geometry and
43accuracy. Discrete reciprocity collapses the recording to one air run,
44kicked at the microphone, with every pattern projected from the field after
45each step; on the development machine this takes about 8 s. The render then
46solves each mode of each stimulus in closed form (the leapfrog restricted
47to a sine eigenmode is a scalar two-term recursion) and filters each kernel
48with a complex one-pole per root, one GPU thread per (root, kernel) pair:
495 s of drum in 1.4 s, for any setting of the tuning parameters.
51Stimuli are a list of events, `[{"t": 0}, {"t": 0.6, "set": {"f0": 147}}]`,
52each an excitation at time t with any tuning parameter overridden for that
53event alone. The system is linear and time-invariant, so events superpose
54exactly (the test suite holds this to 4e-8) and cost no new precompute;
55playing a drum roll, or the same head retuned per strike, reuses one bake.
57## The file format
59An instrument file (see `src/format.ts` for the schema, `instruments/` for
60the three instances) contains:
62- `params`: every parameter with unit, default, range, and its class.
63- `derived`: an ordered map of arithmetic expressions (`src/expr.ts`) for
64 the discretization: grid sizes from stability bounds, the timestep from
65 the CFL condition, mode counts, buffer sizes. The instrument, not the
66 host, knows its numerics.
67- `shaders`: WGSL, with every grid size and parameter declared as a
68 pipeline `override` constant, so the text is fixed and one file covers
69 every setting.
70- `precompute` and `render`: dispatch schedules (buffers, setup passes, a
71 loop with optional buffer rotation for leapfrog histories), with the
72 precompute's outputs read back into the bake and the render's inputs
73 uploaded from it.
75An optional `visual` section describes how to draw the instrument, and it
76deliberately contains almost no geometry of its own. It names the passes to
77run (the same medium shader the precompute uses) and which of the resulting
78fields to raymarch: the body mask as a shaded solid, each coupling profile
79as a translucent cloud. What the viewer shows is therefore what the solver
80sees, grid effects included; coarsen `nx` and the sound hole staircases,
81thin a wall below what the grid can carry and it draws as a haze instead of
82a surface, which is exactly the leak the solver has. The only declarative
83part is a short list of markers (the object's outline, the microphone)
84whose coordinates are expressions over the parameters, drawn as crisp
85overlays. A `partials` entry tells the spectrum view where each rendered
86mode's pole sits in the slots buffer.
88The bake sidecar (`.bake`) is keyed by a SHA-256 over the instrument's
89name, its precompute definition, and the geometry and accuracy values, so
90an edited instrument or a moved microphone invalidates it while tuning
91edits do not.
93The three files are generated by `author/build.ts` (`npm run author`), and
94the physics they encode is soundbox's, where it is compiled from MATLAB by
95[numbl](https://numbl.org). numbl remains the source of truth in a concrete
96sense: the test suite renders each instrument from its bake and compares
97against the numbl-compiled timestepped solver for the equivalent scene,
98run on the same GPU.
100## The page
102The page shows the three altitudes at once. The parameter sliders are split
103by class, with the 3D view updating immediately on any geometry or accuracy
104edit (the medium pass costs milliseconds) while the bake indicator goes
105stale; Precompute, with progress and a cancel, is the price of the change.
106After a render the trace is drawn with its spectrum on a log-frequency
107axis: the measured curve, tick marks at every rendered mode's discrete
108frequency (read back from the slots buffer, so they are the scheme's own
109partials, dispersion included), and shading over the band the grid does not
110resolve. For the default chime the ticks sit at 741, 2548 and 4415 Hz where
111the continuous bar would put 780, 3120 and 7020; the measured peaks land on
112the ticks, and the gap is the numerical dispersion made visible.
114## Accuracy, measured
116At nx = 32 over 6000 steps, against the timestepped solver: chime 3.1e-4,
117dulcimer 1.4e-4, drum 2.4e-3 relative L2 error, the drum retuned and
118restruck from the same bake 3.5e-4. The drum's floor of 2.4e-3 is f32
119rounding, shared with the solver itself. The one deliberate approximation
120is the membrane's mode set: modes are recorded up to `mode_margin` times
121the air grid's spatial resolution across the head, and the error against
122the full solver falls from 5.3e-2 at margin 1.25 to 4.0e-3 at the default
123margin 2, saturating at the f32 floor from 2.5 up. Line objects have few
124modes and record all of them. The room response length is the other
125truncation; the CLI reports how far down the kernels are where it cuts.
127## Honest limitations
129The physics inherits soundbox's simplifications: one-way coupling (no
130sympathetic resonance, no air loading on the membrane), rigid bodies with
131an absorbing skin rather than modes of their own, a bar pinned rather than
132free at its ends, and excitations that are initial conditions, so there is
133no bowing or blowing. The fast path produces the microphone trace and
134nothing else; there is no pressure field to draw. Multi-event renders cost
135one synthesis pass per (event, mode, root) over the whole trace, so a long
136event list scales linearly even where events are sparse. And the closed
137form holds only because the generated air is uniform; an instrument with a
138spatially varying sound speed would break the reciprocity the precompute
139rests on.
141## Running it
143```
144npm install
145npm run render -- instruments/drum.instrument.json --seconds 5
146npm run render -- instruments/drum.instrument.json --set f0=90 --set shellr=0.2
147npm run render -- instruments/drum.instrument.json --events hits.json
148npm run dev # the page: sliders, bake, render, listen
149npm test # host checks + validation against soundbox's solver
150```
152The CLI needs the optional `webgpu` package (Google Dawn) and, on a
153headless Linux box, working Vulkan drivers. The tests additionally assume
154the vault layout: a soundbox checkout at `../soundbox` with its own
155`node_modules` installed (numbl arrives through it).