use super::next_power_of_two;
use crate::math::dsp::fft::FftPlanner;
use std::f64::consts::TAU;
#[derive(Debug, Clone)]
pub struct ThdnResult {
pub f0: f64,
pub sample_rate: u32,
pub thdn_percent: f64,
pub thdn_db: f64,
pub rms_notched: f64,
pub rms_total: f64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct NotchBiquad {
pub(crate) b0: f64,
pub(crate) b1: f64,
pub(crate) b2: f64,
pub(crate) a1: f64,
pub(crate) a2: f64,
}
impl NotchBiquad {
pub(crate) fn design(f0: f64, q: f64, fs: f64) -> Self {
let w0 = TAU * f0 / fs;
let cos_w0 = w0.cos();
let alpha = w0.sin() / (2.0 * q);
let b0 = 1.0;
let b1 = -2.0 * cos_w0;
let b2 = 1.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
}
}
pub(crate) fn apply(&self, signal: &[f64]) -> Vec<f64> {
let mut out = vec![0.0f64; signal.len()];
let mut x1 = 0.0f64;
let mut x2 = 0.0f64;
let mut y1 = 0.0f64;
let mut y2 = 0.0f64;
for (i, &x0) in signal.iter().enumerate() {
let y0 = self.b0 * x0 + self.b1 * x1 + self.b2 * x2 - self.a1 * y1 - self.a2 * y2;
out[i] = y0;
x2 = x1;
x1 = x0;
y2 = y1;
y1 = y0;
}
out
}
}
pub fn generate_sine_f64(f0: f64, sample_rate: u32, num_samples: usize, gain: f64) -> Vec<f64> {
let sr = sample_rate as f64;
let omega = TAU * f0 / sr;
(0..num_samples)
.map(|i| (i as f64 * omega).sin() * gain)
.collect()
}
pub fn measure_thdn<F>(
f0: f64,
sample_rate: u32,
duration_s: f64,
notch_q: f64,
stability_blocks: usize,
process_fn: F,
) -> ThdnResult
where
F: FnOnce(&[f64]) -> Vec<f32>,
{
let n_total = (sample_rate as f64 * duration_s).ceil() as usize + stability_blocks;
let input = generate_sine_f64(f0, sample_rate, n_total, 1.0);
let output_f32 = process_fn(&input);
assert_eq!(output_f32.len(), input.len());
let start = stability_blocks;
let output: Vec<f64> = output_f32[start..].iter().map(|&x| x as f64).collect();
let n = output.len();
let rms_total = {
let sum_sq: f64 = output.iter().map(|&x| x * x).sum();
(sum_sq / n as f64).sqrt()
};
let notch = NotchBiquad::design(f0, notch_q, sample_rate as f64);
let notched = notch.apply(&output);
let settle = 2000.min(n.saturating_sub(1));
let notched_stable = ¬ched[settle..];
let rms_notched = {
let sum_sq: f64 = notched_stable.iter().map(|&x| x * x).sum();
(sum_sq / notched_stable.len() as f64).sqrt()
};
let thdn_percent = if rms_total > 1e-15 {
100.0 * rms_notched / rms_total
} else {
0.0
};
let thdn_db = if thdn_percent > 1e-15 {
20.0 * (thdn_percent / 100.0).log10()
} else {
f64::NEG_INFINITY
};
ThdnResult {
f0,
sample_rate,
thdn_percent,
thdn_db,
rms_notched,
rms_total,
}
}
#[derive(Debug, Clone)]
pub struct SmpteImdResult {
pub f_low: f64,
pub f_high: f64,
pub ratio: f64,
pub sample_rate: u32,
pub imd_percent: f64,
pub imd_db: f64,
pub sideband_percents: Vec<(i32, f64)>,
}
pub fn generate_smpte_tones(
f_low: f64,
f_high: f64,
ratio: f64,
sample_rate: u32,
num_samples: usize,
gain: f64,
) -> Vec<f64> {
let sr = sample_rate as f64;
let omega_low = TAU * f_low / sr;
let omega_high = TAU * f_high / sr;
let amp_high = gain * ratio / (ratio + 1.0);
let amp_low = gain / (ratio + 1.0);
(0..num_samples)
.map(|i| {
let t = i as f64;
amp_low * (t * omega_low).sin() + amp_high * (t * omega_high).sin()
})
.collect()
}
pub fn measure_smpte_imd<F>(
f_low: f64,
f_high: f64,
ratio: f64,
sample_rate: u32,
duration_s: f64,
stability_blocks: usize,
process_fn: F,
) -> SmpteImdResult
where
F: FnOnce(&[f64]) -> Vec<f32>,
{
let n_signal = (sample_rate as f64 * duration_s).ceil() as usize + stability_blocks;
let input = generate_smpte_tones(f_low, f_high, ratio, sample_rate, n_signal, 0.9);
let output_f32 = process_fn(&input);
assert_eq!(output_f32.len(), input.len());
let start = stability_blocks;
let output: Vec<f64> = output_f32[start..].iter().map(|&x| x as f64).collect();
let n = output.len();
let n_fft = next_power_of_two(n);
let sr = sample_rate as f64;
let bin_width = sr / n_fft as f64;
let window: Vec<f64> = crate::testing::aliasing::blackman_harris_4term(n);
let mut re = vec![0.0f64; n_fft];
let mut im = vec![0.0f64; n_fft];
for (i, &x) in output.iter().enumerate() {
re[i] = x * window[i];
}
let fft = FftPlanner::<f64>::new(n_fft);
fft.process(&mut re, &mut im);
let mag: Vec<f64> = re
.iter()
.zip(im.iter())
.map(|(&r, &i)| (r * r + i * i).sqrt())
.collect();
let carrier_bin = (f_high / bin_width).round() as usize;
let carrier_mag = if carrier_bin < mag.len() {
mag[carrier_bin]
} else {
0.0
};
let max_sideband_order = 6i32;
let bin_search = (2.0 / bin_width).ceil() as usize;
let mut sideband_percents: Vec<(i32, f64)> = Vec::new();
for n_order in 1..=max_sideband_order {
for &sign in &[-1i32, 1i32] {
let expected_freq = f_high + sign as f64 * n_order as f64 * f_low;
if expected_freq <= 0.0 || expected_freq >= (sr / 2.0 - bin_width) {
continue;
}
let expected_bin = (expected_freq / bin_width).round() as usize;
let start_bin = expected_bin.saturating_sub(bin_search);
let end_bin = (expected_bin + bin_search).min(mag.len() - 1);
if start_bin >= end_bin {
continue;
}
let mut _peak_bin = start_bin;
let mut peak_val = 0.0f64;
for b in start_bin + 1..end_bin {
if mag[b] > mag[b - 1] && mag[b] > mag[b + 1] && mag[b] > peak_val {
_peak_bin = b;
peak_val = mag[b];
}
}
if peak_val > 0.0 && carrier_mag > 1e-15 {
let pct = 100.0 * peak_val / carrier_mag;
sideband_percents.push((n_order * sign, pct));
}
}
}
let imd_percent = if carrier_mag > 1e-15 {
let sum_sq: f64 = sideband_percents
.iter()
.map(|(_, p)| (p / 100.0).powi(2))
.sum();
100.0 * sum_sq.sqrt()
} else {
0.0
};
let imd_db = if imd_percent > 1e-15 {
20.0 * (imd_percent / 100.0).log10()
} else {
f64::NEG_INFINITY
};
SmpteImdResult {
f_low,
f_high,
ratio,
sample_rate,
imd_percent,
imd_db,
sideband_percents,
}
}