#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub use super::equalizer::EqMode;
use super::{
downsample::{build_fft_cache, downsample},
equalizer,
ldpc::{
bp::{bp_decode, check_crc14},
osd::{osd_decode, osd_decode_deep, osd_decode_deep4},
},
llr::{compute_llr, compute_snr_db, symbol_spectra, sync_quality},
message::pack28,
params::{BP_MAX_ITER, LDPC_N},
subtract::subtract_signal_weighted,
sync::{SyncCandidate, coarse_sync, fine_sync_power_split, refine_candidate},
wave_gen::message_to_tones,
};
pub type FftCache = Vec<num_complex::Complex<f32>>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DecodeDepth {
Bp,
BpAll,
BpAllOsd,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum DecodeStrictness {
Strict,
#[default]
Normal,
Deep,
}
impl DecodeStrictness {
pub fn osd_max_errors(self, osd_depth: u8) -> u32 {
match (self, osd_depth) {
(Self::Strict, 3) => 20,
(Self::Strict, 4) => 24,
(Self::Strict, _) => 22,
(Self::Normal, 3) => 26,
(Self::Normal, 4) => 30,
(Self::Normal, _) => 29,
(Self::Deep, 3) => 30,
(Self::Deep, 4) => 36,
(Self::Deep, _) => 40,
}
}
pub fn ap_max_errors(self, locked_bits: usize) -> u32 {
match (self, locked_bits >= 55) {
(Self::Strict, true) => 20,
(Self::Strict, false) => 24,
(Self::Normal, true) => 25,
(Self::Normal, false) => 30,
(Self::Deep, true) => 30,
(Self::Deep, false) => 36,
}
}
pub fn osd_score_min(self) -> f32 {
match self {
Self::Strict => 3.0,
Self::Normal => 2.2,
Self::Deep => 2.0,
}
}
}
#[derive(Debug, Clone)]
pub struct DecodeResult {
pub message77: [u8; 77],
pub freq_hz: f32,
pub dt_sec: f32,
pub hard_errors: u32,
pub sync_score: f32,
pub pass: u8,
pub sync_cv: f32,
pub snr_db: f32,
}
#[derive(Debug, Clone, Default)]
pub struct ApHint {
pub call1: Option<String>,
pub call2: Option<String>,
pub grid: Option<String>,
pub report: Option<String>,
}
impl ApHint {
pub fn new() -> Self {
Self::default()
}
pub fn with_call1(mut self, call: &str) -> Self {
self.call1 = Some(call.to_string());
self
}
pub fn with_call2(mut self, call: &str) -> Self {
self.call2 = Some(call.to_string());
self
}
pub fn with_grid(mut self, grid: &str) -> Self {
self.grid = Some(grid.to_string());
self
}
pub fn with_report(mut self, rpt: &str) -> Self {
self.report = Some(rpt.to_string());
self
}
pub fn has_info(&self) -> bool {
self.call1.is_some() || self.call2.is_some()
}
pub fn build_ap(&self, apmag: f32) -> ([bool; LDPC_N], [f32; LDPC_N]) {
let mut mask = [false; LDPC_N];
let mut ap_llr = [0.0f32; LDPC_N];
let mut set_call_bits = |call: &str, start: usize| {
if let Some(n28) = pack28(call) {
for i in 0..28 {
let bit = ((n28 >> (27 - i)) & 1) as u8;
mask[start + i] = true;
ap_llr[start + i] = if bit == 1 { apmag } else { -apmag };
}
mask[start + 28] = true;
ap_llr[start + 28] = -apmag; }
};
if let Some(ref c1) = self.call1 {
set_call_bits(c1, 0); }
if let Some(ref c2) = self.call2 {
set_call_bits(c2, 29); }
if let Some(ref grid) = self.grid
&& let Some(igrid) = super::message::pack_grid4(grid)
{
mask[58] = true;
ap_llr[58] = -apmag; for i in 0..15 {
let bit = ((igrid >> (14 - i)) & 1) as u8;
mask[59 + i] = true;
ap_llr[59 + i] = if bit == 1 { apmag } else { -apmag };
}
}
if let Some(ref rpt) = self.report {
let igrid_val: Option<u32> = match rpt.as_str() {
"RRR" => Some(32_400 + 2),
"RR73" => Some(32_400 + 3),
"73" => Some(32_400 + 4),
_ => None,
};
if let Some(igrid) = igrid_val {
mask[58] = true;
ap_llr[58] = -apmag; for i in 0..15 {
let bit = ((igrid >> (14 - i)) & 1) as u8;
mask[59 + i] = true;
ap_llr[59 + i] = if bit == 1 { apmag } else { -apmag };
}
}
}
if self.has_info() {
mask[74] = true;
ap_llr[74] = -apmag; mask[75] = true;
ap_llr[75] = -apmag; mask[76] = true;
ap_llr[76] = apmag; }
(mask, ap_llr)
}
}
pub fn decode_frame(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_frame_inner(
audio,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
DecodeStrictness::Normal,
&[],
EqMode::Off,
None,
)
.0
}
pub fn decode_frame_with_cache(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> (Vec<DecodeResult>, FftCache) {
decode_frame_inner(
audio,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
DecodeStrictness::Normal,
&[],
EqMode::Off,
None,
)
}
fn process_candidate(
cand: &SyncCandidate,
audio: &[i16],
fft_cache: &[num_complex::Complex<f32>],
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
) -> Option<DecodeResult> {
let osd_score_min = strictness.osd_score_min();
let (cd0, _) = downsample(audio, cand.freq_hz, Some(fft_cache));
let refined = refine_candidate(&cd0, cand, 10);
let i_start = ((refined.dt_sec + 0.5) * 200.0).round() as usize;
let cs_raw = symbol_spectra(&cd0, i_start);
let nsync = sync_quality(&cs_raw);
if nsync <= 6 {
return None;
}
let sync_cv = {
let (sa, sb, sc) = fine_sync_power_split(&cd0, i_start);
let mean = (sa + sb + sc) / 3.0;
if mean > f32::EPSILON {
let sq = (sa - mean).powi(2) + (sb - mean).powi(2) + (sc - mean).powi(2);
sq.sqrt() / mean
} else {
0.0
}
};
let try_decode = |cs: &[[num_complex::Complex<f32>; 8]; 79],
use_ap: bool|
-> Option<DecodeResult> {
let llr_set = compute_llr(cs);
let llr_variants: &[(&[f32; LDPC_N], u8)] = match depth {
DecodeDepth::Bp => &[(&llr_set.llra, 0)],
DecodeDepth::BpAll | DecodeDepth::BpAllOsd => &[
(&llr_set.llra, 0),
(&llr_set.llrb, 1),
(&llr_set.llrc, 2),
(&llr_set.llrd, 3),
],
};
for &(llr, pass_id) in llr_variants {
if let Some(bp) = bp_decode(llr, None, BP_MAX_ITER, Some(check_crc14)) {
let itone = message_to_tones(&bp.message77);
let snr_db = compute_snr_db(cs, &itone);
return Some(DecodeResult {
message77: bp.message77,
freq_hz: cand.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: bp.hard_errors,
sync_score: refined.score,
pass: pass_id,
sync_cv,
snr_db,
});
}
}
if depth == DecodeDepth::BpAllOsd && nsync >= 12 && cand.score >= osd_score_min {
let freq_dup = known
.iter()
.any(|r| (r.freq_hz - cand.freq_hz).abs() < 20.0);
if !freq_dup {
let osd_depth: u8 = if nsync >= 18 { 3 } else { 2 };
for llr_osd in [&llr_set.llra, &llr_set.llrb, &llr_set.llrc, &llr_set.llrd] {
let osd_result = if osd_depth == 3 {
osd_decode_deep(llr_osd, 3, Some(check_crc14))
} else {
osd_decode(llr_osd)
};
if let Some(osd) = osd_result {
let max_errors = strictness.osd_max_errors(osd_depth);
if osd.hard_errors >= max_errors {
continue;
}
let itone = message_to_tones(&osd.message77);
let snr_db = compute_snr_db(cs, &itone);
return Some(DecodeResult {
message77: osd.message77,
freq_hz: cand.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: osd.hard_errors,
sync_score: refined.score,
pass: if osd_depth == 3 { 5 } else { 4 },
sync_cv,
snr_db,
});
}
}
if nsync >= 18 {
for llr_osd in [&llr_set.llra, &llr_set.llrb, &llr_set.llrc, &llr_set.llrd] {
if let Some(osd4) = osd_decode_deep4(llr_osd, 30, Some(check_crc14)) {
let max_errors = strictness.osd_max_errors(4);
if osd4.hard_errors >= max_errors {
continue;
}
let itone = message_to_tones(&osd4.message77);
let snr_db = compute_snr_db(cs, &itone);
return Some(DecodeResult {
message77: osd4.message77,
freq_hz: cand.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: osd4.hard_errors,
sync_score: refined.score,
pass: 13,
sync_cv,
snr_db,
});
}
}
}
}
}
if use_ap
&& let Some(ap) = ap_hint
&& ap.has_info()
{
let apmag = llr_set.llra.iter().map(|v| v.abs()).fold(0.0f32, f32::max) * 1.01;
let mut ap_passes: Vec<(ApHint, u8)> = Vec::new();
if ap.call1.is_some() && ap.call2.is_some() {
for (rpt, pid) in [("RRR", 9u8), ("RR73", 10), ("73", 11)] {
let ap_full = ap.clone().with_report(rpt);
ap_passes.push((ap_full, pid));
}
}
if ap.call2.is_some() && ap.call1.is_none() {
let ap7 = ap.clone().with_call1("CQ");
ap_passes.push((ap7, 7));
}
if ap.call1.is_some() && ap.call2.is_some() {
ap_passes.push((ap.clone(), 8));
}
ap_passes.push((ap.clone(), 6));
for (ap_cfg, pass_id) in &ap_passes {
let (ap_mask, ap_llr_override) = ap_cfg.build_ap(apmag);
let locked_bits = ap_mask.iter().filter(|&&m| m).count();
let max_errors: u32 = strictness.ap_max_errors(locked_bits);
for &(base_llr, _) in llr_variants {
let mut llr_ap = *base_llr;
for i in 0..LDPC_N {
if ap_mask[i] {
llr_ap[i] = ap_llr_override[i];
}
}
let check_result =
|msg77: [u8; 77], hard_errors: u32| -> Option<DecodeResult> {
if hard_errors >= max_errors {
return None;
}
let text = super::message::unpack77(&msg77)?;
if !super::message::is_plausible_message(&text) {
return None;
}
let upper = text.to_uppercase();
if let Some(ref c1) = ap_cfg.call1
&& !upper.contains(&c1.to_uppercase())
{
return None;
}
if let Some(ref c2) = ap_cfg.call2
&& !upper.contains(&c2.to_uppercase())
{
return None;
}
let itone = message_to_tones(&msg77);
let snr_db = compute_snr_db(cs, &itone);
Some(DecodeResult {
message77: msg77,
freq_hz: cand.freq_hz,
dt_sec: refined.dt_sec,
hard_errors,
sync_score: refined.score,
pass: *pass_id,
sync_cv,
snr_db,
})
};
if let Some(bp) =
bp_decode(&llr_ap, Some(&ap_mask), BP_MAX_ITER, Some(check_crc14))
&& let Some(r) = check_result(bp.message77, bp.hard_errors)
{
return Some(r);
}
if depth == DecodeDepth::BpAllOsd
&& let Some(osd) = osd_decode_deep(&llr_ap, 2, Some(check_crc14))
&& let Some(r) = check_result(osd.message77, osd.hard_errors)
{
return Some(r);
}
}
}
}
None
};
match eq_mode {
EqMode::Off => try_decode(&cs_raw, true),
EqMode::Local => {
let mut cs_eq = cs_raw.clone();
equalizer::equalize_local(&mut cs_eq);
try_decode(&cs_eq, true)
}
EqMode::Adaptive => {
let mut cs_eq = cs_raw.clone();
equalizer::equalize_local(&mut cs_eq);
if let Some(r) = try_decode(&cs_eq, true) {
return Some(r);
}
try_decode(&cs_raw, true)
}
}
}
fn decode_frame_inner(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
precomputed_fft: Option<&[num_complex::Complex<f32>]>,
) -> (Vec<DecodeResult>, Vec<num_complex::Complex<f32>>) {
let candidates = coarse_sync(audio, freq_min, freq_max, sync_min, freq_hint, max_cand);
if candidates.is_empty() {
let fft_cache = match precomputed_fft {
Some(c) => c.to_vec(),
None => build_fft_cache(audio),
};
return (Vec::new(), fft_cache);
}
let fft_cache = match precomputed_fft {
Some(c) => c.to_vec(),
None => build_fft_cache(audio),
};
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
process_candidate(
cand, audio, &fft_cache, depth, strictness, known, eq_mode, None,
)
})
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<DecodeResult> = candidates
.iter()
.filter_map(|cand| {
process_candidate(
cand, audio, &fft_cache, depth, strictness, known, eq_mode, None,
)
})
.collect();
let mut results: Vec<DecodeResult> = Vec::new();
for r in raw {
if !known.iter().any(|k| k.message77 == r.message77)
&& !results.iter().any(|x| x.message77 == r.message77)
{
results.push(r);
}
}
(results, fft_cache)
}
pub fn decode_frame_subtract(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
let mut residual = audio.to_vec();
let mut all_results: Vec<DecodeResult> = Vec::new();
let passes: &[f32] = &[1.0, 0.75, 0.5];
for &factor in passes {
let (new, _) = decode_frame_inner(
&residual,
freq_min,
freq_max,
sync_min * factor,
freq_hint,
depth,
max_cand,
strictness,
&all_results,
EqMode::Off,
None,
);
for r in &new {
let sub_gain = if r.sync_cv > 0.3 { 0.5 } else { 1.0 };
subtract_signal_weighted(&mut residual, r, sub_gain);
}
all_results.extend(new);
}
all_results
}
pub fn decode_frame_subtract_with_known(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
known: &[DecodeResult],
precomputed_fft: Option<FftCache>,
) -> Vec<DecodeResult> {
let mut residual = audio.to_vec();
let mut all_results: Vec<DecodeResult> = known.to_vec();
let known_count = known.len();
let passes: &[f32] = &[1.0, 0.75, 0.5];
for (i, &factor) in passes.iter().enumerate() {
let fft = if i == 0 {
precomputed_fft.as_deref()
} else {
None
};
let (new, _) = decode_frame_inner(
&residual,
freq_min,
freq_max,
sync_min * factor,
freq_hint,
depth,
max_cand,
strictness,
&all_results,
EqMode::Off,
fft,
);
for r in &new {
let sub_gain = if r.sync_cv > 0.3 { 0.5 } else { 1.0 };
subtract_signal_weighted(&mut residual, r, sub_gain);
}
all_results.extend(new);
}
all_results.split_off(known_count)
}
pub fn decode_sniper(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_sniper_eq(audio, target_freq, depth, max_cand, EqMode::Off)
}
pub fn decode_sniper_eq(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
eq_mode: EqMode,
) -> Vec<DecodeResult> {
decode_sniper_ap(audio, target_freq, depth, max_cand, eq_mode, None)
}
pub fn decode_sniper_ap(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
) -> Vec<DecodeResult> {
decode_sniper_inner(audio, target_freq, depth, max_cand, eq_mode, ap_hint, 0.8)
}
pub fn decode_sniper_sic(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
) -> Vec<DecodeResult> {
let pass1 = decode_sniper_inner(audio, target_freq, depth, max_cand, eq_mode, ap_hint, 0.8);
let mut residual: Vec<i16> = audio.to_vec();
let mut subtracted = false;
for r in &pass1 {
if (r.freq_hz - target_freq).abs() > 25.0 {
let gain = if r.sync_cv > 0.3 { 0.5 } else { 1.0 };
subtract_signal_weighted(&mut residual, r, gain);
subtracted = true;
}
}
if !subtracted {
return pass1;
}
let pass2 = decode_sniper_inner(
&residual,
target_freq,
depth,
max_cand,
eq_mode,
ap_hint,
0.6,
);
let mut results = pass1;
for r in pass2 {
if !results.iter().any(|x| x.message77 == r.message77) {
results.push(r);
}
}
results
}
fn decode_sniper_inner(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
sync_min: f32,
) -> Vec<DecodeResult> {
let freq_min = (target_freq - 250.0).max(100.0);
let freq_max = (target_freq + 250.0).min(5900.0);
let candidates = coarse_sync(
audio,
freq_min,
freq_max,
sync_min,
Some(target_freq),
max_cand,
);
if candidates.is_empty() {
return Vec::new();
}
let fft_cache = build_fft_cache(audio);
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
process_candidate(
cand,
audio,
&fft_cache,
depth,
DecodeStrictness::Normal,
&[],
eq_mode,
ap_hint,
)
})
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<DecodeResult> = candidates
.iter()
.filter_map(|cand| {
process_candidate(
cand,
audio,
&fft_cache,
depth,
DecodeStrictness::Normal,
&[],
eq_mode,
ap_hint,
)
})
.collect();
let mut results: Vec<DecodeResult> = Vec::new();
for r in raw {
if !results.iter().any(|x| x.message77 == r.message77) {
results.push(r);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn silence_no_decode() {
let audio = vec![0i16; 15 * 12_000];
let results = decode_frame(&audio, 200.0, 2800.0, 1.0, None, DecodeDepth::Bp, 10);
assert!(results.is_empty(), "silence should decode nothing");
}
#[test]
fn sniper_silence_no_decode() {
let audio = vec![0i16; 15 * 12_000];
let results = decode_sniper(&audio, 1000.0, DecodeDepth::Bp, 10);
assert!(results.is_empty());
}
#[test]
fn dt_accuracy_at_nominal_start() {
use super::super::message::pack77_type1;
use super::super::wave_gen::{message_to_tones, tones_to_f32};
let msg = pack77_type1("CQ", "JA1ABC", "PM95").unwrap();
let itone = message_to_tones(&msg);
let pcm = tones_to_f32(&itone, 1000.0, 1.0);
let mut audio_f32 = vec![0.0f32; 180_000];
let start = (0.5 * 12000.0) as usize; for (i, &s) in pcm.iter().enumerate() {
if start + i < audio_f32.len() {
audio_f32[start + i] = s;
}
}
let audio: Vec<i16> = audio_f32
.iter()
.map(|&s| (s * 20000.0).clamp(-32767.0, 32767.0) as i16)
.collect();
let results = decode_frame(&audio, 100.0, 3000.0, 1.0, None, DecodeDepth::BpAllOsd, 200);
assert!(!results.is_empty(), "should decode the signal");
let dt = results[0].dt_sec;
eprintln!("DT = {dt:+.3} s (expected ≈ 0.0)");
assert!(dt.abs() < 0.5, "DT={dt} is too far from 0");
}
}