#[cfg(test)]
mod tests {
use crate::dsp::gate::*;
use proptest::prelude::*;
#[test]
fn test_gate_params_default() {
let params = GateParams::default();
assert_eq!(params.threshold_open_db, -70.0);
assert_eq!(params.threshold_close_db, -80.0);
assert_eq!(params.hold_frames, 2048);
assert_eq!(params.fade_frames, 256);
}
#[test]
fn test_hysteresis_basic_transitions() {
let mut dh = DynamicHysteresis::new();
let params = GateParams::new(-10.0, -20.0, 10, 10, 1e-4);
let th_open = 1.0;
let th_close = 0.5;
assert_eq!(dh.state(), GateState::Open);
assert_eq!(dh.multiplier(), 1.0);
dh.update(0.1, th_open, th_close, ¶ms, 5);
assert_eq!(
dh.state(),
GateState::Open,
"Should remain Open during hold"
);
dh.update(0.1, th_open, th_close, ¶ms, 5);
assert_eq!(
dh.state(),
GateState::FadingOut,
"Should enter FadingOut after hold_frames"
);
dh.update(0.1, th_open, th_close, ¶ms, 5);
assert_eq!(dh.state(), GateState::FadingOut);
assert_eq!(dh.multiplier(), 0.5);
dh.update(0.1, th_open, th_close, ¶ms, 5);
assert_eq!(dh.state(), GateState::Closed);
assert_eq!(dh.multiplier(), 0.0);
dh.update(2.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::FadingIn);
assert_eq!(
dh.multiplier(),
0.1,
"Starts opening immediately when sound returns"
);
dh.update(2.0, th_open, th_close, ¶ms, 4);
assert_eq!(dh.state(), GateState::FadingIn);
assert_eq!(dh.multiplier(), 0.5);
dh.update(2.0, th_open, th_close, ¶ms, 5);
assert_eq!(dh.state(), GateState::Open);
assert_eq!(dh.multiplier(), 1.0);
}
#[test]
fn test_hysteresis_interrupted_fade() {
let mut dh = DynamicHysteresis::new();
let params = GateParams::new(-70.0, -80.0, 10, 10, 1e-4);
let th_open = 1.0;
let th_close = 0.5;
dh.update(0.1, th_open, th_close, ¶ms, 11);
assert_eq!(dh.state(), GateState::FadingOut);
dh.update(0.1, th_open, th_close, ¶ms, 5);
assert_eq!(dh.multiplier(), 0.5);
dh.update(2.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::FadingIn);
assert_eq!(dh.multiplier(), 0.6);
dh.update(2.0, th_open, th_close, ¶ms, 2);
assert_eq!(dh.multiplier(), 0.8);
dh.update(0.1, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::FadingOut);
assert_eq!(dh.multiplier(), 0.7);
}
#[test]
fn test_hysteresis_apply_gain_ramp() {
let params = GateParams::new(-70.0, -80.0, 2048, 100, 1e-4);
let mut buffer = [1.0f32; 10];
let mut dh = DynamicHysteresis::new();
dh.update(0.0, 1.0, 0.5, ¶ms, 2047);
assert_eq!(dh.state(), GateState::Open);
dh.update(0.0, 1.0, 0.5, ¶ms, 10);
assert_eq!(dh.state(), GateState::FadingOut);
assert_eq!(
dh.multiplier(),
1.0,
"In the transition block the multiplier is still 1.0"
);
dh.update(0.0, 1.0, 0.5, ¶ms, 10);
assert_eq!(dh.multiplier(), 0.9);
buffer.fill(1.0);
dh.apply_gain_rt(&mut buffer, 10);
assert!((buffer[0] - 1.0).abs() < 1e-3);
assert!((buffer[9] - 0.91).abs() < 1e-3);
let mut dh = DynamicHysteresis::new();
dh.update(0.0, 1.0, 0.5, ¶ms, 2048); dh.update(0.0, 1.0, 0.5, ¶ms, 101); assert_eq!(dh.state(), GateState::Closed);
dh.update(2.0, 1.0, 0.5, ¶ms, 1);
assert_eq!(dh.multiplier(), 0.01);
dh.update(2.0, 1.0, 0.5, ¶ms, 10);
assert_eq!(dh.multiplier(), 0.11);
buffer.fill(1.0);
dh.apply_gain_rt(&mut buffer, 10);
assert!((buffer[0] - 0.01).abs() < 1e-3);
assert!((buffer[9] - 0.10).abs() < 1e-3);
}
#[test]
fn test_sub_block_granularity() {
let mut dh = DynamicHysteresis::new();
let params = GateParams::new(-70.0, -80.0, 2048, 256, 1e-4);
let th_open = 1.0;
let th_close = 0.5;
dh.update(0.0, th_open, th_close, ¶ms, 2048);
assert_eq!(dh.state(), GateState::FadingOut);
dh.update(0.0, th_open, th_close, ¶ms, 4096);
assert_eq!(dh.state(), GateState::Closed);
assert_eq!(dh.multiplier(), 0.0);
let mut buffer = vec![1.0f32; 4096];
dh.apply_gain_rt(&mut buffer, 4096);
assert!(
(buffer[0] - 1.0).abs() < 1e-3,
"Start of fade-out should be maximum volume"
);
assert!(
(buffer[128] - 0.5).abs() < 1e-2,
"Middle of fade-out should be half volume"
);
assert_eq!(buffer[256], 0.0, "End of ramp did not strictly silence");
assert_eq!(
buffer[4095], 0.0,
"Remainder of buffer not filled with zeros"
);
dh.update(2.0, th_open, th_close, ¶ms, 4096);
assert_eq!(dh.state(), GateState::Open);
assert_eq!(dh.multiplier(), 1.0);
let mut buffer2 = vec![1.0f32; 4096];
dh.apply_gain_rt(&mut buffer2, 4096);
assert_eq!(buffer2[0], 0.0, "Start of fade-in should be silence");
assert!(
(buffer2[128] - 0.5).abs() < 1e-2,
"Middle of fade-in should be half volume"
);
assert_eq!(
buffer2[256], 1.0,
"End of ramp did not fully open the volume"
);
assert_eq!(
buffer2[4095], 1.0,
"Remainder of buffer not preserved at maximum volume"
);
}
#[test]
fn test_unit_block_processing() {
let mut dh = DynamicHysteresis::new();
let params = GateParams::new(-70.0, -80.0, 2, 2, 1e-4);
let th_open = 1.0;
let th_close = 0.5;
assert_eq!(dh.state(), GateState::Open);
dh.update(0.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::Open, "Hold 1/2");
dh.update(0.0, th_open, th_close, ¶ms, 1);
assert_eq!(
dh.state(),
GateState::FadingOut,
"Entered fade-out after hold=2"
);
dh.update(0.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::FadingOut, "FadeOut 1/2");
assert_eq!(dh.multiplier(), 0.5);
dh.update(0.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::Closed, "Full silence achieved");
assert_eq!(dh.multiplier(), 0.0);
dh.update(2.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::FadingIn, "Start of FadeIn");
assert_eq!(dh.multiplier(), 0.5);
dh.update(2.0, th_open, th_close, ¶ms, 1);
assert_eq!(dh.state(), GateState::Open, "Fully open again");
assert_eq!(dh.multiplier(), 1.0);
let mut buffer = [1.0f32; 1];
dh.update(0.0, th_open, th_close, ¶ms, 2); dh.update(0.0, th_open, th_close, ¶ms, 1); dh.apply_gain_rt(&mut buffer, 1);
assert!(
(buffer[0] - 1.0).abs() < 1e-6,
"The ramp start multiplier is 1.0"
);
dh.update(0.0, th_open, th_close, ¶ms, 1); dh.apply_gain_rt(&mut buffer, 1);
assert!(
(buffer[0] - 0.5).abs() < 1e-6,
"The ramp start multiplier for this block was 0.5"
);
}
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: Some(Box::new(
proptest::test_runner::FileFailurePersistence::SourceParallel(
"tests/proptest-regressions"
)
)),
.. ProptestConfig::with_cases(10_000)
})]
#[test]
#[ignore = "proptest 10k casos; roda no tests-long (gate_envelope_continuity_proptest)"]
fn gate_envelope_continuity_on_reversal(
fade_frames in 4usize..256,
n_samples in 1usize..256,
) {
let params = GateParams::new(-70.0, -80.0, 1, fade_frames, 1e-4);
let th_open = 1.0;
let th_close = 0.5;
let inv = params.inv_fade_frames;
let max_step = n_samples as f32 * inv;
{
let mut dh = DynamicHysteresis::new();
dh.update(0.0, th_open, th_close, ¶ms, params.hold_frames.max(1));
assert_eq!(dh.state(), GateState::FadingOut, "must enter FadingOut");
let target_fc = fade_frames / 2;
let elapsed = fade_frames.saturating_sub(target_fc);
dh.update(0.0, th_open, th_close, ¶ms, elapsed);
if dh.state() == GateState::Closed {
return Ok(());
}
let prev_mult = dh.multiplier();
dh.update(2.0, th_open, th_close, ¶ms, n_samples);
let new_mult = dh.multiplier();
assert!(
(new_mult - prev_mult).abs() <= max_step + 1e-6,
"FadingOut→FadingIn discontinuity: prev={prev_mult}, new={new_mult}, max_step={max_step}"
);
}
{
let mut dh = DynamicHysteresis::new();
dh.update(0.0, th_open, th_close, ¶ms, params.hold_frames.max(1));
if dh.state() == GateState::FadingOut {
dh.update(0.0, th_open, th_close, ¶ms, params.fade_frames);
}
dh.update(2.0, th_open, th_close, ¶ms, 1);
if dh.state() != GateState::FadingIn {
return Ok(());
}
let target_fc = fade_frames * 3 / 4;
let remaining = target_fc.saturating_sub(1);
if remaining > 0 {
dh.update(2.0, th_open, th_close, ¶ms, remaining);
}
if dh.state() != GateState::FadingIn {
return Ok(());
}
let prev_mult = dh.multiplier();
dh.update(0.0, th_open, th_close, ¶ms, n_samples);
let new_mult = dh.multiplier();
assert!(
(new_mult - prev_mult).abs() <= max_step + 1e-6,
"FadingIn→FadingOut discontinuity: prev={prev_mult}, new={new_mult}, max_step={max_step}"
);
}
}
}
}