mfsk-core 0.1.0

Pure-Rust library for WSJT-family digital amateur-radio modes (FT8/FT4/FST4/WSPR/JT9/JT65): protocol traits, DSP, FEC codecs, message codecs, decoders and synthesisers — unified behind a zero-cost generic abstraction.
Documentation

mfsk-core

CI crates.io docs.rs License

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.

[dependencies]
mfsk-core = { version = "0.1", features = ["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::fecLdpc174_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 mfsk_core::ft8::{
    decode::{decode_frame, DecodeDepth},
    wave_gen::{message_to_tones, tones_to_i16},
};
use mfsk_core::msg::wsjt77::{pack77, unpack77};

// 1. Synthesise an FT8 frame and pad it into a 15-second slot.
let msg77 = pack77("CQ", "JA1ABC", "PM95").unwrap();
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.
for r in decode_frame(&audio, 100.0, 3_000.0, 1.0, None, DecodeDepth::BpAllOsd, 50) {
    if let Some(text) = unpack77(&r.message77) {
        println!("{:7.1} Hz  dt={:+.2} s  SNR={:+.0} dB  {}",
                 r.freq_hz, r.dt_sec, r.snr_db, text);
    }
}

Each protocol module documents its top-level entry points and carries its own Quick example:

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.