use alloc::vec;
use alloc::vec::Vec;
use core::f32::consts::PI;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::core::fft::default_planner;
use super::WSPR_SYNC_VECTOR;
use super::baseband::{BASEBAND_RATE, CENTER_HZ};
use super::demod::TONE_SPACING_HZ;
const NFFT: usize = 512;
const STRIDE: usize = 128;
const WORKING_BINS: usize = 411;
const MIN_SNR_LIN: f32 = 0.158_489_32; const SNR_SCALING_DB: f32 = 26.3;
#[derive(Clone, Copy, Debug)]
pub struct BasebandCandidate {
pub start_sample: usize,
pub freq_hz: f32,
pub drift_hz: f32,
pub sync: f32,
pub snr_db: f32,
}
struct Spectro {
ps: Vec<f32>,
n_time: usize,
smspec: [f32; WORKING_BINS],
#[allow(dead_code)]
noise_level: f32,
}
const DF_BASEBAND: f32 = BASEBAND_RATE / NFFT as f32;
fn build_spectro(idat: &[f32], qdat: &[f32]) -> Spectro {
debug_assert_eq!(idat.len(), qdat.len());
let np = idat.len();
if np < NFFT {
return Spectro {
ps: Vec::new(),
n_time: 0,
smspec: [0.0; WORKING_BINS],
noise_level: 1.0,
};
}
let n_time = 4 * (np / NFFT) - 1;
let mut window = [0.0f32; NFFT];
for (j, w) in window.iter_mut().enumerate() {
*w = (PI * j as f32 / NFFT as f32).sin();
}
let mut planner = default_planner();
let fft = planner.plan_forward(NFFT);
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0.0, 0.0); NFFT];
let mut ps = vec![0.0f32; n_time * NFFT];
for t in 0..n_time {
let start = t * STRIDE;
for j in 0..NFFT {
let s = if start + j < np {
Complex::new(idat[start + j] * window[j], qdat[start + j] * window[j])
} else {
Complex::new(0.0, 0.0)
};
buf[j] = s;
}
fft.process(&mut buf);
let row = &mut ps[t * NFFT..(t + 1) * NFFT];
for j in 0..NFFT {
let k = (j + NFFT / 2) % NFFT;
row[j] = buf[k].norm_sqr();
}
}
let mut psavg = [0.0f32; NFFT];
for t in 0..n_time {
let row = &ps[t * NFFT..(t + 1) * NFFT];
for j in 0..NFFT {
psavg[j] += row[j];
}
}
let mut smspec = [0.0f32; WORKING_BINS];
for i in 0..WORKING_BINS {
let mut acc = 0.0f32;
for jw in -3i32..=3i32 {
let k = (NFFT as i32) / 2 - 205 + i as i32 + jw;
if k >= 0 && (k as usize) < NFFT {
acc += psavg[k as usize];
}
}
smspec[i] = acc;
}
let mut sorted: Vec<f32> = smspec.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let noise_level = sorted[(WORKING_BINS as f32 * 30.0 / 100.0) as usize].max(1e-30);
for v in smspec.iter_mut() {
*v = *v / noise_level - 1.0;
if *v < MIN_SNR_LIN {
*v = 0.1 * MIN_SNR_LIN;
}
}
Spectro {
ps,
n_time,
smspec,
noise_level,
}
}
fn find_peaks(spec: &Spectro, max_peaks: usize) -> Vec<(usize, f32)> {
let mut peaks: Vec<(usize, f32)> = Vec::new();
for j in 1..(WORKING_BINS - 1) {
let v = spec.smspec[j];
if v > spec.smspec[j - 1] && v > spec.smspec[j + 1] {
let snr_db = 10.0 * v.max(1e-30).log10() - SNR_SCALING_DB;
peaks.push((j, snr_db));
}
}
peaks.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
peaks.truncate(max_peaks);
peaks
}
#[inline]
fn smspec_to_bin(j: usize) -> usize {
NFFT / 2 - 205 + j
}
type RefinedCell = (i32, i32, i32, f32);
fn refine_alignment_top_k(
spec: &Spectro,
bin0: usize,
max_drift: i32,
top_k: usize,
) -> Vec<RefinedCell> {
let mut cells: Vec<RefinedCell> = Vec::new();
for dfreq in -2i32..=2i32 {
let ifr = bin0 as i32 + dfreq;
if ifr < 4 || (ifr as usize) + 4 >= NFFT {
continue;
}
for k0 in -10i32..22i32 {
for idrift in -max_drift..=max_drift {
let mut ss = 0.0f32;
let mut pow = 0.0f32;
for k in 0..162i32 {
let drift_offset =
((k as f32 - 81.0) / 81.0) * (idrift as f32) / (2.0 * DF_BASEBAND);
let ifd = ifr + drift_offset as i32;
if ifd - 3 < 0 || (ifd + 3) as usize >= NFFT {
continue;
}
let kindex = k0 + 2 * k;
if kindex < 0 || (kindex as usize) >= spec.n_time {
continue;
}
let row = &spec.ps[kindex as usize * NFFT..(kindex as usize + 1) * NFFT];
let p0 = row[(ifd - 3) as usize].sqrt();
let p1 = row[(ifd - 1) as usize].sqrt();
let p2 = row[(ifd + 1) as usize].sqrt();
let p3 = row[(ifd + 3) as usize].sqrt();
let pr3 = WSPR_SYNC_VECTOR[k as usize] as f32;
ss += (2.0 * pr3 - 1.0) * ((p1 + p3) - (p0 + p2));
pow += p0 + p1 + p2 + p3;
}
if pow <= 0.0 {
continue;
}
let sync = ss / pow;
cells.push((STRIDE as i32 * (k0 + 1), dfreq, idrift, sync));
}
}
}
cells.sort_unstable_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(core::cmp::Ordering::Equal));
cells.truncate(top_k);
cells
}
pub fn coarse_baseband(
idat: &[f32],
qdat: &[f32],
pad_samples_audio: usize,
max_peaks: usize,
max_drift_hz: i32,
) -> Vec<BasebandCandidate> {
let spec = build_spectro(idat, qdat);
if spec.n_time == 0 {
return Vec::new();
}
let peaks = find_peaks(&spec, max_peaks);
let pad_baseband = (pad_samples_audio as f32 / 32.0).round() as i32;
const TOP_K_PER_PEAK: usize = 1;
let mut out = Vec::with_capacity(peaks.len() * TOP_K_PER_PEAK);
let _ = pad_baseband; for (j, snr_db) in peaks {
let bin0 = smspec_to_bin(j);
let cells = refine_alignment_top_k(&spec, bin0, max_drift_hz, TOP_K_PER_PEAK);
for (shift_baseband, dfreq, idrift, sync) in cells {
if !sync.is_finite() {
continue;
}
let ifr = bin0 as i32 + dfreq;
let freq_offset_hz = (ifr - NFFT as i32 / 2) as f32 * DF_BASEBAND;
let centre_audio_hz = CENTER_HZ + freq_offset_hz;
let tone0_audio_hz = centre_audio_hz - 1.5 * TONE_SPACING_HZ;
let start_audio_signed = shift_baseband as i64 * 32;
let start_sample = start_audio_signed.max(0) as usize;
out.push(BasebandCandidate {
start_sample,
freq_hz: tone0_audio_hz,
drift_hz: idrift as f32,
sync,
snr_db,
});
}
}
out.sort_unstable_by(|a, b| {
b.sync
.partial_cmp(&a.sync)
.unwrap_or(core::cmp::Ordering::Equal)
});
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wspr::baseband::decimate_to_baseband;
use crate::wspr::tx::synthesize_type1;
#[test]
fn finds_synth_signal_near_centre() {
let freq = 1500.0;
let audio = synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("synth");
let mut padded = vec![0.0f32; super::super::baseband::NPOINTS_MAX];
padded[..audio.len()].copy_from_slice(&audio);
let (idat, qdat) = decimate_to_baseband(&padded);
let cands = coarse_baseband(&idat, &qdat, 0, 50, 0);
assert!(!cands.is_empty(), "should find at least one candidate");
let top = cands[0];
assert!(
(top.freq_hz - 1500.0).abs() < 1.5,
"expected ~1500 Hz, got {}",
top.freq_hz
);
}
}