use crate::error::{Error, Result};
use boostr::model::audio::stft::{CenterPad, StftOptions, stft};
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use numr::tensor::Tensor;
const FFT_SIZES: [usize; 3] = [512, 1024, 2048];
const LOG_EPS: f64 = 1e-10;
const REL_LOG_EPS: f64 = 3e-9;
fn log_floor(peak_a: f64) -> f64 {
(REL_LOG_EPS * peak_a).max(LOG_EPS)
}
fn hann_window_f64(n: usize) -> Vec<f64> {
use std::f64::consts::PI;
(0..n)
.map(|i| 0.5 - 0.5 * (2.0 * PI * i as f64 / n.max(1) as f64).cos())
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpectralDistance {
pub log_mag_l1: f64,
pub spectral_convergence: f64,
}
pub fn multi_resolution_stft_distance(
a: &[f32],
b: &[f32],
sample_rate: u32,
) -> Result<SpectralDistance> {
if a.is_empty() || b.is_empty() {
return Err(Error::InvalidArgument {
arg: "a",
reason: format!(
"both signals must be non-empty, got a.len()={} b.len()={}",
a.len(),
b.len()
),
});
}
if sample_rate == 0 {
return Err(Error::InvalidArgument {
arg: "sample_rate",
reason: "sample rate is 0".to_string(),
});
}
let n = a.len().min(b.len());
let a64: Vec<f64> = a[..n].iter().map(|&s| s as f64).collect();
let b64: Vec<f64> = b[..n].iter().map(|&s| s as f64).collect();
let device = CpuDevice::default();
let client = CpuClient::new(device.clone());
let wave_a = Tensor::<CpuRuntime>::from_slice(&a64, &[1, n], &device)?;
let wave_b = Tensor::<CpuRuntime>::from_slice(&b64, &[1, n], &device)?;
let mut log_mag_l1_sum = 0.0f64;
let mut spectral_convergence_sum = 0.0f64;
let mut resolutions_used = 0usize;
for &n_fft in &FFT_SIZES {
if n < n_fft {
continue;
}
let opts = StftOptions {
n_fft,
hop_length: n_fft / 4,
center: true,
pad: CenterPad::Zeros,
};
let window = Tensor::<CpuRuntime>::from_slice(&hann_window_f64(n_fft), &[n_fft], &device)?;
let (mag_a, _) = stft(&client, &wave_a, &window, opts)?;
let (mag_b, _) = stft(&client, &wave_b, &window, opts)?;
let mag_a: Vec<f64> = mag_a.contiguous()?.to_vec();
let mag_b: Vec<f64> = mag_b.contiguous()?.to_vec();
debug_assert_eq!(mag_a.len(), mag_b.len());
let peak_a = mag_a.iter().cloned().fold(0.0f64, f64::max);
let floor = log_floor(peak_a);
let mut log_l1 = 0.0f64;
let mut diff_sq = 0.0f64;
let mut ref_sq = 0.0f64;
for (&x, &y) in mag_a.iter().zip(mag_b.iter()) {
log_l1 += ((x + floor).ln() - (y + floor).ln()).abs();
let d = x - y;
diff_sq += d * d;
ref_sq += x * x;
}
let bins = mag_a.len().max(1) as f64;
log_mag_l1_sum += log_l1 / bins;
spectral_convergence_sum += diff_sq.sqrt() / ref_sq.sqrt().max(floor);
resolutions_used += 1;
}
if resolutions_used == 0 {
return Err(Error::InvalidArgument {
arg: "a",
reason: format!(
"common signal length {n} is shorter than every FFT size in {FFT_SIZES:?}"
),
});
}
let count = resolutions_used as f64;
Ok(SpectralDistance {
log_mag_l1: log_mag_l1_sum / count,
spectral_convergence: spectral_convergence_sum / count,
})
}
pub fn snr_db(reference: &[f32], test: &[f32]) -> Result<f64> {
if reference.is_empty() || test.is_empty() {
return Err(Error::InvalidArgument {
arg: "reference",
reason: format!(
"both signals must be non-empty, got reference.len()={} test.len()={}",
reference.len(),
test.len()
),
});
}
let n = reference.len().min(test.len());
let mut signal_energy = 0.0f64;
let mut noise_energy = 0.0f64;
for i in 0..n {
let r = reference[i] as f64;
let t = test[i] as f64;
signal_energy += r * r;
let d = r - t;
noise_energy += d * d;
}
if noise_energy == 0.0 {
return Ok(f64::INFINITY);
}
if signal_energy == 0.0 {
return Ok(f64::NEG_INFINITY);
}
Ok(10.0 * (signal_energy / noise_energy).log10())
}
#[cfg(test)]
mod tests {
use super::*;
const LEN: usize = 8192;
const RATE: u32 = 16_000;
fn sine(amplitude: f32, freq: f64, len: usize) -> Vec<f32> {
(0..len)
.map(|n| {
amplitude * (std::f64::consts::TAU * freq * n as f64 / RATE as f64).sin() as f32
})
.collect()
}
fn noise(len: usize, seed: u64) -> Vec<f32> {
let mut state = seed | 1;
(0..len)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
((state as f64 / u64::MAX as f64) * 2.0 - 1.0) as f32
})
.collect()
}
#[test]
fn identical_signals_have_zero_distance() {
let a = sine(0.8, 440.0, LEN);
let d = multi_resolution_stft_distance(&a, &a, RATE).expect("distance");
assert!(d.log_mag_l1 < 1e-4, "log_mag_l1 = {}", d.log_mag_l1);
assert!(
d.spectral_convergence < 1e-4,
"spectral_convergence = {}",
d.spectral_convergence
);
}
#[test]
fn identical_signals_have_infinite_snr() {
let a = sine(0.8, 440.0, LEN);
assert_eq!(snr_db(&a, &a).expect("snr"), f64::INFINITY);
}
#[test]
fn one_db_scaled_copy_matches_known_log_mag_l1() {
let a = sine(0.5, 440.0, LEN);
let scale = 10f32.powf(1.0 / 20.0);
let b: Vec<f32> = a.iter().map(|&s| s * scale).collect();
let d = multi_resolution_stft_distance(&a, &b, RATE).expect("distance");
let expected = (scale as f64).ln();
assert!(
(d.log_mag_l1 - expected).abs() < 0.01,
"log_mag_l1 = {}, expected {expected}",
d.log_mag_l1
);
}
#[test]
fn pure_scale_of_white_noise_matches_ln_scale_tightly() {
let a = noise(LEN, 0x1234_5678);
let scale = 10f32.powf(1.0 / 20.0);
let b: Vec<f32> = a.iter().map(|&s| s * scale).collect();
let d = multi_resolution_stft_distance(&a, &b, RATE).expect("distance");
let expected = (scale as f64).ln();
assert!(
(d.log_mag_l1 - expected).abs() < 1e-6,
"log_mag_l1 = {}, expected {expected}",
d.log_mag_l1
);
}
#[test]
fn white_noise_vs_silence_is_a_large_distance() {
let ref_signal = sine(0.8, 440.0, LEN); let baseline = multi_resolution_stft_distance(&ref_signal, &ref_signal, RATE)
.expect("baseline distance");
let loud = noise(LEN, 0xA5A5_1234);
let silence = vec![0.0f32; LEN];
let d = multi_resolution_stft_distance(&loud, &silence, RATE).expect("distance");
assert!(
(d.spectral_convergence - 1.0).abs() < 1e-6,
"spectral_convergence = {}",
d.spectral_convergence
);
assert!(
d.log_mag_l1 > baseline.log_mag_l1 + 1.0,
"log_mag_l1 = {}, baseline = {}",
d.log_mag_l1,
baseline.log_mag_l1
);
}
#[test]
fn white_noise_vs_silence_has_very_negative_snr() {
let silence = vec![0.0f32; LEN];
let loud = noise(LEN, 42);
assert_eq!(snr_db(&silence, &loud).expect("snr"), f64::NEG_INFINITY);
}
#[test]
fn rejects_empty_signals() {
assert!(multi_resolution_stft_distance(&[], &[1.0], RATE).is_err());
assert!(multi_resolution_stft_distance(&[1.0], &[], RATE).is_err());
assert!(snr_db(&[], &[1.0]).is_err());
}
#[test]
fn rejects_zero_sample_rate() {
let a = sine(0.5, 440.0, LEN);
assert!(multi_resolution_stft_distance(&a, &a, 0).is_err());
}
#[test]
fn a_signal_shorter_than_every_fft_size_is_an_error() {
let a = vec![0.1f32; 100];
assert!(multi_resolution_stft_distance(&a, &a, RATE).is_err());
}
#[test]
fn mismatched_lengths_are_truncated_not_rejected() {
let a = sine(0.5, 440.0, LEN);
let b = sine(0.5, 440.0, LEN - 37);
assert!(multi_resolution_stft_distance(&a, &b, RATE).is_ok());
assert!(snr_db(&a, &b).is_ok());
}
}