use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use num_complex::Complex;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use super::dsp::downsample::{DownsampleCfg, build_fft_cache, downsample_cached};
use super::dsp::subtract::SubtractCfg;
use super::equalize::{EqMode, equalize_local};
use super::llr::{
compute_llr_fast, compute_llr_partial, compute_snr_db, descramble_info, symbol_spectra,
sync_quality,
};
use super::protocol::BpPooledFec;
use super::sync::{AudioSource, RxGrid, SyncCandidate, coarse_sync, fine_sync_power_per_block};
use super::tx::codeword_to_itone;
use super::{FecCodec, FecOpts, MessageCodec, Protocol};
#[cfg(feature = "std")]
static TRACE_NSYNC_FAIL: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
#[cfg(feature = "std")]
static TRACE_NSYNC_PASS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
#[cfg(feature = "std")]
static TRACE_OSD_ATTEMPT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
#[cfg(feature = "std")]
fn stage_trace_enabled<P: Protocol>() -> bool {
let var = match P::ID {
super::ProtocolId::Ft4 => "MFSK_TRACE_STAGE_FT4",
super::ProtocolId::Fst4 => "MFSK_TRACE_STAGE_FST4",
_ => return false,
};
std::env::var(var).is_ok()
}
#[derive(Clone)]
pub struct FftCache(pub(crate) Vec<Complex<f32>>);
impl FftCache {
pub(crate) fn as_slice(&self) -> &[Complex<f32>] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LlrEffort {
Minimal,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DecodeDepth {
pub llr_effort: LlrEffort,
pub osd: bool,
}
impl DecodeDepth {
pub const EMBEDDED: Self = Self {
llr_effort: LlrEffort::Minimal,
osd: false,
};
pub const BP_ONLY: Self = Self {
llr_effort: LlrEffort::Full,
osd: false,
};
pub const FULL: Self = Self {
llr_effort: LlrEffort::Full,
osd: true,
};
}
#[cfg(feature = "internal-testing")]
pub fn osd_escalation_gates<P: Protocol>() -> (u32, u32) {
osd_escalation_gates_impl::<P>()
}
#[cfg(not(feature = "internal-testing"))]
#[allow(dead_code)] pub(crate) fn osd_escalation_gates<P: Protocol>() -> (u32, u32) {
osd_escalation_gates_impl::<P>()
}
fn osd_escalation_gates_impl<P: Protocol>() -> (u32, u32) {
if P::ID == super::ProtocolId::Ft4 {
((12 * P::N_SYNC + 10) / 21, (18 * P::N_SYNC + 10) / 21)
} else if P::ID == super::ProtocolId::Fst4 {
(12, 20)
} else {
(12, 18)
}
}
#[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) => 28,
(Self::Normal, 4) => 30,
(Self::Normal, _) => 31,
(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 ft8_nharderrors_max(self) -> u32 {
match self {
Self::Strict => 22,
Self::Normal => 36,
Self::Deep => 37,
}
}
}
#[derive(Debug, Clone)]
pub struct DecodeResult {
pub info: Box<[u8]>,
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,
}
impl DecodeResult {
pub fn message77(&self) -> &[u8] {
&self.info[..77]
}
}
#[cfg(feature = "internal-testing")]
pub struct SnrCtx<'a> {
pub cs: &'a [Complex<f32>],
pub itone: &'a [u8],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub cd0: &'a [Complex<f32>],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub ds_rate_hz: f32,
#[cfg_attr(not(feature = "ft4"), allow(dead_code))]
pub cand_score: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub cand_freq_hz: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub fft_cache: &'a [Complex<f32>],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub ds_cfg: &'a DownsampleCfg,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub refined_freq_hz: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub i_start: i32,
}
#[cfg(not(feature = "internal-testing"))]
pub(crate) struct SnrCtx<'a> {
pub cs: &'a [Complex<f32>],
pub itone: &'a [u8],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub cd0: &'a [Complex<f32>],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub ds_rate_hz: f32,
#[cfg_attr(not(feature = "ft4"), allow(dead_code))]
pub cand_score: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub cand_freq_hz: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub fft_cache: &'a [Complex<f32>],
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub ds_cfg: &'a DownsampleCfg,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub refined_freq_hz: f32,
#[cfg_attr(not(feature = "fst4"), allow(dead_code))]
pub i_start: i32,
}
#[cfg(feature = "internal-testing")]
pub trait GenericPipelineProtocol: Protocol
where
Self::Fec: BpPooledFec,
{
fn snr_db(ctx: SnrCtx<'_>) -> f32 {
compute_snr_db::<Self>(ctx.cs, ctx.itone)
}
}
#[cfg(not(feature = "internal-testing"))]
pub(crate) trait GenericPipelineProtocol: Protocol
where
Self::Fec: BpPooledFec,
{
fn snr_db(ctx: SnrCtx<'_>) -> f32 {
compute_snr_db::<Self>(ctx.cs, ctx.itone)
}
}
#[cfg(feature = "internal-testing")]
pub fn process_candidate_basic<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
sync_q_min: u32,
) -> Option<DecodeResult>
where
P::Fec: BpPooledFec,
{
process_candidate_basic_impl::<P>(
cand, fft_cache, cfg, depth, strictness, known, eq_mode, sync_q_min, None, false, false,
)
}
#[cfg(not(feature = "internal-testing"))]
#[allow(dead_code)]
pub(crate) fn process_candidate_basic<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
sync_q_min: u32,
) -> Option<DecodeResult>
where
P::Fec: BpPooledFec,
{
process_candidate_basic_impl::<P>(
cand, fft_cache, cfg, depth, strictness, known, eq_mode, sync_q_min, None, false, false,
)
}
#[cfg(feature = "internal-testing")]
#[allow(clippy::too_many_arguments)]
pub fn process_candidate_precomputed<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
sync_q_min: u32,
precomputed_refine: (Vec<Complex<f32>>, f32, i32, f32),
skip_snr: bool,
skip_llr_nsym_max: bool,
) -> Option<DecodeResult>
where
P::Fec: BpPooledFec,
{
process_candidate_basic_impl::<P>(
cand,
fft_cache,
cfg,
depth,
strictness,
known,
eq_mode,
sync_q_min,
Some(precomputed_refine),
skip_snr,
skip_llr_nsym_max,
)
}
#[cfg_attr(not(feature = "ft4"), allow(dead_code))]
pub(crate) fn ft4_snr_db(cand_score: f32) -> f32 {
let snr = cand_score - 1.0;
if snr > 0.0 {
(10.0 * snr.log10() - 14.8).max(-21.0)
} else {
-21.0
}
}
fn process_candidate_basic_impl<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
sync_q_min: u32,
precomputed_refine: Option<(Vec<Complex<f32>>, f32, i32, f32)>,
skip_snr: bool,
skip_llr_nsym_max: bool,
) -> Option<DecodeResult>
where
P::Fec: BpPooledFec,
{
let ntones = P::NTONES as usize;
let n_sym = P::N_SYMBOLS as usize;
let ds_rate = cfg.input_rate as f32 / P::NDOWN as f32;
let tx_start = P::TX_START_OFFSET_S;
let precomputed_freq = precomputed_refine
.as_ref()
.map(|&(_, freq_hz, i0, score)| (freq_hz, i0, score));
let cd0 = match precomputed_refine {
Some((cd0, ..)) => cd0,
None => {
let mut cd0 = downsample_cached(fft_cache, cand.freq_hz, cfg);
let sum2: f32 = cd0.iter().map(|c| c.norm_sqr()).sum::<f32>() / cd0.len() as f32;
if sum2 > f32::EPSILON {
let inv = 1.0 / sum2.sqrt();
for c in cd0.iter_mut() {
*c *= inv;
}
}
cd0
}
};
let _ = ntones;
let _ = n_sym;
let bp_max_iter: u32 = if P::ID == super::ProtocolId::Ft4 {
40
} else {
30
};
let cd0_base = cd0;
let try_position = |freq_hz: f32, i0: i32, score: f32| -> Option<DecodeResult> {
let df_hz = freq_hz - cand.freq_hz;
let cd0 = super::sync2d::freq_shift_cd0(&cd0_base, df_hz, ds_rate);
let refined = SyncCandidate {
freq_hz,
dt_sec: (i0 as f32) / ds_rate - tx_start,
score,
};
let cs_raw = symbol_spectra::<P>(&cd0, i0);
let nsync = sync_quality::<P>(&cs_raw);
if nsync <= sync_q_min {
#[cfg(feature = "std")]
TRACE_NSYNC_FAIL.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
return None;
}
#[cfg(feature = "std")]
TRACE_NSYNC_PASS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
let per_block = fine_sync_power_per_block::<P>(&cd0, i0);
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 {
let var = per_block.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / n;
var.sqrt() / mean
} else {
0.0
}
} else {
0.0
};
let decode = |cs: &[Complex<f32>]| -> Option<DecodeResult> {
let fec = P::Fec::default();
let mut bp_scratch = <P::Fec as BpPooledFec>::Scratch::default();
let bp_opts = FecOpts {
bp_max_iter,
osd_depth: 0,
ap_mask: None,
verify_info: Some(<P::Msg as MessageCodec>::verify_info),
..FecOpts::default()
};
let deinterleave = |v: &mut Vec<f32>| {
if let Some(table) = P::CODEWORD_INTERLEAVE {
deinterleave_llr_vec(v, table);
}
};
let mut try_bp = |llr: &Vec<f32>, pass_id: u8| -> Option<DecodeResult> {
let mut r = fec.decode_soft_pooled(llr, &bp_opts, &mut bp_scratch)?;
let snr_db = if skip_snr {
f32::NAN
} else {
let itone = encode_tones_for_snr::<P>(&r.info, &fec);
P::snr_db(SnrCtx {
cs,
itone: &itone,
cd0: &cd0,
ds_rate_hz: ds_rate,
cand_score: cand.score,
cand_freq_hz: cand.freq_hz,
fft_cache,
ds_cfg: cfg,
refined_freq_hz: refined.freq_hz,
i_start: i0,
})
};
descramble_info::<P>(&mut r.info);
Some(DecodeResult {
info: r.info.into_boxed_slice(),
freq_hz: refined.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: r.hard_errors,
sync_score: refined.score,
pass: pass_id,
sync_cv,
snr_db,
})
};
let mut llr_set = compute_llr_fast::<P, f32>(cs);
deinterleave(&mut llr_set.llra);
deinterleave(&mut llr_set.llrd);
if let Some(r) = try_bp(&llr_set.llra, 0) {
return Some(r);
}
llr_set.llrb = compute_llr_partial::<P, f32, f32>(cs, 2);
deinterleave(&mut llr_set.llrb);
if let Some(r) = try_bp(&llr_set.llrb, 1) {
return Some(r);
}
if let Some(mid) = P::LLR_NSYM_MID {
llr_set.llre = compute_llr_partial::<P, f32, f32>(cs, mid as usize);
if let Some(r) = try_bp(&llr_set.llre, 6) {
return Some(r);
}
}
if !skip_llr_nsym_max {
llr_set.llrc = compute_llr_partial::<P, f32, f32>(cs, P::LLR_NSYM_MAX as usize);
deinterleave(&mut llr_set.llrc);
if let Some(r) = try_bp(&llr_set.llrc, 2) {
return Some(r);
}
}
if let Some(r) = try_bp(&llr_set.llrd, 3) {
return Some(r);
}
let mut variants: Vec<(&Vec<f32>, u8)> = Vec::with_capacity(5);
variants.push((&llr_set.llra, 0u8));
variants.push((&llr_set.llrb, 1));
if !llr_set.llre.is_empty() {
variants.push((&llr_set.llre, 6));
}
if !skip_llr_nsym_max {
variants.push((&llr_set.llrc, 2));
}
variants.push((&llr_set.llrd, 3));
let is_fst4 = P::ID == super::ProtocolId::Fst4;
let (osd_attempt_min, osd_depth3_min) = osd_escalation_gates::<P>();
if depth.osd && nsync >= osd_attempt_min {
let freq_dup = known
.iter()
.any(|r| (r.freq_hz - cand.freq_hz).abs() < 20.0);
if !freq_dup {
#[cfg(feature = "std")]
TRACE_OSD_ATTEMPT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
let osd_depth: u8 = if nsync >= osd_depth3_min { 3 } else { 2 };
let osd_opts = FecOpts {
bp_max_iter,
osd_depth: osd_depth as u32,
ap_mask: None,
verify_info: Some(<P::Msg as MessageCodec>::verify_info),
..FecOpts::default()
};
for (llr, _) in &variants {
if let Some(mut r) = fec.decode_soft_pooled(llr, &osd_opts, &mut bp_scratch)
{
if !is_fst4 && r.hard_errors >= strictness.osd_max_errors(osd_depth) {
continue;
}
let itone = encode_tones_for_snr::<P>(&r.info, &fec);
let snr_db = P::snr_db(SnrCtx {
cs,
itone: &itone,
cd0: &cd0,
ds_rate_hz: ds_rate,
cand_score: cand.score,
cand_freq_hz: cand.freq_hz,
fft_cache,
ds_cfg: cfg,
refined_freq_hz: refined.freq_hz,
i_start: i0,
});
descramble_info::<P>(&mut r.info);
return Some(DecodeResult {
info: r.info.into_boxed_slice(),
freq_hz: refined.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: r.hard_errors,
sync_score: refined.score,
pass: if osd_depth == 3 { 5 } else { 4 },
sync_cv,
snr_db,
});
}
}
if nsync >= osd_depth3_min {
let osd4_opts = FecOpts {
bp_max_iter,
osd_depth: 4,
ap_mask: None,
verify_info: Some(<P::Msg as MessageCodec>::verify_info),
..FecOpts::default()
};
for (llr, _) in &variants {
if let Some(mut r) =
fec.decode_soft_pooled(llr, &osd4_opts, &mut bp_scratch)
{
if !is_fst4 && r.hard_errors >= strictness.osd_max_errors(4) {
continue;
}
let itone = encode_tones_for_snr::<P>(&r.info, &fec);
let snr_db = P::snr_db(SnrCtx {
cs,
itone: &itone,
cd0: &cd0,
ds_rate_hz: ds_rate,
cand_score: cand.score,
cand_freq_hz: cand.freq_hz,
fft_cache,
ds_cfg: cfg,
refined_freq_hz: refined.freq_hz,
i_start: i0,
});
descramble_info::<P>(&mut r.info);
return Some(DecodeResult {
info: r.info.into_boxed_slice(),
freq_hz: refined.freq_hz,
dt_sec: refined.dt_sec,
hard_errors: r.hard_errors,
sync_score: refined.score,
pass: 13,
sync_cv,
snr_db,
});
}
}
}
}
}
None
};
match eq_mode {
EqMode::Off => decode(&cs_raw),
EqMode::Local => {
let mut cs_eq = cs_raw.clone();
equalize_local::<P>(&mut cs_eq);
decode(&cs_eq)
}
}
};
let (freq_hz, i0, score) = if let Some(r) = precomputed_freq {
r
} else if P::ID == super::ProtocolId::Ft4 {
let s2 = super::sync2d::ft4_sync_search::<P>(&cd0_base, cand);
(s2.freq_hz, s2.i0, s2.score)
} else {
let s2 = super::sync2d::fst4_sync_search::<P>(&cd0_base, cand);
(s2.freq_hz, s2.i0, s2.score)
};
let result = try_position(freq_hz, i0, score);
if result.is_some() {
return result;
}
if P::ID != super::ProtocolId::Ft4 && depth.osd {
for ioffset in [1i32, -1i32] {
if let Some(r) = try_position(freq_hz, i0 + ioffset, score) {
return Some(r);
}
}
}
None
}
fn deinterleave_llr_vec(llr: &mut [f32], table: &[u16]) {
debug_assert_eq!(
llr.len(),
table.len(),
"interleave table length must match LLR length"
);
let original: Vec<f32> = llr.to_vec();
for j in 0..llr.len() {
llr[table[j] as usize] = original[j];
}
}
fn encode_tones_for_snr<P: Protocol>(info: &[u8], fec: &P::Fec) -> Vec<u8> {
let mut cw = vec![0u8; P::Fec::N];
fec.encode(info, &mut cw);
codeword_to_itone::<P>(&cw)
}
#[cfg(any(feature = "ft4", feature = "fst4"))]
pub(crate) fn known_filtered_on_result<'a>(
known: &'a [DecodeResult],
cb: Option<&'a (dyn Fn(&DecodeResult) + Sync)>,
) -> Option<impl Fn(&DecodeResult) + Sync + use<'a>> {
cb.map(move |cb| {
move |r: &DecodeResult| {
if !known.iter().any(|k| k.info == r.info) {
cb(r);
}
}
})
}
#[cfg(any(feature = "ft4", feature = "fst4"))]
pub(crate) fn dedup_known(raw: Vec<DecodeResult>, known: &[DecodeResult]) -> Vec<DecodeResult> {
raw.into_iter()
.filter(|r| !known.iter().any(|k| k.info == r.info))
.collect()
}
#[cfg(any(feature = "jt9", feature = "wspr", feature = "q65"))]
pub(crate) fn scan_dedup_match<T, M: PartialEq>(
seen: &[T],
cand: &T,
msg: impl Fn(&T) -> &M,
freq_hz: impl Fn(&T) -> f32,
start_sample: impl Fn(&T) -> i64,
freq_tol_hz: f32,
time_tol_samples: i64,
) -> bool {
scan_dedup_match_cross(
seen,
cand,
&msg,
&freq_hz,
&start_sample,
&msg,
&freq_hz,
&start_sample,
freq_tol_hz,
time_tol_samples,
)
}
#[cfg(any(feature = "jt9", feature = "jt65", feature = "wspr", feature = "q65"))]
pub(crate) fn scan_dedup_match_cross<S, C, M: PartialEq>(
seen: &[S],
cand: &C,
seen_msg: impl Fn(&S) -> &M,
seen_freq_hz: impl Fn(&S) -> f32,
seen_start_sample: impl Fn(&S) -> i64,
cand_msg: impl Fn(&C) -> &M,
cand_freq_hz: impl Fn(&C) -> f32,
cand_start_sample: impl Fn(&C) -> i64,
freq_tol_hz: f32,
time_tol_samples: i64,
) -> bool {
let cand_msg = cand_msg(cand);
let cand_freq = cand_freq_hz(cand);
let cand_time = cand_start_sample(cand);
seen.iter().any(|prev| {
seen_msg(prev) == cand_msg
&& (seen_freq_hz(prev) - cand_freq).abs() <= freq_tol_hz
&& (seen_start_sample(prev) - cand_time).abs() <= time_tol_samples
})
}
#[cfg(feature = "internal-testing")]
#[allow(clippy::too_many_arguments)]
pub fn decode_frame<P: GenericPipelineProtocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
sync_q_min: u32,
precomputed_fft: Option<&[Complex<f32>]>,
on_result: Option<&(dyn Fn(&DecodeResult) + Sync)>,
) -> (Vec<DecodeResult>, FftCache)
where
P::Fec: BpPooledFec,
{
decode_frame_impl::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
strictness,
eq_mode,
sync_q_min,
precomputed_fft,
on_result,
)
}
#[cfg(not(feature = "internal-testing"))]
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn decode_frame<P: GenericPipelineProtocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
sync_q_min: u32,
precomputed_fft: Option<&[Complex<f32>]>,
on_result: Option<&(dyn Fn(&DecodeResult) + Sync)>,
) -> (Vec<DecodeResult>, FftCache)
where
P::Fec: BpPooledFec,
{
decode_frame_impl::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
strictness,
eq_mode,
sync_q_min,
precomputed_fft,
on_result,
)
}
#[cfg(feature = "internal-testing")]
pub fn refine_candidate_position<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
) -> (Vec<Complex<f32>>, f32, i32, f32)
where
P::Fec: BpPooledFec,
{
refine_candidate_position_impl::<P>(cand, fft_cache, cfg)
}
#[cfg(not(feature = "internal-testing"))]
pub(crate) fn refine_candidate_position<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
) -> (Vec<Complex<f32>>, f32, i32, f32)
where
P::Fec: BpPooledFec,
{
refine_candidate_position_impl::<P>(cand, fft_cache, cfg)
}
fn refine_candidate_position_impl<P: GenericPipelineProtocol>(
cand: &SyncCandidate,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
) -> (Vec<Complex<f32>>, f32, i32, f32)
where
P::Fec: BpPooledFec,
{
let mut cd0 = downsample_cached(fft_cache, cand.freq_hz, cfg);
let sum2: f32 = cd0.iter().map(|c| c.norm_sqr()).sum::<f32>() / cd0.len() as f32;
if sum2 > f32::EPSILON {
let inv = 1.0 / sum2.sqrt();
for c in cd0.iter_mut() {
*c *= inv;
}
}
let s2 = if P::ID == super::ProtocolId::Ft4 {
super::sync2d::ft4_sync_search::<P>(&cd0, cand)
} else {
super::sync2d::fst4_sync_search::<P>(&cd0, cand)
};
(cd0, s2.freq_hz, s2.i0, s2.score)
}
type RefinedSurvivor = (SyncCandidate, Vec<Complex<f32>>, f32, i32, f32);
fn dedup_refined_candidates<P: GenericPipelineProtocol>(
candidates: Vec<SyncCandidate>,
fft_cache: &[Complex<f32>],
cfg: &DownsampleCfg,
) -> Vec<RefinedSurvivor>
where
P::Fec: BpPooledFec,
{
#[cfg(feature = "parallel")]
let refined: Vec<(Vec<Complex<f32>>, f32, i32, f32)> = candidates
.par_iter()
.map(|c| refine_candidate_position::<P>(c, fft_cache, cfg))
.collect();
#[cfg(not(feature = "parallel"))]
let refined: Vec<(Vec<Complex<f32>>, f32, i32, f32)> = candidates
.iter()
.map(|c| refine_candidate_position::<P>(c, fft_cache, cfg))
.collect();
let freq_tol = 0.10 * P::TONE_SPACING_HZ;
const I0_TOL: i32 = 2;
let mut order: Vec<usize> = (0..candidates.len()).collect();
order.sort_by(|&a, &b| {
refined[b]
.3
.partial_cmp(&refined[a].3)
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut kept_positions: Vec<(f32, i32)> = Vec::new();
let mut keep = vec![false; candidates.len()];
for idx in order {
let (_, f, i0, _) = &refined[idx];
let dup = kept_positions
.iter()
.any(|&(kf, ki)| (f - kf).abs() < freq_tol && (i0 - ki).abs() <= I0_TOL);
if !dup {
kept_positions.push((*f, *i0));
keep[idx] = true;
}
}
candidates
.into_iter()
.zip(refined)
.zip(keep)
.filter_map(|((c, (cd0, f, i0, s)), k)| if k { Some((c, cd0, f, i0, s)) } else { None })
.collect()
}
#[allow(clippy::too_many_arguments)]
fn decode_frame_impl<P: GenericPipelineProtocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
sync_q_min: u32,
precomputed_fft: Option<&[Complex<f32>]>,
on_result: Option<&(dyn Fn(&DecodeResult) + Sync)>,
) -> (Vec<DecodeResult>, FftCache)
where
P::Fec: BpPooledFec,
{
#[cfg(feature = "std")]
let trace = stage_trace_enabled::<P>();
#[cfg(not(feature = "std"))]
#[allow(unused_variables)]
let trace = false;
#[cfg(feature = "std")]
let __trace_t0 = trace.then(std::time::Instant::now);
let candidates = if P::ID == super::ProtocolId::Ft4 {
super::ft4_coarse::ft4_coarse_sync(audio, freq_min, freq_max, sync_min, freq_hint, max_cand)
} else {
coarse_sync::<P>(
AudioSource::Real(audio),
freq_min,
freq_max,
sync_min,
freq_hint,
max_cand,
RxGrid::real(12_000.0),
)
};
#[cfg(feature = "std")]
if let Some(t0) = __trace_t0 {
eprintln!(
"TRACE_STAGE coarse_sync={:.1}ms n_candidates={}",
t0.elapsed().as_secs_f64() * 1000.0,
candidates.len()
);
}
let fft_cache = FftCache(match precomputed_fft {
Some(c) => c.to_vec(),
None => build_fft_cache(audio, cfg),
});
if candidates.is_empty() {
return (Vec::new(), fft_cache);
}
#[cfg(feature = "std")]
if trace {
TRACE_NSYNC_FAIL.store(0, core::sync::atomic::Ordering::Relaxed);
TRACE_NSYNC_PASS.store(0, core::sync::atomic::Ordering::Relaxed);
TRACE_OSD_ATTEMPT.store(0, core::sync::atomic::Ordering::Relaxed);
}
let raw: Vec<DecodeResult> = if P::ID == super::ProtocolId::Ft4 {
#[cfg(feature = "std")]
let __trace_t1 = trace.then(std::time::Instant::now);
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
let r = process_candidate_basic::<P>(
cand,
fft_cache.as_slice(),
cfg,
depth,
strictness,
&[],
eq_mode,
sync_q_min,
)?;
if let Some(cb) = on_result {
cb(&r);
}
Some(r)
})
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<DecodeResult> = candidates
.iter()
.filter_map(|cand| {
let r = process_candidate_basic::<P>(
cand,
fft_cache.as_slice(),
cfg,
depth,
strictness,
&[],
eq_mode,
sync_q_min,
)?;
if let Some(cb) = on_result {
cb(&r);
}
Some(r)
})
.collect();
#[cfg(feature = "std")]
if let Some(t1) = __trace_t1 {
eprintln!(
"TRACE_STAGE decode_loop={:.1}ms nsync_fail={} nsync_pass={} osd_attempt={} n_decoded={}",
t1.elapsed().as_secs_f64() * 1000.0,
TRACE_NSYNC_FAIL.load(core::sync::atomic::Ordering::Relaxed),
TRACE_NSYNC_PASS.load(core::sync::atomic::Ordering::Relaxed),
TRACE_OSD_ATTEMPT.load(core::sync::atomic::Ordering::Relaxed),
raw.len()
);
}
raw
} else {
#[cfg(feature = "std")]
let __trace_t1 = trace.then(std::time::Instant::now);
#[cfg(feature = "std")]
let candidates_len = candidates.len();
let deduped = dedup_refined_candidates::<P>(candidates, fft_cache.as_slice(), cfg);
#[cfg(feature = "std")]
let deduped_len = deduped.len();
#[cfg(feature = "std")]
if let Some(t1) = __trace_t1 {
eprintln!(
"TRACE_STAGE dedup_refined_candidates={:.1}ms n_before={} n_after={}",
t1.elapsed().as_secs_f64() * 1000.0,
candidates_len,
deduped_len
);
}
#[cfg(feature = "std")]
let __trace_t2 = trace.then(std::time::Instant::now);
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = deduped
.into_par_iter()
.filter_map(|(cand, cd0, freq_hz, i0, score)| {
let r = process_candidate_basic_impl::<P>(
&cand,
fft_cache.as_slice(),
cfg,
depth,
strictness,
&[],
eq_mode,
sync_q_min,
Some((cd0, freq_hz, i0, score)),
false,
false,
)?;
if let Some(cb) = on_result {
cb(&r);
}
Some(r)
})
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<DecodeResult> = deduped
.into_iter()
.filter_map(|(cand, cd0, freq_hz, i0, score)| {
let r = process_candidate_basic_impl::<P>(
&cand,
fft_cache.as_slice(),
cfg,
depth,
strictness,
&[],
eq_mode,
sync_q_min,
Some((cd0, freq_hz, i0, score)),
false,
false,
)?;
if let Some(cb) = on_result {
cb(&r);
}
Some(r)
})
.collect();
#[cfg(feature = "std")]
if let Some(t2) = __trace_t2 {
eprintln!(
"TRACE_STAGE decode_loop={:.1}ms nsync_fail={} nsync_pass={} osd_attempt={} n_decoded={}",
t2.elapsed().as_secs_f64() * 1000.0,
TRACE_NSYNC_FAIL.load(core::sync::atomic::Ordering::Relaxed),
TRACE_NSYNC_PASS.load(core::sync::atomic::Ordering::Relaxed),
TRACE_OSD_ATTEMPT.load(core::sync::atomic::Ordering::Relaxed),
raw.len()
);
}
raw
};
let mut results: Vec<DecodeResult> = Vec::new();
for r in raw {
match results.iter_mut().find(|x| x.info == r.info) {
Some(existing) if r.sync_score > existing.sync_score => *existing = r,
Some(_) => {}
None => results.push(r),
}
}
(results, fft_cache)
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub(crate) fn decode_frame_subtract<P: GenericPipelineProtocol>(
audio: &[i16],
ds_cfg: &DownsampleCfg,
sub_cfg: &SubtractCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
max_rounds: usize,
sync_q_min: u32,
lpf_half: usize,
lpf_endcorrection: bool,
refine_freq_radius_hz: f32,
precomputed_fft: Option<&[Complex<f32>]>,
on_result: Option<&(dyn Fn(&DecodeResult) + Sync)>,
) -> Vec<DecodeResult>
where
P::Fec: BpPooledFec,
{
#[cfg(feature = "std")]
let trace = stage_trace_enabled::<P>();
#[cfg(not(feature = "std"))]
#[allow(unused_variables)]
let trace = false;
#[cfg(feature = "std")]
if trace {
TRACE_NSYNC_FAIL.store(0, core::sync::atomic::Ordering::Relaxed);
TRACE_NSYNC_PASS.store(0, core::sync::atomic::Ordering::Relaxed);
TRACE_OSD_ATTEMPT.store(0, core::sync::atomic::Ordering::Relaxed);
}
let mut residual = audio.to_vec();
let mut all_results: Vec<DecodeResult> = Vec::new();
let passes: &[f32] = &[1.0, 0.75, 0.5][..max_rounds];
let fec = P::Fec::default();
for (pass_idx, &factor) in passes.iter().enumerate() {
#[cfg(feature = "std")]
let __trace_tp = trace.then(std::time::Instant::now);
let candidates = if P::ID == super::ProtocolId::Ft4 {
super::ft4_coarse::ft4_coarse_sync(
&residual,
freq_min,
freq_max,
sync_min * factor,
freq_hint,
max_cand,
)
} else {
coarse_sync::<P>(
AudioSource::Real(&residual),
freq_min,
freq_max,
sync_min * factor,
freq_hint,
max_cand,
RxGrid::real(12_000.0),
)
};
#[cfg(feature = "std")]
if let Some(tp) = __trace_tp {
eprintln!(
"TRACE_STAGE_SIC pass={} coarse_sync={:.1}ms n_candidates={}",
pass_idx,
tp.elapsed().as_secs_f64() * 1000.0,
candidates.len()
);
}
if candidates.is_empty() {
continue;
}
let fft_cache = match (pass_idx, precomputed_fft) {
(0, Some(c)) => c.to_vec(),
_ => build_fft_cache(&residual, ds_cfg),
};
#[cfg(feature = "std")]
let __trace_tp2 = trace.then(std::time::Instant::now);
#[cfg(feature = "parallel")]
let new: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
process_candidate_basic::<P>(
cand,
&fft_cache,
ds_cfg,
depth,
strictness,
&all_results,
EqMode::Off,
sync_q_min,
)
})
.collect();
#[cfg(not(feature = "parallel"))]
let new: Vec<DecodeResult> = candidates
.iter()
.filter_map(|cand| {
process_candidate_basic::<P>(
cand,
&fft_cache,
ds_cfg,
depth,
strictness,
&all_results,
EqMode::Off,
sync_q_min,
)
})
.collect();
#[cfg(feature = "std")]
if let Some(tp2) = __trace_tp2 {
eprintln!(
"TRACE_STAGE_SIC pass={} decode_loop={:.1}ms nsync_fail={} nsync_pass={} osd_attempt={} n_new={}",
pass_idx,
tp2.elapsed().as_secs_f64() * 1000.0,
TRACE_NSYNC_FAIL.swap(0, core::sync::atomic::Ordering::Relaxed),
TRACE_NSYNC_PASS.swap(0, core::sync::atomic::Ordering::Relaxed),
TRACE_OSD_ATTEMPT.swap(0, core::sync::atomic::Ordering::Relaxed),
new.len()
);
}
let mut deduped: Vec<DecodeResult> = Vec::new();
for r in new {
if !all_results.iter().any(|k| k.info == r.info)
&& !deduped.iter().any(|x| x.info == r.info)
{
deduped.push(r);
}
}
for r in &deduped {
let mut info_for_tx = r.info.to_vec();
descramble_info::<P>(&mut info_for_tx);
let tones = encode_tones_for_snr::<P>(&info_for_tx, &fec);
let refined_freq = super::dsp::subtract::refine_freq(
&residual,
&tones,
r.freq_hz,
r.dt_sec,
sub_cfg,
refine_freq_radius_hz,
0.1,
);
super::dsp::subtract::subtract_tones_lpf(
&mut residual,
&tones,
refined_freq,
r.dt_sec,
sub_cfg,
lpf_half,
lpf_endcorrection,
);
}
if let Some(cb) = on_result {
for r in &deduped {
cb(r);
}
}
all_results.extend(deduped);
}
all_results
}