#![allow(dead_code)]
use ndarray::Array1;
use num_complex::Complex64;
use rustfft::FftPlanner;
use std::f64::consts::PI;
pub fn reconstruct_minimum_phase(freq: &Array1<f64>, spl: &Array1<f64>) -> Array1<f64> {
let n = spl.len();
if n == 0 {
return Array1::from_vec(Vec::new());
}
if n == 1 || freq.len() != n {
return Array1::from_vec(vec![0.0; n]);
}
let ln10 = 10.0_f64.ln();
let ln_mag_input: Vec<f64> = spl.iter().map(|&s| s * ln10 / 20.0).collect();
let f_max = freq[n - 1];
if !(f_max.is_finite() && f_max > 0.0) {
return Array1::from_vec(vec![0.0; n]);
}
let n_linear = (512 * n).next_power_of_two().clamp(131072, 524288);
let f_grid_max = 16.0 * f_max;
let df = f_grid_max / (n_linear as f64 - 1.0);
let freq_slice = freq.as_slice().expect("freq must be contiguous");
let high_slope = final_octave_log_slope(freq_slice, &ln_mag_input);
let linear_ln_mag: Vec<f64> = (0..n_linear)
.map(|k| {
let f = k as f64 * df;
if f <= freq[0] {
ln_mag_input[0]
} else if f <= f_max {
interp_linear_flat_edges(freq_slice, &ln_mag_input, f)
} else {
ln_mag_input[n - 1] + high_slope * (f / f_max).ln()
}
})
.collect();
let mut extended = Vec::with_capacity(2 * n_linear);
extended.extend_from_slice(&linear_ln_mag);
for k in (0..n_linear).rev() {
extended.push(linear_ln_mag[k]);
}
let linear_hilbert_full = hilbert_transform(&extended);
let linear_hilbert = &linear_hilbert_full[..n_linear];
let linear_freqs: Vec<f64> = (0..n_linear).map(|k| k as f64 * df).collect();
let phase_rad: Vec<f64> = freq
.iter()
.map(|&f| interp_linear_flat_edges(&linear_freqs, linear_hilbert, f))
.collect();
Array1::from_vec(phase_rad.iter().map(|&p| -p.to_degrees()).collect())
}
fn final_octave_log_slope(freq: &[f64], ln_mag: &[f64]) -> f64 {
let n = freq.len();
if n < 4 {
return 0.0;
}
let f_max = freq[n - 1];
let f_start = f_max * 0.5;
let mut xs: Vec<f64> = Vec::with_capacity(n);
let mut ys: Vec<f64> = Vec::with_capacity(n);
for (f, m) in freq.iter().zip(ln_mag.iter()) {
if *f >= f_start && *f <= f_max && f.is_finite() && *f > 0.0 {
xs.push(f.ln());
ys.push(*m);
}
}
if xs.len() < 4 {
return 0.0;
}
let k = xs.len() as f64;
let mean_x = xs.iter().sum::<f64>() / k;
let mean_y = ys.iter().sum::<f64>() / k;
let mut num = 0.0;
let mut den = 0.0;
for (x, y) in xs.iter().zip(ys.iter()) {
let dx = x - mean_x;
num += dx * (y - mean_y);
den += dx * dx;
}
if den.abs() < 1e-12 {
0.0
} else {
num / den
}
}
fn interp_linear_flat_edges(xs: &[f64], ys: &[f64], x: f64) -> f64 {
debug_assert_eq!(xs.len(), ys.len());
let n = xs.len();
if n == 0 {
return 0.0;
}
if x <= xs[0] {
return ys[0];
}
if x >= xs[n - 1] {
return ys[n - 1];
}
let i = xs.partition_point(|&xi| xi < x);
let x0 = xs[i - 1];
let x1 = xs[i];
let dx = x1 - x0;
if dx.abs() < f64::EPSILON {
return ys[i];
}
let t = (x - x0) / dx;
ys[i - 1] * (1.0 - t) + ys[i] * t
}
fn hilbert_transform(signal: &[f64]) -> Vec<f64> {
let n = signal.len();
if n == 0 {
return Vec::new();
}
let n_fft = n.next_power_of_two();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(n_fft);
let ifft = planner.plan_fft_inverse(n_fft);
let mut spectrum: Vec<Complex64> = signal
.iter()
.map(|&x| Complex64::new(x, 0.0))
.chain(std::iter::repeat_n(Complex64::new(0.0, 0.0), n_fft - n))
.collect();
fft.process(&mut spectrum);
let half = n_fft / 2;
for s in spectrum.iter_mut().take(half).skip(1) {
*s *= Complex64::new(2.0, 0.0);
}
for s in spectrum.iter_mut().skip(half + 1) {
*s = Complex64::new(0.0, 0.0);
}
ifft.process(&mut spectrum);
spectrum[..n].iter().map(|c| c.im / n_fft as f64).collect()
}
pub fn compute_excess_phase(total_phase: &Array1<f64>, min_phase: &Array1<f64>) -> Array1<f64> {
total_phase - min_phase
}
pub fn estimate_delay_from_excess_phase(
freq: &Array1<f64>,
excess_phase: &Array1<f64>,
) -> (f64, Array1<f64>) {
let n = freq.len();
if n < 2 {
return (0.0, excess_phase.clone());
}
let sum_f: f64 = freq.iter().sum();
let sum_phi: f64 = excess_phase.iter().map(|&p| p.to_radians()).sum();
let sum_f2: f64 = freq.iter().map(|&f| f * f).sum();
let sum_f_phi: f64 = freq
.iter()
.zip(excess_phase.iter())
.map(|(&f, &p)| f * p.to_radians())
.sum();
let n_f = n as f64;
let denom = n_f * sum_f2 - sum_f * sum_f;
if denom.abs() < 1e-12 {
return (0.0, excess_phase.clone());
}
let slope = (n_f * sum_f_phi - sum_f * sum_phi) / denom;
let intercept = (sum_phi - slope * sum_f) / n_f;
let delay_s = -slope / (2.0 * PI);
let delay_ms = delay_s * 1000.0;
let residual: Vec<f64> = freq
.iter()
.zip(excess_phase.iter())
.map(|(&f, &phi)| {
let linear_component = (slope * f + intercept).to_degrees();
phi - linear_component
})
.collect();
(delay_ms, Array1::from_vec(residual))
}
pub fn unwrap_phase_degrees(phase_deg: &Array1<f64>) -> Array1<f64> {
let mut unwrapped = Vec::with_capacity(phase_deg.len());
if phase_deg.is_empty() {
return Array1::from_vec(unwrapped);
}
let mut prev = phase_deg[0];
unwrapped.push(prev);
let mut offset = 0.0;
for &p in phase_deg.iter().skip(1) {
let diff = p - prev;
let wraps = (diff / 360.0).round();
offset -= wraps * 360.0;
unwrapped.push(p + offset);
prev = p;
}
Array1::from_vec(unwrapped)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_approx_eq(a: f64, b: f64, epsilon: f64) {
assert!(
(a - b).abs() < epsilon,
"assertion failed: {} ≈ {} (diff = {}, epsilon = {})",
a,
b,
(a - b).abs(),
epsilon
);
}
#[test]
fn test_hilbert_transform_dc() {
let signal = vec![1.0, 1.0, 1.0, 1.0];
let hilbert = hilbert_transform(&signal);
for h in hilbert {
assert!(h.abs() < 0.01, "Hilbert of DC should be ~0, got {}", h);
}
}
#[test]
fn test_unwrap_phase_no_wrap() {
let phase = Array1::from_vec(vec![0.0, 10.0, 20.0, 30.0]);
let unwrapped = unwrap_phase_degrees(&phase);
assert_eq!(unwrapped, phase);
}
#[test]
fn test_unwrap_phase_with_wrap() {
let phase = Array1::from_vec(vec![160.0, 170.0, -170.0, -160.0]);
let unwrapped = unwrap_phase_degrees(&phase);
assert_approx_eq(unwrapped[0], 160.0, 0.01);
assert_approx_eq(unwrapped[1], 170.0, 0.01);
assert_approx_eq(unwrapped[2], 190.0, 0.01); assert_approx_eq(unwrapped[3], 200.0, 0.01); }
#[test]
fn test_estimate_delay_linear_phase() {
let delay_ms = 1.0;
let delay_s = delay_ms / 1000.0;
let freq = Array1::linspace(100.0, 1000.0, 100);
let excess_phase: Array1<f64> = freq.map(|&f| -360.0 * f * delay_s);
let (estimated_delay, residual) = estimate_delay_from_excess_phase(&freq, &excess_phase);
assert_approx_eq(estimated_delay, delay_ms, 0.01);
let max_residual = residual.iter().map(|&r| r.abs()).fold(0.0, f64::max);
assert!(
max_residual < 1.0,
"Residual should be < 1 degree, got {}",
max_residual
);
}
#[test]
fn test_reconstruct_minimum_phase_flat() {
let freq = Array1::linspace(20.0, 20000.0, 100);
let spl = Array1::from_elem(100, 85.0);
let phase = reconstruct_minimum_phase(&freq, &spl);
let max_abs = phase.iter().map(|&p| p.abs()).fold(0.0_f64, f64::max);
assert!(
max_abs < 0.5,
"flat-magnitude max |phase| should be < 0.5°, got {:.4}°",
max_abs
);
}
#[test]
fn test_unwrap_phase_multi_wrap() {
let phase = Array1::from_vec(vec![0.0, 10.0, -350.0, -710.0]);
let unwrapped = unwrap_phase_degrees(&phase);
assert_approx_eq(unwrapped[0], 0.0, 0.01);
assert_approx_eq(unwrapped[1], 10.0, 0.01);
assert_approx_eq(unwrapped[2], 10.0, 0.01); assert_approx_eq(unwrapped[3], 10.0, 0.01); }
#[test]
fn test_compute_excess_phase() {
let total = Array1::from_vec(vec![-45.0, -90.0, -135.0]);
let min = Array1::from_vec(vec![-30.0, -60.0, -90.0]);
let excess = compute_excess_phase(&total, &min);
assert_approx_eq(excess[0], -15.0, 0.01);
assert_approx_eq(excess[1], -30.0, 0.01);
assert_approx_eq(excess[2], -45.0, 0.01);
}
#[test]
fn test_reconstruct_minimum_phase_lowpass() {
let n = 256;
let freq = Array1::linspace(20.0, 20000.0, n);
let fc = 1000.0_f64;
let spl: Array1<f64> = freq.map(|&f| -10.0 * (1.0 + (f / fc).powi(2)).log10());
let phase = reconstruct_minimum_phase(&freq, &spl);
let expected: Vec<f64> = freq.iter().map(|&f| -(f / fc).atan().to_degrees()).collect();
let residuals: Vec<f64> = expected
.iter()
.zip(phase.iter())
.map(|(&e, &p)| (e - p).abs())
.collect();
let edge = n / 10;
let mid_max = residuals
.iter()
.skip(edge)
.take(n - 2 * edge)
.cloned()
.fold(0.0_f64, f64::max);
assert!(
mid_max < 5.0,
"mid80 max residual on linear-grid lowpass should be < 5°, got {:.2}°",
mid_max
);
}
#[test]
fn test_excess_phase_pure_delay() {
let n = 100;
let freq = Array1::linspace(100.0, 5000.0, n);
let delay_ms = 2.0;
let delay_s = delay_ms / 1000.0;
let spl = Array1::from_elem(n, 85.0);
let min_phase = reconstruct_minimum_phase(&freq, &spl);
let delay_phase: Array1<f64> = freq.map(|&f| -360.0 * f * delay_s);
let total_phase = &min_phase + &delay_phase;
let excess = compute_excess_phase(&total_phase, &min_phase);
let (estimated_delay, residual) = estimate_delay_from_excess_phase(&freq, &excess);
assert!(
(estimated_delay - delay_ms).abs() < 0.2,
"Estimated delay {:.3}ms should be close to {:.1}ms",
estimated_delay,
delay_ms
);
let max_residual = residual.iter().map(|&r| r.abs()).fold(0.0_f64, f64::max);
assert!(
max_residual < 5.0,
"Residual should be small, got max {:.1}°",
max_residual
);
}
#[test]
fn test_estimate_delay_accuracy() {
let n = 200;
let freq = Array1::linspace(100.0, 8000.0, n);
let delay_ms = 3.0;
let delay_s = delay_ms / 1000.0;
let phase: Array1<f64> = freq.map(|&f| -360.0 * f * delay_s);
let (estimated, residual) = estimate_delay_from_excess_phase(&freq, &phase);
assert!(
(estimated - delay_ms).abs() < 0.1,
"Expected {:.1}ms, got {:.3}ms (error {:.4}ms)",
delay_ms,
estimated,
(estimated - delay_ms).abs()
);
let max_residual = residual.iter().map(|&r| r.abs()).fold(0.0_f64, f64::max);
assert!(
max_residual < 1.0,
"Residual should be < 1°, got {:.3}°",
max_residual
);
}
#[test]
fn test_reconstruct_minimum_phase_flat_128() {
let n = 128;
let freq = Array1::linspace(20.0, 20000.0, n);
let spl = Array1::from_elem(n, 80.0);
let phase = reconstruct_minimum_phase(&freq, &spl);
assert!(
phase.iter().all(|p| p.is_finite()),
"all phase values should be finite"
);
let max_abs = phase.iter().map(|&p| p.abs()).fold(0.0_f64, f64::max);
assert!(
max_abs < 0.5,
"flat-magnitude max |phase| should be < 0.5°, got {:.4}°",
max_abs
);
}
#[test]
fn test_reconstruct_minimum_phase_lowpass_256() {
let n = 256;
let freq = Array1::linspace(20.0, 20000.0, n);
let fc: f64 = 1000.0;
let spl: Array1<f64> =
freq.map(|&f| -10.0 * (1.0 + (f / fc).powi(2)).log10());
let phase = reconstruct_minimum_phase(&freq, &spl);
assert!(
phase.iter().all(|p| p.is_finite()),
"all phase values should be finite"
);
let mid = phase[n / 2];
assert!(
mid < -30.0,
"midband phase on 1st-order lowpass should be ~-45° (got {:.1}°)",
mid
);
}
#[test]
fn test_hilbert_transform_empty_and_single() {
let empty = hilbert_transform(&[]);
assert!(empty.is_empty());
let single = hilbert_transform(&[1.0]);
assert_eq!(single.len(), 1);
assert!(single[0].is_finite());
}
#[test]
fn test_estimate_delay_single_element() {
let freq = Array1::from_vec(vec![100.0]);
let phase = Array1::from_vec(vec![-45.0]);
let (delay, residual) = estimate_delay_from_excess_phase(&freq, &phase);
assert_eq!(delay, 0.0);
assert_eq!(residual.len(), 1);
}
#[test]
fn test_unwrap_phase_empty() {
let phase = Array1::from_vec(vec![]);
let unwrapped = unwrap_phase_degrees(&phase);
assert!(unwrapped.is_empty());
}
#[test]
fn reconstruct_minimum_phase_lowpass_log_spaced_accurate() {
use std::f64::consts::PI;
fn log_grid(n: usize, lo: f64, hi: f64) -> Array1<f64> {
Array1::from_vec(
(0..n)
.map(|i| lo * (hi / lo).powf(i as f64 / (n - 1) as f64))
.collect(),
)
}
let n = 256;
let freq = log_grid(n, 20.0, 20000.0);
let fc = 1000.0_f64;
let omega_c = 2.0 * PI * fc;
let spl = Array1::from_vec(
freq.iter()
.map(|&f| {
let omega = 2.0 * PI * f;
let mag = omega_c / (omega_c * omega_c + omega * omega).sqrt();
20.0 * mag.log10()
})
.collect::<Vec<f64>>(),
);
let expected: Vec<f64> = freq
.iter()
.map(|&f| {
let omega = 2.0 * PI * f;
-((omega / omega_c).atan()).to_degrees()
})
.collect();
let recon = reconstruct_minimum_phase(&freq, &spl);
let residuals: Vec<f64> = expected
.iter()
.zip(recon.iter())
.map(|(&e, &r)| (e - r).abs())
.collect();
let full_max = residuals.iter().cloned().fold(0.0_f64, f64::max);
assert!(
full_max < 5.0,
"full-range max residual should be < 5°, got {:.2}°",
full_max
);
let edge = n / 10;
let mid_max = residuals
.iter()
.skip(edge)
.take(n - 2 * edge)
.cloned()
.fold(0.0_f64, f64::max);
assert!(
mid_max < 2.5,
"mid80 max residual should be < 2.5°, got {:.2}°",
mid_max
);
let low_mid_max = freq
.iter()
.zip(residuals.iter())
.filter(|&(f, _)| *f < 600.0)
.map(|(_, r)| *r)
.fold(0.0_f64, f64::max);
assert!(
low_mid_max < 1.0,
"residual below 600 Hz should be < 1°, got {:.3}°",
low_mid_max
);
}
}