use crate::engine::dsp::downsample::DownsampleCfg;
use crate::engine::pipeline;
pub use crate::engine::pipeline::{DecodeDepth, DecodeResult, DecodeStrictness, FftCache};
pub use crate::msg::ApHint;
use crate::msg::decode_request::{DecodeOutcome, DecodeRequest, FrameDecodable, SniperRequest};
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 = 16;
const REFINE_STEPS: i32 = 40;
fn dedup_known(raw: Vec<DecodeResult>, known: &[DecodeResult]) -> Vec<DecodeResult> {
raw.into_iter()
.filter(|r| !known.iter().any(|k| k.info == r.info))
.collect()
}
macro_rules! impl_frame_decodable {
($proto:ty, $cfg:expr) => {
impl pipeline::GenericPipelineProtocol for $proto {}
impl FrameDecodable for $proto {
type DecodeResult = DecodeResult;
fn __single_pass(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
let (raw, fft_cache) = pipeline::decode_frame::<$proto>(
req.audio,
&$cfg,
req.freq_min,
req.freq_max,
req.sync_min,
req.freq_hint,
req.depth,
req.max_cand,
req.strictness,
req.eq_mode,
SYNC_Q_MIN,
);
DecodeOutcome {
results: dedup_known(raw, req.known),
fft_cache,
}
}
fn __sniper(req: &SniperRequest<'_, Self>) -> DecodeOutcome<Self> {
let results = crate::msg::pipeline_ap::decode_sniper_ap::<$proto>(
req.audio,
&$cfg,
req.target_freq,
250.0,
req.sync_min,
req.depth,
req.max_cand,
req.strictness,
req.eq_mode,
REFINE_STEPS,
SYNC_Q_MIN / 2,
req.ap_hint,
);
let fft_cache = FftCache(crate::engine::dsp::downsample::build_fft_cache(
req.audio, &$cfg,
));
DecodeOutcome { results, fft_cache }
}
}
};
}
impl_frame_decodable!(super::Fst4s15, FST4_15_DOWNSAMPLE);
impl_frame_decodable!(super::Fst4s30, FST4_30_DOWNSAMPLE);
impl_frame_decodable!(super::Fst4s60, FST4_60A_DOWNSAMPLE);
impl_frame_decodable!(super::Fst4s120, FST4_120_DOWNSAMPLE);
impl_frame_decodable!(super::Fst4s300, FST4_300_DOWNSAMPLE);
#[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 = DecodeRequest::<crate::fst4::Fst4s60>::new(&slot, 1000.0, 2000.0, 0.8, 20)
.decode()
.results;
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>(
gfsk: &crate::engine::dsp::gfsk::GfskCfg,
freq_min: f32,
freq_max: f32,
) where
P: crate::engine::Protocol
+ crate::engine::FrameLayout
+ FrameDecodable<DecodeResult = DecodeResult>,
{
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 = DecodeRequest::<P>::new(&slot, freq_min, freq_max, 0.8, 20)
.decode()
.results;
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>(
&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>(
&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>(
&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>(
&super::super::encode::FST4_300_GFSK,
1000.0,
2000.0,
);
}
#[test]
fn decode_request_accepts_all_param_combos() {
let empty = vec![0i16; 12 * 60 * 1000]; for osd in [false, true] {
let _ = DecodeRequest::<crate::fst4::Fst4s60>::new(&empty, 100.0, 3000.0, 0.8, 5)
.osd(osd)
.decode();
let _ = DecodeRequest::<crate::fst4::Fst4s60>::sniper(&empty, 1500.0, 5)
.osd(osd)
.decode();
}
}
}