| .github | |
| scripts | |
| src | |
| tests | |
| .gitignore | |
| index.html | |
| package-lock.json | |
| package.json | |
| README.md | |
| THEORY.md | |
| tsconfig.json | |
| vite.config.ts |
steelsim41#
A browser application for designing a virtual cymbal and simulating a drumstick strike on it. The cymbal is described by a short JavaScript script (material, bounding box, outline, thickness field, stress-free profile, mount, and losses) that the user edits in the page, or that an AI writes for them. The sound is computed by solving the geometrically nonlinear (von Karman) plate equations with in-plane inertia and a spatially varying thickness, stepped explicitly on the GPU through WebGPU. The model and the numerical method are described in THEORY.md.
The physics and most of the interface come from steelsim31 (the hanging steel sheet); the changes here are the scripted design, the thickness field, the mount, an arbitrary outline (including a center hole), and the removal of the push excitation, the hit series, and the feature-list shape editor.
The app is deployed by the deploy workflow on every push to main: https://steelsim41--proof-of-concept.vault1-sites.magland.org/
Running#
Requires Node 20 or later and a browser with WebGPU (Chrome or Edge 113+; Firefox and Safari with WebGPU enabled).
npm install
npm run dev # development server
npm run build # static site in dist/
npm test # all tests (see below)
Using the app#
The sidebar has three parts: the design script, the strike and simulation parameters, and the controls. The main area shows the plate (2D color map or a lit 3D height field; drag to orbit, wheel to zoom), the movie and playback toolbar, and the spectrogram of the listener signal.
Design script. Pick a preset from the dropdown (a flat steel rectangle by default; a bowed rectangle, a flat disc on a stand, a 16 inch crash, the same crash with a dozen dents, a 20 inch ride, an ellipse with a twist, an 18 inch china) and edit it freely; the script is compiled as you type (or with Ctrl+Enter) and the view shows the resulting stress-free shape, colored by height or by thickness. Errors are reported under the script, and a summary lists the bounding box, the material, the thickness range, the mass, the grid, the profile's peak-to-peak height and largest slope (with a warning above the shallow-shell range), the mount, and the losses. Revert restores the selected preset.
The script is a plain JavaScript function body that returns the design object. All lengths are meters; inch and mm are conversion constants. Every field function receives (x, y, r, theta): the point relative to the center of the bounding box, its distance from the center, and its angle from the x axis.
const D = 16 * inch, R = D / 2;
return {
name: "16 inch crash",
material: { E: 110e9, nu: 0.34, rho: 8800 }, // Pa, -, kg/m^3
bounds: { width: D, height: D }, // bounding box [m]
inside: circle(D, 12 * mm), // outline, or (x, y, r, theta) => boolean; omit for the whole box
thickness: (x, y, r) => lerp(1.4 * mm, 0.8 * mm, clamp((r - 60 * mm) / (R - 60 * mm), 0, 1)),
profile: (x, y, r) => 16 * mm * (1 - (r / R) ** 2) + dome(r, 60 * mm, 12 * mm), // stress-free height [m]
mount: { radius: 20 * mm, stiffness: 2e4, damping: 5 }, // or null for a free plate
loss: { sigma0: 0.6, sigma1: 2.5e-4, sigmaIn: 20 },
};
thickness and profile may be numbers or functions; inside, mount, and loss are optional (mount may also give x and y). Helpers in scope: circle(diameter, hole), ellipse(width, height, hole), rectangle(width, height, hole) (outlines), clamp, lerp, smoothstep(e0, e1, x), bump(d, radius, height) (Gaussian), dome(d, radius, height) (cosine dome with zero slope at the rim), bow(s, span, sag, offset) (parabolic bow), and noise2d(seed, length) (a smooth random field of unit RMS). The profile's mean plane is removed before use, since only its slopes enter the physics and a tilt would add a spurious membrane coupling.
Strike and simulation. The strike point is given as fractions of the bounding box (the yellow ring in the view), with the stick speed, its effective mass, the Hertz contact stiffness and exponent, and the tip radius. The listener distance and direction can be changed while a simulation runs. The grid spacing (8 mm by default), the in-plane mass factor, the sample rate, and the frame rate take effect at the next Start; the panel reports the grid, the frequencies at which grid dispersion lowers the partials by 3 and 10 percent, the time step, and the 60 dB decay times of the design's losses. The rigid-body projection applies to free plates only; a mounted plate takes the reaction of the strike through the mount.
Running. Press Start. The stick strikes the plate at rest in its stress-free shape, and the recording of audio and movie frames starts at the impact. The view shows the total shape by default (profile plus vibration); untick Show profile to see the vibration alone, which makes a small vibration on a curved cymbal visible. Pause halts the run temporarily; Stop ends it, keeps the audio and movie, and unlocks the script and the fields. To extend a finished run, raise the duration and press Continue. After the run, drag the time slider to explore the movie; Play starts the audio from the slider position and moves the movie and the spectrogram cursor with it; Live returns to the running view and Design to the stress-free shape. The audio can be played or downloaded as a 16-bit WAV at any time; Absolute level maps 1 Pa to full scale shifted by the gain slider (the peak level in dBFS is shown with a clipping warning), and Loudness and Peak normalize each run instead.
URL parameters. ?preset=crash-16 selects a preset; any strike, listener, or numerics parameter can be preset in SI units, for example ?preset=ride-20&spacing=0.006&duration=3&autostart=1.
The default grid spacing of 8 mm keeps the simulation interactive but lowers the bending partials by a few percent near 1 kHz and by 10 percent a few kilohertz higher, depending on the thickness; the panel reports the two frequencies. The stable time step is set by the in-plane wave speed (about 1.3 microseconds at 8 mm for steel and 1.7 for bronze) and does not depend on the thickness, so a thick ride costs the same per step as a thin crash; a 16 inch cymbal at 8 mm has about 2000 nodes and runs at 60 to 70 thousand steps per second on an integrated GPU, or roughly 50 s for 4 s of sound.
Tests#
npm test runs three groups of tests with Node's built-in runner.
tests/cpuRef.test.tschecks the float64 reference of the spatial discretization without a GPU: symmetry of the stiffness with exactly three rigid modes per block on uniform, tapered, elliptical, and holed plates, forces as the exact gradient of the discrete energy (flat and bowed, uniform and tapered, free and mounted), dissipativity of the loss terms and the mount's dashpot, the scaling of the bending frequencies with the thickness, the mount's stiffness against rigid translation and tilt, and convergence of the free square and circular plate frequencies to the reference values of Gorman and Leissa.tests/design.test.tschecks the design script: compilation, the checks and their messages, the helpers, the mask of a disc with a hole, the removal of the mean plane, the mount patch, and that every preset evaluates on the default grid.tests/gpu.test.tsserves the app with Vite and drives a headless Chromium through Playwright. It compares one step of the WGSL kernel with the float64 reference on flat, bowed, tapered, mounted, and elliptical presets (?selftest=1in the browser shows the same numbers), runs a short strike on a free plate and on a mounted cymbal to completion, and checks that a script with an error is reported and cannot start. It needs Playwright's Chromium (npx playwright install chromium) and a machine whose Chromium has WebGPU;npm run test:cpuruns only the first two groups.
Benchmark#
npm run bench runs the crash preset in headless Chromium at several grid spacings and with several concurrent runs and prints steps per second and the wall time per second of audio (--quick for a short version, --browsers N to spread the concurrent runs over N Chromium instances; on Node 22 add --experimental-strip-types). The kernel is latency-bound on grids of a few thousand nodes: an RTX 6000 Ada runs the 16 inch crash at 8 mm at about 140k steps per second, 2.2 times an Intel Iris Xe, and at 4 mm at about the same rate, 5 times the integrated GPU; concurrent runs on one card add only about 40 percent in total, since the card's dispatch rate is the limit.
Layout#
src/params.ts: simulation parameters, grid and mask, the plate as evaluated on the grid, material functions, patches of the strike and the mount, grid dispersion estimate.src/design.ts: the script API, the helpers, compilation and checks, evaluation on the grid.src/presets.ts: the preset scripts.src/sim/cpuRef.ts: float64 reference of the spatial discretization; used for the time-step estimate and the tests.src/sim/shaders/: WGSL kernels (time step, rigid-body sums and projection, listener sum, display extraction).src/sim/simulation.ts: WebGPU driver (buffers, batch encoding, readback).src/selftest.ts: the in-browser GPU-versus-CPU comparison.src/render/: 2D and 3D WebGPU rendering of the plate.src/audio/: FFT, spectrogram, WAV encoder, playback, level mapping.src/ui/editor.ts,src/ui/form.ts, andsrc/main.ts: the script editor, the parameter form, and the rest of the interface.tests/: the three test files above.