feat(audio): STK waveguide whistle + shift accent modifier
Two significant audio changes and one control refactor:
1. **Whistle rewritten as digital waveguide (Cook/STK model)**
Previous implementation used two biquad resonators (main fundamental
Q=45 + formant Q=14) driven by white noise. User reported "still very
airy" after multiple tuning passes — biquads fundamentally cannot
produce a whistled tone, only filtered noise.
New implementation follows Perry Cook's STK Flute algorithm:
breath ──► (+) ──► jetDelay ──► NL(x*(x*x-1)) ──► dcBlock ──► (+) ──► boreDelay ──┬──► out
▲ ▲ │
│ −jetRefl·temp │ +endRefl·temp │
│ │ │
└───────── 1-pole LPF ◄───────────────────────────────┴──────────────────┘
Key insights from STK Flute.h / Faust flute.dsp research:
- The BORE delay line (length = SR/freq) is the primary resonator.
Its closed-loop feedback generates ALL harmonics automatically via
comb filtering. You don't need 5-10 biquads tuned to harmonics —
the delay line IS a comb filter with infinite teeth.
- The JET delay (0.32 × bore) models the air jet's travel time
across the embouchure. jetRatio controls flute vs pennywhistle vs
ocarina character.
- CRITICAL: the nonlinearity is y = x*(x*x - 1), NOT tanh. The cubic
has a negative-slope region at x=0 which makes it a limit-cycle
generator — it converts steady DC breath pressure into sustained
oscillation. tanh is monotonic and can only saturate; it cannot
self-oscillate. This was the core missing ingredient.
- The loop filter is a SIMPLE 1-pole LPF (coefficients 0.35/0.65),
not a high-Q biquad. The 1-pole provides just enough loop damping
to close the loop under unity gain + rolls off high harmonics for
natural bore damping. A biquad would kill the harmonics we're
trying to keep.
- Breath excitation must include a DC component: noise alone cannot
drive oscillation. `breath = dc * (1 + noise_gain*noise + vibrato)`.
- DC blocker after the cubic removes the bias the NL would otherwise
pump into the loop.
Added ACVoice fields: whistle_bore_buf[2048], whistle_jet_buf[512],
whistle_bore_w, whistle_jet_w, whistle_lp1, whistle_hp_x1/y1.
Removed biquad fields (whistle_main_*, whistle_formant_*).
Buffers cleared on voice init.
Source refs:
- https://ccrma.stanford.edu/software/stk/Flute_8h_source.html
- https://github.com/thestk/stk/blob/master/include/JetTable.h
- https://github.com/grame-cncm/faust/blob/master-dev/examples/physicalModeling/faust-stk/flute.dsp
2. **Shift-as-accent modifier** (notepat.mjs). Previous behavior: tap
shift toggled quickMode (changed attack from 0.005 to 0.002 and
release from 0.08 to 0.02 — subtle, rarely useful). User asked to
deprecate shift's current behavior and use it as an ACCENT modifier
— capitalized/shifted playing = louder, more impactful hits.
New behavior:
- Tap shift on its own: no-op (quickMode toggle removed)
- Hold shift while triggering a note: velocity × 1.4 (capped to 1.0)
- Hold shift while triggering a drum: drumVol × 1.5 (no cap, the
drum bus compressor handles peak control)
This works live — you can rapidly alternate shifted/unshifted hits
for dynamic accents without any mode switching. Natural "capital
letter = louder" feel.
3. **Drum bus peak compressor (audio.c)** — bundled from earlier work
that was cancelled mid-build. 0.95 threshold, 5ms attack, 200ms
release. Preserves individual hit dynamics while preventing the
soft_clip saturation that was making rapid kicks feel "stacking
quieter". Each hit's initial transient passes through at full
amplitude; sustained overlap gets gain-reduced gracefully.
4. **Full drum pad labels when space allows (notepat.mjs)** — bundled.
Uses PERCUSSION_NAMES ("kick", "snare", "splash", etc.) when the
pad is wide enough to fit the full word. Falls back to 3-char
abbreviations (BAS/SNR/SPL) only when tight. User: "across the
board in notepat no need to truncate terms".
Cancelled in-flight dynamic-coot (was at 96s) to bundle these changes
— net savings: one flash cycle, and the whistle rewrite needs testing
as a complete unit.