use alloc::vec;
use alloc::vec::Vec;
use num_complex::Complex;
use rustfft::FftPlanner;
use super::ModulationParams;
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_for<P: ModulationParams>(
audio: &[f32],
sample_rate: u32,
nstep_per_symbol: usize,
) -> Self {
let nsps = (sample_rate as f32 * P::SYMBOL_DT).round() as usize;
let t_step = (nsps / nstep_per_symbol).max(1);
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 = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(nsps);
let mut scratch = vec![Complex::new(0f32, 0f32); fft.get_inplace_scratch_len()];
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_with_scratch(&mut buf, &mut scratch);
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();
let keep = (sorted.len() as f32 * 0.95) as usize;
let noise_per_bin = if keep > 0 {
sorted.select_nth_unstable_by(keep - 1, |a, b| {
a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
});
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 sync_power_at_bin(
spec: &Spectrogram,
start_row: usize,
bin: usize,
sync_positions: &[u32],
rows_per_symbol: usize,
) -> f32 {
if bin >= spec.n_freq {
return 0.0;
}
let mut sync_pwr = 0.0f32;
for &sym_idx in sync_positions {
let row = start_row + (sym_idx as usize) * rows_per_symbol;
sync_pwr += spec.get(row, bin);
}
sync_pwr
}
pub fn score_candidate(
spec: &Spectrogram,
start_row: usize,
base_bin: usize,
sync_positions: &[u32],
rows_per_symbol: usize,
) -> f32 {
let Some(&last_pos) = sync_positions.last() else {
return 0.0;
};
let last_row = start_row + (last_pos as usize) * rows_per_symbol;
if last_row >= spec.n_time || base_bin >= spec.n_freq {
return 0.0;
}
let sync_pwr = sync_power_at_bin(spec, start_row, base_bin, sync_positions, rows_per_symbol);
let noise_floor = spec.noise_per_bin * sync_positions.len() as f32;
sync_pwr / (sync_pwr + noise_floor)
}