use super::Fst4s60;
use crate::core::Protocol;
use crate::core::dsp::downsample::DownsampleCfg;
use crate::core::equalize::EqMode;
use crate::core::pipeline::{self, FftCache};
use crate::core::pipeline::DecodeStrictness;
pub use crate::core::pipeline::{DecodeDepth, DecodeResult};
pub const FST4_15_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 180_000,
fft2_size: 10_000,
tone_spacing_hz: 12_000.0 / 720.0,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
pub const FST4_30_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 362_880,
fft2_size: 8_640,
tone_spacing_hz: 12_000.0 / 1_680.0,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
pub const FST4_60A_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 746_496,
fft2_size: 6_912,
tone_spacing_hz: 12_000.0 / 3_888.0,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
pub const FST4_120_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 1_443_200,
fft2_size: 7_040,
tone_spacing_hz: 12_000.0 / 8_200.0,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
pub const FST4_300_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 4_194_304,
fft2_size: 8_192,
tone_spacing_hz: 12_000.0 / 21_504.0,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
const SYNC_Q_MIN: u32 = 10;
const REFINE_STEPS: i32 = 40;
pub fn decode_frame_for<P: Protocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_frame_with_options_for::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
None,
DecodeDepth::BpAllOsd,
max_cand,
)
}
pub fn decode_frame_with_options_for<P: Protocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> Vec<DecodeResult> {
pipeline::decode_frame::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
DecodeStrictness::Normal,
EqMode::Off,
REFINE_STEPS,
SYNC_Q_MIN,
)
.0
}
pub fn decode_frame_with_cache_for<P: Protocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
) -> (Vec<DecodeResult>, FftCache) {
decode_frame_with_cache_and_options_for::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
None,
DecodeDepth::BpAllOsd,
max_cand,
)
}
pub fn decode_frame_with_cache_and_options_for<P: Protocol>(
audio: &[i16],
cfg: &DownsampleCfg,
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> (Vec<DecodeResult>, FftCache) {
pipeline::decode_frame::<P>(
audio,
cfg,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
DecodeStrictness::Normal,
EqMode::Off,
REFINE_STEPS,
SYNC_Q_MIN,
)
}
pub fn decode_frame(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_frame_for::<Fst4s60>(
audio,
&FST4_60A_DOWNSAMPLE,
freq_min,
freq_max,
sync_min,
max_cand,
)
}
pub fn decode_frame_with_options(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> Vec<DecodeResult> {
decode_frame_with_options_for::<Fst4s60>(
audio,
&FST4_60A_DOWNSAMPLE,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
)
}
pub fn decode_frame_with_cache(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
max_cand: usize,
) -> (Vec<DecodeResult>, FftCache) {
decode_frame_with_cache_for::<Fst4s60>(
audio,
&FST4_60A_DOWNSAMPLE,
freq_min,
freq_max,
sync_min,
max_cand,
)
}
pub fn decode_frame_with_cache_and_options(
audio: &[i16],
freq_min: f32,
freq_max: f32,
sync_min: f32,
freq_hint: Option<f32>,
depth: DecodeDepth,
max_cand: usize,
) -> (Vec<DecodeResult>, FftCache) {
decode_frame_with_cache_and_options_for::<Fst4s60>(
audio,
&FST4_60A_DOWNSAMPLE,
freq_min,
freq_max,
sync_min,
freq_hint,
depth,
max_cand,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn synth_decode_roundtrip_cq_ja1abc() {
if std::env::var("RUN_FST4_ROUNDTRIP").is_err() {
eprintln!("skipping FST4 roundtrip (set RUN_FST4_ROUNDTRIP=1 to enable)");
return;
}
use super::super::encode::{message_to_tones, tones_to_i16};
use crate::msg::wsjt77::{pack77, unpack77};
let msg77 = pack77("CQ", "JA1ABC", "PM95").expect("pack77");
let tones = message_to_tones(&msg77);
let audio = tones_to_i16(&tones, 1500.0, 10_000);
let mut slot = vec![0i16; 60 * 12_000];
let offset = 12_000;
let copy_len = audio.len().min(slot.len() - offset);
slot[offset..offset + copy_len].copy_from_slice(&audio[..copy_len]);
let results = decode_frame(&slot, 1000.0, 2000.0, 0.8, 20);
assert!(
!results.is_empty(),
"expected at least one decode from clean synth, got none"
);
let texts: Vec<String> = results
.iter()
.filter_map(|r| {
let msg77: &[u8; 77] = r.message77().try_into().ok()?;
unpack77(msg77)
})
.collect();
assert!(
texts
.iter()
.any(|t| t.contains("JA1ABC") && t.contains("PM95")),
"expected to recover 'JA1ABC PM95', got {:?}",
texts
);
}
fn synth_roundtrip_for<P: crate::core::Protocol + crate::core::FrameLayout>(
cfg: &DownsampleCfg,
gfsk: &crate::core::dsp::gfsk::GfskCfg,
freq_min: f32,
freq_max: f32,
) {
use super::super::encode::{message_to_tones, tones_to_i16_with_gfsk};
use crate::msg::wsjt77::{pack77, unpack77};
let msg77 = pack77("CQ", "JA1ABC", "PM95").expect("pack77");
let tones = message_to_tones(&msg77);
let audio = tones_to_i16_with_gfsk(&tones, (freq_min + freq_max) / 2.0, 10_000, gfsk);
let slot_len = (P::T_SLOT_S * 12_000.0).round() as usize;
let mut slot = vec![0i16; slot_len];
let offset = 12_000usize;
let copy_len = audio.len().min(slot_len.saturating_sub(offset));
slot[offset..offset + copy_len].copy_from_slice(&audio[..copy_len]);
let results = decode_frame_for::<P>(&slot, cfg, freq_min, freq_max, 0.8, 20);
assert!(
!results.is_empty(),
"expected at least one decode from clean synth, got none"
);
let texts: Vec<String> = results
.iter()
.filter_map(|r| {
let msg77: &[u8; 77] = r.message77().try_into().ok()?;
unpack77(msg77)
})
.collect();
assert!(
texts
.iter()
.any(|t| t.contains("JA1ABC") && t.contains("PM95")),
"expected to recover 'JA1ABC PM95', got {:?}",
texts
);
}
#[test]
fn synth_decode_roundtrip_fst4_15() {
if std::env::var("RUN_FST4_ROUNDTRIP").is_err() {
eprintln!("skipping FST4-15 roundtrip (set RUN_FST4_ROUNDTRIP=1 to enable)");
return;
}
synth_roundtrip_for::<super::super::Fst4s15>(
&FST4_15_DOWNSAMPLE,
&super::super::encode::FST4_15_GFSK,
1000.0,
2000.0,
);
}
#[test]
fn synth_decode_roundtrip_fst4_30() {
if std::env::var("RUN_FST4_ROUNDTRIP").is_err() {
eprintln!("skipping FST4-30 roundtrip (set RUN_FST4_ROUNDTRIP=1 to enable)");
return;
}
synth_roundtrip_for::<super::super::Fst4s30>(
&FST4_30_DOWNSAMPLE,
&super::super::encode::FST4_30_GFSK,
1000.0,
2000.0,
);
}
#[test]
fn synth_decode_roundtrip_fst4_120() {
if std::env::var("RUN_FST4_ROUNDTRIP").is_err() {
eprintln!("skipping FST4-120 roundtrip (set RUN_FST4_ROUNDTRIP=1 to enable)");
return;
}
synth_roundtrip_for::<super::super::Fst4s120>(
&FST4_120_DOWNSAMPLE,
&super::super::encode::FST4_120_GFSK,
1000.0,
2000.0,
);
}
#[test]
fn synth_decode_roundtrip_fst4_300() {
if std::env::var("RUN_FST4_ROUNDTRIP").is_err() {
eprintln!("skipping FST4-300 roundtrip (set RUN_FST4_ROUNDTRIP=1 to enable)");
return;
}
synth_roundtrip_for::<super::super::Fst4s300>(
&FST4_300_DOWNSAMPLE,
&super::super::encode::FST4_300_GFSK,
1000.0,
2000.0,
);
}
#[test]
fn decode_frame_with_options_accepts_all_param_combos() {
let empty = vec![0i16; 12 * 60 * 1000]; for &depth in &[DecodeDepth::BpAll, DecodeDepth::BpAllOsd] {
let _ = decode_frame_with_options(&empty, 100.0, 3000.0, 0.8, None, depth, 5);
}
}
#[test]
fn decode_frame_with_cache_and_options_accepts_all_param_combos() {
let empty = vec![0i16; 12 * 60 * 1000]; for &depth in &[DecodeDepth::BpAll, DecodeDepth::BpAllOsd] {
let _ = decode_frame_with_cache_and_options(&empty, 100.0, 3000.0, 0.8, None, depth, 5);
}
}
}