use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::engine::dsp::fir_decimate::{FirStage, design_lowpass};
use super::baseband::{NFFT1, NFFT2, NPOINTS_MAX};
pub const AUDIO_RATE_HZ: f32 = 12_000.0;
pub const DECIM: usize = 32;
pub const NTAPS: usize = 1793;
pub const GROUP_DELAY: usize = (NTAPS - 1) / 2;
pub const REFERENCE_GAIN: f32 = NFFT1 as f32 / 1000.0;
fn mixer_table() -> [(f32, f32); 8] {
let mut t = [(0.0f32, 0.0f32); 8];
for (n, e) in t.iter_mut().enumerate() {
let phi = -core::f32::consts::PI * n as f32 / 4.0;
*e = (phi.cos(), phi.sin());
}
t
}
pub struct StreamingDdc<'a> {
taps_rev: &'a mut [f32],
mixer: [(f32, f32); 8],
hist_i: &'a mut [f32],
hist_q: &'a mut [f32],
hist_len: usize,
win_start: usize,
n_in: usize,
to_next_out: usize,
}
pub const HIST_LEN: usize = NTAPS + 512;
pub struct DdcBufs {
taps: alloc::vec::Vec<f32>,
hist_i: alloc::vec::Vec<f32>,
hist_q: alloc::vec::Vec<f32>,
}
impl Default for DdcBufs {
fn default() -> Self {
Self::new()
}
}
impl DdcBufs {
pub fn new() -> Self {
Self {
taps: vec![0.0; NTAPS],
hist_i: vec![0.0; HIST_LEN],
hist_q: vec![0.0; HIST_LEN],
}
}
pub fn as_parts(&mut self) -> (&mut [f32], &mut [f32], &mut [f32]) {
(&mut self.taps, &mut self.hist_i, &mut self.hist_q)
}
}
impl<'a> StreamingDdc<'a> {
pub fn new_in(taps: &'a mut [f32], hist_i: &'a mut [f32], hist_q: &'a mut [f32]) -> Self {
assert_eq!(taps.len(), NTAPS, "taps must be NTAPS long");
assert_eq!(hist_i.len(), HIST_LEN, "hist_i must be HIST_LEN long");
assert_eq!(hist_q.len(), HIST_LEN, "hist_q must be HIST_LEN long");
let designed = design_lowpass(NTAPS, 1.0 / (2.0 * DECIM as f32));
let last = NTAPS - 1;
for k in 0..NTAPS {
taps[k] = designed[last - k];
}
hist_i[..NTAPS].fill(0.0);
hist_q[..NTAPS].fill(0.0);
Self {
taps_rev: taps,
mixer: mixer_table(),
hist_i,
hist_q,
hist_len: NTAPS,
win_start: 0,
n_in: 0,
to_next_out: GROUP_DELAY + 1,
}
}
pub fn push(&mut self, audio: &[f32], out_i: &mut Vec<f32>, out_q: &mut Vec<f32>) {
for &s in audio {
let (c, sn) = self.mixer[self.n_in % 8];
self.hist_i[self.hist_len] = s * c;
self.hist_q[self.hist_len] = s * sn;
self.hist_len += 1;
self.win_start += 1;
self.n_in += 1;
self.to_next_out -= 1;
if self.to_next_out == 0 {
self.to_next_out = DECIM;
let (i, q) = self.dot();
out_i.push(i * REFERENCE_GAIN);
out_q.push(q * REFERENCE_GAIN);
}
if self.hist_len == HIST_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 = NTAPS;
self.win_start = 0;
}
pub fn flush(&mut self, out_i: &mut Vec<f32>, out_q: &mut Vec<f32>) {
let zeros = vec![0.0f32; GROUP_DELAY];
self.push(&zeros, out_i, out_q);
}
fn dot(&self) -> (f32, f32) {
let a = self.win_start;
let hi = &self.hist_i[a..a + NTAPS];
let hq = &self.hist_q[a..a + NTAPS];
let h = &self.taps_rev[..];
let mut ai = [0.0f32; 4];
let mut aq = [0.0f32; 4];
let chunks = NTAPS / 4;
for c in 0..chunks {
let b = c * 4;
for l in 0..4 {
ai[l] += h[b + l] * hi[b + l];
aq[l] += h[b + l] * hq[b + l];
}
}
let mut si = ai[0] + ai[1] + ai[2] + ai[3];
let mut sq = aq[0] + aq[1] + aq[2] + aq[3];
for k in chunks * 4..NTAPS {
si += h[k] * hi[k];
sq += h[k] * hq[k];
}
(si, sq)
}
}
pub const CASCADE_N1: usize = 43;
pub const CASCADE_DECIM1: usize = 8;
pub const CASCADE_N2: usize = 223;
pub const CASCADE_DECIM2: usize = 4;
const CASCADE_HIST_MARGIN: usize = 256;
pub struct StreamingDdcCascade {
mixer: [(f32, f32); 8],
n_in: usize,
stage1: FirStage,
stage2: FirStage,
}
impl StreamingDdcCascade {
pub fn new() -> Self {
assert_eq!(
CASCADE_DECIM1 * CASCADE_DECIM2,
DECIM,
"cascade decimation must match DECIM"
);
Self {
mixer: mixer_table(),
n_in: 0,
stage1: FirStage::new(
CASCADE_N1,
CASCADE_DECIM1,
700.0 / AUDIO_RATE_HZ,
CASCADE_HIST_MARGIN,
),
stage2: FirStage::new(
CASCADE_N2,
CASCADE_DECIM2,
187.5 / (AUDIO_RATE_HZ / CASCADE_DECIM1 as f32),
CASCADE_HIST_MARGIN,
),
}
}
pub fn push(&mut self, audio: &[f32], out_i: &mut Vec<f32>, out_q: &mut Vec<f32>) {
for &s in audio {
let (c, sn) = self.mixer[self.n_in % 8];
self.n_in += 1;
let stage1_out = self.stage1.push_one(s * c, s * sn);
if let Some((i2, q2)) = stage1_out.and_then(|(i1, q1)| self.stage2.push_one(i1, q1)) {
out_i.push(i2 * REFERENCE_GAIN);
out_q.push(q2 * REFERENCE_GAIN);
}
}
}
pub fn flush(&mut self, out_i: &mut Vec<f32>, out_q: &mut Vec<f32>) {
let group_delay1 = (CASCADE_N1 - 1) / 2;
let group_delay2 = (CASCADE_N2 - 1) / 2;
let zeros = vec![0.0f32; group_delay1 + group_delay2 * CASCADE_DECIM1 + 1];
self.push(&zeros, out_i, out_q);
}
}
impl Default for StreamingDdcCascade {
fn default() -> Self {
Self::new()
}
}
pub fn ddc_to_baseband_cascade(audio: &[f32]) -> (Vec<f32>, Vec<f32>) {
let n_in = audio.len().min(NPOINTS_MAX);
let mut ddc = StreamingDdcCascade::new();
let mut idat = Vec::with_capacity(NFFT2);
let mut qdat = Vec::with_capacity(NFFT2);
ddc.push(&audio[..n_in], &mut idat, &mut qdat);
ddc.flush(&mut idat, &mut qdat);
idat.resize(NFFT2, 0.0);
qdat.resize(NFFT2, 0.0);
idat.truncate(NFFT2);
qdat.truncate(NFFT2);
(idat, qdat)
}
pub fn ddc_to_baseband(audio: &[f32]) -> (Vec<f32>, Vec<f32>) {
let n_in = audio.len().min(NPOINTS_MAX);
let mut bufs = DdcBufs::new();
let (t, hi, hq) = bufs.as_parts();
let mut ddc = StreamingDdc::new_in(t, hi, hq);
let mut idat = Vec::with_capacity(NFFT2);
let mut qdat = Vec::with_capacity(NFFT2);
ddc.push(&audio[..n_in], &mut idat, &mut qdat);
ddc.flush(&mut idat, &mut qdat);
idat.resize(NFFT2, 0.0);
qdat.resize(NFFT2, 0.0);
idat.truncate(NFFT2);
qdat.truncate(NFFT2);
(idat, qdat)
}
#[cfg(test)]
mod tests {
use super::super::baseband::{BASEBAND_RATE, CENTER_HZ, decimate_to_baseband};
use super::*;
fn tone(freq_hz: f32, amp: f32, n: usize) -> Vec<f32> {
let w = 2.0 * core::f64::consts::PI * freq_hz as f64 / AUDIO_RATE_HZ as f64;
(0..n)
.map(|k| (amp as f64 * (w * k as f64).cos()) as f32)
.collect()
}
#[test]
fn output_shape_matches_the_reference() {
let audio = tone(CENTER_HZ, 0.5, 240_000);
let (i, q) = ddc_to_baseband(&audio);
assert_eq!(i.len(), NFFT2);
assert_eq!(q.len(), NFFT2);
assert!(i.iter().all(|v| v.is_finite()));
assert!(q.iter().all(|v| v.is_finite()));
}
#[test]
fn centre_tone_lands_at_dc_with_reference_amplitude() {
let amp = 0.5f32;
let n = 480_000; let audio = tone(CENTER_HZ, amp, n);
let (i, q) = ddc_to_baseband(&audio);
let mid = 7_000;
let mag = (i[mid] * i[mid] + q[mid] * q[mid]).sqrt();
let expect = amp / 2.0 * REFERENCE_GAIN;
assert!(
(mag - expect).abs() / expect < 0.02,
"|y| = {mag}, expected ~{expect}"
);
}
#[test]
fn offset_tone_becomes_a_rotating_phasor_at_the_offset() {
let offset = 40.0f32;
let n = 480_000;
let audio = tone(CENTER_HZ + offset, 0.5, n);
let (i, q) = ddc_to_baseband(&audio);
let a = 5_000usize;
let b = a + 1000;
let ph = |k: usize| q[k].atan2(i[k]);
let mut d = ph(b) - ph(a);
let expect_total = 2.0 * core::f32::consts::PI * offset * (b - a) as f32 / BASEBAND_RATE;
while d - expect_total > core::f32::consts::PI {
d -= 2.0 * core::f32::consts::PI;
}
while expect_total - d > core::f32::consts::PI {
d += 2.0 * core::f32::consts::PI;
}
let measured_hz = d / (2.0 * core::f32::consts::PI) * BASEBAND_RATE / (b - a) as f32;
assert!(
(measured_hz - offset).abs() < 0.5,
"measured {measured_hz} Hz, expected {offset}"
);
}
#[test]
fn out_of_band_tone_is_rejected() {
let n = 480_000;
let in_band = ddc_to_baseband(&tone(CENTER_HZ, 0.5, n));
let out_band = ddc_to_baseband(&tone(CENTER_HZ + 400.0, 0.5, n));
let rms = |(i, q): &(Vec<f32>, Vec<f32>)| -> f32 {
let span = 5_000..10_000;
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(&out_band) / rms(&in_band)).log10();
assert!(db < -60.0, "out-of-band rejection only {db} dB");
}
#[test]
fn tracks_the_reference_channelizer_on_a_multi_tone() {
let n = NPOINTS_MAX;
let mut audio = vec![0.0f32; n];
for (f, a) in [
(CENTER_HZ - 120.0, 0.30),
(CENTER_HZ - 20.0, 0.20),
(CENTER_HZ + 75.0, 0.25),
(CENTER_HZ + 600.0, 0.40), ] {
let w = 2.0 * core::f64::consts::PI * f as f64 / AUDIO_RATE_HZ as f64;
for (k, s) in audio.iter_mut().enumerate() {
*s += (a * (w * k as f64).cos()) as f32;
}
}
let (ri, rq) = decimate_to_baseband(&audio);
let (di, dq) = ddc_to_baseband(&audio);
let span = 2_000..40_000;
let (mut num_re, mut num_im, mut pr, mut pd) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for k in span {
let (r, d) = ((ri[k], rq[k]), (di[k], dq[k]));
num_re += (r.0 * d.0 + r.1 * d.1) as f64;
num_im += (r.1 * d.0 - r.0 * d.1) as f64;
pr += (r.0 * r.0 + r.1 * r.1) as f64;
pd += (d.0 * d.0 + d.1 * d.1) as f64;
}
let coh = (num_re * num_re + num_im * num_im).sqrt() / (pr * pd).sqrt();
assert!(coh > 0.99, "coherence with the reference only {coh}");
let ratio = (pd / pr).sqrt();
assert!(
(ratio - 1.0).abs() < 0.05,
"amplitude ratio ddc/reference = {ratio}"
);
}
#[test]
fn cascade_output_shape_matches_the_reference() {
let audio = tone(CENTER_HZ, 0.5, 240_000);
let (i, q) = ddc_to_baseband_cascade(&audio);
assert_eq!(i.len(), NFFT2);
assert_eq!(q.len(), NFFT2);
assert!(i.iter().all(|v| v.is_finite()));
assert!(q.iter().all(|v| v.is_finite()));
}
#[test]
fn cascade_centre_tone_lands_at_dc_with_reference_amplitude() {
let amp = 0.5f32;
let n = 480_000;
let audio = tone(CENTER_HZ, amp, n);
let (i, q) = ddc_to_baseband_cascade(&audio);
let mid = 7_000;
let mag = (i[mid] * i[mid] + q[mid] * q[mid]).sqrt();
let expect = amp / 2.0 * REFERENCE_GAIN;
assert!(
(mag - expect).abs() / expect < 0.02,
"|y| = {mag}, expected ~{expect}"
);
}
#[test]
fn cascade_offset_tone_becomes_a_rotating_phasor_at_the_offset() {
let offset = 40.0f32;
let n = 480_000;
let audio = tone(CENTER_HZ + offset, 0.5, n);
let (i, q) = ddc_to_baseband_cascade(&audio);
let a = 5_000usize;
let b = a + 1000;
let ph = |k: usize| q[k].atan2(i[k]);
let mut d = ph(b) - ph(a);
let expect_total = 2.0 * core::f32::consts::PI * offset * (b - a) as f32 / BASEBAND_RATE;
while d - expect_total > core::f32::consts::PI {
d -= 2.0 * core::f32::consts::PI;
}
while expect_total - d > core::f32::consts::PI {
d += 2.0 * core::f32::consts::PI;
}
let measured_hz = d / (2.0 * core::f32::consts::PI) * BASEBAND_RATE / (b - a) as f32;
assert!(
(measured_hz - offset).abs() < 0.5,
"measured {measured_hz} Hz, expected {offset}"
);
}
#[test]
fn cascade_out_of_band_tone_is_rejected() {
let n = 480_000;
let in_band = ddc_to_baseband_cascade(&tone(CENTER_HZ, 0.5, n));
let out_band = ddc_to_baseband_cascade(&tone(CENTER_HZ + 400.0, 0.5, n));
let rms = |(i, q): &(Vec<f32>, Vec<f32>)| -> f32 {
let span = 5_000..10_000;
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(&out_band) / rms(&in_band)).log10();
assert!(db < -60.0, "out-of-band rejection only {db} dB");
}
#[test]
fn cascade_tracks_the_reference_channelizer_on_a_multi_tone() {
let n = NPOINTS_MAX;
let mut audio = vec![0.0f32; n];
for (f, a) in [
(CENTER_HZ - 120.0, 0.30),
(CENTER_HZ - 20.0, 0.20),
(CENTER_HZ + 75.0, 0.25),
(CENTER_HZ + 600.0, 0.40), ] {
let w = 2.0 * core::f64::consts::PI * f as f64 / AUDIO_RATE_HZ as f64;
for (k, s) in audio.iter_mut().enumerate() {
*s += (a * (w * k as f64).cos()) as f32;
}
}
let (ri, rq) = decimate_to_baseband(&audio);
let (di, dq) = ddc_to_baseband_cascade(&audio);
let span = 2_000..40_000;
let (mut num_re, mut num_im, mut pr, mut pd) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for k in span {
let (r, d) = ((ri[k], rq[k]), (di[k], dq[k]));
num_re += (r.0 * d.0 + r.1 * d.1) as f64;
num_im += (r.1 * d.0 - r.0 * d.1) as f64;
pr += (r.0 * r.0 + r.1 * r.1) as f64;
pd += (d.0 * d.0 + d.1 * d.1) as f64;
}
let coh = (num_re * num_re + num_im * num_im).sqrt() / (pr * pd).sqrt();
assert!(coh > 0.99, "coherence with the reference only {coh}");
let ratio = (pd / pr).sqrt();
assert!(
(ratio - 1.0).abs() < 0.05,
"amplitude ratio ddc/reference = {ratio}"
);
}
#[test]
fn cascade_tracks_the_single_stage_implementation() {
let n = NPOINTS_MAX;
let mut audio = vec![0.0f32; n];
for (f, a) in [
(CENTER_HZ - 120.0, 0.30),
(CENTER_HZ - 20.0, 0.20),
(CENTER_HZ + 75.0, 0.25),
(CENTER_HZ + 600.0, 0.40),
] {
let w = 2.0 * core::f64::consts::PI * f as f64 / AUDIO_RATE_HZ as f64;
for (k, s) in audio.iter_mut().enumerate() {
*s += (a * (w * k as f64).cos()) as f32;
}
}
let (si, sq) = ddc_to_baseband(&audio);
let (ci, cq) = ddc_to_baseband_cascade(&audio);
let span = 2_000..40_000;
let (mut num_re, mut num_im, mut ps, mut pc) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for k in span {
let (s, c) = ((si[k], sq[k]), (ci[k], cq[k]));
num_re += (s.0 * c.0 + s.1 * c.1) as f64;
num_im += (s.1 * c.0 - s.0 * c.1) as f64;
ps += (s.0 * s.0 + s.1 * s.1) as f64;
pc += (c.0 * c.0 + c.1 * c.1) as f64;
}
let coh = (num_re * num_re + num_im * num_im).sqrt() / (ps * pc).sqrt();
assert!(coh > 0.99, "cascade vs. single-stage coherence only {coh}");
}
}