use num_complex::Complex;
use crate::error::{SpectrogramError, SpectrogramResult};
use crate::fft_backend::C2cPlan;
use crate::fft_backend::realfft_backend::RealFftC2cPlan;
const DEFAULT_OVERSAMPLE: usize = 8;
pub fn minimum_phase(ir: &[f32]) -> SpectrogramResult<Vec<f32>> {
minimum_phase_with(ir, ir.len(), DEFAULT_OVERSAMPLE)
}
pub fn minimum_phase_with(
ir: &[f32],
out_len: usize,
oversample: usize,
) -> SpectrogramResult<Vec<f32>> {
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 = 1.0 / n as f64;
let mut fft = RealFftC2cPlan::new(n);
let mut buf = vec![Complex::new(0.0f64, 0.0); n];
for (dst, &src) in buf.iter_mut().zip(ir.iter()) {
dst.re = f64::from(src);
}
fft.forward(&mut buf)?;
let max_mag2 = buf.iter().map(Complex::norm_sqr).fold(0.0_f64, f64::max);
let eps = if max_mag2 > 0.0 {
max_mag2 * 1e-20
} else {
1e-300
};
for x in buf.iter_mut() {
let log_mag = 0.5 * (x.norm_sqr() + eps).ln();
x.re = log_mag;
x.im = 0.0;
}
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 *= 2.0;
}
for x in buf.iter_mut().skip(half + 1) {
*x = Complex::new(0.0, 0.0);
}
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<f32> = buf
.iter()
.take(take)
.map(|x| (x.re * inv_n) as f32)
.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}"
);
}
}