const WINDOW_CAP: usize = 1 << 17;
pub fn estimate_lag(reference: &[f32], signal: &[f32], sample_rate: u32, max_ms: f32) -> f32 {
let n = reference.len().min(signal.len());
if n < 16 || sample_rate == 0 {
return 0.0;
}
let max_lag = (((max_ms / 1000.0) * sample_rate as f32) as isize).max(1);
let win = n.min(WINDOW_CAP);
let corr = |lag: isize| -> f32 {
let mut s = 0.0f32;
let mut cnt = 0usize;
for (i, &r) in reference[..win].iter().enumerate() {
let j = i as isize + lag;
if j >= 0 && (j as usize) < win {
s += r * signal[j as usize];
cnt += 1;
}
}
if cnt > 0 { s / cnt as f32 } else { f32::MIN }
};
let mut best = 0isize;
let mut best_v = f32::MIN;
for lag in -max_lag..=max_lag {
let v = corr(lag);
if v > best_v {
best_v = v;
best = lag;
}
}
let (a, b, c) = (corr(best - 1), corr(best), corr(best + 1));
let denom = a - 2.0 * b + c;
let delta = if denom.abs() > 1e-12 { 0.5 * (a - c) / denom } else { 0.0 };
best as f32 + delta.clamp(-1.0, 1.0)
}
fn shift_fractional(signal: &[f32], lag: f32) -> Vec<f32> {
let n = signal.len();
let mut out = vec![0.0f32; n];
for (i, o) in out.iter_mut().enumerate() {
let pos = i as f32 + lag;
let j = pos.floor() as isize;
let frac = pos - j as f32;
let a = if j >= 0 && (j as usize) < n { signal[j as usize] } else { 0.0 };
let b = if j + 1 >= 0 && ((j + 1) as usize) < n { signal[(j + 1) as usize] } else { 0.0 };
*o = a * (1.0 - frac) + b * frac;
}
out
}
pub fn align(reference: &[f32], signal: &[f32], sample_rate: u32, max_ms: f32) -> Vec<f32> {
let lag = estimate_lag(reference, signal, sample_rate, max_ms);
shift_fractional(signal, lag)
}
pub fn azimuth_correct(
left: &[f32],
right: &[f32],
sample_rate: u32,
max_ms: f32,
) -> (Vec<f32>, Vec<f32>) {
let corrected = align(left, right, sample_rate, max_ms);
(left.to_vec(), corrected)
}
#[cfg(test)]
mod tests {
use super::*;
fn signal(sr: u32, n: usize) -> Vec<f32> {
(0..n)
.map(|i| {
let t = i as f32 / sr as f32;
(2.0 * std::f32::consts::PI * 137.0 * t).sin()
+ 0.6 * (2.0 * std::f32::consts::PI * 523.0 * t).sin()
})
.collect()
}
#[test]
fn recovers_known_delay() {
let sr = 48_000u32;
let n = 40_000usize;
let reference = signal(sr, n);
let delay = 17usize;
let mut delayed = vec![0.0f32; n];
delayed[delay..].copy_from_slice(&reference[..n - delay]);
let lag = estimate_lag(&reference, &delayed, sr, 5.0);
assert!((lag - delay as f32).abs() < 0.5, "estimated lag {lag}, want {delay}");
let aligned = align(&reference, &delayed, sr, 5.0);
let err: f32 =
(5_000..35_000).map(|i| (aligned[i] - reference[i]).abs()).sum::<f32>() / 30_000.0;
assert!(err < 0.05, "alignment residual {err}");
}
#[test]
fn azimuth_reduces_interchannel_lag() {
let sr = 48_000u32;
let n = 40_000usize;
let left = signal(sr, n);
let mut right = vec![0.0f32; n];
right[9..].copy_from_slice(&left[..n - 9]); let (l, r) = azimuth_correct(&left, &right, sr, 5.0);
let residual = estimate_lag(&l, &r, sr, 5.0);
assert!(residual.abs() < 1.0, "azimuth not corrected, residual {residual}");
}
}