use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub use super::equalizer::EqMode;
use super::{
Ft8,
downsample::build_fft_cache,
equalizer,
llr::sync_quality,
params,
params::BP_MAX_ITER,
subtract::{subtract_signal_lpf, subtract_signal_lpf_refine_dt},
sync::SyncCandidate,
};
use crate::msg::decode_request::{
DecodeOutcome, DecodeRequest, FrameDecodable, SniperRequest, SupportsSicEarly,
SupportsSicRounds, SupportsWideBandAp,
};
pub use crate::engine::pipeline::FftCache;
pub use crate::engine::pipeline::{DecodeDepth, LlrEffort};
pub use crate::engine::pipeline::DecodeStrictness;
pub use crate::engine::pipeline::DecodeResult;
pub use crate::msg::ap::ApHint;
#[cfg(feature = "fixed-point")]
type LlrT = crate::engine::scalar::Q11i16;
#[cfg(not(feature = "fixed-point"))]
type LlrT = f32;
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 mut bp_scratch =
crate::fec::ldpc::bp::BpScratch::<crate::fec::ldpc::params::Ldpc174_91Params, LlrT>::new();
process_candidate_with_scratch(
cand,
audio,
fft_cache,
depth,
strictness,
known,
eq_mode,
ap_hint,
&mut bp_scratch,
)
}
#[allow(clippy::too_many_arguments)]
fn process_candidate_with_scratch(
cand: &SyncCandidate,
audio: &[i16],
fft_cache: &[num_complex::Complex<f32>],
depth: DecodeDepth,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
bp_scratch: &mut crate::fec::ldpc::bp::BpScratch<
crate::fec::ldpc::params::Ldpc174_91Params,
LlrT,
>,
) -> Option<DecodeResult> {
let _ = strictness; let mut cd0 = crate::engine::dsp::downsample::downsample_cached(
fft_cache,
cand.freq_hz,
&crate::ft8::downsample::FT8_CFG,
);
let refine_result = crate::ft8::refine_fine::fine_refine_3stage(&cd0, cand.dt_sec);
let refined = SyncCandidate {
freq_hz: cand.freq_hz + refine_result.delf_hz,
dt_sec: refine_result.dt_sec,
score: refine_result.score,
};
if refine_result.delf_hz.abs() > f32::EPSILON {
let dt2 = 1.0_f32 / 200.0;
for (k, c) in cd0.iter_mut().enumerate() {
let phi = -core::f32::consts::TAU * refine_result.delf_hz * (k as f32) * dt2;
let rot = num_complex::Complex::new(phi.cos(), phi.sin());
*c *= rot;
}
}
let i_start = ((refined.dt_sec + 0.5) * 200.0).round() as i32;
let sync_cv = {
let scores =
crate::engine::sync::fine_sync_power_per_block::<crate::ft8::Ft8>(&cd0, i_start);
let sa = scores.first().copied().unwrap_or(0.0);
let sb = scores.get(1).copied().unwrap_or(0.0);
let sc = scores.get(2).copied().unwrap_or(0.0);
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
}
};
drop(cd0);
let mut cs_raw: alloc::boxed::Box<[[crate::engine::scalar::Cmplx<f32>; 8]; 79]> =
alloc::vec![[crate::engine::scalar::Cmplx::<f32>::default(); 8]; 79]
.try_into()
.unwrap();
crate::ft8::decode_block::fill_symbol_spectra(
&mut cs_raw,
audio,
refined.freq_hz,
refined.dt_sec,
crate::ft8::decode_block::SymMask::SyncOnly,
Some(fft_cache),
);
let nsync = sync_quality(&cs_raw);
if nsync <= 6 {
return None;
}
crate::ft8::decode_block::fill_symbol_spectra(
&mut cs_raw,
audio,
refined.freq_hz,
refined.dt_sec,
crate::ft8::decode_block::SymMask::DataOnly,
Some(fft_cache),
);
let mut try_decode = |cs: &[[crate::engine::scalar::Cmplx<f32>; 8]; 79],
_use_ap: bool|
-> Option<DecodeResult> {
crate::ft8::decode_block::process_one_candidate_inner(
cs,
&refined,
refined.dt_sec,
nsync,
depth,
BP_MAX_ITER,
bp_scratch,
known,
ap_hint,
strictness,
sync_cv,
)
};
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)
}
}
}
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>]>,
ap_hint: Option<&ApHint>,
) -> (Vec<DecodeResult>, Vec<num_complex::Complex<f32>>) {
let _ = freq_hint;
let spec = crate::ft8::decode_block::compute_spectrogram(audio, freq_max);
let candidates =
crate::ft8::decode_block::coarse_sync(&spec, freq_min, freq_max, sync_min, max_cand);
let fft_cache = match precomputed_fft {
Some(c) => c.to_vec(),
None => build_fft_cache(audio),
};
if candidates.is_empty() {
return (Vec::new(), fft_cache);
}
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
process_candidate(
cand, audio, &fft_cache, depth, strictness, known, 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, strictness, known, eq_mode, ap_hint,
)
})
.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)
}
#[allow(clippy::too_many_arguments)]
fn flat_sic_inner(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
precomputed_fft: Option<&[num_complex::Complex<f32>]>,
n_rounds: usize,
) -> (Vec<DecodeResult>, FftCache) {
let mut residual = audio.to_vec();
sic_inner_passes_with_cache(
&mut residual,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
strictness,
known,
eq_mode,
ap_hint,
precomputed_fft,
n_rounds,
)
}
#[allow(clippy::too_many_arguments)]
fn sic_inner_passes(
residual: &mut [i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
n_rounds: usize,
) -> Vec<DecodeResult> {
sic_inner_passes_with_cache(
residual, freq_min, freq_max, sync_min, depth, max_cand, strictness, known, eq_mode,
ap_hint, None, n_rounds,
)
.0
}
#[allow(clippy::too_many_arguments)]
fn sic_inner_passes_with_cache(
residual: &mut [i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
known: &[DecodeResult],
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
precomputed_fft: Option<&[num_complex::Complex<f32>]>,
n_rounds: usize,
) -> (Vec<DecodeResult>, FftCache) {
let mut all_results: Vec<DecodeResult> = Vec::new();
let mut pass0_cache: Option<FftCache> = None;
let mut bp_scratch =
crate::fec::ldpc::bp::BpScratch::<crate::fec::ldpc::params::Ldpc174_91Params, LlrT>::new();
let mut prev_total: usize = 0;
for ipass in 0..n_rounds {
if ipass >= 1 && all_results.len() == prev_total {
break;
}
prev_total = all_results.len();
let spec = crate::ft8::decode_block::compute_spectrogram(residual, freq_max);
let candidates =
crate::ft8::decode_block::coarse_sync(&spec, freq_min, freq_max, sync_min, max_cand);
drop(spec);
if candidates.is_empty() {
continue;
}
let fft_cache = if ipass == 0
&& let Some(c) = precomputed_fft
{
c.to_vec()
} else {
build_fft_cache(residual)
};
if ipass == 0 {
pass0_cache = Some(FftCache(fft_cache.clone()));
}
for cand in &candidates {
let r = match process_candidate_with_scratch(
cand,
residual,
&fft_cache,
depth,
strictness,
known,
eq_mode,
ap_hint,
&mut bp_scratch,
) {
Some(r) => r,
None => continue,
};
if known.iter().any(|x| x.message77() == r.message77())
|| all_results.iter().any(|x| x.message77() == r.message77())
{
continue;
}
subtract_signal_lpf(residual, &r);
all_results.push(r);
}
}
let fft_cache = pass0_cache.unwrap_or_else(|| FftCache(build_fft_cache(residual)));
(all_results, fft_cache)
}
mod staged_checkpoint {
pub const A_SAMPLES: usize = 141_696;
pub const B_SAMPLES: usize = 162_432;
pub const C_SAMPLES: usize = 172_800;
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn decode_frame_subtract_staged_with_ap_debug_residual(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
ap_hint: Option<&ApHint>,
) -> (Vec<DecodeResult>, Vec<i16>) {
decode_frame_subtract_staged_with_ap_inner(
audio,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
strictness,
EqMode::Off,
ap_hint,
)
}
const CHECKPOINT_SIC_ROUNDS: usize = 3;
#[allow(clippy::too_many_arguments)]
fn decode_frame_subtract_staged_with_ap_inner(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
) -> (Vec<DecodeResult>, Vec<i16>) {
use staged_checkpoint::{A_SAMPLES, B_SAMPLES, C_SAMPLES};
let _ = freq_hint;
if audio.len() < A_SAMPLES {
let (r, _) = flat_sic_inner(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
strictness,
&[],
eq_mode,
ap_hint,
None,
CHECKPOINT_SIC_ROUNDS,
);
return (r, audio.to_vec());
}
const EARLY_SYNC_MIN_SCALE: f32 = 2.0 / 1.3;
let mut residual_a = vec![0i16; audio.len()];
residual_a[..A_SAMPLES].copy_from_slice(&audio[..A_SAMPLES]);
let early_results = sic_inner_passes(
&mut residual_a,
freq_min,
freq_max,
sync_min * EARLY_SYNC_MIN_SCALE,
depth,
max_cand,
strictness,
&[],
eq_mode,
ap_hint,
CHECKPOINT_SIC_ROUNDS,
);
drop(residual_a);
if early_results.is_empty() {
let (r, _) = flat_sic_inner(
audio,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
strictness,
&[],
eq_mode,
ap_hint,
None,
CHECKPOINT_SIC_ROUNDS,
);
return (r, audio.to_vec());
}
let b_len = B_SAMPLES.min(audio.len());
let mut buf_b = vec![0i16; audio.len()];
buf_b[..b_len].copy_from_slice(&audio[..b_len]);
let message_dur_s = params::NZ as f32 / 12_000.0;
let dt_fit_limit_b = b_len as f32 / 12_000.0 - message_dur_s - 0.5;
let mut deferred: Vec<DecodeResult> = Vec::new();
for r in &early_results {
if r.dt_sec < dt_fit_limit_b {
subtract_signal_lpf_refine_dt(&mut buf_b, r);
} else {
deferred.push(r.clone());
}
}
let c_len = C_SAMPLES.min(audio.len());
let mut buf_c = vec![0i16; audio.len()];
buf_c[..b_len].copy_from_slice(&buf_b[..b_len]);
buf_c[b_len..c_len].copy_from_slice(&audio[b_len..c_len]);
for r in &deferred {
subtract_signal_lpf_refine_dt(&mut buf_c, r);
}
let new_results = sic_inner_passes(
&mut buf_c,
freq_min,
freq_max,
sync_min,
depth,
max_cand,
strictness,
&early_results,
eq_mode,
ap_hint,
CHECKPOINT_SIC_ROUNDS,
);
let mut all_results = early_results;
all_results.extend(new_results);
(all_results, buf_c)
}
#[allow(clippy::too_many_arguments)]
fn decode_sniper_inner(
audio: &[i16],
target_freq: f32,
depth: DecodeDepth,
max_cand: usize,
strictness: DecodeStrictness,
eq_mode: EqMode,
ap_hint: Option<&ApHint>,
sync_min: f32,
) -> (Vec<DecodeResult>, FftCache) {
let freq_min = (target_freq - 250.0).max(100.0);
let freq_max = (target_freq + 250.0).min(5900.0);
let spec = crate::ft8::decode_block::compute_spectrogram(audio, freq_max);
let candidates =
crate::ft8::decode_block::coarse_sync(&spec, freq_min, freq_max, sync_min, max_cand);
let fft_cache = FftCache(build_fft_cache(audio));
if candidates.is_empty() {
return (Vec::new(), fft_cache);
}
#[cfg(feature = "parallel")]
let raw: Vec<DecodeResult> = candidates
.par_iter()
.filter_map(|cand| {
process_candidate(
cand,
audio,
fft_cache.as_slice(),
depth,
strictness,
&[],
eq_mode,
ap_hint,
)
})
.collect();
#[cfg(not(feature = "parallel"))]
let raw: Vec<DecodeResult> = candidates
.iter()
.filter_map(|cand| {
process_candidate(
cand,
audio,
fft_cache.as_slice(),
depth,
strictness,
&[],
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, fft_cache)
}
impl FrameDecodable for Ft8 {
type DecodeResult = DecodeResult;
fn __single_pass(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
let (results, fft_cache) = decode_frame_inner(
req.audio,
req.freq_min,
req.freq_max,
req.sync_min,
req.freq_hint,
req.depth,
req.max_cand,
req.strictness,
req.known,
req.eq_mode,
req.fft_cache.as_ref().map(FftCache::as_slice),
req.ap_hint,
);
DecodeOutcome {
results,
fft_cache: FftCache(fft_cache),
}
}
fn __sniper(req: &SniperRequest<'_, Self>) -> DecodeOutcome<Self> {
let (results, fft_cache) = decode_sniper_inner(
req.audio,
req.target_freq,
req.depth,
req.max_cand,
req.strictness,
req.eq_mode,
req.ap_hint,
req.sync_min,
);
DecodeOutcome { results, fft_cache }
}
}
impl SupportsSicRounds for Ft8 {
fn __flat_sic(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
if req.known.is_empty() {
let (results, fft_cache) = flat_sic_inner(
req.audio,
req.freq_min,
req.freq_max,
req.sync_min,
req.depth,
req.max_cand,
req.strictness,
req.known,
req.eq_mode,
req.ap_hint,
req.fft_cache.as_ref().map(FftCache::as_slice),
req.sic_rounds,
);
DecodeOutcome { results, fft_cache }
} else {
let mut audio_clean = req.audio.to_vec();
for r in req.known {
subtract_signal_lpf_refine_dt(&mut audio_clean, r);
}
let (results, fft_cache) = flat_sic_inner(
&audio_clean,
req.freq_min,
req.freq_max,
req.sync_min,
req.depth,
req.max_cand,
req.strictness,
req.known,
req.eq_mode,
req.ap_hint,
None,
req.sic_rounds,
);
DecodeOutcome { results, fft_cache }
}
}
}
impl SupportsSicEarly for Ft8 {
fn __staged_sic(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
let mut audio_clean = req.audio.to_vec();
for r in req.known {
subtract_signal_lpf_refine_dt(&mut audio_clean, r);
}
let (mut results, residual) = decode_frame_subtract_staged_with_ap_inner(
&audio_clean,
req.freq_min,
req.freq_max,
req.sync_min,
req.freq_hint,
req.depth,
req.max_cand,
req.strictness,
req.eq_mode,
req.ap_hint,
);
results.retain(|r| !req.known.iter().any(|k| k.message77() == r.message77()));
let fft_cache = FftCache(build_fft_cache(&residual));
DecodeOutcome { results, fft_cache }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WsjtxDepth {
D1,
D2,
D3,
}
impl<'a> DecodeRequest<'a, Ft8> {
pub fn wsjtx_depth(
audio: &'a [i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
tier: WsjtxDepth,
ap: Option<&'a ApHint>,
) -> Self {
let mut req = Self::new(audio, freq_min, freq_max, sync_min, max_cand)
.osd(!matches!(tier, WsjtxDepth::D1));
req = match tier {
WsjtxDepth::D1 => req.sic_rounds(2),
WsjtxDepth::D2 | WsjtxDepth::D3 => req.sic_early(),
};
if let (WsjtxDepth::D3, Some(ap)) = (tier, ap) {
req = req.ap_hint(ap);
}
req
}
}
impl SupportsWideBandAp for Ft8 {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ap_hint_round_trips_clean_signal() {
use crate::ft8::wave_gen::{message_to_tones, tones_to_i16};
use crate::msg::wsjt77::pack77;
let m77 = pack77("CQ", "K1ABC", "FN42").expect("pack77");
let tones = message_to_tones(&m77);
let samples = tones_to_i16(&tones, 1500.0, 20_000);
let mut audio = vec![0i16; 15 * 12_000];
let off = 6_000usize;
let len = samples.len().min(audio.len() - off);
audio[off..off + len].copy_from_slice(&samples[..len]);
let ap = ApHint::new().with_call1("CQ").with_call2("K1ABC");
let results = DecodeRequest::<Ft8>::new(&audio, 100.0, 3000.0, 1.0, 50)
.ap_hint(&ap)
.decode()
.results;
assert!(
results.iter().any(|r| r.message77() == m77),
"expected to decode the self-synthesized signal with matching AP hint"
);
}
#[test]
fn ap_hint_full_silence_shape() {
let audio = vec![0i16; 15 * 12_000];
let ap = ApHint::new().with_call1("CQ").with_call2("K1ABC");
let out0 = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.decode();
assert!(out0.results.is_empty());
assert!(!out0.fft_cache.is_empty(), "FFT cache should be returned");
let out1 = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.freq_hint(1500.0)
.strictness(DecodeStrictness::Strict)
.ap_hint(&ap)
.decode();
assert!(out1.results.is_empty());
assert!(!out1.fft_cache.is_empty());
}
#[test]
fn sic_early_with_ap_silence_shape() {
let audio = vec![0i16; 15 * 12_000];
let ap = ApHint::new().with_call1("CQ").with_call2("W7VV");
let r_none = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.sic_early()
.decode()
.results;
assert!(r_none.is_empty());
let r_some = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.sic_early()
.ap_hint(&ap)
.decode()
.results;
assert!(r_some.is_empty());
}
#[test]
fn sic_early_known_and_cache_silence_shape() {
let audio = vec![0i16; 15 * 12_000];
let ap = ApHint::new().with_call1("CQ").with_call2("JA1ABC");
let known: Vec<DecodeResult> = Vec::new();
let cache = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.decode()
.fft_cache;
let r_none = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.sic_early()
.known(&known)
.fft_cache(cache.clone())
.decode()
.results;
assert!(r_none.is_empty());
let r_some = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.sic_early()
.known(&known)
.fft_cache(cache)
.ap_hint(&ap)
.decode()
.results;
assert!(r_some.is_empty());
}
#[test]
fn sic_early_subtracts_known_before_checkpoint_a() {
use crate::ft8::wave_gen::{message_to_tones, tones_to_i16};
use crate::msg::wsjt77::pack77;
let m_known = pack77("CQ", "K1ABC", "FN42").expect("pack77 known");
let tones_known = message_to_tones(&m_known);
let f0 = 1500.0_f32;
let mut audio = vec![0i16; 15 * 12_000];
let off = 6_000usize;
let buf = tones_to_i16(&tones_known, f0, 20_000);
let n_sig = buf.len().min(audio.len() - off);
audio[off..off + n_sig].copy_from_slice(&buf[..n_sig]);
let phase1 = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 50)
.decode()
.results;
let known_results: Vec<DecodeResult> = phase1
.iter()
.filter(|r| r.message77() == m_known)
.cloned()
.collect();
assert!(
!known_results.is_empty(),
"Phase 1 must decode the known signal for this test to be meaningful"
);
fn band_energy(samples: &[i16], f_lo: f32, f_hi: f32) -> f64 {
let n = samples.len();
let fs = 12_000.0_f64;
let k_lo = ((f_lo as f64) * (n as f64) / fs).floor() as usize;
let k_hi = ((f_hi as f64) * (n as f64) / fs).ceil() as usize;
let mut energy = 0.0_f64;
for k in k_lo..=k_hi {
let mut re = 0.0_f64;
let mut im = 0.0_f64;
let w = 2.0 * core::f64::consts::PI * (k as f64) / (n as f64);
for (i, &s) in samples.iter().enumerate() {
let phi = w * (i as f64);
re += (s as f64) * phi.cos();
im -= (s as f64) * phi.sin();
}
energy += re * re + im * im;
}
energy
}
let e_before = band_energy(&audio, f0 - 1.0, f0 + 1.0);
let mut audio_clean = audio.clone();
for r in &known_results {
subtract_signal_lpf(&mut audio_clean, r);
}
let (new_results, residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio_clean,
200.0,
2800.0,
1.0,
None,
DecodeDepth::FULL,
50,
DecodeStrictness::Normal,
None,
);
let _ = new_results;
let e_after = band_energy(&residual, f0 - 1.0, f0 + 1.0);
assert!(
e_after * 2.0 < e_before,
"expected residual band energy at known signal's frequency \
to drop by >2× after SIC; got e_before={e_before:.3e}, \
e_after={e_after:.3e} (fix not applied?)"
);
}
#[cfg(test)]
struct TestBpf {
sections: Vec<(f64, f64, f64, f64, f64, f64, f64)>, }
#[cfg(test)]
impl TestBpf {
fn design(n_poles: usize, f_low: f64, f_high: f64, fs: f64) -> Self {
fn csqrt(re: f64, im: f64) -> (f64, f64) {
let r = (re * re + im * im).sqrt();
let theta = im.atan2(re);
let sr = r.sqrt();
(sr * (theta / 2.0).cos(), sr * (theta / 2.0).sin())
}
fn lp_to_bp(p_re: f64, p_im: f64, bw: f64, w0sq: f64) -> [(f64, f64); 2] {
let pbw_re = p_re * bw;
let pbw_im = p_im * bw;
let d_re = pbw_re * pbw_re - pbw_im * pbw_im - 4.0 * w0sq;
let d_im = 2.0 * pbw_re * pbw_im;
let (sd_re, sd_im) = csqrt(d_re, d_im);
[
((pbw_re + sd_re) / 2.0, (pbw_im + sd_im) / 2.0),
((pbw_re - sd_re) / 2.0, (pbw_im - sd_im) / 2.0),
]
}
fn bilinear(s_re: f64, s_im: f64, t: f64) -> (f64, f64) {
let nr = 1.0 + s_re * t;
let ni = s_im * t;
let dr = 1.0 - s_re * t;
let di = -s_im * t;
let d_sq = dr * dr + di * di;
((nr * dr + ni * di) / d_sq, (ni * dr - nr * di) / d_sq)
}
let t = 1.0 / (2.0 * fs);
let wl = (core::f64::consts::PI * f_low / fs).tan() / t;
let wh = (core::f64::consts::PI * f_high / fs).tan() / t;
let w0sq = wl * wh;
let bw = wh - wl;
let half = n_poles / 2;
let mut sections = Vec::with_capacity(n_poles);
for k in 0..half {
let theta = core::f64::consts::PI * (2.0 * k as f64 + n_poles as f64 + 1.0)
/ (2.0 * n_poles as f64);
let (p_re, p_im) = (theta.cos(), theta.sin());
for (s_re, s_im) in lp_to_bp(p_re, p_im, bw, w0sq) {
let (z_re, z_im) = bilinear(s_re, s_im, t);
sections.push((
1.0,
0.0,
-1.0,
-2.0 * z_re,
z_re * z_re + z_im * z_im,
0.0,
0.0,
));
}
}
let fc = (f_low * f_high).sqrt();
let wc = 2.0 * core::f64::consts::PI * fc / fs;
let mag_at = |b0: f64, b1: f64, b2: f64, a1: f64, a2: f64, w: f64| -> f64 {
let (c1, s1) = (w.cos(), w.sin());
let (c2, s2) = ((2.0 * w).cos(), (2.0 * w).sin());
let nr = b0 * c2 + b1 * c1 + b2;
let ni = b0 * s2 + b1 * s1;
let dr = c2 + a1 * c1 + a2;
let di = s2 + a1 * s1;
((nr * nr + ni * ni) / (dr * dr + di * di)).sqrt()
};
let gain: f64 = sections
.iter()
.map(|&(b0, b1, b2, a1, a2, _, _)| mag_at(b0, b1, b2, a1, a2, wc))
.product();
if let Some(sec) = sections.first_mut() {
sec.0 /= gain;
sec.2 /= gain;
}
TestBpf { sections }
}
fn filter(&mut self, input: &[f32]) -> Vec<f32> {
input
.iter()
.map(|&x| {
let mut y = x as f64;
for sec in &mut self.sections {
let (b0, b1, b2, a1, a2, s1, s2) = *sec;
let out = b0 * y + s1;
sec.5 = b1 * y - a1 * out + s2;
sec.6 = b2 * y - a2 * out;
y = out;
}
y as f32
})
.collect()
}
}
#[test]
fn sic_early_eq_mode_reaches_sic_engine() {
use crate::ft8::wave_gen::{message_to_tones, tones_to_f32};
use crate::msg::wsjt77::pack77;
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn uniform01(&mut self) -> f32 {
((self.next_u64() >> 40) as f32 / (1u64 << 24) as f32).max(1e-9)
}
fn gaussian(&mut self) -> f32 {
let u1 = self.uniform01();
let u2 = self.uniform01();
(-2.0 * u1.ln()).sqrt() * (2.0 * core::f32::consts::PI * u2).cos()
}
}
const FS: f32 = 12_000.0;
const F0: f32 = 1_000.0; const BPF_LO: f64 = 1_000.0;
const BPF_HI: f64 = 1_500.0;
const REF_BW: f32 = 2_500.0;
const TARGET_SNR_DB: f32 = -22.0;
let m77 = pack77("CQ", "K1ABC", "FN42").expect("pack77");
let tones = message_to_tones(&m77);
let snr_linear = 10.0_f32.powf(TARGET_SNR_DB / 10.0);
let amplitude = (4.0 * snr_linear * REF_BW / FS).sqrt();
let sig = tones_to_f32(&tones, F0, amplitude);
let n = 15 * 12_000;
let mut mix = vec![0.0f32; n];
let off = 6_000usize;
let n_sig = sig.len().min(n - off);
mix[off..off + n_sig].copy_from_slice(&sig[..n_sig]);
let mut rng = Rng(0xC0FF_EE12_3456_789A);
for s in mix.iter_mut() {
*s += rng.gaussian();
}
let mut bpf = TestBpf::design(4, BPF_LO, BPF_HI, FS as f64);
let filtered = bpf.filter(&mix);
let peak = filtered.iter().map(|s| s.abs()).fold(0.0_f32, f32::max);
let iq_scale = if peak > 1e-6 { 29_000.0 / peak } else { 1.0 };
let audio: Vec<i16> = filtered
.iter()
.map(|&s| (s * iq_scale).clamp(-32_768.0, 32_767.0) as i16)
.collect();
let find = |eq: EqMode| -> Option<DecodeResult> {
let results = DecodeRequest::<Ft8>::new(
&audio,
(BPF_LO as f32) - 50.0,
(BPF_HI as f32) + 50.0,
0.8,
10,
)
.eq_mode(eq)
.sic_early()
.decode()
.results;
results.into_iter().find(|r| r.message77() == m77)
};
assert!(
find(EqMode::Off).is_none(),
"expected the BPF-edge signal to NOT decode with eq_mode(Off) \
(test fixture drifted off the calibrated marginal point)"
);
assert!(
find(EqMode::Local).is_some(),
"expected eq_mode(Local) to decode the same BPF-edge signal \
that eq_mode(Off) misses through .sic_early() — eq_mode is not \
reaching the SIC engine"
);
}
#[test]
fn silence_no_decode() {
let audio = vec![0i16; 15 * 12_000];
let results = DecodeRequest::<Ft8>::new(&audio, 200.0, 2800.0, 1.0, 10)
.osd(false)
.decode()
.results;
assert!(results.is_empty(), "silence should decode nothing");
}
#[test]
fn sniper_silence_no_decode() {
let audio = vec![0i16; 15 * 12_000];
let results = SniperRequest::<Ft8>::new(&audio, 1000.0, 10)
.osd(false)
.decode()
.results;
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 = DecodeRequest::<Ft8>::new(&audio, 100.0, 3000.0, 1.0, 200)
.decode()
.results;
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");
}
#[test]
#[ignore = "manual diagnostic — internal BP/OSD trace on CCIR losing trials (issue #72)"]
fn ft8_diag_internal_osd_trace() {
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
const GOLDEN_FREQ_HZ: f32 = 1500.0;
const FREQ_TOL_HZ: f32 = 5.0;
let manifest = env!("CARGO_MANIFEST_DIR");
let dir = std::path::Path::new(manifest).join("../embedded-poc/assets/ft8_sweep");
for &(chan, snr_tag, trial) in &[
("ccir_poor", "m18", 1u32),
("ccir_poor", "m18", 3),
("ccir_poor", "m17", 3),
("ccir_moderate", "m17", 11),
] {
let path = dir.join(format!("ft8_{chan}_{snr_tag}_{trial:02}.wav"));
let Some(audio) = load_wav_i16(&path) else {
eprintln!("skip {path:?}");
continue;
};
let spec = crate::ft8::decode_block::compute_spectrogram(&audio, 3000.0);
let candidates = crate::ft8::decode_block::coarse_sync(&spec, 100.0, 3000.0, 0.8, 50);
let fft_cache = build_fft_cache(&audio);
for c in candidates
.iter()
.filter(|c| (c.freq_hz - GOLDEN_FREQ_HZ).abs() <= FREQ_TOL_HZ)
{
eprintln!(
"{chan} {snr_tag} trial {trial}: cand freq={:.2} dt={:.3} score={:.4}",
c.freq_hz, c.dt_sec, c.score
);
let r = process_candidate(
c,
&audio,
&fft_cache,
DecodeDepth::FULL,
DecodeStrictness::default(),
&[],
EqMode::Off,
None,
);
eprintln!(" -> process_candidate result: {:?}", r.map(|d| d.pass));
}
}
}
#[test]
#[ignore = "manual diagnostic — issue #180 staged-residual DL8YHR probe"]
fn issue_180_dl8yhr_staged_checkpoint_c_probe() {
use crate::engine::sync::{SyncCandidate, refine_candidate};
use crate::fec::ldpc::bp::bp_decode;
use crate::fec::ldpc::osd::{osd_decode_deep4, osd_decode_npre1, osd_decode_npre1_npre2};
use crate::ft8::Ft8;
use crate::ft8::decode_block::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use crate::ft8::downsample::downsample;
use crate::ft8::llr::compute_llr;
use crate::msg::wsjt77::unpack77;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let target = "CQ DX DL8YHR JO41";
let (results, residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio,
100.0,
3000.0,
0.8,
None,
DecodeDepth::FULL,
200,
DecodeStrictness::Normal,
None,
);
println!(
"decode_frame_subtract_staged: {} decodes (checkpoint-C residual captured)",
results.len()
);
let mut freqs = vec![2606.25f32];
for df in [-6.25, -3.0, 3.0, 6.25, -9.0, 9.0] {
freqs.push(2606.25 + df);
}
let mut dts = vec![0.195f32];
for ddt in [-0.05, 0.05, -0.1, 0.1, -0.16, 0.16] {
dts.push(0.195 + ddt);
}
let mut best: Option<(f32, f32, u32)> = None;
let mut any_hit = false;
for &freq in &freqs {
for &dt in &dts {
let cand = SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let (cd0, _cache) = downsample(&residual, cand.freq_hz, None);
let refined = refine_candidate::<Ft8>(&cd0, &cand, 10);
let mut cs = symbol_spectra_direct::<i16>(
&residual,
cand.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
let q = sync_quality(&cs);
fill_symbol_spectra(
&mut cs,
&residual,
cand.freq_hz,
refined.dt_sec,
SymMask::DataOnly,
None,
);
let llr_set = compute_llr::<f32>(&cs);
for llr in [&llr_set.llra, &llr_set.llrb, &llr_set.llrc, &llr_set.llrd] {
if let Some(bp) = bp_decode(llr, None, 40, None) {
let text = unpack77(&bp.message77).unwrap_or_default();
if text == target {
println!(
"BP HIT: freq={freq:.2} dt={dt:.3} refined_dt={:+.3} q={q}",
refined.dt_sec
);
any_hit = true;
}
}
let osd = if q >= 18 {
osd_decode_npre1_npre2(llr)
} else {
osd_decode_npre1(llr)
};
if let Some(o) = osd {
let text = unpack77(&o.message77).unwrap_or_default();
if text == target {
println!(
"OSD(wsjtx-faithful) HIT: freq={freq:.2} dt={dt:.3} refined_dt={:+.3} q={q}",
refined.dt_sec
);
any_hit = true;
}
}
if let Some(o) = osd_decode_deep4(llr, 30, None) {
let text = unpack77(&o.message77).unwrap_or_default();
if text == target {
println!(
"OSD(deep4) HIT: freq={freq:.2} dt={dt:.3} refined_dt={:+.3} q={q}",
refined.dt_sec
);
any_hit = true;
}
}
}
let is_better = match &best {
None => true,
Some((_, _, bq)) => q > *bq,
};
if is_better {
best = Some((freq, dt, q));
}
println!(
" probe freq={freq:.2} dt={dt:.3} refined_dt={:+.3} q={q}",
refined.dt_sec
);
}
}
if let Some((freq, dt, q)) = best {
println!(
"\nBest sync_quality on staged checkpoint-C residual: freq={freq:.2} dt={dt:.3} q={q}"
);
}
println!("any_hit={any_hit}");
{
println!(
"\nPer-block Costas breakdown, no-refine (raw candidate dt/freq fed directly):"
);
println!(" jt9 (real, this run): is1=2 is2=7 is3=6 nsync=15");
let step = 1.0f32 / 200.0;
for k in -3i32..=3 {
let dt = 0.195f32 + (k as f32) * step;
let freq = 2606.25f32;
let cand = SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let cs = symbol_spectra_direct::<i16>(
&residual,
cand.freq_hz,
cand.dt_sec,
SymMask::SyncOnly,
None,
);
print!(" mfsk-core dt={dt:.4} (k={k:+}):");
let mut total = 0u32;
for (bi, block) in <Ft8 as crate::engine::FrameLayout>::SYNC_MODE
.blocks()
.iter()
.enumerate()
{
let start = block.start_symbol as usize;
let mut hits = 0u32;
for (t, &expected) in block.pattern.iter().enumerate() {
let sym = start + t;
let mut best_tone = 0usize;
let mut best_val = cs[sym][0].norm_sqr();
for a in 1..8 {
let v = cs[sym][a].norm_sqr();
if v > best_val {
best_val = v;
best_tone = a;
}
}
if best_tone == expected as usize {
hits += 1;
}
}
total += hits;
print!(" is{}={hits}", bi + 1);
}
println!(" nsync={total}");
}
let cand0 = SyncCandidate {
freq_hz: 2606.25,
dt_sec: 0.195,
score: 0.0,
};
let (cd0, _cache) = downsample(&residual, cand0.freq_hz, None);
let refined = refine_candidate::<Ft8>(&cd0, &cand0, 10);
let cs = symbol_spectra_direct::<i16>(
&residual,
cand0.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
print!(" mfsk-core refine_candidate dt={:.4}:", refined.dt_sec);
let mut total = 0u32;
for (bi, block) in <Ft8 as crate::engine::FrameLayout>::SYNC_MODE
.blocks()
.iter()
.enumerate()
{
let start = block.start_symbol as usize;
let mut hits = 0u32;
for (t, &expected) in block.pattern.iter().enumerate() {
let sym = start + t;
let mut best_tone = 0usize;
let mut best_val = cs[sym][0].norm_sqr();
for a in 1..8 {
let v = cs[sym][a].norm_sqr();
if v > best_val {
best_val = v;
best_tone = a;
}
}
if best_tone == expected as usize {
hits += 1;
}
}
total += hits;
print!(" is{}={hits}", bi + 1);
}
println!(" nsync={total}");
let cs_raw =
symbol_spectra_direct::<i16>(&audio, 2606.25, 0.195, SymMask::SyncOnly, None);
print!(" mfsk-core RAW audio dt=0.1950 (no subtraction at all):");
let mut total_raw = 0u32;
for (bi, block) in <Ft8 as crate::engine::FrameLayout>::SYNC_MODE
.blocks()
.iter()
.enumerate()
{
let start = block.start_symbol as usize;
let mut hits = 0u32;
for (t, &expected) in block.pattern.iter().enumerate() {
let sym = start + t;
let mut best_tone = 0usize;
let mut best_val = cs_raw[sym][0].norm_sqr();
for a in 1..8 {
let v = cs_raw[sym][a].norm_sqr();
if v > best_val {
best_val = v;
best_tone = a;
}
}
if best_tone == expected as usize {
hits += 1;
}
}
total_raw += hits;
print!(" is{}={hits}", bi + 1);
}
println!(" nsync={total_raw}");
let icos7 = [3u8, 1, 4, 0, 6, 5, 2];
let cs_all =
symbol_spectra_direct::<i16>(&residual, 2606.25, 0.195, SymMask::SyncOnly, None);
for (label, block_start, k_start) in [
("Block-1", 0usize, 1u32),
("Block-2", 36, 37),
("Block-3", 72, 73),
] {
println!("\n{label} (sym {}..{}):", block_start, block_start + 6);
for t in 0..7 {
let sym = block_start + t;
let argmax_of =
|cs: &[[num_complex::Complex<f32>; 8]; 79]| -> (usize, Vec<f32>) {
let mags: Vec<f32> = (0..8).map(|a| cs[sym][a].norm()).collect();
let mut best = 0usize;
for a in 1..8 {
if mags[a] > mags[best] {
best = a;
}
}
(best, mags)
};
let (best_res, mags_res) = argmax_of(&cs_all);
let (best_raw, mags_raw) = argmax_of(&cs_raw);
let mark_res = if best_res == icos7[t] as usize {
"OK "
} else {
"BAD"
};
let mark_raw = if best_raw == icos7[t] as usize {
"OK "
} else {
"BAD"
};
println!(
" k={:>2} t={} exp={} RAW argmax={} [{mark_raw}] {:>7.1?} | RESIDUAL argmax={} [{mark_res}] {:>7.1?}",
k_start + t as u32,
t + 1,
icos7[t],
best_raw,
mags_raw,
best_res,
mags_res,
);
}
}
println!(
"\ncd0 dump (staged residual, f1=2606.25, dt=0.195), Rust idx -> Fortran idx = idx+1:"
);
for rust_idx in 267..=330usize {
let c = cd0[rust_idx];
println!(
" rust_idx={:>4} fortran_idx={:>4} re={:>12.4} im={:>12.4}",
rust_idx,
rust_idx + 1,
c.re,
c.im
);
}
if let Some(msg77) = crate::msg::wsjt77::pack77("WA2FZW", "DL5AXX", "RR73") {
let mut info = vec![0u8; 91];
info[..77].copy_from_slice(&msg77);
let wa2fzw = DecodeResult {
info: info.into_boxed_slice(),
freq_hz: 2545.88,
dt_sec: -0.125,
hard_errors: 0,
sync_score: 0.0,
pass: 0,
sync_cv: 0.0,
snr_db: 0.0,
};
let mut residual2 = residual.clone();
let refined_freq = crate::ft8::subtract::refine_signal_freq(&residual2, &wa2fzw);
let mut wa2fzw_r = wa2fzw.clone();
wa2fzw_r.freq_hz = refined_freq;
subtract_signal_lpf(&mut residual2, &wa2fzw_r);
let cs_wa = symbol_spectra_direct::<i16>(
&residual2,
2606.25,
0.195,
SymMask::SyncOnly,
None,
);
print!(
"\nAfter manually subtracting jt9's WA2FZW DL5AXX RR73 (refined freq={refined_freq:.2}):"
);
let mut total_wa = 0u32;
for block in <Ft8 as crate::engine::FrameLayout>::SYNC_MODE
.blocks()
.iter()
{
let start = block.start_symbol as usize;
let mut hits = 0u32;
for (t, &expected) in block.pattern.iter().enumerate() {
let sym = start + t;
let mut best_tone = 0usize;
let mut best_val = cs_wa[sym][0].norm_sqr();
for a in 1..8 {
let v = cs_wa[sym][a].norm_sqr();
if v > best_val {
best_val = v;
best_tone = a;
}
}
if best_tone == expected as usize {
hits += 1;
}
}
total_wa += hits;
print!(" {hits}");
}
println!(" nsync={total_wa} (was 9 before this subtract; jt9=15)");
} else {
println!(
"\npack77(WA2FZW,DL5AXX,RR73) failed to encode — cannot run confirmation test"
);
}
if let Ok(raw) = std::fs::read("/tmp/jt9_post_sic_dd.raw") {
let jt9_residual: Vec<i16> = raw
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
println!(
"\nLoaded jt9's own post-SIC residual: {} samples",
jt9_residual.len()
);
let cs_jt9 = symbol_spectra_direct::<i16>(
&jt9_residual,
2606.25,
0.195,
SymMask::SyncOnly,
None,
);
print!("mfsk-core's own tone-detection on jt9's post-SIC residual:");
let mut total_jt9 = 0u32;
for block in <Ft8 as crate::engine::FrameLayout>::SYNC_MODE
.blocks()
.iter()
{
let start = block.start_symbol as usize;
let mut hits = 0u32;
for (t, &expected) in block.pattern.iter().enumerate() {
let sym = start + t;
let mut best_tone = 0usize;
let mut best_val = cs_jt9[sym][0].norm_sqr();
for a in 1..8 {
let v = cs_jt9[sym][a].norm_sqr();
if v > best_val {
best_val = v;
best_tone = a;
}
}
if best_tone == expected as usize {
hits += 1;
}
}
total_jt9 += hits;
print!(" {hits}");
}
println!(
" nsync={total_jt9} (mfsk-core residual gave 9; real jt9 on this exact buffer gives 15)"
);
use crate::fec::ldpc::bp::bp_decode;
use crate::fec::ldpc::osd::{
osd_decode_deep4, osd_decode_npre1, osd_decode_npre1_npre2,
};
use crate::ft8::llr::compute_llr;
use crate::msg::wsjt77::unpack77;
let mut cs_full = cs_jt9.clone();
fill_symbol_spectra(
&mut cs_full,
&jt9_residual,
2606.25,
0.195,
SymMask::DataOnly,
None,
);
let llr_set = compute_llr::<f32>(&cs_full);
let mut decoded_msg: Option<String> = None;
for llr in [&llr_set.llra, &llr_set.llrb, &llr_set.llrc, &llr_set.llrd] {
if decoded_msg.is_none()
&& let Some(bp) = bp_decode(llr, None, 40, None)
{
decoded_msg = unpack77(&bp.message77);
}
if decoded_msg.is_none() {
let osd = if total_jt9 >= 18 {
osd_decode_npre1_npre2(llr)
} else {
osd_decode_npre1(llr)
};
if let Some(o) = osd {
decoded_msg = unpack77(&o.message77);
}
}
if decoded_msg.is_none()
&& let Some(o) = osd_decode_deep4(llr, 30, None)
{
decoded_msg = unpack77(&o.message77);
}
}
println!("Full BP/OSD decode on jt9's post-SIC residual: {decoded_msg:?}");
} else {
println!(
"\n/tmp/jt9_post_sic_dd.raw not found — run the instrumented jt9 build first"
);
}
}
}
#[test]
#[ignore = "manual diagnostic — issue #180 DK8NE own-SIC score probe"]
fn issue_180_dk8ne_own_sic_score_probe() {
use crate::engine::sync::refine_candidate;
use crate::ft8::decode_block::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use crate::ft8::downsample::downsample;
use crate::ft8::llr::sync_quality;
use crate::msg::wsjt77::unpack77;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let (results, residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio,
100.0,
3000.0,
0.8,
None,
DecodeDepth::FULL,
200,
DecodeStrictness::Normal,
None,
);
let has_dk8ne = results
.iter()
.any(|r| unpack77(r.message77()).as_deref() == Some("K1BZM DK8NE -10"));
println!("staged pipeline already found DK8NE blind: {has_dk8ne}");
let freq = 244.2f32;
let dt = 0.505f32;
let cand = crate::engine::sync::SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let (cd0, _cache) = downsample(&residual, cand.freq_hz, None);
let refined = refine_candidate::<crate::ft8::Ft8>(&cd0, &cand, 10);
let mut cs = symbol_spectra_direct::<i16>(
&residual,
cand.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
let q = sync_quality(&cs);
fill_symbol_spectra(
&mut cs,
&residual,
cand.freq_hz,
refined.dt_sec,
SymMask::DataOnly,
None,
);
let icos7: [u8; 7] = [3, 1, 4, 0, 6, 5, 2];
let mut is = [0u32; 3];
for (b, base) in [0usize, 36, 72].iter().enumerate() {
for (k, &tone) in icos7.iter().enumerate() {
let sym = base + k;
let mut best = 0usize;
let mut best_mag = -1.0f32;
for t in 0..8 {
let m = cs[sym][t].norm();
if m > best_mag {
best_mag = m;
best = t;
}
}
if best == tone as usize {
is[b] += 1;
}
}
}
println!(
"mfsk-core's OWN staged-SIC residual: freq={freq:.2} dt={dt:.3} refined_dt={:+.3} q={q} is1={} is2={} is3={}",
refined.dt_sec, is[0], is[1], is[2]
);
println!("jt9's own residual (ground truth): nsync=11 is1=1 is2=7 is3=3");
use crate::fec::ldpc::bp::bp_decode;
use crate::fec::ldpc::osd::{osd_decode_deep4, osd_decode_npre1, osd_decode_npre1_npre2};
use crate::ft8::llr::compute_llr;
let llr_set = compute_llr::<f32>(&cs);
let target = "K1BZM DK8NE -10";
for (name, llr) in [
("a", &llr_set.llra),
("b", &llr_set.llrb),
("c", &llr_set.llrc),
("d", &llr_set.llrd),
] {
if let Some(bp) = bp_decode(llr, None, 40, None) {
let text = unpack77(&bp.message77).unwrap_or_default();
println!(" BP({name}) -> {text:?} hard_errors={}", bp.hard_errors);
} else {
println!(" BP({name}) -> no convergence");
}
if let Some(o) = osd_decode_npre1_npre2(llr) {
println!(
" OSD-npre1npre2({name}) -> {:?} hard_errors={}",
unpack77(&o.message77).unwrap_or_default(),
o.hard_errors
);
} else if let Some(o) = osd_decode_npre1(llr) {
println!(
" OSD-npre1({name}) -> {:?} hard_errors={}",
unpack77(&o.message77).unwrap_or_default(),
o.hard_errors
);
} else {
println!(" OSD-npre1(npre2)({name}) -> no candidate");
}
if let Some(o) = osd_decode_deep4(llr, 30, None) {
println!(
" OSD-deep4({name}) -> {:?} hard_errors={}",
unpack77(&o.message77).unwrap_or_default(),
o.hard_errors
);
} else {
println!(" OSD-deep4({name}) -> no candidate");
}
}
let _ = target;
}
#[test]
#[ignore = "manual diagnostic — issue #180 DK8NE data-symbol residual comparison"]
fn issue_180_dk8ne_data_symbol_comparison() {
use crate::engine::sync::refine_candidate;
use crate::ft8::decode_block::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use crate::ft8::downsample::downsample;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let (_results, mfsk_residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio,
100.0,
3000.0,
0.8,
None,
DecodeDepth::FULL,
200,
DecodeStrictness::Normal,
None,
);
let jt9_bytes = match std::fs::read("/tmp/jt9_post_sic_dd.raw") {
Ok(b) => b,
Err(_) => {
eprintln!(
"skipping issue_180_dk8ne_data_symbol_comparison: \
/tmp/jt9_post_sic_dd.raw (WSJT-X jt9 post-SIC residual dump) \
not present — this is a local-only diagnostic input"
);
return;
}
};
let jt9_residual: Vec<i16> = jt9_bytes
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
let freq = 244.2f32;
let dt = 0.505f32;
type SymSpectra = alloc::boxed::Box<[[crate::engine::scalar::Cmplx<f32>; 8]; 79]>;
let mut spectra: Vec<(&str, SymSpectra)> = Vec::new();
for (label, residual) in [
("mfsk-core own SIC", &mfsk_residual),
("jt9 own SIC", &jt9_residual),
] {
let cand = crate::engine::sync::SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let (cd0, _cache) = downsample(residual, cand.freq_hz, None);
let refined = refine_candidate::<crate::ft8::Ft8>(&cd0, &cand, 10);
let mut cs = symbol_spectra_direct::<i16>(
residual,
cand.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
fill_symbol_spectra(
&mut cs,
residual,
cand.freq_hz,
refined.dt_sec,
SymMask::DataOnly,
None,
);
spectra.push((label, cs));
}
let data_syms: Vec<usize> = (7..36).chain(43..72).collect();
let (mfsk_cs, jt9_cs) = (&spectra[0].1, &spectra[1].1);
let mut diverge_count = 0usize;
for &sym in &data_syms {
let argmax = |cs: &[[crate::engine::scalar::Cmplx<f32>; 8]; 79]| -> (usize, f32) {
let mut best = 0usize;
let mut best_mag = -1.0f32;
for t in 0..8 {
let m = cs[sym][t].norm();
if m > best_mag {
best_mag = m;
best = t;
}
}
(best, best_mag)
};
let (m_tone, m_mag) = argmax(mfsk_cs);
let (j_tone, j_mag) = argmax(jt9_cs);
if m_tone != j_tone {
diverge_count += 1;
println!(
" sym={sym:2} DIVERGE mfsk: tone={m_tone} mag={m_mag:8.1} | jt9: tone={j_tone} mag={j_mag:8.1}"
);
}
}
println!(
"\n{diverge_count}/{} data-symbol argmax disagreements between mfsk-core's own SIC residual and jt9's own SIC residual (identical sync-symbol scores, both q=11/is1=1/is2=7/is3=3)",
data_syms.len()
);
let mut mfsk_energy = 0f64;
let mut jt9_energy = 0f64;
for &sym in &data_syms {
for t in 0..8 {
mfsk_energy += (mfsk_cs[sym][t].norm() as f64).powi(2);
jt9_energy += (jt9_cs[sym][t].norm() as f64).powi(2);
}
}
println!(
"data-region total energy: mfsk-core={mfsk_energy:.1} jt9={jt9_energy:.1} ratio={:.3}",
mfsk_energy / jt9_energy
);
}
#[test]
#[ignore = "manual diagnostic — issue #182 DK8NE LLR reliability-ordering comparison"]
fn issue_182_dk8ne_llr_reliability_comparison() {
use crate::engine::sync::refine_candidate;
use crate::ft8::decode_block::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use crate::ft8::downsample::downsample;
use crate::ft8::llr::compute_llr;
use crate::ft8::params::LDPC_N;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let (_results, mfsk_residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio,
100.0,
3000.0,
0.8,
None,
DecodeDepth::FULL,
200,
DecodeStrictness::Normal,
None,
);
let jt9_bytes = match std::fs::read("/tmp/jt9_post_sic_dd.raw") {
Ok(b) => b,
Err(_) => {
eprintln!(
"skipping issue_182_dk8ne_llr_reliability_comparison: \
/tmp/jt9_post_sic_dd.raw (WSJT-X jt9 post-SIC residual dump) \
not present — this is a local-only diagnostic input"
);
return;
}
};
let jt9_residual: Vec<i16> = jt9_bytes
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
let freq = 244.2f32;
let dt = 0.505f32;
let mut llr_sets: Vec<(&str, crate::ft8::llr::LlrSet<f32>)> = Vec::new();
for (label, residual) in [
("mfsk-core own SIC", &mfsk_residual),
("jt9 own SIC", &jt9_residual),
] {
let cand = crate::engine::sync::SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let (cd0, _cache) = downsample(residual, cand.freq_hz, None);
let refined = refine_candidate::<crate::ft8::Ft8>(&cd0, &cand, 10);
let mut cs = symbol_spectra_direct::<i16>(
residual,
cand.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
fill_symbol_spectra(
&mut cs,
residual,
cand.freq_hz,
refined.dt_sec,
SymMask::DataOnly,
None,
);
llr_sets.push((label, compute_llr::<f32>(&cs)));
}
let (mfsk_llr, jt9_llr) = (&llr_sets[0].1, &llr_sets[1].1);
const BASIS_SIZE: usize = 91;
for (name, m, j) in [
("a", &mfsk_llr.llra, &jt9_llr.llra),
("b", &mfsk_llr.llrb, &jt9_llr.llrb),
("c", &mfsk_llr.llrc, &jt9_llr.llrc),
("d", &mfsk_llr.llrd, &jt9_llr.llrd),
] {
let hard_disagree = (0..LDPC_N)
.filter(|&i| (m[i] > 0.0) != (j[i] > 0.0))
.count();
let mut m_rank: Vec<usize> = (0..LDPC_N).collect();
m_rank.sort_by(|&x, &y| m[y].abs().partial_cmp(&m[x].abs()).unwrap());
let mut j_rank: Vec<usize> = (0..LDPC_N).collect();
j_rank.sort_by(|&x, &y| j[y].abs().partial_cmp(&j[x].abs()).unwrap());
let m_top: std::collections::HashSet<usize> =
m_rank[..BASIS_SIZE].iter().copied().collect();
let j_top: std::collections::HashSet<usize> =
j_rank[..BASIS_SIZE].iter().copied().collect();
let overlap = m_top.intersection(&j_top).count();
let basis_hard_disagree = m_top
.intersection(&j_top)
.filter(|&&i| (m[i] > 0.0) != (j[i] > 0.0))
.count();
println!(
"llr({name}): hard_disagree={hard_disagree}/{LDPC_N} top-{BASIS_SIZE}-overlap={overlap}/{BASIS_SIZE} basis_hard_disagree={basis_hard_disagree}"
);
}
}
#[test]
#[ignore = "manual diagnostic — issue #182 Fortran-pivot-window OSD basis probe"]
fn issue_182_dk8ne_osd_fortran_pivot_probe() {
use crate::engine::sync::refine_candidate;
use crate::fec::ldpc::bp::bp_llr_zsum;
use crate::fec::ldpc::osd::{
osd_debug_basis_sets, osd_decode, osd_decode_npre1, osd_decode_npre1_fortran_pivot,
};
use crate::fec::ldpc::params::Ldpc174_91Params;
use crate::ft8::decode_block::{SymMask, fill_symbol_spectra, symbol_spectra_direct};
use crate::ft8::downsample::downsample;
use crate::ft8::llr::compute_llr;
use crate::msg::wsjt77::unpack77;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let (_results, mfsk_residual) = decode_frame_subtract_staged_with_ap_debug_residual(
&audio,
100.0,
3000.0,
0.8,
None,
DecodeDepth::FULL,
200,
DecodeStrictness::Normal,
None,
);
let freq = 244.2f32;
let dt = 0.505f32;
let cand = crate::engine::sync::SyncCandidate {
freq_hz: freq,
dt_sec: dt,
score: 0.0,
};
let (cd0, _cache) = downsample(&mfsk_residual, cand.freq_hz, None);
let refined = refine_candidate::<crate::ft8::Ft8>(&cd0, &cand, 10);
let mut cs = symbol_spectra_direct::<i16>(
&mfsk_residual,
cand.freq_hz,
refined.dt_sec,
SymMask::SyncOnly,
None,
);
fill_symbol_spectra(
&mut cs,
&mfsk_residual,
cand.freq_hz,
refined.dt_sec,
SymMask::DataOnly,
None,
);
let llr_set = compute_llr::<f32>(&cs);
let target = "K1BZM DK8NE -10";
for (name, llr) in [
("a", &llr_set.llra),
("b", &llr_set.llrb),
("c", &llr_set.llrc),
("d", &llr_set.llrd),
] {
let current = osd_decode_npre1(llr)
.map(|o| (unpack77(&o.message77).unwrap_or_default(), o.hard_errors));
let fortran_pivot = osd_decode_npre1_fortran_pivot(llr)
.map(|o| (unpack77(&o.message77).unwrap_or_default(), o.hard_errors));
let (basis_current, basis_fortran) = osd_debug_basis_sets(llr);
let set_current: std::collections::HashSet<usize> =
basis_current.iter().copied().collect();
let set_fortran: std::collections::HashSet<usize> =
basis_fortran.iter().copied().collect();
let basis_overlap = set_current.intersection(&set_fortran).count();
let exhaustive = osd_decode(llr)
.map(|o| (unpack77(&o.message77).unwrap_or_default(), o.hard_errors));
println!(
"llr({name}): current_basis={current:?} fortran_pivot_basis={fortran_pivot:?} basis_position_overlap={basis_overlap}/{} exhaustive_order2={exhaustive:?}",
set_current.len()
);
for n_iter in [1u32, 2u32] {
let zsum_vec = bp_llr_zsum::<Ldpc174_91Params>(llr, n_iter);
let mut zsum = [0f32; crate::ft8::params::LDPC_N];
zsum.copy_from_slice(&zsum_vec);
let via_zsum = osd_decode_npre1(&zsum)
.map(|o| (unpack77(&o.message77).unwrap_or_default(), o.hard_errors));
println!(" bp_llr_zsum(llr, {n_iter}) -> osd_decode_npre1: {via_zsum:?}");
}
if let Some((msg, _)) = &fortran_pivot
&& msg == target
{
println!(" -> fortran_pivot_basis RECOVERS {target} on llr variant {name}!");
}
}
}
#[test]
#[ignore = "manual diagnostic — issue #182 zsum-fix phantom check"]
fn issue_182_zsum_fix_phantom_check() {
use crate::msg::wsjt77::unpack77;
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
let ap = ApHint::new().with_call1("K1JT").with_call2("HA0DU");
let results = DecodeRequest::<Ft8>::new(&audio, 100.0, 3000.0, 1.3, 50)
.strictness(DecodeStrictness::Normal)
.sic_early()
.ap_hint(&ap)
.decode()
.results;
for r in &results {
let msg = unpack77(r.message77()).unwrap_or_default();
println!(
"pass={:3} hard_errors={:3} freq={:8.2} dt={:+.3} msg={msg:?}",
r.pass, r.hard_errors, r.freq_hz, r.dt_sec
);
}
}
#[test]
#[ignore = "manual diagnostic — issue #182 post-fix wall-clock check"]
fn issue_182_postfix_wallclock_check() {
fn load_wav_i16(path: &std::path::Path) -> Option<alloc::vec::Vec<i16>> {
let bytes = std::fs::read(path).ok()?;
if bytes.len() < 44 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return None;
}
let mut i = 12usize;
let mut data_off = None;
let mut data_len = 0usize;
while i + 8 <= bytes.len() {
let id = &bytes[i..i + 4];
let sz = u32::from_le_bytes(bytes[i + 4..i + 8].try_into().unwrap()) as usize;
let body = i + 8;
if id == b"data" {
data_off = Some(body);
data_len = sz;
break;
}
match body.checked_add(sz).and_then(|s| s.checked_add(sz & 1)) {
Some(next) => i = next,
None => break,
}
}
let off = data_off?;
let end = off.saturating_add(data_len).min(bytes.len());
Some(
bytes[off..end]
.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
)
}
let manifest = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest).join("../embedded-poc/assets/qso3_busy.wav");
let audio = load_wav_i16(&path).expect("load qso3_busy.wav");
for rep in 0..3 {
let t0 = std::time::Instant::now();
let results = DecodeRequest::<Ft8>::new(&audio, 100.0, 3000.0, 0.8, 200)
.strictness(DecodeStrictness::Normal)
.sic_early()
.decode()
.results;
let elapsed = t0.elapsed();
println!(
"rep={rep} blind staged decode: {:?}, {} decodes",
elapsed,
results.len()
);
}
}
}