use crate::math::dsp::fft::RfftPlanner;
pub const MUSICAL_PITCHES: &[(f64, &str)] = &[
(82.41, "E2"),
(110.00, "A2"),
(146.83, "D3"),
(196.00, "G3"),
(246.94, "B3"),
(329.63, "E4"),
(440.00, "A4"),
(587.33, "D5"),
(659.25, "E5"),
];
pub const STRESS_F0: f64 = 2017.0;
pub const HIGH_GAIN: f64 = 4.0;
#[derive(Debug, Clone)]
pub struct AsrResult {
pub f0: f64,
pub sample_rate: u32,
pub asr_db: f64,
pub asr_linear: f64,
pub harmonic_energy: f64,
pub aliased_energy: f64,
pub num_harmonics: usize,
pub num_aliased: usize,
pub noise_floor: f64,
pub peak_threshold: f64,
pub bin_width: f64,
}
impl AsrResult {
pub fn has_aliasing(&self) -> bool {
self.aliased_energy > f64::EPSILON
}
}
pub fn generate_sine(f0: f64, sample_rate: u32, num_samples: usize, gain: f64) -> Vec<f32> {
let sr = sample_rate as f64;
let omega = 2.0 * std::f64::consts::PI * f0 / sr;
(0..num_samples)
.map(|i| ((i as f64 * omega).sin() * gain) as f32)
.collect()
}
pub fn generate_sine_440hz(num_samples: usize) -> Vec<f32> {
generate_sine(440.0, 48_000, num_samples, 1.0)
}
pub fn blackman_harris_4term(n: usize) -> Vec<f64> {
let n_minus_1 = (n - 1) as f64;
(0..n)
.map(|i| {
let x = 2.0 * std::f64::consts::PI * i as f64 / n_minus_1;
0.35875 - 0.48829 * x.cos() + 0.14128 * (2.0 * x).cos() - 0.01168 * (3.0 * x).cos()
})
.collect()
}
pub fn compute_asr(output: &[f32], f0: f64, sample_rate: u32) -> AsrResult {
let n = output.len();
assert!(
n.is_power_of_two(),
"Signal length {n} must be power of two"
);
assert!(n >= 1024, "Signal too short ({n}) for reliable ASR");
assert!(f0 > 0.0, "f0 must be positive");
let bin_width = sample_rate as f64 / n as f64;
let nyquist = sample_rate as f64 / 2.0;
let num_bins = n / 2 + 1;
let window = blackman_harris_4term(n);
let windowed: Vec<f64> = output
.iter()
.enumerate()
.map(|(i, &s)| s as f64 * window[i])
.collect();
let mut rfft = RfftPlanner::<f64>::new(n);
let mut re = vec![0.0f64; num_bins];
let mut im = vec![0.0f64; num_bins];
rfft.process_forward(&windowed, &mut re, &mut im);
let mag: Vec<f64> = re
.iter()
.zip(im.iter())
.map(|(&r, &i)| (r * r + i * i).sqrt())
.collect();
let noise_floor = median(&mag);
let max_mag = mag.iter().cloned().fold(0.0f64, f64::max);
let peak_threshold = (noise_floor * 6.0).max(max_mag * 1e-4);
let mut peaks: Vec<(usize, f64)> = Vec::new();
for i in 1..num_bins - 1 {
if mag[i] > mag[i - 1] && mag[i] > mag[i + 1] && mag[i] > peak_threshold {
peaks.push((i, mag[i]));
}
}
let tolerance = 1.5 * bin_width;
let mut harmonic_energy = 0.0f64;
let mut aliased_energy = 0.0f64;
let mut num_harmonics = 0usize;
let mut num_aliased = 0usize;
let max_harm = (nyquist / f0).floor() as usize;
for &(bin_idx, peak_mag) in &peaks {
let freq_bin = bin_idx as f64 * bin_width;
let mut is_harmonic = false;
for k in 1..=max_harm {
let expected = k as f64 * f0;
if (freq_bin - expected).abs() <= tolerance {
is_harmonic = true;
break;
}
}
let energy = peak_mag * peak_mag;
if is_harmonic {
harmonic_energy += energy;
num_harmonics += 1;
} else {
aliased_energy += energy;
num_aliased += 1;
}
}
let asr_linear = if harmonic_energy > f64::EPSILON {
aliased_energy / harmonic_energy
} else {
f64::INFINITY
};
let asr_db = if asr_linear <= f64::EPSILON {
f64::NEG_INFINITY
} else {
10.0 * asr_linear.log10()
};
AsrResult {
f0,
sample_rate,
asr_db,
asr_linear,
harmonic_energy,
aliased_energy,
num_harmonics,
num_aliased,
noise_floor,
peak_threshold,
bin_width,
}
}
pub fn asr_aggregate(results: &[AsrResult]) -> f64 {
if results.is_empty() {
return f64::NEG_INFINITY;
}
let sum: f64 = results.iter().map(|r| r.asr_linear).sum();
let mean = sum / results.len() as f64;
if mean <= f64::EPSILON {
f64::NEG_INFINITY
} else {
10.0 * mean.log10()
}
}
pub fn asr_worst_case(results: &[AsrResult]) -> f64 {
results
.iter()
.map(|r| r.asr_db)
.fold(f64::NEG_INFINITY, f64::max)
}
pub(crate) fn median(data: &[f64]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut sorted: Vec<f64> = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = sorted.len() / 2;
if sorted.len().is_multiple_of(2) {
(sorted[mid - 1] + sorted[mid]) * 0.5
} else {
sorted[mid]
}
}
#[cfg(test)]
#[path = "aliasing_test.rs"]
mod tests;