use crate::common::diagnostics::NamErrorCode;
use crate::math::common::AlignedVec;
use crate::math::dsp::fft::FftPlanner;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PhaseType {
Linear,
Minimum,
}
pub const NUM_PHASES: usize = 256;
pub const TAPS_PER_PHASE: usize = 64;
const PROTO_LEN: usize = NUM_PHASES * TAPS_PER_PHASE;
pub struct PolyphaseBank {
coeffs: AlignedVec<f32>,
pub taps_per_phase: usize,
pub group_delay: f64,
pub phase_type: PhaseType,
}
impl PolyphaseBank {
#[inline]
pub fn phase_ptr(&self, phase: usize) -> *const f32 {
debug_assert!(phase < NUM_PHASES);
unsafe { self.coeffs.as_ptr().add(phase * self.taps_per_phase) }
}
#[inline]
pub fn phase_coeffs(&self, phase: usize) -> &[f32] {
debug_assert!(phase < NUM_PHASES);
let start = phase * self.taps_per_phase;
&self.coeffs[start..start + self.taps_per_phase]
}
}
pub fn generate_polyphase_bank(
from_rate: u32,
to_rate: u32,
) -> Result<PolyphaseBank, NamErrorCode> {
let passband_rate = from_rate.min(to_rate) as f64;
let proto_nyq = from_rate as f64 * NUM_PHASES as f64 / 2.0;
let cutoff = 0.95 * passband_rate / 2.0 / proto_nyq;
let proto_f64 = generate_sinc_kaiser(PROTO_LEN, cutoff, 12.0);
let min_phase = to_minimum_phase(&proto_f64);
let centroid = calculate_centroid(&min_phase);
let group_delay = centroid / NUM_PHASES as f64;
let proto_f32: Vec<f32> = min_phase.iter().map(|&x| x as f32).collect();
let mut bank = partition_polyphase(&proto_f32)?;
bank.group_delay = group_delay;
bank.phase_type = PhaseType::Minimum;
Ok(bank)
}
fn generate_sinc_kaiser(length: usize, cutoff: f64, beta: f64) -> Vec<f64> {
let half = (length - 1) as f64 / 2.0;
let i0_beta = bessel_i0(beta);
let mut kernel = Vec::with_capacity(length);
for i in 0..length {
let n = i as f64 - half;
let sinc = if n.abs() < 1e-10 {
cutoff
} else {
let x = std::f64::consts::PI * n * cutoff;
x.sin() / (std::f64::consts::PI * n)
};
let ratio = n / half;
let arg = beta * (1.0 - ratio * ratio).max(0.0).sqrt();
let window = bessel_i0(arg) / i0_beta;
kernel.push(sinc * window);
}
let dc_sum: f64 = kernel.iter().sum();
if dc_sum.abs() > 1e-15 {
for k in &mut kernel {
*k /= dc_sum;
}
}
kernel
}
fn bessel_i0(x: f64) -> f64 {
let mut sum = 1.0_f64;
let mut term = 1.0_f64;
let half_x = x / 2.0;
for k in 1..=20 {
term *= (half_x / k as f64) * (half_x / k as f64);
sum += term;
if term < 1e-15 * sum {
break;
}
}
sum
}
fn calculate_centroid(h: &[f64]) -> f64 {
let mut num = 0.0;
let mut den = 0.0;
for (n, &val) in h.iter().enumerate() {
let energy = val * val;
num += n as f64 * energy;
den += energy;
}
if den > 1e-30 { num / den } else { 0.0 }
}
fn to_minimum_phase(kernel: &[f64]) -> Vec<f64> {
let n_proto = kernel.len();
let n_fft = (4 * n_proto).next_power_of_two();
let planner = FftPlanner::<f64>::new(n_fft);
let mut buf_re = vec![0.0_f64; n_fft];
let mut buf_im = vec![0.0_f64; n_fft];
buf_re[..n_proto].copy_from_slice(kernel);
planner.process(&mut buf_re, &mut buf_im);
let eps = 1e-10_f64;
for i in 0..n_fft {
buf_re[i] = (buf_re[i].hypot(buf_im[i]) + eps).ln();
buf_im[i] = 0.0;
}
planner.process_inverse(&mut buf_re, &mut buf_im);
let half = n_fft / 2;
for item in buf_re.iter_mut().take(half).skip(1) {
*item *= 2.0;
}
for item in buf_re.iter_mut().skip(half + 1) {
*item = 0.0;
}
buf_im.fill(0.0);
planner.process(&mut buf_re, &mut buf_im);
for i in 0..n_fft {
let exp_a = buf_re[i].exp();
let (sin_b, cos_b) = buf_im[i].sin_cos();
buf_re[i] = exp_a * cos_b;
buf_im[i] = exp_a * sin_b;
}
planner.process_inverse(&mut buf_re, &mut buf_im);
buf_re.truncate(n_proto);
buf_re
}
fn partition_polyphase(proto: &[f32]) -> Result<PolyphaseBank, NamErrorCode> {
let taps = TAPS_PER_PHASE;
let total = NUM_PHASES * taps;
let mut coeffs = AlignedVec::new(total, 0.0f32)?;
let gain = NUM_PHASES as f32;
for (n, &coeff) in proto.iter().enumerate() {
let phase = n % NUM_PHASES;
let tap = n / NUM_PHASES;
if tap < taps {
coeffs[phase * taps + (taps - 1 - tap)] = coeff * gain;
}
}
for phase in 0..NUM_PHASES {
let start = phase * taps;
let mut sum = 0.0f32;
for tap in 0..taps {
sum += coeffs[start + tap];
}
if sum.abs() > 1e-9 {
for tap in 0..taps {
coeffs[start + tap] /= sum;
}
}
}
Ok(PolyphaseBank {
coeffs,
taps_per_phase: taps,
group_delay: taps as f64 / 2.0,
phase_type: PhaseType::Linear,
})
}
pub fn generate_polyphase_bank_linear(
from_rate: u32,
to_rate: u32,
) -> Result<PolyphaseBank, NamErrorCode> {
let passband_rate = from_rate.min(to_rate) as f64;
let proto_nyq = from_rate as f64 * NUM_PHASES as f64 / 2.0;
let cutoff = 0.95 * passband_rate / 2.0 / proto_nyq;
let proto_f64 = generate_sinc_kaiser(PROTO_LEN, cutoff, 12.0);
let proto_f32: Vec<f32> = proto_f64.iter().map(|&x| x as f32).collect();
partition_polyphase(&proto_f32)
}
#[doc(hidden)]
pub fn measure_cepstrum_ripple(from_rate: u32, to_rate: u32) -> (f64, f64) {
let passband_rate = from_rate.min(to_rate) as f64;
let proto_nyq = from_rate as f64 * NUM_PHASES as f64 / 2.0;
let cutoff = 0.95 * passband_rate / 2.0 / proto_nyq;
let proto_f64 = generate_sinc_kaiser(PROTO_LEN, cutoff, 12.0);
let min_phase = to_minimum_phase(&proto_f64);
let n_fft = (4usize * PROTO_LEN.max(min_phase.len())).next_power_of_two();
let planner = FftPlanner::<f64>::new(n_fft);
let mut lp_re = vec![0.0f64; n_fft];
let mut lp_im = vec![0.0f64; n_fft];
lp_re[..proto_f64.len()].copy_from_slice(&proto_f64);
planner.process(&mut lp_re, &mut lp_im);
let mut mp_re = vec![0.0f64; n_fft];
let mut mp_im = vec![0.0f64; n_fft];
mp_re[..min_phase.len()].copy_from_slice(&min_phase);
planner.process(&mut mp_re, &mut mp_im);
let mut max_db = 0.0f64;
let mut sum_sq = 0.0f64;
let mut passband_count = 0usize;
let passband_bins = ((cutoff * 0.98) * (n_fft as f64 / 2.0)).ceil() as usize;
let nyq_idx = passband_bins.min(n_fft / 2);
let floor_db: f64 = -60.0;
for i in 0..=nyq_idx {
let lp_mag = lp_re[i].hypot(lp_im[i]).max(1e-30);
let mp_mag = mp_re[i].hypot(mp_im[i]).max(1e-30);
let lp_db = 20.0 * lp_mag.log10();
if lp_db < floor_db {
continue;
}
let diff_db = (lp_db - 20.0 * mp_mag.log10()).abs();
max_db = max_db.max(diff_db);
sum_sq += diff_db * diff_db;
passband_count += 1;
}
let rms_db = (sum_sq / passband_count.max(1) as f64).sqrt();
(max_db, rms_db)
}
#[cfg(test)]
#[path = "sinc_kernel_test.rs"]
mod sinc_kernel_test;