use alloc::vec;
use alloc::vec::Vec;
use super::dotprod::dot_f32;
use super::fir_decimate::design_lowpass;
pub struct PolyphaseResampler {
l: u32,
m: u32,
ntaps: usize,
taps_rev: Vec<Vec<f32>>,
max_depth: usize,
hist_i: Vec<f32>,
hist_q: Vec<f32>,
hist_len: usize,
win_start: usize,
phase: u32,
next_n0: usize,
n_in_abs: Option<usize>,
}
impl PolyphaseResampler {
pub fn new(l: u32, m: u32, ntaps: usize, hist_margin: usize) -> Self {
assert!(ntaps % 2 == 1, "ntaps must be odd for linear phase");
assert!(l > 0 && m > 0, "l and m must be nonzero");
let fc_norm = (1.0 / (2.0 * l as f32)).min(1.0 / (2.0 * m as f32));
let mut h = design_lowpass(ntaps, fc_norm);
for tap in h.iter_mut() {
*tap *= l as f32;
}
let max_depth = ntaps.div_ceil(l as usize);
let mut taps_rev = Vec::with_capacity(l as usize);
for p in 0..l as usize {
let mut phase_taps = vec![0.0f32; max_depth];
let mut j = 0usize;
while p + j * (l as usize) < ntaps {
phase_taps[max_depth - 1 - j] = h[p + j * (l as usize)];
j += 1;
}
taps_rev.push(phase_taps);
}
let hist_cap = max_depth + hist_margin;
let hist_i = vec![0.0f32; hist_cap];
let hist_q = vec![0.0f32; hist_cap];
Self {
l,
m,
ntaps,
taps_rev,
max_depth,
hist_i,
hist_q,
hist_len: max_depth,
win_start: 0,
phase: 0,
next_n0: 0,
n_in_abs: None,
}
}
pub fn group_delay_output(&self) -> usize {
((self.ntaps - 1) / 2) / self.m as usize
}
pub fn push(&mut self, i: f32, q: f32, out_i: &mut Vec<f32>, out_q: &mut Vec<f32>) {
self.hist_i[self.hist_len] = i;
self.hist_q[self.hist_len] = q;
self.hist_len += 1;
self.win_start += 1;
let cur_abs = self.n_in_abs.map_or(0, |a| a + 1);
self.n_in_abs = Some(cur_abs);
while self.next_n0 <= cur_abs {
let back = cur_abs - self.next_n0;
let end = self.win_start + self.max_depth - back;
let start = end - self.max_depth;
let (oi, oq) = self.dot(start);
out_i.push(oi);
out_q.push(oq);
let carry = (self.phase + self.m) / self.l;
self.phase = (self.phase + self.m) % self.l;
self.next_n0 += carry as usize;
}
if self.hist_len == self.hist_i.len() {
self.compact();
}
}
fn compact(&mut self) {
let keep = self.win_start;
self.hist_i.copy_within(keep..self.hist_len, 0);
self.hist_q.copy_within(keep..self.hist_len, 0);
self.hist_len = self.max_depth;
self.win_start = 0;
}
fn dot(&self, start: usize) -> (f32, f32) {
let depth = self.max_depth;
let hi = &self.hist_i[start..start + depth];
let hq = &self.hist_q[start..start + depth];
let h = &self.taps_rev[self.phase as usize][..depth];
(dot_f32(h, hi), dot_f32(h, hq))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use num_traits::Float;
fn tone(freq_hz: f32, fs_hz: f32, amp: f32, n: usize) -> Vec<f32> {
let w = 2.0 * core::f64::consts::PI * freq_hz as f64 / fs_hz as f64;
(0..n)
.map(|k| (amp as f64 * (w * k as f64).cos()) as f32)
.collect()
}
fn run(r: &mut PolyphaseResampler, audio: &[f32]) -> (Vec<f32>, Vec<f32>) {
let mut out_i = Vec::new();
let mut out_q = Vec::new();
for &s in audio {
r.push(s, 0.0, &mut out_i, &mut out_q);
}
(out_i, out_q)
}
#[test]
fn unity_ratio_produces_one_output_per_input() {
let mut r = PolyphaseResampler::new(1, 1, 31, 64);
let n = 2000;
let audio = tone(0.0, 12_000.0, 0.7, n);
let (out_i, out_q) = run(&mut r, &audio);
assert_eq!(out_i.len(), n);
assert_eq!(out_q.len(), n);
let settled = 40..n;
for k in settled {
assert!(
(out_i[k] - 0.7).abs() < 0.01,
"k={k} out_i={} expected ~0.7",
out_i[k]
);
assert!(
out_q[k].abs() < 0.01,
"k={k} out_q={} expected ~0",
out_q[k]
);
}
}
fn ntaps_for_transition(l: u32, fs_in: f32, transition_hz: f32) -> usize {
let n = (4.0 * l as f32 * fs_in / transition_hz).ceil() as usize;
if n.is_multiple_of(2) { n + 1 } else { n }
}
#[test]
fn in_band_tone_amplitude_is_preserved() {
let l = 9u32;
let m = 64u32;
let fs_in = 790.12f32;
let ntaps = ntaps_for_transition(l, fs_in, 20.0);
let mut r = PolyphaseResampler::new(l, m, ntaps, 128);
let n = 20_000;
let audio = tone(10.0, fs_in, 0.5, n);
let (out_i, out_q) = run(&mut r, &audio);
let expected_len = (n as u64 * l as u64 / m as u64) as usize;
assert!(
out_i.len().abs_diff(expected_len) <= 1,
"got {} outputs, expected ~{expected_len}",
out_i.len()
);
let settle = out_i.len() / 4;
let span = settle..(out_i.len() - settle);
let mut peak = 0.0f32;
for k in span {
let mag = (out_i[k] * out_i[k] + out_q[k] * out_q[k]).sqrt();
if mag > peak {
peak = mag;
}
}
assert!(
(peak - 0.5).abs() < 0.05,
"peak magnitude {peak}, expected ~0.5"
);
}
#[test]
fn out_of_band_tone_is_rejected() {
let l = 9u32;
let m = 64u32;
let fs_in = 790.12f32;
let fs_out = fs_in * l as f32 / m as f32; let ntaps = ntaps_for_transition(l, fs_in, 20.0);
let mut in_band = PolyphaseResampler::new(l, m, ntaps, 128);
let mut out_band = PolyphaseResampler::new(l, m, ntaps, 128);
let n = 20_000;
let (i1, q1) = run(&mut in_band, &tone(10.0, fs_in, 0.5, n));
let (i2, q2) = run(&mut out_band, &tone(fs_out, fs_in, 0.5, n));
let rms = |i: &[f32], q: &[f32]| -> f32 {
let settle = i.len() / 4;
let span = settle..(i.len() - settle);
let s: f32 =
span.clone().map(|k| i[k] * i[k] + q[k] * q[k]).sum::<f32>() / span.len() as f32;
s.sqrt()
};
let db = 20.0 * (rms(&i2, &q2) / rms(&i1, &q1)).log10();
assert!(db < -40.0, "out-of-band rejection only {db} dB");
}
#[test]
fn integer_ratio_tracks_fir_stage() {
use super::super::fir_decimate::FirStage;
let decim = 8u32;
let ntaps = 63;
let mut poly = PolyphaseResampler::new(1, decim, ntaps, 128);
let mut fir = FirStage::new(ntaps, decim as usize, 1.0 / (2.0 * decim as f32), 128);
let n = 4000;
let audio = tone(20.0, 12_000.0, 0.6, n);
let (pi, pq) = run(&mut poly, &audio);
let mut fi = Vec::new();
let mut fq = Vec::new();
for &s in &audio {
if let Some((i, q)) = fir.push_one(s, 0.0) {
fi.push(i);
fq.push(q);
}
}
let lead = poly.group_delay_output();
let (pi, pq) = (&pi[lead..], &pq[lead..]);
let len = pi.len().min(fi.len());
let settle = len / 4;
let span = settle..len;
let (mut num_re, mut num_im, mut pf, mut pp) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for k in span {
let (f, p) = ((fi[k], fq[k]), (pi[k], pq[k]));
num_re += (f.0 * p.0 + f.1 * p.1) as f64;
num_im += (f.1 * p.0 - f.0 * p.1) as f64;
pf += (f.0 * f.0 + f.1 * f.1) as f64;
pp += (p.0 * p.0 + p.1 * p.1) as f64;
}
let coh = (num_re * num_re + num_im * num_im).sqrt() / (pf * pp).sqrt();
assert!(coh > 0.99, "coherence with FirStage only {coh}");
}
}