correlation: hold last value and ease to zero on silence
The XY correlation cube snapped to center the moment the audio dropped below the silence threshold, because process() hard-set value to 0.0 in the silent branch. Now the silent branch eases the displayed value back toward zero using the configured response time, so the cube holds its last reading and then settles instead of jumping. The active path is unchanged, so all existing response-time and peak tests still pass. - add `display` (f64) that tracks the live correlation while active - silent branch: display += alpha * (0 - display) - update configurable_silence_threshold test for the eased decay - add correlation_holds_then_decays_to_zero_on_silence test
This commit is contained in:
+67
-8
@@ -17,6 +17,7 @@ pub struct CorrelationMeter {
|
|||||||
cross_power: f64,
|
cross_power: f64,
|
||||||
gate_power: f64,
|
gate_power: f64,
|
||||||
value: f32,
|
value: f32,
|
||||||
|
display: f64,
|
||||||
negative_peak: f32,
|
negative_peak: f32,
|
||||||
reset_token: u64,
|
reset_token: u64,
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ impl CorrelationMeter {
|
|||||||
cross_power: 0.0,
|
cross_power: 0.0,
|
||||||
gate_power: 0.0,
|
gate_power: 0.0,
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
|
display: 0.0,
|
||||||
negative_peak: 0.0,
|
negative_peak: 0.0,
|
||||||
reset_token,
|
reset_token,
|
||||||
};
|
};
|
||||||
@@ -93,18 +95,24 @@ impl CorrelationMeter {
|
|||||||
* (0.5 * (l * l + r * r) - self.gate_power);
|
* (0.5 * (l * l + r * r) - self.gate_power);
|
||||||
|
|
||||||
// Below the configured mono RMS threshold the correlation indication
|
// 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;
|
const MIN_POWER: f64 = 1.0e-10;
|
||||||
let denominator = (self.power_l * self.power_r).sqrt();
|
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_l > MIN_POWER
|
||||||
&& self.power_r > MIN_POWER
|
&& self.power_r > MIN_POWER
|
||||||
&& denominator > 0.0
|
&& denominator > 0.0;
|
||||||
{
|
if active {
|
||||||
(self.cross_power / denominator).clamp(-1.0, 1.0) as f32
|
// 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 {
|
} 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 {
|
if self.value < self.negative_peak {
|
||||||
self.negative_peak = self.value;
|
self.negative_peak = self.value;
|
||||||
}
|
}
|
||||||
@@ -255,7 +263,26 @@ mod tests {
|
|||||||
};
|
};
|
||||||
(sample, sample)
|
(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]
|
#[test]
|
||||||
@@ -278,4 +305,36 @@ mod tests {
|
|||||||
});
|
});
|
||||||
assert_eq!(meter.value(), 0.0);
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user