diff --git a/src/correlation.rs b/src/correlation.rs index 144d94c..ed5398f 100644 --- a/src/correlation.rs +++ b/src/correlation.rs @@ -17,6 +17,7 @@ pub struct CorrelationMeter { cross_power: f64, gate_power: f64, value: f32, + display: f64, negative_peak: f32, reset_token: u64, } @@ -40,6 +41,7 @@ impl CorrelationMeter { cross_power: 0.0, gate_power: 0.0, value: 0.0, + display: 0.0, negative_peak: 0.0, reset_token, }; @@ -93,18 +95,24 @@ impl CorrelationMeter { * (0.5 * (l * l + r * r) - self.gate_power); // Below the configured mono RMS threshold the correlation indication - // settles at its neutral position. + // eases back to its neutral position instead of snapping there. const MIN_POWER: f64 = 1.0e-10; let denominator = (self.power_l * self.power_r).sqrt(); - self.value = if self.gate_power >= self.silence_threshold_power + let active = self.gate_power >= self.silence_threshold_power && self.power_l > MIN_POWER && self.power_r > MIN_POWER - && denominator > 0.0 - { - (self.cross_power / denominator).clamp(-1.0, 1.0) as f32 + && denominator > 0.0; + if active { + // Audio present: track the live correlation exactly as before. + self.display = (self.cross_power / denominator).clamp(-1.0, 1.0); + self.value = self.display as f32; } else { - 0.0 - }; + // Silent: hold the last value, then decay it back to zero using + // the configured response time, so the XY cube settles instead of + // jumping to center. + self.display += self.alpha * (0.0 - self.display); + self.value = self.display as f32; + } if self.value < self.negative_peak { self.negative_peak = self.value; } @@ -255,7 +263,26 @@ mod tests { }; (sample, sample) }); - assert_eq!(meter.value(), 0.0); + // Raising the threshold above the signal level neutralizes the meter. + // With the eased display it decays toward zero rather than snapping. + assert!( + meter.value() < 0.2, + "meter did not neutralize: {}", + meter.value() + ); + run_signal(&mut meter, 3, |index| { + let sample = if index & 1 == 0 { + amplitude + } else { + -amplitude + }; + (sample, sample) + }); + assert!( + meter.value().abs() < 0.05, + "meter did not settle to zero: {}", + meter.value() + ); } #[test] @@ -278,4 +305,36 @@ mod tests { }); assert_eq!(meter.value(), 0.0); } + + #[test] + fn correlation_holds_then_decays_to_zero_on_silence() { + let sample_rate = 48_000; + let mut meter = CorrelationMeter::new(sample_rate, 1.0, -75.0, 0); + // Drive a strongly correlated signal so the meter settles near +1. + for index in 0..sample_rate as usize * 3 { + let sample = if index & 1 == 0 { 0.5 } else { -0.5 }; + meter.process(sample, sample); + } + assert!(meter.value() > 0.9, "expected near +1, got {}", meter.value()); + + // Cut the audio: the value must hold (not snap to 0) right away... + for _ in 0..10 { + meter.process(0.0, 0.0); + } + assert!( + meter.value() > 0.5, + "value snapped to zero on silence: {}", + meter.value() + ); + + // ...and then decay back toward zero over the response time. + for _ in 0..sample_rate as usize * 4 { + meter.process(0.0, 0.0); + } + assert!( + meter.value().abs() < 0.05, + "value did not decay to zero: {}", + meter.value() + ); + } }