Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ A high-performance Node.js C++ extension for digital amateur radio protocols, pr
| FST4W| ❌ | ✅ | 12 kHz | 120.0s | Variable |
| WSPR | ❌ | ✅ | 12 kHz | 110.6s | ~6 Hz |

MSK144 support is exposed as mode `WSJTXMode.MSK144` with 48 kHz encoded
audio, 15-second transmissions, standard 77-bit messages, and MSK144 short
message forms such as `<KA1ABC WB9XYZ> R-03`.
## Installation

### NPM Installation (Recommended)
Expand Down Expand Up @@ -247,7 +250,8 @@ enum WSJTXMode {
FST4 = 5,
Q65 = 6,
FST4W = 7,
WSPR = 8
WSPR = 9,
MSK144 = 10
}
```

Expand Down
1 change: 1 addition & 0 deletions native/wsjtx_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ static const ModeMetadata MODE_TABLE[] = {
/* FST4W */ { 12000, 120.0, 0, 1 },
/* JT65JT9 */ { 11025, 46.8, 0, 1 },
/* WSPR */ { 12000, 110.6, 0, 1 },
/* MSK144 */ { 48000, 15.0, 1, 1 },
};

static const int MODE_COUNT = sizeof(MODE_TABLE) / sizeof(MODE_TABLE[0]);
Expand Down
3 changes: 2 additions & 1 deletion native/wsjtx_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ typedef enum {
WSJTX_MODE_Q65 = 6,
WSJTX_MODE_FST4W = 7,
WSJTX_MODE_JT65JT9 = 8,
WSJTX_MODE_WSPR = 9
WSJTX_MODE_WSPR = 9,
WSJTX_MODE_MSK144 = 10
} wsjtx_mode_t;

/* Decoded message (C-compatible version of WsjtxMessage) */
Expand Down
8 changes: 5 additions & 3 deletions native/wsjtx_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ namespace wsjtx_nodejs
// ---- Helpers ----

void WSJTXLibWrapper::ValidateMode(Napi::Env env, int mode) {
if (mode < 0 || mode > WSJTX_MODE_WSPR)
if (mode < 0 || mode > WSJTX_MODE_MSK144)
throw std::invalid_argument("Invalid mode value");
}

Expand All @@ -342,7 +342,8 @@ namespace wsjtx_nodejs
throw std::invalid_argument("Message must not be empty");
}

const size_t maxLength = (mode == WSJTX_MODE_FT8 || mode == WSJTX_MODE_FT4) ? 37 : 22;
const size_t maxLength = (mode == WSJTX_MODE_FT8 || mode == WSJTX_MODE_FT4 ||
mode == WSJTX_MODE_MSK144) ? 37 : 22;
if (message.length() > maxLength) {
throw std::invalid_argument("Message must be 1-" + std::to_string(maxLength) + " characters long");
}
Expand Down Expand Up @@ -441,7 +442,8 @@ namespace wsjtx_nodejs

void EncodeWorker::Execute()
{
// FT8 at 48kHz for 12.64s = ~607,000 samples; 1M buffer is plenty
// FT8 at 48kHz for 12.64s is ~607k samples; MSK144-15 is 720k.
// 1M samples leaves room for all currently supported encoders.
static const int MAX_SAMPLES = 1024 * 1024;
audioData_.resize(MAX_SAMPLES);
int numSamples = 0;
Expand Down
21 changes: 18 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ const FREQ_MAX = 30_000_000;
const THREADS_MIN = 1;
const THREADS_MAX = 16;
const MESSAGE_MAX_LEN = 37;
const MSK144_DEFAULT_LOW_FREQ = 300;
const MSK144_DEFAULT_HIGH_FREQ = 2700;
const MSK144_DEFAULT_TOLERANCE = 100;

export class WSJTXLib {
private readonly native: NativeWSJTXLib;
Expand All @@ -103,9 +106,9 @@ export class WSJTXLib {
frequency: options.frequency,
txFrequency: options.txFrequency ?? options.frequency,
threads: options.threads ?? this.config.maxThreads,
lowFreq: options.lowFreq ?? this.config.defaultLowFreq,
highFreq: options.highFreq ?? this.config.defaultHighFreq,
tolerance: options.tolerance ?? this.config.defaultTolerance,
lowFreq: options.lowFreq ?? this.defaultLowFreq(mode),
highFreq: options.highFreq ?? this.defaultHighFreq(mode),
tolerance: options.tolerance ?? this.defaultTolerance(mode),
myCall: options.myCall ?? '',
myGrid: options.myGrid ?? '',
dxCall: options.dxCall ?? '',
Expand Down Expand Up @@ -239,6 +242,18 @@ export class WSJTXLib {
throw new WSJTXError('audioData must be a non-empty Float32Array or Int16Array', 'INVALID');
}
}

private defaultLowFreq(mode: WSJTXMode): number {
return mode === WSJTXMode.MSK144 ? MSK144_DEFAULT_LOW_FREQ : this.config.defaultLowFreq;
}

private defaultHighFreq(mode: WSJTXMode): number {
return mode === WSJTXMode.MSK144 ? MSK144_DEFAULT_HIGH_FREQ : this.config.defaultHighFreq;
}

private defaultTolerance(mode: WSJTXMode): number {
return mode === WSJTXMode.MSK144 ? MSK144_DEFAULT_TOLERANCE : this.config.defaultTolerance;
}
}

export { WSJTXMode, WSJTXError };
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum WSJTXMode {
FST4W = 7,
JT65JT9 = 8,
WSPR = 9,
MSK144 = 10,
}

export type AudioData = Float32Array | Int16Array;
Expand Down Expand Up @@ -46,6 +47,10 @@ export interface WSJTXMessage {
* - apDecode: enables FT8/FT4 AP decode passes. Defaults to true.
* - decodeDepth: WSJT-X decoder depth. Defaults to 1.
* - qsoProgress: WSJT-X QSO progress stage. Defaults to 0.
*
* MSK144 uses 1500 Hz as the nominal audio center frequency. Its decoder is
* typically used with a 300-2700 Hz passband and wider tolerance (100-200 Hz)
* than FT8/FT4; when omitted, the wrapper applies 300 / 2700 / 100 defaults.
*/
export interface DecodeOptions {
frequency: number;
Expand Down
12 changes: 10 additions & 2 deletions test/wsjtx.basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,19 @@ describe('WSJTX library — smoke', () => {
assert.strictEqual(WSJTXMode.FT4, 1);
assert.strictEqual(WSJTXMode.JT65JT9, 8);
assert.strictEqual(WSJTXMode.WSPR, 9);
assert.strictEqual(WSJTXMode.MSK144, 10);
});

it('returns capabilities for all 10 modes', () => {
it('returns capabilities for all 11 modes', () => {
const caps = lib.getAllModeCapabilities();
assert.strictEqual(caps.length, 10);
assert.strictEqual(caps.length, 11);
});

it('reports MSK144 supports both encode and decode', () => {
assert.ok(lib.isEncodingSupported(WSJTXMode.MSK144));
assert.ok(lib.isDecodingSupported(WSJTXMode.MSK144));
assert.strictEqual(lib.getSampleRate(WSJTXMode.MSK144), 48000);
assert.strictEqual(lib.getTransmissionDuration(WSJTXMode.MSK144), 15.0);
});

it('rejects invalid mode in decode', async () => {
Expand Down
44 changes: 42 additions & 2 deletions test/wsjtx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,22 @@ describe('WSJTX library — regression', () => {
assert.ok(lib.isDecodingSupported(WSJTXMode.WSPR));
});

it('mode capabilities array covers all 10 modes', () => {
it('MSK144 sample rate is 48 kHz', () => {
assert.strictEqual(lib.getSampleRate(WSJTXMode.MSK144), 48000);
});

it('MSK144 transmission duration is 15.0 s', () => {
assert.strictEqual(lib.getTransmissionDuration(WSJTXMode.MSK144), 15.0);
});

it('MSK144 supports both encoding and decoding', () => {
assert.ok(lib.isEncodingSupported(WSJTXMode.MSK144));
assert.ok(lib.isDecodingSupported(WSJTXMode.MSK144));
});

it('mode capabilities array covers all 11 modes', () => {
const caps = lib.getAllModeCapabilities();
assert.strictEqual(caps.length, 10);
assert.strictEqual(caps.length, 11);
assert.ok(caps.every((c) => c.sampleRate > 0 && c.duration > 0));
});

Expand All @@ -124,6 +137,7 @@ describe('WSJTX library — regression', () => {
assert.strictEqual(WSJTXMode.FT4, 1);
assert.strictEqual(WSJTXMode.JT65JT9, 8);
assert.strictEqual(WSJTXMode.WSPR, 9);
assert.strictEqual(WSJTXMode.MSK144, 10);
});
});

Expand Down Expand Up @@ -184,6 +198,21 @@ describe('WSJTX library — regression', () => {
assert.ok(result.audioData.length > 0);
});

it('MSK144 encodes standard messages', async () => {
const result = await lib.encode(WSJTXMode.MSK144, 'K1ABC W9XYZ EN37', 1500);
assert.strictEqual(result.messageSent.trim(), 'K1ABC W9XYZ EN37');
assert.ok(result.audioData instanceof Float32Array);
assert.ok(
result.audioData.length >= 700_000 && result.audioData.length <= 730_000,
`unexpected sample count: ${result.audioData.length}`,
);
});

it('MSK144 encodes short messages', async () => {
const result = await lib.encode(WSJTXMode.MSK144, '<KA1ABC WB9XYZ> R-03', 1500);
assert.strictEqual(result.messageSent.trim(), '<KA1ABC WB9XYZ> R-03');
assert.ok(result.audioData.length >= 700_000 && result.audioData.length <= 730_000);
});
it('rejects FT8 messages longer than 37 characters', async () => {
await assert.rejects(
() => lib.encode(WSJTXMode.FT8, 'A'.repeat(38), 1500),
Expand Down Expand Up @@ -323,6 +352,17 @@ describe('WSJTX library — regression', () => {
assert.strictEqual(r.success, true);
});

it('MSK144 silence decode succeeds with empty messages', async () => {
const silence = new Float32Array(ENCODE_SAMPLE_RATE * 15);
const r = await lib.decode(WSJTXMode.MSK144, silence, {
frequency: 1500,
threads: 1,
tolerance: 100,
decodeDepth: 1,
});
assert.strictEqual(r.success, true);
assert.deepStrictEqual(r.messages, []);
});
it('decode with very narrow scan window still succeeds (does not crash)', async () => {
const r = await lib.decode(WSJTXMode.FT8, silence, {
frequency: 1500,
Expand Down
1 change: 0 additions & 1 deletion wsjtx_lib
Submodule wsjtx_lib deleted from 623ed8
63 changes: 63 additions & 0 deletions wsjtx_lib/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
Loading