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::ModulationParams;
use crate::core::fft::default_planner;
use super::{WSPR_SYNC_VECTOR, Wspr};
#[derive(Clone)]
pub struct ToneMagnitudes {
pub mags: Vec<[f32; 4]>, pub noise_power_est: f32,
}
pub fn extract_tone_magnitudes(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<ToneMagnitudes> {
let nsps = (sample_rate as f32 * <Wspr as ModulationParams>::SYMBOL_DT).round() as usize;
let df = sample_rate as f32 / nsps as f32;
let bin_pos = base_freq_hz / df;
let base_bin = bin_pos.round() as usize;
let residual_hz = base_freq_hz - base_bin as f32 * df;
if start_sample + 162 * nsps > audio.len() || base_bin + 4 >= nsps / 2 {
return None;
}
let mut planner = default_planner();
let fft = planner.plan_forward(nsps);
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0.0f32, 0.0); nsps];
let mut mags = Vec::with_capacity(162);
let mut noise_acc = 0.0f32;
let mut noise_count = 0u32;
let mix_w = -2.0 * PI * residual_hz / sample_rate as f32;
for i in 0..162 {
let sym_start = start_sample + i * nsps;
if residual_hz.abs() > 1e-6 {
for k in 0..nsps {
let abs_n = sym_start + k;
let ph = mix_w * abs_n as f32;
let s = audio[abs_n];
buf[k] = Complex::new(s * ph.cos(), s * ph.sin());
}
} else {
for (slot, &s) in buf.iter_mut().zip(&audio[sym_start..sym_start + nsps]) {
*slot = Complex::new(s, 0.0);
}
}
fft.process(&mut buf);
mags.push([
buf[base_bin].norm(),
buf[base_bin + 1].norm(),
buf[base_bin + 2].norm(),
buf[base_bin + 3].norm(),
]);
for k in 4..8 {
let bin = base_bin + k;
if bin < nsps / 2 {
noise_acc += buf[bin].norm_sqr();
noise_count += 1;
}
}
}
let noise_power_est = if noise_count > 0 {
noise_acc / noise_count as f32
} else {
1.0
};
Some(ToneMagnitudes {
mags,
noise_power_est,
})
}
pub fn mags_to_llrs(tm: &ToneMagnitudes) -> [f32; 162] {
let mut bmet = [0.0f32; 162];
for i in 0..162 {
let sync = WSPR_SYNC_VECTOR[i];
let (e, o) = if sync == 0 {
(tm.mags[i][0], tm.mags[i][2])
} else {
(tm.mags[i][1], tm.mags[i][3])
};
bmet[i] = e - o;
}
let n = bmet.len() as f32;
let mean = bmet.iter().sum::<f32>() / n;
let mean_sq = bmet.iter().map(|&x| x * x).sum::<f32>() / n;
let var = mean_sq - mean * mean;
let sig = if var > 0.0 {
var.sqrt()
} else {
mean_sq.sqrt()
};
if sig > 0.0 {
for x in bmet.iter_mut() {
*x /= sig;
}
}
let _ = tm.noise_power_est; const SCALE: f32 = 2.83;
for x in bmet.iter_mut() {
*x *= SCALE;
}
bmet
}
pub fn sync_score(tm: &ToneMagnitudes) -> f32 {
let mut sync_pwr = 0.0f32;
let mut off_pwr = 0.0f32;
for i in 0..162 {
let mags = tm.mags[i];
let (s_a, s_b, o_a, o_b) = if WSPR_SYNC_VECTOR[i] == 0 {
(mags[0], mags[2], mags[1], mags[3])
} else {
(mags[1], mags[3], mags[0], mags[2])
};
sync_pwr += s_a * s_a + s_b * s_b;
off_pwr += o_a * o_a + o_b * o_b;
}
let noise_floor = tm.noise_power_est * 162.0;
let denom = sync_pwr + off_pwr + noise_floor;
if denom > 0.0 {
(sync_pwr - off_pwr) / denom
} else {
0.0
}
}
pub fn demodulate_aligned(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> [f32; 162] {
match extract_tone_magnitudes(audio, sample_rate, start_sample, base_freq_hz) {
Some(tm) => mags_to_llrs(&tm),
None => [0f32; 162],
}
}
#[cfg(test)]
mod tests {
use super::super::tx::synthesize_audio;
use super::*;
#[test]
fn recovers_llr_sign_noise_free() {
let mut symbols = [0u8; 162];
for i in 0..162 {
let data_bit = (i & 1) as u8;
let sync = WSPR_SYNC_VECTOR[i];
symbols[i] = 2 * data_bit + sync;
}
let audio = synthesize_audio(&symbols, 12_000, 1500.0, 0.3);
let llrs = demodulate_aligned(&audio, 12_000, 0, 1500.0);
for i in 0..162 {
let expect_positive = (i & 1) == 0;
if expect_positive {
assert!(
llrs[i] > 0.0,
"symbol {} LLR should be > 0, got {}",
i,
llrs[i]
);
} else {
assert!(
llrs[i] < 0.0,
"symbol {} LLR should be < 0, got {}",
i,
llrs[i]
);
}
}
}
}