/ concept-collection / dulcimer
Sign in
concept-collection / dulcimer
dulcimer / README.md
119 lines · 5.8 KBCodeBlameHistory
3One plucked string and the box it sounds over, simulated as two coupled wave
4equations and solved live in the browser. Both solvers are written as MATLAB,
5run by [numbl](https://numbl.org) and compiled to WebGPU compute shaders; the
6pressure field is drawn straight out of the buffer the solver writes, a
7microphone records the air at one point every timestep, and the recording
8plays back at the pitch a microphone there would have heard.
10This is the instrument-shaped sibling of
11[acoustic-scattering-2d](https://github.com/concept-collection/acoustic-scattering-2d),
12whose MATLAB-to-WGSL compiler pipeline it reuses, and of
13[acoustic-scattering-3d](https://github.com/concept-collection/acoustic-scattering-3d),
14whose volume renderer it adapts. What is new here is that two equations run
15together, on two different grids, with the coupling between them part of the
16editable model.
18## The equations
20The string carries transverse displacement u (metres) on a line of nodes:
22```
23u_tt = cs^2 u_xx - kap^2 u_xxxx - 2 sig0 u_t + 2 sig1 (u_xx)_t
24```
26cs = 2 Ls f0 is the wave speed a string of length Ls needs to sound the
27fundamental f0. The fourth-derivative term is bending stiffness, expressed
28through the inharmonicity coefficient B (partial n lands near
29n f0 sqrt(1 + B n^2)); sig0 is plain decay, quoted as a 60 dB time; sig1
30damps high frequencies faster than low, which is why a plucked note starts
31bright and mellows. The pluck is an initial condition: a triangle drawn to
32the pluck point, released from rest. This is the standard stiff-string
33formulation (Bilbao, Numerical Sound Synthesis, ch. 7), with the standard
34explicit scheme.
36The air carries pressure p on a rectangular grid around the instrument:
38```
39p_tt + 2 sig p_t = c^2 lapw(p, wall) + s
40```
42`lapw` is a wall-masked Laplacian, and it is how the body is rigid. Each face
43of the 7-point stencil is scaled by the mask at the neighbour it reads, so a
44face into the shell carries no flux: the discrete Neumann (sound-hard)
45condition, in the divergence form div(w grad p). The flat siblings make
46walls out of fast material instead, which is unavailable here: wood at about
474000 m/s would cut the global timestep twelve-fold, where a mask costs
48nothing. What a mask cannot do is absorb, so the scene paints a thin lossy
49skin over the shell's surfaces, and that parameter is what tames the
50cavity's ring.
52The coupling from string to air is one-way and comes in two routes, each with
53its own gain. `string radiation` injects the string's acceleration along its
54own line of cells: the direct route, idealized, since a thin string is in
55reality a very poor radiator. `bridge drive` takes the string's pull at its
56bridge end (the tension times the arriving slope) and drives a patch of air
57just above the top plate: the real instrument's main route, minus the
58plate's own resonances, which this model does not carry. Either gain at zero
59switches that route off. The air does not push back on the string; at these
60amplitudes the back-reaction is far below everything else the model already
61neglects.
63## Two files, two grids
65As in the 2d sibling, the *model* (init and step, both grids together) is
66compiled: numbl lowers it to typed IR and this project's backend emits one
67WGSL kernel per source line, with five host-provided operations — `dxx`,
68`dxxxx` on the string, `lapw` in the air, and `spread`/`bridge` carrying the
69string into the air — as the only places anything reads a neighbour. The
70*scene* (the body: walls, absorption, and the two coupling profiles) is
71interpreted on the CPU once per edit, so it has the whole MATLAB subset
72available and costs no recompile.
74The two grids are not independent. The air's CFL condition fixes the
75timestep, and the string then chooses the finest node spacing that is stable
76at that dt for anything the sliders can reach, so moving a parameter never
77forces a recompile. In numbers, at the default 128-cell grid: 7.8 mm cells,
78dt = 6.6 µs (a 152 kHz sample rate), and a string of 59 nodes.
80## Listening
82The microphone is a one-thread GPU dispatch riding in the same submission as
83each timestep, so recording costs nothing and no readback happens until you
84ask to listen. Because everything is in SI units, playback needs no
85translation: one sample per timestep at 1/dt per second is real time at real
86pitch. A .wav download (48 kHz, resampled) is one button over.
88Two ways to run. *Watching* takes a few timesteps per display frame, the
89wave crawling in slow motion. *Render note* runs the solver flat out with no
90display until the requested seconds of audio exist, then plays them. On a
91discrete GPU a second of audio takes very roughly half a minute to render at
92the default grid; on an integrated one it can take several minutes, which is
93what the draft 64 grid is for (honest only to about 2.7 kHz, but several
94times faster).
96## Honest limitations
98The grid resolves sound to about 5.5 kHz at the default size, so the top of
99the timbre is simply absent, and the note is duller than a real instrument.
100The soundboard has no modes of its own: the bridge drives the air directly,
101so the body colours the sound only through its cavity and hole. The wall
102mask is perfectly rigid apart from its absorption skin, and the box walls
103are drawn at grid resolution, so plates thinner than about two cells leak.
104The coupling gains are physically arbitrary; the equations are linear, so
105they set relative balance, not absolute loudness.
107## Running it
109```
110npm install
111npm run dev # local dev server
112npm test # solver checks against desktop WebGPU (Google Dawn)
113npm run smoke # headless-browser check of the built page (environment permitting)
114```
116`npm test` compiles the actual .m files to actual shaders and checks physics:
117the measured pitch of the plucked string, the 60 dB decay time, that a sealed
118box keeps sound out and a sound hole lets the cavity speak, and that the
119microphone's spectrum sits on the string's partial comb.
moveopenescclose