From fa951e570ff6fea1b2c783c561aeef3d804a44ce Mon Sep 17 00:00:00 2001 From: Mollusk Date: Tue, 2 Jun 2026 16:00:15 -0400 Subject: [PATCH] test(audio): edge-case unit tests for the noise gate 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 --- src/audio/gate.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/audio/gate.rs b/src/audio/gate.rs index 6bc85a1..2b3073a 100644 --- a/src/audio/gate.rs +++ b/src/audio/gate.rs @@ -227,4 +227,112 @@ mod tests { let mut f = frame(0); assert!(!g.process(&mut f, 0.05), "an unopened gate on silence must not transmit"); } + + #[test] + fn frame_rms_known_values() { + assert_eq!(frame_rms(&[]), 0.0); + assert_eq!(frame_rms(&frame(0)), 0.0); + + let f_high = frame(16384); + let rms_high = frame_rms(&f_high); + assert!((rms_high - 0.5).abs() < 1e-4, "rms_high was {}", rms_high); + + let f_low = frame(3277); + let rms_low = frame_rms(&f_low); + assert!((rms_low - 0.1).abs() < 1e-3, "rms_low was {}", rms_low); + } + + #[test] + fn empty_frame_transmit_follows_gate_state() { + let mut g = NoiseGate::new(SR); + // fresh gate (never opened) + assert!(!g.process(&mut [], 0.05)); + + // open it with loud signals + for _ in 0..3 { + let mut f = frame(10000); + assert!(g.process(&mut f, 0.05)); + } + + // now empty frame should transmit + assert!(g.process(&mut [], 0.05)); + } + + #[test] + fn disabled_parks_envelope_so_reenable_has_no_fade_in() { + let mut g = NoiseGate::new(SR); + let mut f1 = frame(5000); + assert!(g.process(&mut f1, 0.0)); // disabled + + let mut f2 = frame(10000); + assert!(g.process(&mut f2, 0.05)); // enabled + assert!(f2[0].abs() > 9000, "expected first sample of enabled frame to have no fade-in, got {}", f2[0]); + } + + #[test] + fn hold_keeps_open_through_window_then_releases_to_closed() { + let mut g = NoiseGate::new(SR); + // open it + for _ in 0..3 { + let mut f = frame(10000); + g.process(&mut f, 0.05); + } + + let mut results = Vec::new(); + for _ in 0..25 { + let mut f = frame(0); + results.push(g.process(&mut f, 0.05)); + } + + assert!(results[4], "should still transmit at the 5th silent frame"); + assert!(!results[19], "should not transmit at the 20th silent frame"); + } + + #[test] + fn sustained_mid_level_refreshes_hold_and_stays_open() { + let mut g = NoiseGate::new(SR); + // open it loud + for _ in 0..3 { + let mut f = frame(10000); + g.process(&mut f, 0.05); + } + + // feed 30x frame(1310) (rms ~0.04, between close 0.03 and open 0.05) + for _ in 0..30 { + let mut f = frame(1310); + assert!(g.process(&mut f, 0.05)); + } + assert!(g.open, "gate must stay open (hold refreshed by mid-level input)"); + } + + #[test] + fn loud_signal_reopens_a_releasing_gate() { + let mut g = NoiseGate::new(SR); + // open it + for _ in 0..3 { + let mut f = frame(10000); + g.process(&mut f, 0.05); + } + + // feed silent frames to fully close + let mut closed = false; + for _ in 0..40 { + let mut f = frame(0); + if !g.process(&mut f, 0.05) { + closed = true; + break; + } + } + assert!(closed); + + // feed frame(10000) @ 0.05 a few times + let mut last_peak = 0; + for _ in 0..3 { + let mut f = frame(10000); + assert!(g.process(&mut f, 0.05)); + last_peak = peak(&f); + } + assert!(g.open); + assert!(last_peak >= 9900, "peak of the 3rd reopened frame must be >= 9900, got {}", last_peak); + } }