@e04/ft8ts 0.0.8 → 0.0.10
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/README.md +53 -34
- package/dist/cli.js +1747 -703
- package/dist/cli.js.map +1 -1
- package/dist/ft8ts.cjs +1756 -663
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +44 -9
- package/dist/ft8ts.mjs +1754 -663
- package/dist/ft8ts.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +18 -2
- package/src/ft4/constants.ts +3 -0
- package/src/ft4/decode.ts +977 -0
- package/src/ft4/encode.ts +45 -0
- package/src/ft4/scramble.ts +14 -0
- package/src/ft8/constants.ts +7 -0
- package/src/ft8/decode.ts +547 -298
- package/src/ft8/encode.ts +6 -5
- package/src/index.ts +6 -0
- package/src/util/constants.ts +4 -17
- package/src/util/decode174_91.ts +61 -55
- package/src/util/fft.ts +97 -37
- package/src/util/waveform.ts +97 -21
package/dist/cli.js
CHANGED
|
@@ -2,19 +2,10 @@
|
|
|
2
2
|
import { writeFileSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
+
/** Shared constants used by FT8, FT4, pack77, etc. */
|
|
5
6
|
const SAMPLE_RATE$1 = 12_000;
|
|
6
|
-
|
|
7
|
-
const NFFT1 = 2 * NSPS; // 3840
|
|
8
|
-
const NSTEP = NSPS / 4; // 480
|
|
9
|
-
const NMAX = 15 * SAMPLE_RATE$1; // 180000
|
|
10
|
-
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
11
|
-
const NDOWN = 60;
|
|
12
|
-
const NN = 79;
|
|
13
|
-
const KK = 91;
|
|
7
|
+
/** LDPC(174,91) code (shared by FT8 and FT4). */
|
|
14
8
|
const N_LDPC = 174;
|
|
15
|
-
const M_LDPC = N_LDPC - KK; // 83
|
|
16
|
-
const icos7 = [3, 1, 4, 0, 6, 5, 2];
|
|
17
|
-
const graymap = [0, 1, 3, 2, 5, 6, 4, 7];
|
|
18
9
|
const gHex = [
|
|
19
10
|
"8329ce11bf31eaf509f27fc",
|
|
20
11
|
"761c264e25c259335493132",
|
|
@@ -239,6 +230,8 @@ for (let i = 0; i < 83; i++) {
|
|
|
239
230
|
* LDPC (174,91) Belief Propagation decoder for FT8.
|
|
240
231
|
* Port of bpdecode174_91.f90 and decode174_91.f90.
|
|
241
232
|
*/
|
|
233
|
+
const KK = 91;
|
|
234
|
+
const M_LDPC = N_LDPC - KK; // 83
|
|
242
235
|
function platanh(x) {
|
|
243
236
|
if (x > 0.9999999)
|
|
244
237
|
return 18.71;
|
|
@@ -387,36 +380,48 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
387
380
|
const N = N_LDPC;
|
|
388
381
|
const K = KK;
|
|
389
382
|
const gen = getGenerator();
|
|
383
|
+
const absllr = new Float64Array(N);
|
|
384
|
+
for (let i = 0; i < N; i++)
|
|
385
|
+
absllr[i] = Math.abs(llr[i]);
|
|
390
386
|
// Sort by reliability (descending)
|
|
391
|
-
const indices = Array
|
|
392
|
-
|
|
387
|
+
const indices = new Array(N);
|
|
388
|
+
for (let i = 0; i < N; i++)
|
|
389
|
+
indices[i] = i;
|
|
390
|
+
indices.sort((a, b) => absllr[b] - absllr[a]);
|
|
393
391
|
// Reorder generator matrix columns
|
|
394
392
|
const genmrb = new Uint8Array(K * N);
|
|
395
|
-
for (let
|
|
396
|
-
|
|
397
|
-
|
|
393
|
+
for (let k = 0; k < K; k++) {
|
|
394
|
+
const row = k * N;
|
|
395
|
+
for (let i = 0; i < N; i++) {
|
|
396
|
+
genmrb[row + i] = gen[row + indices[i]];
|
|
398
397
|
}
|
|
399
398
|
}
|
|
400
399
|
// Gaussian elimination to get systematic form on the K most-reliable bits
|
|
400
|
+
const maxPivotCol = Math.min(K + 20, N);
|
|
401
401
|
for (let id = 0; id < K; id++) {
|
|
402
402
|
let found = false;
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
const idRow = id * N;
|
|
404
|
+
for (let icol = id; icol < maxPivotCol; icol++) {
|
|
405
|
+
if (genmrb[idRow + icol] === 1) {
|
|
405
406
|
if (icol !== id) {
|
|
406
407
|
// Swap columns
|
|
407
408
|
for (let k = 0; k < K; k++) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
genmrb[
|
|
409
|
+
const row = k * N;
|
|
410
|
+
const tmp = genmrb[row + id];
|
|
411
|
+
genmrb[row + id] = genmrb[row + icol];
|
|
412
|
+
genmrb[row + icol] = tmp;
|
|
411
413
|
}
|
|
412
414
|
const tmp = indices[id];
|
|
413
415
|
indices[id] = indices[icol];
|
|
414
416
|
indices[icol] = tmp;
|
|
415
417
|
}
|
|
416
418
|
for (let ii = 0; ii < K; ii++) {
|
|
417
|
-
if (ii
|
|
419
|
+
if (ii === id)
|
|
420
|
+
continue;
|
|
421
|
+
const iiRow = ii * N;
|
|
422
|
+
if (genmrb[iiRow + id] === 1) {
|
|
418
423
|
for (let c = 0; c < N; c++) {
|
|
419
|
-
genmrb[
|
|
424
|
+
genmrb[iiRow + c] ^= genmrb[idRow + c];
|
|
420
425
|
}
|
|
421
426
|
}
|
|
422
427
|
}
|
|
@@ -430,80 +435,82 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
430
435
|
// Hard decisions on reordered received word
|
|
431
436
|
const hdec = new Int8Array(N);
|
|
432
437
|
for (let i = 0; i < N; i++) {
|
|
433
|
-
|
|
438
|
+
const idx = indices[i];
|
|
439
|
+
hdec[i] = llr[idx] >= 0 ? 1 : 0;
|
|
434
440
|
}
|
|
435
441
|
const absrx = new Float64Array(N);
|
|
436
442
|
for (let i = 0; i < N; i++) {
|
|
437
|
-
absrx[i] =
|
|
443
|
+
absrx[i] = absllr[indices[i]];
|
|
438
444
|
}
|
|
439
|
-
//
|
|
440
|
-
const
|
|
445
|
+
// Encode hard decision on MRB (c0): xor selected rows of genmrb.
|
|
446
|
+
const c0 = new Int8Array(N);
|
|
441
447
|
for (let i = 0; i < K; i++) {
|
|
448
|
+
if (hdec[i] !== 1)
|
|
449
|
+
continue;
|
|
450
|
+
const row = i * N;
|
|
442
451
|
for (let j = 0; j < N; j++) {
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
function mrbencode(me) {
|
|
447
|
-
const codeword = new Int8Array(N);
|
|
448
|
-
for (let i = 0; i < K; i++) {
|
|
449
|
-
if (me[i] === 1) {
|
|
450
|
-
for (let j = 0; j < N; j++) {
|
|
451
|
-
codeword[j] ^= g2[j * K + i];
|
|
452
|
-
}
|
|
453
|
-
}
|
|
452
|
+
c0[j] ^= genmrb[row + j];
|
|
454
453
|
}
|
|
455
|
-
return codeword;
|
|
456
454
|
}
|
|
457
|
-
const m0 = hdec.slice(0, K);
|
|
458
|
-
const c0 = mrbencode(m0);
|
|
459
|
-
const bestCw = new Int8Array(c0);
|
|
460
455
|
let dmin = 0;
|
|
461
456
|
for (let i = 0; i < N; i++) {
|
|
462
457
|
const x = c0[i] ^ hdec[i];
|
|
463
458
|
dmin += x * absrx[i];
|
|
464
459
|
}
|
|
460
|
+
let bestFlip1 = -1;
|
|
461
|
+
let bestFlip2 = -1;
|
|
465
462
|
// Order-1: flip single bits in the info portion
|
|
466
463
|
for (let i1 = K - 1; i1 >= 0; i1--) {
|
|
467
464
|
if (apmask[indices[i1]] === 1)
|
|
468
465
|
continue;
|
|
469
|
-
const
|
|
470
|
-
me[i1] ^= 1;
|
|
471
|
-
const ce = mrbencode(me);
|
|
466
|
+
const row1 = i1 * N;
|
|
472
467
|
let dd = 0;
|
|
473
468
|
for (let j = 0; j < N; j++) {
|
|
474
|
-
const x =
|
|
469
|
+
const x = c0[j] ^ genmrb[row1 + j] ^ hdec[j];
|
|
475
470
|
dd += x * absrx[j];
|
|
476
471
|
}
|
|
477
472
|
if (dd < dmin) {
|
|
478
473
|
dmin = dd;
|
|
479
|
-
|
|
474
|
+
bestFlip1 = i1;
|
|
475
|
+
bestFlip2 = -1;
|
|
480
476
|
}
|
|
481
477
|
}
|
|
482
478
|
// Order-2: flip pairs of least-reliable info bits (limited search)
|
|
483
479
|
if (norder >= 2) {
|
|
484
|
-
const ntry = Math.min(
|
|
485
|
-
|
|
480
|
+
const ntry = Math.min(64, K);
|
|
481
|
+
const iMin = Math.max(0, K - ntry);
|
|
482
|
+
for (let i1 = K - 1; i1 >= iMin; i1--) {
|
|
486
483
|
if (apmask[indices[i1]] === 1)
|
|
487
484
|
continue;
|
|
488
|
-
|
|
485
|
+
const row1 = i1 * N;
|
|
486
|
+
for (let i2 = i1 - 1; i2 >= iMin; i2--) {
|
|
489
487
|
if (apmask[indices[i2]] === 1)
|
|
490
488
|
continue;
|
|
491
|
-
const
|
|
492
|
-
me[i1] ^= 1;
|
|
493
|
-
me[i2] ^= 1;
|
|
494
|
-
const ce = mrbencode(me);
|
|
489
|
+
const row2 = i2 * N;
|
|
495
490
|
let dd = 0;
|
|
496
491
|
for (let j = 0; j < N; j++) {
|
|
497
|
-
const x =
|
|
492
|
+
const x = c0[j] ^ genmrb[row1 + j] ^ genmrb[row2 + j] ^ hdec[j];
|
|
498
493
|
dd += x * absrx[j];
|
|
499
494
|
}
|
|
500
495
|
if (dd < dmin) {
|
|
501
496
|
dmin = dd;
|
|
502
|
-
|
|
497
|
+
bestFlip1 = i1;
|
|
498
|
+
bestFlip2 = i2;
|
|
503
499
|
}
|
|
504
500
|
}
|
|
505
501
|
}
|
|
506
502
|
}
|
|
503
|
+
const bestCw = new Int8Array(c0);
|
|
504
|
+
if (bestFlip1 >= 0) {
|
|
505
|
+
const row1 = bestFlip1 * N;
|
|
506
|
+
for (let j = 0; j < N; j++)
|
|
507
|
+
bestCw[j] ^= genmrb[row1 + j];
|
|
508
|
+
if (bestFlip2 >= 0) {
|
|
509
|
+
const row2 = bestFlip2 * N;
|
|
510
|
+
for (let j = 0; j < N; j++)
|
|
511
|
+
bestCw[j] ^= genmrb[row2 + j];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
507
514
|
// Reorder codeword back to original order
|
|
508
515
|
const finalCw = new Int8Array(N);
|
|
509
516
|
for (let i = 0; i < N; i++) {
|
|
@@ -514,14 +521,12 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
514
521
|
return null;
|
|
515
522
|
// Compute dmin in original order
|
|
516
523
|
let dminOrig = 0;
|
|
517
|
-
const hdecOrig = new Int8Array(N);
|
|
518
|
-
for (let i = 0; i < N; i++)
|
|
519
|
-
hdecOrig[i] = llr[i] >= 0 ? 1 : 0;
|
|
520
524
|
let nhe = 0;
|
|
521
525
|
for (let i = 0; i < N; i++) {
|
|
522
|
-
const
|
|
526
|
+
const hard = llr[i] >= 0 ? 1 : 0;
|
|
527
|
+
const x = finalCw[i] ^ hard;
|
|
523
528
|
nhe += x;
|
|
524
|
-
dminOrig += x *
|
|
529
|
+
dminOrig += x * absllr[i];
|
|
525
530
|
}
|
|
526
531
|
return {
|
|
527
532
|
message91: bits91,
|
|
@@ -566,6 +571,8 @@ function getGenerator() {
|
|
|
566
571
|
* Radix-2 Cooley-Tukey FFT for FT8 decoding.
|
|
567
572
|
* Supports real-to-complex, complex-to-complex, and inverse transforms.
|
|
568
573
|
*/
|
|
574
|
+
const RADIX2_PLAN_CACHE = new Map();
|
|
575
|
+
const BLUESTEIN_PLAN_CACHE = new Map();
|
|
569
576
|
function fftComplex(re, im, inverse) {
|
|
570
577
|
const n = re.length;
|
|
571
578
|
if (n <= 1)
|
|
@@ -574,9 +581,10 @@ function fftComplex(re, im, inverse) {
|
|
|
574
581
|
bluestein(re, im, inverse);
|
|
575
582
|
return;
|
|
576
583
|
}
|
|
584
|
+
const { bitReversed } = getRadix2Plan(n);
|
|
577
585
|
// Bit-reversal permutation
|
|
578
|
-
let j = 0;
|
|
579
586
|
for (let i = 0; i < n; i++) {
|
|
587
|
+
const j = bitReversed[i];
|
|
580
588
|
if (j > i) {
|
|
581
589
|
let tmp = re[i];
|
|
582
590
|
re[i] = re[j];
|
|
@@ -585,12 +593,6 @@ function fftComplex(re, im, inverse) {
|
|
|
585
593
|
im[i] = im[j];
|
|
586
594
|
im[j] = tmp;
|
|
587
595
|
}
|
|
588
|
-
let m = n >> 1;
|
|
589
|
-
while (m >= 1 && j >= m) {
|
|
590
|
-
j -= m;
|
|
591
|
-
m >>= 1;
|
|
592
|
-
}
|
|
593
|
-
j += m;
|
|
594
596
|
}
|
|
595
597
|
const sign = inverse ? 1 : -1;
|
|
596
598
|
for (let size = 2; size <= n; size <<= 1) {
|
|
@@ -617,53 +619,98 @@ function fftComplex(re, im, inverse) {
|
|
|
617
619
|
}
|
|
618
620
|
}
|
|
619
621
|
if (inverse) {
|
|
622
|
+
const scale = 1 / n;
|
|
620
623
|
for (let i = 0; i < n; i++) {
|
|
621
|
-
re[i]
|
|
622
|
-
im[i]
|
|
624
|
+
re[i] = re[i] * scale;
|
|
625
|
+
im[i] = im[i] * scale;
|
|
623
626
|
}
|
|
624
627
|
}
|
|
625
628
|
}
|
|
626
629
|
function bluestein(re, im, inverse) {
|
|
627
630
|
const n = re.length;
|
|
628
|
-
const m =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const aIm = new Float64Array(m);
|
|
632
|
-
const bRe = new Float64Array(m);
|
|
633
|
-
const bIm = new Float64Array(m);
|
|
631
|
+
const { m, chirpRe, chirpIm, bFftRe, bFftIm, aRe, aIm } = getBluesteinPlan(n, inverse);
|
|
632
|
+
aRe.fill(0);
|
|
633
|
+
aIm.fill(0);
|
|
634
634
|
for (let i = 0; i < n; i++) {
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
bIm[i] = -sinA;
|
|
642
|
-
}
|
|
643
|
-
for (let i = 1; i < n; i++) {
|
|
644
|
-
bRe[m - i] = bRe[i];
|
|
645
|
-
bIm[m - i] = bIm[i];
|
|
635
|
+
const cosA = chirpRe[i];
|
|
636
|
+
const sinA = chirpIm[i];
|
|
637
|
+
const inRe = re[i];
|
|
638
|
+
const inIm = im[i];
|
|
639
|
+
aRe[i] = inRe * cosA - inIm * sinA;
|
|
640
|
+
aIm[i] = inRe * sinA + inIm * cosA;
|
|
646
641
|
}
|
|
647
642
|
fftComplex(aRe, aIm, false);
|
|
648
|
-
fftComplex(bRe, bIm, false);
|
|
649
643
|
for (let i = 0; i < m; i++) {
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
644
|
+
const ar = aRe[i];
|
|
645
|
+
const ai = aIm[i];
|
|
646
|
+
const br = bFftRe[i];
|
|
647
|
+
const bi = bFftIm[i];
|
|
648
|
+
aRe[i] = ar * br - ai * bi;
|
|
649
|
+
aIm[i] = ar * bi + ai * br;
|
|
654
650
|
}
|
|
655
651
|
fftComplex(aRe, aIm, true);
|
|
656
652
|
const scale = inverse ? 1 / n : 1;
|
|
657
653
|
for (let i = 0; i < n; i++) {
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
const sinA = Math.sin(angle);
|
|
654
|
+
const cosA = chirpRe[i];
|
|
655
|
+
const sinA = chirpIm[i];
|
|
661
656
|
const r = aRe[i] * cosA - aIm[i] * sinA;
|
|
662
657
|
const iIm = aRe[i] * sinA + aIm[i] * cosA;
|
|
663
658
|
re[i] = r * scale;
|
|
664
659
|
im[i] = iIm * scale;
|
|
665
660
|
}
|
|
666
661
|
}
|
|
662
|
+
function getRadix2Plan(n) {
|
|
663
|
+
let plan = RADIX2_PLAN_CACHE.get(n);
|
|
664
|
+
if (plan)
|
|
665
|
+
return plan;
|
|
666
|
+
const bits = 31 - Math.clz32(n);
|
|
667
|
+
const bitReversed = new Uint32Array(n);
|
|
668
|
+
for (let i = 1; i < n; i++) {
|
|
669
|
+
bitReversed[i] = (bitReversed[i >> 1] >> 1) | ((i & 1) << (bits - 1));
|
|
670
|
+
}
|
|
671
|
+
plan = { bitReversed };
|
|
672
|
+
RADIX2_PLAN_CACHE.set(n, plan);
|
|
673
|
+
return plan;
|
|
674
|
+
}
|
|
675
|
+
function getBluesteinPlan(n, inverse) {
|
|
676
|
+
const key = `${n}:${inverse ? 1 : 0}`;
|
|
677
|
+
const cached = BLUESTEIN_PLAN_CACHE.get(key);
|
|
678
|
+
if (cached)
|
|
679
|
+
return cached;
|
|
680
|
+
const m = nextPow2(n * 2 - 1);
|
|
681
|
+
const s = inverse ? 1 : -1;
|
|
682
|
+
const chirpRe = new Float64Array(n);
|
|
683
|
+
const chirpIm = new Float64Array(n);
|
|
684
|
+
for (let i = 0; i < n; i++) {
|
|
685
|
+
const angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
|
|
686
|
+
chirpRe[i] = Math.cos(angle);
|
|
687
|
+
chirpIm[i] = Math.sin(angle);
|
|
688
|
+
}
|
|
689
|
+
const bFftRe = new Float64Array(m);
|
|
690
|
+
const bFftIm = new Float64Array(m);
|
|
691
|
+
for (let i = 0; i < n; i++) {
|
|
692
|
+
const cosA = chirpRe[i];
|
|
693
|
+
const sinA = chirpIm[i];
|
|
694
|
+
bFftRe[i] = cosA;
|
|
695
|
+
bFftIm[i] = -sinA;
|
|
696
|
+
}
|
|
697
|
+
for (let i = 1; i < n; i++) {
|
|
698
|
+
bFftRe[m - i] = bFftRe[i];
|
|
699
|
+
bFftIm[m - i] = bFftIm[i];
|
|
700
|
+
}
|
|
701
|
+
fftComplex(bFftRe, bFftIm, false);
|
|
702
|
+
const plan = {
|
|
703
|
+
m,
|
|
704
|
+
chirpRe,
|
|
705
|
+
chirpIm,
|
|
706
|
+
bFftRe,
|
|
707
|
+
bFftIm,
|
|
708
|
+
aRe: new Float64Array(m),
|
|
709
|
+
aIm: new Float64Array(m),
|
|
710
|
+
};
|
|
711
|
+
BLUESTEIN_PLAN_CACHE.set(key, plan);
|
|
712
|
+
return plan;
|
|
713
|
+
}
|
|
667
714
|
/** Next power of 2 >= n */
|
|
668
715
|
function nextPow2(n) {
|
|
669
716
|
let v = 1;
|
|
@@ -923,607 +970,789 @@ function unpack77(bits77, book) {
|
|
|
923
970
|
return { msg: "", success: false };
|
|
924
971
|
}
|
|
925
972
|
|
|
973
|
+
/** FT4-specific constants (lib/ft4/ft4_params.f90). */
|
|
974
|
+
const GRAYMAP = [0, 1, 3, 2];
|
|
975
|
+
|
|
976
|
+
// Message scrambling vector (rvec) from WSJT-X.
|
|
977
|
+
const RVEC = [
|
|
978
|
+
0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1,
|
|
979
|
+
0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0,
|
|
980
|
+
1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1,
|
|
981
|
+
];
|
|
982
|
+
function xorWithScrambler(bits77) {
|
|
983
|
+
const out = new Array(77);
|
|
984
|
+
for (let i = 0; i < 77; i++) {
|
|
985
|
+
out[i] = ((bits77[i] ?? 0) + RVEC[i]) & 1;
|
|
986
|
+
}
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const COSTAS_A = [0, 1, 3, 2];
|
|
991
|
+
const COSTAS_B = [1, 0, 2, 3];
|
|
992
|
+
const COSTAS_C = [2, 3, 1, 0];
|
|
993
|
+
const COSTAS_D = [3, 2, 0, 1];
|
|
994
|
+
const NSPS$1 = 576;
|
|
995
|
+
const NFFT1$1 = 4 * NSPS$1; // 2304
|
|
996
|
+
const NH1 = NFFT1$1 / 2; // 1152
|
|
997
|
+
const NMAX$1 = 21 * 3456; // 72576
|
|
998
|
+
const NHSYM$1 = Math.floor((NMAX$1 - NFFT1$1) / NSPS$1); // 122
|
|
999
|
+
const NDOWN$1 = 18;
|
|
1000
|
+
const NN$1 = 103;
|
|
1001
|
+
const NFFT2$1 = NMAX$1 / NDOWN$1; // 4032
|
|
1002
|
+
const NSS = NSPS$1 / NDOWN$1; // 32
|
|
1003
|
+
const FS2$1 = SAMPLE_RATE$1 / NDOWN$1; // 666.67 Hz
|
|
1004
|
+
const MAX_FREQ = 4910;
|
|
1005
|
+
const SYNC_PASS_MIN = 1.2;
|
|
1006
|
+
const TWO_PI$2 = 2 * Math.PI;
|
|
1007
|
+
const HARD_SYNC_PATTERNS = [
|
|
1008
|
+
{ offset: 0, bits: [0, 0, 0, 1, 1, 0, 1, 1] },
|
|
1009
|
+
{ offset: 66, bits: [0, 1, 0, 0, 1, 1, 1, 0] },
|
|
1010
|
+
{ offset: 132, bits: [1, 1, 1, 0, 0, 1, 0, 0] },
|
|
1011
|
+
{ offset: 198, bits: [1, 0, 1, 1, 0, 0, 0, 1] },
|
|
1012
|
+
];
|
|
1013
|
+
const COSTAS_BLOCKS$1 = 4;
|
|
1014
|
+
const FT4_SYNC_STRIDE = 33 * NSS;
|
|
1015
|
+
const FT4_MAX_TWEAK = 16;
|
|
1016
|
+
const LDPC_BITS = 174;
|
|
1017
|
+
const BITMETRIC_LEN = 2 * NN$1;
|
|
1018
|
+
const FRAME_LEN = NN$1 * NSS;
|
|
1019
|
+
const NUTTALL_WINDOW = makeNuttallWindow(NFFT1$1);
|
|
1020
|
+
const DOWNSAMPLE_CTX = createDownsampleContext();
|
|
1021
|
+
const TWEAKED_SYNC_TEMPLATES = createTweakedSyncTemplates();
|
|
926
1022
|
/**
|
|
927
|
-
* Decode all
|
|
928
|
-
* Input: mono audio samples at `sampleRate` Hz, duration ~
|
|
1023
|
+
* Decode all FT4 signals in a buffer.
|
|
1024
|
+
* Input: mono audio samples at `sampleRate` Hz, duration ~6s.
|
|
929
1025
|
*/
|
|
930
|
-
function decode(samples, options = {}) {
|
|
1026
|
+
function decode$1(samples, options = {}) {
|
|
931
1027
|
const sampleRate = options.sampleRate ?? SAMPLE_RATE$1;
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
const
|
|
1028
|
+
const freqLow = options.freqLow ?? 200;
|
|
1029
|
+
const freqHigh = options.freqHigh ?? 3000;
|
|
1030
|
+
const syncMin = options.syncMin ?? 1.2;
|
|
935
1031
|
const depth = options.depth ?? 2;
|
|
936
|
-
const maxCandidates = options.maxCandidates ??
|
|
1032
|
+
const maxCandidates = options.maxCandidates ?? 100;
|
|
937
1033
|
const book = options.hashCallBook;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
dd[i] = samples[i];
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
dd = resample(samples, sampleRate, SAMPLE_RATE$1, NMAX);
|
|
948
|
-
}
|
|
949
|
-
// Compute huge FFT for downsampling caching
|
|
950
|
-
const NFFT1_LONG = 192000;
|
|
951
|
-
const cxRe = new Float64Array(NFFT1_LONG);
|
|
952
|
-
const cxIm = new Float64Array(NFFT1_LONG);
|
|
953
|
-
for (let i = 0; i < NMAX; i++) {
|
|
1034
|
+
const dd = sampleRate === SAMPLE_RATE$1
|
|
1035
|
+
? copySamplesToDecodeWindow$1(samples)
|
|
1036
|
+
: resample$1(samples, sampleRate, SAMPLE_RATE$1, NMAX$1);
|
|
1037
|
+
const cxRe = new Float64Array(NMAX$1);
|
|
1038
|
+
const cxIm = new Float64Array(NMAX$1);
|
|
1039
|
+
for (let i = 0; i < NMAX$1; i++)
|
|
954
1040
|
cxRe[i] = dd[i] ?? 0;
|
|
955
|
-
}
|
|
956
1041
|
fftComplex(cxRe, cxIm, false);
|
|
957
|
-
|
|
958
|
-
|
|
1042
|
+
const candidates = getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates);
|
|
1043
|
+
if (candidates.length === 0)
|
|
1044
|
+
return [];
|
|
1045
|
+
const workspace = createDecodeWorkspace$1();
|
|
959
1046
|
const decoded = [];
|
|
960
1047
|
const seenMessages = new Set();
|
|
961
|
-
for (const
|
|
962
|
-
const
|
|
963
|
-
if (!
|
|
1048
|
+
for (const candidate of candidates) {
|
|
1049
|
+
const one = decodeCandidate(candidate, cxRe, cxIm, depth, book, workspace);
|
|
1050
|
+
if (!one)
|
|
964
1051
|
continue;
|
|
965
|
-
if (seenMessages.has(
|
|
1052
|
+
if (seenMessages.has(one.msg))
|
|
966
1053
|
continue;
|
|
967
|
-
seenMessages.add(
|
|
968
|
-
decoded.push(
|
|
969
|
-
freq: result.freq,
|
|
970
|
-
dt: result.dt - 0.5,
|
|
971
|
-
snr: result.snr,
|
|
972
|
-
msg: result.msg,
|
|
973
|
-
sync: cand.sync,
|
|
974
|
-
});
|
|
1054
|
+
seenMessages.add(one.msg);
|
|
1055
|
+
decoded.push(one);
|
|
975
1056
|
}
|
|
976
1057
|
return decoded;
|
|
977
1058
|
}
|
|
978
|
-
function
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1059
|
+
function createDecodeWorkspace$1() {
|
|
1060
|
+
return {
|
|
1061
|
+
coarseRe: new Float64Array(NFFT2$1),
|
|
1062
|
+
coarseIm: new Float64Array(NFFT2$1),
|
|
1063
|
+
fineRe: new Float64Array(NFFT2$1),
|
|
1064
|
+
fineIm: new Float64Array(NFFT2$1),
|
|
1065
|
+
frameRe: new Float64Array(FRAME_LEN),
|
|
1066
|
+
frameIm: new Float64Array(FRAME_LEN),
|
|
1067
|
+
symbRe: new Float64Array(NSS),
|
|
1068
|
+
symbIm: new Float64Array(NSS),
|
|
1069
|
+
csRe: new Float64Array(4 * NN$1),
|
|
1070
|
+
csIm: new Float64Array(4 * NN$1),
|
|
1071
|
+
s4: new Float64Array(4 * NN$1),
|
|
1072
|
+
s2: new Float64Array(1 << 8),
|
|
1073
|
+
bitmetrics1: new Float64Array(BITMETRIC_LEN),
|
|
1074
|
+
bitmetrics2: new Float64Array(BITMETRIC_LEN),
|
|
1075
|
+
bitmetrics3: new Float64Array(BITMETRIC_LEN),
|
|
1076
|
+
llra: new Float64Array(LDPC_BITS),
|
|
1077
|
+
llrb: new Float64Array(LDPC_BITS),
|
|
1078
|
+
llrc: new Float64Array(LDPC_BITS),
|
|
1079
|
+
llr: new Float64Array(LDPC_BITS),
|
|
1080
|
+
apmask: new Int8Array(LDPC_BITS),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function copySamplesToDecodeWindow$1(samples) {
|
|
1084
|
+
const out = new Float64Array(NMAX$1);
|
|
1085
|
+
const len = Math.min(samples.length, NMAX$1);
|
|
1086
|
+
for (let i = 0; i < len; i++)
|
|
1087
|
+
out[i] = samples[i];
|
|
1088
|
+
return out;
|
|
1089
|
+
}
|
|
1090
|
+
function decodeCandidate(candidate, cxRe, cxIm, depth, book, workspace) {
|
|
1091
|
+
ft4Downsample(cxRe, cxIm, candidate.freq, DOWNSAMPLE_CTX, workspace.coarseRe, workspace.coarseIm);
|
|
1092
|
+
normalizeComplexPower(workspace.coarseRe, workspace.coarseIm, NMAX$1 / NDOWN$1);
|
|
1093
|
+
for (let segment = 1; segment <= 3; segment++) {
|
|
1094
|
+
const coarse = findBestSyncLocation(workspace.coarseRe, workspace.coarseIm, segment);
|
|
1095
|
+
if (coarse.smax < SYNC_PASS_MIN)
|
|
1096
|
+
continue;
|
|
1097
|
+
const f1 = candidate.freq + coarse.idfbest;
|
|
1098
|
+
if (f1 <= 10 || f1 >= 4990)
|
|
1099
|
+
continue;
|
|
1100
|
+
ft4Downsample(cxRe, cxIm, f1, DOWNSAMPLE_CTX, workspace.fineRe, workspace.fineIm);
|
|
1101
|
+
normalizeComplexPower(workspace.fineRe, workspace.fineIm, NSS * NN$1);
|
|
1102
|
+
extractFrame(workspace.fineRe, workspace.fineIm, coarse.ibest, workspace.frameRe, workspace.frameIm);
|
|
1103
|
+
const badsync = buildBitMetrics$1(workspace.frameRe, workspace.frameIm, workspace);
|
|
1104
|
+
if (badsync)
|
|
1105
|
+
continue;
|
|
1106
|
+
if (!passesHardSyncQuality(workspace.bitmetrics1))
|
|
1107
|
+
continue;
|
|
1108
|
+
buildLlrs(workspace);
|
|
1109
|
+
const result = tryDecodePasses$1(workspace, depth);
|
|
1110
|
+
if (!result)
|
|
1111
|
+
continue;
|
|
1112
|
+
const message77Scrambled = result.message91.slice(0, 77);
|
|
1113
|
+
if (!hasNonZeroBit(message77Scrambled))
|
|
1114
|
+
continue;
|
|
1115
|
+
const message77 = xorWithScrambler(message77Scrambled);
|
|
1116
|
+
const { msg, success } = unpack77(message77, book);
|
|
1117
|
+
if (!success || msg.trim().length === 0)
|
|
1118
|
+
continue;
|
|
1119
|
+
return {
|
|
1120
|
+
freq: f1,
|
|
1121
|
+
dt: coarse.ibest / FS2$1 - 0.5,
|
|
1122
|
+
snr: toFt4Snr(candidate.sync - 1.0),
|
|
1123
|
+
msg,
|
|
1124
|
+
sync: coarse.smax,
|
|
1125
|
+
};
|
|
1004
1126
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
t0a += s[idx * NHSYM + m];
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const m36 = m + nssy * 36;
|
|
1031
|
-
if (m36 >= 0 && m36 < NHSYM && iCostas < halfSize) {
|
|
1032
|
-
tb += s[iCostas * NHSYM + m36];
|
|
1033
|
-
for (let tone = 0; tone <= 6; tone++) {
|
|
1034
|
-
const idx = i + nfos * tone;
|
|
1035
|
-
if (idx < halfSize)
|
|
1036
|
-
t0b += s[idx * NHSYM + m36];
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
const m72 = m + nssy * 72;
|
|
1040
|
-
if (m72 >= 0 && m72 < NHSYM && iCostas < halfSize) {
|
|
1041
|
-
tc += s[iCostas * NHSYM + m72];
|
|
1042
|
-
for (let tone = 0; tone <= 6; tone++) {
|
|
1043
|
-
const idx = i + nfos * tone;
|
|
1044
|
-
if (idx < halfSize)
|
|
1045
|
-
t0c += s[idx * NHSYM + m72];
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
function findBestSyncLocation(cdRe, cdIm, segment) {
|
|
1130
|
+
let ibest = -1;
|
|
1131
|
+
let idfbest = 0;
|
|
1132
|
+
let smax = -99;
|
|
1133
|
+
for (let isync = 1; isync <= 2; isync++) {
|
|
1134
|
+
let idfmin;
|
|
1135
|
+
let idfmax;
|
|
1136
|
+
let idfstp;
|
|
1137
|
+
let ibmin;
|
|
1138
|
+
let ibmax;
|
|
1139
|
+
let ibstp;
|
|
1140
|
+
if (isync === 1) {
|
|
1141
|
+
idfmin = -12;
|
|
1142
|
+
idfmax = 12;
|
|
1143
|
+
idfstp = 3;
|
|
1144
|
+
ibmin = -344;
|
|
1145
|
+
ibmax = 1012;
|
|
1146
|
+
if (segment === 1) {
|
|
1147
|
+
ibmin = 108;
|
|
1148
|
+
ibmax = 560;
|
|
1048
1149
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const syncVal = t0 > 0 ? t / t0 : 0;
|
|
1053
|
-
const tbc = tb + tc;
|
|
1054
|
-
const t0bc = t0b + t0c;
|
|
1055
|
-
const t0bc2 = (t0bc - tbc) / 6.0;
|
|
1056
|
-
const syncBc = t0bc2 > 0 ? tbc / t0bc2 : 0;
|
|
1057
|
-
sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
// Find peaks
|
|
1061
|
-
const candidates0 = [];
|
|
1062
|
-
const mlag = 10;
|
|
1063
|
-
for (let i = ia; i <= ib; i++) {
|
|
1064
|
-
let bestSync = -1;
|
|
1065
|
-
let bestJ = 0;
|
|
1066
|
-
for (let j = -mlag; j <= mlag; j++) {
|
|
1067
|
-
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
1068
|
-
if (v > bestSync) {
|
|
1069
|
-
bestSync = v;
|
|
1070
|
-
bestJ = j;
|
|
1150
|
+
else if (segment === 2) {
|
|
1151
|
+
ibmin = 560;
|
|
1152
|
+
ibmax = 1012;
|
|
1071
1153
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
let bestJ2 = 0;
|
|
1076
|
-
for (let j = -JZ; j <= JZ; j++) {
|
|
1077
|
-
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
1078
|
-
if (v > bestSync2) {
|
|
1079
|
-
bestSync2 = v;
|
|
1080
|
-
bestJ2 = j;
|
|
1154
|
+
else {
|
|
1155
|
+
ibmin = -344;
|
|
1156
|
+
ibmax = 108;
|
|
1081
1157
|
}
|
|
1158
|
+
ibstp = 4;
|
|
1082
1159
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
|
|
1102
|
-
const base = syncValues[pctileIdx] ?? 1;
|
|
1103
|
-
if (base > 0) {
|
|
1104
|
-
for (const c of candidates0)
|
|
1105
|
-
c.sync /= base;
|
|
1106
|
-
}
|
|
1107
|
-
// Remove near-duplicate candidates
|
|
1108
|
-
for (let i = 0; i < candidates0.length; i++) {
|
|
1109
|
-
for (let j = 0; j < i; j++) {
|
|
1110
|
-
const fdiff = Math.abs(candidates0[i].freq - candidates0[j].freq);
|
|
1111
|
-
const tdiff = Math.abs(candidates0[i].dt - candidates0[j].dt);
|
|
1112
|
-
if (fdiff < 4.0 && tdiff < 0.04) {
|
|
1113
|
-
if (candidates0[i].sync >= candidates0[j].sync) {
|
|
1114
|
-
candidates0[j].sync = 0;
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
candidates0[i].sync = 0;
|
|
1160
|
+
else {
|
|
1161
|
+
idfmin = idfbest - 4;
|
|
1162
|
+
idfmax = idfbest + 4;
|
|
1163
|
+
idfstp = 1;
|
|
1164
|
+
ibmin = ibest - 5;
|
|
1165
|
+
ibmax = ibest + 5;
|
|
1166
|
+
ibstp = 1;
|
|
1167
|
+
}
|
|
1168
|
+
for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
|
|
1169
|
+
const templates = TWEAKED_SYNC_TEMPLATES.get(idf);
|
|
1170
|
+
if (!templates)
|
|
1171
|
+
continue;
|
|
1172
|
+
for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
|
|
1173
|
+
const sync = sync4d(cdRe, cdIm, istart, templates);
|
|
1174
|
+
if (sync > smax) {
|
|
1175
|
+
smax = sync;
|
|
1176
|
+
ibest = istart;
|
|
1177
|
+
idfbest = idf;
|
|
1118
1178
|
}
|
|
1119
1179
|
}
|
|
1120
1180
|
}
|
|
1121
1181
|
}
|
|
1122
|
-
|
|
1123
|
-
const filtered = candidates0.filter((c) => c.sync >= syncmin);
|
|
1124
|
-
filtered.sort((a, b) => b.sync - a.sync);
|
|
1125
|
-
return { candidates: filtered.slice(0, maxcand), sbase };
|
|
1182
|
+
return { ibest, idfbest, smax };
|
|
1126
1183
|
}
|
|
1127
|
-
function
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1184
|
+
function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
1185
|
+
const df = SAMPLE_RATE$1 / NFFT1$1;
|
|
1186
|
+
const fac = 1 / 300;
|
|
1187
|
+
const savg = new Float64Array(NH1);
|
|
1188
|
+
const s = new Float64Array(NH1 * NHSYM$1);
|
|
1189
|
+
const savsm = new Float64Array(NH1);
|
|
1190
|
+
const xRe = new Float64Array(NFFT1$1);
|
|
1191
|
+
const xIm = new Float64Array(NFFT1$1);
|
|
1192
|
+
for (let j = 0; j < NHSYM$1; j++) {
|
|
1193
|
+
const ia = j * NSPS$1;
|
|
1194
|
+
const ib = ia + NFFT1$1;
|
|
1195
|
+
if (ib > NMAX$1)
|
|
1196
|
+
break;
|
|
1197
|
+
xIm.fill(0);
|
|
1198
|
+
for (let i = 0; i < NFFT1$1; i++)
|
|
1199
|
+
xRe[i] = fac * dd[ia + i] * NUTTALL_WINDOW[i];
|
|
1200
|
+
fftComplex(xRe, xIm, false);
|
|
1201
|
+
for (let bin = 1; bin <= NH1; bin++) {
|
|
1202
|
+
const idx = bin - 1;
|
|
1203
|
+
const re = xRe[bin] ?? 0;
|
|
1204
|
+
const im = xIm[bin] ?? 0;
|
|
1205
|
+
const power = re * re + im * im;
|
|
1206
|
+
s[idx * NHSYM$1 + j] = power;
|
|
1207
|
+
savg[idx] = (savg[idx] ?? 0) + power;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (let i = 0; i < NH1; i++)
|
|
1211
|
+
savg[i] = (savg[i] ?? 0) / NHSYM$1;
|
|
1212
|
+
for (let i = 7; i < NH1 - 7; i++) {
|
|
1134
1213
|
let sum = 0;
|
|
1135
|
-
let
|
|
1136
|
-
const lo = Math.max(ia, i - window);
|
|
1137
|
-
const hi = Math.min(ib, i + window);
|
|
1138
|
-
for (let j = lo; j <= hi; j++) {
|
|
1214
|
+
for (let j = i - 7; j <= i + 7; j++)
|
|
1139
1215
|
sum += savg[j];
|
|
1140
|
-
|
|
1216
|
+
savsm[i] = sum / 15;
|
|
1217
|
+
}
|
|
1218
|
+
let nfa = Math.round(freqLow / df);
|
|
1219
|
+
if (nfa < Math.round(200 / df))
|
|
1220
|
+
nfa = Math.round(200 / df);
|
|
1221
|
+
let nfb = Math.round(freqHigh / df);
|
|
1222
|
+
if (nfb > Math.round(MAX_FREQ / df))
|
|
1223
|
+
nfb = Math.round(MAX_FREQ / df);
|
|
1224
|
+
const sbase = ft4Baseline(savg, nfa, nfb, df);
|
|
1225
|
+
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1226
|
+
if ((sbase[bin - 1] ?? 0) <= 0)
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1230
|
+
const idx = bin - 1;
|
|
1231
|
+
savsm[idx] = (savsm[idx] ?? 0) / sbase[idx];
|
|
1232
|
+
}
|
|
1233
|
+
const fOffset = (-1.5 * SAMPLE_RATE$1) / NSPS$1;
|
|
1234
|
+
const candidates = [];
|
|
1235
|
+
for (let i = nfa + 1; i <= nfb - 1; i++) {
|
|
1236
|
+
const left = savsm[i - 2] ?? 0;
|
|
1237
|
+
const center = savsm[i - 1] ?? 0;
|
|
1238
|
+
const right = savsm[i] ?? 0;
|
|
1239
|
+
if (center >= left && center >= right && center >= syncMin) {
|
|
1240
|
+
const den = left - 2 * center + right;
|
|
1241
|
+
const del = den !== 0 ? (0.5 * (left - right)) / den : 0;
|
|
1242
|
+
const fpeak = (i + del) * df + fOffset;
|
|
1243
|
+
if (fpeak < 200 || fpeak > MAX_FREQ)
|
|
1244
|
+
continue;
|
|
1245
|
+
const speak = center - 0.25 * (left - right) * del;
|
|
1246
|
+
candidates.push({ freq: fpeak, sync: speak });
|
|
1141
1247
|
}
|
|
1142
|
-
sbase[i] = count > 0 ? 10 * Math.log10(Math.max(1e-30, sum / count)) : 0;
|
|
1143
1248
|
}
|
|
1144
|
-
|
|
1249
|
+
candidates.sort((a, b) => b.sync - a.sync);
|
|
1250
|
+
return candidates.slice(0, maxCandidates);
|
|
1145
1251
|
}
|
|
1146
|
-
function
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
let smax = 0;
|
|
1159
|
-
let ibest = i0;
|
|
1160
|
-
for (let idt = i0 - 10; idt <= i0 + 10; idt++) {
|
|
1161
|
-
const sync = sync8d(cd0Re, cd0Im, idt, null, null, false);
|
|
1162
|
-
if (sync > smax) {
|
|
1163
|
-
smax = sync;
|
|
1164
|
-
ibest = idt;
|
|
1165
|
-
}
|
|
1252
|
+
function makeNuttallWindow(n) {
|
|
1253
|
+
const out = new Float64Array(n);
|
|
1254
|
+
const a0 = 0.3635819;
|
|
1255
|
+
const a1 = -0.4891775;
|
|
1256
|
+
const a2 = 0.1365995;
|
|
1257
|
+
const a3 = -0.0106411;
|
|
1258
|
+
for (let i = 0; i < n; i++) {
|
|
1259
|
+
out[i] =
|
|
1260
|
+
a0 +
|
|
1261
|
+
a1 * Math.cos((2 * Math.PI * i) / n) +
|
|
1262
|
+
a2 * Math.cos((4 * Math.PI * i) / n) +
|
|
1263
|
+
a3 * Math.cos((6 * Math.PI * i) / n);
|
|
1166
1264
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1265
|
+
return out;
|
|
1266
|
+
}
|
|
1267
|
+
function ft4Baseline(savg, nfa, nfb, df) {
|
|
1268
|
+
const sbase = new Float64Array(NH1);
|
|
1269
|
+
sbase.fill(1);
|
|
1270
|
+
const ia = Math.max(Math.round(200 / df), nfa);
|
|
1271
|
+
const ib = Math.min(NH1, nfb);
|
|
1272
|
+
if (ib <= ia)
|
|
1273
|
+
return sbase;
|
|
1274
|
+
const sDb = new Float64Array(NH1);
|
|
1275
|
+
for (let i = ia; i <= ib; i++)
|
|
1276
|
+
sDb[i - 1] = 10 * Math.log10(Math.max(1e-30, savg[i - 1]));
|
|
1277
|
+
const nseg = 10;
|
|
1278
|
+
const npct = 10;
|
|
1279
|
+
const nlen = Math.max(1, Math.trunc((ib - ia + 1) / nseg));
|
|
1280
|
+
const i0 = Math.trunc((ib - ia + 1) / 2);
|
|
1281
|
+
const x = [];
|
|
1282
|
+
const y = [];
|
|
1283
|
+
for (let seg = 0; seg < nseg; seg++) {
|
|
1284
|
+
const ja = ia + seg * nlen;
|
|
1285
|
+
if (ja > ib)
|
|
1286
|
+
break;
|
|
1287
|
+
const jb = Math.min(ib, ja + nlen - 1);
|
|
1288
|
+
const vals = [];
|
|
1289
|
+
for (let i = ja; i <= jb; i++)
|
|
1290
|
+
vals.push(sDb[i - 1]);
|
|
1291
|
+
const base = percentile(vals, npct);
|
|
1292
|
+
for (let i = ja; i <= jb; i++) {
|
|
1293
|
+
const v = sDb[i - 1];
|
|
1294
|
+
if (v <= base) {
|
|
1295
|
+
x.push(i - i0);
|
|
1296
|
+
y.push(v);
|
|
1297
|
+
}
|
|
1185
1298
|
}
|
|
1186
1299
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt, null, null, false);
|
|
1194
|
-
}
|
|
1195
|
-
let maxss = -1;
|
|
1196
|
-
let maxIdx = 4;
|
|
1197
|
-
for (let i = 0; i < 9; i++) {
|
|
1198
|
-
if (ss[i] > maxss) {
|
|
1199
|
-
maxss = ss[i];
|
|
1200
|
-
maxIdx = i;
|
|
1300
|
+
const coeff = x.length >= 5 ? polyfitLeastSquares(x, y, 4) : null;
|
|
1301
|
+
if (coeff) {
|
|
1302
|
+
for (let i = ia; i <= ib; i++) {
|
|
1303
|
+
const t = i - i0;
|
|
1304
|
+
const db = coeff[0] + t * (coeff[1] + t * (coeff[2] + t * (coeff[3] + t * coeff[4]))) + 0.65;
|
|
1305
|
+
sbase[i - 1] = 10 ** (db / 10);
|
|
1201
1306
|
}
|
|
1202
1307
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
symbRe.fill(0);
|
|
1214
|
-
symbIm.fill(0);
|
|
1215
|
-
if (i1 >= 0 && i1 + 31 < NP2) {
|
|
1216
|
-
for (let j = 0; j < 32; j++) {
|
|
1217
|
-
symbRe[j] = cd0Re[i1 + j];
|
|
1218
|
-
symbIm[j] = cd0Im[i1 + j];
|
|
1308
|
+
else {
|
|
1309
|
+
const halfWindow = 25;
|
|
1310
|
+
for (let i = ia; i <= ib; i++) {
|
|
1311
|
+
const lo = Math.max(ia, i - halfWindow);
|
|
1312
|
+
const hi = Math.min(ib, i + halfWindow);
|
|
1313
|
+
let sum = 0;
|
|
1314
|
+
let count = 0;
|
|
1315
|
+
for (let j = lo; j <= hi; j++) {
|
|
1316
|
+
sum += savg[j - 1];
|
|
1317
|
+
count++;
|
|
1219
1318
|
}
|
|
1220
|
-
|
|
1221
|
-
fftComplex(symbRe, symbIm, false);
|
|
1222
|
-
for (let tone = 0; tone < 8; tone++) {
|
|
1223
|
-
const re = symbRe[tone] / 1000;
|
|
1224
|
-
const im = symbIm[tone] / 1000;
|
|
1225
|
-
csRe[tone * NN + k] = re;
|
|
1226
|
-
csIm[tone * NN + k] = im;
|
|
1227
|
-
s8[tone * NN + k] = Math.sqrt(re * re + im * im);
|
|
1319
|
+
sbase[i - 1] = count > 0 ? sum / count : 1;
|
|
1228
1320
|
}
|
|
1229
1321
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1322
|
+
return sbase;
|
|
1323
|
+
}
|
|
1324
|
+
function percentile(values, pct) {
|
|
1325
|
+
if (values.length === 0)
|
|
1326
|
+
return 0;
|
|
1327
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1328
|
+
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((pct / 100) * (sorted.length - 1))));
|
|
1329
|
+
return sorted[idx];
|
|
1330
|
+
}
|
|
1331
|
+
function polyfitLeastSquares(x, y, degree) {
|
|
1332
|
+
const n = degree + 1;
|
|
1333
|
+
const mat = Array.from({ length: n }, () => new Float64Array(n + 1));
|
|
1334
|
+
const xPows = new Float64Array(2 * degree + 1);
|
|
1335
|
+
for (let p = 0; p <= 2 * degree; p++) {
|
|
1336
|
+
let sum = 0;
|
|
1337
|
+
for (let i = 0; i < x.length; i++)
|
|
1338
|
+
sum += x[i] ** p;
|
|
1339
|
+
xPows[p] = sum;
|
|
1340
|
+
}
|
|
1341
|
+
for (let row = 0; row < n; row++) {
|
|
1342
|
+
for (let col = 0; col < n; col++)
|
|
1343
|
+
mat[row][col] = xPows[row + col];
|
|
1344
|
+
let rhs = 0;
|
|
1345
|
+
for (let i = 0; i < x.length; i++)
|
|
1346
|
+
rhs += y[i] * x[i] ** row;
|
|
1347
|
+
mat[row][n] = rhs;
|
|
1348
|
+
}
|
|
1349
|
+
for (let col = 0; col < n; col++) {
|
|
1350
|
+
let pivot = col;
|
|
1351
|
+
let maxAbs = Math.abs(mat[col][col]);
|
|
1352
|
+
for (let row = col + 1; row < n; row++) {
|
|
1353
|
+
const a = Math.abs(mat[row][col]);
|
|
1354
|
+
if (a > maxAbs) {
|
|
1355
|
+
maxAbs = a;
|
|
1356
|
+
pivot = row;
|
|
1242
1357
|
}
|
|
1243
|
-
if (maxTone === icos7[k])
|
|
1244
|
-
nsync++;
|
|
1245
1358
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const i2 = Math.floor((i & 63) / 8);
|
|
1265
|
-
const i3 = i & 7;
|
|
1266
|
-
if (nsym === 1) {
|
|
1267
|
-
const re = csRe[graymap[i3] * NN + ks - 1];
|
|
1268
|
-
const im = csIm[graymap[i3] * NN + ks - 1];
|
|
1269
|
-
s2[i] = Math.sqrt(re * re + im * im);
|
|
1270
|
-
}
|
|
1271
|
-
else if (nsym === 2) {
|
|
1272
|
-
const sRe = csRe[graymap[i2] * NN + ks - 1] + csRe[graymap[i3] * NN + ks];
|
|
1273
|
-
const sIm = csIm[graymap[i2] * NN + ks - 1] + csIm[graymap[i3] * NN + ks];
|
|
1274
|
-
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
1275
|
-
}
|
|
1276
|
-
else {
|
|
1277
|
-
const sRe = csRe[graymap[i1] * NN + ks - 1] +
|
|
1278
|
-
csRe[graymap[i2] * NN + ks] +
|
|
1279
|
-
csRe[graymap[i3] * NN + ks + 1];
|
|
1280
|
-
const sIm = csIm[graymap[i1] * NN + ks - 1] +
|
|
1281
|
-
csIm[graymap[i2] * NN + ks] +
|
|
1282
|
-
csIm[graymap[i3] * NN + ks + 1];
|
|
1283
|
-
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
// Fortran: i32 = 1 + (k-1)*3 + (ihalf-1)*87 (1-based)
|
|
1287
|
-
const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
|
|
1288
|
-
for (let ib = 0; ib <= ibmax; ib++) {
|
|
1289
|
-
// max of s2 where bit (ibmax-ib) of index is 1
|
|
1290
|
-
let max1 = -1e30, max0 = -1e30;
|
|
1291
|
-
for (let i = 0; i < nt; i++) {
|
|
1292
|
-
const bitSet = (i & (1 << (ibmax - ib))) !== 0;
|
|
1293
|
-
if (bitSet) {
|
|
1294
|
-
if (s2[i] > max1)
|
|
1295
|
-
max1 = s2[i];
|
|
1296
|
-
}
|
|
1297
|
-
else {
|
|
1298
|
-
if (s2[i] > max0)
|
|
1299
|
-
max0 = s2[i];
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
const idx = i32 + ib - 1; // Convert to 0-based
|
|
1303
|
-
if (idx >= 0 && idx < N_LDPC) {
|
|
1304
|
-
const bm = max1 - max0;
|
|
1305
|
-
if (nsym === 1) {
|
|
1306
|
-
bmeta[idx] = bm;
|
|
1307
|
-
const den = Math.max(max1, max0);
|
|
1308
|
-
bmetd[idx] = den > 0 ? bm / den : 0;
|
|
1309
|
-
}
|
|
1310
|
-
else if (nsym === 2) {
|
|
1311
|
-
bmetb[idx] = bm;
|
|
1312
|
-
}
|
|
1313
|
-
else {
|
|
1314
|
-
bmetc[idx] = bm;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1359
|
+
if (maxAbs < 1e-12)
|
|
1360
|
+
return null;
|
|
1361
|
+
if (pivot !== col) {
|
|
1362
|
+
const tmp = mat[col];
|
|
1363
|
+
mat[col] = mat[pivot];
|
|
1364
|
+
mat[pivot] = tmp;
|
|
1365
|
+
}
|
|
1366
|
+
const pivotVal = mat[col][col];
|
|
1367
|
+
for (let c = col; c <= n; c++)
|
|
1368
|
+
mat[col][c] = mat[col][c] / pivotVal;
|
|
1369
|
+
for (let row = 0; row < n; row++) {
|
|
1370
|
+
if (row === col)
|
|
1371
|
+
continue;
|
|
1372
|
+
const factor = mat[row][col];
|
|
1373
|
+
if (factor === 0)
|
|
1374
|
+
continue;
|
|
1375
|
+
for (let c = col; c <= n; c++)
|
|
1376
|
+
mat[row][c] = mat[row][c] - factor * mat[col][c];
|
|
1319
1377
|
}
|
|
1320
1378
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1379
|
+
const coeff = new Array(n);
|
|
1380
|
+
for (let i = 0; i < n; i++)
|
|
1381
|
+
coeff[i] = mat[i][n];
|
|
1382
|
+
return coeff;
|
|
1383
|
+
}
|
|
1384
|
+
function createDownsampleContext() {
|
|
1385
|
+
const df = SAMPLE_RATE$1 / NMAX$1;
|
|
1386
|
+
const baud = SAMPLE_RATE$1 / NSPS$1;
|
|
1387
|
+
const bwTransition = 0.5 * baud;
|
|
1388
|
+
const bwFlat = 4 * baud;
|
|
1389
|
+
const iwt = Math.max(1, Math.trunc(bwTransition / df));
|
|
1390
|
+
const iwf = Math.max(1, Math.trunc(bwFlat / df));
|
|
1391
|
+
const iws = Math.trunc(baud / df);
|
|
1392
|
+
const raw = new Float64Array(NFFT2$1);
|
|
1393
|
+
for (let i = 0; i < iwt && i < raw.length; i++) {
|
|
1394
|
+
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (iwt - 1 - i)) / iwt));
|
|
1395
|
+
}
|
|
1396
|
+
for (let i = iwt; i < iwt + iwf && i < raw.length; i++)
|
|
1397
|
+
raw[i] = 1;
|
|
1398
|
+
for (let i = iwt + iwf; i < 2 * iwt + iwf && i < raw.length; i++) {
|
|
1399
|
+
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (i - (iwt + iwf))) / iwt));
|
|
1400
|
+
}
|
|
1401
|
+
const window = new Float64Array(NFFT2$1);
|
|
1402
|
+
for (let i = 0; i < NFFT2$1; i++) {
|
|
1403
|
+
const src = (i + iws) % NFFT2$1;
|
|
1404
|
+
window[i] = raw[src];
|
|
1405
|
+
}
|
|
1406
|
+
return { df, window };
|
|
1407
|
+
}
|
|
1408
|
+
function ft4Downsample(cxRe, cxIm, f0, ctx, outRe, outIm) {
|
|
1409
|
+
outRe.fill(0);
|
|
1410
|
+
outIm.fill(0);
|
|
1411
|
+
const i0 = Math.round(f0 / ctx.df);
|
|
1412
|
+
if (i0 >= 0 && i0 <= NMAX$1 / 2) {
|
|
1413
|
+
outRe[0] = cxRe[i0] ?? 0;
|
|
1414
|
+
outIm[0] = cxIm[i0] ?? 0;
|
|
1415
|
+
}
|
|
1416
|
+
for (let i = 1; i <= NFFT2$1 / 2; i++) {
|
|
1417
|
+
const hi = i0 + i;
|
|
1418
|
+
if (hi >= 0 && hi <= NMAX$1 / 2) {
|
|
1419
|
+
outRe[i] = cxRe[hi] ?? 0;
|
|
1420
|
+
outIm[i] = cxIm[hi] ?? 0;
|
|
1421
|
+
}
|
|
1422
|
+
const lo = i0 - i;
|
|
1423
|
+
if (lo >= 0 && lo <= NMAX$1 / 2) {
|
|
1424
|
+
const idx = NFFT2$1 - i;
|
|
1425
|
+
outRe[idx] = cxRe[lo] ?? 0;
|
|
1426
|
+
outIm[idx] = cxIm[lo] ?? 0;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const scale = 1 / NFFT2$1;
|
|
1430
|
+
for (let i = 0; i < NFFT2$1; i++) {
|
|
1431
|
+
const w = (ctx.window[i] ?? 0) * scale;
|
|
1432
|
+
outRe[i] = outRe[i] * w;
|
|
1433
|
+
outIm[i] = outIm[i] * w;
|
|
1434
|
+
}
|
|
1435
|
+
fftComplex(outRe, outIm, true);
|
|
1436
|
+
}
|
|
1437
|
+
function normalizeComplexPower(re, im, denom) {
|
|
1438
|
+
let sum = 0;
|
|
1439
|
+
for (let i = 0; i < re.length; i++)
|
|
1440
|
+
sum += re[i] * re[i] + im[i] * im[i];
|
|
1441
|
+
if (sum <= 0)
|
|
1442
|
+
return;
|
|
1443
|
+
const scale = 1 / Math.sqrt(sum / denom);
|
|
1444
|
+
for (let i = 0; i < re.length; i++) {
|
|
1445
|
+
re[i] = re[i] * scale;
|
|
1446
|
+
im[i] = im[i] * scale;
|
|
1339
1447
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return null;
|
|
1353
|
-
// Unpack
|
|
1354
|
-
const { msg, success } = unpack77(message77, book);
|
|
1355
|
-
if (!success || msg.trim().length === 0)
|
|
1356
|
-
return null;
|
|
1357
|
-
// Estimate SNR
|
|
1358
|
-
let xsig = 0;
|
|
1359
|
-
let xnoi = 0;
|
|
1360
|
-
const itone = getTones$1(result.cw);
|
|
1361
|
-
for (let i = 0; i < 79; i++) {
|
|
1362
|
-
xsig += s8[itone[i] * NN + i] ** 2;
|
|
1363
|
-
const ios = (itone[i] + 4) % 7;
|
|
1364
|
-
xnoi += s8[ios * NN + i] ** 2;
|
|
1448
|
+
}
|
|
1449
|
+
function extractFrame(cbRe, cbIm, ibest, outRe, outIm) {
|
|
1450
|
+
for (let i = 0; i < outRe.length; i++) {
|
|
1451
|
+
const src = ibest + i;
|
|
1452
|
+
if (src >= 0 && src < cbRe.length) {
|
|
1453
|
+
outRe[i] = cbRe[src];
|
|
1454
|
+
outIm[i] = cbIm[src];
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
outRe[i] = 0;
|
|
1458
|
+
outIm[i] = 0;
|
|
1459
|
+
}
|
|
1365
1460
|
}
|
|
1366
|
-
let snr = 0.001;
|
|
1367
|
-
const arg = xsig / Math.max(xnoi, 1e-30) - 1.0;
|
|
1368
|
-
if (arg > 0.1)
|
|
1369
|
-
snr = arg;
|
|
1370
|
-
snr = 10 * Math.log10(snr) - 27.0;
|
|
1371
|
-
if (snr < -24)
|
|
1372
|
-
snr = -24;
|
|
1373
|
-
return { msg, freq: f1, dt: xdt, snr };
|
|
1374
1461
|
}
|
|
1375
|
-
function
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
for (let
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
k += 7;
|
|
1388
|
-
const indx = cw[i] * 4 + cw[i + 1] * 2 + cw[i + 2];
|
|
1389
|
-
tones[k] = graymap[indx];
|
|
1390
|
-
k++;
|
|
1462
|
+
function createTweakedSyncTemplates() {
|
|
1463
|
+
const base = createBaseSyncTemplates();
|
|
1464
|
+
const fsample = FS2$1 / 2;
|
|
1465
|
+
const out = new Map();
|
|
1466
|
+
for (let idf = -FT4_MAX_TWEAK; idf <= FT4_MAX_TWEAK; idf++) {
|
|
1467
|
+
const tweak = createFrequencyTweak(idf, 2 * NSS, fsample);
|
|
1468
|
+
out.set(idf, [
|
|
1469
|
+
applyTweak(base[0], tweak),
|
|
1470
|
+
applyTweak(base[1], tweak),
|
|
1471
|
+
applyTweak(base[2], tweak),
|
|
1472
|
+
applyTweak(base[3], tweak),
|
|
1473
|
+
]);
|
|
1391
1474
|
}
|
|
1392
|
-
return
|
|
1475
|
+
return out;
|
|
1393
1476
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const
|
|
1404
|
-
const
|
|
1405
|
-
const ft = f0 + 8.5 * baud;
|
|
1406
|
-
const it = Math.min(Math.round(ft / df), NFFT1 / 2);
|
|
1407
|
-
const fb = f0 - 1.5 * baud;
|
|
1408
|
-
const ib = Math.max(1, Math.round(fb / df));
|
|
1409
|
-
c1Re.fill(0);
|
|
1410
|
-
c1Im.fill(0);
|
|
1477
|
+
function createBaseSyncTemplates() {
|
|
1478
|
+
return [
|
|
1479
|
+
buildSyncTemplate(COSTAS_A),
|
|
1480
|
+
buildSyncTemplate(COSTAS_B),
|
|
1481
|
+
buildSyncTemplate(COSTAS_C),
|
|
1482
|
+
buildSyncTemplate(COSTAS_D),
|
|
1483
|
+
];
|
|
1484
|
+
}
|
|
1485
|
+
function buildSyncTemplate(tones) {
|
|
1486
|
+
const re = new Float64Array(2 * NSS);
|
|
1487
|
+
const im = new Float64Array(2 * NSS);
|
|
1411
1488
|
let k = 0;
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const pi = Math.PI;
|
|
1421
|
-
const taper = new Float64Array(101);
|
|
1422
|
-
for (let i = 0; i <= 100; i++) {
|
|
1423
|
-
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
1424
|
-
}
|
|
1425
|
-
for (let i = 0; i <= 100; i++) {
|
|
1426
|
-
if (i >= NFFT2)
|
|
1427
|
-
break;
|
|
1428
|
-
const tap = taper[100 - i];
|
|
1429
|
-
c1Re[i] = c1Re[i] * tap;
|
|
1430
|
-
c1Im[i] = c1Im[i] * tap;
|
|
1431
|
-
}
|
|
1432
|
-
const endTap = k - 1;
|
|
1433
|
-
for (let i = 0; i <= 100; i++) {
|
|
1434
|
-
const idx = endTap - 100 + i;
|
|
1435
|
-
if (idx >= 0 && idx < NFFT2) {
|
|
1436
|
-
const tap = taper[i];
|
|
1437
|
-
c1Re[idx] = c1Re[idx] * tap;
|
|
1438
|
-
c1Im[idx] = c1Im[idx] * tap;
|
|
1489
|
+
let phi = 0;
|
|
1490
|
+
for (const tone of tones) {
|
|
1491
|
+
const dphi = (TWO_PI$2 * tone * 2) / NSS;
|
|
1492
|
+
for (let j = 0; j < NSS / 2; j++) {
|
|
1493
|
+
re[k] = Math.cos(phi);
|
|
1494
|
+
im[k] = Math.sin(phi);
|
|
1495
|
+
phi = (phi + dphi) % TWO_PI$2;
|
|
1496
|
+
k++;
|
|
1439
1497
|
}
|
|
1440
1498
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1499
|
+
return { re, im };
|
|
1500
|
+
}
|
|
1501
|
+
function createFrequencyTweak(idf, npts, fsample) {
|
|
1502
|
+
const re = new Float64Array(npts);
|
|
1503
|
+
const im = new Float64Array(npts);
|
|
1504
|
+
const dphi = (TWO_PI$2 * idf) / fsample;
|
|
1505
|
+
const stepRe = Math.cos(dphi);
|
|
1506
|
+
const stepIm = Math.sin(dphi);
|
|
1507
|
+
let wRe = 1;
|
|
1508
|
+
let wIm = 0;
|
|
1509
|
+
for (let i = 0; i < npts; i++) {
|
|
1510
|
+
const newRe = wRe * stepRe - wIm * stepIm;
|
|
1511
|
+
const newIm = wRe * stepIm + wIm * stepRe;
|
|
1512
|
+
wRe = newRe;
|
|
1513
|
+
wIm = newIm;
|
|
1514
|
+
re[i] = wRe;
|
|
1515
|
+
im[i] = wIm;
|
|
1516
|
+
}
|
|
1517
|
+
return { re, im };
|
|
1518
|
+
}
|
|
1519
|
+
function applyTweak(template, tweak) {
|
|
1520
|
+
const re = new Float64Array(template.re.length);
|
|
1521
|
+
const im = new Float64Array(template.im.length);
|
|
1522
|
+
for (let i = 0; i < template.re.length; i++) {
|
|
1523
|
+
const sr = template.re[i];
|
|
1524
|
+
const si = template.im[i];
|
|
1525
|
+
const tr = tweak.re[i];
|
|
1526
|
+
const ti = tweak.im[i];
|
|
1527
|
+
re[i] = tr * sr - ti * si;
|
|
1528
|
+
im[i] = tr * si + ti * sr;
|
|
1529
|
+
}
|
|
1530
|
+
return { re, im };
|
|
1531
|
+
}
|
|
1532
|
+
function sync4d(cdRe, cdIm, i0, templates) {
|
|
1533
|
+
let sync = 0;
|
|
1534
|
+
for (let i = 0; i < COSTAS_BLOCKS$1; i++) {
|
|
1535
|
+
const start = i0 + i * FT4_SYNC_STRIDE;
|
|
1536
|
+
const z = correlateStride2(cdRe, cdIm, start, templates[i].re, templates[i].im);
|
|
1537
|
+
if (z.count <= 16)
|
|
1538
|
+
continue;
|
|
1539
|
+
sync += Math.hypot(z.re, z.im) / (2 * NSS);
|
|
1464
1540
|
}
|
|
1541
|
+
return sync;
|
|
1465
1542
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1543
|
+
function correlateStride2(cdRe, cdIm, start, templateRe, templateIm) {
|
|
1544
|
+
let zRe = 0;
|
|
1545
|
+
let zIm = 0;
|
|
1546
|
+
let count = 0;
|
|
1547
|
+
for (let i = 0; i < templateRe.length; i++) {
|
|
1548
|
+
const idx = start + 2 * i;
|
|
1549
|
+
if (idx < 0 || idx >= cdRe.length)
|
|
1550
|
+
continue;
|
|
1551
|
+
const sRe = templateRe[i];
|
|
1552
|
+
const sIm = templateIm[i];
|
|
1553
|
+
const dRe = cdRe[idx];
|
|
1554
|
+
const dIm = cdIm[idx];
|
|
1555
|
+
zRe += dRe * sRe + dIm * sIm;
|
|
1556
|
+
zIm += dIm * sRe - dRe * sIm;
|
|
1557
|
+
count++;
|
|
1558
|
+
}
|
|
1559
|
+
return { re: zRe, im: zIm, count };
|
|
1560
|
+
}
|
|
1561
|
+
function buildBitMetrics$1(cdRe, cdIm, workspace) {
|
|
1562
|
+
const { csRe, csIm, s4, symbRe, symbIm, bitmetrics1, bitmetrics2, bitmetrics3, s2 } = workspace;
|
|
1563
|
+
for (let k = 0; k < NN$1; k++) {
|
|
1564
|
+
const i1 = k * NSS;
|
|
1565
|
+
for (let i = 0; i < NSS; i++) {
|
|
1566
|
+
symbRe[i] = cdRe[i1 + i];
|
|
1567
|
+
symbIm[i] = cdIm[i1 + i];
|
|
1568
|
+
}
|
|
1569
|
+
fftComplex(symbRe, symbIm, false);
|
|
1570
|
+
for (let tone = 0; tone < 4; tone++) {
|
|
1571
|
+
const idx = tone * NN$1 + k;
|
|
1572
|
+
const re = symbRe[tone];
|
|
1573
|
+
const im = symbIm[tone];
|
|
1574
|
+
csRe[idx] = re;
|
|
1575
|
+
csIm[idx] = im;
|
|
1576
|
+
s4[idx] = Math.hypot(re, im);
|
|
1479
1577
|
}
|
|
1480
1578
|
}
|
|
1481
|
-
let
|
|
1482
|
-
for (let
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1579
|
+
let nsync = 0;
|
|
1580
|
+
for (let k = 0; k < 4; k++) {
|
|
1581
|
+
if (maxTone(s4, k) === COSTAS_A[k])
|
|
1582
|
+
nsync++;
|
|
1583
|
+
if (maxTone(s4, 33 + k) === COSTAS_B[k])
|
|
1584
|
+
nsync++;
|
|
1585
|
+
if (maxTone(s4, 66 + k) === COSTAS_C[k])
|
|
1586
|
+
nsync++;
|
|
1587
|
+
if (maxTone(s4, 99 + k) === COSTAS_D[k])
|
|
1588
|
+
nsync++;
|
|
1589
|
+
}
|
|
1590
|
+
bitmetrics1.fill(0);
|
|
1591
|
+
bitmetrics2.fill(0);
|
|
1592
|
+
bitmetrics3.fill(0);
|
|
1593
|
+
if (nsync < 6)
|
|
1594
|
+
return true;
|
|
1595
|
+
for (let nseq = 1; nseq <= 3; nseq++) {
|
|
1596
|
+
const nsym = nseq === 1 ? 1 : nseq === 2 ? 2 : 4;
|
|
1597
|
+
const nt = 1 << (2 * nsym);
|
|
1598
|
+
const ibmax = nseq === 1 ? 1 : nseq === 2 ? 3 : 7;
|
|
1599
|
+
for (let ks = 1; ks <= NN$1 - nsym + 1; ks += nsym) {
|
|
1600
|
+
for (let i = 0; i < nt; i++) {
|
|
1601
|
+
const i1 = Math.floor(i / 64);
|
|
1602
|
+
const i2 = Math.floor((i & 63) / 16);
|
|
1603
|
+
const i3 = Math.floor((i & 15) / 4);
|
|
1604
|
+
const i4 = i & 3;
|
|
1605
|
+
if (nsym === 1) {
|
|
1606
|
+
const t = GRAYMAP[i4];
|
|
1607
|
+
const idx = t * NN$1 + (ks - 1);
|
|
1608
|
+
s2[i] = Math.hypot(csRe[idx], csIm[idx]);
|
|
1609
|
+
}
|
|
1610
|
+
else if (nsym === 2) {
|
|
1611
|
+
const t3 = GRAYMAP[i3];
|
|
1612
|
+
const t4 = GRAYMAP[i4];
|
|
1613
|
+
const iA = t3 * NN$1 + (ks - 1);
|
|
1614
|
+
const iB = t4 * NN$1 + ks;
|
|
1615
|
+
const re = csRe[iA] + csRe[iB];
|
|
1616
|
+
const im = csIm[iA] + csIm[iB];
|
|
1617
|
+
s2[i] = Math.hypot(re, im);
|
|
1618
|
+
}
|
|
1619
|
+
else {
|
|
1620
|
+
const t1 = GRAYMAP[i1];
|
|
1621
|
+
const t2 = GRAYMAP[i2];
|
|
1622
|
+
const t3 = GRAYMAP[i3];
|
|
1623
|
+
const t4 = GRAYMAP[i4];
|
|
1624
|
+
const iA = t1 * NN$1 + (ks - 1);
|
|
1625
|
+
const iB = t2 * NN$1 + ks;
|
|
1626
|
+
const iC = t3 * NN$1 + (ks + 1);
|
|
1627
|
+
const iD = t4 * NN$1 + (ks + 2);
|
|
1628
|
+
const re = csRe[iA] + csRe[iB] + csRe[iC] + csRe[iD];
|
|
1629
|
+
const im = csIm[iA] + csIm[iB] + csIm[iC] + csIm[iD];
|
|
1630
|
+
s2[i] = Math.hypot(re, im);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const ipt = 1 + (ks - 1) * 2;
|
|
1634
|
+
for (let ib = 0; ib <= ibmax; ib++) {
|
|
1635
|
+
const mask = 1 << (ibmax - ib);
|
|
1636
|
+
let max1 = -1e30;
|
|
1637
|
+
let max0 = -1e30;
|
|
1638
|
+
for (let i = 0; i < nt; i++) {
|
|
1639
|
+
const v = s2[i];
|
|
1640
|
+
if ((i & mask) !== 0) {
|
|
1641
|
+
if (v > max1)
|
|
1642
|
+
max1 = v;
|
|
1497
1643
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1644
|
+
else if (v > max0) {
|
|
1645
|
+
max0 = v;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const idx = ipt + ib;
|
|
1649
|
+
if (idx > BITMETRIC_LEN)
|
|
1650
|
+
continue;
|
|
1651
|
+
const bm = max1 - max0;
|
|
1652
|
+
if (nseq === 1) {
|
|
1653
|
+
bitmetrics1[idx - 1] = bm;
|
|
1654
|
+
}
|
|
1655
|
+
else if (nseq === 2) {
|
|
1656
|
+
bitmetrics2[idx - 1] = bm;
|
|
1657
|
+
}
|
|
1658
|
+
else {
|
|
1659
|
+
bitmetrics3[idx - 1] = bm;
|
|
1503
1660
|
}
|
|
1504
1661
|
}
|
|
1505
|
-
sync += zRe * zRe + zIm * zIm;
|
|
1506
1662
|
}
|
|
1507
1663
|
}
|
|
1508
|
-
|
|
1664
|
+
bitmetrics2[208] = bitmetrics1[208];
|
|
1665
|
+
bitmetrics2[209] = bitmetrics1[209];
|
|
1666
|
+
bitmetrics3[208] = bitmetrics1[208];
|
|
1667
|
+
bitmetrics3[209] = bitmetrics1[209];
|
|
1668
|
+
normalizeBitMetrics(bitmetrics1);
|
|
1669
|
+
normalizeBitMetrics(bitmetrics2);
|
|
1670
|
+
normalizeBitMetrics(bitmetrics3);
|
|
1671
|
+
return false;
|
|
1509
1672
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1512
|
-
let
|
|
1513
|
-
for (let
|
|
1673
|
+
function maxTone(s4, symbolIndex) {
|
|
1674
|
+
let bestTone = 0;
|
|
1675
|
+
let bestValue = -1;
|
|
1676
|
+
for (let tone = 0; tone < 4; tone++) {
|
|
1677
|
+
const v = s4[tone * NN$1 + symbolIndex];
|
|
1678
|
+
if (v > bestValue) {
|
|
1679
|
+
bestValue = v;
|
|
1680
|
+
bestTone = tone;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return bestTone;
|
|
1684
|
+
}
|
|
1685
|
+
function normalizeBitMetrics(bmet) {
|
|
1686
|
+
let sum = 0;
|
|
1687
|
+
let sum2 = 0;
|
|
1688
|
+
for (let i = 0; i < bmet.length; i++) {
|
|
1514
1689
|
sum += bmet[i];
|
|
1515
1690
|
sum2 += bmet[i] * bmet[i];
|
|
1516
1691
|
}
|
|
1517
|
-
const avg = sum /
|
|
1518
|
-
const avg2 = sum2 /
|
|
1692
|
+
const avg = sum / bmet.length;
|
|
1693
|
+
const avg2 = sum2 / bmet.length;
|
|
1519
1694
|
const variance = avg2 - avg * avg;
|
|
1520
1695
|
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
1521
|
-
if (sigma
|
|
1522
|
-
|
|
1523
|
-
|
|
1696
|
+
if (sigma <= 0)
|
|
1697
|
+
return;
|
|
1698
|
+
for (let i = 0; i < bmet.length; i++)
|
|
1699
|
+
bmet[i] = bmet[i] / sigma;
|
|
1700
|
+
}
|
|
1701
|
+
function passesHardSyncQuality(bitmetrics1) {
|
|
1702
|
+
const hard = new Uint8Array(bitmetrics1.length);
|
|
1703
|
+
for (let i = 0; i < bitmetrics1.length; i++)
|
|
1704
|
+
hard[i] = bitmetrics1[i] >= 0 ? 1 : 0;
|
|
1705
|
+
let score = 0;
|
|
1706
|
+
for (const pattern of HARD_SYNC_PATTERNS) {
|
|
1707
|
+
for (let i = 0; i < pattern.bits.length; i++) {
|
|
1708
|
+
if (hard[pattern.offset + i] === pattern.bits[i])
|
|
1709
|
+
score++;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return score >= 10;
|
|
1713
|
+
}
|
|
1714
|
+
function buildLlrs(workspace) {
|
|
1715
|
+
const { bitmetrics1, bitmetrics2, bitmetrics3, llra, llrb, llrc } = workspace;
|
|
1716
|
+
for (let i = 0; i < 58; i++) {
|
|
1717
|
+
llra[i] = bitmetrics1[8 + i];
|
|
1718
|
+
llra[58 + i] = bitmetrics1[74 + i];
|
|
1719
|
+
llra[116 + i] = bitmetrics1[140 + i];
|
|
1720
|
+
llrb[i] = bitmetrics2[8 + i];
|
|
1721
|
+
llrb[58 + i] = bitmetrics2[74 + i];
|
|
1722
|
+
llrb[116 + i] = bitmetrics2[140 + i];
|
|
1723
|
+
llrc[i] = bitmetrics3[8 + i];
|
|
1724
|
+
llrc[58 + i] = bitmetrics3[74 + i];
|
|
1725
|
+
llrc[116 + i] = bitmetrics3[140 + i];
|
|
1524
1726
|
}
|
|
1525
1727
|
}
|
|
1526
|
-
function
|
|
1728
|
+
function tryDecodePasses$1(workspace, depth) {
|
|
1729
|
+
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
1730
|
+
const scalefac = 2.83;
|
|
1731
|
+
const sources = [workspace.llra, workspace.llrb, workspace.llrc];
|
|
1732
|
+
workspace.apmask.fill(0);
|
|
1733
|
+
for (const src of sources) {
|
|
1734
|
+
for (let i = 0; i < LDPC_BITS; i++)
|
|
1735
|
+
workspace.llr[i] = scalefac * src[i];
|
|
1736
|
+
const result = decode174_91(workspace.llr, workspace.apmask, maxosd);
|
|
1737
|
+
if (result)
|
|
1738
|
+
return result;
|
|
1739
|
+
}
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
function hasNonZeroBit(bits) {
|
|
1743
|
+
for (const bit of bits) {
|
|
1744
|
+
if (bit !== 0)
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
function toFt4Snr(syncMinusOne) {
|
|
1750
|
+
if (syncMinusOne > 0) {
|
|
1751
|
+
return Math.round(Math.max(-21, 10 * Math.log10(syncMinusOne) - 14.8));
|
|
1752
|
+
}
|
|
1753
|
+
return -21;
|
|
1754
|
+
}
|
|
1755
|
+
function resample$1(input, fromRate, toRate, outLen) {
|
|
1527
1756
|
const out = new Float64Array(outLen);
|
|
1528
1757
|
const ratio = fromRate / toRate;
|
|
1529
1758
|
for (let i = 0; i < outLen; i++) {
|
|
@@ -2046,10 +2275,10 @@ function packFreeText(msg) {
|
|
|
2046
2275
|
return bits; // 77 bits
|
|
2047
2276
|
}
|
|
2048
2277
|
|
|
2049
|
-
const TWO_PI = 2 * Math.PI;
|
|
2050
|
-
const
|
|
2051
|
-
const
|
|
2052
|
-
const
|
|
2278
|
+
const TWO_PI$1 = 2 * Math.PI;
|
|
2279
|
+
const FT8_DEFAULT_SAMPLE_RATE = 12_000;
|
|
2280
|
+
const FT8_DEFAULT_SAMPLES_PER_SYMBOL = 1_920;
|
|
2281
|
+
const FT8_DEFAULT_BT = 2.0;
|
|
2053
2282
|
const MODULATION_INDEX = 1.0;
|
|
2054
2283
|
function assertPositiveFinite(value, name) {
|
|
2055
2284
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -2072,33 +2301,36 @@ function gfskPulse(bt, tt) {
|
|
|
2072
2301
|
const scale = Math.PI * Math.sqrt(2 / Math.log(2)) * bt;
|
|
2073
2302
|
return 0.5 * (erfApprox(scale * (tt + 0.5)) - erfApprox(scale * (tt - 0.5)));
|
|
2074
2303
|
}
|
|
2075
|
-
function
|
|
2076
|
-
// Mirrors the FT8 path in lib/ft8/gen_ft8wave.f90.
|
|
2304
|
+
function generateGfskWaveform(tones, options, defaults, shape) {
|
|
2077
2305
|
const nsym = tones.length;
|
|
2078
2306
|
if (nsym === 0) {
|
|
2079
2307
|
return new Float32Array(0);
|
|
2080
2308
|
}
|
|
2081
|
-
const sampleRate = options.sampleRate ??
|
|
2082
|
-
const nsps = options.samplesPerSymbol ??
|
|
2083
|
-
const bt = options.bt ??
|
|
2309
|
+
const sampleRate = options.sampleRate ?? defaults.sampleRate;
|
|
2310
|
+
const nsps = options.samplesPerSymbol ?? defaults.samplesPerSymbol;
|
|
2311
|
+
const bt = options.bt ?? defaults.bt;
|
|
2084
2312
|
const f0 = options.baseFrequency ?? 0;
|
|
2313
|
+
const initialPhase = options.initialPhase ?? 0;
|
|
2085
2314
|
assertPositiveFinite(sampleRate, "sampleRate");
|
|
2086
2315
|
assertPositiveFinite(nsps, "samplesPerSymbol");
|
|
2087
2316
|
assertPositiveFinite(bt, "bt");
|
|
2088
2317
|
if (!Number.isFinite(f0)) {
|
|
2089
2318
|
throw new Error("baseFrequency must be finite");
|
|
2090
2319
|
}
|
|
2320
|
+
if (!Number.isFinite(initialPhase)) {
|
|
2321
|
+
throw new Error("initialPhase must be finite");
|
|
2322
|
+
}
|
|
2091
2323
|
if (!Number.isInteger(nsps)) {
|
|
2092
2324
|
throw new Error("samplesPerSymbol must be an integer");
|
|
2093
2325
|
}
|
|
2094
|
-
const nwave = nsym * nsps;
|
|
2326
|
+
const nwave = (nsym) * nsps;
|
|
2095
2327
|
const pulse = new Float64Array(3 * nsps);
|
|
2096
2328
|
for (let i = 0; i < pulse.length; i++) {
|
|
2097
2329
|
const tt = (i + 1 - 1.5 * nsps) / nsps;
|
|
2098
2330
|
pulse[i] = gfskPulse(bt, tt);
|
|
2099
2331
|
}
|
|
2100
2332
|
const dphi = new Float64Array((nsym + 2) * nsps);
|
|
2101
|
-
const dphiPeak = (TWO_PI * MODULATION_INDEX) / nsps;
|
|
2333
|
+
const dphiPeak = (TWO_PI$1 * MODULATION_INDEX) / nsps;
|
|
2102
2334
|
for (let j = 0; j < nsym; j++) {
|
|
2103
2335
|
const tone = tones[j];
|
|
2104
2336
|
const ib = j * nsps;
|
|
@@ -2113,33 +2345,52 @@ function generateFT8Waveform(tones, options = {}) {
|
|
|
2113
2345
|
dphi[i] += dphiPeak * firstTone * pulse[nsps + i];
|
|
2114
2346
|
dphi[tailBase + i] += dphiPeak * lastTone * pulse[i];
|
|
2115
2347
|
}
|
|
2116
|
-
const carrierDphi = (TWO_PI * f0) / sampleRate;
|
|
2348
|
+
const carrierDphi = (TWO_PI$1 * f0) / sampleRate;
|
|
2117
2349
|
for (let i = 0; i < dphi.length; i++) {
|
|
2118
2350
|
dphi[i] += carrierDphi;
|
|
2119
2351
|
}
|
|
2120
2352
|
const wave = new Float32Array(nwave);
|
|
2121
|
-
let phi =
|
|
2353
|
+
let phi = initialPhase % TWO_PI$1;
|
|
2354
|
+
if (phi < 0)
|
|
2355
|
+
phi += TWO_PI$1;
|
|
2356
|
+
const phaseStart = nsps;
|
|
2122
2357
|
for (let k = 0; k < nwave; k++) {
|
|
2123
|
-
const j =
|
|
2358
|
+
const j = phaseStart + k;
|
|
2124
2359
|
wave[k] = Math.sin(phi);
|
|
2125
2360
|
phi += dphi[j];
|
|
2126
|
-
phi %= TWO_PI;
|
|
2361
|
+
phi %= TWO_PI$1;
|
|
2127
2362
|
if (phi < 0) {
|
|
2128
|
-
phi += TWO_PI;
|
|
2363
|
+
phi += TWO_PI$1;
|
|
2129
2364
|
}
|
|
2130
2365
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2366
|
+
{
|
|
2367
|
+
const nramp = Math.round(nsps / 8);
|
|
2368
|
+
for (let i = 0; i < nramp; i++) {
|
|
2369
|
+
const up = (1 - Math.cos((TWO_PI$1 * i) / (2 * nramp))) / 2;
|
|
2370
|
+
wave[i] *= up;
|
|
2371
|
+
}
|
|
2372
|
+
const tailStart = nwave - nramp;
|
|
2373
|
+
for (let i = 0; i < nramp; i++) {
|
|
2374
|
+
const down = (1 + Math.cos((TWO_PI$1 * i) / (2 * nramp))) / 2;
|
|
2375
|
+
wave[tailStart + i] *= down;
|
|
2376
|
+
}
|
|
2140
2377
|
}
|
|
2141
2378
|
return wave;
|
|
2142
2379
|
}
|
|
2380
|
+
function generateFT8Waveform(tones, options = {}) {
|
|
2381
|
+
// Mirrors the FT8 path in lib/ft8/gen_ft8wave.f90.
|
|
2382
|
+
return generateGfskWaveform(tones, options, {
|
|
2383
|
+
sampleRate: FT8_DEFAULT_SAMPLE_RATE,
|
|
2384
|
+
samplesPerSymbol: FT8_DEFAULT_SAMPLES_PER_SYMBOL,
|
|
2385
|
+
bt: FT8_DEFAULT_BT,
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/** FT8-specific constants (lib/ft8/ft8_params.f90). */
|
|
2390
|
+
/** 7-symbol Costas array for sync. */
|
|
2391
|
+
const COSTAS = [3, 1, 4, 0, 6, 5, 2];
|
|
2392
|
+
/** 8-tone Gray mapping. */
|
|
2393
|
+
const GRAY_MAP = [0, 1, 3, 2, 5, 6, 4, 7];
|
|
2143
2394
|
|
|
2144
2395
|
function generateLdpcGMatrix() {
|
|
2145
2396
|
const K = 91;
|
|
@@ -2190,21 +2441,21 @@ function encode174_91(msg77) {
|
|
|
2190
2441
|
}
|
|
2191
2442
|
return codeword;
|
|
2192
2443
|
}
|
|
2193
|
-
function getTones(codeword) {
|
|
2444
|
+
function getTones$1(codeword) {
|
|
2194
2445
|
const tones = new Array(79).fill(0);
|
|
2195
2446
|
for (let i = 0; i < 7; i++)
|
|
2196
|
-
tones[i] =
|
|
2447
|
+
tones[i] = COSTAS[i];
|
|
2197
2448
|
for (let i = 0; i < 7; i++)
|
|
2198
|
-
tones[36 + i] =
|
|
2449
|
+
tones[36 + i] = COSTAS[i];
|
|
2199
2450
|
for (let i = 0; i < 7; i++)
|
|
2200
|
-
tones[72 + i] =
|
|
2451
|
+
tones[72 + i] = COSTAS[i];
|
|
2201
2452
|
let k = 7;
|
|
2202
2453
|
for (let j = 1; j <= 58; j++) {
|
|
2203
2454
|
const i = j * 3 - 3; // codeword is 0-indexed in JS, but the loop was j=1 to 58
|
|
2204
2455
|
if (j === 30)
|
|
2205
2456
|
k += 7;
|
|
2206
2457
|
const indx = codeword[i] * 4 + codeword[i + 1] * 2 + codeword[i + 2];
|
|
2207
|
-
tones[k] =
|
|
2458
|
+
tones[k] = GRAY_MAP[indx];
|
|
2208
2459
|
k++;
|
|
2209
2460
|
}
|
|
2210
2461
|
return tones;
|
|
@@ -2212,54 +2463,830 @@ function getTones(codeword) {
|
|
|
2212
2463
|
function encodeMessage(msg) {
|
|
2213
2464
|
const bits77 = pack77(msg);
|
|
2214
2465
|
const codeword = encode174_91(bits77);
|
|
2215
|
-
return getTones(codeword);
|
|
2466
|
+
return getTones$1(codeword);
|
|
2216
2467
|
}
|
|
2217
2468
|
function encode(msg, options = {}) {
|
|
2218
2469
|
return generateFT8Waveform(encodeMessage(msg), options);
|
|
2219
2470
|
}
|
|
2220
2471
|
|
|
2221
|
-
|
|
2472
|
+
const NSPS = 1920;
|
|
2473
|
+
const NFFT1 = 2 * NSPS; // 3840
|
|
2474
|
+
const NSTEP = NSPS / 4; // 480
|
|
2475
|
+
const NMAX = 15 * 12_000; // 180000
|
|
2476
|
+
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
2477
|
+
const NDOWN = 60;
|
|
2478
|
+
const NN = 79;
|
|
2479
|
+
const NFFT1_LONG = 192000;
|
|
2480
|
+
const NFFT2 = 3200;
|
|
2481
|
+
const NP2 = 2812;
|
|
2482
|
+
const COSTAS_BLOCKS = 7;
|
|
2483
|
+
const COSTAS_SYMBOL_LEN = 32;
|
|
2484
|
+
const SYNC_TIME_SHIFTS = [0, 36, 72];
|
|
2485
|
+
const TAPER_SIZE = 101;
|
|
2486
|
+
const TAPER_LAST = TAPER_SIZE - 1;
|
|
2487
|
+
const TWO_PI = 2 * Math.PI;
|
|
2488
|
+
const MAX_DECODE_PASSES_DEPTH3 = 2;
|
|
2489
|
+
const SUBTRACTION_GAIN = 0.95;
|
|
2490
|
+
const SUBTRACTION_PHASE_SHIFT = Math.PI / 2;
|
|
2491
|
+
const MIN_SUBTRACTION_SNR = -22;
|
|
2492
|
+
const FS2 = SAMPLE_RATE$1 / NDOWN;
|
|
2493
|
+
const DT2 = 1.0 / FS2;
|
|
2494
|
+
const DOWNSAMPLE_DF = SAMPLE_RATE$1 / NFFT1_LONG;
|
|
2495
|
+
const DOWNSAMPLE_BAUD = SAMPLE_RATE$1 / NSPS;
|
|
2496
|
+
const DOWNSAMPLE_SCALE = Math.sqrt(NFFT2 / NFFT1_LONG);
|
|
2497
|
+
const TAPER = buildTaper(TAPER_SIZE);
|
|
2498
|
+
const COSTAS_SYNC = buildCostasSyncTemplates();
|
|
2499
|
+
const FREQ_SHIFT_SYNC = buildFrequencyShiftSyncTemplates();
|
|
2222
2500
|
/**
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2501
|
+
* Decode all FT8 signals in an audio buffer.
|
|
2502
|
+
* Input: mono audio samples at `sampleRate` Hz, duration ~15s.
|
|
2225
2503
|
*/
|
|
2226
|
-
function
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
const
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2504
|
+
function decode(samples, options = {}) {
|
|
2505
|
+
const sampleRate = options.sampleRate ?? SAMPLE_RATE$1;
|
|
2506
|
+
const nfa = options.freqLow ?? 200;
|
|
2507
|
+
const nfb = options.freqHigh ?? 3000;
|
|
2508
|
+
const syncmin = options.syncMin ?? 1.2;
|
|
2509
|
+
const depth = options.depth ?? 2;
|
|
2510
|
+
const maxCandidates = options.maxCandidates ?? 300;
|
|
2511
|
+
const book = options.hashCallBook;
|
|
2512
|
+
const dd = sampleRate === SAMPLE_RATE$1
|
|
2513
|
+
? copySamplesToDecodeWindow(samples)
|
|
2514
|
+
: resample(samples, sampleRate, SAMPLE_RATE$1, NMAX);
|
|
2515
|
+
const residual = new Float64Array(dd);
|
|
2516
|
+
const cxRe = new Float64Array(NFFT1_LONG);
|
|
2517
|
+
const cxIm = new Float64Array(NFFT1_LONG);
|
|
2518
|
+
const workspace = createDecodeWorkspace();
|
|
2519
|
+
const toneCache = new Map();
|
|
2520
|
+
const decoded = [];
|
|
2521
|
+
const seenMessages = new Set();
|
|
2522
|
+
const maxPasses = depth >= 3 ? MAX_DECODE_PASSES_DEPTH3 : 1;
|
|
2523
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
2524
|
+
cxRe.fill(0);
|
|
2525
|
+
cxIm.fill(0);
|
|
2526
|
+
cxRe.set(residual);
|
|
2527
|
+
fftComplex(cxRe, cxIm, false);
|
|
2528
|
+
const { candidates, sbase } = sync8(residual, nfa, nfb, syncmin, maxCandidates);
|
|
2529
|
+
const coarseFrequencyUses = countCandidateFrequencies(candidates);
|
|
2530
|
+
const coarseDownsampleCache = new Map();
|
|
2531
|
+
let decodedInPass = 0;
|
|
2532
|
+
for (const cand of candidates) {
|
|
2533
|
+
const result = ft8b(residual, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book, workspace, coarseDownsampleCache, coarseFrequencyUses);
|
|
2534
|
+
if (!result)
|
|
2535
|
+
continue;
|
|
2536
|
+
const messageKey = normalizeMessageKey(result.msg);
|
|
2537
|
+
if (seenMessages.has(messageKey))
|
|
2538
|
+
continue;
|
|
2539
|
+
seenMessages.add(messageKey);
|
|
2540
|
+
decoded.push({
|
|
2541
|
+
freq: result.freq,
|
|
2542
|
+
dt: result.dt - 0.5,
|
|
2543
|
+
snr: result.snr,
|
|
2544
|
+
msg: result.msg,
|
|
2545
|
+
sync: cand.sync,
|
|
2546
|
+
});
|
|
2547
|
+
decodedInPass++;
|
|
2548
|
+
if (pass + 1 < maxPasses) {
|
|
2549
|
+
subtractDecodedSignal(residual, result, toneCache);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (decodedInPass === 0)
|
|
2553
|
+
break;
|
|
2554
|
+
}
|
|
2555
|
+
return decoded;
|
|
2556
|
+
}
|
|
2557
|
+
function normalizeMessageKey(msg) {
|
|
2558
|
+
return msg.trim().replace(/\s+/g, " ").toUpperCase();
|
|
2559
|
+
}
|
|
2560
|
+
function countCandidateFrequencies(candidates) {
|
|
2561
|
+
const counts = new Map();
|
|
2562
|
+
for (const c of candidates) {
|
|
2563
|
+
counts.set(c.freq, (counts.get(c.freq) ?? 0) + 1);
|
|
2564
|
+
}
|
|
2565
|
+
return counts;
|
|
2566
|
+
}
|
|
2567
|
+
function createDecodeWorkspace() {
|
|
2568
|
+
return {
|
|
2569
|
+
cd0Re: new Float64Array(NFFT2),
|
|
2570
|
+
cd0Im: new Float64Array(NFFT2),
|
|
2571
|
+
shiftRe: new Float64Array(NFFT2),
|
|
2572
|
+
shiftIm: new Float64Array(NFFT2),
|
|
2573
|
+
s8: new Float64Array(8 * NN),
|
|
2574
|
+
csRe: new Float64Array(8 * NN),
|
|
2575
|
+
csIm: new Float64Array(8 * NN),
|
|
2576
|
+
symbRe: new Float64Array(COSTAS_SYMBOL_LEN),
|
|
2577
|
+
symbIm: new Float64Array(COSTAS_SYMBOL_LEN),
|
|
2578
|
+
s2: new Float64Array(1 << 9),
|
|
2579
|
+
bmeta: new Float64Array(N_LDPC),
|
|
2580
|
+
bmetb: new Float64Array(N_LDPC),
|
|
2581
|
+
bmetc: new Float64Array(N_LDPC),
|
|
2582
|
+
bmetd: new Float64Array(N_LDPC),
|
|
2583
|
+
llr: new Float64Array(N_LDPC),
|
|
2584
|
+
apmask: new Int8Array(N_LDPC),
|
|
2585
|
+
ss: new Float64Array(9),
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
function copySamplesToDecodeWindow(samples) {
|
|
2589
|
+
const out = new Float64Array(NMAX);
|
|
2590
|
+
const len = Math.min(samples.length, NMAX);
|
|
2591
|
+
for (let i = 0; i < len; i++)
|
|
2592
|
+
out[i] = samples[i];
|
|
2593
|
+
return out;
|
|
2594
|
+
}
|
|
2595
|
+
function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
2596
|
+
const JZ = 62;
|
|
2597
|
+
const fftSize = nextPow2(NFFT1); // 4096
|
|
2598
|
+
const halfSize = fftSize / 2;
|
|
2599
|
+
const tstep = NSTEP / SAMPLE_RATE$1;
|
|
2600
|
+
const df = SAMPLE_RATE$1 / fftSize;
|
|
2601
|
+
const fac = 1.0 / 300.0;
|
|
2602
|
+
const s = new Float64Array(halfSize * NHSYM);
|
|
2603
|
+
const savg = new Float64Array(halfSize);
|
|
2604
|
+
const xRe = new Float64Array(fftSize);
|
|
2605
|
+
const xIm = new Float64Array(fftSize);
|
|
2606
|
+
for (let j = 0; j < NHSYM; j++) {
|
|
2607
|
+
const ia = j * NSTEP;
|
|
2608
|
+
xRe.fill(0);
|
|
2609
|
+
xIm.fill(0);
|
|
2610
|
+
for (let i = 0; i < NSPS && ia + i < dd.length; i++)
|
|
2611
|
+
xRe[i] = fac * dd[ia + i];
|
|
2612
|
+
fftComplex(xRe, xIm, false);
|
|
2613
|
+
for (let i = 0; i < halfSize; i++) {
|
|
2614
|
+
const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
|
|
2615
|
+
s[i * NHSYM + j] = power;
|
|
2616
|
+
savg[i] = savg[i] + power;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const sbase = computeBaseline(savg, nfa, nfb, df, halfSize);
|
|
2620
|
+
const ia = Math.max(1, Math.round(nfa / df));
|
|
2621
|
+
const ib = Math.min(halfSize - 14, Math.round(nfb / df));
|
|
2622
|
+
const nssy = Math.floor(NSPS / NSTEP);
|
|
2623
|
+
const nfos = Math.round(SAMPLE_RATE$1 / NSPS / df);
|
|
2624
|
+
const jstrt = Math.round(0.5 / tstep);
|
|
2625
|
+
const width = 2 * JZ + 1;
|
|
2626
|
+
const sync2d = new Float64Array((ib - ia + 1) * width);
|
|
2627
|
+
for (let i = ia; i <= ib; i++) {
|
|
2628
|
+
for (let jj = -JZ; jj <= JZ; jj++) {
|
|
2629
|
+
let ta = 0;
|
|
2630
|
+
let tb = 0;
|
|
2631
|
+
let tc = 0;
|
|
2632
|
+
let t0a = 0;
|
|
2633
|
+
let t0b = 0;
|
|
2634
|
+
let t0c = 0;
|
|
2635
|
+
for (let n = 0; n < COSTAS_BLOCKS; n++) {
|
|
2636
|
+
const m = jj + jstrt + nssy * n;
|
|
2637
|
+
const iCostas = i + nfos * COSTAS[n];
|
|
2638
|
+
if (m >= 0 && m < NHSYM && iCostas < halfSize) {
|
|
2639
|
+
ta += s[iCostas * NHSYM + m];
|
|
2640
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2641
|
+
const idx = i + nfos * tone;
|
|
2642
|
+
if (idx < halfSize)
|
|
2643
|
+
t0a += s[idx * NHSYM + m];
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
const m36 = m + nssy * 36;
|
|
2647
|
+
if (m36 >= 0 && m36 < NHSYM && iCostas < halfSize) {
|
|
2648
|
+
tb += s[iCostas * NHSYM + m36];
|
|
2649
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2650
|
+
const idx = i + nfos * tone;
|
|
2651
|
+
if (idx < halfSize)
|
|
2652
|
+
t0b += s[idx * NHSYM + m36];
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const m72 = m + nssy * 72;
|
|
2656
|
+
if (m72 >= 0 && m72 < NHSYM && iCostas < halfSize) {
|
|
2657
|
+
tc += s[iCostas * NHSYM + m72];
|
|
2658
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2659
|
+
const idx = i + nfos * tone;
|
|
2660
|
+
if (idx < halfSize)
|
|
2661
|
+
t0c += s[idx * NHSYM + m72];
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
const t = ta + tb + tc;
|
|
2666
|
+
const t0 = (t0a + t0b + t0c - t) / 6.0;
|
|
2667
|
+
const syncVal = t0 > 0 ? t / t0 : 0;
|
|
2668
|
+
const tbc = tb + tc;
|
|
2669
|
+
const t0bc = (t0b + t0c - tbc) / 6.0;
|
|
2670
|
+
const syncBc = t0bc > 0 ? tbc / t0bc : 0;
|
|
2671
|
+
sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const candidates0 = [];
|
|
2675
|
+
const mlag = 10;
|
|
2676
|
+
for (let i = ia; i <= ib; i++) {
|
|
2677
|
+
let bestSync = -1;
|
|
2678
|
+
let bestJ = 0;
|
|
2679
|
+
for (let j = -mlag; j <= mlag; j++) {
|
|
2680
|
+
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
2681
|
+
if (v > bestSync) {
|
|
2682
|
+
bestSync = v;
|
|
2683
|
+
bestJ = j;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
let bestSync2 = -1;
|
|
2687
|
+
let bestJ2 = 0;
|
|
2688
|
+
for (let j = -JZ; j <= JZ; j++) {
|
|
2689
|
+
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
2690
|
+
if (v > bestSync2) {
|
|
2691
|
+
bestSync2 = v;
|
|
2692
|
+
bestJ2 = j;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (bestSync >= syncmin) {
|
|
2696
|
+
candidates0.push({
|
|
2697
|
+
freq: i * df,
|
|
2698
|
+
dt: (bestJ - 0.5) * tstep,
|
|
2699
|
+
sync: bestSync,
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
if (bestJ2 !== bestJ && bestSync2 >= syncmin) {
|
|
2703
|
+
candidates0.push({
|
|
2704
|
+
freq: i * df,
|
|
2705
|
+
dt: (bestJ2 - 0.5) * tstep,
|
|
2706
|
+
sync: bestSync2,
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const syncValues = candidates0.map((c) => c.sync);
|
|
2711
|
+
syncValues.sort((a, b) => a - b);
|
|
2712
|
+
const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
|
|
2713
|
+
const base = syncValues[pctileIdx] ?? 1;
|
|
2714
|
+
if (base > 0) {
|
|
2715
|
+
for (const c of candidates0)
|
|
2716
|
+
c.sync /= base;
|
|
2717
|
+
}
|
|
2718
|
+
for (let i = 0; i < candidates0.length; i++) {
|
|
2719
|
+
for (let j = 0; j < i; j++) {
|
|
2720
|
+
const fdiff = Math.abs(candidates0[i].freq - candidates0[j].freq);
|
|
2721
|
+
const tdiff = Math.abs(candidates0[i].dt - candidates0[j].dt);
|
|
2722
|
+
if (fdiff < 4.0 && tdiff < 0.04) {
|
|
2723
|
+
if (candidates0[i].sync >= candidates0[j].sync) {
|
|
2724
|
+
candidates0[j].sync = 0;
|
|
2725
|
+
}
|
|
2726
|
+
else {
|
|
2727
|
+
candidates0[i].sync = 0;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const filtered = candidates0.filter((c) => c.sync >= syncmin);
|
|
2733
|
+
filtered.sort((a, b) => b.sync - a.sync);
|
|
2734
|
+
return { candidates: filtered.slice(0, maxcand), sbase };
|
|
2735
|
+
}
|
|
2736
|
+
function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
2737
|
+
const sbase = new Float64Array(nh1);
|
|
2738
|
+
const ia = Math.max(1, Math.round(nfa / df));
|
|
2739
|
+
const ib = Math.min(nh1 - 1, Math.round(nfb / df));
|
|
2740
|
+
const window = 50;
|
|
2741
|
+
for (let i = 0; i < nh1; i++) {
|
|
2742
|
+
let sum = 0;
|
|
2743
|
+
let count = 0;
|
|
2744
|
+
const lo = Math.max(ia, i - window);
|
|
2745
|
+
const hi = Math.min(ib, i + window);
|
|
2746
|
+
for (let j = lo; j <= hi; j++) {
|
|
2747
|
+
sum += savg[j];
|
|
2748
|
+
count++;
|
|
2749
|
+
}
|
|
2750
|
+
sbase[i] = count > 0 ? 10 * Math.log10(Math.max(1e-30, sum / count)) : 0;
|
|
2751
|
+
}
|
|
2752
|
+
return sbase;
|
|
2753
|
+
}
|
|
2754
|
+
function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book, workspace, coarseDownsampleCache, coarseFrequencyUses) {
|
|
2755
|
+
loadCoarseDownsample(cxRe, cxIm, f1, workspace, coarseDownsampleCache, coarseFrequencyUses);
|
|
2756
|
+
let ibest = findBestTimeOffset(workspace.cd0Re, workspace.cd0Im, xdt);
|
|
2757
|
+
const delfbest = findBestFrequencyShift(workspace.cd0Re, workspace.cd0Im, ibest);
|
|
2758
|
+
f1 += delfbest;
|
|
2759
|
+
ft8Downsample(cxRe, cxIm, f1, workspace);
|
|
2760
|
+
ibest = refineTimeOffset(workspace.cd0Re, workspace.cd0Im, ibest, workspace.ss);
|
|
2761
|
+
xdt = (ibest - 1) * DT2;
|
|
2762
|
+
extractSoftSymbols(workspace.cd0Re, workspace.cd0Im, ibest, workspace);
|
|
2763
|
+
const minCostasHits = depth >= 3 ? 6 : 7;
|
|
2764
|
+
if (!passesSyncGate(workspace.s8, minCostasHits))
|
|
2765
|
+
return null;
|
|
2766
|
+
buildBitMetrics(workspace);
|
|
2767
|
+
const result = tryDecodePasses(workspace, depth);
|
|
2768
|
+
if (!result)
|
|
2769
|
+
return null;
|
|
2770
|
+
if (result.cw.every((b) => b === 0))
|
|
2771
|
+
return null;
|
|
2772
|
+
const message77 = result.message91.slice(0, 77);
|
|
2773
|
+
if (!isValidMessageType(message77))
|
|
2774
|
+
return null;
|
|
2775
|
+
const { msg, success } = unpack77(message77, book);
|
|
2776
|
+
if (!success || msg.trim().length === 0)
|
|
2777
|
+
return null;
|
|
2778
|
+
const snr = estimateSnr(workspace.s8, result.cw);
|
|
2779
|
+
return { msg, freq: f1, dt: xdt, snr };
|
|
2780
|
+
}
|
|
2781
|
+
function loadCoarseDownsample(cxRe, cxIm, f0, workspace, coarseDownsampleCache, coarseFrequencyUses) {
|
|
2782
|
+
const cached = coarseDownsampleCache.get(f0);
|
|
2783
|
+
if (cached) {
|
|
2784
|
+
workspace.cd0Re.set(cached.re);
|
|
2785
|
+
workspace.cd0Im.set(cached.im);
|
|
2786
|
+
}
|
|
2787
|
+
else {
|
|
2788
|
+
ft8Downsample(cxRe, cxIm, f0, workspace);
|
|
2789
|
+
const uses = coarseFrequencyUses.get(f0) ?? 0;
|
|
2790
|
+
if (uses > 1) {
|
|
2791
|
+
coarseDownsampleCache.set(f0, {
|
|
2792
|
+
re: new Float64Array(workspace.cd0Re),
|
|
2793
|
+
im: new Float64Array(workspace.cd0Im),
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const remaining = (coarseFrequencyUses.get(f0) ?? 1) - 1;
|
|
2798
|
+
if (remaining <= 0) {
|
|
2799
|
+
coarseFrequencyUses.delete(f0);
|
|
2800
|
+
coarseDownsampleCache.delete(f0);
|
|
2801
|
+
}
|
|
2802
|
+
else {
|
|
2803
|
+
coarseFrequencyUses.set(f0, remaining);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
function findBestTimeOffset(cd0Re, cd0Im, xdt) {
|
|
2807
|
+
const i0 = Math.round((xdt + 0.5) * FS2);
|
|
2808
|
+
let smax = 0;
|
|
2809
|
+
let ibest = i0;
|
|
2810
|
+
for (let idt = i0 - 10; idt <= i0 + 10; idt++) {
|
|
2811
|
+
const sync = sync8d(cd0Re, cd0Im, idt, COSTAS_SYNC.re, COSTAS_SYNC.im);
|
|
2812
|
+
if (sync > smax) {
|
|
2813
|
+
smax = sync;
|
|
2814
|
+
ibest = idt;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return ibest;
|
|
2818
|
+
}
|
|
2819
|
+
function findBestFrequencyShift(cd0Re, cd0Im, ibest) {
|
|
2820
|
+
let smax = 0;
|
|
2821
|
+
let delfbest = 0;
|
|
2822
|
+
for (const tpl of FREQ_SHIFT_SYNC) {
|
|
2823
|
+
const sync = sync8d(cd0Re, cd0Im, ibest, tpl.re, tpl.im);
|
|
2824
|
+
if (sync > smax) {
|
|
2825
|
+
smax = sync;
|
|
2826
|
+
delfbest = tpl.delf;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return delfbest;
|
|
2830
|
+
}
|
|
2831
|
+
function refineTimeOffset(cd0Re, cd0Im, ibest, ss) {
|
|
2832
|
+
for (let idt = -4; idt <= 4; idt++) {
|
|
2833
|
+
ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt, COSTAS_SYNC.re, COSTAS_SYNC.im);
|
|
2834
|
+
}
|
|
2835
|
+
let maxss = -1;
|
|
2836
|
+
let maxIdx = 4;
|
|
2837
|
+
for (let i = 0; i < 9; i++) {
|
|
2838
|
+
if (ss[i] > maxss) {
|
|
2839
|
+
maxss = ss[i];
|
|
2840
|
+
maxIdx = i;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return ibest + maxIdx - 4;
|
|
2844
|
+
}
|
|
2845
|
+
function extractSoftSymbols(cd0Re, cd0Im, ibest, workspace) {
|
|
2846
|
+
const { s8, csRe, csIm, symbRe, symbIm } = workspace;
|
|
2847
|
+
for (let k = 0; k < NN; k++) {
|
|
2848
|
+
const i1 = ibest + k * COSTAS_SYMBOL_LEN;
|
|
2849
|
+
symbRe.fill(0);
|
|
2850
|
+
symbIm.fill(0);
|
|
2851
|
+
if (i1 >= 0 && i1 + COSTAS_SYMBOL_LEN - 1 < NP2) {
|
|
2852
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
2853
|
+
symbRe[j] = cd0Re[i1 + j];
|
|
2854
|
+
symbIm[j] = cd0Im[i1 + j];
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
fftComplex(symbRe, symbIm, false);
|
|
2858
|
+
for (let tone = 0; tone < 8; tone++) {
|
|
2859
|
+
const re = symbRe[tone] / 1000;
|
|
2860
|
+
const im = symbIm[tone] / 1000;
|
|
2861
|
+
const idx = tone * NN + k;
|
|
2862
|
+
csRe[idx] = re;
|
|
2863
|
+
csIm[idx] = im;
|
|
2864
|
+
s8[idx] = Math.sqrt(re * re + im * im);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
function passesSyncGate(s8, minCostasHits) {
|
|
2869
|
+
let nsync = 0;
|
|
2870
|
+
for (let k = 0; k < COSTAS_BLOCKS; k++) {
|
|
2871
|
+
for (const offset of SYNC_TIME_SHIFTS) {
|
|
2872
|
+
let maxTone = 0;
|
|
2873
|
+
let maxVal = -1;
|
|
2874
|
+
for (let t = 0; t < 8; t++) {
|
|
2875
|
+
const v = s8[t * NN + k + offset];
|
|
2876
|
+
if (v > maxVal) {
|
|
2877
|
+
maxVal = v;
|
|
2878
|
+
maxTone = t;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
if (maxTone === COSTAS[k])
|
|
2882
|
+
nsync++;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return nsync >= minCostasHits;
|
|
2886
|
+
}
|
|
2887
|
+
function buildBitMetrics(workspace) {
|
|
2888
|
+
const { csRe, csIm, bmeta, bmetb, bmetc, bmetd, s2 } = workspace;
|
|
2889
|
+
bmeta.fill(0);
|
|
2890
|
+
bmetb.fill(0);
|
|
2891
|
+
bmetc.fill(0);
|
|
2892
|
+
bmetd.fill(0);
|
|
2893
|
+
for (let nsym = 1; nsym <= 3; nsym++) {
|
|
2894
|
+
const nt = 1 << (3 * nsym);
|
|
2895
|
+
const ibmax = nsym === 1 ? 2 : nsym === 2 ? 5 : 8;
|
|
2896
|
+
for (let ihalf = 1; ihalf <= 2; ihalf++) {
|
|
2897
|
+
for (let k = 1; k <= 29; k += nsym) {
|
|
2898
|
+
const ks = ihalf === 1 ? k + 7 : k + 43;
|
|
2899
|
+
for (let i = 0; i < nt; i++) {
|
|
2900
|
+
const i1 = Math.floor(i / 64);
|
|
2901
|
+
const i2 = Math.floor((i & 63) / 8);
|
|
2902
|
+
const i3 = i & 7;
|
|
2903
|
+
if (nsym === 1) {
|
|
2904
|
+
const re = csRe[GRAY_MAP[i3] * NN + ks - 1];
|
|
2905
|
+
const im = csIm[GRAY_MAP[i3] * NN + ks - 1];
|
|
2906
|
+
s2[i] = Math.sqrt(re * re + im * im);
|
|
2907
|
+
}
|
|
2908
|
+
else if (nsym === 2) {
|
|
2909
|
+
const sRe = csRe[GRAY_MAP[i2] * NN + ks - 1] + csRe[GRAY_MAP[i3] * NN + ks];
|
|
2910
|
+
const sIm = csIm[GRAY_MAP[i2] * NN + ks - 1] + csIm[GRAY_MAP[i3] * NN + ks];
|
|
2911
|
+
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
const sRe = csRe[GRAY_MAP[i1] * NN + ks - 1] +
|
|
2915
|
+
csRe[GRAY_MAP[i2] * NN + ks] +
|
|
2916
|
+
csRe[GRAY_MAP[i3] * NN + ks + 1];
|
|
2917
|
+
const sIm = csIm[GRAY_MAP[i1] * NN + ks - 1] +
|
|
2918
|
+
csIm[GRAY_MAP[i2] * NN + ks] +
|
|
2919
|
+
csIm[GRAY_MAP[i3] * NN + ks + 1];
|
|
2920
|
+
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
|
|
2924
|
+
for (let ib = 0; ib <= ibmax; ib++) {
|
|
2925
|
+
let max1 = -1e30;
|
|
2926
|
+
let max0 = -1e30;
|
|
2927
|
+
for (let i = 0; i < nt; i++) {
|
|
2928
|
+
const bitSet = (i & (1 << (ibmax - ib))) !== 0;
|
|
2929
|
+
if (bitSet) {
|
|
2930
|
+
if (s2[i] > max1)
|
|
2931
|
+
max1 = s2[i];
|
|
2932
|
+
}
|
|
2933
|
+
else {
|
|
2934
|
+
if (s2[i] > max0)
|
|
2935
|
+
max0 = s2[i];
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
const idx = i32 + ib - 1;
|
|
2939
|
+
if (idx < 0 || idx >= N_LDPC)
|
|
2940
|
+
continue;
|
|
2941
|
+
const bm = max1 - max0;
|
|
2942
|
+
if (nsym === 1) {
|
|
2943
|
+
bmeta[idx] = bm;
|
|
2944
|
+
const den = Math.max(max1, max0);
|
|
2945
|
+
bmetd[idx] = den > 0 ? bm / den : 0;
|
|
2946
|
+
}
|
|
2947
|
+
else if (nsym === 2) {
|
|
2948
|
+
bmetb[idx] = bm;
|
|
2949
|
+
}
|
|
2950
|
+
else {
|
|
2951
|
+
bmetc[idx] = bm;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
normalizeBmet(bmeta);
|
|
2958
|
+
normalizeBmet(bmetb);
|
|
2959
|
+
normalizeBmet(bmetc);
|
|
2960
|
+
normalizeBmet(bmetd);
|
|
2961
|
+
}
|
|
2962
|
+
function tryDecodePasses(workspace, depth) {
|
|
2963
|
+
const scalefac = 2.83;
|
|
2964
|
+
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
2965
|
+
const bmetrics = [workspace.bmeta, workspace.bmetb, workspace.bmetc, workspace.bmetd];
|
|
2966
|
+
workspace.apmask.fill(0);
|
|
2967
|
+
for (let ipass = 0; ipass < 4; ipass++) {
|
|
2968
|
+
const metric = bmetrics[ipass];
|
|
2969
|
+
for (let i = 0; i < N_LDPC; i++)
|
|
2970
|
+
workspace.llr[i] = scalefac * metric[i];
|
|
2971
|
+
const result = decode174_91(workspace.llr, workspace.apmask, maxosd);
|
|
2972
|
+
if (result && result.nharderrors >= 0 && result.nharderrors <= 36)
|
|
2973
|
+
return result;
|
|
2974
|
+
}
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
function isValidMessageType(message77) {
|
|
2978
|
+
const n3v = (message77[71] << 2) | (message77[72] << 1) | message77[73];
|
|
2979
|
+
const i3v = (message77[74] << 2) | (message77[75] << 1) | message77[76];
|
|
2980
|
+
if (i3v > 5 || (i3v === 0 && n3v > 6))
|
|
2981
|
+
return false;
|
|
2982
|
+
if (i3v === 0 && n3v === 2)
|
|
2983
|
+
return false;
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
function estimateSnr(s8, cw) {
|
|
2987
|
+
let xsig = 0;
|
|
2988
|
+
let xnoi = 0;
|
|
2989
|
+
const itone = getTones(cw);
|
|
2990
|
+
for (let i = 0; i < 79; i++) {
|
|
2991
|
+
xsig += s8[itone[i] * NN + i] ** 2;
|
|
2992
|
+
const ios = (itone[i] + 4) % 7;
|
|
2993
|
+
xnoi += s8[ios * NN + i] ** 2;
|
|
2994
|
+
}
|
|
2995
|
+
let snr = 0.001;
|
|
2996
|
+
const arg = xsig / Math.max(xnoi, 1e-30) - 1.0;
|
|
2997
|
+
if (arg > 0.1)
|
|
2998
|
+
snr = arg;
|
|
2999
|
+
snr = 10 * Math.log10(snr) - 27.0;
|
|
3000
|
+
return snr < -24 ? -24 : snr;
|
|
3001
|
+
}
|
|
3002
|
+
function getTones(cw) {
|
|
3003
|
+
const tones = new Array(79).fill(0);
|
|
3004
|
+
for (let i = 0; i < 7; i++)
|
|
3005
|
+
tones[i] = COSTAS[i];
|
|
3006
|
+
for (let i = 0; i < 7; i++)
|
|
3007
|
+
tones[36 + i] = COSTAS[i];
|
|
3008
|
+
for (let i = 0; i < 7; i++)
|
|
3009
|
+
tones[72 + i] = COSTAS[i];
|
|
3010
|
+
let k = 7;
|
|
3011
|
+
for (let j = 1; j <= 58; j++) {
|
|
3012
|
+
const i = (j - 1) * 3;
|
|
3013
|
+
if (j === 30)
|
|
3014
|
+
k += 7;
|
|
3015
|
+
const indx = cw[i] * 4 + cw[i + 1] * 2 + cw[i + 2];
|
|
3016
|
+
tones[k] = GRAY_MAP[indx];
|
|
3017
|
+
k++;
|
|
3018
|
+
}
|
|
3019
|
+
return tones;
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
3023
|
+
* Identical to Fortran ft8_downsample.
|
|
3024
|
+
*/
|
|
3025
|
+
function ft8Downsample(cxRe, cxIm, f0, workspace) {
|
|
3026
|
+
const { cd0Re, cd0Im, shiftRe, shiftIm } = workspace;
|
|
3027
|
+
const df = DOWNSAMPLE_DF;
|
|
3028
|
+
const baud = DOWNSAMPLE_BAUD;
|
|
3029
|
+
const i0 = Math.round(f0 / df);
|
|
3030
|
+
const ft = f0 + 8.5 * baud;
|
|
3031
|
+
const it = Math.min(Math.round(ft / df), NFFT1_LONG / 2);
|
|
3032
|
+
const fb = f0 - 1.5 * baud;
|
|
3033
|
+
const ib = Math.max(1, Math.round(fb / df));
|
|
3034
|
+
cd0Re.fill(0);
|
|
3035
|
+
cd0Im.fill(0);
|
|
3036
|
+
let k = 0;
|
|
3037
|
+
for (let i = ib; i <= it; i++) {
|
|
3038
|
+
if (k >= NFFT2)
|
|
3039
|
+
break;
|
|
3040
|
+
cd0Re[k] = cxRe[i];
|
|
3041
|
+
cd0Im[k] = cxIm[i];
|
|
3042
|
+
k++;
|
|
3043
|
+
}
|
|
3044
|
+
for (let i = 0; i <= TAPER_LAST; i++) {
|
|
3045
|
+
if (i >= NFFT2)
|
|
3046
|
+
break;
|
|
3047
|
+
const tap = TAPER[TAPER_LAST - i];
|
|
3048
|
+
cd0Re[i] = cd0Re[i] * tap;
|
|
3049
|
+
cd0Im[i] = cd0Im[i] * tap;
|
|
3050
|
+
}
|
|
3051
|
+
const endTap = k - 1;
|
|
3052
|
+
for (let i = 0; i <= TAPER_LAST; i++) {
|
|
3053
|
+
const idx = endTap - TAPER_LAST + i;
|
|
3054
|
+
if (idx >= 0 && idx < NFFT2) {
|
|
3055
|
+
const tap = TAPER[i];
|
|
3056
|
+
cd0Re[idx] = cd0Re[idx] * tap;
|
|
3057
|
+
cd0Im[idx] = cd0Im[idx] * tap;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
const shift = i0 - ib;
|
|
3061
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
3062
|
+
let srcIdx = (i + shift) % NFFT2;
|
|
3063
|
+
if (srcIdx < 0)
|
|
3064
|
+
srcIdx += NFFT2;
|
|
3065
|
+
shiftRe[i] = cd0Re[srcIdx];
|
|
3066
|
+
shiftIm[i] = cd0Im[srcIdx];
|
|
3067
|
+
}
|
|
3068
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
3069
|
+
cd0Re[i] = shiftRe[i];
|
|
3070
|
+
cd0Im[i] = shiftIm[i];
|
|
3071
|
+
}
|
|
3072
|
+
fftComplex(cd0Re, cd0Im, true);
|
|
3073
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
3074
|
+
cd0Re[i] = cd0Re[i] * DOWNSAMPLE_SCALE;
|
|
3075
|
+
cd0Im[i] = cd0Im[i] * DOWNSAMPLE_SCALE;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
function sync8d(cd0Re, cd0Im, i0, syncRe, syncIm) {
|
|
3079
|
+
let sync = 0;
|
|
3080
|
+
const stride = 36 * COSTAS_SYMBOL_LEN;
|
|
3081
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3082
|
+
const base = i * COSTAS_SYMBOL_LEN;
|
|
3083
|
+
let iStart = i0 + i * COSTAS_SYMBOL_LEN;
|
|
3084
|
+
for (let block = 0; block < 3; block++, iStart += stride) {
|
|
3085
|
+
if (iStart < 0 || iStart + COSTAS_SYMBOL_LEN - 1 >= NP2)
|
|
3086
|
+
continue;
|
|
3087
|
+
let zRe = 0;
|
|
3088
|
+
let zIm = 0;
|
|
3089
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3090
|
+
const sRe = syncRe[base + j];
|
|
3091
|
+
const sIm = syncIm[base + j];
|
|
3092
|
+
const dRe = cd0Re[iStart + j];
|
|
3093
|
+
const dIm = cd0Im[iStart + j];
|
|
3094
|
+
zRe += dRe * sRe + dIm * sIm;
|
|
3095
|
+
zIm += dIm * sRe - dRe * sIm;
|
|
3096
|
+
}
|
|
3097
|
+
sync += zRe * zRe + zIm * zIm;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return sync;
|
|
3101
|
+
}
|
|
3102
|
+
function normalizeBmet(bmet) {
|
|
3103
|
+
const n = bmet.length;
|
|
3104
|
+
let sum = 0;
|
|
3105
|
+
let sum2 = 0;
|
|
3106
|
+
for (let i = 0; i < n; i++) {
|
|
3107
|
+
sum += bmet[i];
|
|
3108
|
+
sum2 += bmet[i] * bmet[i];
|
|
3109
|
+
}
|
|
3110
|
+
const avg = sum / n;
|
|
3111
|
+
const avg2 = sum2 / n;
|
|
3112
|
+
const variance = avg2 - avg * avg;
|
|
3113
|
+
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
3114
|
+
if (sigma > 0) {
|
|
3115
|
+
for (let i = 0; i < n; i++)
|
|
3116
|
+
bmet[i] = bmet[i] / sigma;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
function resample(input, fromRate, toRate, outLen) {
|
|
3120
|
+
const out = new Float64Array(outLen);
|
|
3121
|
+
const ratio = fromRate / toRate;
|
|
3122
|
+
for (let i = 0; i < outLen; i++) {
|
|
3123
|
+
const srcIdx = i * ratio;
|
|
3124
|
+
const lo = Math.floor(srcIdx);
|
|
3125
|
+
const frac = srcIdx - lo;
|
|
3126
|
+
const v0 = lo < input.length ? (input[lo] ?? 0) : 0;
|
|
3127
|
+
const v1 = lo + 1 < input.length ? (input[lo + 1] ?? 0) : 0;
|
|
3128
|
+
out[i] = v0 * (1 - frac) + v1 * frac;
|
|
3129
|
+
}
|
|
3130
|
+
return out;
|
|
3131
|
+
}
|
|
3132
|
+
function subtractDecodedSignal(residual, result, toneCache) {
|
|
3133
|
+
if (result.snr < MIN_SUBTRACTION_SNR)
|
|
3134
|
+
return;
|
|
3135
|
+
const msgKey = normalizeMessageKey(result.msg);
|
|
3136
|
+
let tones = toneCache.get(msgKey);
|
|
3137
|
+
if (!tones) {
|
|
3138
|
+
try {
|
|
3139
|
+
tones = encodeMessage(result.msg);
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
toneCache.set(msgKey, tones);
|
|
3145
|
+
}
|
|
3146
|
+
const waveI = generateFT8Waveform(tones, {
|
|
3147
|
+
sampleRate: SAMPLE_RATE$1,
|
|
3148
|
+
samplesPerSymbol: NSPS,
|
|
3149
|
+
baseFrequency: result.freq,
|
|
3150
|
+
initialPhase: 0,
|
|
3151
|
+
});
|
|
3152
|
+
const waveQ = generateFT8Waveform(tones, {
|
|
3153
|
+
sampleRate: SAMPLE_RATE$1,
|
|
3154
|
+
samplesPerSymbol: NSPS,
|
|
3155
|
+
baseFrequency: result.freq,
|
|
3156
|
+
initialPhase: SUBTRACTION_PHASE_SHIFT,
|
|
3157
|
+
});
|
|
3158
|
+
const start = Math.round(result.dt * SAMPLE_RATE$1);
|
|
3159
|
+
let srcStart = start;
|
|
3160
|
+
let tplStart = 0;
|
|
3161
|
+
if (srcStart < 0) {
|
|
3162
|
+
tplStart = -srcStart;
|
|
3163
|
+
srcStart = 0;
|
|
3164
|
+
}
|
|
3165
|
+
const maxLen = Math.min(residual.length - srcStart, waveI.length - tplStart, waveQ.length - tplStart);
|
|
3166
|
+
if (maxLen <= 0)
|
|
3167
|
+
return;
|
|
3168
|
+
let sii = 0;
|
|
3169
|
+
let sqq = 0;
|
|
3170
|
+
let siq = 0;
|
|
3171
|
+
let sri = 0;
|
|
3172
|
+
let srq = 0;
|
|
3173
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3174
|
+
const wi = waveI[tplStart + i];
|
|
3175
|
+
const wq = waveQ[tplStart + i];
|
|
3176
|
+
const rv = residual[srcStart + i];
|
|
3177
|
+
sii += wi * wi;
|
|
3178
|
+
sqq += wq * wq;
|
|
3179
|
+
siq += wi * wq;
|
|
3180
|
+
sri += rv * wi;
|
|
3181
|
+
srq += rv * wq;
|
|
3182
|
+
}
|
|
3183
|
+
const det = sii * sqq - siq * siq;
|
|
3184
|
+
if (det <= 1e-9)
|
|
3185
|
+
return;
|
|
3186
|
+
const ampI = (sri * sqq - srq * siq) / det;
|
|
3187
|
+
const ampQ = (srq * sii - sri * siq) / det;
|
|
3188
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3189
|
+
const wi = waveI[tplStart + i];
|
|
3190
|
+
const wq = waveQ[tplStart + i];
|
|
3191
|
+
const idx = srcStart + i;
|
|
3192
|
+
residual[idx] = residual[idx] - SUBTRACTION_GAIN * (ampI * wi + ampQ * wq);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
function buildTaper(size) {
|
|
3196
|
+
const taper = new Float64Array(size);
|
|
3197
|
+
const last = size - 1;
|
|
3198
|
+
for (let i = 0; i < size; i++)
|
|
3199
|
+
taper[i] = 0.5 * (1.0 + Math.cos((i * Math.PI) / last));
|
|
3200
|
+
return taper;
|
|
3201
|
+
}
|
|
3202
|
+
function buildCostasSyncTemplates() {
|
|
3203
|
+
const re = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3204
|
+
const im = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3205
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3206
|
+
let phi = 0;
|
|
3207
|
+
const dphi = (TWO_PI * COSTAS[i]) / COSTAS_SYMBOL_LEN;
|
|
3208
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3209
|
+
re[i * COSTAS_SYMBOL_LEN + j] = Math.cos(phi);
|
|
3210
|
+
im[i * COSTAS_SYMBOL_LEN + j] = Math.sin(phi);
|
|
3211
|
+
phi = (phi + dphi) % TWO_PI;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
return { re, im };
|
|
3215
|
+
}
|
|
3216
|
+
function buildFrequencyShiftSyncTemplates() {
|
|
3217
|
+
const templates = [];
|
|
3218
|
+
for (let ifr = -5; ifr <= 5; ifr++) {
|
|
3219
|
+
const delf = ifr * 0.5;
|
|
3220
|
+
const dphi = TWO_PI * delf * DT2;
|
|
3221
|
+
const twkRe = new Float64Array(COSTAS_SYMBOL_LEN);
|
|
3222
|
+
const twkIm = new Float64Array(COSTAS_SYMBOL_LEN);
|
|
3223
|
+
let phi = 0;
|
|
3224
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3225
|
+
twkRe[j] = Math.cos(phi);
|
|
3226
|
+
twkIm[j] = Math.sin(phi);
|
|
3227
|
+
phi = (phi + dphi) % TWO_PI;
|
|
3228
|
+
}
|
|
3229
|
+
const re = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3230
|
+
const im = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3231
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3232
|
+
const base = i * COSTAS_SYMBOL_LEN;
|
|
3233
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3234
|
+
const idx = base + j;
|
|
3235
|
+
const csRe = COSTAS_SYNC.re[idx];
|
|
3236
|
+
const csIm = COSTAS_SYNC.im[idx];
|
|
3237
|
+
const tRe = twkRe[j] * csRe - twkIm[j] * csIm;
|
|
3238
|
+
const tIm = twkRe[j] * csIm + twkIm[j] * csRe;
|
|
3239
|
+
re[idx] = tRe;
|
|
3240
|
+
im[idx] = tIm;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
templates.push({ delf, re, im });
|
|
3244
|
+
}
|
|
3245
|
+
return templates;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
/// <reference types="node" />
|
|
3249
|
+
/**
|
|
3250
|
+
* Parse a WAV file buffer into sample rate and normalized float samples.
|
|
3251
|
+
* Supports PCM format 1, 8/16/32-bit samples.
|
|
3252
|
+
*/
|
|
3253
|
+
function parseWavBuffer(buf) {
|
|
3254
|
+
if (buf.length < 44)
|
|
3255
|
+
throw new Error("File too small for WAV");
|
|
3256
|
+
const riff = buf.toString("ascii", 0, 4);
|
|
3257
|
+
const wave = buf.toString("ascii", 8, 12);
|
|
3258
|
+
if (riff !== "RIFF" || wave !== "WAVE")
|
|
3259
|
+
throw new Error("Not a WAV file");
|
|
3260
|
+
let offset = 12;
|
|
3261
|
+
let fmtFound = false;
|
|
3262
|
+
let sampleRate = 0;
|
|
3263
|
+
let bitsPerSample = 0;
|
|
3264
|
+
let numChannels = 1;
|
|
3265
|
+
let audioFormat = 0;
|
|
3266
|
+
let dataOffset = 0;
|
|
3267
|
+
let dataSize = 0;
|
|
3268
|
+
while (offset < buf.length - 8) {
|
|
3269
|
+
const chunkId = buf.toString("ascii", offset, offset + 4);
|
|
3270
|
+
const chunkSize = buf.readUInt32LE(offset + 4);
|
|
3271
|
+
offset += 8;
|
|
3272
|
+
if (chunkId === "fmt ") {
|
|
3273
|
+
audioFormat = buf.readUInt16LE(offset);
|
|
3274
|
+
numChannels = buf.readUInt16LE(offset + 2);
|
|
3275
|
+
sampleRate = buf.readUInt32LE(offset + 4);
|
|
3276
|
+
bitsPerSample = buf.readUInt16LE(offset + 14);
|
|
3277
|
+
fmtFound = true;
|
|
3278
|
+
}
|
|
3279
|
+
else if (chunkId === "data") {
|
|
3280
|
+
dataOffset = offset;
|
|
3281
|
+
dataSize = chunkSize;
|
|
3282
|
+
break;
|
|
3283
|
+
}
|
|
3284
|
+
offset += chunkSize;
|
|
3285
|
+
}
|
|
3286
|
+
if (!fmtFound)
|
|
3287
|
+
throw new Error("No fmt chunk found");
|
|
3288
|
+
if (audioFormat !== 1)
|
|
3289
|
+
throw new Error(`Unsupported audio format: ${audioFormat} (only PCM=1)`);
|
|
2263
3290
|
if (dataOffset === 0)
|
|
2264
3291
|
throw new Error("No data chunk found");
|
|
2265
3292
|
const bytesPerSample = bitsPerSample / 8;
|
|
@@ -2341,9 +3368,11 @@ Usage:
|
|
|
2341
3368
|
ft8ts encode "<message>" [options]
|
|
2342
3369
|
|
|
2343
3370
|
Decode options:
|
|
3371
|
+
--mode <ft8|ft4> Mode: ft8 (default) or ft4
|
|
2344
3372
|
--low <hz> Lower frequency bound (default: 200)
|
|
2345
3373
|
--high <hz> Upper frequency bound (default: 3000)
|
|
2346
3374
|
--depth <1|2|3> Decoding depth (default: 2)
|
|
3375
|
+
--max-candidates <n> Max candidate signals to decode (default: 300)
|
|
2347
3376
|
|
|
2348
3377
|
Encode options:
|
|
2349
3378
|
--out <file> Output WAV file (default: output.wav)
|
|
@@ -2364,9 +3393,19 @@ function runDecode(argv) {
|
|
|
2364
3393
|
}
|
|
2365
3394
|
const wavFile = argv[0];
|
|
2366
3395
|
const options = {};
|
|
3396
|
+
let mode = "ft8";
|
|
2367
3397
|
for (let i = 1; i < argv.length; i++) {
|
|
2368
3398
|
const arg = argv[i];
|
|
2369
|
-
if (arg === "--
|
|
3399
|
+
if (arg === "--mode") {
|
|
3400
|
+
const value = argv[++i];
|
|
3401
|
+
if (value === "ft8" || value === "ft4") {
|
|
3402
|
+
mode = value;
|
|
3403
|
+
}
|
|
3404
|
+
else {
|
|
3405
|
+
throw new Error(`Invalid --mode: ${value ?? "(missing)"}. Use ft8 or ft4`);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
else if (arg === "--low") {
|
|
2370
3409
|
options.freqLow = Number(argv[++i]);
|
|
2371
3410
|
}
|
|
2372
3411
|
else if (arg === "--high") {
|
|
@@ -2375,6 +3414,9 @@ function runDecode(argv) {
|
|
|
2375
3414
|
else if (arg === "--depth") {
|
|
2376
3415
|
options.depth = Number(argv[++i]);
|
|
2377
3416
|
}
|
|
3417
|
+
else if (arg === "--max-candidates") {
|
|
3418
|
+
options.maxCandidates = Number(argv[++i]);
|
|
3419
|
+
}
|
|
2378
3420
|
else {
|
|
2379
3421
|
throw new Error(`Unknown argument: ${arg}`);
|
|
2380
3422
|
}
|
|
@@ -2384,7 +3426,9 @@ function runDecode(argv) {
|
|
|
2384
3426
|
const { sampleRate, samples } = parseWavBuffer(readFileSync(filePath));
|
|
2385
3427
|
console.log(`WAV: ${sampleRate} Hz, ${samples.length} samples, ${(samples.length / sampleRate).toFixed(1)}s`);
|
|
2386
3428
|
const startTime = performance.now();
|
|
2387
|
-
const decoded =
|
|
3429
|
+
const decoded = mode === "ft4"
|
|
3430
|
+
? decode$1(samples, { ...options, sampleRate })
|
|
3431
|
+
: decode(samples, { ...options, sampleRate });
|
|
2388
3432
|
const elapsed = performance.now() - startTime;
|
|
2389
3433
|
console.log(`\nDecoded ${decoded.length} messages in ${(elapsed / 1000).toFixed(2)}s:\n`);
|
|
2390
3434
|
console.log(" dt snr freq message");
|