feat(percussion): stochastic per-hit variation + BPM-locked flam timing
User: "every perc sound needs stochastics... claps are not always the
same etc... like the clap and bass should have subtle variation in
timbre across multiple pressings" + "can the repeat be based on
timing / bpm".
playPercussion() now randomizes tone, volume, and (where it matters)
duration on every hit so repeated presses don't sound machine-stamped.
Two inline helpers:
rj(center, frac) — jitter center by ±frac (e.g. rj(180, 0.05) =
180 ± 5% = 171 to 189)
rn(min, max) — uniform random in range
Per-drum variation spread (most subtle → most dramatic):
cowbell tone ±4% vol ±8% (mechanical sound, tight)
ride tone ±6% vol ±10%
crash tone ±6-7% vol ±8-10%
snare tone ±4-8% vol ±8-10%
hat-closed tone ±8% vol ±10%
hat-open tone ±7% vol ±8-10% duration ±10%
snap tone ±10% vol ±10%
clap tone ±8-12% vol ±8-12% + BPM-locked flam + timing ±30%
kick tone ±4-12% vol ±5-10% + sub beat freq ±4Hz
tambourine tone ±8-10% vol ±10-12% (the point of the instrument)
The kick's TWO-SINE sub wobble now also jitters its beat frequency
per hit — f1 is chosen around 44 Hz ±5%, f2 is then f1 + 8-12 Hz, so
the beat frequency lands somewhere between 8 and 12 Hz on each press
instead of being locked at exactly 10 Hz. Feels more organic.
The clap's three body bursts plus the tail now derive their timing
from a `flam` unit computed from metronomeBPM:
flam = (60000 / bpm) * 0.025 → 12.5 ms at 120 bpm,
25 ms at 60 bpm,
8.3 ms at 180 bpm
Each burst fires at `flam * rn(lo, hi)` so a faster tempo makes the
clap tighter and a slower tempo looser. This is the answer to "can
the repeat be based on timing / bpm" — drum rolls and flam bursts now
lock to the current metronome tempo.
The rj/rn helpers are inline closures inside playPercussion, so there's
no per-call allocation overhead beyond one Math.random() per jitter,
and no cross-hit state to manage.