Merge gemini/limiter-tests: dense soft-limiter test battery
This commit is contained in:
+194
-1
@@ -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<i16> = input.iter().map(|&s| s as i16).collect();
|
||||
assert_eq!(out, expected, "below ceiling input must be bit-exact at unity gain");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user