use alloc::vec::Vec;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use crate::engine::pipeline::scan_dedup_match;
use crate::msg::WsprMessage;
use super::search::SearchParams;
#[cfg(any())]
use super::search::coarse_search;
#[derive(Clone, Debug)]
pub struct WsprResult {
pub message: WsprMessage,
pub freq_hz: f32,
pub start_sample: usize,
pub dt_sec: f32,
pub drift_hz: f32,
pub info_bits: [u8; 50],
pub snr_db: f32,
}
pub fn decode_at(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
) -> Option<WsprResult> {
decode_at_with_drift(audio, sample_rate, start_sample, freq_hz, 0.0)
}
#[derive(Debug, Clone, Default)]
pub struct WsprCallsignTable {
entries: Vec<(String, Option<String>)>,
}
impl WsprCallsignTable {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, msg: &crate::msg::WsprMessage) {
let entry = match msg {
crate::msg::WsprMessage::Type1 { callsign, grid, .. } => {
(callsign.clone(), Some(grid.clone()))
}
crate::msg::WsprMessage::Type2 { callsign, .. } => (callsign.clone(), None),
crate::msg::WsprMessage::Type3 { .. } => return,
};
if !self.entries.contains(&entry) {
self.entries.push(entry);
}
}
pub fn accepts(&self, msg: &crate::msg::WsprMessage) -> bool {
match msg {
crate::msg::WsprMessage::Type1 { callsign, grid, .. } => self
.entries
.iter()
.any(|(c, g)| c == callsign && g.as_deref() == Some(grid.as_str())),
crate::msg::WsprMessage::Type2 { callsign, .. } => {
self.entries.iter().any(|(c, _)| c == callsign)
}
crate::msg::WsprMessage::Type3 { .. } => false,
}
}
}
#[cfg(test)]
mod callsign_table_tests {
use super::WsprCallsignTable;
use crate::msg::WsprMessage;
fn t1(callsign: &str, grid: &str) -> WsprMessage {
WsprMessage::Type1 {
callsign: callsign.into(),
grid: grid.into(),
power_dbm: 30,
}
}
#[test]
fn accepts_only_recorded_callsigns() {
let mut table = WsprCallsignTable::new();
assert!(!table.accepts(&t1("W3BI", "FN20")), "empty table accepted");
table.record(&t1("W3BI", "FN20"));
assert!(table.accepts(&t1("W3BI", "FN20")));
assert!(
!table.accepts(&t1("ZZ9ZZZ", "AA00")),
"unrecorded callsign accepted"
);
}
#[test]
fn type1_matches_on_grid_too_type2_does_not() {
let mut table = WsprCallsignTable::new();
table.record(&t1("W3BI", "FN20"));
assert!(
!table.accepts(&t1("W3BI", "AA00")),
"Type 1 accepted a mismatched grid"
);
assert!(table.accepts(&WsprMessage::Type2 {
callsign: "W3BI".into(),
power_dbm: 30,
}));
}
#[test]
fn hashed_callsigns_are_never_accepted() {
let mut table = WsprCallsignTable::new();
table.record(&t1("W3BI", "FN20"));
assert!(!table.accepts(&WsprMessage::Type3 {
callsign_hash: 0x05c31,
grid6: "FN20aa".into(),
power_dbm: 30,
}));
}
}
pub fn decode_at_baseband(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
) -> Option<WsprResult> {
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<WsprResult> {
decode_at_baseband_nblocks_gated(
idat,
qdat,
sample_rate,
start_sample,
freq_hz,
drift_hz,
nblocks,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn decode_at_baseband_nblocks_gated(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
nblocks: &[usize],
confirmed: Option<&WsprCallsignTable>,
) -> Option<WsprResult> {
decode_at_baseband_nblocks_gated_drift(
idat,
qdat,
sample_rate,
start_sample,
freq_hz,
drift_hz,
nblocks,
confirmed,
true,
)
}
#[allow(clippy::type_complexity)]
fn refine_cascade(
idat: &[f32],
qdat: &[f32],
f0_baseband_init: f32,
lag_baseband_init: i32,
drift_hz: f32,
refine_drift: bool,
out: &mut super::demod::IsQs,
) -> (f32, i32, f32, f32) {
super::instrument::bump(&super::instrument::CANDIDATES);
let mut best_freq = f0_baseband_init;
let mut best_lag = lag_baseband_init;
let mut best_drift = drift_hz;
let mut scratch = super::demod::IsQs::zeroed();
super::demod::tone_amplitudes_into(idat, qdat, best_freq, best_lag, best_drift, out);
let mut best_sync = super::demod::sync_score_isqs(out);
macro_rules! try_cell {
($f:expr, $lag:expr, $drift:expr) => {{
super::demod::tone_amplitudes_into(idat, qdat, $f, $lag, $drift, &mut scratch);
let sync = super::demod::sync_score_isqs(&scratch);
if sync > best_sync {
best_sync = sync;
best_freq = $f;
best_lag = $lag;
best_drift = $drift;
core::mem::swap(out, &mut scratch);
}
}};
}
for &dlag in &[-128i32, -64, 64, 128] {
try_cell!(best_freq, lag_baseband_init + dlag, best_drift);
}
for i in -2i32..=2 {
if i == 0 {
continue;
}
try_cell!(f0_baseband_init + i as f32 * 0.25, best_lag, best_drift);
}
if refine_drift {
for &dd in &[0.5f32, -0.5] {
let before = best_sync;
try_cell!(best_freq, best_lag, drift_hz + dd);
if best_sync > before {
break;
}
}
}
const MINSYNC1: f32 = 0.10;
if best_sync > MINSYNC1 {
super::instrument::bump(&super::instrument::MINSYNC1_PASS);
let centre_lag = best_lag;
for &dlag in &[-32i32, -16, 16, 32] {
try_cell!(best_freq, centre_lag + dlag, best_drift);
}
let centre_freq = best_freq;
for i in -2i32..=2 {
if i == 0 {
continue;
}
try_cell!(centre_freq + i as f32 * 0.05, best_lag, best_drift);
}
}
(best_freq, best_lag, best_drift, best_sync)
}
#[cfg(any(test, feature = "internal-testing"))]
pub fn debug_refined_sync(
idat: &[f32],
qdat: &[f32],
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
refine_drift: bool,
) -> f32 {
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 mut isqs = super::demod::IsQs::zeroed();
refine_cascade(
idat,
qdat,
f0_baseband_init,
lag_baseband_init,
drift_hz,
refine_drift,
&mut isqs,
)
.3
}
#[allow(clippy::too_many_arguments)]
pub fn decode_at_baseband_nblocks_gated_drift(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
start_sample: usize,
freq_hz: f32,
drift_hz: f32,
nblocks: &[usize],
confirmed: Option<&WsprCallsignTable>,
refine_drift: bool,
) -> Option<WsprResult> {
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 mut best_isqs = super::demod::IsQs::zeroed();
let (best_freq, best_lag, best_drift, best_sync) = refine_cascade(
idat,
qdat,
f0_baseband_init,
lag_baseband_init,
drift_hz,
refine_drift,
&mut best_isqs,
);
const MINSYNC2_EARLY: f32 = 0.12;
const MINSYNC2_FINAL: f32 = 0.10;
let minsync2 = if refine_drift {
MINSYNC2_EARLY
} else {
MINSYNC2_FINAL
};
if best_sync <= minsync2 {
super::instrument::bump(&super::instrument::MINSYNC2_REJECTED);
return None;
}
decode_from_refined(
idat,
qdat,
sample_rate,
best_freq,
best_lag,
best_drift,
&best_isqs,
nblocks,
confirmed,
None,
)
}
#[allow(clippy::too_many_arguments)]
fn decode_from_refined(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
best_freq: f32,
best_lag: i32,
best_drift: f32,
best_isqs: &super::demod::IsQs,
nblocks: &[usize],
confirmed: Option<&WsprCallsignTable>,
budget: Option<&(dyn Fn() -> bool + Sync)>,
) -> Option<WsprResult> {
use crate::engine::{FecOpts, MessageCodec};
let codec = crate::fec::ConvFano;
let mut fano_scratch = crate::fec::conv::fano::FanoScratch::new();
let mut best_type1: Option<(u32, WsprResult)> = None;
let mut best_other: Option<(u32, WsprResult)> = None;
const IIFAC: i32 = 8;
const N_JITTER: i32 = 128 / IIFAC;
let mut jitter_isqs = super::demod::IsQs::zeroed();
'rungs: for &nblock in nblocks {
for idt in 0..=N_JITTER {
if let Some(check) = budget
&& !check()
{
super::instrument::bump(&super::instrument::LADDER_BUDGET_ABORTED);
break 'rungs;
}
let mut ii = (idt + 1) / 2;
if idt % 2 == 1 {
ii = -ii;
}
let lag = best_lag + IIFAC * ii;
let isqs = if idt == 0 {
best_isqs
} else {
super::demod::tone_amplitudes_into(
idat,
qdat,
best_freq,
lag,
best_drift,
&mut jitter_isqs,
);
&jitter_isqs
};
#[cfg(feature = "std")]
let t_bm = std::time::Instant::now();
let bm = if nblock == 0 {
super::demod::nblock1_bit_metrics_opt(isqs, true)
} else {
super::demod::nblock_bit_metrics(isqs, nblock)
};
#[cfg(feature = "std")]
super::instrument::add_us(
&super::instrument::BIT_METRICS_US,
t_bm.elapsed().as_micros() as u32,
);
let mut llrs = bm;
deinterleave_llrs(&mut llrs);
if crate::fec::conv::fano::wsprd_soft_symbol_rms(&llrs)
<= crate::fec::conv::fano::WSPRD_MIN_RMS
{
continue;
}
super::instrument::bump(&super::instrument::FANO_ATTEMPTS);
#[cfg(not(feature = "wspr-fano-cap-fast"))]
const WSPR_FANO_CYCLE_BUDGET: u64 = 10_000;
#[cfg(feature = "wspr-fano-cap-fast")]
const WSPR_FANO_CYCLE_BUDGET: u64 = 5_000;
let fec_opts = FecOpts {
max_cycles_per_bit: Some(WSPR_FANO_CYCLE_BUDGET),
..FecOpts::default()
};
#[cfg(feature = "std")]
let t_fano = std::time::Instant::now();
let fano_res = codec.decode_soft_pooled(&llrs, &fec_opts, &mut fano_scratch);
#[cfg(feature = "std")]
super::instrument::add_us(
&super::instrument::FANO_US,
t_fano.elapsed().as_micros() as u32,
);
let (info_bits, hard_errors) = if let Some(fec_res) = fano_res {
super::instrument::bump(&super::instrument::FANO_OK);
let mut info = [0u8; 50];
info.copy_from_slice(&fec_res.info);
(info, fec_res.hard_errors)
} else if let Some((info, nhardmin)) = confirmed.and_then(|table| {
super::instrument::bump(&super::instrument::OSD_ATTEMPTS);
#[cfg(feature = "std")]
let t_osd = std::time::Instant::now();
let osd_res = super::osd::osd_decode_packed(&llrs);
#[cfg(feature = "std")]
super::instrument::add_us(
&super::instrument::OSD_US,
t_osd.elapsed().as_micros() as u32,
);
let (info, nhardmin) = osd_res?;
let msg = crate::msg::Wspr50Message
.unpack(&info, &crate::engine::DecodeContext::default())?;
table.accepts(&msg).then_some((info, nhardmin))
}) {
super::instrument::bump(&super::instrument::OSD_OK);
(info, nhardmin)
} else {
continue;
};
let Some(message) = crate::msg::Wspr50Message
.unpack(&info_bits, &crate::engine::DecodeContext::default())
else {
continue;
};
let lag_audio = lag * 32;
let dt_sec = lag_audio as f32 / sample_rate as f32 - 1.0;
let candidate = WsprResult {
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,
drift_hz: best_drift,
info_bits,
snr_db: 0.0,
};
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));
}
}
}
break 'rungs;
}
}
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<WsprResult> {
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;
fn decode_pass1_candidate(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
pad: usize,
c: &super::coarse_baseband::BasebandCandidate,
) -> Option<(WsprResult, usize)> {
let mut d = decode_at_baseband(
idat,
qdat,
sample_rate,
c.start_sample,
c.freq_hz,
c.drift_hz,
)?;
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);
d.snr_db = c.snr_db;
Some((d, start_refined))
}
#[cfg(not(feature = "wspr-pass2-topn"))]
fn decode_pass2_candidate(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
pad: usize,
c: &super::coarse_baseband::BasebandCandidate,
confirmed: &WsprCallsignTable,
) -> Option<WsprResult> {
let mut d = decode_at_baseband_nblocks_gated_drift(
idat,
qdat,
sample_rate,
c.start_sample,
c.freq_hz,
c.drift_hz,
&[1, 2, 3, 0],
Some(confirmed),
false,
)?;
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);
d.snr_db = c.snr_db;
Some(d)
}
#[cfg(feature = "wspr-pass2-topn")]
pub const PASS2_DEEP_LADDER_TOP_N: usize = 2;
#[cfg(feature = "wspr-pass2-topn")]
const MINSYNC2_FINAL: f32 = 0.10;
#[cfg(feature = "wspr-pass2-topn")]
pub struct WsprPass2Candidate<'c> {
pub sync: f32,
pub c: &'c super::coarse_baseband::BasebandCandidate,
pub freq: f32,
pub lag: i32,
pub drift: f32,
pub isqs: super::demod::IsQs,
}
#[cfg(feature = "wspr-pass2-topn")]
pub fn rank_pass2_candidates<'c>(
idat: &[f32],
qdat: &[f32],
cands: &'c [super::coarse_baseband::BasebandCandidate],
) -> Vec<WsprPass2Candidate<'c>> {
fn refine_one<'c>(
idat: &[f32],
qdat: &[f32],
c: &'c super::coarse_baseband::BasebandCandidate,
) -> Option<WsprPass2Candidate<'c>> {
let f0_center_init = c.freq_hz + 1.5 * super::demod::TONE_SPACING_HZ;
let f0_baseband_init = f0_center_init - super::baseband::CENTER_HZ;
let lag_baseband_init = c.start_sample as i32 / 32;
let mut isqs = super::demod::IsQs::zeroed();
let (freq, lag, drift, sync) = refine_cascade(
idat,
qdat,
f0_baseband_init,
lag_baseband_init,
c.drift_hz,
false,
&mut isqs,
);
if sync <= MINSYNC2_FINAL {
super::instrument::bump(&super::instrument::MINSYNC2_REJECTED);
return None;
}
Some(WsprPass2Candidate {
sync,
c,
freq,
lag,
drift,
isqs,
})
}
#[cfg(feature = "parallel")]
let mut survivors: Vec<WsprPass2Candidate<'_>> = cands
.par_iter()
.filter_map(|c| refine_one(idat, qdat, c))
.collect();
#[cfg(not(feature = "parallel"))]
let mut survivors: Vec<WsprPass2Candidate<'_>> = cands
.iter()
.filter_map(|c| refine_one(idat, qdat, c))
.collect();
survivors.sort_by(|a, b| {
b.sync
.partial_cmp(&a.sync)
.unwrap_or(core::cmp::Ordering::Equal)
});
survivors.truncate(PASS2_DEEP_LADDER_TOP_N);
survivors
}
#[cfg(feature = "wspr-pass2-topn")]
pub fn deep_decode_pass2_candidate(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
pad: usize,
confirmed: &WsprCallsignTable,
r: &WsprPass2Candidate<'_>,
budget: Option<&(dyn Fn() -> bool + Sync)>,
) -> Option<WsprResult> {
let mut d = decode_from_refined(
idat,
qdat,
sample_rate,
r.freq,
r.lag,
r.drift,
&r.isqs,
&[1, 2, 3, 0],
Some(confirmed),
budget,
)?;
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);
d.snr_db = r.c.snr_db;
Some(d)
}
#[cfg(feature = "wspr-pass2-topn")]
pub fn decode_pass2_top_n(
idat: &[f32],
qdat: &[f32],
sample_rate: u32,
pad: usize,
cands: &[super::coarse_baseband::BasebandCandidate],
confirmed: &WsprCallsignTable,
budget: Option<&(dyn Fn() -> bool + Sync)>,
) -> Vec<WsprResult> {
let survivors = rank_pass2_candidates(idat, qdat, cands);
#[cfg(feature = "parallel")]
let out: Vec<WsprResult> = survivors
.par_iter()
.filter_map(|r| {
deep_decode_pass2_candidate(idat, qdat, sample_rate, pad, confirmed, r, budget)
})
.collect();
#[cfg(not(feature = "parallel"))]
let out: Vec<WsprResult> = survivors
.iter()
.filter_map(|r| {
deep_decode_pass2_candidate(idat, qdat, sample_rate, pad, confirmed, r, budget)
})
.collect();
out
}
pub fn decode_scan(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<WsprResult> {
decode_scan_inner(audio, sample_rate, nominal_start_sample, params, None, None)
}
pub fn decode_scan_with_table(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
confirmed: &mut WsprCallsignTable,
) -> Vec<WsprResult> {
decode_scan_inner(
audio,
sample_rate,
nominal_start_sample,
params,
None,
Some(confirmed),
)
}
pub fn decode_scan_streaming(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
on_result: &(dyn Fn(&WsprResult) + Sync),
) -> Vec<WsprResult> {
decode_scan_inner(
audio,
sample_rate,
nominal_start_sample,
params,
Some(on_result),
None,
)
}
fn decode_scan_inner(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
on_result: Option<&(dyn Fn(&WsprResult) + Sync)>,
carried: Option<&mut WsprCallsignTable>,
) -> Vec<WsprResult> {
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;
#[cfg(all(feature = "wspr-ddc", feature = "wspr-ddc-cascade"))]
compile_error!("wspr-ddc and wspr-ddc-cascade select different channelizers — enable only one");
#[cfg(not(any(feature = "wspr-ddc", feature = "wspr-ddc-cascade")))]
let (mut idat, mut qdat) = super::baseband::decimate_to_baseband(&padded);
#[cfg(feature = "wspr-ddc")]
let (mut idat, mut qdat) = super::ddc::ddc_to_baseband(&padded);
#[cfg(feature = "wspr-ddc-cascade")]
let (mut idat, mut qdat) = super::ddc::ddc_to_baseband_cascade(&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 = bb_cands;
cands.truncate(params.max_candidates);
let _audio = &padded[..]; let mut seen: Vec<WsprResult> = 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::engine::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 found: Vec<(WsprResult, usize)> = Vec::new();
for early_pass in 0..2 {
if early_pass == 1 && found.is_empty() {
break;
}
let pass_cands = if early_pass == 0 {
core::mem::take(&mut cands)
} else {
let mut c = super::coarse_baseband::coarse_baseband(
&idat,
&qdat,
pad,
params.max_candidates,
max_drift,
);
c.truncate(params.max_candidates);
c
};
#[cfg(feature = "parallel")]
let raw: Vec<(WsprResult, usize)> = pass_cands
.par_iter()
.filter_map(|c| decode_pass1_candidate(&idat, &qdat, sample_rate, pad, c))
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<(WsprResult, usize)> = pass_cands
.iter()
.filter_map(|c| decode_pass1_candidate(&idat, &qdat, sample_rate, pad, c))
.collect();
let mut this_pass: Vec<(WsprResult, usize)> = Vec::new();
for (d, start_refined) in raw {
let dup = scan_dedup_match(
&seen,
&d,
|r| &r.message,
|r| r.freq_hz,
|r| r.start_sample as i64,
FREQ_DEDUP_HZ,
TIME_DEDUP_SAMPLES,
);
if !dup {
if let Some(cb) = on_result {
cb(&d);
}
this_pass.push((d.clone(), start_refined));
seen.push(d);
}
}
for (d, start_refined) in &this_pass {
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 idat,
&mut qdat,
f0_audio,
shift_baseband,
d.drift_hz,
&symbols,
);
}
found.extend(this_pass);
}
let mut confirmed = match &carried {
Some(t) => (*t).clone(),
None => WsprCallsignTable::new(),
};
for (d, _) in &found {
confirmed.record(&d.message);
}
{
const PASS2_MAX_DRIFT: i32 = 0;
let bb_cands2 = super::coarse_baseband::coarse_baseband(
&idat,
&qdat,
pad,
params.max_candidates,
PASS2_MAX_DRIFT,
);
#[cfg(feature = "wspr-pass2-topn")]
let raw2: Vec<WsprResult> =
decode_pass2_top_n(&idat, &qdat, sample_rate, pad, &bb_cands2, &confirmed, None);
#[cfg(not(feature = "wspr-pass2-topn"))]
#[cfg(feature = "parallel")]
let raw2: Vec<WsprResult> = bb_cands2
.par_iter()
.filter_map(|c| decode_pass2_candidate(&idat, &qdat, sample_rate, pad, c, &confirmed))
.collect();
#[cfg(not(feature = "wspr-pass2-topn"))]
#[cfg(not(feature = "parallel"))]
let raw2: Vec<WsprResult> = bb_cands2
.iter()
.filter_map(|c| decode_pass2_candidate(&idat, &qdat, sample_rate, pad, c, &confirmed))
.collect();
for d in raw2 {
let dup = scan_dedup_match(
&seen,
&d,
|r| &r.message,
|r| r.freq_hz,
|r| r.start_sample as i64,
FREQ_DEDUP_HZ,
TIME_DEDUP_SAMPLES,
);
if !dup {
if let Some(cb) = on_result {
cb(&d);
}
seen.push(d);
}
}
}
if let Some(t) = carried {
for d in &seen {
t.record(&d.message);
}
}
seen
}
pub fn decode_scan_default(audio: &[f32], sample_rate: u32) -> Vec<WsprResult> {
decode_scan(audio, sample_rate, 0, &SearchParams::default())
}
const WSPR_SUBTRACT: crate::engine::dsp::subtract::SubtractCfg =
crate::engine::dsp::subtract::SubtractCfg {
sample_rate: 12_000.0,
tone_spacing_hz: 1.4648,
samples_per_symbol: 8192,
base_offset_s: 1.0,
gfsk: None,
};
const WSPR_SUBTRACT_LPF_HALF: usize = 600;
#[deprecated(
note = "not the wsprd-equivalent decoder — `decode_scan` is. This wraps a second \
SIC layer around it that the reference has no counterpart for, costing 3.3x \
for zero marginal recall on the WSJT-X golden. Use `decode_scan`."
)]
pub fn decode_scan_subtract(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<WsprResult> {
decode_scan_subtract_inner(audio, sample_rate, nominal_start_sample, params, None)
}
#[deprecated(note = "see `decode_scan_subtract` — use `decode_scan_streaming`.")]
pub fn decode_scan_subtract_streaming(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
on_result: &(dyn Fn(&WsprResult) + Sync),
) -> Vec<WsprResult> {
decode_scan_subtract_inner(
audio,
sample_rate,
nominal_start_sample,
params,
Some(on_result),
)
}
fn decode_scan_subtract_inner(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
on_result: Option<&(dyn Fn(&WsprResult) + Sync)>,
) -> Vec<WsprResult> {
use crate::engine::dsp::subtract::subtract_tones_lpf;
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<WsprResult> = 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 = scan_dedup_match(
&all,
&d,
|r| &r.message,
|r| r.freq_hz,
|r| r.start_sample as i64,
FREQ_DEDUP_HZ,
TIME_DEDUP_SAMPLES,
);
if dup {
continue;
}
let symbols = super::encode_channel_symbols(&d.info_bits);
subtract_tones_lpf(
&mut residual_i16,
&symbols,
d.freq_hz,
d.dt_sec,
&WSPR_SUBTRACT,
WSPR_SUBTRACT_LPF_HALF,
false,
);
if let Some(cb) = on_result {
cb(&d);
}
all.push(d);
added += 1;
}
if added == 0 {
break;
}
}
all
}
fn deinterleave_llrs(llrs: &mut [f32; 162]) {
crate::engine::interleave::deinterleave_bitrev(llrs);
}
#[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,
}
);
}
}