Recover analyzer DSP and realtime pipeline corrections

This commit is contained in:
Mikei386
2026-07-21 19:57:52 +02:00
parent e499bac928
commit 91aeccb938
27 changed files with 3410 additions and 924 deletions
+151
View File
@@ -0,0 +1,151 @@
//! Continuous stereo correlation meter.
//!
//! The detector integrates L², R² and L·R with the same time constant and
//! derives the normalized correlation only afterwards. Its timing therefore
//! does not depend on ALSA period size or browser frame rate.
#[derive(Clone, Debug)]
pub struct CorrelationMeter {
sample_rate: u32,
response_seconds: f32,
alpha: f64,
power_l: f64,
power_r: f64,
cross_power: f64,
value: f32,
negative_peak: f32,
reset_token: u64,
}
impl CorrelationMeter {
pub fn new(sample_rate: u32, response_seconds: f32, reset_token: u64) -> Self {
let mut meter = Self {
sample_rate: 0,
response_seconds: 0.0,
alpha: 1.0,
power_l: 0.0,
power_r: 0.0,
cross_power: 0.0,
value: 0.0,
negative_peak: 1.0,
reset_token,
};
meter.configure(sample_rate, response_seconds, reset_token);
meter
}
pub fn configure(&mut self, sample_rate: u32, response_seconds: f32, reset_token: u64) {
let sample_rate = sample_rate.max(8_000);
let response_seconds = normalize_response_seconds(response_seconds);
if self.sample_rate != sample_rate || self.response_seconds != response_seconds {
self.sample_rate = sample_rate;
self.response_seconds = response_seconds;
self.alpha = 1.0
- (-1.0 / (f64::from(sample_rate) * f64::from(response_seconds))).exp();
}
if self.reset_token != reset_token {
self.reset_token = reset_token;
self.negative_peak = 1.0;
}
}
pub fn process(&mut self, left: f32, right: f32) {
let l = f64::from(left);
let r = f64::from(right);
self.power_l += self.alpha * (l * l - self.power_l);
self.power_r += self.alpha * (r * r - self.power_r);
self.cross_power += self.alpha * (l * r - self.cross_power);
// Below roughly -100 dBFS RMS per channel the quotient is no longer a
// useful phase measurement and should settle at the neutral position.
const MIN_POWER: f64 = 1.0e-10;
let denominator = (self.power_l * self.power_r).sqrt();
self.value = if self.power_l > MIN_POWER && self.power_r > MIN_POWER && denominator > 0.0 {
(self.cross_power / denominator).clamp(-1.0, 1.0) as f32
} else {
0.0
};
if self.value < self.negative_peak {
self.negative_peak = self.value;
}
}
pub fn value(&self) -> f32 {
self.value
}
pub fn negative_peak(&self) -> f32 {
self.negative_peak
}
}
pub fn normalize_response_seconds(value: f32) -> f32 {
if value.is_finite() && value >= 1.75 {
2.5
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_signal<F>(meter: &mut CorrelationMeter, seconds: usize, mut signal: F)
where
F: FnMut(usize) -> (f32, f32),
{
let count = meter.sample_rate as usize * seconds;
for index in 0..count {
let (left, right) = signal(index);
meter.process(left, right);
}
}
#[test]
fn detects_positive_negative_and_quadrature_signals() {
let sample_rate = 48_000;
let phase_step = 2.0 * std::f32::consts::PI * 1_000.0 / sample_rate as f32;
for (phase, expected) in [(0.0, 1.0), (std::f32::consts::PI, -1.0), (std::f32::consts::FRAC_PI_2, 0.0)] {
let mut meter = CorrelationMeter::new(sample_rate, 1.0, 0);
run_signal(&mut meter, 5, |index| {
let angle = phase_step * index as f32;
(angle.sin(), (angle + phase).sin())
});
assert!((meter.value() - expected).abs() < 0.002, "phase {phase}: {}", meter.value());
}
}
#[test]
fn response_time_and_peak_reset_are_deterministic() {
let sample_rate = 48_000;
let mut fast = CorrelationMeter::new(sample_rate, 1.0, 0);
let mut slow = CorrelationMeter::new(sample_rate, 2.5, 0);
for index in 0..sample_rate as usize {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
fast.process(sample, sample);
slow.process(sample, sample);
}
assert!(fast.value() > 0.999);
assert!(slow.value() > 0.999);
run_signal(&mut fast, 2, |index| {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
(sample, -sample)
});
assert!(fast.negative_peak() < -0.7);
fast.configure(sample_rate, 1.0, 1);
assert_eq!(fast.negative_peak(), 1.0);
}
#[test]
fn silence_and_single_channel_are_neutral() {
let mut meter = CorrelationMeter::new(48_000, 1.0, 0);
run_signal(&mut meter, 2, |_| (0.0, 0.0));
assert_eq!(meter.value(), 0.0);
run_signal(&mut meter, 2, |index| {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
(sample, 0.0)
});
assert_eq!(meter.value(), 0.0);
}
}