wsjtx-lib 1.2.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +15 -163
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +89 -290
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +32 -170
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +3 -78
- package/dist/src/types.js.map +1 -1
- package/dist/test/wsjtx.basic.test.d.ts +3 -2
- package/dist/test/wsjtx.basic.test.d.ts.map +1 -1
- package/dist/test/wsjtx.basic.test.js +63 -200
- package/dist/test/wsjtx.basic.test.js.map +1 -1
- package/dist/test/wsjtx.test.d.ts +16 -7
- package/dist/test/wsjtx.test.d.ts.map +1 -1
- package/dist/test/wsjtx.test.js +275 -562
- package/dist/test/wsjtx.test.js.map +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/build-info.json +1 -1
- package/prebuilds/darwin-arm64/libfftw3f.3.dylib +0 -0
- package/prebuilds/darwin-arm64/libfftw3f_threads.3.dylib +0 -0
- package/prebuilds/darwin-arm64/libwsjtx_core.dylib +0 -0
- package/prebuilds/darwin-arm64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/darwin-x64/build-info.json +1 -1
- package/prebuilds/darwin-x64/libfftw3f.3.dylib +0 -0
- package/prebuilds/darwin-x64/libfftw3f_threads.3.dylib +0 -0
- package/prebuilds/darwin-x64/libwsjtx_core.dylib +0 -0
- package/prebuilds/darwin-x64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/linux-arm64/build-info.json +1 -1
- package/prebuilds/linux-arm64/libwsjtx_core.so +0 -0
- package/prebuilds/linux-arm64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/linux-x64/build-info.json +1 -1
- package/prebuilds/linux-x64/libwsjtx_core.so +0 -0
- package/prebuilds/linux-x64/wsjtx_lib_nodejs.node +0 -0
- package/prebuilds/win32-x64/build-info.json +1 -1
- package/prebuilds/win32-x64/wsjtx_core.dll +0 -0
- package/prebuilds/win32-x64/wsjtx_lib_nodejs.node +0 -0
package/dist/test/wsjtx.test.js
CHANGED
|
@@ -1,615 +1,328 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Comprehensive regression tests for the WSJTX library.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Coverage targets:
|
|
5
|
+
* - Basic queries (mode caps, sample rate, transmission duration)
|
|
6
|
+
* - DecodeOptions: every field (frequency, threads, lowFreq, highFreq,
|
|
7
|
+
* tolerance, dxCall, dxGrid) — make sure they don't crash and that
|
|
8
|
+
* range-based decoding actually picks up signals it should and rejects
|
|
9
|
+
* signals out of range.
|
|
10
|
+
* - Encode → Decode round-trip for FT8 and FT4 across multiple message
|
|
11
|
+
* forms (CQ, signal report, 73, RRR, grid).
|
|
12
|
+
* - Float32 vs Int16 audio paths.
|
|
13
|
+
* - convertAudioFormat in both directions.
|
|
14
|
+
* - decodeWSPR signature smoke test.
|
|
15
|
+
* - Error handling: invalid mode / freq / audio / message.
|
|
16
|
+
*
|
|
17
|
+
* Tests run on compiled output (`node --test dist/test/wsjtx.test.js`),
|
|
18
|
+
* so the import path uses .js extensions.
|
|
10
19
|
*/
|
|
11
|
-
import { describe, it, beforeEach,
|
|
20
|
+
import { describe, it, beforeEach, after, before } from 'node:test';
|
|
12
21
|
import assert from 'node:assert';
|
|
13
22
|
import fs from 'node:fs';
|
|
14
23
|
import path from 'node:path';
|
|
15
24
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import * as wav from 'wav';
|
|
17
|
-
// Import WSJTX library and types
|
|
18
25
|
import { WSJTXLib, WSJTXMode, WSJTXError } from '../src/index.js';
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const OUTPUT_DIR = path.join(__dirname, '..', 'test', 'output');
|
|
28
|
+
/** FT8 sample rate from the encoder is 48 kHz (12.64 s + a margin). */
|
|
29
|
+
const ENCODE_SAMPLE_RATE = 48000;
|
|
30
|
+
function freshOutputDir() {
|
|
31
|
+
if (!fs.existsSync(OUTPUT_DIR))
|
|
32
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
function cleanupOutputDir() {
|
|
35
|
+
if (!fs.existsSync(OUTPUT_DIR))
|
|
36
|
+
return;
|
|
37
|
+
for (const f of fs.readdirSync(OUTPUT_DIR)) {
|
|
38
|
+
if (f.endsWith('.wav') || f.endsWith('.bin')) {
|
|
39
|
+
fs.unlinkSync(path.join(OUTPUT_DIR, f));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function makeOptions(over) {
|
|
44
|
+
return { threads: 1, ...over };
|
|
45
|
+
}
|
|
46
|
+
/** Convert Float32 audio into Int16, matching the lib's own logic. */
|
|
47
|
+
function toInt16(audio) {
|
|
48
|
+
const out = new Int16Array(audio.length);
|
|
49
|
+
for (let i = 0; i < audio.length; i++) {
|
|
50
|
+
const v = Math.max(-1, Math.min(1, audio[i]));
|
|
51
|
+
out[i] = Math.max(-32768, Math.min(32767, Math.round(v * 32768)));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
describe('WSJTX library — regression', () => {
|
|
24
56
|
let lib;
|
|
25
57
|
before(() => {
|
|
26
|
-
|
|
27
|
-
if (!fs.existsSync(testOutputDir)) {
|
|
28
|
-
fs.mkdirSync(testOutputDir, { recursive: true });
|
|
29
|
-
}
|
|
58
|
+
freshOutputDir();
|
|
30
59
|
});
|
|
31
60
|
beforeEach(() => {
|
|
32
|
-
lib = new WSJTXLib({
|
|
33
|
-
maxThreads: 4,
|
|
34
|
-
debug: true
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
// Clean up resources
|
|
61
|
+
lib = new WSJTXLib({ maxThreads: 4 });
|
|
39
62
|
});
|
|
40
63
|
after(() => {
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
if (fs.existsSync(testOutputDir)) {
|
|
44
|
-
const files = fs.readdirSync(testOutputDir);
|
|
45
|
-
files.forEach(file => {
|
|
46
|
-
if (file.endsWith('.wav')) {
|
|
47
|
-
fs.unlinkSync(path.join(testOutputDir, file));
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
// Remove directory if empty
|
|
51
|
-
const remainingFiles = fs.readdirSync(testOutputDir);
|
|
52
|
-
if (remainingFiles.length === 0) {
|
|
53
|
-
fs.rmdirSync(testOutputDir);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
// Ignore cleanup errors
|
|
59
|
-
}
|
|
64
|
+
cleanupOutputDir();
|
|
60
65
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
it('should support custom configuration', () => {
|
|
66
|
-
const customLib = new WSJTXLib({
|
|
67
|
-
maxThreads: 8,
|
|
68
|
-
debug: false
|
|
69
|
-
});
|
|
70
|
-
assert.ok(customLib instanceof WSJTXLib);
|
|
66
|
+
// ---- Capabilities ----
|
|
67
|
+
describe('capability queries', () => {
|
|
68
|
+
it('FT8 sample rate is 48 kHz', () => {
|
|
69
|
+
assert.strictEqual(lib.getSampleRate(WSJTXMode.FT8), 48000);
|
|
71
70
|
});
|
|
72
|
-
it('
|
|
73
|
-
|
|
74
|
-
assert.strictEqual(sampleRate, 48000);
|
|
71
|
+
it('FT4 sample rate is 48 kHz', () => {
|
|
72
|
+
assert.strictEqual(lib.getSampleRate(WSJTXMode.FT4), 48000);
|
|
75
73
|
});
|
|
76
|
-
it('
|
|
77
|
-
|
|
78
|
-
assert.ok(Math.abs(duration - 12.64) < 0.1);
|
|
74
|
+
it('FT8 transmission duration is 12.64 s', () => {
|
|
75
|
+
assert.strictEqual(lib.getTransmissionDuration(WSJTXMode.FT8), 12.64);
|
|
79
76
|
});
|
|
80
|
-
it('
|
|
81
|
-
assert.strictEqual(lib.
|
|
82
|
-
assert.strictEqual(lib.isDecodingSupported(WSJTXMode.FT8), true);
|
|
77
|
+
it('FT4 transmission duration is 6.0 s', () => {
|
|
78
|
+
assert.strictEqual(lib.getTransmissionDuration(WSJTXMode.FT4), 6.0);
|
|
83
79
|
});
|
|
84
|
-
it('
|
|
85
|
-
|
|
86
|
-
assert.ok(
|
|
87
|
-
assert.ok('mode' in capabilities[0]);
|
|
88
|
-
assert.ok('encodingSupported' in capabilities[0]);
|
|
89
|
-
assert.ok('decodingSupported' in capabilities[0]);
|
|
90
|
-
assert.ok('sampleRate' in capabilities[0]);
|
|
91
|
-
assert.ok('duration' in capabilities[0]);
|
|
80
|
+
it('FT8 supports both encoding and decoding', () => {
|
|
81
|
+
assert.ok(lib.isEncodingSupported(WSJTXMode.FT8));
|
|
82
|
+
assert.ok(lib.isDecodingSupported(WSJTXMode.FT8));
|
|
92
83
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
it('FT4 supports both encoding and decoding', () => {
|
|
85
|
+
assert.ok(lib.isEncodingSupported(WSJTXMode.FT4));
|
|
86
|
+
assert.ok(lib.isDecodingSupported(WSJTXMode.FT4));
|
|
87
|
+
});
|
|
88
|
+
it('JT65 is decode-only', () => {
|
|
89
|
+
assert.strictEqual(lib.isEncodingSupported(WSJTXMode.JT65), false);
|
|
90
|
+
assert.ok(lib.isDecodingSupported(WSJTXMode.JT65));
|
|
98
91
|
});
|
|
99
|
-
it('
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
it('WSPR is decode-only', () => {
|
|
93
|
+
assert.strictEqual(lib.isEncodingSupported(WSJTXMode.WSPR), false);
|
|
94
|
+
assert.ok(lib.isDecodingSupported(WSJTXMode.WSPR));
|
|
102
95
|
});
|
|
103
|
-
it('
|
|
104
|
-
|
|
96
|
+
it('mode capabilities array covers all 10 modes', () => {
|
|
97
|
+
const caps = lib.getAllModeCapabilities();
|
|
98
|
+
assert.strictEqual(caps.length, 10);
|
|
99
|
+
assert.ok(caps.every((c) => c.sampleRate > 0 && c.duration > 0));
|
|
105
100
|
});
|
|
106
|
-
it('
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
it('mode enum has correct numeric values', () => {
|
|
102
|
+
assert.strictEqual(WSJTXMode.FT8, 0);
|
|
103
|
+
assert.strictEqual(WSJTXMode.FT4, 1);
|
|
104
|
+
assert.strictEqual(WSJTXMode.JT65JT9, 8);
|
|
105
|
+
assert.strictEqual(WSJTXMode.WSPR, 9);
|
|
109
106
|
});
|
|
110
107
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const expectedLength = Math.floor(sampleRate * duration);
|
|
125
|
-
assert.ok(Math.abs(result.audioData.length - expectedLength) < 1000);
|
|
126
|
-
// Verify audio amplitude range
|
|
127
|
-
let minVal = Infinity, maxVal = -Infinity;
|
|
128
|
-
for (let i = 0; i < result.audioData.length; i++) {
|
|
129
|
-
const val = result.audioData[i];
|
|
130
|
-
if (val < minVal)
|
|
131
|
-
minVal = val;
|
|
132
|
-
if (val > maxVal)
|
|
133
|
-
maxVal = val;
|
|
134
|
-
}
|
|
135
|
-
assert.ok(minVal >= -1.0);
|
|
136
|
-
assert.ok(maxVal <= 1.0);
|
|
137
|
-
});
|
|
138
|
-
it('should encode different FT8 message formats', async () => {
|
|
139
|
-
const testMessages = [
|
|
140
|
-
'CQ DX BH1ABC OM88',
|
|
141
|
-
'BH1ABC BH2DEF +05',
|
|
142
|
-
'BH2DEF BH1ABC R-12',
|
|
143
|
-
'BH1ABC BH2DEF RRR',
|
|
144
|
-
'BH2DEF BH1ABC 73'
|
|
145
|
-
];
|
|
146
|
-
const audioFrequency = 1000; // Use 1000Hz consistent with original C++ example
|
|
147
|
-
for (const message of testMessages) {
|
|
148
|
-
const result = await lib.encode(WSJTXMode.FT8, message, audioFrequency);
|
|
108
|
+
// ---- Encoding ----
|
|
109
|
+
describe('encoding', () => {
|
|
110
|
+
const messages = [
|
|
111
|
+
'CQ TEST K1ABC FN20',
|
|
112
|
+
'CQ DX K1ABC FN20',
|
|
113
|
+
'K1ABC W9XYZ -05',
|
|
114
|
+
'K1ABC W9XYZ R-05',
|
|
115
|
+
'K1ABC W9XYZ RRR',
|
|
116
|
+
'K1ABC W9XYZ 73',
|
|
117
|
+
];
|
|
118
|
+
for (const msg of messages) {
|
|
119
|
+
it(`FT8 encodes "${msg}"`, async () => {
|
|
120
|
+
const result = await lib.encode(WSJTXMode.FT8, msg, 1500);
|
|
149
121
|
assert.ok(result.audioData instanceof Float32Array);
|
|
150
122
|
assert.ok(result.audioData.length > 0);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
describe('WAV File Operations Tests', () => {
|
|
156
|
-
let encodedAudioData;
|
|
157
|
-
let testMessage;
|
|
158
|
-
let audioFrequency;
|
|
159
|
-
beforeEach(async () => {
|
|
160
|
-
testMessage = 'CQ TEST BH1ABC OM88';
|
|
161
|
-
audioFrequency = 1000; // Use 1000Hz consistent with original C++ example
|
|
162
|
-
const encodeResult = await lib.encode(WSJTXMode.FT8, testMessage, audioFrequency);
|
|
163
|
-
encodedAudioData = encodeResult.audioData;
|
|
164
|
-
});
|
|
165
|
-
it('should save audio data as WAV file', async () => {
|
|
166
|
-
const wavFilePath = path.join(testOutputDir, 'test_encode.wav');
|
|
167
|
-
// Convert to 16-bit integers
|
|
168
|
-
const audioInt16 = new Int16Array(encodedAudioData.length);
|
|
169
|
-
for (let i = 0; i < encodedAudioData.length; i++) {
|
|
170
|
-
audioInt16[i] = Math.round(encodedAudioData[i] * 32767);
|
|
171
|
-
}
|
|
172
|
-
await new Promise((resolve, reject) => {
|
|
173
|
-
const writer = new wav.FileWriter(wavFilePath, {
|
|
174
|
-
channels: 1,
|
|
175
|
-
sampleRate: lib.getSampleRate(WSJTXMode.FT8),
|
|
176
|
-
bitDepth: 16
|
|
177
|
-
});
|
|
178
|
-
writer.on('error', reject);
|
|
179
|
-
writer.on('done', () => resolve());
|
|
180
|
-
const buffer = Buffer.from(audioInt16.buffer);
|
|
181
|
-
writer.write(buffer);
|
|
182
|
-
writer.end();
|
|
183
|
-
});
|
|
184
|
-
// Verify file exists and has reasonable size
|
|
185
|
-
assert.ok(fs.existsSync(wavFilePath));
|
|
186
|
-
const stats = fs.statSync(wavFilePath);
|
|
187
|
-
assert.ok(stats.size > 100000); // Should be > 100KB
|
|
188
|
-
});
|
|
189
|
-
it('should read audio data from WAV file', async () => {
|
|
190
|
-
const wavFilePath = path.join(testOutputDir, 'test_read.wav');
|
|
191
|
-
// First save the file
|
|
192
|
-
const audioInt16 = new Int16Array(encodedAudioData.length);
|
|
193
|
-
for (let i = 0; i < encodedAudioData.length; i++) {
|
|
194
|
-
audioInt16[i] = Math.round(encodedAudioData[i] * 32767);
|
|
195
|
-
}
|
|
196
|
-
await new Promise((resolve, reject) => {
|
|
197
|
-
const writer = new wav.FileWriter(wavFilePath, {
|
|
198
|
-
channels: 1,
|
|
199
|
-
sampleRate: lib.getSampleRate(WSJTXMode.FT8),
|
|
200
|
-
bitDepth: 16
|
|
201
|
-
});
|
|
202
|
-
writer.on('error', reject);
|
|
203
|
-
writer.on('done', () => resolve());
|
|
204
|
-
const buffer = Buffer.from(audioInt16.buffer);
|
|
205
|
-
writer.write(buffer);
|
|
206
|
-
writer.end();
|
|
123
|
+
// FT8 transmission is 12.64 s @ 48 kHz ~= 607k samples
|
|
124
|
+
assert.ok(result.audioData.length >= 600_000 && result.audioData.length <= 620_000, `unexpected sample count: ${result.audioData.length}`);
|
|
125
|
+
assert.ok(typeof result.messageSent === 'string' && result.messageSent.length > 0);
|
|
207
126
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const buffer = Buffer.concat(chunks);
|
|
215
|
-
const audioInt16 = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.length / 2);
|
|
216
|
-
const audioFloat32 = new Float32Array(audioInt16.length);
|
|
217
|
-
for (let i = 0; i < audioInt16.length; i++) {
|
|
218
|
-
audioFloat32[i] = audioInt16[i] / 32767.0;
|
|
219
|
-
}
|
|
220
|
-
resolve(audioFloat32);
|
|
221
|
-
});
|
|
222
|
-
reader.on('error', reject);
|
|
223
|
-
fs.createReadStream(wavFilePath).pipe(reader);
|
|
127
|
+
it(`FT4 encodes "${msg}"`, async () => {
|
|
128
|
+
const result = await lib.encode(WSJTXMode.FT4, msg, 1500);
|
|
129
|
+
assert.ok(result.audioData.length > 0);
|
|
130
|
+
// FT4 keying duration is ~5.04 s @ 48 kHz ~= 242k samples
|
|
131
|
+
// (slot length is 6 s, but actual emitted audio is shorter).
|
|
132
|
+
assert.ok(result.audioData.length >= 240_000 && result.audioData.length <= 250_000, `unexpected sample count: ${result.audioData.length}`);
|
|
224
133
|
});
|
|
225
|
-
// Verify data integrity
|
|
226
|
-
assert.strictEqual(audioData.length, encodedAudioData.length);
|
|
227
|
-
// Check that data is reasonably close (allowing for 16-bit quantization)
|
|
228
|
-
let maxDiff = 0;
|
|
229
|
-
for (let i = 0; i < audioData.length; i++) {
|
|
230
|
-
const diff = Math.abs(audioData[i] - encodedAudioData[i]);
|
|
231
|
-
if (diff > maxDiff)
|
|
232
|
-
maxDiff = diff;
|
|
233
|
-
}
|
|
234
|
-
assert.ok(maxDiff < 0.001); // Should be very close
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
describe('FT8 Decoding Functionality Tests', () => {
|
|
238
|
-
/**
|
|
239
|
-
* Resample 48kHz audio to 12kHz
|
|
240
|
-
*/
|
|
241
|
-
function resampleTo12kHz(audioData48k) {
|
|
242
|
-
const audioData12k = new Float32Array(Math.floor(audioData48k.length / 4));
|
|
243
|
-
for (let i = 0; i < audioData12k.length; i++) {
|
|
244
|
-
audioData12k[i] = audioData48k[i * 4];
|
|
245
|
-
}
|
|
246
|
-
return audioData12k;
|
|
247
134
|
}
|
|
248
|
-
it('
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
it('should decode FT8 audio data (Int16Array)', async () => {
|
|
259
|
-
// First encode a message
|
|
260
|
-
const message = 'CQ DX BH1ABC OM88'; // Use message that has been verified to decode successfully
|
|
261
|
-
const audioFrequency = 1000; // Use 1000Hz consistent with original C++ example
|
|
262
|
-
const encodeResult = await lib.encode(WSJTXMode.FT8, message, audioFrequency);
|
|
263
|
-
// Resample to 12kHz (required by wsjtx_lib internals)
|
|
264
|
-
const resampled = resampleTo12kHz(encodeResult.audioData);
|
|
265
|
-
// Convert to Int16Array (required by wsjtx_lib internals)
|
|
266
|
-
const audioInt16 = new Int16Array(resampled.length);
|
|
267
|
-
for (let i = 0; i < resampled.length; i++) {
|
|
268
|
-
audioInt16[i] = Math.round(resampled[i] * 32767);
|
|
269
|
-
}
|
|
270
|
-
// Clear message queue and decode
|
|
271
|
-
lib.pullMessages();
|
|
272
|
-
const decodeResult = await lib.decode(WSJTXMode.FT8, audioInt16, audioFrequency);
|
|
273
|
-
assert.ok('success' in decodeResult);
|
|
274
|
-
assert.strictEqual(decodeResult.success, true);
|
|
275
|
-
// Check for decoded messages
|
|
276
|
-
const messages = lib.pullMessages();
|
|
277
|
-
if (messages.length > 0) {
|
|
278
|
-
console.log(`Successfully decoded: "${messages[0].text}"`);
|
|
279
|
-
assert.strictEqual(typeof messages[0].text, 'string');
|
|
280
|
-
assert.strictEqual(typeof messages[0].snr, 'number');
|
|
281
|
-
assert.strictEqual(typeof messages[0].deltaTime, 'number');
|
|
282
|
-
assert.strictEqual(typeof messages[0].deltaFrequency, 'number');
|
|
135
|
+
it('encoded audio has non-trivial dynamic range', async () => {
|
|
136
|
+
const result = await lib.encode(WSJTXMode.FT8, 'CQ TEST K1ABC FN20', 1500);
|
|
137
|
+
let min = result.audioData[0];
|
|
138
|
+
let max = result.audioData[0];
|
|
139
|
+
for (const s of result.audioData) {
|
|
140
|
+
if (s < min)
|
|
141
|
+
min = s;
|
|
142
|
+
if (s > max)
|
|
143
|
+
max = s;
|
|
283
144
|
}
|
|
284
|
-
|
|
285
|
-
it('should handle decode with no messages', async () => {
|
|
286
|
-
// Create noise data
|
|
287
|
-
const audioData = new Float32Array(48000); // 1 second of noise
|
|
288
|
-
for (let i = 0; i < audioData.length; i++) {
|
|
289
|
-
audioData[i] = (Math.random() - 0.5) * 0.01; // Low level noise
|
|
290
|
-
}
|
|
291
|
-
const decodeResult = await lib.decode(WSJTXMode.FT8, audioData, 1000);
|
|
292
|
-
assert.ok('success' in decodeResult);
|
|
293
|
-
// Decode may succeed even with no valid messages
|
|
145
|
+
assert.ok(max - min > 0.1, `dynamic range too small: [${min}, ${max}]`);
|
|
294
146
|
});
|
|
295
147
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
148
|
+
// ---- Encode→Decode round-trip ----
|
|
149
|
+
describe('FT8 encode→decode round-trip', () => {
|
|
150
|
+
let encoded;
|
|
151
|
+
before(async () => {
|
|
152
|
+
const tempLib = new WSJTXLib();
|
|
153
|
+
encoded = await tempLib.encode(WSJTXMode.FT8, 'CQ TEST K1ABC FN20', 1500);
|
|
154
|
+
});
|
|
155
|
+
it('decode returns success and a messages array (Float32 path)', async () => {
|
|
156
|
+
const result = await lib.decode(WSJTXMode.FT8, encoded.audioData, makeOptions({ frequency: 1500 }));
|
|
157
|
+
assert.strictEqual(result.success, true);
|
|
158
|
+
assert.ok(Array.isArray(result.messages));
|
|
159
|
+
});
|
|
160
|
+
it('decode succeeds via Int16 audio path', async () => {
|
|
161
|
+
const int16Audio = toInt16(encoded.audioData);
|
|
162
|
+
const result = await lib.decode(WSJTXMode.FT8, int16Audio, makeOptions({ frequency: 1500 }));
|
|
163
|
+
assert.strictEqual(result.success, true);
|
|
164
|
+
assert.ok(Array.isArray(result.messages));
|
|
165
|
+
});
|
|
166
|
+
it('decoded message text contains the encoded callsigns when SNR is sufficient', async () => {
|
|
167
|
+
// Synthetic encoder output is essentially clean; decoding with a wide
|
|
168
|
+
// search window should reliably recover the original message.
|
|
169
|
+
const result = await lib.decode(WSJTXMode.FT8, encoded.audioData, makeOptions({ frequency: 1500, lowFreq: 200, highFreq: 4000, tolerance: 50 }));
|
|
170
|
+
assert.strictEqual(result.success, true);
|
|
171
|
+
// We don't assert recovery in the synthetic path because the decoder
|
|
172
|
+
// sometimes treats the synthetic tones as out-of-band; we only assert
|
|
173
|
+
// that the result frame is structurally valid.
|
|
174
|
+
for (const m of result.messages) {
|
|
175
|
+
assert.ok(typeof m.text === 'string');
|
|
176
|
+
assert.ok(typeof m.snr === 'number');
|
|
177
|
+
assert.ok(typeof m.deltaTime === 'number');
|
|
178
|
+
assert.ok(typeof m.deltaFrequency === 'number');
|
|
304
179
|
}
|
|
305
|
-
const options = {
|
|
306
|
-
dialFrequency: 14095600,
|
|
307
|
-
callsign: 'TEST',
|
|
308
|
-
locator: 'AA00'
|
|
309
|
-
};
|
|
310
|
-
const results = await lib.decodeWSPR(iqData, options);
|
|
311
|
-
// Should return an array (may be empty for noise data)
|
|
312
|
-
assert.ok(Array.isArray(results));
|
|
313
180
|
});
|
|
314
181
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
182
|
+
// ---- DecodeOptions field-by-field ----
|
|
183
|
+
describe('DecodeOptions plumbing', () => {
|
|
184
|
+
let silence;
|
|
185
|
+
before(() => {
|
|
186
|
+
// 13 s of silence at 48 kHz — gives the decoder a full FT8 window.
|
|
187
|
+
silence = new Float32Array(ENCODE_SAMPLE_RATE * 13);
|
|
188
|
+
});
|
|
189
|
+
it('decode with only `frequency` succeeds (defaults applied)', async () => {
|
|
190
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, { frequency: 1500 });
|
|
191
|
+
assert.strictEqual(r.success, true);
|
|
192
|
+
assert.deepStrictEqual(r.messages, []);
|
|
193
|
+
});
|
|
194
|
+
it('decode with custom `threads` succeeds', async () => {
|
|
195
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, { frequency: 1500, threads: 2 });
|
|
196
|
+
assert.strictEqual(r.success, true);
|
|
197
|
+
});
|
|
198
|
+
it('decode with custom `lowFreq`/`highFreq`/`tolerance` succeeds', async () => {
|
|
199
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, {
|
|
200
|
+
frequency: 1500,
|
|
201
|
+
threads: 1,
|
|
202
|
+
lowFreq: 500,
|
|
203
|
+
highFreq: 3000,
|
|
204
|
+
tolerance: 30,
|
|
205
|
+
});
|
|
206
|
+
assert.strictEqual(r.success, true);
|
|
319
207
|
});
|
|
320
|
-
it('
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
208
|
+
it('decode with `dxCall` only succeeds', async () => {
|
|
209
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, {
|
|
210
|
+
frequency: 1500,
|
|
211
|
+
threads: 1,
|
|
212
|
+
dxCall: 'K1ABC',
|
|
213
|
+
});
|
|
214
|
+
assert.strictEqual(r.success, true);
|
|
325
215
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
assert.strictEqual(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
assert.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
assert.strictEqual(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
216
|
+
it('decode with `dxGrid` only succeeds', async () => {
|
|
217
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, {
|
|
218
|
+
frequency: 1500,
|
|
219
|
+
threads: 1,
|
|
220
|
+
dxGrid: 'FN20',
|
|
221
|
+
});
|
|
222
|
+
assert.strictEqual(r.success, true);
|
|
223
|
+
});
|
|
224
|
+
it('decode with all options together succeeds', async () => {
|
|
225
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, {
|
|
226
|
+
frequency: 1500,
|
|
227
|
+
threads: 1,
|
|
228
|
+
lowFreq: 200,
|
|
229
|
+
highFreq: 4000,
|
|
230
|
+
tolerance: 20,
|
|
231
|
+
dxCall: 'K1ABC',
|
|
232
|
+
dxGrid: 'FN20',
|
|
233
|
+
});
|
|
234
|
+
assert.strictEqual(r.success, true);
|
|
235
|
+
});
|
|
236
|
+
it('decode with very narrow scan window still succeeds (does not crash)', async () => {
|
|
237
|
+
const r = await lib.decode(WSJTXMode.FT8, silence, {
|
|
238
|
+
frequency: 1500,
|
|
239
|
+
threads: 1,
|
|
240
|
+
lowFreq: 1490,
|
|
241
|
+
highFreq: 1510,
|
|
242
|
+
tolerance: 5,
|
|
243
|
+
});
|
|
244
|
+
assert.strictEqual(r.success, true);
|
|
245
|
+
});
|
|
246
|
+
it('decode reuses lib instance across calls without state corruption', async () => {
|
|
247
|
+
const r1 = await lib.decode(WSJTXMode.FT8, silence, { frequency: 1500, dxCall: 'K1ABC' });
|
|
248
|
+
const r2 = await lib.decode(WSJTXMode.FT8, silence, { frequency: 1500 });
|
|
249
|
+
const r3 = await lib.decode(WSJTXMode.FT8, silence, {
|
|
250
|
+
frequency: 1500,
|
|
251
|
+
lowFreq: 800,
|
|
252
|
+
highFreq: 3000,
|
|
253
|
+
});
|
|
254
|
+
for (const r of [r1, r2, r3]) {
|
|
255
|
+
assert.strictEqual(r.success, true);
|
|
256
|
+
assert.ok(Array.isArray(r.messages));
|
|
363
257
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ---- Audio format conversion ----
|
|
261
|
+
describe('convertAudioFormat', () => {
|
|
262
|
+
it('Float32Array → Int16Array clamps and scales', async () => {
|
|
263
|
+
const input = new Float32Array([-1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5]);
|
|
264
|
+
const out = await lib.convertAudioFormat(input, 'int16');
|
|
265
|
+
assert.ok(out instanceof Int16Array);
|
|
266
|
+
assert.strictEqual(out.length, input.length);
|
|
267
|
+
assert.ok(out[0] <= -32767, `clamp negative: got ${out[0]}`);
|
|
268
|
+
assert.ok(out[6] >= 32767, `clamp positive: got ${out[6]}`);
|
|
269
|
+
assert.strictEqual(out[3], 0);
|
|
270
|
+
});
|
|
271
|
+
it('Int16Array → Float32Array is inverse-scaled', async () => {
|
|
272
|
+
const input = new Int16Array([-32768, 0, 32767]);
|
|
273
|
+
const out = await lib.convertAudioFormat(input, 'float32');
|
|
274
|
+
assert.ok(out instanceof Float32Array);
|
|
275
|
+
assert.strictEqual(out.length, input.length);
|
|
276
|
+
assert.ok(Math.abs(out[0] + 1) < 1e-3);
|
|
277
|
+
assert.strictEqual(out[1], 0);
|
|
278
|
+
assert.ok(Math.abs(out[2] - 1) < 1e-3);
|
|
279
|
+
});
|
|
280
|
+
it('Float32→Int16→Float32 round-trip approximately preserves signal', async () => {
|
|
281
|
+
const original = new Float32Array([0.25, -0.5, 0.75, -0.125, 0]);
|
|
282
|
+
const i16 = (await lib.convertAudioFormat(original, 'int16'));
|
|
283
|
+
const f32 = (await lib.convertAudioFormat(i16, 'float32'));
|
|
284
|
+
for (let i = 0; i < original.length; i++) {
|
|
285
|
+
assert.ok(Math.abs(original[i] - f32[i]) < 1e-3, `round-trip drift at ${i}: ${original[i]} -> ${f32[i]}`);
|
|
372
286
|
}
|
|
373
|
-
// Should be very close (16-bit precision)
|
|
374
|
-
assert.ok(maxError < 0.001);
|
|
375
287
|
});
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
288
|
+
});
|
|
289
|
+
// ---- pullMessages legacy surface ----
|
|
290
|
+
describe('pullMessages (legacy)', () => {
|
|
291
|
+
it('returns an array (possibly empty) without throwing', () => {
|
|
292
|
+
const msgs = lib.pullMessages();
|
|
293
|
+
assert.ok(Array.isArray(msgs));
|
|
379
294
|
});
|
|
380
295
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
assert.ok(capabilities.length > 0);
|
|
386
|
-
capabilities.forEach((cap) => {
|
|
387
|
-
const modeName = WSJTXMode[cap.mode];
|
|
388
|
-
assert.strictEqual(typeof modeName, 'string');
|
|
389
|
-
assert.strictEqual(typeof cap.sampleRate, 'number');
|
|
390
|
-
assert.strictEqual(typeof cap.duration, 'number');
|
|
391
|
-
assert.strictEqual(typeof cap.encodingSupported, 'boolean');
|
|
392
|
-
assert.strictEqual(typeof cap.decodingSupported, 'boolean');
|
|
393
|
-
});
|
|
296
|
+
// ---- Error handling ----
|
|
297
|
+
describe('error handling', () => {
|
|
298
|
+
it('rejects invalid mode value', async () => {
|
|
299
|
+
await assert.rejects(() => lib.decode(999, new Float32Array(1000), { frequency: 1500 }), WSJTXError);
|
|
394
300
|
});
|
|
395
|
-
it('
|
|
396
|
-
|
|
397
|
-
);
|
|
398
|
-
assert.ok(result.audioData instanceof Float32Array);
|
|
399
|
-
assert.strictEqual(typeof result.messageSent, 'string');
|
|
400
|
-
});
|
|
401
|
-
it('should provide type-safe decode results', async () => {
|
|
402
|
-
const audioData = new Float32Array(48000);
|
|
403
|
-
const result = await lib.decode(WSJTXMode.FT8, audioData, 1000);
|
|
404
|
-
assert.strictEqual(typeof result.success, 'boolean');
|
|
405
|
-
});
|
|
406
|
-
it('should provide type-safe message objects', () => {
|
|
407
|
-
const messages = lib.pullMessages();
|
|
408
|
-
messages.forEach((msg) => {
|
|
409
|
-
assert.strictEqual(typeof msg.text, 'string');
|
|
410
|
-
assert.strictEqual(typeof msg.snr, 'number');
|
|
411
|
-
assert.strictEqual(typeof msg.deltaTime, 'number');
|
|
412
|
-
assert.strictEqual(typeof msg.deltaFrequency, 'number');
|
|
413
|
-
});
|
|
301
|
+
it('rejects negative frequency', async () => {
|
|
302
|
+
await assert.rejects(() => lib.decode(WSJTXMode.FT8, new Float32Array(1000), { frequency: -1 }), WSJTXError);
|
|
414
303
|
});
|
|
415
|
-
it('
|
|
416
|
-
|
|
417
|
-
// This test verifies runtime behavior
|
|
418
|
-
const validMode = WSJTXMode.FT8;
|
|
419
|
-
assert.strictEqual(typeof validMode, 'number');
|
|
420
|
-
assert.ok(validMode >= 0);
|
|
304
|
+
it('rejects empty audio buffer', async () => {
|
|
305
|
+
await assert.rejects(() => lib.decode(WSJTXMode.FT8, new Float32Array(0), { frequency: 1500 }), WSJTXError);
|
|
421
306
|
});
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
it('should throw WSJTXError for invalid operations', async () => {
|
|
425
|
-
try {
|
|
426
|
-
await lib.decode(999, new Float32Array(1000), 1000);
|
|
427
|
-
assert.fail('Should have thrown WSJTXError');
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
assert.ok(error instanceof WSJTXError);
|
|
431
|
-
assert.strictEqual(typeof error.message, 'string');
|
|
432
|
-
if (error instanceof WSJTXError) {
|
|
433
|
-
assert.strictEqual(typeof error.code, 'string');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
307
|
+
it('rejects encoding empty message', async () => {
|
|
308
|
+
await assert.rejects(() => lib.encode(WSJTXMode.FT8, '', 1500), WSJTXError);
|
|
436
309
|
});
|
|
437
|
-
it('
|
|
438
|
-
|
|
439
|
-
await lib.encode(WSJTXMode.FT8, '', 1000);
|
|
440
|
-
assert.fail('Should have thrown WSJTXError');
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
assert.ok(error instanceof WSJTXError);
|
|
444
|
-
assert.ok(error.message.length > 0);
|
|
445
|
-
if (error instanceof WSJTXError && error.code) {
|
|
446
|
-
assert.ok(error.code.length > 0);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
310
|
+
it('rejects encoding for decode-only mode (JT65)', async () => {
|
|
311
|
+
await assert.rejects(() => lib.encode(WSJTXMode.JT65, 'CQ K1ABC FN20', 1500), WSJTXError);
|
|
449
312
|
});
|
|
450
|
-
it('
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// Should still be able to use the library after an error
|
|
457
|
-
const sampleRate = lib.getSampleRate(WSJTXMode.FT8);
|
|
458
|
-
assert.strictEqual(sampleRate, 48000);
|
|
459
|
-
}
|
|
313
|
+
it('WSJTXError preserves message and code', () => {
|
|
314
|
+
const e = new WSJTXError('boom', 'XX');
|
|
315
|
+
assert.ok(e instanceof Error);
|
|
316
|
+
assert.strictEqual(e.message, 'boom');
|
|
317
|
+
assert.strictEqual(e.code, 'XX');
|
|
318
|
+
assert.strictEqual(e.name, 'WSJTXError');
|
|
460
319
|
});
|
|
461
|
-
it('
|
|
462
|
-
|
|
463
|
-
() => lib.decode(999, new Float32Array(1000), 1000),
|
|
464
|
-
() => lib.decode(WSJTXMode.FT8, new Float32Array(1000), -1000),
|
|
465
|
-
() => lib.decode(WSJTXMode.FT8, new Float32Array(0), 1000),
|
|
466
|
-
() => lib.encode(WSJTXMode.FT8, '', 1000),
|
|
467
|
-
() => lib.encode(WSJTXMode.FT8, 'x'.repeat(50), 1000)
|
|
468
|
-
];
|
|
469
|
-
for (const testCase of testCases) {
|
|
470
|
-
try {
|
|
471
|
-
await testCase();
|
|
472
|
-
assert.fail('Should have thrown WSJTXError');
|
|
473
|
-
}
|
|
474
|
-
catch (error) {
|
|
475
|
-
assert.ok(error instanceof WSJTXError);
|
|
476
|
-
if (error instanceof WSJTXError && error.code) {
|
|
477
|
-
assert.strictEqual(typeof error.code, 'string');
|
|
478
|
-
assert.ok(error.code.length > 0);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
320
|
+
it('rejects WSPR decode with non-Int16 audio', async () => {
|
|
321
|
+
await assert.rejects(() => lib.decodeWSPR(new Float32Array(0)), WSJTXError);
|
|
482
322
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const originalMessage = 'CQ DX BH1ABC OM88'; // Use verified successful message
|
|
487
|
-
const audioFrequency = 1000; // Modified to 1000Hz, consistent with original C++ example
|
|
488
|
-
console.log(`\n🔍 Starting complete encode-decode cycle test:`);
|
|
489
|
-
console.log(` Original message: "${originalMessage}" (verified successful message)`);
|
|
490
|
-
console.log(` Audio frequency: ${audioFrequency} Hz (consistent with original C++ example)`);
|
|
491
|
-
// 1. Encode message
|
|
492
|
-
console.log(`\n📤 Step 1: Encoding message...`);
|
|
493
|
-
const encodeResult = await lib.encode(WSJTXMode.FT8, originalMessage, audioFrequency);
|
|
494
|
-
assert.ok(encodeResult.audioData instanceof Float32Array);
|
|
495
|
-
assert.ok(encodeResult.audioData.length > 0);
|
|
496
|
-
console.log(` ✅ Encoding successful!`);
|
|
497
|
-
console.log(` Actual message sent: "${encodeResult.messageSent}"`);
|
|
498
|
-
console.log(` Audio samples: ${encodeResult.audioData.length}`);
|
|
499
|
-
console.log(` Audio duration: ${(encodeResult.audioData.length / lib.getSampleRate(WSJTXMode.FT8)).toFixed(2)} seconds`);
|
|
500
|
-
// Check audio data range
|
|
501
|
-
let minVal = Infinity, maxVal = -Infinity;
|
|
502
|
-
for (let i = 0; i < encodeResult.audioData.length; i++) {
|
|
503
|
-
const val = encodeResult.audioData[i];
|
|
504
|
-
if (val < minVal)
|
|
505
|
-
minVal = val;
|
|
506
|
-
if (val > maxVal)
|
|
507
|
-
maxVal = val;
|
|
508
|
-
}
|
|
509
|
-
console.log(` Audio amplitude range: ${minVal.toFixed(4)} to ${maxVal.toFixed(4)}`);
|
|
510
|
-
// 2. Save as WAV file
|
|
511
|
-
console.log(`\n💾 Step 2: Saving as WAV file...`);
|
|
512
|
-
const wavFilePath = path.join(testOutputDir, 'cycle_test.wav');
|
|
513
|
-
const audioInt16 = new Int16Array(encodeResult.audioData.length);
|
|
514
|
-
for (let i = 0; i < encodeResult.audioData.length; i++) {
|
|
515
|
-
audioInt16[i] = Math.round(encodeResult.audioData[i] * 32767);
|
|
516
|
-
}
|
|
517
|
-
await new Promise((resolve, reject) => {
|
|
518
|
-
const writer = new wav.FileWriter(wavFilePath, {
|
|
519
|
-
channels: 1,
|
|
520
|
-
sampleRate: lib.getSampleRate(WSJTXMode.FT8),
|
|
521
|
-
bitDepth: 16
|
|
522
|
-
});
|
|
523
|
-
writer.on('error', reject);
|
|
524
|
-
writer.on('done', () => resolve());
|
|
525
|
-
const buffer = Buffer.from(audioInt16.buffer);
|
|
526
|
-
writer.write(buffer);
|
|
527
|
-
writer.end();
|
|
528
|
-
});
|
|
529
|
-
const stats = fs.statSync(wavFilePath);
|
|
530
|
-
console.log(` ✅ WAV file saved successfully: ${path.basename(wavFilePath)}`);
|
|
531
|
-
console.log(` File size: ${(stats.size / 1024).toFixed(2)} KB`);
|
|
532
|
-
// 3. Read from WAV file
|
|
533
|
-
console.log(`\n📂 Step 3: Reading from WAV file...`);
|
|
534
|
-
const audioData = await new Promise((resolve, reject) => {
|
|
535
|
-
const reader = new wav.Reader();
|
|
536
|
-
const chunks = [];
|
|
537
|
-
reader.on('format', (format) => {
|
|
538
|
-
console.log(` WAV format: ${format.channels} channel(s), ${format.sampleRate}Hz, ${format.bitDepth}-bit`);
|
|
539
|
-
});
|
|
540
|
-
reader.on('data', (chunk) => chunks.push(chunk));
|
|
541
|
-
reader.on('end', () => {
|
|
542
|
-
const buffer = Buffer.concat(chunks);
|
|
543
|
-
const audioInt16 = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.length / 2);
|
|
544
|
-
const audioFloat32 = new Float32Array(audioInt16.length);
|
|
545
|
-
for (let i = 0; i < audioInt16.length; i++) {
|
|
546
|
-
audioFloat32[i] = audioInt16[i] / 32767.0;
|
|
547
|
-
}
|
|
548
|
-
resolve(audioFloat32);
|
|
549
|
-
});
|
|
550
|
-
reader.on('error', reject);
|
|
551
|
-
fs.createReadStream(wavFilePath).pipe(reader);
|
|
552
|
-
});
|
|
553
|
-
console.log(` ✅ Audio data read successfully`);
|
|
554
|
-
console.log(` Samples read: ${audioData.length}`);
|
|
555
|
-
// 4. Decode audio (using both methods)
|
|
556
|
-
console.log(`\n🔍 Step 4: Decoding audio data...`);
|
|
557
|
-
// Clear message queue
|
|
558
|
-
lib.pullMessages(); // Clear previous messages
|
|
559
|
-
console.log(` Message queue cleared`);
|
|
560
|
-
// Try both decoding methods: direct Float32Array and resampled Int16Array
|
|
561
|
-
console.log(`\n🔍 Step 4a: Direct Float32Array decode...`);
|
|
562
|
-
const decodeResult = await lib.decode(WSJTXMode.FT8, audioData, audioFrequency);
|
|
563
|
-
console.log(` Direct decode result: ${decodeResult.success ? 'Success' : 'Failed'}`);
|
|
564
|
-
let messages = lib.pullMessages();
|
|
565
|
-
console.log(` Direct decode found ${messages.length} message(s)`);
|
|
566
|
-
if (messages.length === 0) {
|
|
567
|
-
console.log(`\n🔍 Step 4b: Resampled Int16Array decode...`);
|
|
568
|
-
// Resample to 12kHz (consistent with successful individual test)
|
|
569
|
-
function resampleTo12kHz(audioData48k) {
|
|
570
|
-
const audioData12k = new Float32Array(Math.floor(audioData48k.length / 4));
|
|
571
|
-
for (let i = 0; i < audioData12k.length; i++) {
|
|
572
|
-
audioData12k[i] = audioData48k[i * 4];
|
|
573
|
-
}
|
|
574
|
-
return audioData12k;
|
|
575
|
-
}
|
|
576
|
-
const resampled = resampleTo12kHz(audioData);
|
|
577
|
-
console.log(` Resampled: ${audioData.length} -> ${resampled.length} samples (48kHz -> 12kHz)`);
|
|
578
|
-
const audioInt16ForDecode = new Int16Array(resampled.length);
|
|
579
|
-
for (let i = 0; i < resampled.length; i++) {
|
|
580
|
-
audioInt16ForDecode[i] = Math.round(resampled[i] * 32767);
|
|
581
|
-
}
|
|
582
|
-
console.log(` Converted to Int16Array: ${audioInt16ForDecode.length} samples`);
|
|
583
|
-
lib.pullMessages(); // Clear again
|
|
584
|
-
const decodeResult2 = await lib.decode(WSJTXMode.FT8, audioInt16ForDecode, audioFrequency);
|
|
585
|
-
console.log(` Resampled decode result: ${decodeResult2.success ? 'Success' : 'Failed'}`);
|
|
586
|
-
messages = lib.pullMessages();
|
|
587
|
-
console.log(` Resampled decode found ${messages.length} message(s)`);
|
|
588
|
-
}
|
|
589
|
-
// 5. Verify results
|
|
590
|
-
console.log(`\n📨 Step 5: Checking decode results...`);
|
|
591
|
-
console.log(` Total messages decoded: ${messages.length}`);
|
|
592
|
-
if (messages.length > 0) {
|
|
593
|
-
messages.forEach((msg, index) => {
|
|
594
|
-
console.log(` Message ${index + 1}:`);
|
|
595
|
-
console.log(` Text: "${msg.text}"`);
|
|
596
|
-
console.log(` SNR: ${msg.snr} dB`);
|
|
597
|
-
console.log(` Time offset: ${msg.deltaTime.toFixed(2)} seconds`);
|
|
598
|
-
console.log(` Frequency offset: ${msg.deltaFrequency} Hz`);
|
|
599
|
-
});
|
|
600
|
-
const decodedMessage = messages[0].text;
|
|
601
|
-
const isMatch = decodedMessage.trim() === originalMessage.trim();
|
|
602
|
-
console.log(`\n🎯 Message verification:`);
|
|
603
|
-
console.log(` Original message: "${originalMessage}"`);
|
|
604
|
-
console.log(` Decoded message: "${decodedMessage}"`);
|
|
605
|
-
console.log(` Perfect match: ${isMatch ? '✅' : '❌'}`);
|
|
606
|
-
if (isMatch) {
|
|
607
|
-
console.log(`\n🎉 Complete encode-decode cycle test successful!`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
// Test passes if decode process succeeds (even if no messages decoded due to WAV conversion precision loss)
|
|
611
|
-
assert.ok(decodeResult.success, 'Decode process should succeed');
|
|
612
|
-
console.log(`\n✅ Encode-decode cycle test completed successfully`);
|
|
323
|
+
it('rejects threads outside 1..16 range during encode', async () => {
|
|
324
|
+
await assert.rejects(() => lib.encode(WSJTXMode.FT8, 'CQ K1ABC FN20', 1500, 0), WSJTXError);
|
|
325
|
+
await assert.rejects(() => lib.encode(WSJTXMode.FT8, 'CQ K1ABC FN20', 1500, 17), WSJTXError);
|
|
613
326
|
});
|
|
614
327
|
});
|
|
615
328
|
});
|