1# cymroom
3A browser companion of [cymsim](https://vault1.magland.org/proof-of-concept/cymsim) and [cymsimweb](https://vault1.magland.org/proof-of-concept/cymsimweb). Those programs simulate the struck cymbal itself and compute its sound at one built-in listener; a `.cymsim` file they write can carry the normal acceleration of every node at the audio rate, which is exactly what another program needs to compute the sound elsewhere. cymroom is that other program: it loads a `.cymsim` file, places the cymbal in a 3D scene, and will compute the sound at any listener position, with or without room walls, by solving the acoustic wave equation in air on the GPU. The solver does not exist yet; what exists is the viewer, which shows every part of the file in 3D.
5## Running
7Requires Node 20 or later and a browser with WebGL.
9 npm install
10 npm run dev # development server
11 npm run build # static site in dist/
13## Using the app
15Load a `.cymsim` file with the button, by dropping it on the page, or with `?file=<url>` for a served file. The file may come from the Python package or from cymsimweb; the acceleration record (`output.record_acceleration: true` there) is optional but is what makes the probe and the acceleration movie available.
17The scene shows the cymbal at true scale with a ground grid and the stored listener position. Drag to orbit, wheel to zoom, right-drag to pan. The surface color is bronze, thickness, or rest height; overlays toggle the wireframe, the boundary loop, the nodal normals, the hit markers (orange for stick, magenta for mallet, placed at each configured hit's r and phi), the listener, the grid, and the axes.
19**Animation.** The playback bar animates whichever records the file holds; the acceleration record is the default when present. The displacement movie deforms the surface (scaled by the exaggeration selector) and tints it red/blue against the movie's overall peak. The acceleration record is a movie at the audio rate: each frame tints the surface by that sample's normal acceleration, normalized per frame because the signal decays through many decades, and borrows the nearest displacement frame for the shape. Both play against wall time divided by the slowdown factor; the position slider scrubs.
21**Probe.** Click a node to see it in the Probe tab: its position, thickness and normal, and, when the file has the acceleration record, that node's acceleration waveform and spectrogram, with a Play button that plays the node as a pickup (normalized; an accelerometer glued to that point). This is the signal the future solver will use as its source, one node at a time. Without the record the probe falls back to the node's displacement from the movie frames, which is a strobe for display rather than a signal.
23**Listener.** The Listener section moves the listener (x, y, z in meters; the scene marker follows). When the file has the acceleration record, the sound at the new position is re-rendered from it with the same free-field model as cymsim's built-in listener: a Rayleigh integral over element-mean accelerations for the front path and a diffracted path over the rim (the boundary loop, subdivided to about 720 points) for the back term, each element at its own fractional delay. The gather runs on WebGPU when the browser has it and on the CPU otherwise. `tools/render-check.ts` re-renders at the file's stored position and compares with the stored signals; both agree to about 1.5e-4 relative rms on the test file, on both backends (`--gpu` runs the WGSL kernel under Dawn, using the cymsimweb checkout's `webgpu` package).
25**Audio.** The Audio tab plays and plots the stored baffled and unbaffled signals and, after the listener has moved, the re-rendered pair. Playback is normalized to 0.9 full scale.
27The other tabs show the file summary (which groups are present and their sizes), the configuration YAML, the energy log (log scale, with the nonlinearity meter max|w|/h below), and the hit table.
29## Tools
31`node tools/make-test-file.ts out.cymsim` generates a small real file (the coarse test configuration, 0.35 s, with the acceleration record) using cymsimweb's CPU loop; it requires a cymsimweb checkout beside this repository. `node tools/read-check.ts file.cymsim` loads a file headlessly and prints what it found.
33## Layout
35| path | contents |
36|---|---|
37| `src/config/`, `src/io/`, `src/analysis/` | copied from cymsimweb: configuration schema and YAML, the .cymsim (HDF5) reader/writer, the acceleration and displacement stores, the STFT |
38| `src/scene.ts` | the three.js scene: cymbal mesh, overlays, picking |
39| `src/playback.ts` | frame sources over the two records and the slowed-down player |
40| `src/listener.ts` | listener re-rendering from the acceleration record (WebGPU gather, CPU fallback) |
41| `src/audio/` | copied from cymsimweb: Web Audio playback, WAV encoding |
42| `src/ui/plots.ts` | waveform, spectrogram and energy canvases |
43| `src/main.ts` | wiring |
45The copied modules follow cymsimweb's; the format is versioned (`format_version` attribute), so the copies only need updating when the format does.
47## Planned: the solver
49The intent is a finite-difference solution of the acoustic wave equation in air on a WebGPU grid, with the room as impedance boundaries. One engine, two source modes. In *IR mode* an impulse fires at the listener and the field is recorded at the cymbal nodes; by acoustic reciprocity (the Green's function is symmetric, and impedance walls preserve that) these recordings are the node-to-listener impulse responses, and the audio is the sum over nodes of the acceleration record convolved with them. The impulse run only needs to last the length of the room response, independent of the signal duration, and the responses are reusable across strikes. In *field mode* the per-element accelerations drive the grid directly over a short window, which is the intuitive picture (waves leaving the cymbal) and a cross-check of the IR-mode audio. Both modes treat the cymbal as transparent sources, the same simplification as the built-in listener model; the strict reciprocal dual of a dipole layer records the normal pressure gradient at the nodes rather than the pressure, which distinguishes the unbaffled from the baffled reconstruction.
51A grid that reaches 20 kHz in a room-sized volume is out of reach (the cell count grows as the cube of the bandwidth), so the solver will be band-limited to a few kHz; the free-field re-render above covers the full band without walls, and the top of the band with walls will need a geometric model.
53For depicting the field, the plan is volumetric rather than a slice: the pressure grid becomes a 3D texture composited by a single ray-marching pass in the existing scene, with |p| as opacity and sign as color, so a wavefront reads as a translucent expanding shell (from the listener in IR mode, washing over the cymbal; from the cymbal in field mode). The field then appears as one more entry in the Animate selector, reusing the slowdown and scrubbing controls, with at most a gain control added.