README
¶
go-ft8
go-ft8 is a Go implementation of an FT8 codec. It decodes one 15-second FT8
slot from 12 kHz mono-signed 16-bit PCM audio, encodes supported standard FT8
messages to protocol symbols, and exposes both a strict decoder path and
optional deeper experimental search modes.
This repository is intended for FT8 protocol experimentation, parity work, profiling, and integration into larger Go applications that need FT8 message recovery or standard-message encoding.
Status
This is active research code. The default decoder path is designed to preserve
strict corpus parity behavior, while DeepDecoderOptions trades speed and
strictness for additional recall.
The implementation is not a clean-room project. It was developed through
WSJT-X/jt9 parity research, source-adjacent investigation, and behavioral
comparison against an installed jt9 -8 decoder.
Install
go get github.com/ColonelBlimp/go-ft8
Import the codec package:
import "github.com/ColonelBlimp/go-ft8/ft8"
Basic Usage
DecodeMessages expects a single FT8 receive slot as 12 kHz mono-signed
16-bit PCM samples.
package main
import (
"fmt"
"github.com/ColonelBlimp/go-ft8/ft8"
)
func main() {
var pcm []int16 // Fill with one 15-second, 12 kHz mono FT8 slot.
messages := ft8.DecodeMessages(pcm)
for _, msg := range messages {
fmt.Printf("%7.1f Hz %+5.2f s %s\n", msg.FreqHz, msg.DTSec, msg.Text)
}
}
For a receiver stream, keep one decoder instance per stream so hash and A7 history can carry across adjacent slots:
decoder := ft8.NewDecoder()
messages := decoder.DecodeMessages(pcm)
For labeled strict/deep output:
result := ft8.DecodeStructured(pcm, ft8.StructuredDecodeOptions{
IncludeDeep: true,
})
for _, msg := range result.Messages {
fmt.Println(msg.Mode, msg.Text)
}
Structured decode also has report and checked variants for service integrations:
report, err := ft8.DecodeStructuredChecked(pcm, ft8.StructuredDecodeOptions{
IncludeDeep: true,
})
if err != nil {
panic(err)
}
fmt.Println(report.Result.Messages)
fmt.Printf("%+v\n", report.StrictReport.Diagnostics)
For production logging or empty-result investigation, use the report API:
report := ft8.DecodeMessagesWithReport(pcm, ft8.DecoderOptions{})
fmt.Println(report.Messages)
fmt.Printf("%+v\n", report.Diagnostics)
For station-manager style integrations that should reject malformed slots or configuration mistakes before decode work starts, use the checked API:
report, err := ft8.DecodeMessagesChecked(pcm, ft8.DecoderOptions{})
if err != nil {
// Invalid input length or invalid decoder options.
panic(err)
}
fmt.Println(report.Messages)
Checked decode errors support errors.Is with ErrInvalidDecodeInput and
ErrInvalidDecoderOptions, plus errors.As with *ft8.DecodeInputError or
*ft8.DecoderOptionError for structured details such as sample counts or the
invalid option field.
Encode supported standard FT8 messages to protocol bits, LDPC codeword, and tone sequence:
encoded, err := ft8.EncodeStandardMessage("CQ K1ABC FN42")
if err != nil {
// The first encoder surface intentionally accepts standard messages only.
panic(err)
}
fmt.Println(encoded.Text, encoded.Tones)
The package stops at FT8 protocol artifacts. Audio output, transmit scheduling, PTT, and radio control belong in separate packages.
Message Format Coverage
go-ft8 does not yet implement every WSJT-X 77-bit message family. Current
decode support is aimed at ordinary FT8 QSO traffic and service integration, not
full contest/DXpedition parity.
Supported decode payloads:
| Type | Status |
|---|---|
i3=0,n3=0 |
Free text, up to 13 characters |
i3=1 |
Standard messages: CQ, calls, grid, reports, RRR, RR73, 73 |
i3=2 |
Standard /P form used by European VHF-style messages |
i3=4 |
Compound/nonstandard calls using 12-bit hash context |
Known decode gaps:
| Type | Missing family |
|---|---|
i3=0,n3=1 |
DXpedition / Fox-Hound |
i3=0,n3=3 and i3=0,n3=4 |
ARRL Field Day |
i3=0,n3=5 |
Telemetry, 18 hex characters |
i3=3 |
ARRL RTTY Roundup |
i3=5 |
EU VHF contest with hashed calls, report, serial, and grid6 |
Decoded candidates with unsupported payload formats are rejected during unpack
and counted in DecodeDiagnostics.UnpackFailures. The public decoder also
currently filters decoded text containing /R and text beginning with TU; .
The reference message-family table is WSJT-X's 77-bit format description: https://github.com/WSJTX/wsjtx/blob/master/lib/77bit/77bit.txt
Decoder Modes
- Strict mode is the default and is used by
DecodeMessages. - Deep mode is available through
DeepDecoderOptionsorDecodeStructured(..., StructuredDecodeOptions{IncludeDeep: true}). - Custom search thresholds, frequency ranges, candidate caps, block counts, and
Costas gates are available through
DecoderOptions.
Production PocketFFT Backend
The default build uses pure-Go Gonum FFT support for portability. Production
builds should use the faster CGO PocketFFT backend with the pocketfft build
tag:
go test -tags pocketfft ./...
PocketFFT is vendored under internal/pfft/pocketfft/ and keeps its upstream
BSD 3-Clause license notices. The pure-Go Gonum backend remains the fallback for
builds where CGO or vendored C is not acceptable.
Benchmarks
Reference decode wall-clock benchmarks on an Intel Core i3-10100F, Linux amd64, Go 1.26.3, using the production PocketFFT backend and the six bundled WAV fixtures:
GOCACHE=/tmp/go-build go test -tags pocketfft ./ft8 -run=^$ \
-bench='BenchmarkDecode(Messages|Structured).*PerFixture' \
-benchmem -benchtime=1x -count=3
| Benchmark | Mean per 15s slot | Observed fixture range |
|---|---|---|
Strict DecodeMessages |
0.607 s | 0.568-0.677 s |
Deep DecodeMessagesWithOptions(DeepDecoderOptions()) |
3.84 s | 3.21-4.43 s |
Structured strict+deep DecodeStructured(...IncludeDeep) |
4.43 s | 3.78-5.07 s |
These numbers are local reference measurements, not performance guarantees. Wall time varies with CPU, CGO toolchain, OS scheduling, fixture content, and decode options.
Development
This repository uses Task for common development commands. Run the production test path with:
task test:prod
Run production decode benchmarks with:
task bench:prod
task bench:structured-deep-prod
Run the fixture-independent smoke tests:
task test:smoke
Run race-detector smoke tests:
task test:race
task test:race-prod
The package version is tracked in version.txt as Go module SemVer without a
leading v. Common version tasks:
task version:get
task version:set -- 0.1.0
task version:bump:patch
task version:tag
task version:push-tag
task version:tag validates version.txt, runs smoke tests for the default
and PocketFFT paths, requires a clean working tree, and creates an annotated
local tag such as v0.1.0. Pushing the tag is a separate explicit step.
task version:push-tag loads .env and requires GITHUB_TOKEN for a
non-interactive GitHub HTTPS push. The root .env file is ignored by git.
GITHUB_TOKEN=github_pat_...
The full corpus and diagnostic tests depend on the local WAV/truth fixture corpus and its expected testdata layout. Keep decode-scale or synchronization changes behind parity checks, because small numeric changes can affect strict-mode message recovery.
Licensing
This repository is distributed under GPL-3.0-only. See LICENSE and docs/LICENSING.md.
Because this is a WSJT-X/jt9-derived implementation, redistribution of this repository or derivative binaries should preserve the GPLv3 license text, the project NOTICE, the derivative-status note in docs/WSJTX_DERIVATIVE.md, and applicable third-party notices.