Skip to content
 
 

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

js8call-wasm-decoder

A browser-native JS8Call decoder compiled to WebAssembly from Rust. Runs entirely client-side — no server, no Python, no WSJT-X installation required.

Decodes JS8Call (all four speed modes: Slow / Normal / Fast / Turbo) using belief-propagation LDPC and Gray-coded 8-FSK demodulation. Accepts raw PCM audio at 12 kHz mono via a Web Worker, so the main thread is never blocked.


What Is JS8Call?

JS8Call is a weak-signal digital mode for amateur radio derived from FT8. It uses 8-tone FSK with 79 symbols per transmission and a (174, 91) LDPC error-correcting code, allowing contacts at signal levels well below the noise floor. Website: https://js8call.com


Quick Start — Pre-compiled (no build required)

The dist/ directory contains pre-compiled files ready to drop into any web project served over HTTP.

Files needed

File Purpose
dist/js8call_wasm.js JavaScript glue generated by wasm-bindgen
dist/js8call_wasm_bg.wasm Compiled WebAssembly binary (~350 KB)
dist/js8call-worker.js Module Web Worker (audio accumulation + decode)
dist/js8call.js Controller class for the main thread

All four files must be served from the same HTTP directory. They cannot be loaded from file:// URLs due to WASM CORS restrictions.

Minimal example

Copy the four dist/ files to your web server. Add to your HTML:

<script src="js8call.js"></script>
// 1. Create decoder and wire up callbacks
const decoder = new Js8CallDecoder();
await decoder.init(
  (messages, slotTime) => {
    messages.forEach(m => {
      console.log(`${m.time}  ${m.freq.toFixed(0)} Hz  SNR ${m.snr > 0 ? '+' : ''}${m.snr}  ${m.msg}`);
    });
  },
  (statusText) => console.log('[JS8]', statusText)
);

// 2. Start buffering in Normal mode (15.6 s slots)
//    Mode IDs: 0=Slow(30s)  1=Normal(15.6s)  2=Fast(10s)  3=Turbo(6s)
decoder.start(1);

// 3. Feed raw audio.  Each ArrayBuffer must be:
//    [4-byte uint32 LE timestamp][Int16 PCM samples at 12 kHz mono]
//    (The 4-byte header matches the /ws/audio WebSocket frame format
//     used by the T-Embed SI4732 firmware — strip or keep as-is.)
websocket.onmessage = (e) => {
  decoder.pushAudioFrame(e.data);
};

// 4. Change speed mode at runtime (no restart needed)
decoder.setMode(2);  // switch to Fast

// 5. Stop and release
decoder.stop();

Decoded message format

Each entry in the messages array has:

{
  msg:  string,   // decoded text, e.g. "W1AW K1TTT FN31"
  snr:  number,   // signal-to-noise ratio in dB
  freq: number,   // carrier frequency in Hz
  time: string,   // HH:MM:SS UTC at slot boundary
  dt:   number    // time offset (0 — not yet implemented)
}

Audio Input Requirements

Parameter Value
Sample rate 12 000 Hz (12 kHz)
Channels Mono
Format Int16 (signed 16-bit)
Frame header 4 bytes uint32 LE timestamp prefix (can be zeros)

The 4-byte timestamp prefix matches the WebSocket audio frame format of the T-Embed SI4732 firmware. If your audio source does not have this header, prepend four zero bytes:

// Prepend a dummy 4-byte header to a raw Int16 buffer
function wrapFrame(int16ArrayBuffer) {
  const out = new Uint8Array(4 + int16ArrayBuffer.byteLength);
  out.set(new Uint8Array(int16ArrayBuffer), 4);
  return out.buffer;
}
decoder.pushAudioFrame(wrapFrame(rawPcmBuffer));

JS8Call Speed Modes

ID Name Slot duration Symbol period Tone spacing
0 Slow 30 s 320 ms 3.125 Hz
1 Normal 15.6 s 160 ms 6.25 Hz
2 Fast 10 s 80 ms 12.5 Hz
3 Turbo 6 s 40 ms 25.0 Hz

All modes use 79 symbols per frame, 8-FSK modulation, and the same (174, 91) LDPC code. Tune your radio to USB on a JS8Call frequency:

Band Frequency (USB dial)
80 m 3.578 MHz
40 m 7.078 MHz
30 m 10.130 MHz
20 m 14.078 MHz
17 m 18.104 MHz
15 m 21.078 MHz

Decimator API (optional)

The Decimator class converts audio from higher sample rates down to 12 kHz using a Kaiser-windowed FIR lowpass filter. Useful if your audio source is at 48 kHz, 96 kHz, etc.

// Load js8call_wasm.js first (in a module context)
import init, { Decimator } from './js8call_wasm.js';
await init();

const dec = new Decimator(4, 127);   // 48 kHz → 12 kHz, 127 taps
const output = dec.process(float32InputAt48kHz);  // returns Float32Array at 12 kHz

Building from Source

Follow these steps to recompile the WASM from the Rust source in wasm/.

Prerequisites

1. Rust

Download and run the installer from https://rustup.rs

Accept the default (press Enter for option 1). Close and reopen your terminal after install.

rustc --version

2. WebAssembly compile target

rustup target add wasm32-unknown-unknown

3. Visual Studio C++ build tools (Windows only)

cargo install wasm-pack compiles a native Windows binary and requires the MSVC C++ runtime libraries.

