use crate::core::dsp::downsample::{DownsampleCfg, build_fft_cache, downsample_cached};
use crate::core::equalize::{EqMode, equalize_local};
use crate::core::llr::{compute_llr, compute_snr_db, symbol_spectra, sync_quality};
use crate::core::pipeline::{DecodeDepth, DecodeResult, DecodeStrictness};
use crate::core::sync::{SyncCandidate, coarse_sync, fine_sync_power_per_block, refine_candidate};
use crate::core::tx::codeword_to_itone;
use crate::core::{FecCodec, FecOpts, Protocol};
use num_complex::Complex;
use super::ap::{ApHint, WsjtApCompatible};
use super::wsjt77::{is_plausible_message, unpack77};
fn ap_max_errors(strictness: DecodeStrictness, locked_bits: usize) -> u32 {
match (strictness, locked_bits >= 55) {
(DecodeStrictness::Strict, true) => 20,
(DecodeStrictness::Strict, false) => 24,
(DecodeStrictness::Normal, true) => 25,
(DecodeStrictness::Normal, false) => 30,
(DecodeStrictness::Deep, true) => 30,
(DecodeStrictness::Deep, false) => 36,
}
}
pub fn ap_bits_for<P: Protocol>(hint: &ApHint) -> (Vec<u8>, Vec<u8>)
where
P::Msg: WsjtApCompatible,
{
hint.build_bits(P::Fec::N)
}
pub fn ap_passes(base: &ApHint) -> Vec<(ApHint, u8)> {
let mut passes = Vec::new();
if base.call1.is_some() && base.call2.is_some() {
for (rpt, pid) in [("RRR", 9u8), ("RR73", 10), ("73", 11)] {
passes.push((base.clone().with_report(rpt), pid));
}
}
if base.call2.is_some() && base.call1.is_none() {
passes.push((base.clone().with_call1("CQ"), 7));
}
if base.call1.is_some() && base.call2.is_some() {
passes.push((base.clone(), 8));
}
passes.push((base.clone(), 6));
passes
}
pub fn process_candidate_ap<P: Protocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
ds_cfg: &DownsampleCfg,
depth: DecodeDepth,
strictness: DecodeStrictness,
eq_mode: EqMode,
refine_steps: i32,
sync_q_min: u32,
ap_hint: Option<&ApHint>,
) -> Option<DecodeResult>
where
P::Msg: WsjtApCompatible,
{
let ds_rate = 12_000.0 / P::NDOWN as f32;
let tx_start = P::TX_START_OFFSET_S;
let cd0 = downsample_cached(fft_cache, cand.freq_hz, ds_cfg);
let refined = refine_candidate::<P>(&cd0, cand, refine_steps);
let i_start = ((refined.dt_sec + tx_start) * ds_rate).round() as usize;
let cs_raw = symbol_spectra::<P>(&cd0, i_start);
let nsync = sync_quality::<P>(&cs_raw);
if nsync <= sync_q_min {
return None;
}
let per_block = fine_sync_power_per_block::<P>(&cd0, i_start);
let sync_cv = if !per_block.is_empty() {
let n = per_block.len() as f32;
let mean = per_block.iter().sum::<f32>() / n;
if mean > f32::EPSILON {
(per_block.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / n).sqrt() / mean
} else {
0.0
}
} else {
0.0
};
let fec = P::Fec::default();
let cs_eq = {
let mut v = cs_raw.clone();
equalize_local::<P>(&mut v);
v
};
#[cfg(feature = "eq-fallback")]
let try_order: &[(&[Complex<f32>], bool)] = match eq_mode {
EqMode::Off => &[(&cs_raw, false)],
EqMode::Local => &[(&cs_eq, true)],
EqMode::Adaptive => &[(&cs_eq, true), (&cs_raw, false)],
};
#[cfg(not(feature = "eq-fallback"))]
let try_order: &[(&[Complex<f32>], bool)] = match eq_mode {
EqMode::Off => &[(&cs_raw, false)],
EqMode::Local | EqMode::Adaptive => &[(&cs_eq, true)],
};
for (cs_ref, _used_eq) in try_order {
let cs_ref: &[Complex<f32>] = cs_ref;
let llr_set = compute_llr::<P>(cs_ref);
let variants: Vec<(&Vec<f32>, u8)> = match depth {
DecodeDepth::Bp => vec![(&llr_set.llra, 0)],
DecodeDepth::BpAll | DecodeDepth::BpAllOsd => vec![
(&llr_set.llra, 0),
(&llr_set.llrb, 1),
(&llr_set.llrc, 2),
(&llr_set.llrd, 3),
],
};
for (llr, pass_id) in &variants {
let bp_opts = FecOpts {
bp_max_iter: 30,
osd_depth: 0,
ap_mask: None,
verify_info: Some(<P::Msg as crate::core::MessageCodec>::verify_info),
};
if let Some(r) = fec.decode_soft(llr, &bp_opts)
&& let Some(res) =
finalise_result::<P>(&r, cand, &refined, sync_cv, *pass_id, cs_ref, None, &fec)
{
return Some(res);
}
}
if let Some(hint) = ap_hint
&& hint.has_info()
{
for (ap_cfg, pass_id) in ap_passes(hint) {
let (mask, values) = ap_bits_for::<P>(&ap_cfg);
let locked = mask.iter().filter(|&&m| m != 0).count();
let max_errors = ap_max_errors(strictness, locked);
for (llr, _) in &variants {
let ap_opts = FecOpts {
bp_max_iter: 30,
osd_depth: 0,
ap_mask: Some((&mask, &values)),
verify_info: Some(<P::Msg as crate::core::MessageCodec>::verify_info),
};
if let Some(r) = fec.decode_soft(llr, &ap_opts)
&& r.hard_errors < max_errors
&& let Some(res) = finalise_result::<P>(
&r,
cand,
&refined,
sync_cv,
pass_id,
cs_ref,
Some(&ap_cfg),
&fec,
)
{
return Some(res);
}
if depth == DecodeDepth::BpAllOsd {
#[cfg(feature = "osd-deep")]
let depths: &[u32] = if locked >= 55 { &[2, 3] } else { &[2] };
#[cfg(not(feature = "osd-deep"))]
let depths: &[u32] = &[2];
let _ = locked;
for &od in depths {
let osd_opts = FecOpts {
bp_max_iter: 30,
osd_depth: od,
ap_mask: Some((&mask, &values)),
verify_info: Some(
<P::Msg as crate::core::MessageCodec>::verify_info,
),
};
if let Some(r) = fec.decode_soft(llr, &osd_opts)
&& r.hard_errors < max_errors
&& let Some(res) = finalise_result::<P>(
&r,
cand,
&refined,
sync_cv,
pass_id,
cs_ref,
Some(&ap_cfg),
&fec,
)
{
return Some(res);
}
}
}
}
}
}
}
None
}
fn finalise_result<P: Protocol>(
fec_result: &crate::core::FecResult,
cand: &SyncCandidate,
refined: &SyncCandidate,
sync_cv: f32,
pass_id: u8,
cs: &[Complex<f32>],
ap_cfg: Option<&ApHint>,
fec: &P::Fec,
) -> Option<DecodeResult> {
let msg77: [u8; 77] = fec_result.info[..77].try_into().ok()?;
let text = unpack77(&msg77)?;
if text.is_empty() || !is_plausible_message(&text) {
return None;
}
if let Some(ap) = ap_cfg {
let upper = text.to_uppercase();
if let Some(ref c1) = ap.call1
&& !upper.contains(&c1.to_uppercase())
{
return None;
}
if let Some(ref c2) = ap.call2
&& !upper.contains(&c2.to_uppercase())
{
return None;
}
}
let mut cw = vec![0u8; P::Fec::N];
fec.encode(&fec_result.info, &mut cw);
let itone = codeword_to_itone::<P>(&cw);
let snr_db = compute_snr_db::<P>(cs, &itone);
Some(DecodeResult {
info: fec_result.info.clone().into_boxed_slice(),
freq_hz: cand.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: fec_result.hard_errors,
sync_score: refined.score,
pass: pass_id,
sync_cv,
snr_db,
})
}
#[allow(clippy::too_many_arguments)]
pub fn decode_sniper_ap<P: Protocol>(
audio: &[i16],
ds_cfg: &DownsampleCfg,
target_freq: f32,
search_hz: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
refine_steps: i32,
sync_q_min: u32,
ap_hint: Option<&ApHint>,
) -> Vec<DecodeResult>
where
P::Msg: WsjtApCompatible,
{
let freq_min = (target_freq - search_hz).max(100.0);
let freq_max = (target_freq + search_hz).min(5_900.0);
let candidates = coarse_sync::<P>(
audio,
freq_min,
freq_max,
sync_min,
Some(target_freq),
max_cand,
);
if candidates.is_empty() {
return Vec::new();
}
let has_ap = ap_hint.is_some_and(|h| h.has_info());
let fft_cache = build_fft_cache(audio, ds_cfg);
let mut results: Vec<DecodeResult> = Vec::new();
for cand in &candidates {
if let Some(r) = process_candidate_ap::<P>(
cand,
&fft_cache,
ds_cfg,
depth,
strictness,
eq_mode,
refine_steps,
sync_q_min,
ap_hint,
) {
let new = !results.iter().any(|x| x.info == r.info);
if new {
results.push(r);
if has_ap {
break;
}
}
}
}
results
}