Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7e1daa8
Add Q65 upstream patch overlay
Iu-yang1 May 23, 2026
0e88c2b
Apply Q65 overlay during core build
Iu-yang1 May 23, 2026
b3b4000
Patch native Q65 capability and wrapper plumbing
Iu-yang1 May 23, 2026
ff7cb4a
Add Q65 smoke regression coverage
Iu-yang1 May 23, 2026
3ac69b2
Document Q65 transmit and receive chain
Iu-yang1 May 23, 2026
f522307
Expose Q65 mode-specific TypeScript options
Iu-yang1 May 23, 2026
667d495
Add Q65 encode and decode option plumbing
Iu-yang1 May 23, 2026
583354a
Add Q65 options to native C ABI
Iu-yang1 May 23, 2026
b07fb7b
Implement native Q65 encode and decode options
Iu-yang1 May 23, 2026
81def6a
Carry encode options through N-API worker
Iu-yang1 May 23, 2026
f9a0fc2
Support object encode options in N-API wrapper
Iu-yang1 May 23, 2026
823c959
Parameterize Q65 overlay for all periods and submodes
Iu-yang1 May 23, 2026
6909c0b
Cover Q65 period and submode options in smoke tests
Iu-yang1 May 23, 2026
c2e17a4
Update Q65 docs for full public options
Iu-yang1 May 23, 2026
b4335b8
Add Q65 Linux smoke build workflow
Iu-yang1 May 23, 2026
f9054bd
Fix strict Q65 option narrowing
Iu-yang1 May 23, 2026
3cffb9f
Add official Q65 sample decode probe
Iu-yang1 May 23, 2026
e8f7cff
Add official Q65 sample decode workflow
Iu-yang1 May 23, 2026
829e23b
Diagnose official Q65 sample decode parameters
Iu-yang1 May 23, 2026
a94af33
Expose decode UTC option for disk samples
Iu-yang1 May 23, 2026
50c582d
Pass decode UTC option to native layer
Iu-yang1 May 23, 2026
63ce2d3
Add UTC field to decode C ABI
Iu-yang1 May 23, 2026
869dcff
Restore Q65Period type export
Iu-yang1 May 23, 2026
afe022a
Wire decode UTC through Q65 overlay
Iu-yang1 May 23, 2026
3d1a628
Pass sample UTC and add Q65 self-roundtrip probe
Iu-yang1 May 23, 2026
ca66ca7
Probe Q65 decode with default drift and audio stats
Iu-yang1 May 23, 2026
0201198
Expose raw decoder output and disk decode options
Iu-yang1 May 23, 2026
aa6bd1b
Make Q65 sample probe action-friendly
Iu-yang1 May 23, 2026
8e0e9bc
Package Q65 diagnostic report as workflow artifact
Iu-yang1 May 23, 2026
48333e5
Pass Q65 disk decode diagnostic options to native
Iu-yang1 May 23, 2026
952ab4d
Add Q65 disk decode diagnostic fields to C ABI
Iu-yang1 May 23, 2026
5bbd555
Add Q65 diagnostic native overlay
Iu-yang1 May 23, 2026
9855176
Apply Q65 diagnostic overlay during native build
Iu-yang1 May 23, 2026
e95b6cc
Include raw decoder output in Q65 diagnostic report
Iu-yang1 May 23, 2026
d5ee0e6
Add Q65 decoder input preservation diagnostics
Iu-yang1 May 23, 2026
2f5ad0c
Include Q65 input preservation diagnostics overlay
Iu-yang1 May 23, 2026
583592b
Remove Q65 diagnostic-only public options
Iu-yang1 May 23, 2026
eeac01f
Restore stable public TypeScript API for Q65
Iu-yang1 May 23, 2026
7789ffb
Remove Q65 diagnostic workflow
Iu-yang1 May 23, 2026
f88887a
Remove Q65 diagnostic sample script
Iu-yang1 May 23, 2026
4df01d4
Remove Q65 diagnostic overlay
Iu-yang1 May 23, 2026
99176db
Stop applying removed Q65 diagnostic overlay
Iu-yang1 May 23, 2026
33cb1d7
Remove Q65 input diagnostic overlay file
Iu-yang1 May 23, 2026
4cbabb5
Restore stable C ABI for Q65
Iu-yang1 May 23, 2026
598adcb
Fold Q65 input fix into production overlay
Iu-yang1 May 23, 2026
ac83f60
Add Q65 self round-trip regression test
Iu-yang1 May 23, 2026
5ab2a08
Document Q65 encode and decode usage
Iu-yang1 May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Build and Package

