mfsk_core/ft8/mod.rs
1//! # `ft8` — FT8 decoder and synthesiser
2//!
3//! FT8 is the most widely-used WSJT-family mode: 15-second slots,
4//! 8-GFSK modulation at 6.25 baud (= 160 ms / symbol), LDPC(174, 91)
5//! with CRC-14 inside a 77-bit WSJT message, and three Costas-7 sync
6//! blocks at positions 0 / 36 / 72.
7//!
8//! ## Sample rate
9//!
10//! The internal decode pipeline assumes **12 000 Hz** PCM input.
11//! For other sample rates (e.g. 44 100, 48 000 Hz), use
12//! [`resample::resample_to_12k`] to convert before calling
13//! [`decode::decode_frame`] or [`decode::decode_sniper_ap`].
14//!
15//! The WASM wrapper (`ft8-web`) accepts a `sample_rate` parameter
16//! on each decode function and handles this conversion automatically.
17//!
18//! ## Protocol trait
19//!
20//! The zero-sized [`Ft8`] type implements the generic
21//! [`crate::core::Protocol`] trait so downstream pipeline code (shared with
22//! FT4, FT2, FST4) can dispatch on `P: Protocol` at compile time.
23//!
24//! ## Quick example
25//!
26//! Decode the top-scoring message in a 15-second slot:
27//!
28//! ```no_run
29//! use mfsk_core::ft8::decode::{decode_frame, DecodeDepth};
30//! use mfsk_core::msg::wsjt77::unpack77;
31//!
32//! # let audio: Vec<i16> = vec![];
33//! // `audio` is 180_000 i16 samples at 12 kHz (15 s, slot-aligned).
34//! let results = decode_frame(
35//! &audio,
36//! /* freq_min */ 100.0,
37//! /* freq_max */ 3_000.0,
38//! /* sync_min */ 1.0,
39//! /* freq_hint */ None,
40//! DecodeDepth::BpAllOsd,
41//! /* max_cand */ 200,
42//! );
43//! for r in &results {
44//! if let Some(text) = unpack77(&r.message77) {
45//! println!("{:7.1} Hz dt={:+.2} s SNR={:+.0} dB {}",
46//! r.freq_hz, r.dt_sec, r.snr_db, text);
47//! }
48//! }
49//! ```
50
51pub mod decode;
52pub mod downsample;
53pub mod equalizer;
54pub mod hash_table;
55pub mod ldpc;
56pub mod llr;
57pub mod message;
58pub mod params;
59pub mod resample;
60pub mod subtract;
61pub mod sync;
62pub mod wave_gen;
63
64use crate::core::{FrameLayout, ModulationParams, Protocol, ProtocolId, SyncBlock, SyncMode};
65use crate::fec::Ldpc174_91;
66use crate::msg::Wsjt77Message;
67
68/// FT8 protocol marker: 8-GFSK, 79 symbols over a 15 s slot, 6.25 Hz tone
69/// spacing, three 7-symbol Costas arrays, LDPC(174,91) FEC, WSJT 77-bit
70/// message payload. Carries no data — used as a type-level switch.
71#[derive(Copy, Clone, Debug, Default)]
72pub struct Ft8;
73
74impl ModulationParams for Ft8 {
75 const NTONES: u32 = params::NTONES as u32;
76 const BITS_PER_SYMBOL: u32 = 3;
77 const NSPS: u32 = params::NSPS as u32;
78 const SYMBOL_DT: f32 = params::SYMBOL_DT;
79 const TONE_SPACING_HZ: f32 = 6.25;
80 const GRAY_MAP: &'static [u8] = &FT8_GRAY_MAP;
81 const GFSK_BT: f32 = 2.0;
82 const GFSK_HMOD: f32 = 1.0;
83 const NFFT_PER_SYMBOL_FACTOR: u32 = 2; // NFFT1 = 2 × NSPS = 3840
84 const NSTEP_PER_SYMBOL: u32 = 4; // quarter-symbol coarse-sync step
85 const NDOWN: u32 = 60; // 12 000 / 60 = 200 Hz baseband
86}
87
88impl FrameLayout for Ft8 {
89 const N_DATA: u32 = params::ND as u32;
90 const N_SYNC: u32 = params::NS as u32;
91 const N_SYMBOLS: u32 = params::NN as u32;
92 const N_RAMP: u32 = 0; // ramp is internal to gfsk::synth
93 const SYNC_MODE: SyncMode = SyncMode::Block(&FT8_SYNC_BLOCKS);
94 const T_SLOT_S: f32 = 15.0;
95 const TX_START_OFFSET_S: f32 = 0.5;
96}
97
98impl Protocol for Ft8 {
99 type Fec = Ldpc174_91;
100 type Msg = Wsjt77Message;
101 const ID: ProtocolId = ProtocolId::Ft8;
102}
103
104// `params::GRAYMAP` / `params::COSTAS` are `[usize; _]` for historical reasons,
105// but `ModulationParams::GRAY_MAP` etc. require `&'static [u8]`. Narrow them
106// here at compile time.
107const FT8_GRAY_MAP: [u8; 8] = {
108 let mut out = [0u8; 8];
109 let mut i = 0;
110 while i < 8 {
111 out[i] = params::GRAYMAP[i] as u8;
112 i += 1;
113 }
114 out
115};
116
117const FT8_COSTAS: [u8; 7] = {
118 let mut out = [0u8; 7];
119 let mut i = 0;
120 while i < 7 {
121 out[i] = params::COSTAS[i] as u8;
122 i += 1;
123 }
124 out
125};
126
127/// FT8 has three identical Costas arrays at symbols 0 / 36 / 72.
128const FT8_SYNC_BLOCKS: [SyncBlock; 3] = [
129 SyncBlock {
130 start_symbol: 0,
131 pattern: &FT8_COSTAS,
132 },
133 SyncBlock {
134 start_symbol: 36,
135 pattern: &FT8_COSTAS,
136 },
137 SyncBlock {
138 start_symbol: 72,
139 pattern: &FT8_COSTAS,
140 },
141];