use alloc::vec::Vec;
use crate::msg::WsprMessage;
#[cfg(any())]
use super::search::coarse_search;
use super::search::{SearchParams, SyncCandidate};
#[derive(Clone, Debug)]
pub struct WsprDecode {
pub message: WsprMessage,
pub freq_hz: f32,
pub start_sample: usize,
pub dt_sec: f32,
pub info_bits: [u8; 50],
}
pub fn decode_at(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
) -> Option<WsprDecode> {
decode_at_with_drift(audio, sample_rate, start_sample, freq_hz, 0.0)
}
pub fn decode_at_baseband(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
) -> Option<WsprDecode> {
decode_at_baseband_nblocks(
idat,
qdat,
sample_rate,
start_sample,
freq_hz,
drift_hz,
&[1],
)
}
pub fn decode_at_baseband_nblocks(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
nblocks: &[usize],
) -> Option<WsprDecode> {
use crate::core::{FecCodec, FecOpts, MessageCodec};
let f0_center_init = freq_hz + 1.5 * super::demod::TONE_SPACING_HZ;
let f0_baseband_init = f0_center_init - super::baseband::CENTER_HZ;
let lag_baseband_init = start_sample as i32 / 32;
let codec = crate::fec::ConvFano;
let mut best_lag = lag_baseband_init;
let mut best_lag_sync = f32::NEG_INFINITY;
let mut best_lag_isqs = None;
for &dlag in &[-128i32, -64, 0, 64, 128] {
let lag = lag_baseband_init + dlag;
let isqs = super::demod::tone_amplitudes(idat, qdat, f0_baseband_init, lag, drift_hz);
let sync = super::demod::sync_score_isqs(&isqs);
if sync > best_lag_sync {
best_lag_sync = sync;
best_lag = lag;
best_lag_isqs = Some(isqs);
}
}
let mut best_freq = f0_baseband_init;
let mut best_freq_sync = best_lag_sync;
let mut best_isqs = best_lag_isqs.expect("at least one lag eval succeeded");
for &df in &[-1.0f32, -0.5, 0.5, 1.0] {
let f = f0_baseband_init + df;
let isqs = super::demod::tone_amplitudes(idat, qdat, f, best_lag, drift_hz);
let sync = super::demod::sync_score_isqs(&isqs);
if sync > best_freq_sync {
best_freq_sync = sync;
best_freq = f;
best_isqs = isqs;
}
}
let mut best_type1: Option<(u32, WsprDecode)> = None;
let mut best_other: Option<(u32, WsprDecode)> = None;
for &nblock in nblocks {
let bm = super::demod::nblock_bit_metrics(&best_isqs, nblock);
let mut llrs = bm;
deinterleave_llrs(&mut llrs);
let (info_bits, hard_errors) =
if let Some(fec_res) = codec.decode_soft(&llrs, &FecOpts::default()) {
let mut info = [0u8; 50];
info.copy_from_slice(&fec_res.info);
(info, fec_res.hard_errors)
} else if let Some((info, nhardmin)) = super::osd::osd_decode(&llrs) {
const OSD_HARD_ERR_MAX: u32 = 44;
if nhardmin > OSD_HARD_ERR_MAX {
continue;
}
let Some(msg) =
crate::msg::Wspr50Message.unpack(&info, &crate::core::DecodeContext::default())
else {
continue;
};
if matches!(msg, crate::msg::WsprMessage::Type3 { .. }) {
continue;
}
(info, nhardmin)
} else {
continue;
};
let Some(message) =
crate::msg::Wspr50Message.unpack(&info_bits, &crate::core::DecodeContext::default())
else {
continue;
};
let lag_audio = best_lag * 32;
let dt_sec = lag_audio as f32 / sample_rate as f32 - 1.0;
let candidate = WsprDecode {
message: message.clone(),
freq_hz: best_freq + super::baseband::CENTER_HZ - 1.5 * super::demod::TONE_SPACING_HZ,
start_sample: lag_audio.max(0) as usize,
dt_sec,
info_bits,
};
let he = hard_errors;
match message {
crate::msg::WsprMessage::Type1 { .. } | crate::msg::WsprMessage::Type2 { .. } => {
if best_type1.as_ref().is_none_or(|(b, _)| he < *b) {
best_type1 = Some((he, candidate));
}
}
crate::msg::WsprMessage::Type3 { .. } => {
if best_other.as_ref().is_none_or(|(b, _)| he < *b) {
best_other = Some((he, candidate));
}
}
}
}
best_type1.map(|(_, d)| d).or(best_other.map(|(_, d)| d))
}
pub fn decode_at_with_drift(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
) -> Option<WsprDecode> {
let (idat, qdat) = super::baseband::decimate_to_baseband(audio);
decode_at_baseband(&idat, &qdat, sample_rate, start_sample, freq_hz, drift_hz)
}
const NEGATIVE_DT_PAD_SEC: f32 = 3.0;
pub fn decode_scan(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<WsprDecode> {
let pad = (NEGATIVE_DT_PAD_SEC * sample_rate as f32) as usize;
let mut padded = alloc::vec![0f32; pad + audio.len()];
padded[pad..].copy_from_slice(audio);
let nominal_shifted = nominal_start_sample + pad;
let (idat, qdat) = super::baseband::decimate_to_baseband(&padded);
let max_drift = 4i32;
let bb_cands = super::coarse_baseband::coarse_baseband(
&idat,
&qdat,
pad,
params.max_candidates,
max_drift,
);
let _ = nominal_shifted;
let mut cands: Vec<SyncCandidate> = bb_cands
.iter()
.map(|c| SyncCandidate {
start_sample: c.start_sample,
freq_hz: c.freq_hz,
score: c.sync,
})
.collect();
cands.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(core::cmp::Ordering::Equal)
});
cands.truncate(params.max_candidates);
let _audio = &padded[..]; let mut seen: Vec<WsprDecode> = Vec::new();
const FREQ_DEDUP_HZ: f32 = 5.0;
const TIME_DEDUP_SAMPLES: i64 = 8192; const REFINE_FREQ_RADIUS_HZ: f32 = 1.0;
const REFINE_FREQ_STEP_HZ: f32 = 1.0;
let nsps = (sample_rate as f32 * <super::Wspr as crate::core::ModulationParams>::SYMBOL_DT)
.round() as i64;
let refine_time_radius = nsps / 8; let refine_time_step = nsps / 8; let _ = (
REFINE_FREQ_RADIUS_HZ,
REFINE_FREQ_STEP_HZ,
refine_time_radius,
refine_time_step,
);
let mut pass1: Vec<(WsprDecode, usize)> = Vec::new();
for c in &cands {
let Some(mut d) =
decode_at_baseband(&idat, &qdat, sample_rate, c.start_sample, c.freq_hz, 0.0)
else {
continue;
};
let start_refined = d.start_sample;
d.dt_sec = (start_refined as i64 - pad as i64) as f32 / sample_rate as f32 - 1.0;
d.start_sample = start_refined.saturating_sub(pad);
let dup = seen.iter().any(|prev| {
prev.message == d.message
&& (prev.freq_hz - d.freq_hz).abs() <= FREQ_DEDUP_HZ
&& (prev.start_sample as i64 - d.start_sample as i64).abs() <= TIME_DEDUP_SAMPLES
});
if !dup {
pass1.push((d.clone(), start_refined));
seen.push(d);
}
}
if !pass1.is_empty() {
let mut idat2 = idat.clone();
let mut qdat2 = qdat.clone();
for (d, start_refined) in &pass1 {
let symbols = super::encode_channel_symbols(&d.info_bits);
let f0_audio = d.freq_hz + 1.5 * super::demod::TONE_SPACING_HZ;
let shift_baseband = (*start_refined as i32) / 32;
super::subtract::subtract_signal_baseband(
&mut idat2,
&mut qdat2,
f0_audio,
shift_baseband,
0.0,
&symbols,
);
}
let bb_cands2 = super::coarse_baseband::coarse_baseband(
&idat2,
&qdat2,
pad,
params.max_candidates,
max_drift,
);
for c in bb_cands2 {
let Some(mut d) = decode_at_baseband_nblocks(
&idat2,
&qdat2,
sample_rate,
c.start_sample,
c.freq_hz,
c.drift_hz,
&[1, 2, 3],
) else {
continue;
};
let start_refined = d.start_sample;
d.dt_sec = (start_refined as i64 - pad as i64) as f32 / sample_rate as f32 - 1.0;
d.start_sample = start_refined.saturating_sub(pad);
let dup = seen.iter().any(|prev| {
prev.message == d.message
&& (prev.freq_hz - d.freq_hz).abs() <= FREQ_DEDUP_HZ
&& (prev.start_sample as i64 - d.start_sample as i64).abs()
<= TIME_DEDUP_SAMPLES
});
if !dup {
seen.push(d);
}
}
}
seen
}
pub fn decode_scan_default(audio: &[f32], sample_rate: u32) -> Vec<WsprDecode> {
decode_scan(audio, sample_rate, 0, &SearchParams::default())
}
const WSPR_SUBTRACT: crate::core::dsp::subtract::SubtractCfg =
crate::core::dsp::subtract::SubtractCfg {
sample_rate: 12_000.0,
tone_spacing_hz: 1.4648,
samples_per_symbol: 8192,
base_offset_s: 1.0,
gfsk: None,
};
#[allow(dead_code)]
const WSPR_SUBTRACT_LPF_HALF: usize = 600;
pub fn decode_scan_subtract(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<WsprDecode> {
use crate::core::dsp::subtract::subtract_tones;
let mut residual_i16: Vec<i16> = audio
.iter()
.map(|&x| (x * 32767.0).clamp(-32768.0, 32767.0) as i16)
.collect();
let mut all: Vec<WsprDecode> = Vec::new();
const FREQ_DEDUP_HZ: f32 = 5.0;
const TIME_DEDUP_SAMPLES: i64 = 8192;
const NPASSES: usize = 2;
for _pass in 0..NPASSES {
let residual_f32: Vec<f32> = residual_i16.iter().map(|&s| s as f32 / 32_768.0).collect();
let new_decodes = decode_scan(&residual_f32, sample_rate, nominal_start_sample, params);
if new_decodes.is_empty() {
break;
}
let mut added = 0usize;
for d in new_decodes {
let dup = all.iter().any(|prev| {
prev.message == d.message
&& (prev.freq_hz - d.freq_hz).abs() <= FREQ_DEDUP_HZ
&& (prev.start_sample as i64 - d.start_sample as i64).abs()
<= TIME_DEDUP_SAMPLES
});
if dup {
continue;
}
let symbols = super::encode_channel_symbols(&d.info_bits);
subtract_tones(
&mut residual_i16,
&symbols,
d.freq_hz,
d.dt_sec,
1.0,
&WSPR_SUBTRACT,
);
all.push(d);
added += 1;
}
if added == 0 {
break;
}
}
all
}
fn deinterleave_llrs(llrs: &mut [f32; 162]) {
let mut tmp = [0f32; 162];
let mut p = 0u8;
let mut i = 0u8;
while p < 162 {
let i64 = i as u64;
let j = ((((i64 * 0x8020_0802u64) & 0x0884_4221_10u64).wrapping_mul(0x0101_0101_01u64))
>> 32) as u8 as usize;
if j < 162 {
tmp[p as usize] = llrs[j];
p += 1;
}
i = i.wrapping_add(1);
}
*llrs = tmp;
}
#[cfg(test)]
mod tests {
use super::super::search::SearchParams;
use super::super::synthesize_type1;
use super::*;
use crate::msg::WsprMessage;
#[test]
fn synth_decode_roundtrip_k1abc_fn42_37() {
let freq = 1500.0;
let audio =
synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("valid message");
let r = decode_at(&audio, 12_000, 0, freq).expect("decode");
assert_eq!(
r.message,
WsprMessage::Type1 {
callsign: "K1ABC".into(),
grid: "FN42".into(),
power_dbm: 37,
}
);
}
#[test]
fn scan_recovers_message_without_freq_hint() {
let freq = 1500.0;
let audio = synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("synth");
let decodes = decode_scan(
&audio,
12_000,
0,
&SearchParams {
freq_min_hz: 1450.0,
freq_max_hz: 1550.0,
..SearchParams::default()
},
);
assert!(!decodes.is_empty(), "at least one decode");
let d = decodes.into_iter().next().unwrap();
assert_eq!(
d.message,
WsprMessage::Type1 {
callsign: "K1ABC".into(),
grid: "FN42".into(),
power_dbm: 37,
}
);
assert!((d.freq_hz - 1500.0).abs() <= 2.0);
}
#[test]
fn survives_moderate_awgn() {
use std::f32::consts::PI;
let freq = 1500.0;
let mut audio =
synthesize_type1("K9AN", "EN50", 33, 12_000, freq, 0.5).expect("valid message");
let mut seed: u32 = 0x1234_5678;
for (i, s) in audio.iter_mut().enumerate() {
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345);
let rnd = ((seed >> 16) as f32 / 32768.0 - 1.0) * 0.10;
let off = 0.05 * (2.0 * PI * 2345.7 * i as f32 / 12_000.0).sin();
*s += rnd + off;
}
let r = decode_at(&audio, 12_000, 0, freq).expect("decode under noise");
assert_eq!(
r.message,
WsprMessage::Type1 {
callsign: "K9AN".into(),
grid: "EN50".into(),
power_dbm: 33,
}
);
}
}