use crate::{Complex64 as C, DspError, fft, invalid, numeric::sum};
use std::f64::consts::PI;
pub const LOWRATE_MATERIALIZATION_VERSION: u32 = 1;
pub const MAX_LOWRATE_MATERIALIZED_TAPS: usize = 1 << 20;
pub fn lowrate_len_48k(taps_low: usize, fs_low: f64) -> Option<usize> {
if taps_low == 0 || !fs_low.is_finite() || fs_low <= 0.0 {
return None;
}
let length = (taps_low as f64 * (48000.0 / fs_low)).ceil();
(length.is_finite() && (1.0..=MAX_LOWRATE_MATERIALIZED_TAPS as f64).contains(&length))
.then_some(length as usize)
}
pub fn firwin(taps: usize, cutoff: f64, fs: f64) -> Result<Vec<f64>, DspError> {
let fc = cutoff / (0.5 * fs);
if fc <= 0. || fc >= 1. {
return Err(invalid("cutoff must lie between zero and Nyquist"));
}
if taps == 0 {
return Err(invalid("firwin needs positive taps"));
}
let mut h: Vec<_> = (0..taps)
.map(|i| {
let m = i as f64 - 0.5 * (taps as f64 - 1.);
let x = fc * m;
let y = PI * if x == 0. { 1e-20 } else { x };
let sinc = y.sin() / y;
let phase = if i == taps - 1 {
PI
} else {
-PI + i as f64 * (2. * PI / (taps - 1) as f64)
};
let window = if taps == 1 {
1.
} else {
0.54 + (1.0 - 0.54) * phase.cos()
};
(fc * sinc - 0.) * window
})
.collect();
let scale = sum(&h);
for v in &mut h {
*v /= scale;
}
Ok(h)
}
pub fn lowrate_response(taps: &[f64], fs_low: f64, frequencies: &[f64]) -> Vec<C> {
if let Some((length, stride)) = lowrate_fft_grid(taps, fs_low, frequencies) {
let mut padded = vec![0.0; length];
padded[..taps.len()].copy_from_slice(taps);
let spectrum = fft::rfft_owned(padded);
let edge = fs_low / 2.0;
return frequencies
.iter()
.enumerate()
.map(|(k, &f)| {
if f >= edge {
C::new(0.0, 0.0)
} else {
spectrum[k * stride] * ((edge - f) / (0.1 * edge)).clamp(0.0, 1.0)
}
})
.collect();
}
lowrate_response_direct(taps, fs_low, frequencies)
}
fn lowrate_fft_grid(taps: &[f64], fs_low: f64, frequencies: &[f64]) -> Option<(usize, usize)> {
let n = frequencies.len().checked_sub(1)?.checked_mul(2)?;
let ratio = 48000.0 / fs_low;
if !n.is_power_of_two()
|| !ratio.is_finite()
|| !(1.0..=u32::MAX as f64).contains(&ratio)
|| ratio.fract() != 0.0
|| taps.iter().any(|v| !v.is_finite())
{
return None;
}
let step = 48000.0 / n as f64;
if !frequencies
.iter()
.enumerate()
.all(|(k, &f)| f == k as f64 * step)
{
return None;
}
let ratio = ratio as usize;
let (mut a, mut b) = (n, ratio);
while b != 0 {
(a, b) = (b, a % b);
}
let length = n / a;
(taps.len() <= length).then_some((length, ratio / a))
}
fn lowrate_response_direct(taps: &[f64], fs_low: f64, frequencies: &[f64]) -> Vec<C> {
let edge = fs_low / 2.0;
frequencies
.iter()
.map(|&f| {
if f >= edge {
return C::new(0.0, 0.0);
}
let mut sum = C::new(0.0, 0.0);
for (k, &tap) in taps.iter().enumerate() {
let phase = (-2.0 * PI * (f * k as f64)) / fs_low;
sum += C::from_polar(1.0, phase) * tap;
}
sum * ((edge - f) / (0.1 * edge)).clamp(0.0, 1.0)
})
.collect()
}
pub fn lowrate_to_48k(taps_low: &[f64], fs_low: f64) -> Vec<f64> {
let Some(n48) = lowrate_len_48k(taps_low.len(), fs_low) else {
return Vec::new();
};
let Some(nfft) = n48
.checked_mul(2)
.and_then(usize::checked_next_power_of_two)
else {
return Vec::new();
};
let frequencies: Vec<_> = (0..=nfft / 2)
.map(|k| k as f64 * (48000.0 / nfft as f64))
.collect();
let mut taps = fft::irfft(&lowrate_response(taps_low, fs_low, &frequencies), nfft);
taps.truncate(n48);
taps
}
#[cfg(test)]
mod tests {
use super::*;
fn taps(n: usize) -> Vec<f64> {
(0..n)
.map(|k| ((k * 37 % 113) as f64 - 56.0) / (k + 1) as f64)
.collect()
}
#[test]
fn lowrate_small_fft_matches_dtft_and_edge_ramp() {
for (fs, n, length, stride) in [
(600.0, 16384, 1024, 5),
(600.0, 32768, 2048, 5),
(600.0, 131072, 8192, 5),
(1000.0, 4096, 256, 3),
] {
let taps = taps(100);
let frequencies: Vec<_> = (0..=n / 2)
.map(|k| k as f64 * (48000.0 / n as f64))
.collect();
assert_eq!(
lowrate_fft_grid(&taps, fs, &frequencies),
Some((length, stride))
);
let got = lowrate_response(&taps, fs, &frequencies);
let expected = lowrate_response_direct(&taps, fs, &frequencies);
for ((a, b), f) in got.iter().zip(&expected).zip(frequencies) {
assert!((*a - *b).norm() < 2e-11, "fs={fs}, n={n}, f={f}");
if f >= fs / 2.0 {
assert_eq!(*a, C::new(0.0, 0.0));
}
}
}
}
#[test]
fn lowrate_nonstandard_grids_keep_direct_accumulation() {
let taps = taps(100);
for (fs, frequencies) in [
(600.0, vec![-12.0, 0.0, 17.3, 269.9, 270.0, 299.9, 300.0]),
(
700.0,
(0..=8192).map(|k| k as f64 * (48000.0 / 16384.0)).collect(),
),
(600.0, vec![0.0, 12000.0, 24000.0]),
] {
assert_eq!(lowrate_fft_grid(&taps, fs, &frequencies), None);
assert_eq!(
lowrate_response(&taps, fs, &frequencies),
lowrate_response_direct(&taps, fs, &frequencies)
);
}
let mut frequencies: Vec<_> = (0..=8192).map(|k| k as f64 * (48000.0 / 16384.0)).collect();
frequencies[10] = f64::from_bits(frequencies[10].to_bits() + 1);
assert_eq!(lowrate_fft_grid(&taps, 600.0, &frequencies), None);
}
#[test]
fn lowrate_materialization_preserves_inverse_and_crop() {
let taps = taps(100);
let frequencies: Vec<_> = (0..=8192).map(|k| k as f64 * (48000.0 / 16384.0)).collect();
let mut expected = fft::irfft(&lowrate_response_direct(&taps, 600.0, &frequencies), 16384);
expected.truncate(8000);
let got = lowrate_to_48k(&taps, 600.0);
assert_eq!(got.len(), 8000);
assert!(got.iter().zip(expected).all(|(a, b)| (a - b).abs() < 1e-12));
for fs in [0.0, -600.0, f64::INFINITY, f64::NAN, f64::MIN_POSITIVE, 1.0] {
assert!(lowrate_to_48k(&taps, fs).is_empty());
}
assert!(lowrate_to_48k(&[], 600.0).is_empty());
assert_eq!(lowrate_len_48k(100, 600.0), Some(8000));
assert_eq!(lowrate_len_48k(usize::MAX, 600.0), None);
}
}