use super::Ft4;
use crate::engine::dsp::downsample::DownsampleCfg;
use crate::engine::dsp::subtract::SubtractCfg;
use crate::engine::pipeline;
use crate::msg::pipeline_ap;
pub use crate::engine::pipeline::{DecodeDepth, DecodeResult, DecodeStrictness, FftCache};
pub use crate::msg::ApHint;
use crate::msg::decode_request::{
DecodeOutcome, DecodeRequest, FrameDecodable, SniperRequest, SupportsSicRounds,
};
pub const FT4_DOWNSAMPLE: DownsampleCfg = DownsampleCfg {
input_rate: 12_000,
fft1_size: 92_160,
fft2_size: 5_120,
tone_spacing_hz: 20.833,
leading_pad_tones: 1.5,
trailing_pad_tones: 1.5,
ntones: 4,
edge_taper_bins: 101,
};
pub const FT4_SUBTRACT: SubtractCfg = SubtractCfg {
sample_rate: 12_000.0,
tone_spacing_hz: 20.833,
samples_per_symbol: 576,
base_offset_s: 0.5,
gfsk: Some(crate::engine::dsp::subtract::GfskParams {
bt: 1.0,
hmod: 1.0,
ramp_samples: 576 / 8,
}),
};
const REFINE_STEPS: i32 = 32;
const SYNC_Q_MIN: u32 = 8;
impl pipeline::GenericPipelineProtocol for Ft4 {
fn snr_db(ctx: pipeline::SnrCtx<'_>) -> f32 {
pipeline::ft4_snr_db(ctx.cand_score)
}
}
impl FrameDecodable for Ft4 {
type DecodeResult = DecodeResult;
fn __single_pass(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
let filtered_cb = pipeline::known_filtered_on_result(req.known, req.on_result);
let on_result: Option<&(dyn Fn(&DecodeResult) + Sync)> = filtered_cb
.as_ref()
.map(|f| f as &(dyn Fn(&DecodeResult) + Sync));
let (raw, fft_cache) = pipeline::decode_frame::<Ft4>(
req.audio,
&FT4_DOWNSAMPLE,
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,
req.fft_cache.as_ref().map(FftCache::as_slice),
on_result,
);
DecodeOutcome {
results: pipeline::dedup_known(raw, req.known),
fft_cache,
}
}
fn __sniper(req: &SniperRequest<'_, Self>) -> DecodeOutcome<Self> {
let max_cand = req.max_cand.min(15);
let results = pipeline_ap::decode_sniper_ap::<Ft4>(
req.audio,
&FT4_DOWNSAMPLE,
req.target_freq,
250.0,
req.sync_min,
req.depth,
max_cand,
req.strictness,
req.eq_mode,
REFINE_STEPS,
SYNC_Q_MIN / 2,
req.ap_hint,
req.on_result,
);
let fft_cache = FftCache(crate::engine::dsp::downsample::build_fft_cache(
req.audio,
&FT4_DOWNSAMPLE,
));
DecodeOutcome { results, fft_cache }
}
}
impl SupportsSicRounds for Ft4 {
fn __flat_sic(req: &DecodeRequest<'_, Self>) -> DecodeOutcome<Self> {
let filtered_cb = pipeline::known_filtered_on_result(req.known, req.on_result);
let on_result: Option<&(dyn Fn(&DecodeResult) + Sync)> = filtered_cb
.as_ref()
.map(|f| f as &(dyn Fn(&DecodeResult) + Sync));
let raw = pipeline::decode_frame_subtract::<Ft4>(
req.audio,
&FT4_DOWNSAMPLE,
&FT4_SUBTRACT,
req.freq_min,
req.freq_max,
req.sync_min,
req.freq_hint,
req.depth,
req.max_cand,
req.strictness,
req.sic_rounds,
SYNC_Q_MIN,
700,
false,
1.0,
req.fft_cache.as_ref().map(FftCache::as_slice),
on_result,
);
let fft_cache = FftCache(crate::engine::dsp::downsample::build_fft_cache(
req.audio,
&FT4_DOWNSAMPLE,
));
DecodeOutcome {
results: pipeline::dedup_known(raw, req.known),
fft_cache,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::msg::decode_request::DecodeRequest;
#[test]
fn decode_request_accepts_all_param_combos() {
let empty = vec![0i16; 12 * 7500]; for osd in [false, true] {
let _ = DecodeRequest::<Ft4>::new(&empty, 100.0, 3000.0, 1.0, 5)
.osd(osd)
.decode();
let _ = DecodeRequest::<Ft4>::new(&empty, 100.0, 3000.0, 1.0, 5)
.osd(osd)
.sic_rounds(3)
.decode();
let _ = DecodeRequest::<Ft4>::sniper(&empty, 1500.0, 5)
.osd(osd)
.decode();
}
}
#[test]
fn snr_db_dispatches_to_ft4_formula() {
let cs: [num_complex::Complex<f32>; 0] = [];
let itone: [u8; 0] = [];
let fft_cache: [num_complex::Complex<f32>; 0] = [];
for &cand_score in &[0.5f32, 1.0, 1.5, 3.0, 10.0] {
let via_trait = <Ft4 as pipeline::GenericPipelineProtocol>::snr_db(pipeline::SnrCtx {
cs: &cs,
itone: &itone,
cd0: &[],
ds_rate_hz: 0.0,
cand_score,
cand_freq_hz: 1000.0,
fft_cache: &fft_cache,
ds_cfg: &FT4_DOWNSAMPLE,
refined_freq_hz: 1000.0,
i_start: 0,
});
assert_eq!(via_trait, pipeline::ft4_snr_db(cand_score));
}
}
}