on:
workflow_dispatch:
push:
branches: [main, develop]
branches: [main, develop, feature/q65-chain]
tags: ['v*']
pull_request:
branches: [main]
Expand All @@ -11,6 +12,72 @@ env:
NODE_VERSION: '20'

jobs:
q65-linux-smoke:
name: Q65 Linux smoke build
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Install native build dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
cmake build-essential gfortran libfftw3-dev libboost-all-dev \
pkg-config python3 ca-certificates

- name: Install Node dependencies
run: npm ci --ignore-scripts

- name: Build native addon and TypeScript
run: npm run build

- name: Run Q65 smoke regression tests
run: npm test

- name: Print Q65 option smoke output
run: |
node --input-type=module - <<'JS'
import { WSJTXLib, WSJTXMode } from './dist/src/index.js';
const lib = new WSJTXLib();
const cases = [
{ q65Period: 30, q65Submode: 'B' },
{ q65Period: 60, q65Submode: 'A' },
{ q65Period: 120, q65Submode: 'E' },
];
for (const q65 of cases) {
const encoded = await lib.encode(WSJTXMode.Q65, 'CQ K1ABC FN20', 1500, {
threads: 1,
...q65,
});
console.log(JSON.stringify({
mode: 'Q65',
...q65,
sampleRate: encoded.sampleRate,
samples: encoded.audioData.length,
seconds: encoded.audioData.length / encoded.sampleRate,
messageSent: encoded.messageSent.trim(),
}));
}
JS

- uses: actions/upload-artifact@v4
if: failure()
with:
name: q65-linux-smoke-debug
path: |
build/
CMakeFiles/
CMakeCache.txt
retention-days: 7

build:
name: Build ${{ matrix.platform }}-${{ matrix.arch }}
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -361,4 +428,4 @@ jobs:
tar -czf "wsjtx-lib-${VERSION}-${platform}.tar.gz" -C prebuilds "$platform"
done
gh release create "$VERSION" --title "$VERSION" --generate-notes \
wsjtx-lib-${VERSION}-*.tar.gz
wsjtx-lib-${VERSION}-*.tar.gz
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cmake_minimum_required(VERSION 3.15)
# ============================================================================
option(WSJTX_BUILD_CORE_ONLY "Build only wsjtx_core shared library" OFF)
option(WSJTX_BUILD_NODE_ONLY "Build only .node module (requires pre-built wsjtx_core)" OFF)
option(WSJTX_ENABLE_Q65_CHAIN "Apply local Q65 TX/RX overlay to wsjtx_lib" ON)

