use alloc::vec;
use alloc::vec::Vec;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::core::ModulationParams;
use crate::core::fft::default_planner;
use super::Wspr;
pub struct Spectrogram {
pub mags_sqr: Vec<f32>,
pub n_time: usize,
pub n_freq: usize,
pub t_step: usize,
pub nsps: usize,
pub df: f32,
pub noise_per_bin: f32,
}
impl Spectrogram {
pub fn build(audio: &[f32], sample_rate: u32) -> Self {
let nsps = (sample_rate as f32 * <Wspr as ModulationParams>::SYMBOL_DT).round() as usize;
let t_step = nsps / 4;
let n_freq = nsps / 2;
if audio.len() < nsps || t_step == 0 {
return Self {
mags_sqr: Vec::new(),
n_time: 0,
n_freq: 0,
t_step: 0,
nsps,
df: sample_rate as f32 / nsps as f32,
noise_per_bin: 1.0,
};
}
let n_time = (audio.len() - nsps) / t_step + 1;
let mut mags_sqr = vec![0f32; n_time * n_freq];
let mut planner = default_planner();
let fft = planner.plan_forward(nsps);
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0f32, 0f32); nsps];
for t in 0..n_time {
let start = t * t_step;
for (slot, &s) in buf.iter_mut().zip(&audio[start..start + nsps]) {
*slot = Complex::new(s, 0.0);
}
fft.process(&mut buf);
let row = &mut mags_sqr[t * n_freq..(t + 1) * n_freq];
for (slot, c) in row.iter_mut().zip(buf.iter().take(n_freq)) {
*slot = c.norm_sqr();
}
}
let mut sorted = mags_sqr.clone();
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let keep = (sorted.len() as f32 * 0.95) as usize;
let noise_per_bin = if keep > 0 {
sorted[..keep].iter().sum::<f32>() / keep as f32
} else {
1.0
};
Self {
mags_sqr,
n_time,
n_freq,
t_step,
nsps,
df: sample_rate as f32 / nsps as f32,
noise_per_bin: noise_per_bin.max(1e-6),
}
}
#[inline]
pub fn get(&self, t: usize, f: usize) -> f32 {
self.mags_sqr[t * self.n_freq + f]
}
}
pub fn score_candidate(spec: &Spectrogram, t_row: usize, base_bin: usize) -> f32 {
use super::WSPR_SYNC_VECTOR;
const ROWS_PER_SYMBOL: usize = 4;
let last_row = t_row + 161 * ROWS_PER_SYMBOL;
if last_row >= spec.n_time || base_bin + 4 > spec.n_freq {
return 0.0;
}
let mut sync_pwr = 0.0f32;
let mut off_pwr = 0.0f32;
for i in 0..162 {
let t = t_row + i * ROWS_PER_SYMBOL;
let m0 = spec.get(t, base_bin);
let m1 = spec.get(t, base_bin + 1);
let m2 = spec.get(t, base_bin + 2);
let m3 = spec.get(t, base_bin + 3);
if WSPR_SYNC_VECTOR[i] == 0 {
sync_pwr += m0 + m2;
off_pwr += m1 + m3;
} else {
sync_pwr += m1 + m3;
off_pwr += m0 + m2;
}
}
let noise_floor = spec.noise_per_bin * 162.0;
let denom = sync_pwr + off_pwr + noise_floor;
if denom > 0.0 {
(sync_pwr - off_pwr) / denom
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::super::synthesize_type1;
use super::*;
#[test]
fn spec_matches_direct_demod() {
let freq = 1500.0;
let audio = synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("synth");
let spec = Spectrogram::build(&audio, 12_000);
assert!(spec.n_time > 0);
assert_eq!(spec.n_freq, 4096);
let true_bin = 1024;
let true_t = 0usize;
let best_score = score_candidate(&spec, true_t, true_bin);
for dt in [-2i32, -1, 1, 2] {
if let Ok(t) = (true_t as i32 + dt).try_into() {
let s = score_candidate(&spec, t, true_bin);
assert!(s < best_score, "dt={} scored {} >= {}", dt, s, best_score);
}
}
for df in [-2i32, -1, 1, 2] {
let b = (true_bin as i32 + df) as usize;
let s = score_candidate(&spec, true_t, b);
assert!(s < best_score, "df={} scored {} >= {}", df, s, best_score);
}
}
}