/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / README.md
89 lines · 13.5 KBPreviewCodeBlameHistoryRaw
1# cymbalsim
3A browser application for designing a plate or a cymbal and simulating a drumstick strike on it. The design is a short script in MATLAB syntax (material with its loss constants, bounding box, outline, thickness field, an optional stress-free profile), run in the page by [numbl](https://numbl.org), that the user edits in the page or that an AI writes for them. The sound is computed in one of two ways. The **linear model** computes the modes of the plate once per design and synthesizes the strike from them in about a second, on the CPU, in a pool of Web Workers. The **full model** solves the geometrically nonlinear (von Karman) plate equations with in-plane inertia and a spatially varying thickness, stepped explicitly on the GPU through WebGPU; it takes tens of seconds per second of sound and captures the pitch glide and the wash of a hard hit. Both use the same grid, so the linear sound is the small-amplitude limit of the full one. The model and the numerical method are described in [THEORY.md](THEORY.md).
5The focus is on free plates, sheets, discs and ellipses, flat by default; every plate hangs freely.
7## Running
9Requires Node 20 or later. The linear model runs in any modern browser; the full model and the 3D view need WebGPU (Chrome or Edge 113+; Firefox and Safari with WebGPU enabled).
11 npm install
12 npm run dev # development server
13 npm run build # static site in dist/
14 npm test # all tests (see below)
16## Using the app
18The sidebar holds the run controls at its top, then the design script, the strike, listener and numerics parameters, and the mode browser. The main area shows the plate (a lit 3D height field with WebGPU, or a 2D color map; drag to orbit, wheel to zoom), the movie and playback toolbar, and the spectrogram of the listener signal.
20**Run mode.** The select at the top chooses the linear or the full model. In linear mode the modes of the current design are computed as soon as the script compiles (a progress bar shows the slices), and Synthesize produces the sound; after the first synthesis, a change of the strike or the listener parameters re-synthesizes automatically. In full mode Start begins the GPU run; Pause halts it, Stop ends it and keeps the audio and movie, and Continue extends a finished run after the duration has been raised.
22**Design script.** Pick a preset from the dropdown (a flat steel rectangle, an aluminum square, a bowed rectangle, a flat bronze disc, a tapered 16 inch crash, a 20 inch ride, the same crash with a bow and a bell, that crash hammered, a steel ellipse, a thunder sheet) and edit it freely; the script is compiled as you type (or with Ctrl+Enter) and the view shows the resulting stress-free shape. 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 loss constants with the coincidence frequency, and the estimated decay. Revert restores the selected preset.
24The script fills in a struct named `design`. All lengths are meters. The outline, the thickness, and the profile are numbers or function handles `@(x, y, r, theta)` of the point relative to the center of the bounding box, its distance from the center, and its angle from the x axis; the handles are applied to whole grid matrices, so they use elementwise operators. A script is self-contained: nothing is in scope but numbl's own functions, so a script defines the shapes it needs as ordinary MATLAB functions at the bottom of the file, and the unit constants at the top.
26```matlab
27inch = 0.0254; mm = 1e-3;
28D = 16*inch; R = D/2;
29design.name = '16 inch flat crash';
30design.material.E = 110e9; % Pa
31design.material.nu = 0.34;
32design.material.rho = 8800; % kg/m^3
33design.material.lossFactor = 2e-4; % internal friction of the metal (hysteretic loss factor)
34design.material.alpha = 18e-6; % thermal expansion [1/K] } thermal constants, for the
35design.material.cp = 370; % specific heat [J/(kg K)] } thermoelastic damping
36design.material.k = 40; % conductivity [W/(m K)] }
37design.bounds.width = D; % bounding box [m]
38design.bounds.height = D;
39design.inside = @(x, y, r, theta) r <= R; % omit for the whole box
40design.thickness = @(x, y, r, theta) 1.2*mm + (0.7*mm - 1.2*mm) * ramp(40*mm, R, r);
41design.profile = 0; % optional stress-free height [m]; a handle for a bow or a bell
43function v = ramp(a, b, x)
44v = min(max((x - a) ./ (b - a), 0), 1);
45end
46```
48The decay of the sound is not prescribed but derived: from the loss factor of the metal's internal friction (`lossFactor`, 0 when omitted), the thermoelastic damping that follows from the three thermal constants (all three or none; without them there is no thermoelastic loss) and `design.temperature` (293 K by default), the radiation of each mode into the air, which the linear model computes from the mode's shape. A script that still sets `design.loss` is refused with a message saying so.
50**Modes.** In linear mode the mode browser lists every mode up to the cutoff frequency with its decay time and, beside it, the decay time each mechanism alone would give: the air (radiation), the metal (internal friction) and thermal (thermoelastic); click a row to draw the mode's shape on the plate (scaled to 1 mm), and filter the list by frequency. The modal frequencies are also drawn as ticks on the left edge of the spectrogram, in either run mode, so that the partials of a full run can be compared with the modes.
52**Strike and listener.** 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, its angle from the plate's normal, and its azimuth can be changed at any time; the microphone in the view moves with them, drawn nearer than the listener is so that both fit in the frame.
54**Numerics.** The grid spacing (8 mm by default), the cutoff frequency of the modes, the sample rate, and the frame rate. The Notes under the group report the grid, the frequencies at which grid dispersion lowers the partials by 3 and 10 percent, the count and time of the modes, the time step and step count of the full model, and the decay times of the fitted two-parameter loss of the full model next to the estimate it was fitted to.
56**Playback.** After a 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; Design returns to the stress-free shape and, in full mode, Live to the running view. 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 the two normalized modes scale each run to its loudness or its peak. When the signal to be played is loud enough to hurt in headphones (a peak within 10 dB of full scale, or a short-term level of -20 dBFS RMS over some 50 ms window, which every loudness-normalized hit reaches), Play first shows a warning to turn the volume down, with a Play anyway button. Once accepted, the same signal replays without a prompt; a new recording, another level mode or a change of the gain brings the warning back.
58**Tutorial.** The Tutorial button at the top of the sidebar opens a panel beside the view with a guided tour of the physics and the mathematics of the simulator in eighteen pages: the bending stiffness and the plate equation, dispersion, modes and their scaling laws, the density of modes, the grid and its dispersion error, the eigenvalue computation, decay times, the strike and the driving-point impedance, the modal force and the choice of the strike point, the modal synthesis, radiation and directivity, the spectrogram, the taper, curvature, the nonlinear model, and the limitations. Each page reads the design in the editor and shows its numbers (the bending stiffness, the wavelength at 1 kHz, the count of modes against Weyl's estimate, the gains of the first modes at the strike point and at the listener), and the buttons under Try it load presets, change parameters, compute the modes, strike and play, so that every formula can be checked against the model; a few pages remember mode lists for a comparison, such as the plate twice as thick against the reference. The panel keeps its page between visits, and `?tutorial=1` or `?tutorial=<page id>` opens it from the address.
60**URL parameters.** `?preset=crash-16` selects a preset, `mode=full` the run mode, and any strike, listener, or numerics parameter can be preset in SI units, for example `?preset=ride-20&spacing=0.006&duration=3&autostart=1`; `nogpu=1` disables WebGPU.
62## Tests
64`npm test` runs eight groups of tests with Node's built-in runner; `npm run test:cpu` runs the first seven without a browser.
66- `tests/design.test.ts`: the design script (compilation, the checks with their messages, hoisted functions, the mask of a disc with a hole, the orientation of the sampled matrices, the removal of the mean plane, the loss constants of the material), and that every preset evaluates on the default grid.
67- `tests/reference.test.ts`: the float64 reference of the discretization (rigid modes of the stiffness, forces as the gradient of the energy, dissipativity of the losses, thickness scaling).
68- `tests/assemble.test.ts`: the assembled stiffness equals the linearized reference force on a flat plate and on a bowed, tapered, holed plate.
69- `tests/modal.test.ts`: the banded factorization and its inertia count, the tridiagonal eigensolver, all modes of a small square and a small disc against a dense solve (degenerate pairs included), and the free square and disc frequencies against Gorman's and Leissa's values.
70- `tests/synth.test.ts`: a single mode under the stick follows the closed-form damped sinusoid, the radiation gain of a piston equals the analytic Rayleigh sum, the attack and the tail join, the parts computed by the workers sum to the whole.
71- `tests/loss.test.ts`: Zener's thermoelastic loss factor peaks where it should, the outline perimeter of a rectangle and a disc, the limits of the average radiation efficiency, the two-parameter fit, the radiation rate of a piston against the closed form and the FFT convolution against the direct double sum for every mode, and the thermoelastic rate of a uniform plate.
72- `tests/tutorial.test.ts`: every page of the tutorial renders with no design, with a plate, and with its modes, without undefined values and with balanced math delimiters; the actions run against an in-process host; the scaling pages' comparisons give the predicted ratios (2 for twice the thickness, 4 for half the size on a grid of the same cell count) to rounding.
73- `tests/gpu.test.ts`: serves the app with Vite and drives a headless Chromium through Playwright: the in-page self-test of the WGSL kernel against the float64 reference on five presets (`?selftest=1` shows the same numbers in the browser), the linear model's modes and synthesis, the tutorial panel with its typeset mathematics and an action that computes the modes, the loudness warning before playback, an error in a script, short full runs on a flat and on a tapered plate, the absence of a high-frequency noise floor in a curved design's full run, and a light hit simulated both ways, whose spectra must agree. It needs Playwright's Chromium (`npx playwright install chromium`) and a machine whose Chromium has WebGPU.
75## Layout
77- `src/params.ts`: simulation parameters (strike, listener, numerics), grid and mask, the plate as evaluated on the grid, material functions, patches, listener geometry, grid dispersion.
78- `src/design.ts`, `src/mruntime.ts`, `src/presets.ts`: running the script with numbl, the checks on the design struct, evaluation on the grid, and the preset scripts.
79- `src/loss.ts`: the losses from physical constants (internal friction, Zener's thermoelastic damping, the average radiation efficiency, the outline perimeter) and the two-parameter fit of the full model.
80- `src/model/reference.ts`: float64 reference of the discretization (forces, losses, energy, the time-step estimate).
81- `src/model/assemble.ts`: the linearized stiffness in banded storage and the lumped mass.
82- `src/modal/banded.ts`, `lanczos.ts`, `modes.ts`: the banded factorization with its inertia count, shift-invert Lanczos on one slice, and the sliced computation of all modes through a job runner.
83- `src/modal/damping.ts`: the per-mode radiation rate from the mode shape by FFT convolution of the radiation resistance, and the per-mode thermoelastic rate weighted by the local bending energy.
84- `src/modal/synth.ts`, `worker.ts`, `pool.ts`: the modal synthesis (contact, radiation gains, attack table, tail, movie), the worker, and the pool that runs the eigenvalue jobs, the per-mode losses, and the synthesis split between the workers.
85- `src/sim/shaders/`, `src/sim/simulation.ts`, `src/sim/selftest.ts`: the WGSL kernels (step, listener sum, extraction), the WebGPU driver, and the in-browser comparison with the reference.
86- `src/render/`: 2D and 3D rendering of the plate with the markers of the strike and the listener, with a 2D canvas fallback.
87- `src/audio/`: FFT, spectrogram, WAV encoder, playback, level mapping.
88- `src/ui/`: the script editor, the parameter form, the mode browser; `src/main.ts` wires it all together.
89- `src/tutorial/steps.ts`, `tutorial.ts`: the pages of the tutorial (text with TeX mathematics, live numbers from the application's state, and actions on a small host interface that `main.ts` implements), and the panel that shows them with KaTeX.
↑↓ move↵openescclose