Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ else()
project(wsjtx_lib_nodejs LANGUAGES C CXX Fortran)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

Expand Down
91 changes: 79 additions & 12 deletions native/wsjtx_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ static inline wsjtx_lib* to_lib(wsjtx_handle_t h) {
return static_cast<wsjtx_lib*>(h);
}

/* Apply v2 decode options (dxCall, dxGrid, freq range) onto the lib instance.
* Empty hiscall/hisgrid leave existing dx info unchanged on the instance.
* Range fields are always applied so callers get deterministic behavior. */
static void apply_decode_options(wsjtx_lib* lib, const wsjtx_decode_options_t* opts) {
if (opts->hiscall[0]) lib->setDxCall(std::string(opts->hiscall));
if (opts->hisgrid[0]) lib->setDxGrid(std::string(opts->hisgrid));
lib->setDecodeRange(opts->low_freq, opts->high_freq, opts->tolerance);
}

/* ---- Lifecycle ---- */

WSJTX_API wsjtx_handle_t wsjtx_create(void) {
Expand All @@ -58,7 +67,7 @@ WSJTX_API void wsjtx_destroy(wsjtx_handle_t handle) {
delete to_lib(handle);
}

/* ---- Decode ---- */
/* ---- Decode (legacy) ---- */

WSJTX_API int wsjtx_decode_float(wsjtx_handle_t handle, int mode,
float* samples, int num_samples, int freq, int threads)
Expand Down Expand Up @@ -90,6 +99,44 @@ WSJTX_API int wsjtx_decode_int16(wsjtx_handle_t handle, int mode,
}
}

/* ---- Decode (v2 with options) ---- */

WSJTX_API int wsjtx_decode_float_v2(wsjtx_handle_t handle, int mode,
const float* samples, int num_samples,
const wsjtx_decode_options_t* options)
{
if (!handle || !options) return WSJTX_ERR_INVALID_HANDLE;
if (!valid_mode(mode)) return WSJTX_ERR_INVALID_MODE;

try {
wsjtx_lib* lib = to_lib(handle);
apply_decode_options(lib, options);
std::vector<float> data(samples, samples + num_samples);
lib->decode(static_cast<wsjtxMode>(mode), data, options->frequency, options->threads);
return WSJTX_OK;
} catch (...) {
return WSJTX_ERR_EXCEPTION;
}
}

WSJTX_API int wsjtx_decode_int16_v2(wsjtx_handle_t handle, int mode,
const int16_t* samples, int num_samples,
const wsjtx_decode_options_t* options)
{
if (!handle || !options) return WSJTX_ERR_INVALID_HANDLE;
if (!valid_mode(mode)) return WSJTX_ERR_INVALID_MODE;

try {
wsjtx_lib* lib = to_lib(handle);
apply_decode_options(lib, options);
std::vector<short int> data(samples, samples + num_samples);
lib->decode(static_cast<wsjtxMode>(mode), data, options->frequency, options->threads);
return WSJTX_OK;
} catch (...) {
return WSJTX_ERR_EXCEPTION;
}
}

/* ---- Encode ---- */

