use alloc::boxed::Box;
use alloc::vec::Vec;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use super::super::params::{COSTAS, COSTAS_POS, NSPS};
use super::types::{AudioSample, SAMPLE_RATE_HZ, TONE_SPACING_HZ, TX_START_OFFSET_S};
use crate::engine::sync::SyncCandidate;
const BIN: usize = NSPS / 32;
const CD0_RATE_HZ: f32 = SAMPLE_RATE_HZ / BIN as f32;
const STAGE_A_REACH: i32 = 10;
const STAGE_C_REACH: i32 = 4;
const STAGE_B_STEPS: i32 = 5;
const STAGE_B_STEP_HZ: f32 = 0.5;
const REACH: i32 = STAGE_A_REACH + STAGE_C_REACH;
const NB: usize = 32 + 2 * REACH as usize;
const NSYNC: usize = 21;
struct Scratch {
bins: [[Complex<f32>; NB]; NSYNC],
lo_re: [[f32; BIN]; 8],
lo_im: [[f32; BIN]; 8],
tweak: [[Complex<f32>; 32]; 2 * STAGE_B_STEPS as usize + 1],
}
pub fn fine_sync_12k<S: AudioSample>(audio: &[S], cands: &[SyncCandidate]) -> Vec<SyncCandidate> {
if cands.is_empty() {
return Vec::new();
}
let zero = Complex::new(0.0f32, 0.0);
let mut sc: Box<Scratch> = Box::new(Scratch {
bins: [[zero; NB]; NSYNC],
lo_re: [[0.0; BIN]; 8],
lo_im: [[0.0; BIN]; 8],
tweak: [[zero; 32]; 2 * STAGE_B_STEPS as usize + 1],
});
for (k, row) in sc.tweak.iter_mut().enumerate() {
let delf = (k as i32 - STAGE_B_STEPS) as f32 * STAGE_B_STEP_HZ;
let dphi = core::f32::consts::TAU * delf / CD0_RATE_HZ;
for (m, c) in row.iter_mut().enumerate() {
let ph = dphi * m as f32;
*c = Complex::new(ph.cos(), -ph.sin());
}
}
cands
.iter()
.map(|c| refine_one(audio, c, &mut sc))
.collect()
}
fn refine_one<S: AudioSample>(audio: &[S], c: &SyncCandidate, sc: &mut Scratch) -> SyncCandidate {
let two_pi_over_fs = core::f32::consts::TAU / SAMPLE_RATE_HZ;
let i0 = ((c.dt_sec + TX_START_OFFSET_S) * CD0_RATE_HZ).round() as i32;
let mut rot = [Complex::new(0.0f32, 0.0); 8];
for &tone in COSTAS.iter() {
let w = two_pi_over_fs * (c.freq_hz + tone as f32 * TONE_SPACING_HZ);
for n in 0..BIN {
let ph = w * n as f32;
sc.lo_re[tone][n] = ph.cos();
sc.lo_im[tone][n] = ph.sin();
}
let ph = w * BIN as f32;
rot[tone] = Complex::new(ph.cos(), -ph.sin());
}
let len = audio.len() as i64;
let mut s = 0;
for &block in COSTAS_POS.iter() {
for (k, &tone) in COSTAS.iter().enumerate() {
let first = i0 - REACH + ((block + k) * 32) as i32;
let mut phasor = Complex::new(1.0f32, 0.0);
let (lo_re, lo_im) = (&sc.lo_re[tone], &sc.lo_im[tone]);
for j in 0..NB {
let st = (first as i64 + j as i64) * BIN as i64;
sc.bins[s][j] = if st >= 0 && st + BIN as i64 <= len {
let win = &audio[st as usize..st as usize + BIN];
let (mut re, mut im) = (0.0f32, 0.0f32);
for n in 0..BIN {
let x = win[n].to_f32();
re += x * lo_re[n];
im -= x * lo_im[n];
}
Complex::new(re, im) * phasor
} else {
Complex::new(0.0, 0.0)
};
phasor *= rot[tone];
}
s += 1;
}
}
let bins = &sc.bins;
let sync = |d: i32, tw: Option<&[Complex<f32>; 32]>| -> f32 {
let j0 = (d + REACH) as usize;
let mut p = 0.0f32;
for sym in bins.iter() {
let span = &sym[j0..j0 + 32];
let z = match tw {
None => span.iter().fold(Complex::new(0.0, 0.0), |a, b| a + b),
Some(tw) => span
.iter()
.zip(tw.iter())
.fold(Complex::new(0.0, 0.0), |a, (b, t)| a + b * t),
};
p += z.norm_sqr();
}
p
};
let mut da = -STAGE_A_REACH;
let mut best = f32::MIN;
for d in -STAGE_A_REACH..=STAGE_A_REACH {
let p = sync(d, None);
if p > best {
best = p;
da = d;
}
}
let mut kb = STAGE_B_STEPS as usize;
let mut best = f32::MIN;
for (k, tw) in sc.tweak.iter().enumerate() {
let p = sync(da, Some(tw));
if p > best {
best = p;
kb = k;
}
}
let delf = (kb as i32 - STAGE_B_STEPS) as f32 * STAGE_B_STEP_HZ;
let tw = &sc.tweak[kb];
let mut dc = da;
let mut best = f32::MIN;
for d in da - STAGE_C_REACH..=da + STAGE_C_REACH {
let p = sync(d, Some(tw));
if p > best {
best = p;
dc = d;
}
}
SyncCandidate {
freq_hz: c.freq_hz + delf,
dt_sec: (i0 + dc) as f32 / CD0_RATE_HZ - TX_START_OFFSET_S,
score: c.score,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ft8::wave_gen::{message_to_tones, tones_to_i16};
use alloc::vec;
#[test]
fn recovers_an_off_grid_signal_through_noise() {
const F_TRUE: f32 = 1501.2;
const DT_TRUE: f32 = 0.137;
let msg: Vec<u8> = (0..77).map(|i| ((i * 7 + 3) % 5 == 0) as u8).collect();
let itone = message_to_tones(&msg);
let sig = tones_to_i16(&itone, F_TRUE, 2_000);
let mut audio = vec![0i16; 180_000];
let start = ((TX_START_OFFSET_S + DT_TRUE) * SAMPLE_RATE_HZ).round() as usize;
let mut state: u32 = 0x1234_5678;
for (i, a) in audio.iter_mut().enumerate() {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
let noise = (state % 8_001) as i32 - 4_000;
let s = if i >= start && i - start < sig.len() {
sig[i - start] as i32
} else {
0
};
*a = (s + noise).clamp(-32_768, 32_767) as i16;
}
let coarse = SyncCandidate {
freq_hz: 1_500.0,
dt_sec: 0.10,
score: 1.0,
};
let r = fine_sync_12k(&audio, &[coarse]).remove(0);
assert!(
(r.freq_hz - F_TRUE).abs() <= STAGE_B_STEP_HZ,
"freq {} vs {F_TRUE}",
r.freq_hz
);
assert!(
(r.dt_sec - DT_TRUE).abs() <= 1.0 / CD0_RATE_HZ,
"dt {} vs {DT_TRUE}",
r.dt_sec
);
assert_eq!(r.score, 1.0);
}
#[test]
fn empty_input_allocates_nothing_and_returns_nothing() {
let audio = [0i16; 16];
assert!(fine_sync_12k(&audio, &[]).is_empty());
}
}