use crate::{Complex64 as C, DspError, fft, fir, invalid};
use std::f64::consts::PI;
pub const DESIGN_RATE: u32 = 48_000;
pub const OUTPUT_RATES: [u32; 6] = [44_100, 48_000, 88_200, 96_000, 176_400, 192_000];
pub fn is_output_rate(rate: u32) -> bool {
OUTPUT_RATES.contains(&rate)
}
pub fn taps_at(taps_48k: usize, rate: u32) -> usize {
if taps_48k == 0 {
return 0;
}
let scaled = (taps_48k as u128 * u128::from(rate) + 24_000) / 48_000;
scaled.clamp(1, usize::MAX as u128) as usize
}
pub fn delay_samples(ms: f64, rate: u32) -> i64 {
(ms * f64::from(rate) / 1000.0).round_ties_even() as i64
}
pub fn fir_response(taps: &[f64], rate: u32, frequencies: &[f64]) -> Vec<C> {
frequencies
.iter()
.map(|&f| {
let mut value = C::new(0.0, 0.0);
for (n, &tap) in taps.iter().enumerate() {
value += C::from_polar(1.0, -2.0 * PI * f * n as f64 / f64::from(rate)) * tap;
}
value
})
.collect()
}
fn check(taps: &[f64], rate: u32) -> Result<(), DspError> {
if !is_output_rate(rate) {
return Err(invalid("unsupported output sample rate"));
}
if taps.iter().any(|tap| !tap.is_finite()) {
return Err(invalid("non-finite FIR coefficient"));
}
Ok(())
}
fn response_grid(taps: &[f64], step: f64, bins: usize) -> Result<Vec<C>, DspError> {
let length = taps
.len()
.checked_add(bins - 1)
.and_then(usize::checked_next_power_of_two)
.ok_or_else(|| invalid("FIR transform length overflow"))?;
let angle = PI * step / f64::from(DESIGN_RATE);
let chirp = |n: usize| C::from_polar(1.0, angle * n as f64 * n as f64);
let mut a = vec![C::default(); length];
let mut b = vec![C::default(); length];
for (n, &tap) in taps.iter().enumerate() {
a[n] = chirp(n).conj() * tap;
}
for (n, value) in b[..bins].iter_mut().enumerate() {
*value = chirp(n);
}
for n in 1..taps.len() {
b[length - n] = chirp(n);
}
let mut a = fft::fft(&a);
for (value, kernel) in a.iter_mut().zip(fft::fft(&b)) {
*value *= kernel;
}
let mut result = fft::ifft(&a);
result.truncate(bins);
for (n, value) in result.iter_mut().enumerate() {
*value *= chirp(n).conj();
}
Ok(result)
}
fn peak_time(taps: &[f64]) -> f64 {
let mut peak = 0;
for i in 1..taps.len() {
if taps[i].abs() > taps[peak].abs() {
peak = i;
}
}
let mut offset = 0.0;
let center = taps[peak].abs();
if peak > 0 && peak + 1 < taps.len() && center > 0.0 {
let left = taps[peak - 1].abs() / center;
let right = taps[peak + 1].abs() / center;
let curvature = left - 2.0 + right;
if curvature != 0.0 {
offset = (0.5 * (left - right) / curvature).clamp(-0.5, 0.5);
}
}
(peak as f64 + offset) / f64::from(DESIGN_RATE)
}
pub fn fir_to_rate(taps_48k: &[f64], rate: u32) -> Result<Vec<f64>, DspError> {
check(taps_48k, rate)?;
if rate == DESIGN_RATE || taps_48k.is_empty() {
return Ok(taps_48k.to_vec());
}
let length = taps_at(taps_48k.len(), rate);
let nfft = length
.checked_mul(64)
.map(|n| n.max(taps_at(1 << 17, rate)))
.and_then(usize::checked_next_power_of_two)
.ok_or_else(|| invalid("FIR transform length overflow"))?;
rebuild(taps_48k, rate, length, nfft)
}
fn rebuild(taps_48k: &[f64], rate: u32, length: usize, nfft: usize) -> Result<Vec<f64>, DspError> {
let step = f64::from(rate) / nfft as f64;
let end = 23_000.0_f64.min(f64::from(rate) / 2.0);
let bins = (end / step).floor() as usize + 1;
let mut spectrum = response_grid(taps_48k, step, bins)?;
let magnitude = fir_response(taps_48k, DESIGN_RATE, &[20_000.0])[0].norm();
let time = peak_time(taps_48k);
let extension = |f: f64| C::from_polar(magnitude, -2.0 * PI * f * time);
for (k, value) in spectrum.iter_mut().enumerate() {
let f = k as f64 * step;
if f > 20_000.0 {
let weight = 0.5 * (1.0 - (PI * (f - 20_000.0) / (end - 20_000.0)).cos());
*value = *value * (1.0 - weight) + extension(f) * weight;
}
}
spectrum.extend((bins..=nfft / 2).map(|k| extension(k as f64 * step)));
let mut result = fft::irfft(&spectrum, nfft);
result.truncate(length);
if result.iter().any(|value| !value.is_finite()) {
return Err(invalid("non-finite output FIR coefficient"));
}
Ok(result)
}
pub fn lowrate_to_rate(taps_low: &[f64], fs_low: f64, rate: u32) -> Result<Vec<f64>, DspError> {
check(taps_low, rate)?;
if fs_low > f64::from(DESIGN_RATE) || fir::lowrate_len_48k(taps_low.len(), fs_low).is_none() {
return Err(invalid("invalid or oversized low-rate FIR recipe"));
}
if rate == DESIGN_RATE {
return Ok(fir::lowrate_to_48k(taps_low, fs_low));
}
let length = (taps_low.len() as f64 * (f64::from(rate) / fs_low)).ceil() as usize;
let nfft = length
.checked_mul(2)
.and_then(usize::checked_next_power_of_two)
.ok_or_else(|| invalid("low-rate FIR transform length overflow"))?;
let frequencies: Vec<_> = (0..=nfft / 2)
.map(|k| k as f64 * (f64::from(rate) / nfft as f64))
.collect();
let mut result = fft::irfft(&fir::lowrate_response(taps_low, fs_low, &frequencies), nfft);
result.truncate(length);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn output_rate_peak_time_uses_absolute_parabolic_peak() {
for (taps, expected) in [
(vec![1.0, 0.5, 0.0], 0.0),
(vec![0.0, 0.5, 1.0], 2.0),
(vec![0.0, -1.0, 0.5], 1.0 + 1.0 / 6.0),
(vec![0.5, -1.0, 0.5], 1.0),
(vec![0.0; 3], 0.0),
(vec![0.0, 1e300, -5e299], 1.0 + 1.0 / 6.0),
] {
assert!((peak_time(&taps) * 48000.0 - expected).abs() < 1e-14);
}
}
#[test]
fn output_rate_inverse_period_converges_without_time_shift() {
for length in [2048, 16384] {
let mut taps = vec![0.0; length];
taps[length / 4] = 1.0;
for rate in OUTPUT_RATES.into_iter().filter(|&rate| rate != DESIGN_RATE) {
let count = taps_at(length, rate);
let nfft = (64 * count).max(taps_at(1 << 17, rate)).next_power_of_two();
let normal = rebuild(&taps, rate, count, nfft).unwrap();
let longer = rebuild(&taps, rate, count, 2 * nfft).unwrap();
let bound: f64 = normal.iter().zip(&longer).map(|(a, b)| (a - b).abs()).sum();
eprintln!(
"RATE PERIOD taps={length} rate={rate} FFT={nfft} vs {}: DTFT absolute bound={bound:e}",
2 * nfft
);
assert!(bound < 1e-4, "inverse period has not converged: {bound}");
}
}
}
#[test]
fn chirp_grid_matches_direct_dtft() {
let taps: Vec<_> = (0..127)
.map(|i| ((i * 17 % 31) as f64 - 15.0) / 31.0)
.collect();
for step in [0.18310546875, 0.336456298828125, 13.7] {
let f: Vec<_> = (0..1003).map(|k| k as f64 * step).collect();
let got = response_grid(&taps, step, f.len()).unwrap();
for (got, expected) in got.iter().zip(fir_response(&taps, DESIGN_RATE, &f)) {
assert!((*got - expected).norm() < 1e-10);
}
}
}
}