# Disable vcpkg manifest mode if detected
if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg")
Expand Down Expand Up @@ -255,7 +256,11 @@ include_directories(
# Dependencies for the core library
set(LIBRARIES_FROM_REFERENCES ${FFTW3F_LIBRARIES} ${FFTW_THREADS_LIBRARIES})

# Build the Fortran/C++ core
# Build the Fortran/C++ core. Apply the Q65 source overlay before the
# submodule is configured so the generated wsjtx_lib target includes Q65 TX/RX.
if(WSJTX_ENABLE_Q65_CHAIN)
include("${CMAKE_SOURCE_DIR}/cmake/patch-wsjtx-q65.cmake")
endif()
add_subdirectory(wsjtx_lib)
link_directories(${FFTW3F_LIBRARY_DIRS})

Expand Down
166 changes: 121 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A high-performance Node.js C++ extension for digital amateur radio protocols, pr
| 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 |
| Q65 | | ✅ | 12 kHz | 30/60/120/300s | Variable |
| FST4W| ❌ | ✅ | 12 kHz | 120.0s | Variable |
| WSPR | ❌ | ✅ | 12 kHz | 110.6s | ~6 Hz |

Expand Down Expand Up @@ -140,19 +140,16 @@ async function example() {
console.log(`Generated ${encodeResult.audioData.length} audio samples`);
console.log(`Message sent: "${encodeResult.messageSent}"`);

// Decode audio data (example with proper resampling for FT8)
// Decode audio data
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
);
const decodeResult = await lib.decode(WSJTXMode.FT8, audioData, {
frequency: 1000,
threads: 4
});

// Get decoded messages
const messages = lib.pullMessages();
messages.forEach(msg => {
decodeResult.messages.forEach(msg => {
console.log(`Decoded: "${msg.text}" (SNR: ${msg.snr} dB, ΔT: ${msg.deltaTime}s)`);
});
}
Expand All @@ -173,33 +170,52 @@ Creates a new WSJTX library instance.
**Parameters:**
- `config` (optional): Configuration options
- `maxThreads`: Maximum number of threads (1-16, default: 4)
- `encodeSampleRate`: Process-global FT8/FT4/Q65 encode output sample rate (`12000` or `48000`, default: `12000`)
- `debug`: Enable debug logging (default: false)

#### Methods

##### `decode(mode, audioData, frequency, threads?): Promise<DecodeResult>`
##### `decode(mode, audioData, options): Promise<DecodeResult>`

Decode digital radio signals from audio data.

**Parameters:**
- `mode`: WSJTXMode enum value
- `audioData`: Float32Array or Int16Array of audio samples
- `frequency`: 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.

##### `encode(mode, message, frequency, threads?): Promise<EncodeResult>`
- `options`: DecodeOptions object
- `frequency`: Audio frequency in Hz (typically 500-3000 Hz)
- `txFrequency`: Transmit audio frequency in Hz (optional, defaults to `frequency`)
- `threads`: Number of threads to use (optional, default: 4)
- `lowFreq`: Lower decode frequency limit in Hz (optional, default: 200)
- `highFreq`: Upper decode frequency limit in Hz (optional, default: 4000)
- `tolerance`: Frequency tolerance in Hz (optional, default: 20)
- `myCall`, `myGrid`, `dxCall`, `dxGrid`: Optional AP decode context
- `apDecode`: Enable AP decode passes (optional, default: true)
- `decodeDepth`: WSJT-X decoder depth (optional, default: 1)
- `qsoProgress`: WSJT-X QSO progress stage (optional, default: 0)
- `q65Period`: Q65 period in seconds: `30`, `60`, `120`, or `300` (optional, default: `60`)
- `q65Submode`: Q65 submode: `'A'`, `'B'`, `'C'`, `'D'`, `'E'`, or `0`-`4` (optional, default: `'A'`)
- `q65MaxDrift`: Q65 max drift control (optional, default: `50`)
- `q65ClearAveraging`: Clear Q65 averaging state before decode (optional, default: false)
- `q65SingleDecode`: Request Q65 single-candidate decode behavior (optional, default: false)
- `q65Averaging`: Enable Q65 averaged decode passes (optional, default: false)

**Returns:** Promise resolving to DecodeResult with success status and decoded messages

**Note:** Use `lib.getSampleRate(mode)` to determine the expected sample rate for a mode. Q65 uses 12 kHz audio by default.

##### `encode(mode, message, frequency, threadsOrOptions?): Promise<EncodeResult>`

Encode a message into audio waveform for transmission.

**Parameters:**
- `mode`: WSJTXMode enum value
- `message`: Message text to encode (FT8/FT4 structured messages: 1-37 characters; free text payloads are limited by WSJT-X to 13 characters)
- `message`: Message text to encode (FT8/FT4/Q65 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)
- `threadsOrOptions`: Either a thread count number or an EncodeOptions object (optional, default: 4)
- `threads`: Number of threads to use
- `q65Period`: Q65 period in seconds: `30`, `60`, `120`, or `300` (optional, default: `60`)
- `q65Submode`: Q65 submode: `'A'`, `'B'`, `'C'`, `'D'`, `'E'`, or `0`-`4` (optional, default: `'A'`)

**Returns:** Promise resolving to EncodeResult with audio data and actual message sent

Expand Down Expand Up @@ -252,7 +268,8 @@ enum WSJTXMode {
FST4 = 5,
Q65 = 6,
FST4W = 7,
WSPR = 8
JT65JT9 = 8,
WSPR = 9
}
```

Expand All @@ -264,18 +281,30 @@ interface WSJTXMessage {
snr: number; // Signal-to-noise ratio in dB
deltaTime: number; // Time offset in seconds
deltaFrequency: number; // Frequency offset in Hz
timestamp: number; // seconds-of-day reported by the decoder
sync: number; // Sync quality
}
```

#### EncodeResult

```typescript
interface EncodeResult {
audioData: Float32Array; // Generated audio waveform (48kHz sample rate)
audioData: Float32Array; // Generated audio waveform
messageSent: string; // Actual message encoded
sampleRate: number; // Output sample rate
}
```

#### Q65 Options

```typescript
type Q65Period = 30 | 60 | 120 | 300;
type Q65Submode = 'A' | 'B' | 'C' | 'D' | 'E' | 0 | 1 | 2 | 3 | 4;
```

Q65 options are accepted by both `encode()` and `decode()` so the TX/RX chain can be configured symmetrically.

#### WSPRResult

```typescript
Expand Down Expand Up @@ -329,30 +358,73 @@ async function ft8Example() {
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 decodeResult = await lib.decode(WSJTXMode.FT8, encodeResult.audioData, {
frequency: audioFrequency,
threads: 1
});

const messages = lib.pullMessages();
console.log(`Decoded ${messages.length} messages`);
console.log(`Decoded ${decodeResult.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;
### Q65 Encode-Decode Cycle

```typescript
import { WSJTXLib, WSJTXMode } from 'wsjtx-lib';

async function q65Example() {
const lib = new WSJTXLib({ maxThreads: 4 });
const message = 'CQ K1ABC FN20';
const audioFrequency = 1500;

// Encode a Q65-30A frame at 12 kHz.
const encoded = await lib.encode(WSJTXMode.Q65, message, audioFrequency, {
threads: 1,
q65Period: 30,
q65Submode: 'A'
});

console.log(`Encoded: "${encoded.messageSent.trim()}"`);
console.log(`Samples: ${encoded.audioData.length}`);
console.log(`Sample rate: ${encoded.sampleRate} Hz`);

// Decode with matching Q65 period/submode and a wide enough search window.
const decoded = await lib.decode(WSJTXMode.Q65, encoded.audioData, {
frequency: audioFrequency,
txFrequency: audioFrequency,
threads: 1,
lowFreq: 0,
highFreq: 5000,
tolerance: 5000,
q65Period: 30,
q65Submode: 'A',
q65MaxDrift: 50,
q65ClearAveraging: true
});

decoded.messages.forEach((msg) => {
console.log(`Decoded: "${msg.text.trim()}" SNR=${msg.snr} dB DT=${msg.deltaTime}s Freq=${msg.deltaFrequency} Hz`);
});
}
```

For other Q65 variants, use the same API and change the period/submode pair:

```typescript
await lib.encode(WSJTXMode.Q65, 'CQ K1ABC FN20', 1500, {
q65Period: 120,
q65Submode: 'E'
});

await lib.decode(WSJTXMode.Q65, audioData, {
frequency: 1500,
q65Period: 120,
q65Submode: 'E',
q65MaxDrift: 100,
q65Averaging: true
});
```

### WSPR Decoding

```typescript
Expand Down Expand Up @@ -429,7 +501,9 @@ The library throws `WSJTXError` for all operation failures:
import { WSJTXError } from 'wsjtx-lib';

try {
await lib.decode(WSJTXMode.FT8, audioData, 1000);
await lib.decode(WSJTXMode.FT8, audioData, {
frequency: 1000
});
} catch (error) {
if (error instanceof WSJTXError) {
console.error(`WSJTX Error [${error.code}]: ${error.message}`);
Expand All @@ -453,11 +527,13 @@ try {

2. **Sample Rates**: Different modes require different sample rates. Use `lib.getSampleRate(mode)` to get the correct rate.

3. **Audio Resampling**: For optimal FT8 decoding, audio may need to be resampled from 48kHz to 12kHz. See examples for implementation.
3. **Q65 Parameters**: Q65 TX and RX must use the same period/submode pair. Supported periods are `30`, `60`, `120`, and `300`; supported submodes are `A` through `E`.

4. **Audio Resampling**: Input audio should match the sample rate expected by the selected mode. Q65 expects 12 kHz audio.

4. **Thread Safety**: Each WSJTXLib instance should be used from a single thread. Create separate instances for concurrent operations.
5. **Thread Safety**: Each WSJTXLib instance should be used from a single thread. Create separate instances for concurrent operations.

5. **Message Queue**: The `pullMessages()` method clears the internal message queue. Call it regularly to avoid memory buildup.
6. **Message Queue**: `decode()` returns decoded messages directly. `pullMessages()` is also available for compatibility with the internal message queue.

## Building from Source (Advanced)

Expand Down
Loading