/ simulated_instruments / cymbal
Sign in
simulated_instruments / cymbal
cymbal / webapp / README.md
7.6 KBPreviewCodeBlameHistoryRaw

Cymbal Simulator — web app#

Interactive browser implementation of the physical model described in ../README.md. The algorithm itself is written in MATLAB syntax, in ../matlab/*.m, and executed client-side by numbl inside a Web Worker; the TypeScript here is only the UI, the plots, the WAV encoder, and the marshalling of parameters and tensors in and out of the numbl runtime.

The signal chain: a Hertz stick contact (or a seeded repeated figure, with Strokes above 1), the modes of a bronze shell of revolution from one finite element eigensolve per nodal-diameter order, hybridized to stand for the hammering that makes a real cymbal irregular, a nonlinear cascade of modal energy up the frequency ladder, an oscillator bank radiating through a coherent sum of surface dipoles to the listener point, and Kaiser-windowed anti-aliased decimation to 48 kHz.

Usage#

npm install
npm run dev        # development server
npm run build      # production build in dist/
npm run selftest   # run ../matlab/cymbal_validate.m through numbl in Node

The self-test runs the closed-form and reproducibility checks and the pinned diagnostics. The MATLAB code can also be run directly: cd ../matlab && numbl run cymbal_demo.m (or cymbal_validate.m).

Where the time goes#

The expensive stage is the mode basis: one generalized eigensolve per nodal-diameter order, about two seconds at the defaults, and that is exactly the piece the worker caches. Striking somewhere else, changing the stick, moving the microphone or retuning the cascade all reuse the cached modes. The synthesis that follows is an oscillator bank rather than the closed-form exponentials of the sibling triangle repository, because the cascade makes every amplitude time-varying; the per-frame phase advance is a unit-modulus multiply and the oscillator basis is hoisted out of the loop, so a three-second render costs about the same again.

Reading and editing the code in the app#

The Code button opens the MATLAB source in a CodeMirror editor with MATLAB syntax highlighting. Every .m file in ../matlab is listed, tagged by whether the app executes it or it is a CLI entry point. The sources are editable: an edit re-renders after the usual debounce, so changing the physics in the editor is heard and plotted the same way moving a slider is. Zeroing the meridian curvature cs in cymbal_shell.m, for instance, decouples bending from stretching and collapses the partials back onto those of a flat disc.

Edits live in memory only. They are deliberately not persisted, so a reload restores the code the app shipped with; "Revert file" and "Revert all" undo them sooner, and a dot marks the files that differ from the shipped source. A fault in edited code is reported with its file and line (numbl skips a file it cannot parse and then fails at the call site, so the worker captures the parse warning and reports that instead), the last successful render stays on screen, and reverting recovers.

How rendering is tiered#

Changing any parameter triggers a debounced fast preview (oversample 1); the Render button runs the full-quality pass. While the current result is a preview, the Play and Download buttons and the status badge say so. The worker runs each stage as a separate numbl execution and keys its caches by tier: plate geometry, material, damping, radiation scaling and discretization parameters invalidate the mode basis; the stick, the gesture, the cascade and the listener only re-run the strike process, the cascade and the radiation; pre-delay, normalization and bit depth only re-run the output stage. Intermediate tensors stay inside the worker as numbl RuntimeValues.

The New seed button redraws the timing and velocities of a repeated figure without touching any other parameter (a single stroke is deterministic, so the button is disabled there). The Loop button repeats the render continuously, swapping in each new render as parameters change. Parameters persist in localStorage; "Reset to defaults" restores the values in src/params.ts (which ../matlab/cymbal_defaults.m mirrors).

Layout#

The cascade plot#

The plot the sibling repositories do not have. The cascade solve tracks the energy of every mode at the control rate, and the plot collapses that onto a log-frequency by time grid, so the upward flux is visible directly. Dropping Cascade gain to zero flattens it into horizontal streaks, every mode simply decaying where the stick left it, and the sound turns from a crash into a gong.

Placing the stick#

The model addresses the contact by arclength fraction along the meridian, strike_r, which is the right coordinate for the physics and a poor one to aim with. The cross-section at the top of the page draws the real profile to scale, with the bell, bow and edge marked and a ruler in centimetres, and both contacts are draggable: the amber marker is strike_r and the dim one is roll_r2, which even-numbered strokes land on. The strip underneath shows what the current contact actually excites, band by band, which is the quickest way to see why the bell and the edge are different instruments: a mode with n nodal diameters falls off as roughly r^n toward the axis, so at the bell an n = 40 mode sits at 1e-22 of its rim amplitude and simply is not driven. Striking the bell puts 91 of the 378 modes into play, striking the outermost edge 178.

There is deliberately no azimuth control. The plate is a surface of revolution and the hammering is applied as a statistical mixing with no azimuthal structure, so striking at a different azimuth is exactly equivalent to rotating the listener, which the listener coordinates already do.

Three sliders worth pulling first#

Hybridization at 0 is the ideal, perfectly round plate. Its high nodal-diameter modes cannot radiate at all, so a few low-order partials carry everything and the instrument becomes a small bell; sliding it back up is hammering the cymbal. Fluctuation is off at the defaults: it broadens each partial by letting its amplitude wander about the mean the cascade solves for, which on paper closes the comb into a wash and in practice adds grit, so a few tenths is as far as it is worth taking. Stroke velocity sweeps the whole dynamic behaviour through one physical quantity, because the cascade strength goes as the square of the amplitude: at 0.7 m/s the same cymbal pings, at 6 m/s it opens right up.

A note on the seeded generator#

The stroke pattern draws from a 32-bit LCG in cymbal_rng.m rather than MATLAB's rand, so a given seed produces the identical pattern in the numbl CLI, in Node, and in the browser. That is what lets the self-test pin exact impulse sums and stroke times.

↑↓ move↵openescclose