Expand description
§mfsk-core
Pure-Rust library for WSJT-family digital amateur-radio modes: FT8, FT4, FST4, WSPR, JT9, JT65 and Q65-30A. Decode, encode, and synthesis in a single crate.
§Why this exists
WSJT-X is the reference implementation of these modes and will stay that way — it is battle-tested on the desktop, heavily optimised, and the source of truth for every protocol constant you will find in this crate. But it is also a mixed Fortran / C / Qt application built around a specific desktop workflow. That makes it a poor fit whenever you want to run the decoders somewhere else:
- in a browser as a WASM PWA (the original driver for this library — a waterfall + sniper-mode decoder that runs in Chrome / Safari without an install step),
- on Android or iOS for portable operation, where linking a Fortran runtime is a non-starter,
- in a headless Rust application (skimmer, monitoring station, remote SDR front end) that wants async I/O and safe memory handling,
- or as the core of a new protocol experiment that reuses FT8’s LDPC and sync machinery for a different modulation / FEC / message recipe.
Each of the seven protocols here shares roughly 80 % of its signal path with at least one sibling: 8-GFSK / FSK demodulation, soft- decision LDPC / convolutional / Reed-Solomon / QRA decoding, 77- / 72- / 50-bit WSJT message packing, spectrogram-based sync search. In the Fortran codebase that commonality is expressed by copy-and-paste between per-mode source files; here it is expressed by a small set of traits.
§The abstraction
A protocol in this crate is a zero-sized type (e.g. Ft8)
that implements four traits:
ModulationParams— tone count, symbol rate, Gray map, GFSK shaping constants.FrameLayout— total symbols, sync / data symbol counts, slot length, sync-block layout.Protocol— the top-level trait, tying the above together with two associated types:Protocol::Fec(implementingFecCodec) andProtocol::Msg(implementingMessageCodec).
Because everything is expressed as const associated items + ZSTs,
the generic pipeline code — coarse_sync::<P>, decode_frame::<P>,
the LDPC inner loop — is monomorphised per protocol. LLVM sees
a fully specialised function for each P, inlines the constants,
and autovectorises the hot loops. The generated machine code is
byte-identical to a hand-written per-protocol decoder; the only
thing the abstraction costs is longer compile times.
This pays off most clearly when you add a new protocol. FST4-60A joined the library post-hoc without touching any of the shared sync / DSP / FEC code — the entire implementation is the trait impl block on a single ZST plus a ~50-element Costas pattern table. Similarly, swapping an LDPC codec between two LDPC modes or exposing the same 77-bit message layer to FT8, FT4, and FST4 are one-line changes, not cross-cutting refactors.
§Why Rust
- Safety: bit-level FEC routines (LDPC belief propagation, Karn’s Berlekamp-Massey + Forney for RS, Fano sequential decoding) are textbook index-heavy code. Writing them in safe Rust eliminates an entire class of memory-corruption bugs that Fortran / C ports have historically hidden.
- Generics + trait bounds: describing a protocol family as data + traits is natural. The equivalent in C++ would be template metaprogramming with subtler error messages; in Fortran, it simply isn’t on offer.
- Targets: the same code compiles to
wasm32-unknown-unknown(WASM SIMD 128-bit viarustfft), to Androidarm64-v8avia the NDK (NEON SIMD), and to anyx86_64-*-unknownhost for servers — from a single source tree. - Ecosystem:
rustfft,num-complex,crc,rayonare plug-and-play, so the crate’s dependency graph is small and reviewable.
§Relationship to WSJT-X
Every algorithm in this crate is derived from WSJT-X (Joe Taylor
K1JT et al.). Source files cite the corresponding upstream file
they port (lib/ft8/…, lib/ft4/…, lib/fst4/…, lib/wsprd/…,
lib/jt65_*.f90, lib/jt9_*.f90, lib/packjt.f90, etc.).
Licensed GPL-3.0-or-later, matching upstream.
mfsk-core is not a replacement for WSJT-X. The goal is to
broaden the set of platforms and applications that can host WSJT
decoding — WSJT-X on the desktop, mfsk-core everywhere else.
§Module layout
core— protocol traits, DSP (resample / downsample / GFSK / subtract), sync, LLR, equaliser, pipeline driver.fec— LDPC(174, 91), LDPC(240, 101), convolutional r=½ K=32 Fano, Reed-Solomon(63, 12) over GF(2⁶), and the QRA(15, 65) over GF(2⁶) Q-ary RA codec used by Q65 (belief-propagation decoder via Walsh-Hadamard messages).msg— 77-bit WSJT, 72-bit JT, 50-bit WSPR and Q65 message codecs + callsign hash table.ft8/ft4/fst4/wspr/jt9/jt65/q65— per-protocol ZSTs, decoders and synthesisers. Each is gated behind a feature of the same name.
§Feature flags
| Feature | Default? | What it enables |
|---|---|---|
ft8 | yes | FT8 (15 s, 8-GFSK, LDPC(174,91)) |
ft4 | yes | FT4 (7.5 s, 4-GFSK, LDPC(174,91)) |
fst4 | FST4-60A (60 s, 4-GFSK, LDPC(240,101)) | |
wspr | WSPR (120 s, 4-FSK, conv r=½ K=32 + Fano) | |
jt9 | JT9 (60 s, 9-FSK, conv r=½ K=32 + Fano) | |
jt65 | JT65 (60 s, 65-FSK, RS(63,12)) | |
q65 | Q65-30A + Q65-60A‥E (65-FSK, QRA(15,65) GF(64)) | |
full | Aggregate of all seven protocols | |
parallel | yes | Rayon-parallel candidate processing |
osd-deep | OSD-3 fallback on AP decodes (extra CPU) | |
eq-fallback | Non-EQ fallback inside EqMode::Adaptive |
§Runtime registry
PROTOCOLS is a &'static [ProtocolMeta] listing every
Protocol impl wired into the current build. Each entry carries
the protocol’s id, display name, and every constant the trait
surface exposes (modulation / frame / FEC / message). Use it
when a UI layer or FFI bridge needs to enumerate “what does this
build support?” without hardcoding its own list:
for p in PROTOCOLS {
println!("{}: {} tones × {} bits, {} s slot",
p.name, p.ntones, p.bits_per_symbol, p.t_slot_s);
}by_id / by_name / for_protocol_id cover the common
lookup patterns. All six Q65 sub-modes (Q65-30A, Q65-60A‥E)
appear as distinct registry entries because their NSPS and tone
spacing differ; they share ProtocolId::Q65 because the FFI
protocol tag is family-level.
§Trait surface verification
tests/protocol_invariants.rs runs a single generic
assert_protocol_invariants::<P: Protocol> over every wired ZST
(FT8 / FT4 / FST4 / WSPR / JT9 / JT65 plus all six Q65 sub-modes
— 11 in total). It pins 17 trait-level invariants:
2^BITS_PER_SYMBOL ≤ NTONES, SYMBOL_DT × 12000 == NSPS,
N_SYMBOLS == N_DATA + N_SYNC, sync-mode self-consistency,
FecCodec::K ≥ MessageCodec::PAYLOAD_BITS, and so on. Adding a
new Protocol impl is a one-line registry edit + a one-line
test invocation; the same generic body proves the new ZST’s
constants are internally consistent without any per-protocol
glue. Drift between trait doc and implementation is caught
mechanically — the work that landed Q65 surfaced one such
discrepancy in GRAY_MAP and fixed it in the same pass.
§Library stack
┌─────────────────────────────────────────────────────┐
│ ft8 ft4 fst4 wspr jt9 jt65 … │ per-protocol ZSTs
│ (each implements Protocol + FrameLayout) │ (feature-gated)
└─────────────┬─────────────────┬─────────────────────┘
│ │
┌────────▼────────┐ ┌─────▼──────┐
│ msg │ │ fec │ shared codecs
│ Wsjt77 · Jt72 │ │ LDPC · RS │ behind traits
│ Wspr50 · Hash │ │ ConvFano │
└────────┬────────┘ └─────┬──────┘
│ │
┌───▼─────────────────▼───┐
│ core │ Protocol trait, DSP
│ sync · llr · equalize · │ (resample / GFSK /
│ pipeline · tx · dsp │ downsample / subtract)
└─────────────────────────┘Each protocol declares its slot length, tone count, Gray map,
Costas / sync pattern, FEC codec and message codec at compile time
via the Protocol trait. The generic code in core —
coarse sync, fine sync, LLR computation, LDPC / RS / convolutional
decode, GFSK synthesis — works for any type that satisfies the
trait.
§Quick start
# Cargo.toml
[dependencies]
mfsk-core = { version = "0.1", features = ["ft8", "ft4"] }Round-trip a synthesised FT8 frame through the decoder:
use mfsk_core::ft8::{
decode::{decode_frame, DecodeDepth},
wave_gen::{message_to_tones, tones_to_i16},
};
use mfsk_core::msg::wsjt77::{pack77, unpack77};
// 1. Pack a standard FT8 message and synthesise 12 kHz i16 PCM.
// The synth produces just the transmitted frame (~12.64 s);
// pad to the full 15 s slot with the signal starting at 0.5 s.
let msg77 = pack77("CQ", "JA1ABC", "PM95").expect("pack");
let tones = message_to_tones(&msg77);
let frame = tones_to_i16(&tones, /* freq */ 1500.0, /* amp */ 20_000);
let mut audio = vec![0i16; 180_000]; // 15 s @ 12 kHz
let start = (0.5 * 12_000.0) as usize;
for (i, &s) in frame.iter().enumerate() {
if start + i < audio.len() { audio[start + i] = s; }
}
// 2. Decode it back across the full FT8 band.
let results = decode_frame(
&audio,
/* freq_min */ 100.0,
/* freq_max */ 3_000.0,
/* sync_min */ 1.0,
/* freq_hint */ None,
DecodeDepth::BpAllOsd,
/* max_cand */ 50,
);
assert!(!results.is_empty(), "roundtrip must decode");
let text = unpack77(&results[0].message77).expect("unpack");
assert_eq!(text, "CQ JA1ABC PM95");Re-exports§
pub use crate::core::DecodeContext;pub use crate::core::FecCodec;pub use crate::core::FecOpts;pub use crate::core::FecResult;pub use crate::core::FrameLayout;pub use crate::core::MessageCodec;pub use crate::core::MessageFields;pub use crate::core::ModulationParams;pub use crate::core::Protocol;pub use crate::core::ProtocolId;pub use crate::core::SyncBlock;pub use crate::core::SyncMode;pub use crate::registry::PROTOCOLS;pub use crate::registry::ProtocolMeta;pub use crate::registry::by_id;pub use crate::registry::by_name;pub use crate::registry::for_protocol_id;pub use crate::fst4::Fst4s60;pub use crate::ft4::Ft4;pub use crate::ft8::Ft8;pub use crate::jt9::Jt9;pub use crate::jt65::Jt65;pub use crate::q65::Q65a30;pub use crate::wspr::Wspr;
Modules§
- core
core— protocol traits, DSP, sync, pipeline- fec
fec— forward-error-correction codecs- fst4
fst4— FST4-60A decoder and synthesiser- ft4
ft4— FT4 decoder and synthesiser- ft8
ft8— FT8 decoder and synthesiser- jt9
jt9— JT9 decoder and synthesiser- jt65
jt65— JT65 decoder and synthesiser- msg
msg— message-layer codecs and callsign hash table- q65
q65— Q65 decoder and synthesiser- registry
- Compile-time registry of every protocol mfsk-core builds with.
- wspr
wspr— WSPR decoder and synthesiser