Crash cymbal sounds like a pitched bell: findings from the cymbal-scatter spike #91
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal#91
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
patches/crash.hsounds like a struck bell, not a crash: very coherent, too much fundamental, and decidedly pitched. As bcox put it, a drummer wouldn't hit it unless the band were in the same key. The final ringout is a tuned bell with a distinct pitch.A listening spike on 2026-09-13 found out why, and how far the engine gets without new physics. This ticket records what it found, so a spec can start from it. Nothing here is merged or scheduled.
Where the spike is
Commit
a1b22acon local branchworktree-cymbal-scatter(worktree.claude/worktrees/cymbal-scatter), one WIP commit offmainatfd59eca. Not pushed. Every change is markedSPIKE (cymbal-scatter):dsp/modal_loop.h:SetScatterandScatteredPickup, the pickup-side phase noise and gain tilt.dsp/mode.handdsp/mode_bank.h: read-only accessors for each mode's quadrature state and pickup weight.dsp/modal_layer.h: note-on plumbing, withspike_*fields onModalPatch. They aren't serialized.materials/spec.handmaterials/generators.h: plates honorsecond_group_ratio, with their ownspike_second_aspect, plusspike_low_scatter.patches/crash.h:BuildCrashSpike(CrashSpike), where every knob below is a field.patches/factory.h: about 60crash-*variant rows. These break the bank tests. The suite hasn't been run on this branch.tools/spike_lowmodes.cc: prints a plate table's lowest modes and how far they sit from a harmonic series.To render any variant:
bazel run //patches:audition -- --patch crash-r-dm50 <dir>.The favorite so far is
crash-r-dm50:low_scatter0.12;bcox found
crash-r-dm30nearly as good.How the numbers were taken
Every figure is from key 60 and the audition renderer, summed to mono:
volumedetectmean dB over the stated window, with cascaded 2-pole filters setting the bands.aspectralstats.Flatness and crest are proxies. Each direction was decided by listening, and the verdicts are quoted.
1. Break up the modes' phase on the pickup, never on the resonator
The bell quality is a linear mode bank: steady lines, fixed phase relationships, exponential decay. Randomizing phase addresses it, and the spike tried three ways to do that.
v1: a random rotation of each mode's state every sample. The accumulated phase random-walks, and that puts its energy at slow rates. bcox: "I can hear the phase crinkling around; that modulation would need to be much higher frequency."
v2: a bounded, audio-rate phase deviation applied to the state. Each mode got a noise skirt, and the result sounded better. It broke the felt choke. The felt damper stays passive only if it reads and pushes the same state, which
CoupledMode::Process's comment derives. Rotating the state between that read and the next push breaks the pairing. Atamplitude_tilt0.35 with a 1 rad deviation, a full choke left the tail louder than no choke:At 0.3 rad it was roughly neutral.
v3: the same deviation applied when reading the pickup. Each mode is heard as
cos φ · x − sin φ · y, so the resonator, both sites and both junctions never see φ. The choke goes back to behaving like the shipped one:An early choke (1.2→1.3 s) takes the v3 tail 11.3 dB below its unchoked level by 5.5–6 s.
The noise shape that worked: each bin gets one one-pole-lowpassed noise sequence, scaled to 1 RMS. The deviation is
(floor + depth × envelope) × noisein radians, clamped at 0.24 cycles. The noise cutoff is proportional to the bin's mean frequency.One number disagrees with the patch's own notes.
crash.h's header records the shipped choke as an energy ratio of 0.097 over 8192 samples at 4.5 s. The windowed means above show only 2–5 dB. The two measurements use different methods, and this isn't resolved.2.
amplitude_tiltis the wrong control for "less fundamental"materials::ApplyScatterAndTiltmultiplies each mode's radial profile byratio^tilt. That weakens the stick, the hand and the pickup together, so the level you hear moves roughly as gain², and the choke loses its grip. Withamplitude_tilt0.35 and no scatter, the choke changed the tail by only −0.4 dB (4.5–5 s) and −1.0 dB (5.5–6 s).A pickup-only tilt, with gain per bin = (bin mean ratio)^tilt normalized to the largest, leaves the stick and the hand alone. It's also loop-free, so it's safe on a live macro.
It's a weak lever, though, because it can only turn things down. Going from 0.3 to 0.9 moves the balance only 2–3 dB and costs about 7.5 dB of overall level, with trim already at +6 dB.
Velocity. Adding 0.6 × velocity to the pickup tilt gives about 4 dB of velocity-driven swing in the lows' share of the total: −12.0 → −15.9 dB from velocity 0.3 to 1.0. That's the right direction. Real cymbals have much more fundamental on a soft stroke and much less on a hard one.
The shipped-style patch runs the other way. In
crash-pk60-*, which has a fixed pickup tilt of 0.6 and still has the noise wash, the lows' share rises with velocity: −11.4 → −8.3 dB. The suspected cause is the wash layer's own velocity scaling. That isn't verified.3. The plate's aspect ratio is why it's pitched
GeneratePlateenumerates(p² + (q·aspect)²) / (1 + aspect²). At aspect 1.41 ≈ √2, that expression is(p² + 2q²)/3, which lands on thirds. With the shippedratio_scatter0.02, the lowest modes come out at:1 : 1.979 : 2.962 : 3.566 : 3.929 : 5.562 : 5.986 : 6.200
That's nearly a harmonic series. Modes 2–8 average 66 cents from integer multiples of the lowest. √3 (1.73) has the same problem in quarters. The p = q diagonal (1, 4, 9, 16…) is exact at every aspect, which the header already notes.
Denser, irregular lows. A second plate group at an unrelated frequency ratio, with its own non-√n aspect, plus a scatter weighted by 1/ratio so the low modes move most:
Below 2 kHz, spectral crest over 0.3–2 s falls from 124 (single group) to 89 (dense-f). Aspect 1.53 alone did not reduce crest (130).
What the second group costs:
bcox: "dense-f sounds great but needs more sibilance."
Remove the noise wash. Removing the unvoiced layer drops the single-group layout (
crash-cq2-*) by 12 dB above 8 kHz, but drops dense-f by only 6–7 dB, because dense-f's mode skirts reach higher. bcox preferred dense-f without it. The wash is also hand-drawn and nearly gone by 1.5 s, and a choke can't reach it.4. Stick contact time is the brightness and velocity lever
This builds on dense-f with no wash, pickup tilt 0.9, and 2× noise cutoff. Mean dB over 0.3–2 s:
bcox: 0.8 ms "sounds like a medium hit", and 0.4 ms "a smash".
A shorter contact adds real high-frequency excitation instead of attenuating the lows. That's also what happens physically on a harder stroke. Velocity → contact time is the honest velocity mapping. Whether the modal layer can modulate contact time today hasn't been checked.
5. The ringout is pitched because the lows ring longest
AddMode's decay law isdecay_seconds × ratio^−damping. Withdamping≥ 0, the lowest modes outlast everything, and the scatter's envelope term fades as the ring decays. So the ringout is the few low lines, clean. Negative damping makes the lows die first. All rows below are the 0.8 ms hit, rendered to 9 s:bcox: dm30 and dm50 are "both pretty good, dm50 may be a little better", and "the dm15 range is also useful."
Over the 2–3.5 s tail, damping 0.22 → 0.0 raises the >5 kHz share by 2.8 dB. Moving the phase noise from its floor onto the envelope term lowers the tail's >3 kHz flatness from 0.75 to 0.66 (both at damping 0.10); bcox had called the tail "a bit white".
Proposed controls
Two patch-level choices underlie all four: a plate layout with no √n aspect plus a second, unrelated group, and pickup-side phase noise.
Open questions and constraints
ModalPatch::damping_tiltisn't the ringout knob as it stands. It tilts by array position,2^(−tilt·m/count), and a two-group table restarts at low ratios halfway through the array. It would give the second group's low modes long decays. The knob has to tilt by frequency ratio. The destination-enum comment also keeps damping tilt off the modulation list as a note-on warp the refresh "cannot afford." Reading it at note-on respects that. A live version would reopen the decision: by count it's one exp lookup and one multiply per refreshed mode, but it isn't timed.plugin/program.h'ssizeof(ModalLayerType) <= sizeof(Layer)guard rejected one weight per mode (512 B). It also rejected two 16-bin tables. The spike runs 8 bins, with 16 modes sharing each noise sequence, and packs its scalars to fit. A real design has to budget this, or put per-mode data such as log ratios in the sharedModeTable.What only the physics can do
Everything above is shaped at note-on or driven by the envelope and velocity. The spike gets a convincing one-shot crash, but not one that responds to what the plate is doing right now:
Those need the amplitude-dependent, spectrum-spreading term in the bridge bus that the modal design leaves parked ("Cymbals: what this claims, and what it approximates" in
docs/superpowers/specs/2026-08-31-modal-synthesis-design.md).