use num_complex::Complex;
use crate::Sample;
use crate::error::{SpectrogramError, SpectrogramResult};
use crate::fft_backend::C2cPlan;
const DEFAULT_OVERSAMPLE: usize = 8;
pub fn minimum_phase<T: Sample>(ir: &[T]) -> SpectrogramResult<Vec<T>> {
minimum_phase_with(ir, ir.len(), DEFAULT_OVERSAMPLE)
}
pub fn minimum_phase_with<T: Sample>(
ir: &[T],
out_len: usize,
oversample: usize,
) -> SpectrogramResult<Vec<T>> {
if ir.is_empty() {
return Err(SpectrogramError::invalid_input(
"impulse response must not be empty",
));
}
if out_len == 0 {
return Err(SpectrogramError::invalid_input(
"out_len must be greater than zero",
));
}
let oversample = oversample.max(1);
let n = (ir.len() * oversample).next_power_of_two();
let inv_n = T::one() / T::from_usize(n);
let mut fft = T::plan_c2c(n)?;
let mut buf = vec![Complex::new(T::zero(), T::zero()); n];
for (dst, &src) in buf.iter_mut().zip(ir.iter()) {
dst.re = src;
}
fft.forward(&mut buf)?;
let max_mag2 = buf.iter().map(Complex::norm_sqr).fold(T::zero(), T::max);
let eps = if max_mag2 > T::zero() {
max_mag2 * T::from_f64(1e-20)
} else {
T::from_f64(1e-300)
};
for x in buf.iter_mut() {
let log_mag = T::from_f64(0.5) * (x.norm_sqr() + eps).ln();
x.re = log_mag;
x.im = T::zero();
}
fft.inverse(&mut buf)?;
for x in buf.iter_mut() {
*x *= inv_n;
}
let half = n / 2;
for x in buf.iter_mut().take(half).skip(1) {
*x *= T::from_f64(2.0);
}
for x in buf.iter_mut().skip(half + 1) {
*x = Complex::new(T::zero(), T::zero());
}
fft.forward(&mut buf)?;
for x in buf.iter_mut() {
let mag = x.re.exp();
*x = Complex::new(mag * x.im.cos(), mag * x.im.sin());
}
fft.inverse(&mut buf)?;
let take = out_len.min(n);
let out: Vec<T> = buf.iter().take(take).map(|x| x.re * inv_n).collect();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use num_complex::Complex;
fn mag_at(h: &[f32], n: usize, k: usize) -> f64 {
let mut acc = Complex::new(0.0f64, 0.0);
let w = -2.0 * std::f64::consts::PI * k as f64 / n as f64;
for (idx, &v) in h.iter().enumerate() {
acc += Complex::from_polar(f64::from(v), w * idx as f64);
}
acc.norm()
}
#[test]
fn magnitude_response_is_preserved() {
let taps = 64;
let fc = 0.15; let mid = (taps - 1) as f64 / 2.0;
let lin: Vec<f32> = (0..taps)
.map(|k| {
let x = k as f64 - mid;
let sinc = if x.abs() < 1e-9 {
2.0 * fc
} else {
(2.0 * std::f64::consts::PI * fc * x).sin() / (std::f64::consts::PI * x)
};
let w =
0.5 - 0.5 * (2.0 * std::f64::consts::PI * k as f64 / (taps - 1) as f64).cos();
(sinc * w) as f32
})
.collect();
let mp = minimum_phase(&lin).unwrap();
assert_eq!(mp.len(), lin.len());
let n = 512;
for k in 0..=n / 2 {
let a = mag_at(&lin, n, k);
let b = mag_at(&mp, n, k);
assert!(
(a - b).abs() < 1e-2 + 1e-2 * a,
"bin {k}: linear {a} vs min-phase {b}"
);
}
}
#[test]
fn energy_is_front_loaded() {
let taps = 64;
let mid = (taps - 1) as f64 / 2.0;
let lin: Vec<f32> = (0..taps)
.map(|k| {
let x = k as f64 - mid;
let sinc = if x.abs() < 1e-9 {
0.3
} else {
(0.3 * std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x)
};
sinc as f32
})
.collect();
let mp = minimum_phase(&lin).unwrap();
let centroid = |h: &[f32]| {
let (mut num, mut den) = (0.0f64, 0.0f64);
for (i, &v) in h.iter().enumerate() {
let e = f64::from(v) * f64::from(v);
num += i as f64 * e;
den += e;
}
num / den
};
let lin_c = centroid(&lin);
let mp_c = centroid(&mp);
assert!(
mp_c < lin_c * 0.5,
"min-phase energy centroid {mp_c} should be well before linear-phase {lin_c}"
);
}
}