Covers the previously-untested branches of the NoiseGate envelope/timing:
frame_rms known values, empty-frame transmit-follows-state, disabled gate
parks the envelope open (no fade-in on re-enable), hold-window-then-release
ordering, sustained mid-level refreshes the hold, and a loud signal
re-opening a releasing gate. Gate tests 6 -> 12; test-only, no prod change.
Implemented by Gemini per next-task.md; reviewed against the real diff and
re-verified (build + clippy --all-targets + test all green) by the senior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-frame hard-cut noise gate with a stateful envelope gate
(src/audio/gate.rs):
- Hysteresis: opens at the slider threshold, closes only below 0.6x that,
so speech near the threshold doesn't flap the gate.
- Attack/release: per-sample gain ramp (5ms open, 80ms close) instead of a
click — the gate fades rather than dropping frames outright.
- Hangover: holds the gate open 200ms after the level drops, so word tails
and brief pauses aren't chopped.
A fully-closed frame still reports don't-transmit, preserving the original
bandwidth win of not sending pure silence (receiver jitter buffer conceals
the gap). The live slider value is read per frame so changes apply
immediately. Settings slider gains a one-line hint. 6 unit tests cover
attack shape, hysteresis, hangover-then-release, disabled pass-through, and
the closed-gate no-transmit path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>