use alloc::vec;
use alloc::vec::Vec;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
fn fine_sync_power_signed(cd0: &[Complex<f32>], i: i32) -> f32 {
use crate::ft8::params::COSTAS;
const DS_SPB: i32 = 32;
let icos7: [u8; 7] = [3, 1, 4, 0, 6, 5, 2];
debug_assert_eq!(icos7.len(), COSTAS.len());
let np2 = cd0.len() as i32;
let mut total = 0.0_f32;
for block_off in [0_i32, 36, 72].iter().copied() {
let mut block_power = 0.0_f32;
for (k, &tone) in icos7.iter().enumerate() {
let start = i + block_off * DS_SPB + (k as i32) * DS_SPB;
if start < 0 || start + DS_SPB > np2 {
continue;
}
let dphi = core::f32::consts::TAU * (tone as f32) / (DS_SPB as f32);
let mut z = Complex::new(0.0_f32, 0.0);
let mut phi = 0.0_f32;
for j in 0..DS_SPB {
let s = cd0[start as usize + j as usize];
let r = Complex::new(phi.cos(), phi.sin());
z += s * r.conj();
phi += dphi;
if phi > core::f32::consts::PI {
phi -= core::f32::consts::TAU;
}
}
block_power += z.norm_sqr();
}
total += block_power;
}
total
}
pub const CD0_LEN: usize = 3200;
pub const DS_RATE: f32 = 200.0;
#[derive(Debug, Clone, Copy)]
pub struct FineRefine {
pub dt_sec: f32,
pub delf_hz: f32,
pub score: f32,
}
fn shift_freq(cd0: &[Complex<f32>], delf_hz: f32, out: &mut [Complex<f32>]) {
debug_assert_eq!(cd0.len(), out.len());
let dt2 = 1.0 / DS_RATE;
for (k, (c, o)) in cd0.iter().zip(out.iter_mut()).enumerate() {
let phi = -core::f32::consts::TAU * delf_hz * (k as f32) * dt2;
let rot = Complex::new(phi.cos(), phi.sin());
*o = *c * rot;
}
}
pub fn fine_refine_3stage(cd0: &[Complex<f32>], initial_dt_sec: f32) -> FineRefine {
debug_assert_eq!(cd0.len(), CD0_LEN);
let i0 = ((initial_dt_sec + 0.5) * DS_RATE).round() as i32;
let mut ibest_a = i0;
let mut smax_a = f32::MIN;
for delta in -10..=10_i32 {
let i = i0 + delta;
let s = fine_sync_power_signed(cd0, i);
if s > smax_a {
smax_a = s;
ibest_a = i;
}
}
let mut delfbest = 0.0_f32;
let mut smax_b = fine_sync_power_signed(cd0, ibest_a);
let mut tmp: Vec<Complex<f32>> = vec![Complex::new(0.0, 0.0); cd0.len()];
for ifr in -5..=5_i32 {
if ifr == 0 {
continue;
}
let delf = ifr as f32 * 0.5;
shift_freq(cd0, delf, &mut tmp);
let s = fine_sync_power_signed(&tmp, ibest_a);
if s > smax_b {
smax_b = s;
delfbest = delf;
}
}
if delfbest.abs() > f32::EPSILON {
shift_freq(cd0, delfbest, &mut tmp);
} else {
tmp.copy_from_slice(cd0);
}
let mut ibest_c = ibest_a;
let mut smax_c = f32::MIN;
for delta in -4..=4_i32 {
let i = ibest_a + delta;
let s = fine_sync_power_signed(&tmp, i);
if s > smax_c {
smax_c = s;
ibest_c = i;
}
}
let dt_sec = (ibest_c as f32) / DS_RATE - 0.5;
FineRefine {
dt_sec,
delf_hz: delfbest,
score: smax_c,
}
}
#[cfg(all(test, feature = "fft-rustfft"))]
mod tests {
use super::*;
use crate::ft8::downsample::downsample;
use crate::ft8::params::COSTAS;
use crate::ft8::wave_gen::tones_to_f32;
fn costas_only_tones() -> [u8; 79] {
let mut t = [0u8; 79];
for (i, &c) in COSTAS.iter().enumerate() {
t[i] = c as u8; t[36 + i] = c as u8; t[72 + i] = c as u8; }
t
}
fn synth_slot(freq_hz: f32, dt_sec: f32) -> Vec<i16> {
let tones = costas_only_tones();
let pcm_f32 = tones_to_f32(&tones, freq_hz, 0.5);
let mut slot = vec![0i16; 15 * 12_000];
let start = ((0.5 + dt_sec) * 12_000.0).round() as isize;
for (i, &s) in pcm_f32.iter().enumerate() {
let dst = start + i as isize;
if (0..slot.len() as isize).contains(&dst) {
slot[dst as usize] = (s * 16_000.0) as i16;
}
}
slot
}
#[test]
fn freq_snap_zero_offset() {
let slot = synth_slot(1500.0, 0.0);
let (cd0, _) = downsample(&slot, 1500.0, None);
let r = fine_refine_3stage(&cd0, 0.0);
assert!(
r.delf_hz.abs() <= 0.5,
"expected |delf| ≤ 0.5, got {}",
r.delf_hz,
);
assert!(
r.dt_sec.abs() <= 0.02,
"expected |dt| ≤ 20 ms, got {}",
r.dt_sec,
);
assert!(r.score > 0.0, "score should be positive on signal");
}
#[test]
fn freq_snap_positive_offset() {
let slot = synth_slot(1500.7, 0.0);
let (cd0, _) = downsample(&slot, 1500.0, None);
let r = fine_refine_3stage(&cd0, 0.0);
let close_to_grid = (r.delf_hz - 0.5).abs() < 0.01 || (r.delf_hz - 1.0).abs() < 0.01;
assert!(
close_to_grid,
"expected delf snap to +0.5 or +1.0, got {}",
r.delf_hz,
);
}
#[test]
fn freq_snap_negative_offset() {
let slot = synth_slot(1500.0 - 1.3, 0.0);
let (cd0, _) = downsample(&slot, 1500.0, None);
let r = fine_refine_3stage(&cd0, 0.0);
let close_to_grid = (r.delf_hz - (-1.5)).abs() < 0.01 || (r.delf_hz - (-1.0)).abs() < 0.01;
assert!(
close_to_grid,
"expected delf snap to -1.5 or -1.0, got {}",
r.delf_hz,
);
}
#[test]
fn dt_snap_positive() {
let slot = synth_slot(1500.0, 0.04);
let (cd0, _) = downsample(&slot, 1500.0, None);
let r = fine_refine_3stage(&cd0, 0.0);
assert!(
(r.dt_sec - 0.04).abs() <= 0.015,
"expected dt ≈ 0.04, got {}",
r.dt_sec,
);
}
#[test]
fn no_signal_low_score() {
let slot_signal = synth_slot(1500.0, 0.0);
let (cd0_signal, _) = downsample(&slot_signal, 1500.0, None);
let s_signal = fine_refine_3stage(&cd0_signal, 0.0).score;
let slot_noise = vec![0i16; 15 * 12_000];
let (cd0_noise, _) = downsample(&slot_noise, 1500.0, None);
let s_noise = fine_refine_3stage(&cd0_noise, 0.0).score;
assert!(
s_signal > 5.0 * s_noise,
"signal score {} should dominate noise score {}",
s_signal,
s_noise,
);
}
}