WSJTX_API int wsjtx_encode(wsjtx_handle_t handle, int mode, int freq,
Expand Down Expand Up @@ -126,25 +173,45 @@ WSJTX_API int wsjtx_encode(wsjtx_handle_t handle, int mode, int freq,

/* ---- Message queue ---- */

static void copy_message(wsjtx_message_t* dst, const WsjtxMessage& src) {
dst->hh = src.hh;
dst->min = src.min;
dst->sec = src.sec;
dst->snr = src.snr;
dst->freq = src.freq;
dst->sync = src.sync;
dst->dt = src.dt;
memset(dst->msg, 0, sizeof(dst->msg));
strncpy(dst->msg, src.msg.c_str(), sizeof(dst->msg) - 1);
}

WSJTX_API int wsjtx_pull_message(wsjtx_handle_t handle, wsjtx_message_t* out_msg) {
if (!handle || !out_msg) return 0;

try {
WsjtxMessage msg;
if (!to_lib(handle)->pullMessage(msg)) return 0;
copy_message(out_msg, msg);
return 1;
} catch (...) {
return 0;
}
}

out_msg->hh = msg.hh;
out_msg->min = msg.min;
out_msg->sec = msg.sec;
out_msg->snr = msg.snr;
out_msg->freq = msg.freq;
out_msg->sync = msg.sync;
out_msg->dt = msg.dt;

memset(out_msg->msg, 0, sizeof(out_msg->msg));
strncpy(out_msg->msg, msg.msg.c_str(), sizeof(out_msg->msg) - 1);
WSJTX_API int wsjtx_pull_messages(wsjtx_handle_t handle,
wsjtx_message_t* out_messages, int max_messages)
{
if (!handle || !out_messages || max_messages <= 0) return 0;

return 1;
try {
wsjtx_lib* lib = to_lib(handle);
WsjtxMessage msg;
int count = 0;
while (count < max_messages && lib->pullMessage(msg)) {
copy_message(&out_messages[count], msg);
count++;
}
return count;
} catch (...) {
return 0;
}
Expand Down
47 changes: 45 additions & 2 deletions native/wsjtx_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ typedef struct {
int cycles;
} wsjtx_decoder_result_t;

/* Decode options for v2 API.
* - frequency: nominal QSO frequency in Hz (passed as nfqso to the decoder)
* - threads: thread hint forwarded to the decoder (1..N)
* - low_freq: decoder scan low limit in Hz (default 200)
* - high_freq: decoder scan high limit in Hz (default 4000)
* - tolerance: frequency tolerance in Hz (default 20)
* - hiscall: DX callsign for AP decode (empty = none)
* - hisgrid: DX 4-char grid for AP decode (empty = none)
*/
typedef struct {
int frequency;
int threads;
int low_freq;
int high_freq;
int tolerance;
char hiscall[13];
char hisgrid[7];
} wsjtx_decode_options_t;

/* ---- Lifecycle ---- */

WSJTX_API wsjtx_handle_t wsjtx_create(void);
Expand All @@ -100,21 +119,38 @@ WSJTX_API void wsjtx_destroy(wsjtx_handle_t handle);
/* ---- Decode ---- */

/**
* Decode audio samples (float format).
* Decode audio samples (float format) — legacy API.
* Results are placed in the internal message queue; use wsjtx_pull_message() to retrieve.
* Returns WSJTX_OK on success, negative error code on failure.
*/
WSJTX_API int wsjtx_decode_float(wsjtx_handle_t handle, int mode,
float* samples, int num_samples, int freq, int threads);

/**
* Decode audio samples (int16 format).
* Decode audio samples (int16 format) — legacy API.
* Results are placed in the internal message queue; use wsjtx_pull_message() to retrieve.
* Returns WSJTX_OK on success, negative error code on failure.
*/
WSJTX_API int wsjtx_decode_int16(wsjtx_handle_t handle, int mode,
int16_t* samples, int num_samples, int freq, int threads);

/**
* Decode audio samples (float format) with full options — v2 API.
* Applies dxCall/dxGrid (for A8 list decode) and the decode frequency range
* before invoking the decoder. Results are placed in the internal queue;
* use wsjtx_pull_messages() to retrieve them in batch.
*/
WSJTX_API int wsjtx_decode_float_v2(wsjtx_handle_t handle, int mode,
const float* samples, int num_samples,
const wsjtx_decode_options_t* options);

/**
* Decode audio samples (int16 format) with full options — v2 API.
*/
WSJTX_API int wsjtx_decode_int16_v2(wsjtx_handle_t handle, int mode,
const int16_t* samples, int num_samples,
const wsjtx_decode_options_t* options);

/* ---- Encode ---- */

/**
Expand All @@ -141,6 +177,13 @@ WSJTX_API int wsjtx_encode(wsjtx_handle_t handle, int mode, int freq,
*/
WSJTX_API int wsjtx_pull_message(wsjtx_handle_t handle, wsjtx_message_t* out_msg);

/**
* Pull up to `max_messages` decoded messages from the queue in one call.
* Returns the number of messages written into `out_messages` (>= 0).
*/
WSJTX_API int wsjtx_pull_messages(wsjtx_handle_t handle,
wsjtx_message_t* out_messages, int max_messages);

/* ---- WSPR ---- */

/**
Expand Down
102 changes: 49 additions & 53 deletions native/wsjtx_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,55 +53,34 @@ namespace wsjtx_nodejs
{
Napi::Env env = info.Env();

if (info.Length() < 5)
{
Napi::TypeError::New(env, "Expected 5 arguments: mode, audioData, frequency, threads, callback")
.ThrowAsJavaScriptException();
if (info.Length() < 4) {
Napi::TypeError::New(env, "Expected: mode, audioData, options, callback").ThrowAsJavaScriptException();
return env.Null();
}

if (!info[0].IsNumber() || !info[2].IsNumber() || !info[3].IsNumber() || !info[4].IsFunction())
{
Napi::TypeError::New(env, "Invalid argument types").ThrowAsJavaScriptException();
return env.Null();
}

int mode = info[0].As<Napi::Number>().Int32Value();
int frequency = info[2].As<Napi::Number>().Int32Value();
int threads = info[3].As<Napi::Number>().Int32Value();
Napi::Function callback = info[4].As<Napi::Function>();

try {
ValidateMode(env, mode);
ValidateFrequency(env, frequency);
ValidateThreads(env, threads);
} catch (const std::exception &e) {
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
return env.Null();
}
Napi::Object optObj = info[2].As<Napi::Object>();
Napi::Function callback = info[3].As<Napi::Function>();

wsjtx_decode_options_t opts = {};
opts.frequency = optObj.Get("frequency").As<Napi::Number>().Int32Value();
opts.threads = optObj.Has("threads") ? optObj.Get("threads").As<Napi::Number>().Int32Value() : 4;
opts.low_freq = optObj.Has("lowFreq") ? optObj.Get("lowFreq").As<Napi::Number>().Int32Value() : 200;
opts.high_freq = optObj.Has("highFreq") ? optObj.Get("highFreq").As<Napi::Number>().Int32Value() : 4000;
opts.tolerance = optObj.Has("tolerance") ? optObj.Get("tolerance").As<Napi::Number>().Int32Value() : 20;
if (optObj.Has("dxCall")) { auto s = optObj.Get("dxCall").As<Napi::String>().Utf8Value(); strncpy(opts.hiscall, s.c_str(), 12); }
if (optObj.Has("dxGrid")) { auto s = optObj.Get("dxGrid").As<Napi::String>().Utf8Value(); strncpy(opts.hisgrid, s.c_str(), 6); }

Napi::Value audioData = info[1];
if (!audioData.IsTypedArray()) {
Napi::TypeError::New(env, "Audio data must be a typed array").ThrowAsJavaScriptException();
return env.Null();
}

Napi::TypedArray typedArray = audioData.As<Napi::TypedArray>();

if (typedArray.TypedArrayType() == napi_float32_array) {
auto floatData = ConvertToFloatArray(env, audioData);
auto worker = new DecodeWorker(callback, handle_, mode, floatData, frequency, threads);
worker->Queue();
auto worker = new DecodeWorker(callback, handle_, mode, floatData, opts); worker->Queue();
} else if (typedArray.TypedArrayType() == napi_int16_array) {
auto intData = ConvertToIntArray(env, audioData);
auto worker = new DecodeWorker(callback, handle_, mode, intData, frequency, threads);
worker->Queue();
auto worker = new DecodeWorker(callback, handle_, mode, intData, opts); worker->Queue();
} else {
Napi::TypeError::New(env, "Audio data must be Float32Array or Int16Array")
.ThrowAsJavaScriptException();
return env.Null();
Napi::TypeError::New(env, "Audio data must be Float32Array or Int16Array").ThrowAsJavaScriptException();
}

return env.Undefined();
}

Expand Down Expand Up @@ -387,41 +366,58 @@ namespace wsjtx_nodejs
: Napi::AsyncWorker(callback), handle_(handle) {}

// DecodeWorker (float)
DecodeWorker::DecodeWorker(Napi::Function &callback, wsjtx_handle_t handle,
int mode, const std::vector<float> &audioData,
int frequency, int threads)
: AsyncWorkerBase(callback, handle), mode_(mode), floatData_(audioData),
frequency_(frequency), threads_(threads), useFloat_(true) {}
DecodeWorker::DecodeWorker(Napi::Function &cb, wsjtx_handle_t h,
int mode, const std::vector<float> &d,
const wsjtx_decode_options_t& o)
: AsyncWorkerBase(cb, h), mode_(mode), floatData_(d),
options_(o), useFloat_(true) {}

// DecodeWorker (int16)
DecodeWorker::DecodeWorker(Napi::Function &callback, wsjtx_handle_t handle,
int mode, const std::vector<short int> &audioData,
int frequency, int threads)
: AsyncWorkerBase(callback, handle), mode_(mode), intData_(audioData),
frequency_(frequency), threads_(threads), useFloat_(false) {}
DecodeWorker::DecodeWorker(Napi::Function &cb, wsjtx_handle_t h,
int mode, const std::vector<short int> &d,
const wsjtx_decode_options_t& o)
: AsyncWorkerBase(cb, h), mode_(mode), intData_(d),
options_(o), useFloat_(false) {}

void DecodeWorker::Execute()
{
int rc;
if (useFloat_) {
rc = wsjtx_decode_float(handle_, mode_,
rc = wsjtx_decode_float_v2(handle_, mode_,
floatData_.data(), static_cast<int>(floatData_.size()),
frequency_, threads_);
&options_);
} else {
rc = wsjtx_decode_int16(handle_, mode_,
rc = wsjtx_decode_int16_v2(handle_, mode_,
reinterpret_cast<int16_t*>(intData_.data()),
static_cast<int>(intData_.size()),
frequency_, threads_);
&options_);
}
if (rc != WSJTX_OK) {
if (rc == WSJTX_OK) {
messages_.resize(MAX_MSGS);
numMessages_ = wsjtx_pull_messages(handle_, messages_.data(), MAX_MSGS);
} else {
SetError("Decode failed with error code " + std::to_string(rc));
}
}

void DecodeWorker::OnOK()
{
Napi::Env env = Env();
Callback().Call({env.Null(), Napi::Boolean::New(env, true)});
auto msgs = Napi::Array::New(env, numMessages_);
for (int i = 0; i < numMessages_; i++) {
auto o = Napi::Object::New(env);
o.Set("text", Napi::String::New(env, messages_[i].msg));
o.Set("snr", Napi::Number::New(env, messages_[i].snr));
o.Set("deltaTime", Napi::Number::New(env, messages_[i].dt));
o.Set("deltaFrequency", Napi::Number::New(env, messages_[i].freq));
o.Set("timestamp", Napi::Number::New(env, messages_[i].hh * 3600 + messages_[i].min * 60 + messages_[i].sec));
o.Set("sync", Napi::Number::New(env, messages_[i].sync));
msgs[i] = o;
}
auto result = Napi::Object::New(env);
result.Set("messages", msgs);
result.Set("success", Napi::Boolean::New(env, true));
Callback().Call({env.Null(), result});
}

// EncodeWorker
Expand Down
23 changes: 6 additions & 17 deletions native/wsjtx_wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,14 @@ class AsyncWorkerBase : public Napi::AsyncWorker {
*/
class DecodeWorker : public AsyncWorkerBase {
public:
DecodeWorker(Napi::Function& callback, wsjtx_handle_t handle,
int mode, const std::vector<float>& audioData,
int frequency, int threads);

DecodeWorker(Napi::Function& callback, wsjtx_handle_t handle,
int mode, const std::vector<short int>& audioData,
int frequency, int threads);

DecodeWorker(Napi::Function& cb, wsjtx_handle_t h, int mode, const std::vector<float>& d, const wsjtx_decode_options_t& o);
DecodeWorker(Napi::Function& cb, wsjtx_handle_t h, int mode, const std::vector<short int>& d, const wsjtx_decode_options_t& o);
protected:
void Execute() override;
void OnOK() override;

void Execute() override; void OnOK() override;
private:
int mode_;
std::vector<float> floatData_;
std::vector<short int> intData_;
bool useFloat_;
int frequency_;
int threads_;
static constexpr int MAX_MSGS = 200;
int mode_; std::vector<float> floatData_; std::vector<short int> intData_; bool useFloat_;
wsjtx_decode_options_t options_; std::vector<wsjtx_message_t> messages_; int numMessages_ = 0;
};

/**
Expand Down
Loading
Loading