mfsk-core
What is this?
mfsk-core is a pure-Rust library for WSJT-family digital amateur-radio
modes — a single crate that implements FT8, FT4, FST4, WSPR, JT9, JT65
and Q65-30A decode / encode / synthesis on top of a small set of shared
primitives (DSP, sync correlation, LLR, LDPC / convolutional /
Reed-Solomon / QRA FEC, message codecs). It runs anywhere Rust runs:
desktop, WASM in the browser, Android/iOS, and no_std embedded MCUs.
The embedded-poc/m5stack-s3-app
crate shipped with the source tree is a working M5StickS3 FT8
controller running the same library on Xtensa LX7 — LCD UI, BLE CI-V to
IC-705, acoustic mic capture, QSO FSM. The image above is one of its
decode slots.
Every algorithm is a Rust re-implementation of WSJT-X (Joe Taylor K1JT and collaborators), which remains the reference implementation — see Attribution below.
Why mfsk-core
- At or near WSJT-X sensitivity parity on every mode but one.
FST4 is within 0.1-0.6 dB of WSJT-X's published thresholds across
all five sub-modes; FT4's AWGN gap is ~0.3 dB; MSK144 matches a real
WSJT-X
jt9build on 25/28 AWGN cross-check cells exactly; FT8 matches the WSJT-X golden set 8/8 and JTDX's 18/18; WSPR and JT9 are 8/8 and 7/7 on their WSJT-X reference recordings. The one exception is disclosed, not hidden: JT65 trails WSJT-X's stochasticftrsdapdecoder by ~7-8 dB at deep SNR, a gap that's root-caused but deliberately not closed (Q65 already covers the same deep-SNR niche). Full numbers, per protocol:docs/notes/BENCHMARKS.md. - Runs where WSJT-X can't. Same algorithms,
no_std-portable: a real shipping product (embedded-poc/m5stack-s3-app, pictured above) decodes real on-air FT8 in ~1.2 s post-slot on an ESP32-S3, plus WASM in the browser and Android/iOS via FFI — none of which a Fortran/C/Qt desktop application can target. - A
Protocoltrait, not per-mode copy-paste. Eight protocol families share one generic, monomorphised decode pipeline (no vtable, no dynamic dispatch on the hot path) — adding FST4-60A to the crate was a trait impl on one ZST, not a cross-cutting refactor. See Design Philosophy.
Supported 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 |
| FST4-15/30/120/300 | 15-300 s | (same LDPC(240, 101)) | 77 bit | (same sync layout) | 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 |
| Q65-30A | 30 s | QRA(15, 65) GF(2⁶) + CRC-12 | 77 bit | 22 distributed slots | q65 |
| Q65-60A‥E | 60 s | (same QRA codec) | 77 bit | (same sync layout) | q65 |
| MSK144 | 15 s | LDPC(128, 90) + CRC-13 | 77 bit | Meteor-ping burst-scan (matched filter) | msk144 |
Eight protocol families, sixteen wired Protocol-trait ZSTs in the
registry: FST4 contributes five T/R-period sub-modes (FST4-15, -30,
-60A, -120, -300) and Q65 contributes one 30-s sub-mode (Q65-30A) plus
five 60-s EME sub-modes (Q65-60A‥E) — both families share FEC, message
codec and sync layout across their sub-modes, differing only in NSPS /
tone spacing (and, for FST4-15 alone, the T/R start offset). MSK144
is the exception: its continuous-phase binary-MSK modulation and
transient-burst timing don't fit the static-slot model every other
protocol here shares, so no ZST implements Protocol for it — its own
msk144::decode::decode_slot driver bypasses engine::pipeline
entirely by design (see
docs/reference/LIBRARY.md §0.6
for why).
PROTOCOLS
exposes one entry per wired ZST (so MSK144 doesn't appear there);
uvpacket (when enabled) adds four more for its rate ladder. See
Static set of protocols for why there's no
runtime register_protocol().
Quick Start
# Cargo.toml
[]
= { = "0.8", = ["ft8", "ft4"] }
New features and fixes land on main immediately as PRs merge, but
crates.io releases are cut on a throttled cadence (see
Status) — if you want a specific fix or new mode before it
ships to crates.io, point at the git repo instead:
= { = "https://github.com/jl1nie/mfsk-core", = "main", = ["ft8", "ft4"] }
Synthesise an FT8 frame and decode it back:
use Ft8;
use ;
use DecodeRequest;
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;
let end = .min;
audio.copy_from_slice;
// 2. Decode it back.
let results = new
.decode
.results;
for r in &results
That's the whole round trip: pack a message → synthesise 12 kHz PCM → decode it back. Each protocol module documents its own top-level entry points and carries its own Quick example:
mfsk_core::ft8—DecodeRequest::<Ft8>(wide-band) +SniperRequest::<Ft8>(narrow-band "sniper" mode)mfsk_core::ft4—DecodeRequest::<Ft4>mfsk_core::fst4—DecodeRequest::<Fst4s60>(FST4-60A); other sub-modes viaDecodeRequest::<Fst4s120>etc.mfsk_core::wspr—decode::decode_scan_defaultmfsk_core::jt9—decode_scan_defaultmfsk_core::jt65—decode_scan_default+decode_at_with_erasures(for low SNR)mfsk_core::q65—DecodeRequest::<P>(wide-band scan) /SniperRequest::<P>(narrow-band, known alignment) for any wired sub-mode including the Q65-60A‥E EME variants;.ap_hint(...)for AP-hint decoding (~2 dB threshold gain when call signs are known);.fading(model, b90_ts)for the fast-fading metric (Gaussian / Lorentzian channel models) that recovers 5–8 dB on Doppler-spread channels — required for microwave EME at 5.7 / 10 / 24 GHz;.ap_list(candidates)(paired withstandard_qso_codewords) for BP-free template matching against the full WSJT-X "AP list" of standard exchanges (~3 dB threshold gain when the callsign pair is known up-front); andMultiPeriodRequest::<P>for averaged multi-slot decode (ionoscatter / weak-EME signals no single-period decode recovers). Q65's own dedicated builders — unlike FT8/FT4/FST4'smsg::decode_request::{DecodeRequest, SniperRequest}— since everyq65::rxfunction operates on&[f32]audio, not&[i16](issue #204)
Features
| Feature | Default | What it enables |
|---|---|---|
ft8 |
✓ | FT8 decode / synth |
ft4 |
✓ | FT4 decode / synth |
fst4 |
FST4-60A decode / synth (+ FST4-15/30/120/300) | |
wspr |
WSPR decode / synth | |
jt9 |
JT9 decode / synth | |
jt65 |
JT65 decode / synth (+ erasure-aware RS) | |
q65 |
Q65-30A decode / synth (QRA soft-decision) | |
uvpacket |
Applied example (experimental): NFM voice-channel packet protocol (QPSK + LDPC), reuses Ldpc240_101 |
|
full |
Aggregate of all seven WSJT protocols + uvpacket + packet-bytes | |
parallel |
✓ | Rayon-parallel candidate processing |
fft-rustfft |
✓ | Default host FFT backend (rustfft, requires std) |
fft-extern |
Pluggable FFT trait — caller binary supplies an FftPlanner impl (esp-dsp on ESP32-S3, CMSIS-DSP on RP2350, …) |
|
fixed-point |
Embedded integer pipeline: u16 spectrogram + i16 DFT + Q11i16 LLR + integer NMS BP — see docs/reference/EMBEDDED.md for the Q-format reference |
|
profile-coarse |
Always-on coarse_sync sub-stage profiling (automatically disabled on wasm32-unknown-unknown to prevent panics) |
Architecture
Every protocol runs the same DSP pipeline, differing only in the constants each stage plugs in (tone count, sync pattern, FEC codec, message layout):
Audio (i16 PCM)
│
▼
FFT spectrogram over the slot
│
▼
Candidate Search coarse frequency/time sync — Costas/sync-pattern
│ correlation across the spectrogram
▼
Synchronization fine time/frequency refinement per candidate
│
▼
Demodulation tone → LLR (soft bits), GFSK/FSK-aware
│
▼
FEC LDPC / convolutional+Fano / Reed-Solomon / QRA decode
│
▼
Decoded Message 77-/72-/50-bit unpack → callsign / grid / report
This is the same shape for FT8's LDPC(174,91) and JT65's
Reed-Solomon(63,12) — only the boxes' contents change per protocol.
See Design Philosophy for how that's expressed
in code (a Protocol trait, not per-mode copy-paste), and
docs/reference/LIBRARY.md §4
for the full data-flow diagram down to function level.
Design Philosophy
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),
- on embedded MCUs (ESP32-S3 with esp-dsp, RP2350 with CMSIS-DSP,
Cortex-M) via
no_std + alloc— the M5Stack Core2 PoC decodes 3–7 FT8 results per 15 s cycle on Xtensa LX6 with the fixed-point hot path, - or as the core of a new protocol experiment that reuses FT8's LDPC and sync machinery for a different modulation / FEC / message recipe.
Why a Protocol trait
The seven protocols share roughly 80 % of their signal path: 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 traits, split by what actually varies per protocol:
- Shared (lives in
engine, generic over anyP: Protocol): coarse sync, fine sync, LLR computation, equalisation, the decode pipeline driver, GFSK synthesis. - Protocol-specific (declared as
constassociated items + ZSTs on the protocol type): tone count, symbol rate, Gray map, GFSK shaping constants (ModulationParams); total symbols, sync/data layout, slot length (FrameLayout); which FEC codec (Protocol::Fec, e.g. LDPC vs Reed-Solomon) and which message codec (Protocol::Msg, e.g. 77-bit WSJT vs 50-bit WSPR) the protocol plugs in.
┌────────────────────────────────────────────────────────┐
│ ft8 ft4 fst4 wspr jt9 jt65 q65 │ per-protocol ZSTs
│ (each implements Protocol + FrameLayout) │ (feature-gated)
└─────────────┬─────────────────┬────────────────────────┘
│ │
┌────────▼─────────┐ ┌────▼─────────┐
│ msg │ │ fec │ shared codecs
│ Wsjt77 · Jt72 │ │ LDPC · RS │ behind traits
│ Wspr50 · Q65 │ │ ConvFano·QRA │
│ · Hash table │ │ │
└────────┬─────────┘ └────┬─────────┘
│ │
┌───▼─────────────────▼───┐
│ core │ Protocol trait, DSP
│ sync · llr · equalize · │ (resample / GFSK /
│ pipeline · tx · dsp │ downsample / subtract)
└─────────────────────────┘
Zero-cost: generic decoder, not a runtime dispatch table
Because everything above 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 on native targets, where SIMD is enabled
by default (SSE/AVX on x86_64, NEON on aarch64). The generated machine
code is byte-identical to a hand-written per-protocol decoder; the
receive path is a chain of free functions in engine::sync →
engine::llr → engine::equalize → engine::pipeline (each generic
over P: Protocol), not a Demodulator / Receiver trait object —
there is no vtable, no dynamic dispatch, on the hot path. On
wasm32-unknown-unknown this autovectorization requires an explicit
build flag — see Building for WebAssembly
below.
This pays off most clearly when adding a new protocol: it's a trait impl on a ZST, not a cross-cutting refactor. FST4-60A joined the crate post-hoc without changing any shared pipeline code — the entire implementation is the trait impl block on a single ZST plus a 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.
Building for WebAssembly
Unlike x86_64/aarch64 hosts, where SSE/AVX or NEON are enabled by
default, wasm32-unknown-unknown ships with no SIMD by default.
mfsk-core has zero hand-written SIMD (by design — it keeps the crate
no_std/portable), so every hot loop depends entirely on LLVM's
autovectorizer, which on wasm32 only emits v128 instructions when
+simd128 is explicitly enabled. This also gates rustfft (the
default FFT backend)'s own wasm-SIMD butterfly kernels. Without the
flag, both mfsk-core's DSP and the FFT backend run fully scalar.
Add this to your consuming project's .cargo/config.toml (this is
a build flag for the binary/wasm-bindgen crate that embeds mfsk-core,
not something the library itself can impose on downstream builds):
[]
= ["-C", "target-feature=+simd128"]
Measured effect (Node, --target nodejs, real decode_wav() FT8
decode calls, median of 5-7 runs per config, same input, steady-state;
methodology and a reproducible harness in
docs/notes/BENCHMARKS.md):
| WAV | without +simd128 |
with +simd128 |
speedup |
|---|---|---|---|
| sim_busy_band.wav | 194.2 ms | 156.5 ms | 19.4% |
| sim_extreme_hard.wav | 216.5 ms | 173.1 ms | 20.0% |
Browser support for wasm SIMD has been universal since ~2021 (Chrome/Firefox/Safari 16.4+); in practice there's no reason not to set this flag for a wasm build.
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, requires+simd128— see Building for WebAssembly above), to Androidarm64-v8avia the NDK (NEON SIMD), tono_std + allocembedded MCUs, 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.
Benchmarks vs. WSJT-X
Not a competitor — a different point in the design space. WSJT-X is
the reference implementation and the source of truth for every
protocol constant in this crate; mfsk-core exists to run the same
algorithms in places a Fortran/C/Qt desktop application can't reach.
| WSJT-X | mfsk-core | |
|---|---|---|
| Language | Fortran + C + Qt | Rust |
| Distribution | Desktop application | Library crate (cargo add) |
no_std / embedded |
✗ | ✓ (ESP32-S3, RP2350, Cortex-M) |
| WASM | ✗ | ✓ (wasm32-unknown-unknown, +simd128 recommended) |
| Android / iOS | ✗ | ✓ (NDK / FFI) |
| FFT backend | fixed (FFTW) | pluggable (rustfft or caller-supplied, e.g. esp-dsp/CMSIS-DSP) |
| Reference implementation | ✓ | derived from WSJT-X, cites source per file |
Headline decode numbers (full per-protocol writeup, including how each
sweep was generated and reproduced, in
docs/notes/BENCHMARKS.md):
| Protocol | Golden-WAV recall | AWGN gap vs. WSJT-X |
|---|---|---|
| FT8 | 8/8 host full-parity (WSJT-X), 18/18 (JTDX) | CCIR fading gap closed |
| FT4 | 6/6 | ~0.3 dB |
| FST4 | 1/1 (FST4-60A) | 0.10-0.60 dB across 5 sub-modes |
| WSPR | 8/8 | matches published sensitivity floor |
| JT9 | 7/7 | no measurable gap |
| JT65 | none available | ~7-8 dB (known, deprioritized — see #169) |
| Q65 | 2 real EME recordings | matches WSJT-X with AP hint; 2 sub-modes measurably beat WSJT-X's own plain decode |
| MSK144 | 3/3 (incl. exact SNR match) | 25/28 cells exact match vs. a real jt9 build |
Embedded wall-clock: M5StickS3 (Xtensa LX7, fixed-point) decodes a
real on-air busy-band FT8 slot in ~1.19 s post-SlotEnd via the
streaming pipeline (FFT overlapped with capture); M5Stack Core2
(Xtensa LX6) on the same recording: ~2.8 s. See
docs/reference/EMBEDDED.md
for the byte-level BP-scratch / spectrogram memory budget.
FAQ
How does this differ from WSJT-X? See
Benchmarks vs. WSJT-X and
Why this exists above — same algorithms, different
deployment targets (library vs desktop app, no_std embedded, WASM).
Does it support no_std? Yes — default-features = false, features = ["alloc", "ft8", "fft-extern"] (or similar) builds without std.
std is only required by the default fft-rustfft backend; embedded
targets swap in their own FFT via fft-extern. See
docs/reference/EMBEDDED.md.
Can I swap the FFT backend? Yes — enable fft-extern instead of
fft-rustfft and provide an FftPlanner impl (engine::fft); the
embedded ports use this for esp-dsp (ESP32-S3) and CMSIS-DSP (RP2350).
How do I use this on embedded hardware? Start with
docs/reference/EMBEDDED.md
(feature-flag map, FFT-extern contract, Q-format reference, full C ABI
tutorial) and, for a complete working example,
embedded-poc/m5stack-s3-app
(a shipping M5StickS3 FT8 controller).
Is the API stable? Not yet — see Status. Breaking
changes follow cargo-style minor bumps (0.x line).
License
GPL-3.0-or-later, matching upstream WSJT-X. See LICENSE.
Reference
The sections above cover getting started and the design rationale. The rest of this document is reference material: attribution, module layout, the FFI surface, contribution workflow, and the detailed per-protocol status / recall tables.
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.
Protocol registry details
Static set of protocols
PROTOCOLS is a const slice — the set of supported protocols is
fixed at compile time by Cargo features. There is no runtime
register_protocol() API by design: every wired ZST is verified by
tests/protocol_invariants.rs to satisfy the trait surface, and that
guarantee can't be extended to types unknown at compile time. UI /
FFI consumers should iterate PROTOCOLS (or filter via by_id /
by_name) at startup; if you need a new protocol, add the ZST + a
protocol_meta! line and rebuild.
Applied example: uvpacket (experimental)
The uvpacket module (--features uvpacket, off by default) is
an in-tree example showing the trait abstractions extend beyond
WSJT-X — a π/4-DQPSK packet protocol for NFM / SSB voice channels
that reuses the shared Ldpc240_101 codec. Experimental and
its public API may change — pin to an exact version. See
docs/reference/UVPACKET.md
(日本語)
for the design narrative, modulation / equaliser / framing details
and per-mode performance characterisation.
Modules
mfsk_core::engine— protocol traits, DSP (resample / downsample / GFSK / subtract), sync, LLR, equaliser, pipeline driver.mfsk_core::fec—Ldpc174_91/Ldpc240_101/ConvFano/ConvFano232/Rs63_12/qra::Q65Codec(with theqra15_65_64::QRA15_65_64_IRR_E23code instance) for Q65.mfsk_core::msg— 77-bit (Wsjt77Message), 72-bit (Jt72Codec), 50-bit (Wspr50Message) and Q65 (Q65Message, 77-bit ↔ 13-symbol packing helpers) message codecs; callsign hash table.mfsk_core::{ft8, ft4, fst4, wspr, jt9, jt65, q65}— per-protocol ZSTs, decoders and synthesisers (each feature-gated). Theq65module exposes one ZST per wired sub-mode —Q65a30for terrestrial work, plusQ65a60/Q65b60/Q65c60/Q65d60/Q65e60for EME at 6 m through 10 GHz+ — with genericsynthesize_standard_for<P>helper plus theDecodeRequest<P>/SniperRequest<P>builders that pick the right NSPS and tone spacing from the type parameter.
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.
mfsk-ffi is not published to crates.io, but every tagged release
attaches a prebuilt linux-x86_64 tarball to the GitHub Release;
other platforms/ABIs (including Android) build locally:
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. Embedded targets (ESP32-S3, RP2350,
Cortex-M) instead use the sibling mfsk-ffi-ft8 crate — see its own
prebuilt binaries below.
Contributing
PRs welcome — recent forks have shipped FT4 SIC, FT4/FST4 depth + strictness controls, and the FT8 wide-band AP path. The local-fence
- CI gates are uniform across direct commits and fork PRs:
-
Pre-commit hook:
.githooks/pre-commitrunscargo fmt --check,cargo clippy --workspace --all-targets --features full -- -D warnings, andRUSTDOCFLAGS=-D warnings cargo doc -p mfsk-core --features full --no-deps(~10–20 s on a warm cache). Enable once per clone:The hook deliberately skips the full
cargo testsuite (kept in CI to keep commits snappy); fmt / clippy / rustdoc each catch a failure mode that would otherwise trip CI after the push. -
CI gates (
.github/workflows/ci.yml): same fmt + clippy fence, pluscargo test -p mfsk-core --features full --release -- --include-ignored(slow synthetic-SNR / AP / fast-fading sweeps enabled), a 13-cell feature matrix that builds every protocol in isolation + the embeddedalloc + ft8 + fft-extern + fixed-pointpreset,cargo test+ the C++ driver formfsk-ffiandcargo testformfsk-ffi-ft8, rustdoc with-D warnings, and acargo publish --dry-runformfsk-core. -
Release: tag-driven (
v0.6.x). Pushing a tag that matches the workspace version (Cargo.toml::[workspace.package].version, inherited bymfsk-core/mfsk-ffi/mfsk-ffi-ft8alike) and is reachable frommaintriggersrelease.yml, which publishesmfsk-coreto crates.io and cuts a GitHub release with auto-generated notes. Prebuiltmfsk-ffi(linux-x86_64) andmfsk-ffi-ft8(linux-x86_64, esp32-xtensa, esp32s3-xtensa) binaries follow on the same tag.
For non-trivial changes, please open an issue first so the
WSJT-X-source-faithfulness lineage of any DSP or FEC change is
visible in review (every protocol constant in this crate cites the
upstream lib/*.f90 it ports from — drift from that lineage tends
to be the failure mode caught by the WSJT-X golden harnesses).
Architecture & ABI reference
For a deeper look at the design — trait hierarchy with worked examples, shared DSP / sync / LLR / pipeline primitives, the C ABI memory model, Kotlin/Android scaffolding — see the library reference:
- English:
docs/reference/LIBRARY.md - 日本語:
docs/reference/LIBRARY.ja.md - Benchmarks vs. WSJT-X, full per-protocol detail:
docs/notes/BENCHMARKS.md— golden-WAV recall + AWGN sensitivity sweep results for every protocol, current-state only (seeCHANGELOG.mdfor how each number got there). - Embedded targets:
English
docs/reference/EMBEDDED.md/ 日本語docs/reference/EMBEDDED.ja.md— generic-scalar architecture (one codebase for f32 host and fixed-point embedded), feature-flag map, FFT-extern contract, Goertzel per-symbol DFT (zero-scratch, 0.6.4+) with BASIS deprecation, Q-format reference, fullmfsk-ffi-ft8C ABI tutorial (streaming + ESP-IDF component layout), performance benchmark, streaming RX pipeline, binary footprint. - FST4 sensitivity benchmark setup:
English
docs/notes/FST4_BENCHMARK.md/ 日本語docs/notes/FST4_BENCHMARK.ja.md— reproducing thefst4sim-driven AWGN/fading SNR sweep (tests/fst4_sweep.rs) from a clean checkout on any machine: prerequisites, buildingfst4simfrom WSJT-X source, generating the WAV corpus, and how to avoid grid-censoring artifacts when reading off the recall crossing. - MSK144 sensitivity benchmark setup:
English
docs/notes/MSK144_BENCHMARK.md/ 日本語docs/notes/MSK144_BENCHMARK.ja.md— the self-containedtests/msk144_snr_sweep.rsregression sweep (no WSJT-X checkout needed), plus how to reproduce the one-time apples-to-apples verification against a real WSJT-Xjt9build (buildingjt9/msk144sim/Hamlib from source,msk144sim's SNR convention, and the measured baseline results). - M5StickS3 FT8 controller manual:
English
docs/reference/MANUAL_M5STICKS3.md/ 日本語docs/reference/MANUAL_M5STICKS3.ja.md— build / flash /cfg.toml/BootModecycle / UI / QSO workflow / troubleshooting.
Status
Latest published tag: v0.8.1 — API is deliberately not frozen:
breaking changes follow cargo-style minor bumps, while a new
protocol/mode addition on its own is patch-level (e.g. MSK144 shipped
as 0.7.4, not 0.8.0) — minor bumps mark more structural changes.
See CHANGELOG.md for the per-release breakdown and
docs/notes/ROADMAP.md for open follow-ups.
Release cadence: PRs merge to main continuously — CHANGELOG.md's
top section always reflects the latest unreleased state, so tracking
main directly (see Quick Start) gets you every
change immediately. Actual crates.io tags/GitHub Releases are cut on a
biweekly cadence (bundling everything merged since the last tag)
rather than after every individual change, to keep update
notifications for crates.io consumers from firing too often — an
out-of-cadence release is still fine for a security fix, a serious
correctness bug, or on explicit request.
Algorithm correctness is covered by the workspace test suite:
end-to-end synth → decode roundtrips for every protocol, real
WSJT-X-distributed reference recordings, *sim-generated AWGN
sensitivity sweeps, and AP-list / fast-fading comparisons — see
Benchmarks vs. WSJT-X above and
docs/notes/BENCHMARKS.md
for the full results. The trait surface itself is pinned by
tests/protocol_invariants.rs — a single generic <P: Protocol>
checker run across every wired ZST. Run with --features full for
full coverage; the default features (ft8, ft4) only exercise the
two default protocols.
embedded-poc/m5stack-s3-app/ and embedded-poc/m5stack-core2-app/
are the production FT8 controller crates (LCD UI + QSO FSM + WiFi-UDP
log streaming), both consuming the board-agnostic
embedded-poc/mfsk-app-shared/. embedded-poc/m5stack-s3/ is a
decoder-only compute-bench crate for S3 timing-regression tracking.
See
docs/reference/EMBEDDED.md
for the integration contract and runtime tuning knobs, and
docs/reference/MANUAL_M5STICKS3.md
for the M5StickS3 controller's build/flash/UI workflow.