| .. | ||
| src | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts |
Triangle 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 seeded roll of Hertz beater contacts (or a single strike, with Strikes set to 1), the bending, stretching, and torsional modes of the open bent rod from a finite element eigensolve, a coherent sum of element 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/triangle_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 triangle_demo.m (or triangle_validate.m).
Why it renders fast#
Nothing is integrated. Between strikes every mode is a damped exponential, so the synthesis evaluates exp(lambda t) over each inter-strike segment in closed form (chunked only to bound memory) and adds each strike's modal velocity jump as it lands. The cost that remains is the eigensolve of the two finite element families, about a second, and that is exactly the piece the worker caches: striking somewhere else, changing the beater, or moving the microphone reuses the cached modes.
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 takes effect on the next render, so changing the physics in the editor is heard and plotted the same way moving a slider is. Removing the bending correction sign flip in triangle_fem.m, for instance, decouples torsion from out-of-plane bending and audibly thins the cluster of partials.
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 failed render leaves nothing playable, so Play and Download go dead until a render succeeds, and reverting recovers.
How rendering is tiered#
Nothing renders on its own: the Render preview button runs a fast pass (oversample 1), Render full runs the full-quality pass, and either can be cancelled while it runs. The status shows the stage, a progress bar and the elapsed time. 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: rod geometry, material, damping, and discretization parameters invalidate the mode basis (the eigensolve); the strike and roll parameters, the beater, and the listener only re-run the strike process and the radiation; pre-delay, normalization, and bit depth only re-run the output stage. Intermediate tensors stay inside the worker as numbl RuntimeValues. Because the eigensolve does not depend on the synthesis rate, previews reuse the full-quality mode basis.
The New seed button redraws the roll timing and velocities without touching any other parameter (a single strike is deterministic, so the button is disabled there). The Loop button repeats the last render continuously, swapping in each new render as it completes. Parameters persist in localStorage; "Reset to defaults" restores the values in src/params.ts (which ../matlab/triangle_defaults.m mirrors).
Layout#
../matlab/— the algorithm:triangle_geometry.m,triangle_fem.m,triangle_basis.m,triangle_strikes.m,triangle_radiate.m,triangle_output.m,triangle_rng.m, plustriangle_render.m/triangle_demo.m/triangle_validate.mfor CLI usesrc/params.ts— parameter schema, defaults, tier assignment, persistencesrc/mfiles.ts— the.msources, bundled at build time and hashed for cache keyingsrc/worker/renderWorker.ts— off-thread numbl execution with tiered cachingsrc/lib/outline.ts— the open-triangle centerline, mirrored fromtriangle_geometry.mfor the scene and the mode-shape plotsrc/components/— 3D scene (three.js), parameter panel, code panel and editor, plots (strikes, waveform, spectrum, spectrogram, mode map, mode shapes)src/selftest.ts— Node harness for the MATLAB validation script
A note on the seeded generator#
The roll process draws from a 32-bit LCG in triangle_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 hit times.