use alloc::boxed::Box;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use super::super::decode::{ApHint, DecodeDepth, DecodeResult, DecodeStrictness};
use super::super::llr::sync_quality;
use super::super::message::unpack77;
use super::super::params::{COSTAS, DEFAULT_BP_MAX_ITER, LDPC_N, NSPS, NTONES};
use super::super::wave_gen::message_to_tones;
use super::coarse_sync::coarse_sync;
#[cfg(all(feature = "fixed-point", not(feature = "fft-rustfft")))]
use super::fill_symbol_spectra::fill_symbol_spectra_goertzel;
use super::fill_symbol_spectra::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use super::spectrogram::{Spectrogram, compute_spectrogram};
use super::types::{
AudioSample, DEFAULT_Q_THRESH, NFFT_SPEC, NMS_ALPHA, NSTEP, SAMPLE_RATE_HZ, TONE_SPACING_HZ,
TX_START_OFFSET_S,
};
use crate::core::scalar::{Cmplx, ComplexSpec};
use crate::core::sync::SyncCandidate;
use crate::fec::ldpc::bp::check_crc14;
#[cfg(feature = "fft-rustfft")]
use crate::fec::ldpc::osd::osd_decode_deep;
pub fn decode_block<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_block_multipass(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
DEFAULT_BP_MAX_ITER,
None,
DecodeStrictness::Normal,
)
}
#[allow(clippy::too_many_arguments)]
pub fn decode_block_tuned<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
bp_max_iter: u32,
) -> Vec<DecodeResult> {
decode_block_multipass(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
bp_max_iter,
None,
DecodeStrictness::Normal,
)
}
#[cfg(feature = "fft-rustfft")]
#[allow(clippy::too_many_arguments)]
pub fn decode_block_with_ap<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
ap_hint: Option<&ApHint>,
) -> Vec<DecodeResult> {
decode_block_multipass(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
DEFAULT_BP_MAX_ITER,
ap_hint,
DecodeStrictness::Normal,
)
}
#[cfg(feature = "fft-rustfft")]
#[allow(clippy::too_many_arguments)]
pub fn decode_block_with_ap_tuned<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
bp_max_iter: u32,
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
decode_block_multipass(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
bp_max_iter,
ap_hint,
strictness,
)
}
#[cfg(feature = "fft-rustfft")]
#[allow(clippy::too_many_arguments)]
fn decode_block_multipass<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
bp_max_iter: u32,
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
use alloc::vec::Vec as AllocVec;
let mut work: AllocVec<i16> = audio.iter().map(|s| s.to_i16()).collect();
let mut all: AllocVec<DecodeResult> = AllocVec::new();
let mut prev_total: usize = 0;
let mut sbase_and_spec: Option<(AllocVec<f32>, Spectrogram)> = None;
for ipass in 0..3 {
if ipass >= 1 && all.len() == prev_total {
break;
}
prev_total = all.len();
let spec = compute_spectrogram(work.as_slice(), freq_max);
if ipass == 0 {
let mut avg = alloc::vec![0.0_f32; spec.n_freq];
crate::ft8::baseline::avg_spectrum(&spec, &mut avg);
let sbase_v = crate::ft8::baseline::fit_baseline(&avg, 0, spec.n_freq - 1);
let spec_clone = Spectrogram {
n_freq: spec.n_freq,
n_time: spec.n_time,
data: spec.data.clone(),
};
sbase_and_spec = Some((sbase_v, spec_clone));
}
let cands = coarse_sync(&spec, freq_min, freq_max, sync_min, pass1_limit());
drop(spec);
let cands = fine_refine_pass1(work.as_slice(), cands);
let pass2 = refine_candidates(work.as_slice(), cands, max_cand);
#[cfg(feature = "std")]
let trace = std::env::var("MFSK_TRACE_PHANTOM").is_ok();
#[cfg(not(feature = "std"))]
let trace = false;
for cand in pass2 {
let single_results = process_candidates_tuned_with_ap(
work.as_slice(),
alloc::vec![cand],
depth,
DEFAULT_Q_THRESH,
bp_max_iter,
ap_hint,
strictness,
);
for r in single_results {
if all.iter().any(|x| x.message77 == r.message77) {
continue;
}
if trace {
#[cfg(feature = "std")]
if let Some(text) = crate::msg::wsjt77::unpack77(&r.message77) {
eprintln!(
" TRACE pass={} freq={:>7.2} dt={:+.4} e={:>2} '{}'",
ipass, r.freq_hz, r.dt_sec, r.hard_errors, text,
);
}
}
crate::ft8::subtract::subtract_signal_lpf(work.as_mut_slice(), &r);
all.push(r);
}
}
}
{
let auto_ap_decodes = super::auto_ap_strategy::run(
audio,
freq_min,
freq_max,
sync_min,
max_cand,
&all,
depth,
bp_max_iter,
strictness,
);
for r in auto_ap_decodes {
if all.iter().any(|x| x.message77 == r.message77) {
continue;
}
crate::ft8::subtract::subtract_signal_lpf(work.as_mut_slice(), &r);
all.push(r);
}
}
#[cfg(not(feature = "fixed-point"))]
if let Some((sbase, spec)) = sbase_and_spec {
let df = SAMPLE_RATE_HZ / NFFT_SPEC as f32;
let tstep = NSTEP as f32 / SAMPLE_RATE_HZ;
let nsps_steps = (NSPS / NSTEP) as f32;
all.retain_mut(|r| {
let raw_snr = recompute_snr_xsnr2(r, &spec, &sbase, df, tstep, nsps_steps, 1.0);
let nsync = recompute_nsync(r, &spec, df, tstep, nsps_steps);
if nsync <= 10 && raw_snr < -24.0 {
return false;
}
r.snr_db = raw_snr.max(-24.0);
true
});
}
#[cfg(feature = "fixed-point")]
let _ = sbase_and_spec;
all
}
#[cfg(all(feature = "fft-rustfft", not(feature = "fixed-point")))]
fn recompute_nsync(
result: &DecodeResult,
spec: &Spectrogram,
df: f32,
tstep: f32,
nsps_steps: f32,
) -> u32 {
use crate::ft8::params::COSTAS;
const NTONES: usize = 8;
let carrier_bin_f = result.freq_hz / df;
let tone_step = TONE_SPACING_HZ / df; let t0 = (TX_START_OFFSET_S + result.dt_sec) / tstep;
let mut count = 0u32;
for &block_off in &[0_usize, 36, 72] {
for (sym_in_block, &expected) in COSTAS.iter().enumerate() {
let k = block_off + sym_in_block;
let m_bin = (t0 + (k as f32) * nsps_steps).round() as i32;
if m_bin < 0 || m_bin as usize >= spec.n_time {
continue;
}
let m_bin = m_bin as usize;
let mut best_t = 0;
let mut best_p = f32::MIN;
for t in 0..NTONES {
let f_bin = (carrier_bin_f + (t as f32) * tone_step).round() as i32;
if f_bin < 0 || f_bin as usize >= spec.n_freq {
continue;
}
let p = spec.power_acc(f_bin as usize, m_bin);
if p > best_p {
best_p = p;
best_t = t;
}
}
if best_t == expected {
count += 1;
}
}
}
count
}
pub fn xsnr2_db_simple(spec: &Spectrogram, result: &DecodeResult, cell_scale: f32) -> f32 {
use crate::ft8::params::NN;
use crate::ft8::wave_gen::message_to_tones;
let df = SAMPLE_RATE_HZ / NFFT_SPEC as f32;
let tstep = NSTEP as f32 / SAMPLE_RATE_HZ;
let nsps_steps = (NSPS / NSTEP) as f32;
let tone_step = TONE_SPACING_HZ / df;
if spec.n_freq == 0 || spec.n_time == 0 {
return -24.0;
}
let carrier_bin = (result.freq_hz / df)
.round()
.clamp(0.0, (spec.n_freq - 1) as f32) as usize;
let f_lo = carrier_bin.saturating_sub(50);
let f_hi = (carrier_bin + 50).min(spec.n_freq - 1);
let t_stride = spec.n_time.div_ceil(50).max(1);
let mut samples: alloc::vec::Vec<f32> =
alloc::vec::Vec::with_capacity((f_hi - f_lo + 1) * spec.n_time.div_ceil(t_stride) + 4);
for f in f_lo..=f_hi {
let mut t = 0usize;
while t < spec.n_time {
samples.push(spec.power_acc(f, t));
t += t_stride;
}
}
let median = if samples.is_empty() {
0.0
} else {
let mid = samples.len() / 2;
samples.select_nth_unstable_by(mid, |a, b| {
a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
});
samples[mid]
};
let xbase = median * cell_scale;
if xbase <= 0.0 || !xbase.is_finite() {
return -24.0;
}
let itone = message_to_tones(&result.message77);
let carrier_bin_f = result.freq_hz / df;
let t0 = (TX_START_OFFSET_S + result.dt_sec) / tstep;
let mut xsig: f32 = 0.0;
for k in 0..NN {
let t = itone[k] as f32;
let f_bin = (carrier_bin_f + t * tone_step).round() as i32;
let m_bin = (t0 + (k as f32) * nsps_steps).round() as i32;
if f_bin < 0 || f_bin as usize >= spec.n_freq || m_bin < 0 || m_bin as usize >= spec.n_time
{
continue;
}
xsig += spec.power_acc(f_bin as usize, m_bin as usize);
}
xsig *= cell_scale;
const XSNR2_CAL_DB: f32 = 46.0;
let ratio = xsig / xbase;
if ratio <= 1.0 {
return -24.0;
}
let snr = 10.0 * ratio.log10() - XSNR2_CAL_DB;
snr.max(-24.0)
}
#[cfg(all(feature = "fft-rustfft", not(feature = "fixed-point")))]
fn recompute_snr_xsnr2(
result: &DecodeResult,
spec: &Spectrogram,
sbase: &[f32],
df: f32,
tstep: f32,
nsps_steps: f32,
cell_scale: f32,
) -> f32 {
let itone = crate::ft8::wave_gen::message_to_tones(&result.message77);
let carrier_bin_f = result.freq_hz / df;
let tone_step = TONE_SPACING_HZ / df;
let t0 = (TX_START_OFFSET_S + result.dt_sec) / tstep;
let mut xsig = 0.0_f32;
for k in 0..79_usize {
let t = itone[k] as f32;
let f_bin = (carrier_bin_f + t * tone_step).round() as i32;
let m_bin = (t0 + (k as f32) * nsps_steps).round() as i32;
if f_bin < 0 || f_bin as usize >= spec.n_freq || m_bin < 0 || m_bin as usize >= spec.n_time
{
continue;
}
xsig += spec.power_acc(f_bin as usize, m_bin as usize);
}
xsig *= cell_scale;
let bin = carrier_bin_f.round() as i32;
let bin = bin.clamp(0, sbase.len() as i32 - 1) as usize;
let sbase_db_compensated = sbase[bin] + 10.0 * cell_scale.log10();
let xbase = 10f32.powf(0.1 * (sbase_db_compensated - 40.0));
let arg = xsig / xbase / 3.0e6 - 1.0;
let xsnr2 = arg.max(0.001);
10.0 * xsnr2.log10() - 27.0
}
#[cfg(not(feature = "fft-rustfft"))]
#[allow(clippy::too_many_arguments)]
fn decode_block_multipass<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
bp_max_iter: u32,
_ap_hint: Option<&ApHint>,
_strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
let spec = compute_spectrogram(audio, freq_max);
let pass1 = coarse_sync(&spec, freq_min, freq_max, sync_min, pass1_limit());
drop(spec);
let pass1 = fine_refine_pass1(audio, pass1);
let pass2 = refine_candidates(audio, pass1, max_cand);
process_candidates_tuned(audio, pass2, depth, DEFAULT_Q_THRESH, bp_max_iter)
}
#[cfg(feature = "fft-rustfft")]
pub(super) fn fine_refine_pass1<S: AudioSample>(
audio: &[S],
cands: alloc::vec::Vec<crate::core::sync::SyncCandidate>,
) -> alloc::vec::Vec<crate::core::sync::SyncCandidate> {
if cands.is_empty() {
return cands;
}
let audio_i16: alloc::vec::Vec<i16> = audio.iter().map(|s| s.to_i16()).collect();
let fft_cache = crate::ft8::downsample::build_fft_cache(&audio_i16);
cands
.into_iter()
.map(|c| {
let (cd0, _) =
crate::ft8::downsample::downsample(&audio_i16, c.freq_hz, Some(&fft_cache));
let r = crate::ft8::refine_fine::fine_refine_3stage(&cd0, c.dt_sec);
crate::core::sync::SyncCandidate {
freq_hz: c.freq_hz + r.delf_hz,
dt_sec: r.dt_sec,
score: c.score,
}
})
.collect()
}
#[cfg(not(feature = "fft-rustfft"))]
pub(super) fn fine_refine_pass1<S: AudioSample>(
_audio: &[S],
cands: alloc::vec::Vec<crate::core::sync::SyncCandidate>,
) -> alloc::vec::Vec<crate::core::sync::SyncCandidate> {
cands
}
#[cfg(feature = "fixed-point")]
pub fn decode_block_into<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
basis_re: &mut [i16],
basis_im: &mut [i16],
) -> Vec<DecodeResult> {
decode_block_into_tuned(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
DEFAULT_BP_MAX_ITER,
basis_re,
basis_im,
)
}
#[cfg(feature = "fixed-point")]
#[allow(clippy::too_many_arguments)]
pub fn decode_block_into_tuned<S: AudioSample>(
audio: &[S],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
bp_max_iter: u32,
basis_re: &mut [i16],
basis_im: &mut [i16],
) -> Vec<DecodeResult> {
let spec = compute_spectrogram(audio, freq_max);
let pass1 = coarse_sync(&spec, freq_min, freq_max, sync_min, pass1_limit());
drop(spec);
let pass2 = refine_candidates_into(audio, pass1, max_cand, basis_re, basis_im);
process_candidates_into_tuned(
audio,
pass2,
depth,
DEFAULT_Q_THRESH,
bp_max_iter,
basis_re,
basis_im,
)
}
const PASS1_LIMIT_DEFAULT: usize = 30;
pub(super) fn pass1_limit() -> usize {
#[cfg(feature = "std")]
{
if let Ok(s) = std::env::var("MFSK_PASS1_LIMIT")
&& let Ok(v) = s.parse::<usize>()
{
return v;
}
}
PASS1_LIMIT_DEFAULT
}
pub type RefinedCandidate = (SyncCandidate, Box<[[Cmplx<f32>; 8]; 79]>, u32);
pub(super) fn refine_candidates<S: AudioSample>(
audio: &[S],
cands: Vec<SyncCandidate>,
max_cand: usize,
) -> Vec<RefinedCandidate> {
refine_candidates_with(cands, max_cand, |c| {
symbol_spectra_direct(audio, c.freq_hz, c.dt_sec, SymMask::SyncBlock0)
})
}
#[cfg(feature = "fixed-point")]
pub fn refine_candidates_into<S: AudioSample>(
audio: &[S],
cands: Vec<SyncCandidate>,
max_cand: usize,
basis_re: &mut [i16],
basis_im: &mut [i16],
) -> Vec<RefinedCandidate> {
use super::super::params::NN;
use super::fill_symbol_spectra::fill_symbol_spectra_goertzel;
let _ = &basis_re;
let _ = &basis_im;
refine_candidates_with(cands, max_cand, |c| {
let mut cs: alloc::boxed::Box<[[Cmplx<f32>; 8]; NN]> =
alloc::vec![[Cmplx::<f32>::default(); 8]; NN]
.try_into()
.unwrap();
fill_symbol_spectra_goertzel(&mut cs, audio, c.freq_hz, c.dt_sec, SymMask::SyncBlock0);
cs
})
}
fn refine_candidates_with<F>(
cands: Vec<SyncCandidate>,
max_cand: usize,
mut cs_for: F,
) -> Vec<RefinedCandidate>
where
F: FnMut(&SyncCandidate) -> Box<[[Cmplx<f32>; 8]; 79]>,
{
use alloc::collections::BinaryHeap;
use core::cmp::{Ordering, Reverse};
struct Slot {
q: u32,
idx: u32,
cand: SyncCandidate,
cs: Box<[[Cmplx<f32>; 8]; 79]>,
}
impl PartialEq for Slot {
fn eq(&self, other: &Self) -> bool {
self.q == other.q && self.idx == other.idx
}
}
impl Eq for Slot {}
impl Ord for Slot {
fn cmp(&self, other: &Self) -> Ordering {
self.q.cmp(&other.q).then_with(|| self.idx.cmp(&other.idx))
}
}
impl PartialOrd for Slot {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut heap: BinaryHeap<Reverse<Slot>> = BinaryHeap::with_capacity(max_cand + 1);
for (idx, c) in cands.into_iter().enumerate() {
let cs = cs_for(&c);
let q = sync_quality_block0(&cs);
let slot = Slot {
q,
idx: idx as u32,
cand: c,
cs,
};
if heap.len() < max_cand {
heap.push(Reverse(slot));
} else if let Some(Reverse(top)) = heap.peek()
&& slot.q > top.q
{
heap.pop();
heap.push(Reverse(slot));
}
}
let mut out: Vec<RefinedCandidate> = heap
.into_iter()
.map(|r| {
let s = r.0;
(s.cand, s.cs, s.q)
})
.collect();
out.sort_by_key(|r| core::cmp::Reverse(r.2));
out
}
#[doc(hidden)]
pub fn sync_quality_block0<S: crate::core::scalar::SpecScalar>(cs: &[[Cmplx<S>; 8]; 79]) -> u32
where
S::Wide: PartialOrd,
{
let mut count = 0u32;
for (t, &expected) in COSTAS.iter().enumerate() {
let sym = t; let best = (0..NTONES)
.max_by(|&a, &b| {
let na = cs[sym][a].norm_sqr_wide();
let nb = cs[sym][b].norm_sqr_wide();
na.partial_cmp(&nb).unwrap_or(core::cmp::Ordering::Equal)
})
.unwrap_or(0);
if best == expected {
count += 1;
}
}
count
}
#[cfg(feature = "fixed-point")]
type LlrT = crate::core::scalar::Q11i16;
#[cfg(not(feature = "fixed-point"))]
type LlrT = f32;
#[cfg(all(feature = "fft-rustfft", not(feature = "fixed-point")))]
#[inline]
fn bp_step_select<T: crate::core::scalar::LlrScalar>(
bp_scratch: &mut crate::fec::ldpc::bp::BpScratch<crate::fec::ldpc::Ldpc174_91Params, T>,
llr: &[T; LDPC_N],
max_iter: u32,
verify: Option<fn(&[u8]) -> bool>,
) -> Option<crate::fec::ldpc::bp::BpResult> {
if std::env::var("MFSK_BP_KIND").as_deref() == Ok("nms") {
return crate::fec::ldpc::bp::bp_decode_nms_with_scratch::<T>(
bp_scratch, llr, None, max_iter, verify, NMS_ALPHA,
);
}
let llr_f32: [f32; LDPC_N] = core::array::from_fn(|i| llr[i].to_f32());
crate::fec::ldpc::bp::bp_decode(&llr_f32, None, max_iter, verify)
}
#[cfg(any(not(feature = "fft-rustfft"), feature = "fixed-point"))]
#[inline]
fn bp_step_select<T: crate::core::scalar::LlrScalar>(
bp_scratch: &mut crate::fec::ldpc::bp::BpScratch<crate::fec::ldpc::Ldpc174_91Params, T>,
llr: &[T; LDPC_N],
max_iter: u32,
verify: Option<fn(&[u8]) -> bool>,
) -> Option<crate::fec::ldpc::bp::BpResult> {
crate::fec::ldpc::bp::bp_decode_nms_with_scratch::<T>(
bp_scratch, llr, None, max_iter, verify, NMS_ALPHA,
)
}
#[doc(hidden)]
pub fn process_candidates<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
) -> Vec<DecodeResult> {
process_candidates_tuned(audio, cands, depth, q_thresh, DEFAULT_BP_MAX_ITER)
}
#[doc(hidden)]
pub fn process_candidates_tuned<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
) -> Vec<DecodeResult> {
process_candidates_tuned_with_ap(
audio,
cands,
depth,
q_thresh,
bp_max_iter,
None,
DecodeStrictness::Normal,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn process_candidates_tuned_with_ap<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
let mut cs_scratch: alloc::boxed::Box<[[Cmplx<f32>; 8]; 79]> =
alloc::vec![[Cmplx::<f32>::default(); 8]; 79]
.try_into()
.unwrap();
process_candidates_with_ap(
audio,
&cands,
depth,
q_thresh,
bp_max_iter,
&mut cs_scratch,
|cs, cand, mask| {
fill_symbol_spectra(cs, audio, cand.freq_hz, cand.dt_sec, mask, None);
},
ap_hint,
strictness,
)
}
#[cfg(feature = "fft-rustfft")]
#[allow(clippy::too_many_arguments)]
pub(super) fn process_candidates_tuned_with_ap_ref<S: AudioSample>(
audio: &[S],
cands: &[RefinedCandidate],
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
) -> Vec<DecodeResult> {
let mut cs_scratch: alloc::boxed::Box<[[Cmplx<f32>; 8]; 79]> =
alloc::vec![[Cmplx::<f32>::default(); 8]; 79]
.try_into()
.unwrap();
process_candidates_with_ap(
audio,
cands,
depth,
q_thresh,
bp_max_iter,
&mut cs_scratch,
|cs, cand, mask| {
fill_symbol_spectra(cs, audio, cand.freq_hz, cand.dt_sec, mask, None);
},
ap_hint,
strictness,
)
}
#[cfg(feature = "fixed-point")]
#[doc(hidden)]
pub fn process_candidates_into<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
basis_re: &mut [i16],
basis_im: &mut [i16],
) -> Vec<DecodeResult> {
process_candidates_into_tuned(
audio,
cands,
depth,
q_thresh,
DEFAULT_BP_MAX_ITER,
basis_re,
basis_im,
)
}
#[cfg(feature = "fixed-point")]
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
pub fn process_candidates_into_tuned<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
basis_re: &mut [i16],
basis_im: &mut [i16],
) -> Vec<DecodeResult> {
let mut cs_scratch: alloc::boxed::Box<[[Cmplx<f32>; 8]; 79]> =
alloc::vec![[Cmplx::<f32>::default(); 8]; 79]
.try_into()
.unwrap();
process_candidates_into_with_cs_scratch_tuned(
audio,
cands,
depth,
q_thresh,
bp_max_iter,
basis_re,
basis_im,
&mut cs_scratch,
)
}
#[cfg(feature = "fixed-point")]
pub fn process_candidates_into_with_cs_scratch<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
basis_re: &mut [i16],
basis_im: &mut [i16],
cs_scratch: &mut [[Cmplx<f32>; 8]; 79],
) -> Vec<DecodeResult> {
process_candidates_into_with_cs_scratch_tuned(
audio,
cands,
depth,
q_thresh,
DEFAULT_BP_MAX_ITER,
basis_re,
basis_im,
cs_scratch,
)
}
#[cfg(feature = "fixed-point")]
#[allow(clippy::too_many_arguments)]
pub fn process_candidates_into_with_cs_scratch_tuned<S: AudioSample>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
basis_re: &mut [i16],
basis_im: &mut [i16],
cs_scratch: &mut [[Cmplx<f32>; 8]; 79],
) -> Vec<DecodeResult> {
let _ = &basis_re;
let _ = &basis_im;
process_candidates_with_ap(
audio,
&cands,
depth,
q_thresh,
bp_max_iter,
cs_scratch,
|cs, cand, mask| {
#[cfg(feature = "fft-rustfft")]
{
fill_symbol_spectra(cs, audio, cand.freq_hz, cand.dt_sec, mask, None);
}
#[cfg(not(feature = "fft-rustfft"))]
fill_symbol_spectra_goertzel(cs, audio, cand.freq_hz, cand.dt_sec, mask);
},
None,
DecodeStrictness::Normal,
)
}
#[allow(clippy::too_many_arguments)]
pub fn process_candidates_into_with_cs_scratch_tuned_with_fill<S, F>(
audio: &[S],
cands: Vec<RefinedCandidate>,
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
cs_scratch: &mut [[Cmplx<f32>; 8]; 79],
fill: F,
) -> Vec<DecodeResult>
where
S: AudioSample,
F: FnMut(&mut [[Cmplx<f32>; 8]; 79], &SyncCandidate, SymMask),
{
process_candidates_with_ap(
audio,
&cands,
depth,
q_thresh,
bp_max_iter,
cs_scratch,
fill,
None,
DecodeStrictness::Normal,
)
}
#[allow(clippy::too_many_arguments)]
fn process_candidates_with_ap<S: AudioSample, F>(
_audio: &[S],
cands: &[RefinedCandidate],
depth: DecodeDepth,
q_thresh: u32,
bp_max_iter: u32,
cs_scratch: &mut [[Cmplx<f32>; 8]; 79],
mut fill: F,
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
) -> Vec<DecodeResult>
where
F: FnMut(&mut [[Cmplx<f32>; 8]; 79], &SyncCandidate, SymMask),
{
let mut results: Vec<DecodeResult> = Vec::new();
let mut bp_scratch =
crate::fec::ldpc::bp::BpScratch::<crate::fec::ldpc::params::Ldpc174_91Params, LlrT>::new();
for (cand, cs_box, _q_block0) in cands.iter() {
*cs_scratch = **cs_box;
fill(cs_scratch, cand, SymMask::SyncBlocks12);
let q = sync_quality(cs_scratch);
if q <= q_thresh {
continue;
}
fill(cs_scratch, cand, SymMask::DataOnly);
if let Some(r) = process_one_candidate_inner(
cs_scratch,
cand,
cand.dt_sec,
q,
depth,
bp_max_iter,
&mut bp_scratch,
&results,
ap_hint,
strictness,
0.0,
) {
results.push(r);
}
}
results
}
pub(super) const WSJTX_NHARDERRORS_MAX: u32 = 36;
#[allow(clippy::too_many_arguments)]
#[allow(unused_variables)] pub(in crate::ft8) fn process_one_candidate_inner(
cs_scratch: &[[Cmplx<f32>; 8]; 79],
cand: &SyncCandidate,
refined_dt: f32,
q: u32,
depth: DecodeDepth,
bp_max_iter: u32,
bp_scratch: &mut crate::fec::ldpc::bp::BpScratch<
crate::fec::ldpc::params::Ldpc174_91Params,
LlrT,
>,
known: &[DecodeResult],
ap_hint: Option<&ApHint>,
strictness: DecodeStrictness,
sync_cv: f32,
) -> Option<DecodeResult> {
let mut accepted: Option<(crate::fec::ldpc::bp::BpResult, u8)> = None;
let llr_a_fast: super::super::llr::LlrSet<LlrT> =
super::super::llr::compute_llr_fast(cs_scratch);
let bp_step1 =
bp_step_select::<LlrT>(bp_scratch, &llr_a_fast.llra, bp_max_iter, Some(check_crc14));
if let Some(bp) = bp_step1
&& bp.hard_errors <= WSJTX_NHARDERRORS_MAX
{
accepted = Some((bp, 0));
}
let (run_d, run_b, run_c) = match depth {
DecodeDepth::BpAll | DecodeDepth::BpAllOsd => (true, true, true),
DecodeDepth::BpAllNoNsym3 => (true, true, false),
DecodeDepth::BpVariantsAd => (true, false, false),
};
if accepted.is_none() && run_d {
let bp_d =
bp_step_select::<LlrT>(bp_scratch, &llr_a_fast.llrd, bp_max_iter, Some(check_crc14));
if let Some(bp) = bp_d
&& bp.hard_errors <= WSJTX_NHARDERRORS_MAX
{
accepted = Some((bp, 3));
}
}
if accepted.is_none() && run_b {
let llrb_arr: [LlrT; LDPC_N] =
super::super::llr::compute_llr_partial::<LlrT>(cs_scratch, 2);
let bp_b = bp_step_select::<LlrT>(bp_scratch, &llrb_arr, bp_max_iter, Some(check_crc14));
if let Some(bp) = bp_b
&& bp.hard_errors <= WSJTX_NHARDERRORS_MAX
{
accepted = Some((bp, 1));
}
}
if accepted.is_none() && run_c {
let llrc_arr: [LlrT; LDPC_N] =
super::super::llr::compute_llr_partial::<LlrT>(cs_scratch, 3);
let bp_c = bp_step_select::<LlrT>(bp_scratch, &llrc_arr, bp_max_iter, Some(check_crc14));
if let Some(bp) = bp_c
&& bp.hard_errors <= WSJTX_NHARDERRORS_MAX
{
accepted = Some((bp, 2));
}
}
#[cfg(feature = "fft-rustfft")]
let prefetched_llr: Option<super::super::llr::LlrSet<f32>> = if accepted.is_none()
&& matches!(depth, DecodeDepth::BpAllOsd)
&& q >= 12
&& ap_hint.is_some()
{
Some(super::super::llr::compute_llr(cs_scratch))
} else {
None
};
#[cfg(not(feature = "fft-rustfft"))]
let prefetched_llr: Option<super::super::llr::LlrSet<f32>> = None;
if accepted.is_none() {
accepted = super::osd_strategy::try_fallback(cs_scratch, prefetched_llr.as_ref(), depth, q);
}
#[cfg(feature = "fft-rustfft")]
if accepted.is_none()
&& let Some(ap) = ap_hint
{
let llr_full_f32: super::super::llr::LlrSet<f32> =
prefetched_llr.unwrap_or_else(|| super::super::llr::compute_llr(cs_scratch));
let apmag = llr_full_f32
.llra
.iter()
.map(|v| v.abs())
.fold(0.0f32, f32::max)
* 1.01;
let llr_variants: [&[f32; LDPC_N]; 4] = [
&llr_full_f32.llra,
&llr_full_f32.llrb,
&llr_full_f32.llrc,
&llr_full_f32.llrd,
];
let mut ap_passes: alloc::vec::Vec<(ApHint, u8)> = alloc::vec::Vec::new();
if ap.has_info() {
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));
if ap.call1.is_some() {
let mut ap5 = ApHint::new();
if let Some(ref c1) = ap.call1 {
ap5 = ap5.with_call1(c1);
}
ap_passes.push((ap5, 5));
}
}
ap_passes.push((ApHint::new().with_call1("CQ"), 12));
'ap_outer: 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 validate = |msg77: [u8; 77], hard_errors: u32| -> bool {
if hard_errors >= max_errors {
return false;
}
let Some(text) = unpack77(&msg77) else {
return false;
};
if !crate::msg::wsjt77::is_plausible_message(&text) {
return false;
}
let upper = text.to_uppercase();
if let Some(ref c1) = ap_cfg.call1
&& !upper.contains(&c1.to_uppercase())
{
return false;
}
if let Some(ref c2) = ap_cfg.call2
&& !upper.contains(&c2.to_uppercase())
{
return false;
}
true
};
if let Some(bp) = crate::fec::ldpc::bp::bp_decode(
&llr_ap,
Some(&ap_mask),
bp_max_iter,
Some(check_crc14),
) && validate(bp.message77, bp.hard_errors)
{
accepted = Some((bp, *pass_id));
break 'ap_outer;
}
if depth == DecodeDepth::BpAllOsd
&& let Some(osd) = osd_decode_deep(&llr_ap, 2, Some(check_crc14))
&& validate(osd.message77, osd.hard_errors)
{
let bp = crate::fec::ldpc::bp::BpResult {
message77: osd.message77,
info: osd.info,
codeword: osd.codeword,
hard_errors: osd.hard_errors,
iterations: 0,
};
accepted = Some((bp, *pass_id));
break 'ap_outer;
}
}
}
}
let (bp, pass_id) = accepted?;
let text = unpack77(&bp.message77)?;
if !crate::msg::wsjt77::is_plausible_message(&text) {
return None;
}
if known.iter().any(|r| r.message77 == bp.message77) {
return None;
}
let itone = message_to_tones(&bp.message77);
let snr_db = super::super::llr::compute_snr_db(cs_scratch, &itone);
Some(DecodeResult {
message77: bp.message77,
freq_hz: cand.freq_hz,
dt_sec: refined_dt,
hard_errors: bp.hard_errors,
sync_score: cand.score,
pass: pass_id,
sync_cv,
snr_db,
})
}