From 90717cda37f7991f33539c5e5b7dfd5846621248 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Fri, 5 Jun 2026 21:31:23 -0400 Subject: [PATCH] test(audio): dense battery for the mix-bus soft limiter Ten more cases pinning the SoftLimiter contract (Gemini, senior-audited): sustained-loud ceiling both polarities, out_gain participation (boost + atten), instant-attack no-overshoot, release direction/monotonicity + gradualness, cross-call state continuity (split == continuous), empty input, extreme i32::MIN/MAX magnitudes, and bit-exact transparency just under the ceiling. Co-Authored-By: Claude Opus 4.8 --- src/audio/limiter.rs | 195 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/src/audio/limiter.rs b/src/audio/limiter.rs index 3039f57..0047008 100644 --- a/src/audio/limiter.rs +++ b/src/audio/limiter.rs @@ -160,7 +160,200 @@ mod tests { #[test] fn silence_is_silence() { let mut lim = SoftLimiter::new(SR); - let out = lim.process(&vec![0i32; 32], 1.0); + let out = lim.process(&[0i32; 32], 1.0); assert!(out.iter().all(|&s| s == 0)); } + + /// 1. Ceiling is honoured for a sustained loud sum. + #[test] + fn ceiling_honored_for_sustained_loud_sum() { + let mut lim = SoftLimiter::new(SR); + let ceiling_ceil = lim.ceiling().ceil() as i16; + + // Sustained positive loud sum + let pos_loud = vec![150_000i32; 1000]; + let out_pos = lim.process(&pos_loud, 1.0); + for &s in &out_pos { + assert!(s > 0, "positive input stays positive, got {s}"); + assert!(s <= ceiling_ceil, "positive sample {s} exceeded ceiling {ceiling_ceil}"); + } + + // Sustained negative loud sum + let mut lim2 = SoftLimiter::new(SR); + let neg_loud = vec![-150_000i32; 1000]; + let out_neg = lim2.process(&neg_loud, 1.0); + let neg_ceiling = -ceiling_ceil; + for &s in &out_neg { + assert!(s < 0, "negative input stays negative, got {s}"); + assert!(s >= neg_ceiling, "negative sample {s} exceeded negative ceiling {neg_ceiling}"); + } + } + + /// 2. `out_gain` participates in limiting. + #[test] + fn out_gain_participates_in_limiting() { + let mut lim = SoftLimiter::new(SR); + let ceiling_ceil = lim.ceiling().ceil() as i16; + + // 10,000 fits in i16, but with out_gain = 8.0 it is 80,000, which is past the ceiling. + let input = vec![10_000i32; 100]; + let out = lim.process(&input, 8.0); + for &s in &out { + assert!(s > 0, "positive stays positive"); + assert!(s <= ceiling_ceil, "sample {s} must be limited to ceiling {ceiling_ceil}"); + assert!((s - ceiling_ceil).abs() <= 2, "sample {s} should ride the ceiling {ceiling_ceil}"); + } + } + + /// 3. `out_gain` below unity attenuates transparently. + #[test] + fn out_gain_below_unity_attenuates_transparently() { + let mut lim = SoftLimiter::new(SR); + let input = vec![10_000i32; 10]; + let out = lim.process(&input, 0.5); + for (i, &s) in out.iter().enumerate() { + let expected = (input[i] as f32 * 0.5).round() as i16; + assert!((s - expected).abs() <= 1, "sample {s} should be close to expected {expected}"); + } + + // Subsequently feed a new sample at unity gain. It must be transparent, + // proving the internal gain state stayed at 1.0. + let out_unity = lim.process(&[5_000i32], 1.0); + assert_eq!(out_unity[0], 5000i16, "gain should remain at 1.0"); + } + + /// 4. Instant attack: the very first loud sample does not overshoot. + #[test] + fn instant_attack_first_loud_sample_does_not_overshoot() { + let mut lim = SoftLimiter::new(SR); + let ceiling_ceil = lim.ceiling().ceil() as i16; + + let loud = vec![200_000i32; 10]; + let out = lim.process(&loud, 1.0); + assert!(out[0] <= ceiling_ceil, "first sample {} must not overshoot ceiling {}", out[0], ceiling_ceil); + } + + /// 5. Release direction & monotonicity. + #[test] + fn release_direction_and_monotonicity() { + let mut lim = SoftLimiter::new(SR); + // Hammer with a loud burst to pull gain down + lim.process(&[200_000; 100], 1.0); + + // Long sub-ceiling buffer of a constant positive mid-level signal + let mid_val = 5000i32; + let sub_ceiling = vec![mid_val; 1000]; + let out = lim.process(&sub_ceiling, 1.0); + + // Output should be monotonic (non-decreasing) + for i in 1..out.len() { + assert!(out[i] >= out[i - 1], "output must be monotonic; index {} was {}, index {} was {}", i - 1, out[i - 1], i, out[i]); + } + + // The end sample should be closer to the original input than the start sample + let start_diff = (mid_val as i16 - out[0]).abs(); + let end_diff = (mid_val as i16 - *out.last().unwrap()).abs(); + assert!(end_diff < start_diff, "end diff {end_diff} should be smaller than start diff {start_diff}"); + } + + /// 6. Release is gradual, not instantaneous. + #[test] + fn release_is_gradual_not_instantaneous() { + let mut lim = SoftLimiter::new(SR); + // Loud burst + lim.process(&[200_000; 100], 1.0); + + // Immediately follow with a sub-ceiling sample + let out = lim.process(&[10_000i32], 1.0); + assert!(out[0] < 10_000, "first quiet sample should still be attenuated (got {})", out[0]); + } + + /// 7. State carries across process calls. + #[test] + fn state_carries_across_process_calls() { + // Test 1: Splitting calls is identical to one single continuous call + let mut lim_single = SoftLimiter::new(SR); + let mut lim_split = SoftLimiter::new(SR); + + let part1 = vec![100_000i32; 100]; + let part2 = vec![150_000i32; 100]; + let mut continuous = part1.clone(); + continuous.extend(&part2); + + let out_single = lim_single.process(&continuous, 1.0); + + let out_split1 = lim_split.process(&part1, 1.0); + let out_split2 = lim_split.process(&part2, 1.0); + let mut out_split = out_split1; + out_split.extend(&out_split2); + + assert_eq!(out_single, out_split, "splitting process calls must produce identical output to a single call"); + + // Test 2: Pre-loaded limiter vs fresh limiter on the same input + let mut lim_preloaded = SoftLimiter::new(SR); + lim_preloaded.process(&[100_000; 100], 1.0); + + let mut lim_fresh = SoftLimiter::new(SR); + + let test_input = vec![10_000i32; 10]; + let out_preloaded = lim_preloaded.process(&test_input, 1.0); + let out_fresh = lim_fresh.process(&test_input, 1.0); + + assert_ne!(out_preloaded, out_fresh, "pre-loaded and fresh limiter outputs should differ"); + assert!(out_preloaded[0] < out_fresh[0], "pre-loaded limiter first sample {} should be smaller than fresh limiter first sample {}", out_preloaded[0], out_fresh[0]); + } + + /// 8. Empty input. + #[test] + fn empty_input_returns_empty_and_does_not_panic() { + let mut lim = SoftLimiter::new(SR); + let out = lim.process(&[], 1.0); + assert!(out.is_empty(), "empty input should return empty vector"); + } + + /// 9. Extreme magnitudes don't panic / produce non-finite casts. + #[test] + fn extreme_magnitudes_do_not_panic_or_non_finite_cast() { + let mut lim = SoftLimiter::new(SR); + let input = vec![i32::MAX, i32::MIN, i32::MAX, i32::MIN]; + + // Gain 0.0 + let out_zero = lim.process(&input, 0.0); + assert_eq!(out_zero.len(), input.len()); + assert!(out_zero.iter().all(|&s| s == 0), "0.0 gain should result in all zeros"); + + // Gain 1.0 + let out_unity = lim.process(&input, 1.0); + assert_eq!(out_unity.len(), input.len()); + + // Gain 10.0 + let out_large = lim.process(&input, 10.0); + assert_eq!(out_large.len(), input.len()); + + // Gain 0.5 + let out_small = lim.process(&input, 0.5); + assert_eq!(out_small.len(), input.len()); + } + + /// 10. A single below-ceiling buffer is bit-exact at unity gain. + #[test] + fn below_ceiling_is_bit_exact_at_unity_gain() { + let mut lim = SoftLimiter::new(SR); + let ceiling_limit = lim.ceiling() as i32; // 31783 + let input = vec![ + 0, + 1, + -1, + 100, + -100, + ceiling_limit, + -ceiling_limit, + ceiling_limit - 1, + -(ceiling_limit - 1), + ]; + + let out = lim.process(&input, 1.0); + let expected: Vec = input.iter().map(|&s| s as i16).collect(); + assert_eq!(out, expected, "below ceiling input must be bit-exact at unity gain"); + } }