A high-performance Node.js C++ extension for digital amateur radio protocols, providing TypeScript support and async/await interfaces for WSJTX library functionality.
- 🚀 High Performance: Native C++ implementation with multi-threading support
- 📡 Multiple Modes: Support for FT8, FT4, JT4, JT65, JT9, FST4, Q65, FST4W, and WSPR
- 🔧 TypeScript Support: Full TypeScript definitions and modern ES modules
- ⚡ Async/Await: Promise-based API for non-blocking operations
- 🎵 Audio Processing: Support for both Float32Array and Int16Array audio formats
- 🌍 Cross-Platform: Prebuilt binaries for Windows, macOS, and Linux
- 📊 WSPR Decoding: Specialized support for WSPR IQ data processing
| Mode | Encoding | Decoding | Sample Rate | Duration | Bandwidth |
|---|---|---|---|---|---|
| FT8 | ✅ | ✅ | 48 kHz | 12.6s | ~50 Hz |
| FT4 | ✅ | ✅ | 48 kHz | 6.0s | ~80 Hz |
| JT4 | ❌ | ✅ | 11.025 kHz | 47.1s | Variable |
| JT65 | ❌ | ✅ | 11.025 kHz | 46.8s | ~180 Hz |
| JT9 | ❌ | ✅ | 12 kHz | 49.0s | ~16 Hz |
| FST4 | ❌ | ✅ | 12 kHz | 60.0s | Variable |
| Q65 | ❌ | ✅ | 12 kHz | 60.0s | Variable |
| FST4W | ❌ | ✅ | 12 kHz | 120.0s | Variable |
| WSPR | ❌ | ✅ | 12 kHz | 110.6s | ~6 Hz |
The package includes prebuilt binaries for major platforms:
npm install wsjtx-libSupported platforms with prebuilt binaries:
- Linux x64 / arm64
- macOS x64 (Intel) / ARM64 (Apple Silicon)
- Windows x64
Runtime binary loading uses node-gyp-build with prebuildify layout
(prebuilds/<platform>-<arch>/*.node), and falls back to
build/Release/*.node for local development builds.
Linux prebuilds are built and checked so neither wsjtx_lib_nodejs.node nor
libwsjtx_core.so requires an executable stack. Downstreams should not patch
PT_GNU_STACK or require GLIBC_TUNABLES=glibc.rtld.execstack=2; if that
condition appears, treat it as a source/build regression.
Only needed if prebuilt binaries are not available for your platform.
- Node.js 16+
- CMake 3.15+
- C++ compiler with C++17 support
- FFTW3 library (single precision)
- Boost libraries
- Fortran compiler (gfortran)
# Install dependencies using Homebrew
brew install cmake fftw boost gcc pkg-config
# Clone and build
git clone --recursive https://github.com/boybook/wsjtx_lib_nodejs.git
cd wsjtx_lib_nodejs
npm install
npm run build# Install dependencies
sudo apt-get update
sudo apt-get install -y \
cmake \
build-essential \
gfortran \
libfftw3-dev \
libboost-all-dev \
pkg-config
# Clone and build
git clone --recursive https://github.com/boybook/wsjtx_lib_nodejs.git
cd wsjtx_lib_nodejs
npm install
npm run buildUse MSYS2/MinGW-w64 for best compatibility:
# Install MSYS2, then in MSYS2 MINGW64 terminal:
pacman -S --needed \
base-devel \
mingw-w64-x86_64-toolchain \
mingw-w64-x86_64-cmake \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-fftw \
mingw-w64-x86_64-boost \
mingw-w64-x86_64-gcc-fortran \
mingw-w64-x86_64-nodejs
# Clone and build
git clone --recursive https://github.com/boybook/wsjtx_lib_nodejs.git
cd wsjtx_lib_nodejs
npm install
npm run buildimport { WSJTXLib, WSJTXMode } from 'wsjtx-lib';
async function example() {
// Create library instance
const lib = new WSJTXLib();
// Encode an FT8 message
const encodeResult = await lib.encode(
WSJTXMode.FT8,
'CQ DX BH1ABC OM88',
1000 // Audio frequency in Hz (typically 500-3000 Hz)
);
console.log(`Generated ${encodeResult.audioData.length} audio samples`);
console.log(`Message sent: "${encodeResult.messageSent}"`);
// Decode audio data (example with proper resampling for FT8)
const audioData = new Float32Array(48000 * 13); // 13 seconds at 48kHz
// ... fill audioData with actual audio samples ...
const decodeResult = await lib.decode(
WSJTXMode.FT8,
audioData,
1000 // Same audio frequency used for encoding
);
// Get decoded messages
const messages = lib.pullMessages();
messages.forEach(msg => {
console.log(`Decoded: "${msg.text}" (SNR: ${msg.snr} dB, ΔT: ${msg.deltaTime}s)`);
});
}new WSJTXLib(config?: WSJTXConfig)Creates a new WSJTX library instance.
Parameters:
config(optional): Configuration optionsmaxThreads: Maximum number of threads (1-16, default: 4)debug: Enable debug logging (default: false)
Decode digital radio signals from audio data.
Parameters:
mode: WSJTXMode enum valueaudioData: Float32Array or Int16Array of audio samplesfrequency: Audio frequency in Hz (typically 500-3000 Hz)threads: Number of threads to use (optional, default: 4)
Returns: Promise resolving to DecodeResult with success status
Note: For optimal FT8 decoding, audio may need resampling. See examples for details.
Use one session for the live FT8 windows so the native decoder can retain its
subtraction, OSD, and AP state. decodeDepth is explicitly 1 (Fast), 2
(Normal), or 3 (Deep); the application default is 3 and is kept unchanged
for every stage.
const session = lib.beginDecodeSession({
sessionId: 'ft8-slot-123',
mode: WSJTXMode.FT8,
decodeDepth: 3,
slotUtc: 123045,
});
await session.decodeStage(audio, 41, { frequency: 1500 });
await session.decodeStage(audio, 47, { frequency: 1500 });
const final = await session.decodeStage(audio, 50, { frequency: 1500 });
const summary = session.endDecodeSession();Supported FT8 stages are 41, 47, 49, and 50. Repeating a stage is skipped and returns no new messages. Each stage result includes native timing and decoder statistics for observation.
Encode a message into audio waveform for transmission.
Parameters:
mode: WSJTXMode enum valuemessage: Message text to encode (FT8/FT4 structured messages: 1-37 characters; free text payloads are limited by WSJT-X to 13 characters)frequency: Audio frequency in Hz (typically 500-3000 Hz)threads: Number of threads to use (optional, default: 4)
Returns: Promise resolving to EncodeResult with audio data and actual message sent
Decode WSPR signals from IQ data.
Parameters:
iqData: Float32Array of interleaved I,Q samplesoptions: WSPRDecodeOptions (optional)dialFrequency: RF dial frequency in Hz (default: 14095600)callsign: Station callsignlocator: Grid locatorquickMode: Enable quick decode mode (default: false)useHashTable: Use hash table optimization (default: true)passes: Number of decode passes (default: 2)subtraction: Enable signal subtraction (default: true)
Returns: Promise resolving to array of WSPR decode results
Retrieve decoded messages from the internal queue.
Returns: Array of decoded messages
isEncodingSupported(mode): boolean- Check if encoding is supported for a modeisDecodingSupported(mode): boolean- Check if decoding is supported for a modegetSampleRate(mode): number- Get required sample rate for a modegetTransmissionDuration(mode): number- Get transmission duration for a modegetAllModeCapabilities(): ModeCapabilities[]- Get capabilities for all modes
convertAudioFormat(audioData, targetFormat): AudioData- Convert between Float32Array and Int16Array
enum WSJTXMode {
FT8 = 0,
FT4 = 1,
JT4 = 2,
JT65 = 3,
JT9 = 4,
FST4 = 5,
Q65 = 6,
FST4W = 7,
WSPR = 8
}interface WSJTXMessage {
text: string; // Decoded message text
snr: number; // Signal-to-noise ratio in dB
deltaTime: number; // Time offset in seconds
deltaFrequency: number; // Frequency offset in Hz
}interface EncodeResult {
audioData: Float32Array; // Generated audio waveform (48kHz sample rate)
messageSent: string; // Actual message encoded
}interface WSPRResult {
frequency: number; // Signal frequency in Hz
sync: number; // Sync quality
snr: number; // Signal-to-noise ratio in dB
deltaTime: number; // Time offset in seconds
drift: number; // Frequency drift in Hz/minute
jitter: number; // Jitter metric
message: string; // Decoded message
callsign: string; // Decoded callsign
locator: string; // Decoded grid locator
power: string; // Decoded power in dBm
cycles: number; // Number of decode cycles
}import { WSJTXLib, WSJTXMode } from 'wsjtx-lib';
import * as fs from 'fs';
import * as wav from 'wav';
async function ft8Example() {
const lib = new WSJTXLib();
const message = 'CQ DX BH1ABC OM88';
const audioFrequency = 1000;
// 1. Encode message
const encodeResult = await lib.encode(WSJTXMode.FT8, message, audioFrequency);
console.log(`Encoded: "${encodeResult.messageSent}"`);
// 2. Save as WAV file
const audioInt16 = new Int16Array(encodeResult.audioData.length);
for (let i = 0; i < encodeResult.audioData.length; i++) {
audioInt16[i] = Math.round(encodeResult.audioData[i] * 32767);
}
const writer = new wav.FileWriter('ft8_test.wav', {
channels: 1,
sampleRate: lib.getSampleRate(WSJTXMode.FT8),
bitDepth: 16
});
const buffer = Buffer.from(audioInt16.buffer);
writer.write(buffer);
writer.end();
// 3. Read back and decode
// Note: For optimal decode, you may need resampling
const resampled = resampleTo12kHz(encodeResult.audioData);
const audioForDecode = new Int16Array(resampled.length);
for (let i = 0; i < resampled.length; i++) {
audioForDecode[i] = Math.round(resampled[i] * 32767);
}
lib.pullMessages(); // Clear queue
const decodeResult = await lib.decode(WSJTXMode.FT8, audioForDecode, audioFrequency);
const messages = lib.pullMessages();
console.log(`Decoded ${messages.length} messages`);
}
// Helper function for resampling (48kHz -> 12kHz)
function resampleTo12kHz(audioData48k: Float32Array): Float32Array {
const audioData12k = new Float32Array(Math.floor(audioData48k.length / 4));
for (let i = 0; i < audioData12k.length; i++) {
audioData12k[i] = audioData48k[i * 4];
}
return audioData12k;
}import { WSJTXLib } from 'wsjtx-lib';
async function wsprExample() {
const lib = new WSJTXLib();
// IQ data (interleaved I,Q samples)
const iqData = new Float32Array(2 * 12000 * 120); // 2 minutes of IQ data
// ... fill with actual IQ data from SDR ...
const options = {
dialFrequency: 14095600, // 20m WSPR frequency
callsign: 'BH1ABC',
locator: 'OM88',
quickMode: false,
passes: 2
};
const results = await lib.decodeWSPR(iqData, options);
console.log('WSPR Decode Results:');
results.forEach(result => {
console.log(`${result.callsign} ${result.locator} ${result.power}dBm (SNR: ${result.snr}dB)`);
});
}import { WSJTXLib } from 'wsjtx-lib';
// Convert Float32Array to Int16Array
const floatData = new Float32Array([0.5, -0.5, 0.25, -0.25]);
const intData = WSJTXLib.convertAudioFormat(floatData, 'int16');
console.log(intData); // Int16Array [16384, -16384, 8192, -8192]
// Convert back to Float32Array
const backToFloat = WSJTXLib.convertAudioFormat(intData, 'float32');
console.log(backToFloat); // Float32Array [0.5, -0.5, 0.25, -0.25] (approximately)import { WSJTXLib, WSJTXMode } from 'wsjtx-lib';
async function multipleMessages() {
const lib = new WSJTXLib();
const audioFrequency = 1000;
const messages = [
'CQ DX BH1ABC OM88', // CQ call
'BH1ABC BH2DEF +05', // Signal report
'BH2DEF BH1ABC R-12', // Report acknowledgment
'BH1ABC BH2DEF RRR', // Received acknowledgment
'BH2DEF BH1ABC 73' // End contact
];
for (const message of messages) {
const result = await lib.encode(WSJTXMode.FT8, message, audioFrequency);
console.log(`"${message}" -> "${result.messageSent}" (${result.audioData.length} samples)`);
}
}The library throws WSJTXError for all operation failures:
import { WSJTXError } from 'wsjtx-lib';
try {
await lib.decode(WSJTXMode.FT8, audioData, 1000);
} catch (error) {
if (error instanceof WSJTXError) {
console.error(`WSJTX Error [${error.code}]: ${error.message}`);
// Common error codes:
// - INVALID_MODE: Invalid mode parameter
// - INVALID_FREQUENCY: Invalid frequency parameter
// - INVALID_AUDIO_DATA: Invalid audio data format/size
// - INVALID_MESSAGE: Invalid message text
// - DECODE_ERROR: Decoding operation failed
// - ENCODE_ERROR: Encoding operation failed
} else {
console.error('Unexpected error:', error);
}
}-
Audio Frequency: The
frequencyparameter is the audio tone frequency within your audio passband (typically 500-3000 Hz), not the RF frequency. -
Sample Rates: Different modes require different sample rates. Use
lib.getSampleRate(mode)to get the correct rate. -
Audio Resampling: For optimal FT8 decoding, audio may need to be resampled from 48kHz to 12kHz. See examples for implementation.
-
Thread Safety: Each WSJTXLib instance should be used from a single thread. Create separate instances for concurrent operations.
-
Message Queue: The
pullMessages()method clears the internal message queue. Call it regularly to avoid memory buildup.
For detailed build instructions when prebuilt binaries are not available, see BUILD.md.
# Clone with submodules
git clone --recursive https://github.com/boybook/wsjtx_lib_nodejs.git
cd wsjtx_lib_nodejs
# Install dependencies
npm install
# Build native module and TypeScript
npm run build
# Run tests
npm test
# Run comprehensive tests
npm run test:full
# Run examples
node examples/examples.jswsjtx_lib_nodejs/
├── src/ # TypeScript source files
├── native/ # C++ wrapper code
├── wsjtx_lib/ # Git submodule (wsjtx_lib library)
├── test/ # Test files
├── examples/ # Usage examples
├── dist/ # Compiled TypeScript output
├── prebuilds/ # Prebuilt binaries for distribution
└── build/ # CMake build directory
npm run build- Build both native module and TypeScriptnpm run build:native- Build only the native C++ modulenpm run build:ts- Build only TypeScriptnpm test- Run basic tests (CI-friendly)npm run test:full- Run comprehensive testsnpm run clean- Clean build artifactsnpm run package- Package prebuilt binaries for distribution
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
- Based on the excellent wsjtx_lib library by PA0PHH
- WSJT-X development team for the original algorithms by K1JT
- Amateur radio community for protocol specifications