mfsk-core
Pure-Rust library for WSJT-family digital amateur-radio modes — a single crate that implements FT8, FT4, FST4, WSPR, JT9 and JT65 decode / encode / synthesis on top of a small set of shared primitives (DSP, sync correlation, LLR, LDPC / convolutional / Reed- Solomon FEC, message codecs).
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,
- 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),
- or as the core of a new protocol experiment that reuses FT8's LDPC and sync machinery for a different modulation / FEC / message recipe.
The six protocols share roughly 80 % of their signal path. In the Fortran codebase that commonality is expressed by copy-and-paste between per-mode source files; here it is expressed by traits.
The abstraction
┌─────────────────────────────────────────────────────┐
│ 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. Dispatch is
monomorphised, so the machine code is byte-identical to a hand-
written per-protocol decoder.
Adding a new protocol is a trait impl on a ZST, not a cross-cutting refactor: FST4-60A joined the crate post-hoc without changing any shared pipeline code.
[]
= { = "0.1", = ["ft8", "ft4"] }
Attribution
Every algorithm in this crate is derived from
WSJT-X (Joe Taylor K1JT and
collaborators). Source files cite the corresponding upstream
lib/ft8/*, lib/ft4/*, lib/fst4/*, lib/wsprd/*, lib/jt65_*,
lib/jt9_*, lib/packjt.f90, etc. that they port from. This is a
Rust re-implementation aimed at broadening the set of platforms
(browser / WASM, Android, embedded) that can host the decoders —
not a replacement for WSJT-X itself, which remains the reference
implementation.
License matches upstream: GPL-3.0-or-later.
Protocols
| Protocol | Slot | FEC | Message | Sync | Feature |
|---|---|---|---|---|---|
| FT8 | 15 s | LDPC(174, 91) + CRC-14 | 77 bit | 3 × Costas-7 | ft8 |
| FT4 | 7.5 s | LDPC(174, 91) + CRC-14 | 77 bit | 4 × Costas-4 | ft4 |
| FST4-60A | 60 s | LDPC(240, 101) + CRC-24 | 77 bit | 5 × Costas-8 | fst4 |
| WSPR | 120 s | Convolutional r=½ K=32 + Fano | 50 bit | Per-symbol LSB (npr3) | wspr |
| JT9 | 60 s | Convolutional r=½ K=32 + Fano | 72 bit | 16 distributed slots | jt9 |
| JT65 | 60 s | Reed-Solomon(63, 12) GF(2⁶) | 72 bit | 63 distributed slots | jt65 |
Modules
mfsk_core::core— protocol traits, DSP (resample / downsample / GFSK / subtract), sync, LLR, equaliser, pipeline driver.mfsk_core::fec—Ldpc174_91/Ldpc240_101/ConvFano/ConvFano232/Rs63_12.mfsk_core::msg— 77-bit (Wsjt77Message), 72-bit (Jt72Codec) and 50-bit (Wspr50Message) message codecs; callsign hash table.mfsk_core::{ft8, ft4, fst4, wspr, jt9, jt65}— per-protocol ZSTs, decoders and synthesisers (each feature-gated).
Features
| Feature | Default | What it enables |
|---|---|---|
ft8 |
✓ | FT8 decode / synth |
ft4 |
✓ | FT4 decode / synth |
fst4 |
FST4-60A decode / synth | |
wspr |
WSPR decode / synth | |
jt9 |
JT9 decode / synth | |
jt65 |
JT65 decode / synth (+ erasure-aware RS) | |
full |
Aggregate of all six protocols | |
parallel |
✓ | Rayon-parallel candidate processing |
osd-deep |
OSD-3 fallback on AP decodes (extra CPU) | |
eq-fallback |
Non-EQ fallback inside EqMode::Adaptive |
Quick example
use ;
use ;
// 1. Synthesise an FT8 frame and pad it into a 15-second slot.
let msg77 = pack77.unwrap;
let tones = message_to_tones;
let frame = tones_to_i16;
let mut audio = vec!; // 15 s @ 12 kHz
let start = as usize;
for in frame.iter.enumerate
// 2. Decode it back.
for r in decode_frame
Each protocol module documents its top-level entry points and carries its own Quick example:
mfsk_core::ft8—decode_frame+decode_sniper_ap(narrow-band "sniper" mode)mfsk_core::ft4—decode_framemfsk_core::fst4— FST4-60Adecode_framemfsk_core::wspr—decode::decode_scan_defaultmfsk_core::jt9—decode_scan_defaultmfsk_core::jt65—decode_scan_default+decode_at_with_erasures(for low SNR)
C / C++ / Kotlin
The mfsk-ffi sibling crate in this repository builds a
libmfsk.{so,a,dylib} + mfsk.h (via cbindgen) that exposes the
same decoder and synthesiser surface through an opaque-handle C ABI.
It is not published to crates.io — consumers clone this repo and run:
cargo build -p mfsk-ffi --release
See mfsk-ffi/examples/cpp_smoke/ for an end-to-end driver test
(including multi-threaded usage) and mfsk-ffi/examples/kotlin_jni/
for an Android/JNI skeleton.
Status
0.1.x — API is deliberately not frozen. Breaking changes follow
cargo-style minor bumps (0.1 → 0.2). Algorithm correctness is
covered by ~150 tests across the workspace, including end-to-end
synth → decode roundtrips for every protocol.