use super::next_power_of_two;
use crate::math::dsp::fft::FftPlanner;
use std::f64::consts::TAU;
#[derive(Debug, Clone)]
pub struct FarinaResult {
pub sample_rate: u32,
pub f1: f64,
pub f2: f64,
pub duration_s: f64,
pub ir_linear: Vec<f64>,
pub fr_magnitude_db: Vec<f64>,
pub fr_phase_rad: Vec<f64>,
pub freq_axis: Vec<f64>,
pub thd_by_order: Vec<(u32, f64)>,
pub thd_total_percent: f64,
}
pub fn generate_farina_sweep(f1: f64, f2: f64, duration_s: f64, sample_rate: u32) -> Vec<f64> {
assert!(f1 > 0.0 && f2 > f1 && duration_s > 0.0 && sample_rate > 0);
let n = (sample_rate as f64 * duration_s).ceil() as usize;
let sr = sample_rate as f64;
let t_scale = sr * duration_s;
let omega1 = TAU * f1;
let ln_ratio = (f2 / f1).ln();
let mut sweep = Vec::with_capacity(n);
for i in 0..n {
let t_norm = i as f64 / t_scale;
let phase = omega1 * duration_s / ln_ratio * ((t_norm * ln_ratio).exp_m1());
sweep.push(phase.sin());
}
sweep
}
pub fn generate_farina_inverse_filter(
sweep: &[f64],
_f1: f64,
_f2: f64,
_duration_s: f64,
_sample_rate: u32,
) -> Vec<f64> {
let n = sweep.len();
let n_fft = next_power_of_two(n);
let fft = FftPlanner::<f64>::new(n_fft);
let mut s_re = vec![0.0f64; n_fft];
let mut s_im = vec![0.0f64; n_fft];
s_re[..n].copy_from_slice(sweep);
fft.process(&mut s_re, &mut s_im);
let eps = 1e-10;
for i in 0..n_fft {
let mag_sq = s_re[i] * s_re[i] + s_im[i] * s_im[i] + eps;
s_re[i] /= mag_sq;
s_im[i] = -s_im[i] / mag_sq;
}
fft.process_inverse(&mut s_re, &mut s_im);
let mut inv: Vec<f64> = s_re[..n].to_vec();
let max_abs = inv.iter().map(|&x| x.abs()).fold(0.0f64, f64::max);
if max_abs > 1e-10 {
let scale = 0.95 / max_abs;
for v in &mut inv {
*v *= scale;
}
}
inv
}
pub(crate) fn deconvolve_farina(y: &[f64], inv_filter: &[f64]) -> Vec<f64> {
assert_eq!(
y.len(),
inv_filter.len(),
"y and inv_filter must have same length for Farina deconvolution"
);
let n = next_power_of_two(y.len());
let fft = FftPlanner::<f64>::new(n);
let mut y_re = vec![0.0f64; n];
let mut y_im = vec![0.0f64; n];
let mut f_re = vec![0.0f64; n];
let mut f_im = vec![0.0f64; n];
y_re[..y.len()].copy_from_slice(y);
f_re[..inv_filter.len()].copy_from_slice(inv_filter);
fft.process(&mut y_re, &mut y_im);
fft.process(&mut f_re, &mut f_im);
for i in 0..n {
let yr = y_re[i];
let yi = y_im[i];
let fr = f_re[i];
let fi = f_im[i];
y_re[i] = yr * fr - yi * fi;
y_im[i] = yr * fi + yi * fr;
}
fft.process_inverse(&mut y_re, &mut y_im);
y_re.truncate(y.len());
y_re
}
pub fn farina_measure<F>(
f1: f64,
f2: f64,
duration_s: f64,
sample_rate: u32,
max_harmonics: u32,
process_fn: F,
) -> FarinaResult
where
F: FnOnce(&[f64]) -> Vec<f32>,
{
let sweep = generate_farina_sweep(f1, f2, duration_s, sample_rate);
let n = sweep.len();
let inv_filter = generate_farina_inverse_filter(&sweep, f1, f2, duration_s, sample_rate);
let output_f32 = process_fn(&sweep);
assert_eq!(
output_f32.len(),
n,
"process_fn output length {} != sweep length {n}",
output_f32.len()
);
let output: Vec<f64> = output_f32.iter().map(|&x| x as f64).collect();
let deconv = deconvolve_farina(&output, &inv_filter);
let sr = sample_rate as f64;
let ln_ratio = (f2 / f1).ln();
let _delay_linear = inv_filter.len() as f64 / sr;
let half_point_s = duration_s * (1.5f64).ln() / ln_ratio;
let half_point_samples = (half_point_s * sr).round() as usize;
let ir_len = if half_point_samples < n {
half_point_samples
} else {
n / 2
};
let ir_linear: Vec<f64> = deconv[..ir_len.min(deconv.len())].to_vec();
let fr_n = next_power_of_two(ir_linear.len());
let mut fr_re = vec![0.0f64; fr_n];
let mut fr_im = vec![0.0f64; fr_n];
fr_re[..ir_linear.len()].copy_from_slice(&ir_linear);
let fft = FftPlanner::<f64>::new(fr_n);
fft.process(&mut fr_re, &mut fr_im);
let num_bins = fr_n / 2 + 1;
let mut fr_magnitude_db = vec![0.0f64; num_bins];
let mut fr_phase_rad = vec![0.0f64; num_bins];
let mut freq_axis = vec![0.0f64; num_bins];
for i in 0..num_bins {
freq_axis[i] = i as f64 * sr / fr_n as f64;
let mag = (fr_re[i] * fr_re[i] + fr_im[i] * fr_im[i]).sqrt();
let mag_db = if mag > 1e-15 {
20.0 * mag.log10()
} else {
-300.0
};
fr_magnitude_db[i] = mag_db;
fr_phase_rad[i] = fr_im[i].atan2(fr_re[i]);
}
let window_len = ir_len;
let mut harmonic_energies: Vec<(u32, f64)> = Vec::new();
for k in 1..=max_harmonics {
let lag_s = duration_s * (k as f64).ln() / ln_ratio;
let lag_samples = (lag_s * sr).round() as isize;
let start = lag_samples.max(0) as usize;
let end = (start + window_len).min(deconv.len());
if end <= start || start >= deconv.len() {
break;
}
let segment = &deconv[start..end];
let energy: f64 = segment.iter().map(|&x| x * x).sum();
harmonic_energies.push((k, energy));
}
let fund_energy = harmonic_energies.first().map(|(_, e)| *e).unwrap_or(0.0);
let mut thd_by_order: Vec<(u32, f64)> = Vec::new();
for (k, energy) in &harmonic_energies {
let thd = if *k == 1 {
0.0
} else if fund_energy > 1e-20 {
100.0 * (energy / fund_energy).sqrt()
} else {
0.0
};
thd_by_order.push((*k, thd));
}
let thd_total_percent = if thd_by_order.len() > 1 {
let sum_sq: f64 = thd_by_order
.iter()
.skip(1)
.map(|(_, thd)| (*thd / 100.0).powi(2))
.sum();
100.0 * sum_sq.sqrt()
} else {
0.0
};
if let Some((1, _)) = thd_by_order.first() {}
FarinaResult {
sample_rate,
f1,
f2,
duration_s,
ir_linear,
fr_magnitude_db,
fr_phase_rad,
freq_axis,
thd_by_order,
thd_total_percent,
}
}