feat(perc): 808-grounded drum kit + drum bus split + dynamic hi-hat pedal
Three related changes bundled into one build cycle:
1. **Drum synthesis rewrite (notepat.mjs)** — all 12 drums rewritten
with research-grounded TR-808 recipes instead of the previous
"layer noise + square + stochastic jitter" approach.
- kick: beater click + pitch snap 130Hz + sustained 50Hz sub
- snare: 808 tonal pair (238/476 Hz exact) + wire rattle noise
- clap: 808 4-burst pattern via staggered attack envelopes
(5/15/25/45 ms) — uses the audio engine's attack
envelope for sub-frame timing instead of setTimeout
- snap: finger-snap physics — broadband click + palm cavity
Helmholtz resonance at 2100 Hz
- hats: 808 6-square inharmonic cluster at exact Roland
frequencies (800/540/522.7/369.6 Hz)
- ride: hat cluster + bell ping (440/587 Hz perfect-5th)
- crash: hat cluster + explosive noise attack + long wash
- splash: short version of crash
- cowbell: 808 triangles 800/540 Hz (3:2 detuned)
- block: single triangle 2500 Hz (clave)
- tambo: 3 staggered noise bursts + 6kHz ting
Key principle: iconic 808 frequencies are NOT tone-jittered.
Per-hit variation moves from tonal (±8-12% → removed) to
timbre (volume balance ±15%, duration ±20%, pan offset jitter).
2. **Dynamic hold model for ringing drums (notepat.mjs)** — open
hat, ride, and crash now use sustain voices that ring while
the key is held and fade on release. Other drums stay as
one-shot transients (kick/clap/snare/snap/hat-c/splash/cowbell/
block/tambo) because hold doesn't make musical sense for them.
New `addSustain(params, bothDuration, releaseFade, releaseUpdate)`
helper creates infinite-duration voices during live play
(pushed to holdVoices array) or finite-duration voices during
reverse playback "both" mode.
`releasePercussionHold()` iterates held voices, applies optional
`releaseUpdate` (e.g. `{tone: 3500}` to dampen open hat brightness
before fade — the "foot pedal closing" effect), then calls
sound.kill() with the per-voice releaseFade. Open hat uses 120ms
fade with brightness dampening, ride 250ms, crash 450ms.
3. **Drum bus split in audio.c mixer** — percussive voices (short
finite duration < 0.5s) bypass the auto-mix divide entirely.
Previously all voices summed into one bus and were normalized
by `max(1.0, voice_sum)`, so 3 drums firing simultaneously
ducked each other to ~1/5 amplitude. Now:
- tone_l/tone_r: accumulate held/sustained voices, divide by
voice_sum (unchanged auto-mix behavior for chords)
- drum_l/drum_r: accumulate transient voices, NO divide
- mix_l = tone_l + drum_l (final soft_clip tanh catches peak)
Result: kick+snare+hat transients stack additively for a
louder peak instead of getting ducked. soft_clip gives a
natural analog saturation character on heavy drum peaks.
4. **Install-debug inline dump (ac-native.c)** — the
`copied /tmp/install-debug.log → /mnt/install-debug.log`
step was printing success but the file wasn't actually landing
on the USB (probably a stale /mnt from the repartition flow).
Now the auto_install_to_hd dumps the full tmpfs log inline via
ac_log at the end, so the complete install trace ALWAYS lands
in ac-native.log (which we know reliably survives). Also added
errno reporting on the /mnt copy so we'll see why it's failing.
5. **mkfs.vfat stderr capture (ac-native.c)** — the last install
attempt failed with `mkfs rc=256 attempt=1/2` and we had no
idea why. Now each mkfs attempt redirects stderr to a dedicated
`/tmp/mkfs-err.log`, then we read it back and ac_log each line
with `[mkfs/1]` or `[mkfs/2]` prefix. Plus WIFEXITED/WEXITSTATUS
decoding of the raw rc so we can distinguish "command not found"
from "mkfs ran and returned 1".
Ready for another w-to-install attempt — this time we'll see the
actual reason mkfs fails in ac-native.log even if /mnt is stale.