If you see LNK1104: cannot open file 'msvcrt.lib' during Step 4:

  • Open Visual Studio Installer (search Start menu)
  • Click Modify next to Visual Studio 2022
  • Check Desktop development with C++
  • Click Modify and wait (~2–4 GB download)
  • Close all terminals and reopen

Quick test first: open x64 Native Tools Command Prompt for VS 2022 instead of a normal terminal. If Step 4 succeeds from there, the workload is already installed — just always run the build from that prompt.

4. wasm-pack

cargo install wasm-pack

Takes 2–5 minutes (compiles from source).

wasm-pack --version

5. Python 3.8+ and the requests library

Download Python from https://python.org (check Add Python to PATH).

pip install requests

Build steps

Step 1 — Navigate to the repository root

cd path/to/js8call-wasm-decoder

Step 2 — Generate the LDPC matrix

The parity-check matrix is not stored in the repo — it is fetched from the ft8_lib source on GitHub and written to wasm/src/ldpc_matrix.rs. This file is auto-generated and gitignored.

python tools/gen_ldpc_matrix.py

Expected output:

Fetching ft8lib source (trying candidate URLs) ...
  Using: https://raw.githubusercontent.com/kgoba/ft8_lib/master/ft8/constants.c
  kFTX_LDPC_Mn: 174 rows -> NM (variable->check)
  kFTX_LDPC_Nm:  83 rows -> MN (check->variable)
  NM max degree: 3
  MN max degree: 7

Wrote wasm/src/ldpc_matrix.rs  (6,850 bytes)
Now rebuild WASM:  wasm-pack build --target web

Step 3 — Compile the WASM

cd wasm
wasm-pack build --target web --out-dir ../wasm/pkg
cd ..

This takes 15–60 seconds on a modern machine. Output ends with:

[INFO]: :-) Done in Xs
[INFO]: :-) Your wasm pkg is ready to publish at ./wasm/pkg.

Step 4 — Copy compiled files to dist/

Windows Command Prompt:

copy wasm\pkg\js8call_wasm.js    dist\js8call_wasm.js
copy wasm\pkg\js8call_wasm_bg.wasm dist\js8call_wasm_bg.wasm

Linux / macOS / Git Bash:

cp wasm/pkg/js8call_wasm.js       dist/
cp wasm/pkg/js8call_wasm_bg.wasm  dist/

The dist/js8call-worker.js and dist/js8call.js files are hand-written and do not need to be recompiled.


Project Structure

js8call-wasm-decoder/
├── README.md
├── LICENSE                     GPL-3.0
├── .gitignore
├── tools/
│   └── gen_ldpc_matrix.py      Fetches LDPC matrix from ft8lib GitHub
├── wasm/
│   ├── Cargo.toml
│   ├── pkg/                    Build output (gitignored)
│   └── src/
│       ├── lib.rs              Public WASM API (Js8Decoder, Decimator)
│       ├── types.rs            SpeedMode, frame geometry constants
│       ├── spectrogram.rs      Hann-windowed sliding FFT
│       ├── sync.rs             Costas array correlation search
│       ├── llr.rs              Gray-coded 8-FSK LLR extraction
│       ├── ldpc.rs             Belief-propagation LDPC decoder
│       ├── message.rs          Callsign / grid / free-text unpacking
│       ├── decimate.rs         Kaiser FIR decimation filter
│       └── ldpc_matrix.rs      GENERATED — do not edit
└── dist/
    ├── js8call_wasm.js         Pre-compiled JS glue (wasm-bindgen)
    ├── js8call_wasm_bg.wasm    Pre-compiled WASM binary
    ├── js8call-worker.js       Module Web Worker
    └── js8call.js              Main-thread controller class

Algorithm Overview

Audio PCM (12 kHz, Int16)
         │
         ▼
  js8call-worker.js
  ┌──────────────────────────────────────┐
  │  Accumulate one slot of samples      │
  │  (6–30 s depending on speed mode)   │
  │                                      │
  │  Js8Decoder.push_samples()           │
  │  Js8Decoder.run_decode()             │
  │                                      │
  │  spectrogram.rs   sliding FFT        │
  │       ↓                              │
  │  sync.rs          Costas search      │
  │       ↓                              │
  │  llr.rs           LLR extraction     │
  │       ↓                              │
  │  ldpc.rs          belief propagation │
  │       ↓                              │
  │  message.rs       callsign decode    │
  └──────────────────────────────────────┘
         │
         ▼
  { msg, snr, freq, time }

Troubleshooting

Problem Fix
couldn't read src/ldpc_matrix.rs Run python tools/gen_ldpc_matrix.py first
stream did not contain valid UTF-8 Re-run gen_ldpc_matrix.py (encoding bug was fixed)
59 errors E0600: cannot apply unary - to u8 Re-run gen_ldpc_matrix.py (padding fix was applied)
LNK1104: cannot open file 'msvcrt.lib' Install Desktop development with C++ in Visual Studio Installer
Linker error only in regular terminal Use x64 Native Tools Command Prompt for VS 2022
WASM loads but no decodes Tune to a JS8Call frequency in USB mode; ensure audio is at 12 kHz mono
Status shows <bits:hex> Directed/relay message type — standard callsign pairs decode correctly
Worker fails with CORS error Files must be served over HTTP, not file://

License

GPL-3.0 — see LICENSE.

The LDPC parity-check matrix is fetched from kgoba/ft8_lib (MIT license) at build time.

JS8Call is developed by KN4CRD. This project is not affiliated with or endorsed by the JS8Call project.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages