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_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 base_bin = (base_freq_hz / df).round() as usize;
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;
for i in 0..162 {
let sym_start = start_sample + i * nsps;
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 m_even = [0.0f32; 162];
let mut m_odd = [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])
};
m_even[i] = e;
m_odd[i] = o;
}
let mean_sig_power = m_even
.iter()
.chain(m_odd.iter())
.map(|&m| m * m)
.sum::<f32>()
/ (2.0 * 162.0);
let sigma2 = tm.noise_power_est.max(mean_sig_power * 1e-4);
let mut llrs = [0f32; 162];
for i in 0..162 {
let raw = (m_even[i] * m_even[i] - m_odd[i] * m_odd[i]) / sigma2;
llrs[i] = raw.clamp(-20.0, 20.0);
}
llrs
}
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]
);
}
}
}
}