@e04/ft8ts 0.0.9 → 0.0.11
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 +27 -26
- package/dist/cli.js +822 -612
- package/dist/cli.js.map +1 -1
- package/dist/ft8ts.cjs +829 -619
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +1 -0
- package/dist/ft8ts.mjs +829 -619
- package/dist/ft8ts.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -0
- package/src/ft4/constants.ts +0 -38
- package/src/ft4/decode.ts +297 -338
- package/src/ft4/encode.ts +6 -1
- package/src/ft4/scramble.ts +6 -1
- package/src/ft8/constants.ts +0 -11
- package/src/ft8/decode.ts +530 -270
- package/src/util/constants.ts +0 -4
- package/src/util/decode174_91.ts +61 -55
- package/src/util/fft.ts +97 -37
- package/src/util/pack_jt77.ts +14 -2
- package/src/util/waveform.ts +8 -4
package/dist/ft8ts.cjs
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
/** Shared constants used by FT8, FT4, pack77, etc. */
|
|
4
4
|
const SAMPLE_RATE = 12_000;
|
|
5
5
|
/** LDPC(174,91) code (shared by FT8 and FT4). */
|
|
6
|
-
const KK = 91;
|
|
7
6
|
const N_LDPC = 174;
|
|
8
|
-
const M_LDPC = N_LDPC - KK; // 83
|
|
9
7
|
const gHex = [
|
|
10
8
|
"8329ce11bf31eaf509f27fc",
|
|
11
9
|
"761c264e25c259335493132",
|
|
@@ -230,6 +228,8 @@ for (let i = 0; i < 83; i++) {
|
|
|
230
228
|
* LDPC (174,91) Belief Propagation decoder for FT8.
|
|
231
229
|
* Port of bpdecode174_91.f90 and decode174_91.f90.
|
|
232
230
|
*/
|
|
231
|
+
const KK = 91;
|
|
232
|
+
const M_LDPC = N_LDPC - KK; // 83
|
|
233
233
|
function platanh(x) {
|
|
234
234
|
if (x > 0.9999999)
|
|
235
235
|
return 18.71;
|
|
@@ -378,36 +378,48 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
378
378
|
const N = N_LDPC;
|
|
379
379
|
const K = KK;
|
|
380
380
|
const gen = getGenerator();
|
|
381
|
+
const absllr = new Float64Array(N);
|
|
382
|
+
for (let i = 0; i < N; i++)
|
|
383
|
+
absllr[i] = Math.abs(llr[i]);
|
|
381
384
|
// Sort by reliability (descending)
|
|
382
|
-
const indices = Array
|
|
383
|
-
|
|
385
|
+
const indices = new Array(N);
|
|
386
|
+
for (let i = 0; i < N; i++)
|
|
387
|
+
indices[i] = i;
|
|
388
|
+
indices.sort((a, b) => absllr[b] - absllr[a]);
|
|
384
389
|
// Reorder generator matrix columns
|
|
385
390
|
const genmrb = new Uint8Array(K * N);
|
|
386
|
-
for (let
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
for (let k = 0; k < K; k++) {
|
|
392
|
+
const row = k * N;
|
|
393
|
+
for (let i = 0; i < N; i++) {
|
|
394
|
+
genmrb[row + i] = gen[row + indices[i]];
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
// Gaussian elimination to get systematic form on the K most-reliable bits
|
|
398
|
+
const maxPivotCol = Math.min(K + 20, N);
|
|
392
399
|
for (let id = 0; id < K; id++) {
|
|
393
400
|
let found = false;
|
|
394
|
-
|
|
395
|
-
|
|
401
|
+
const idRow = id * N;
|
|
402
|
+
for (let icol = id; icol < maxPivotCol; icol++) {
|
|
403
|
+
if (genmrb[idRow + icol] === 1) {
|
|
396
404
|
if (icol !== id) {
|
|
397
405
|
// Swap columns
|
|
398
406
|
for (let k = 0; k < K; k++) {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
genmrb[
|
|
407
|
+
const row = k * N;
|
|
408
|
+
const tmp = genmrb[row + id];
|
|
409
|
+
genmrb[row + id] = genmrb[row + icol];
|
|
410
|
+
genmrb[row + icol] = tmp;
|
|
402
411
|
}
|
|
403
412
|
const tmp = indices[id];
|
|
404
413
|
indices[id] = indices[icol];
|
|
405
414
|
indices[icol] = tmp;
|
|
406
415
|
}
|
|
407
416
|
for (let ii = 0; ii < K; ii++) {
|
|
408
|
-
if (ii
|
|
417
|
+
if (ii === id)
|
|
418
|
+
continue;
|
|
419
|
+
const iiRow = ii * N;
|
|
420
|
+
if (genmrb[iiRow + id] === 1) {
|
|
409
421
|
for (let c = 0; c < N; c++) {
|
|
410
|
-
genmrb[
|
|
422
|
+
genmrb[iiRow + c] ^= genmrb[idRow + c];
|
|
411
423
|
}
|
|
412
424
|
}
|
|
413
425
|
}
|
|
@@ -421,80 +433,82 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
421
433
|
// Hard decisions on reordered received word
|
|
422
434
|
const hdec = new Int8Array(N);
|
|
423
435
|
for (let i = 0; i < N; i++) {
|
|
424
|
-
|
|
436
|
+
const idx = indices[i];
|
|
437
|
+
hdec[i] = llr[idx] >= 0 ? 1 : 0;
|
|
425
438
|
}
|
|
426
439
|
const absrx = new Float64Array(N);
|
|
427
440
|
for (let i = 0; i < N; i++) {
|
|
428
|
-
absrx[i] =
|
|
441
|
+
absrx[i] = absllr[indices[i]];
|
|
429
442
|
}
|
|
430
|
-
//
|
|
431
|
-
const
|
|
443
|
+
// Encode hard decision on MRB (c0): xor selected rows of genmrb.
|
|
444
|
+
const c0 = new Int8Array(N);
|
|
432
445
|
for (let i = 0; i < K; i++) {
|
|
446
|
+
if (hdec[i] !== 1)
|
|
447
|
+
continue;
|
|
448
|
+
const row = i * N;
|
|
433
449
|
for (let j = 0; j < N; j++) {
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
function mrbencode(me) {
|
|
438
|
-
const codeword = new Int8Array(N);
|
|
439
|
-
for (let i = 0; i < K; i++) {
|
|
440
|
-
if (me[i] === 1) {
|
|
441
|
-
for (let j = 0; j < N; j++) {
|
|
442
|
-
codeword[j] ^= g2[j * K + i];
|
|
443
|
-
}
|
|
444
|
-
}
|
|
450
|
+
c0[j] ^= genmrb[row + j];
|
|
445
451
|
}
|
|
446
|
-
return codeword;
|
|
447
452
|
}
|
|
448
|
-
const m0 = hdec.slice(0, K);
|
|
449
|
-
const c0 = mrbencode(m0);
|
|
450
|
-
const bestCw = new Int8Array(c0);
|
|
451
453
|
let dmin = 0;
|
|
452
454
|
for (let i = 0; i < N; i++) {
|
|
453
455
|
const x = c0[i] ^ hdec[i];
|
|
454
456
|
dmin += x * absrx[i];
|
|
455
457
|
}
|
|
458
|
+
let bestFlip1 = -1;
|
|
459
|
+
let bestFlip2 = -1;
|
|
456
460
|
// Order-1: flip single bits in the info portion
|
|
457
461
|
for (let i1 = K - 1; i1 >= 0; i1--) {
|
|
458
462
|
if (apmask[indices[i1]] === 1)
|
|
459
463
|
continue;
|
|
460
|
-
const
|
|
461
|
-
me[i1] ^= 1;
|
|
462
|
-
const ce = mrbencode(me);
|
|
464
|
+
const row1 = i1 * N;
|
|
463
465
|
let dd = 0;
|
|
464
466
|
for (let j = 0; j < N; j++) {
|
|
465
|
-
const x =
|
|
467
|
+
const x = c0[j] ^ genmrb[row1 + j] ^ hdec[j];
|
|
466
468
|
dd += x * absrx[j];
|
|
467
469
|
}
|
|
468
470
|
if (dd < dmin) {
|
|
469
471
|
dmin = dd;
|
|
470
|
-
|
|
472
|
+
bestFlip1 = i1;
|
|
473
|
+
bestFlip2 = -1;
|
|
471
474
|
}
|
|
472
475
|
}
|
|
473
476
|
// Order-2: flip pairs of least-reliable info bits (limited search)
|
|
474
477
|
if (norder >= 2) {
|
|
475
|
-
const ntry = Math.min(
|
|
476
|
-
|
|
478
|
+
const ntry = Math.min(64, K);
|
|
479
|
+
const iMin = Math.max(0, K - ntry);
|
|
480
|
+
for (let i1 = K - 1; i1 >= iMin; i1--) {
|
|
477
481
|
if (apmask[indices[i1]] === 1)
|
|
478
482
|
continue;
|
|
479
|
-
|
|
483
|
+
const row1 = i1 * N;
|
|
484
|
+
for (let i2 = i1 - 1; i2 >= iMin; i2--) {
|
|
480
485
|
if (apmask[indices[i2]] === 1)
|
|
481
486
|
continue;
|
|
482
|
-
const
|
|
483
|
-
me[i1] ^= 1;
|
|
484
|
-
me[i2] ^= 1;
|
|
485
|
-
const ce = mrbencode(me);
|
|
487
|
+
const row2 = i2 * N;
|
|
486
488
|
let dd = 0;
|
|
487
489
|
for (let j = 0; j < N; j++) {
|
|
488
|
-
const x =
|
|
490
|
+
const x = c0[j] ^ genmrb[row1 + j] ^ genmrb[row2 + j] ^ hdec[j];
|
|
489
491
|
dd += x * absrx[j];
|
|
490
492
|
}
|
|
491
493
|
if (dd < dmin) {
|
|
492
494
|
dmin = dd;
|
|
493
|
-
|
|
495
|
+
bestFlip1 = i1;
|
|
496
|
+
bestFlip2 = i2;
|
|
494
497
|
}
|
|
495
498
|
}
|
|
496
499
|
}
|
|
497
500
|
}
|
|
501
|
+
const bestCw = new Int8Array(c0);
|
|
502
|
+
if (bestFlip1 >= 0) {
|
|
503
|
+
const row1 = bestFlip1 * N;
|
|
504
|
+
for (let j = 0; j < N; j++)
|
|
505
|
+
bestCw[j] ^= genmrb[row1 + j];
|
|
506
|
+
if (bestFlip2 >= 0) {
|
|
507
|
+
const row2 = bestFlip2 * N;
|
|
508
|
+
for (let j = 0; j < N; j++)
|
|
509
|
+
bestCw[j] ^= genmrb[row2 + j];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
498
512
|
// Reorder codeword back to original order
|
|
499
513
|
const finalCw = new Int8Array(N);
|
|
500
514
|
for (let i = 0; i < N; i++) {
|
|
@@ -505,14 +519,12 @@ function osdDecode174_91(llr, apmask, norder) {
|
|
|
505
519
|
return null;
|
|
506
520
|
// Compute dmin in original order
|
|
507
521
|
let dminOrig = 0;
|
|
508
|
-
const hdecOrig = new Int8Array(N);
|
|
509
|
-
for (let i = 0; i < N; i++)
|
|
510
|
-
hdecOrig[i] = llr[i] >= 0 ? 1 : 0;
|
|
511
522
|
let nhe = 0;
|
|
512
523
|
for (let i = 0; i < N; i++) {
|
|
513
|
-
const
|
|
524
|
+
const hard = llr[i] >= 0 ? 1 : 0;
|
|
525
|
+
const x = finalCw[i] ^ hard;
|
|
514
526
|
nhe += x;
|
|
515
|
-
dminOrig += x *
|
|
527
|
+
dminOrig += x * absllr[i];
|
|
516
528
|
}
|
|
517
529
|
return {
|
|
518
530
|
message91: bits91,
|
|
@@ -557,6 +569,8 @@ function getGenerator() {
|
|
|
557
569
|
* Radix-2 Cooley-Tukey FFT for FT8 decoding.
|
|
558
570
|
* Supports real-to-complex, complex-to-complex, and inverse transforms.
|
|
559
571
|
*/
|
|
572
|
+
const RADIX2_PLAN_CACHE = new Map();
|
|
573
|
+
const BLUESTEIN_PLAN_CACHE = new Map();
|
|
560
574
|
function fftComplex(re, im, inverse) {
|
|
561
575
|
const n = re.length;
|
|
562
576
|
if (n <= 1)
|
|
@@ -565,9 +579,10 @@ function fftComplex(re, im, inverse) {
|
|
|
565
579
|
bluestein(re, im, inverse);
|
|
566
580
|
return;
|
|
567
581
|
}
|
|
582
|
+
const { bitReversed } = getRadix2Plan(n);
|
|
568
583
|
// Bit-reversal permutation
|
|
569
|
-
let j = 0;
|
|
570
584
|
for (let i = 0; i < n; i++) {
|
|
585
|
+
const j = bitReversed[i];
|
|
571
586
|
if (j > i) {
|
|
572
587
|
let tmp = re[i];
|
|
573
588
|
re[i] = re[j];
|
|
@@ -576,12 +591,6 @@ function fftComplex(re, im, inverse) {
|
|
|
576
591
|
im[i] = im[j];
|
|
577
592
|
im[j] = tmp;
|
|
578
593
|
}
|
|
579
|
-
let m = n >> 1;
|
|
580
|
-
while (m >= 1 && j >= m) {
|
|
581
|
-
j -= m;
|
|
582
|
-
m >>= 1;
|
|
583
|
-
}
|
|
584
|
-
j += m;
|
|
585
594
|
}
|
|
586
595
|
const sign = inverse ? 1 : -1;
|
|
587
596
|
for (let size = 2; size <= n; size <<= 1) {
|
|
@@ -608,53 +617,98 @@ function fftComplex(re, im, inverse) {
|
|
|
608
617
|
}
|
|
609
618
|
}
|
|
610
619
|
if (inverse) {
|
|
620
|
+
const scale = 1 / n;
|
|
611
621
|
for (let i = 0; i < n; i++) {
|
|
612
|
-
re[i]
|
|
613
|
-
im[i]
|
|
622
|
+
re[i] = re[i] * scale;
|
|
623
|
+
im[i] = im[i] * scale;
|
|
614
624
|
}
|
|
615
625
|
}
|
|
616
626
|
}
|
|
617
627
|
function bluestein(re, im, inverse) {
|
|
618
628
|
const n = re.length;
|
|
619
|
-
const m =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const aIm = new Float64Array(m);
|
|
623
|
-
const bRe = new Float64Array(m);
|
|
624
|
-
const bIm = new Float64Array(m);
|
|
629
|
+
const { m, chirpRe, chirpIm, bFftRe, bFftIm, aRe, aIm } = getBluesteinPlan(n, inverse);
|
|
630
|
+
aRe.fill(0);
|
|
631
|
+
aIm.fill(0);
|
|
625
632
|
for (let i = 0; i < n; i++) {
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
bIm[i] = -sinA;
|
|
633
|
-
}
|
|
634
|
-
for (let i = 1; i < n; i++) {
|
|
635
|
-
bRe[m - i] = bRe[i];
|
|
636
|
-
bIm[m - i] = bIm[i];
|
|
633
|
+
const cosA = chirpRe[i];
|
|
634
|
+
const sinA = chirpIm[i];
|
|
635
|
+
const inRe = re[i];
|
|
636
|
+
const inIm = im[i];
|
|
637
|
+
aRe[i] = inRe * cosA - inIm * sinA;
|
|
638
|
+
aIm[i] = inRe * sinA + inIm * cosA;
|
|
637
639
|
}
|
|
638
640
|
fftComplex(aRe, aIm, false);
|
|
639
|
-
fftComplex(bRe, bIm, false);
|
|
640
641
|
for (let i = 0; i < m; i++) {
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
642
|
+
const ar = aRe[i];
|
|
643
|
+
const ai = aIm[i];
|
|
644
|
+
const br = bFftRe[i];
|
|
645
|
+
const bi = bFftIm[i];
|
|
646
|
+
aRe[i] = ar * br - ai * bi;
|
|
647
|
+
aIm[i] = ar * bi + ai * br;
|
|
645
648
|
}
|
|
646
649
|
fftComplex(aRe, aIm, true);
|
|
647
650
|
const scale = inverse ? 1 / n : 1;
|
|
648
651
|
for (let i = 0; i < n; i++) {
|
|
649
|
-
const
|
|
650
|
-
const
|
|
651
|
-
const sinA = Math.sin(angle);
|
|
652
|
+
const cosA = chirpRe[i];
|
|
653
|
+
const sinA = chirpIm[i];
|
|
652
654
|
const r = aRe[i] * cosA - aIm[i] * sinA;
|
|
653
655
|
const iIm = aRe[i] * sinA + aIm[i] * cosA;
|
|
654
656
|
re[i] = r * scale;
|
|
655
657
|
im[i] = iIm * scale;
|
|
656
658
|
}
|
|
657
659
|
}
|
|
660
|
+
function getRadix2Plan(n) {
|
|
661
|
+
let plan = RADIX2_PLAN_CACHE.get(n);
|
|
662
|
+
if (plan)
|
|
663
|
+
return plan;
|
|
664
|
+
const bits = 31 - Math.clz32(n);
|
|
665
|
+
const bitReversed = new Uint32Array(n);
|
|
666
|
+
for (let i = 1; i < n; i++) {
|
|
667
|
+
bitReversed[i] = (bitReversed[i >> 1] >> 1) | ((i & 1) << (bits - 1));
|
|
668
|
+
}
|
|
669
|
+
plan = { bitReversed };
|
|
670
|
+
RADIX2_PLAN_CACHE.set(n, plan);
|
|
671
|
+
return plan;
|
|
672
|
+
}
|
|
673
|
+
function getBluesteinPlan(n, inverse) {
|
|
674
|
+
const key = `${n}:${inverse ? 1 : 0}`;
|
|
675
|
+
const cached = BLUESTEIN_PLAN_CACHE.get(key);
|
|
676
|
+
if (cached)
|
|
677
|
+
return cached;
|
|
678
|
+
const m = nextPow2(n * 2 - 1);
|
|
679
|
+
const s = inverse ? 1 : -1;
|
|
680
|
+
const chirpRe = new Float64Array(n);
|
|
681
|
+
const chirpIm = new Float64Array(n);
|
|
682
|
+
for (let i = 0; i < n; i++) {
|
|
683
|
+
const angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
|
|
684
|
+
chirpRe[i] = Math.cos(angle);
|
|
685
|
+
chirpIm[i] = Math.sin(angle);
|
|
686
|
+
}
|
|
687
|
+
const bFftRe = new Float64Array(m);
|
|
688
|
+
const bFftIm = new Float64Array(m);
|
|
689
|
+
for (let i = 0; i < n; i++) {
|
|
690
|
+
const cosA = chirpRe[i];
|
|
691
|
+
const sinA = chirpIm[i];
|
|
692
|
+
bFftRe[i] = cosA;
|
|
693
|
+
bFftIm[i] = -sinA;
|
|
694
|
+
}
|
|
695
|
+
for (let i = 1; i < n; i++) {
|
|
696
|
+
bFftRe[m - i] = bFftRe[i];
|
|
697
|
+
bFftIm[m - i] = bFftIm[i];
|
|
698
|
+
}
|
|
699
|
+
fftComplex(bFftRe, bFftIm, false);
|
|
700
|
+
const plan = {
|
|
701
|
+
m,
|
|
702
|
+
chirpRe,
|
|
703
|
+
chirpIm,
|
|
704
|
+
bFftRe,
|
|
705
|
+
bFftIm,
|
|
706
|
+
aRe: new Float64Array(m),
|
|
707
|
+
aIm: new Float64Array(m),
|
|
708
|
+
};
|
|
709
|
+
BLUESTEIN_PLAN_CACHE.set(key, plan);
|
|
710
|
+
return plan;
|
|
711
|
+
}
|
|
658
712
|
/** Next power of 2 >= n */
|
|
659
713
|
function nextPow2(n) {
|
|
660
714
|
let v = 1;
|
|
@@ -915,48 +969,54 @@ function unpack77(bits77, book) {
|
|
|
915
969
|
}
|
|
916
970
|
|
|
917
971
|
/** FT4-specific constants (lib/ft4/ft4_params.f90). */
|
|
918
|
-
const COSTAS_A = [0, 1, 3, 2];
|
|
919
|
-
const COSTAS_B = [1, 0, 2, 3];
|
|
920
|
-
const COSTAS_C = [2, 3, 1, 0];
|
|
921
|
-
const COSTAS_D = [3, 2, 0, 1];
|
|
922
972
|
const GRAYMAP = [0, 1, 3, 2];
|
|
973
|
+
|
|
923
974
|
// Message scrambling vector (rvec) from WSJT-X.
|
|
924
975
|
const RVEC = [
|
|
925
976
|
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,
|
|
926
977
|
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,
|
|
927
978
|
1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1,
|
|
928
979
|
];
|
|
980
|
+
function xorWithScrambler(bits77) {
|
|
981
|
+
const out = new Array(77);
|
|
982
|
+
for (let i = 0; i < 77; i++) {
|
|
983
|
+
out[i] = ((bits77[i] ?? 0) + RVEC[i]) & 1;
|
|
984
|
+
}
|
|
985
|
+
return out;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const COSTAS_A$1 = [0, 1, 3, 2];
|
|
989
|
+
const COSTAS_B$1 = [1, 0, 2, 3];
|
|
990
|
+
const COSTAS_C$1 = [2, 3, 1, 0];
|
|
991
|
+
const COSTAS_D$1 = [3, 2, 0, 1];
|
|
929
992
|
const NSPS$1 = 576;
|
|
930
993
|
const NFFT1$1 = 4 * NSPS$1; // 2304
|
|
931
994
|
const NH1 = NFFT1$1 / 2; // 1152
|
|
932
|
-
const NSTEP$1 = NSPS$1;
|
|
933
995
|
const NMAX$1 = 21 * 3456; // 72576
|
|
934
|
-
const NHSYM$1 = Math.floor((NMAX$1 - NFFT1$1) /
|
|
996
|
+
const NHSYM$1 = Math.floor((NMAX$1 - NFFT1$1) / NSPS$1); // 122
|
|
935
997
|
const NDOWN$1 = 18;
|
|
936
|
-
const
|
|
937
|
-
const
|
|
938
|
-
const NN$1 = NS + ND; // 103
|
|
939
|
-
const NFFT2 = NMAX$1 / NDOWN$1; // 4032
|
|
998
|
+
const NN$1 = 103;
|
|
999
|
+
const NFFT2$1 = NMAX$1 / NDOWN$1; // 4032
|
|
940
1000
|
const NSS = NSPS$1 / NDOWN$1; // 32
|
|
941
|
-
const FS2 = SAMPLE_RATE / NDOWN$1; // 666.67 Hz
|
|
1001
|
+
const FS2$1 = SAMPLE_RATE / NDOWN$1; // 666.67 Hz
|
|
942
1002
|
const MAX_FREQ = 4910;
|
|
943
1003
|
const SYNC_PASS_MIN = 1.2;
|
|
944
|
-
const TWO_PI$
|
|
1004
|
+
const TWO_PI$2 = 2 * Math.PI;
|
|
945
1005
|
const HARD_SYNC_PATTERNS = [
|
|
946
1006
|
{ offset: 0, bits: [0, 0, 0, 1, 1, 0, 1, 1] },
|
|
947
1007
|
{ offset: 66, bits: [0, 1, 0, 0, 1, 1, 1, 0] },
|
|
948
1008
|
{ offset: 132, bits: [1, 1, 1, 0, 0, 1, 0, 0] },
|
|
949
1009
|
{ offset: 198, bits: [1, 0, 1, 1, 0, 0, 0, 1] },
|
|
950
1010
|
];
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1011
|
+
const COSTAS_BLOCKS$1 = 4;
|
|
1012
|
+
const FT4_SYNC_STRIDE = 33 * NSS;
|
|
1013
|
+
const FT4_MAX_TWEAK = 16;
|
|
1014
|
+
const LDPC_BITS = 174;
|
|
1015
|
+
const BITMETRIC_LEN = 2 * NN$1;
|
|
1016
|
+
const FRAME_LEN = NN$1 * NSS;
|
|
1017
|
+
const NUTTALL_WINDOW = makeNuttallWindow(NFFT1$1);
|
|
1018
|
+
const DOWNSAMPLE_CTX = createDownsampleContext();
|
|
1019
|
+
const TWEAKED_SYNC_TEMPLATES = createTweakedSyncTemplates();
|
|
960
1020
|
/**
|
|
961
1021
|
* Decode all FT4 signals in a buffer.
|
|
962
1022
|
* Input: mono audio samples at `sampleRate` Hz, duration ~6s.
|
|
@@ -970,154 +1030,158 @@ function decode$1(samples, options = {}) {
|
|
|
970
1030
|
const maxCandidates = options.maxCandidates ?? 100;
|
|
971
1031
|
const book = options.hashCallBook;
|
|
972
1032
|
const dd = sampleRate === SAMPLE_RATE
|
|
973
|
-
?
|
|
1033
|
+
? copySamplesToDecodeWindow$1(samples)
|
|
974
1034
|
: resample$1(samples, sampleRate, SAMPLE_RATE, NMAX$1);
|
|
975
1035
|
const cxRe = new Float64Array(NMAX$1);
|
|
976
1036
|
const cxIm = new Float64Array(NMAX$1);
|
|
977
|
-
for (let i = 0; i < NMAX$1; i++)
|
|
1037
|
+
for (let i = 0; i < NMAX$1; i++)
|
|
978
1038
|
cxRe[i] = dd[i] ?? 0;
|
|
979
|
-
}
|
|
980
1039
|
fftComplex(cxRe, cxIm, false);
|
|
981
1040
|
const candidates = getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates);
|
|
982
|
-
if (candidates.length === 0)
|
|
1041
|
+
if (candidates.length === 0)
|
|
983
1042
|
return [];
|
|
984
|
-
|
|
985
|
-
const downsampleCtx = createDownsampleContext();
|
|
986
|
-
const tweakedSyncTemplates = createTweakedSyncTemplates();
|
|
1043
|
+
const workspace = createDecodeWorkspace$1();
|
|
987
1044
|
const decoded = [];
|
|
988
1045
|
const seenMessages = new Set();
|
|
989
|
-
const apmask = new Int8Array(174);
|
|
990
1046
|
for (const candidate of candidates) {
|
|
991
|
-
const one = decodeCandidate(candidate, cxRe, cxIm,
|
|
992
|
-
if (!one)
|
|
1047
|
+
const one = decodeCandidate(candidate, cxRe, cxIm, depth, book, workspace);
|
|
1048
|
+
if (!one)
|
|
993
1049
|
continue;
|
|
994
|
-
|
|
995
|
-
if (seenMessages.has(one.msg)) {
|
|
1050
|
+
if (seenMessages.has(one.msg))
|
|
996
1051
|
continue;
|
|
997
|
-
}
|
|
998
1052
|
seenMessages.add(one.msg);
|
|
999
1053
|
decoded.push(one);
|
|
1000
1054
|
}
|
|
1001
1055
|
return decoded;
|
|
1002
1056
|
}
|
|
1003
|
-
function
|
|
1004
|
-
|
|
1005
|
-
|
|
1057
|
+
function createDecodeWorkspace$1() {
|
|
1058
|
+
return {
|
|
1059
|
+
coarseRe: new Float64Array(NFFT2$1),
|
|
1060
|
+
coarseIm: new Float64Array(NFFT2$1),
|
|
1061
|
+
fineRe: new Float64Array(NFFT2$1),
|
|
1062
|
+
fineIm: new Float64Array(NFFT2$1),
|
|
1063
|
+
frameRe: new Float64Array(FRAME_LEN),
|
|
1064
|
+
frameIm: new Float64Array(FRAME_LEN),
|
|
1065
|
+
symbRe: new Float64Array(NSS),
|
|
1066
|
+
symbIm: new Float64Array(NSS),
|
|
1067
|
+
csRe: new Float64Array(4 * NN$1),
|
|
1068
|
+
csIm: new Float64Array(4 * NN$1),
|
|
1069
|
+
s4: new Float64Array(4 * NN$1),
|
|
1070
|
+
s2: new Float64Array(1 << 8),
|
|
1071
|
+
bitmetrics1: new Float64Array(BITMETRIC_LEN),
|
|
1072
|
+
bitmetrics2: new Float64Array(BITMETRIC_LEN),
|
|
1073
|
+
bitmetrics3: new Float64Array(BITMETRIC_LEN),
|
|
1074
|
+
llra: new Float64Array(LDPC_BITS),
|
|
1075
|
+
llrb: new Float64Array(LDPC_BITS),
|
|
1076
|
+
llrc: new Float64Array(LDPC_BITS),
|
|
1077
|
+
llr: new Float64Array(LDPC_BITS),
|
|
1078
|
+
apmask: new Int8Array(LDPC_BITS),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function copySamplesToDecodeWindow$1(samples) {
|
|
1082
|
+
const out = new Float64Array(NMAX$1);
|
|
1083
|
+
const len = Math.min(samples.length, NMAX$1);
|
|
1084
|
+
for (let i = 0; i < len; i++)
|
|
1085
|
+
out[i] = samples[i];
|
|
1086
|
+
return out;
|
|
1087
|
+
}
|
|
1088
|
+
function decodeCandidate(candidate, cxRe, cxIm, depth, book, workspace) {
|
|
1089
|
+
ft4Downsample(cxRe, cxIm, candidate.freq, DOWNSAMPLE_CTX, workspace.coarseRe, workspace.coarseIm);
|
|
1090
|
+
normalizeComplexPower(workspace.coarseRe, workspace.coarseIm, NMAX$1 / NDOWN$1);
|
|
1006
1091
|
for (let segment = 1; segment <= 3; segment++) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
let smax = -99;
|
|
1010
|
-
for (let isync = 1; isync <= 2; isync++) {
|
|
1011
|
-
let idfmin;
|
|
1012
|
-
let idfmax;
|
|
1013
|
-
let idfstp;
|
|
1014
|
-
let ibmin;
|
|
1015
|
-
let ibmax;
|
|
1016
|
-
let ibstp;
|
|
1017
|
-
if (isync === 1) {
|
|
1018
|
-
idfmin = -12;
|
|
1019
|
-
idfmax = 12;
|
|
1020
|
-
idfstp = 3;
|
|
1021
|
-
ibmin = -344;
|
|
1022
|
-
ibmax = 1012;
|
|
1023
|
-
if (segment === 1) {
|
|
1024
|
-
ibmin = 108;
|
|
1025
|
-
ibmax = 560;
|
|
1026
|
-
}
|
|
1027
|
-
else if (segment === 2) {
|
|
1028
|
-
ibmin = 560;
|
|
1029
|
-
ibmax = 1012;
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
ibmin = -344;
|
|
1033
|
-
ibmax = 108;
|
|
1034
|
-
}
|
|
1035
|
-
ibstp = 4;
|
|
1036
|
-
}
|
|
1037
|
-
else {
|
|
1038
|
-
idfmin = idfbest - 4;
|
|
1039
|
-
idfmax = idfbest + 4;
|
|
1040
|
-
idfstp = 1;
|
|
1041
|
-
ibmin = ibest - 5;
|
|
1042
|
-
ibmax = ibest + 5;
|
|
1043
|
-
ibstp = 1;
|
|
1044
|
-
}
|
|
1045
|
-
for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
|
|
1046
|
-
const templates = tweakedSyncTemplates.get(idf);
|
|
1047
|
-
if (!templates) {
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
|
|
1051
|
-
const sync = sync4d(cd2.re, cd2.im, istart, templates);
|
|
1052
|
-
if (sync > smax) {
|
|
1053
|
-
smax = sync;
|
|
1054
|
-
ibest = istart;
|
|
1055
|
-
idfbest = idf;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
if (smax < SYNC_PASS_MIN) {
|
|
1092
|
+
const coarse = findBestSyncLocation(workspace.coarseRe, workspace.coarseIm, segment);
|
|
1093
|
+
if (coarse.smax < SYNC_PASS_MIN)
|
|
1061
1094
|
continue;
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (f1 <= 10 || f1 >= 4990) {
|
|
1095
|
+
const f1 = candidate.freq + coarse.idfbest;
|
|
1096
|
+
if (f1 <= 10 || f1 >= 4990)
|
|
1065
1097
|
continue;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
if (metrics.badsync) {
|
|
1098
|
+
ft4Downsample(cxRe, cxIm, f1, DOWNSAMPLE_CTX, workspace.fineRe, workspace.fineIm);
|
|
1099
|
+
normalizeComplexPower(workspace.fineRe, workspace.fineIm, NSS * NN$1);
|
|
1100
|
+
extractFrame(workspace.fineRe, workspace.fineIm, coarse.ibest, workspace.frameRe, workspace.frameIm);
|
|
1101
|
+
const badsync = buildBitMetrics$1(workspace.frameRe, workspace.frameIm, workspace);
|
|
1102
|
+
if (badsync)
|
|
1072
1103
|
continue;
|
|
1073
|
-
|
|
1074
|
-
if (!passesHardSyncQuality(metrics.bitmetrics1)) {
|
|
1104
|
+
if (!passesHardSyncQuality(workspace.bitmetrics1))
|
|
1075
1105
|
continue;
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1106
|
+
buildLlrs(workspace);
|
|
1107
|
+
const result = tryDecodePasses$1(workspace, depth);
|
|
1108
|
+
if (!result)
|
|
1109
|
+
continue;
|
|
1110
|
+
const message77Scrambled = result.message91.slice(0, 77);
|
|
1111
|
+
if (!hasNonZeroBit(message77Scrambled))
|
|
1112
|
+
continue;
|
|
1113
|
+
const message77 = xorWithScrambler(message77Scrambled);
|
|
1114
|
+
const { msg, success } = unpack77(message77, book);
|
|
1115
|
+
if (!success || msg.trim().length === 0)
|
|
1116
|
+
continue;
|
|
1117
|
+
return {
|
|
1118
|
+
freq: f1,
|
|
1119
|
+
dt: coarse.ibest / FS2$1 - 0.5,
|
|
1120
|
+
snr: toFt4Snr(candidate.sync - 1.0),
|
|
1121
|
+
msg,
|
|
1122
|
+
sync: coarse.smax,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
function findBestSyncLocation(cdRe, cdIm, segment) {
|
|
1128
|
+
let ibest = -1;
|
|
1129
|
+
let idfbest = 0;
|
|
1130
|
+
let smax = -99;
|
|
1131
|
+
for (let isync = 1; isync <= 2; isync++) {
|
|
1132
|
+
let idfmin;
|
|
1133
|
+
let idfmax;
|
|
1134
|
+
let idfstp;
|
|
1135
|
+
let ibmin;
|
|
1136
|
+
let ibmax;
|
|
1137
|
+
let ibstp;
|
|
1138
|
+
if (isync === 1) {
|
|
1139
|
+
idfmin = -12;
|
|
1140
|
+
idfmax = 12;
|
|
1141
|
+
idfstp = 3;
|
|
1142
|
+
ibmin = -344;
|
|
1143
|
+
ibmax = 1012;
|
|
1144
|
+
if (segment === 1) {
|
|
1145
|
+
ibmin = 108;
|
|
1146
|
+
ibmax = 560;
|
|
1084
1147
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1148
|
+
else if (segment === 2) {
|
|
1149
|
+
ibmin = 560;
|
|
1150
|
+
ibmax = 1012;
|
|
1088
1151
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1152
|
+
else {
|
|
1153
|
+
ibmin = -344;
|
|
1154
|
+
ibmax = 108;
|
|
1092
1155
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1156
|
+
ibstp = 4;
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
idfmin = idfbest - 4;
|
|
1160
|
+
idfmax = idfbest + 4;
|
|
1161
|
+
idfstp = 1;
|
|
1162
|
+
ibmin = ibest - 5;
|
|
1163
|
+
ibmax = ibest + 5;
|
|
1164
|
+
ibstp = 1;
|
|
1165
|
+
}
|
|
1166
|
+
for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
|
|
1167
|
+
const templates = TWEAKED_SYNC_TEMPLATES.get(idf);
|
|
1168
|
+
if (!templates)
|
|
1096
1169
|
continue;
|
|
1170
|
+
for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
|
|
1171
|
+
const sync = sync4d(cdRe, cdIm, istart, templates);
|
|
1172
|
+
if (sync > smax) {
|
|
1173
|
+
smax = sync;
|
|
1174
|
+
ibest = istart;
|
|
1175
|
+
idfbest = idf;
|
|
1176
|
+
}
|
|
1097
1177
|
}
|
|
1098
|
-
return {
|
|
1099
|
-
freq: f1,
|
|
1100
|
-
dt: ibest / FS2 - 0.5,
|
|
1101
|
-
snr: toFt4Snr(candidate.sync - 1.0),
|
|
1102
|
-
msg,
|
|
1103
|
-
sync: smax,
|
|
1104
|
-
};
|
|
1105
1178
|
}
|
|
1106
1179
|
}
|
|
1107
|
-
return
|
|
1108
|
-
}
|
|
1109
|
-
function copyIntoFt4Buffer(samples) {
|
|
1110
|
-
const out = new Float64Array(NMAX$1);
|
|
1111
|
-
const len = Math.min(samples.length, NMAX$1);
|
|
1112
|
-
for (let i = 0; i < len; i++) {
|
|
1113
|
-
out[i] = samples[i];
|
|
1114
|
-
}
|
|
1115
|
-
return out;
|
|
1180
|
+
return { ibest, idfbest, smax };
|
|
1116
1181
|
}
|
|
1117
1182
|
function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
1118
1183
|
const df = SAMPLE_RATE / NFFT1$1;
|
|
1119
1184
|
const fac = 1 / 300;
|
|
1120
|
-
const window = makeNuttallWindow(NFFT1$1);
|
|
1121
1185
|
const savg = new Float64Array(NH1);
|
|
1122
1186
|
const s = new Float64Array(NH1 * NHSYM$1);
|
|
1123
1187
|
const savsm = new Float64Array(NH1);
|
|
@@ -1126,13 +1190,11 @@ function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
|
1126
1190
|
for (let j = 0; j < NHSYM$1; j++) {
|
|
1127
1191
|
const ia = j * NSPS$1;
|
|
1128
1192
|
const ib = ia + NFFT1$1;
|
|
1129
|
-
if (ib > NMAX$1)
|
|
1193
|
+
if (ib > NMAX$1)
|
|
1130
1194
|
break;
|
|
1131
|
-
}
|
|
1132
1195
|
xIm.fill(0);
|
|
1133
|
-
for (let i = 0; i < NFFT1$1; i++)
|
|
1134
|
-
xRe[i] = fac * dd[ia + i] *
|
|
1135
|
-
}
|
|
1196
|
+
for (let i = 0; i < NFFT1$1; i++)
|
|
1197
|
+
xRe[i] = fac * dd[ia + i] * NUTTALL_WINDOW[i];
|
|
1136
1198
|
fftComplex(xRe, xIm, false);
|
|
1137
1199
|
for (let bin = 1; bin <= NH1; bin++) {
|
|
1138
1200
|
const idx = bin - 1;
|
|
@@ -1143,29 +1205,24 @@ function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
|
1143
1205
|
savg[idx] = (savg[idx] ?? 0) + power;
|
|
1144
1206
|
}
|
|
1145
1207
|
}
|
|
1146
|
-
for (let i = 0; i < NH1; i++)
|
|
1208
|
+
for (let i = 0; i < NH1; i++)
|
|
1147
1209
|
savg[i] = (savg[i] ?? 0) / NHSYM$1;
|
|
1148
|
-
}
|
|
1149
1210
|
for (let i = 7; i < NH1 - 7; i++) {
|
|
1150
1211
|
let sum = 0;
|
|
1151
|
-
for (let j = i - 7; j <= i + 7; j++)
|
|
1212
|
+
for (let j = i - 7; j <= i + 7; j++)
|
|
1152
1213
|
sum += savg[j];
|
|
1153
|
-
}
|
|
1154
1214
|
savsm[i] = sum / 15;
|
|
1155
1215
|
}
|
|
1156
1216
|
let nfa = Math.round(freqLow / df);
|
|
1157
|
-
if (nfa < Math.round(200 / df))
|
|
1217
|
+
if (nfa < Math.round(200 / df))
|
|
1158
1218
|
nfa = Math.round(200 / df);
|
|
1159
|
-
}
|
|
1160
1219
|
let nfb = Math.round(freqHigh / df);
|
|
1161
|
-
if (nfb > Math.round(MAX_FREQ / df))
|
|
1220
|
+
if (nfb > Math.round(MAX_FREQ / df))
|
|
1162
1221
|
nfb = Math.round(MAX_FREQ / df);
|
|
1163
|
-
}
|
|
1164
1222
|
const sbase = ft4Baseline(savg, nfa, nfb, df);
|
|
1165
1223
|
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1166
|
-
if ((sbase[bin - 1] ?? 0) <= 0)
|
|
1224
|
+
if ((sbase[bin - 1] ?? 0) <= 0)
|
|
1167
1225
|
return [];
|
|
1168
|
-
}
|
|
1169
1226
|
}
|
|
1170
1227
|
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1171
1228
|
const idx = bin - 1;
|
|
@@ -1181,9 +1238,8 @@ function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
|
1181
1238
|
const den = left - 2 * center + right;
|
|
1182
1239
|
const del = den !== 0 ? (0.5 * (left - right)) / den : 0;
|
|
1183
1240
|
const fpeak = (i + del) * df + fOffset;
|
|
1184
|
-
if (fpeak < 200 || fpeak > MAX_FREQ)
|
|
1241
|
+
if (fpeak < 200 || fpeak > MAX_FREQ)
|
|
1185
1242
|
continue;
|
|
1186
|
-
}
|
|
1187
1243
|
const speak = center - 0.25 * (left - right) * del;
|
|
1188
1244
|
candidates.push({ freq: fpeak, sync: speak });
|
|
1189
1245
|
}
|
|
@@ -1211,13 +1267,11 @@ function ft4Baseline(savg, nfa, nfb, df) {
|
|
|
1211
1267
|
sbase.fill(1);
|
|
1212
1268
|
const ia = Math.max(Math.round(200 / df), nfa);
|
|
1213
1269
|
const ib = Math.min(NH1, nfb);
|
|
1214
|
-
if (ib <= ia)
|
|
1270
|
+
if (ib <= ia)
|
|
1215
1271
|
return sbase;
|
|
1216
|
-
}
|
|
1217
1272
|
const sDb = new Float64Array(NH1);
|
|
1218
|
-
for (let i = ia; i <= ib; i++)
|
|
1273
|
+
for (let i = ia; i <= ib; i++)
|
|
1219
1274
|
sDb[i - 1] = 10 * Math.log10(Math.max(1e-30, savg[i - 1]));
|
|
1220
|
-
}
|
|
1221
1275
|
const nseg = 10;
|
|
1222
1276
|
const npct = 10;
|
|
1223
1277
|
const nlen = Math.max(1, Math.trunc((ib - ia + 1) / nseg));
|
|
@@ -1226,14 +1280,12 @@ function ft4Baseline(savg, nfa, nfb, df) {
|
|
|
1226
1280
|
const y = [];
|
|
1227
1281
|
for (let seg = 0; seg < nseg; seg++) {
|
|
1228
1282
|
const ja = ia + seg * nlen;
|
|
1229
|
-
if (ja > ib)
|
|
1283
|
+
if (ja > ib)
|
|
1230
1284
|
break;
|
|
1231
|
-
}
|
|
1232
1285
|
const jb = Math.min(ib, ja + nlen - 1);
|
|
1233
1286
|
const vals = [];
|
|
1234
|
-
for (let i = ja; i <= jb; i++)
|
|
1287
|
+
for (let i = ja; i <= jb; i++)
|
|
1235
1288
|
vals.push(sDb[i - 1]);
|
|
1236
|
-
}
|
|
1237
1289
|
const base = percentile(vals, npct);
|
|
1238
1290
|
for (let i = ja; i <= jb; i++) {
|
|
1239
1291
|
const v = sDb[i - 1];
|
|
@@ -1268,9 +1320,8 @@ function ft4Baseline(savg, nfa, nfb, df) {
|
|
|
1268
1320
|
return sbase;
|
|
1269
1321
|
}
|
|
1270
1322
|
function percentile(values, pct) {
|
|
1271
|
-
if (values.length === 0)
|
|
1323
|
+
if (values.length === 0)
|
|
1272
1324
|
return 0;
|
|
1273
|
-
}
|
|
1274
1325
|
const sorted = [...values].sort((a, b) => a - b);
|
|
1275
1326
|
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((pct / 100) * (sorted.length - 1))));
|
|
1276
1327
|
return sorted[idx];
|
|
@@ -1281,19 +1332,16 @@ function polyfitLeastSquares(x, y, degree) {
|
|
|
1281
1332
|
const xPows = new Float64Array(2 * degree + 1);
|
|
1282
1333
|
for (let p = 0; p <= 2 * degree; p++) {
|
|
1283
1334
|
let sum = 0;
|
|
1284
|
-
for (let i = 0; i < x.length; i++)
|
|
1335
|
+
for (let i = 0; i < x.length; i++)
|
|
1285
1336
|
sum += x[i] ** p;
|
|
1286
|
-
}
|
|
1287
1337
|
xPows[p] = sum;
|
|
1288
1338
|
}
|
|
1289
1339
|
for (let row = 0; row < n; row++) {
|
|
1290
|
-
for (let col = 0; col < n; col++)
|
|
1340
|
+
for (let col = 0; col < n; col++)
|
|
1291
1341
|
mat[row][col] = xPows[row + col];
|
|
1292
|
-
}
|
|
1293
1342
|
let rhs = 0;
|
|
1294
|
-
for (let i = 0; i < x.length; i++)
|
|
1343
|
+
for (let i = 0; i < x.length; i++)
|
|
1295
1344
|
rhs += y[i] * x[i] ** row;
|
|
1296
|
-
}
|
|
1297
1345
|
mat[row][n] = rhs;
|
|
1298
1346
|
}
|
|
1299
1347
|
for (let col = 0; col < n; col++) {
|
|
@@ -1306,35 +1354,29 @@ function polyfitLeastSquares(x, y, degree) {
|
|
|
1306
1354
|
pivot = row;
|
|
1307
1355
|
}
|
|
1308
1356
|
}
|
|
1309
|
-
if (maxAbs < 1e-12)
|
|
1357
|
+
if (maxAbs < 1e-12)
|
|
1310
1358
|
return null;
|
|
1311
|
-
}
|
|
1312
1359
|
if (pivot !== col) {
|
|
1313
1360
|
const tmp = mat[col];
|
|
1314
1361
|
mat[col] = mat[pivot];
|
|
1315
1362
|
mat[pivot] = tmp;
|
|
1316
1363
|
}
|
|
1317
1364
|
const pivotVal = mat[col][col];
|
|
1318
|
-
for (let c = col; c <= n; c++)
|
|
1365
|
+
for (let c = col; c <= n; c++)
|
|
1319
1366
|
mat[col][c] = mat[col][c] / pivotVal;
|
|
1320
|
-
}
|
|
1321
1367
|
for (let row = 0; row < n; row++) {
|
|
1322
|
-
if (row === col)
|
|
1368
|
+
if (row === col)
|
|
1323
1369
|
continue;
|
|
1324
|
-
}
|
|
1325
1370
|
const factor = mat[row][col];
|
|
1326
|
-
if (factor === 0)
|
|
1371
|
+
if (factor === 0)
|
|
1327
1372
|
continue;
|
|
1328
|
-
|
|
1329
|
-
for (let c = col; c <= n; c++) {
|
|
1373
|
+
for (let c = col; c <= n; c++)
|
|
1330
1374
|
mat[row][c] = mat[row][c] - factor * mat[col][c];
|
|
1331
|
-
}
|
|
1332
1375
|
}
|
|
1333
1376
|
}
|
|
1334
1377
|
const coeff = new Array(n);
|
|
1335
|
-
for (let i = 0; i < n; i++)
|
|
1378
|
+
for (let i = 0; i < n; i++)
|
|
1336
1379
|
coeff[i] = mat[i][n];
|
|
1337
|
-
}
|
|
1338
1380
|
return coeff;
|
|
1339
1381
|
}
|
|
1340
1382
|
function createDownsampleContext() {
|
|
@@ -1345,84 +1387,81 @@ function createDownsampleContext() {
|
|
|
1345
1387
|
const iwt = Math.max(1, Math.trunc(bwTransition / df));
|
|
1346
1388
|
const iwf = Math.max(1, Math.trunc(bwFlat / df));
|
|
1347
1389
|
const iws = Math.trunc(baud / df);
|
|
1348
|
-
const raw = new Float64Array(NFFT2);
|
|
1390
|
+
const raw = new Float64Array(NFFT2$1);
|
|
1349
1391
|
for (let i = 0; i < iwt && i < raw.length; i++) {
|
|
1350
1392
|
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (iwt - 1 - i)) / iwt));
|
|
1351
1393
|
}
|
|
1352
|
-
for (let i = iwt; i < iwt + iwf && i < raw.length; i++)
|
|
1394
|
+
for (let i = iwt; i < iwt + iwf && i < raw.length; i++)
|
|
1353
1395
|
raw[i] = 1;
|
|
1354
|
-
}
|
|
1355
1396
|
for (let i = iwt + iwf; i < 2 * iwt + iwf && i < raw.length; i++) {
|
|
1356
1397
|
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (i - (iwt + iwf))) / iwt));
|
|
1357
1398
|
}
|
|
1358
|
-
const window = new Float64Array(NFFT2);
|
|
1359
|
-
for (let i = 0; i < NFFT2; i++) {
|
|
1360
|
-
const src = (i + iws) % NFFT2;
|
|
1399
|
+
const window = new Float64Array(NFFT2$1);
|
|
1400
|
+
for (let i = 0; i < NFFT2$1; i++) {
|
|
1401
|
+
const src = (i + iws) % NFFT2$1;
|
|
1361
1402
|
window[i] = raw[src];
|
|
1362
1403
|
}
|
|
1363
1404
|
return { df, window };
|
|
1364
1405
|
}
|
|
1365
|
-
function ft4Downsample(cxRe, cxIm, f0, ctx) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1406
|
+
function ft4Downsample(cxRe, cxIm, f0, ctx, outRe, outIm) {
|
|
1407
|
+
outRe.fill(0);
|
|
1408
|
+
outIm.fill(0);
|
|
1368
1409
|
const i0 = Math.round(f0 / ctx.df);
|
|
1369
1410
|
if (i0 >= 0 && i0 <= NMAX$1 / 2) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1411
|
+
outRe[0] = cxRe[i0] ?? 0;
|
|
1412
|
+
outIm[0] = cxIm[i0] ?? 0;
|
|
1372
1413
|
}
|
|
1373
|
-
for (let i = 1; i <= NFFT2 / 2; i++) {
|
|
1414
|
+
for (let i = 1; i <= NFFT2$1 / 2; i++) {
|
|
1374
1415
|
const hi = i0 + i;
|
|
1375
1416
|
if (hi >= 0 && hi <= NMAX$1 / 2) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1417
|
+
outRe[i] = cxRe[hi] ?? 0;
|
|
1418
|
+
outIm[i] = cxIm[hi] ?? 0;
|
|
1378
1419
|
}
|
|
1379
1420
|
const lo = i0 - i;
|
|
1380
1421
|
if (lo >= 0 && lo <= NMAX$1 / 2) {
|
|
1381
|
-
const idx = NFFT2 - i;
|
|
1382
|
-
|
|
1383
|
-
|
|
1422
|
+
const idx = NFFT2$1 - i;
|
|
1423
|
+
outRe[idx] = cxRe[lo] ?? 0;
|
|
1424
|
+
outIm[idx] = cxIm[lo] ?? 0;
|
|
1384
1425
|
}
|
|
1385
1426
|
}
|
|
1386
|
-
const scale = 1 / NFFT2;
|
|
1387
|
-
for (let i = 0; i < NFFT2; i++) {
|
|
1427
|
+
const scale = 1 / NFFT2$1;
|
|
1428
|
+
for (let i = 0; i < NFFT2$1; i++) {
|
|
1388
1429
|
const w = (ctx.window[i] ?? 0) * scale;
|
|
1389
|
-
|
|
1390
|
-
|
|
1430
|
+
outRe[i] = outRe[i] * w;
|
|
1431
|
+
outIm[i] = outIm[i] * w;
|
|
1391
1432
|
}
|
|
1392
|
-
fftComplex(
|
|
1393
|
-
return { re: c1Re, im: c1Im };
|
|
1433
|
+
fftComplex(outRe, outIm, true);
|
|
1394
1434
|
}
|
|
1395
1435
|
function normalizeComplexPower(re, im, denom) {
|
|
1396
1436
|
let sum = 0;
|
|
1397
|
-
for (let i = 0; i < re.length; i++)
|
|
1437
|
+
for (let i = 0; i < re.length; i++)
|
|
1398
1438
|
sum += re[i] * re[i] + im[i] * im[i];
|
|
1399
|
-
|
|
1400
|
-
if (sum <= 0) {
|
|
1439
|
+
if (sum <= 0)
|
|
1401
1440
|
return;
|
|
1402
|
-
}
|
|
1403
1441
|
const scale = 1 / Math.sqrt(sum / denom);
|
|
1404
1442
|
for (let i = 0; i < re.length; i++) {
|
|
1405
1443
|
re[i] = re[i] * scale;
|
|
1406
1444
|
im[i] = im[i] * scale;
|
|
1407
1445
|
}
|
|
1408
1446
|
}
|
|
1409
|
-
function extractFrame(cbRe, cbIm, ibest) {
|
|
1410
|
-
const outRe = new Float64Array(NN$1 * NSS);
|
|
1411
|
-
const outIm = new Float64Array(NN$1 * NSS);
|
|
1447
|
+
function extractFrame(cbRe, cbIm, ibest, outRe, outIm) {
|
|
1412
1448
|
for (let i = 0; i < outRe.length; i++) {
|
|
1413
1449
|
const src = ibest + i;
|
|
1414
1450
|
if (src >= 0 && src < cbRe.length) {
|
|
1415
1451
|
outRe[i] = cbRe[src];
|
|
1416
1452
|
outIm[i] = cbIm[src];
|
|
1417
1453
|
}
|
|
1454
|
+
else {
|
|
1455
|
+
outRe[i] = 0;
|
|
1456
|
+
outIm[i] = 0;
|
|
1457
|
+
}
|
|
1418
1458
|
}
|
|
1419
|
-
return { re: outRe, im: outIm };
|
|
1420
1459
|
}
|
|
1421
1460
|
function createTweakedSyncTemplates() {
|
|
1422
1461
|
const base = createBaseSyncTemplates();
|
|
1423
|
-
const fsample = FS2 / 2;
|
|
1462
|
+
const fsample = FS2$1 / 2;
|
|
1424
1463
|
const out = new Map();
|
|
1425
|
-
for (let idf = -
|
|
1464
|
+
for (let idf = -FT4_MAX_TWEAK; idf <= FT4_MAX_TWEAK; idf++) {
|
|
1426
1465
|
const tweak = createFrequencyTweak(idf, 2 * NSS, fsample);
|
|
1427
1466
|
out.set(idf, [
|
|
1428
1467
|
applyTweak(base[0], tweak),
|
|
@@ -1435,10 +1474,10 @@ function createTweakedSyncTemplates() {
|
|
|
1435
1474
|
}
|
|
1436
1475
|
function createBaseSyncTemplates() {
|
|
1437
1476
|
return [
|
|
1438
|
-
buildSyncTemplate(COSTAS_A),
|
|
1439
|
-
buildSyncTemplate(COSTAS_B),
|
|
1440
|
-
buildSyncTemplate(COSTAS_C),
|
|
1441
|
-
buildSyncTemplate(COSTAS_D),
|
|
1477
|
+
buildSyncTemplate(COSTAS_A$1),
|
|
1478
|
+
buildSyncTemplate(COSTAS_B$1),
|
|
1479
|
+
buildSyncTemplate(COSTAS_C$1),
|
|
1480
|
+
buildSyncTemplate(COSTAS_D$1),
|
|
1442
1481
|
];
|
|
1443
1482
|
}
|
|
1444
1483
|
function buildSyncTemplate(tones) {
|
|
@@ -1447,11 +1486,11 @@ function buildSyncTemplate(tones) {
|
|
|
1447
1486
|
let k = 0;
|
|
1448
1487
|
let phi = 0;
|
|
1449
1488
|
for (const tone of tones) {
|
|
1450
|
-
const dphi = (TWO_PI$
|
|
1489
|
+
const dphi = (TWO_PI$2 * tone * 2) / NSS;
|
|
1451
1490
|
for (let j = 0; j < NSS / 2; j++) {
|
|
1452
1491
|
re[k] = Math.cos(phi);
|
|
1453
1492
|
im[k] = Math.sin(phi);
|
|
1454
|
-
phi = (phi + dphi) % TWO_PI$
|
|
1493
|
+
phi = (phi + dphi) % TWO_PI$2;
|
|
1455
1494
|
k++;
|
|
1456
1495
|
}
|
|
1457
1496
|
}
|
|
@@ -1460,7 +1499,7 @@ function buildSyncTemplate(tones) {
|
|
|
1460
1499
|
function createFrequencyTweak(idf, npts, fsample) {
|
|
1461
1500
|
const re = new Float64Array(npts);
|
|
1462
1501
|
const im = new Float64Array(npts);
|
|
1463
|
-
const dphi = (TWO_PI$
|
|
1502
|
+
const dphi = (TWO_PI$2 * idf) / fsample;
|
|
1464
1503
|
const stepRe = Math.cos(dphi);
|
|
1465
1504
|
const stepIm = Math.sin(dphi);
|
|
1466
1505
|
let wRe = 1;
|
|
@@ -1489,13 +1528,12 @@ function applyTweak(template, tweak) {
|
|
|
1489
1528
|
return { re, im };
|
|
1490
1529
|
}
|
|
1491
1530
|
function sync4d(cdRe, cdIm, i0, templates) {
|
|
1492
|
-
const starts = [i0, i0 + 33 * NSS, i0 + 66 * NSS, i0 + 99 * NSS];
|
|
1493
1531
|
let sync = 0;
|
|
1494
|
-
for (let i = 0; i <
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1532
|
+
for (let i = 0; i < COSTAS_BLOCKS$1; i++) {
|
|
1533
|
+
const start = i0 + i * FT4_SYNC_STRIDE;
|
|
1534
|
+
const z = correlateStride2(cdRe, cdIm, start, templates[i].re, templates[i].im);
|
|
1535
|
+
if (z.count <= 16)
|
|
1497
1536
|
continue;
|
|
1498
|
-
}
|
|
1499
1537
|
sync += Math.hypot(z.re, z.im) / (2 * NSS);
|
|
1500
1538
|
}
|
|
1501
1539
|
return sync;
|
|
@@ -1506,9 +1544,8 @@ function correlateStride2(cdRe, cdIm, start, templateRe, templateIm) {
|
|
|
1506
1544
|
let count = 0;
|
|
1507
1545
|
for (let i = 0; i < templateRe.length; i++) {
|
|
1508
1546
|
const idx = start + 2 * i;
|
|
1509
|
-
if (idx < 0 || idx >= cdRe.length)
|
|
1547
|
+
if (idx < 0 || idx >= cdRe.length)
|
|
1510
1548
|
continue;
|
|
1511
|
-
}
|
|
1512
1549
|
const sRe = templateRe[i];
|
|
1513
1550
|
const sIm = templateIm[i];
|
|
1514
1551
|
const dRe = cdRe[idx];
|
|
@@ -1519,12 +1556,8 @@ function correlateStride2(cdRe, cdIm, start, templateRe, templateIm) {
|
|
|
1519
1556
|
}
|
|
1520
1557
|
return { re: zRe, im: zIm, count };
|
|
1521
1558
|
}
|
|
1522
|
-
function
|
|
1523
|
-
const csRe
|
|
1524
|
-
const csIm = new Float64Array(4 * NN$1);
|
|
1525
|
-
const s4 = new Float64Array(4 * NN$1);
|
|
1526
|
-
const symbRe = new Float64Array(NSS);
|
|
1527
|
-
const symbIm = new Float64Array(NSS);
|
|
1559
|
+
function buildBitMetrics$1(cdRe, cdIm, workspace) {
|
|
1560
|
+
const { csRe, csIm, s4, symbRe, symbIm, bitmetrics1, bitmetrics2, bitmetrics3, s2 } = workspace;
|
|
1528
1561
|
for (let k = 0; k < NN$1; k++) {
|
|
1529
1562
|
const i1 = k * NSS;
|
|
1530
1563
|
for (let i = 0; i < NSS; i++) {
|
|
@@ -1543,30 +1576,24 @@ function getFt4Bitmetrics(cdRe, cdIm) {
|
|
|
1543
1576
|
}
|
|
1544
1577
|
let nsync = 0;
|
|
1545
1578
|
for (let k = 0; k < 4; k++) {
|
|
1546
|
-
if (maxTone(s4, k) === COSTAS_A[k])
|
|
1579
|
+
if (maxTone(s4, k) === COSTAS_A$1[k])
|
|
1547
1580
|
nsync++;
|
|
1548
|
-
|
|
1549
|
-
if (maxTone(s4, 33 + k) === COSTAS_B[k]) {
|
|
1581
|
+
if (maxTone(s4, 33 + k) === COSTAS_B$1[k])
|
|
1550
1582
|
nsync++;
|
|
1551
|
-
|
|
1552
|
-
if (maxTone(s4, 66 + k) === COSTAS_C[k]) {
|
|
1583
|
+
if (maxTone(s4, 66 + k) === COSTAS_C$1[k])
|
|
1553
1584
|
nsync++;
|
|
1554
|
-
|
|
1555
|
-
if (maxTone(s4, 99 + k) === COSTAS_D[k]) {
|
|
1585
|
+
if (maxTone(s4, 99 + k) === COSTAS_D$1[k])
|
|
1556
1586
|
nsync++;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const bitmetrics1 = new Float64Array(2 * NN$1);
|
|
1560
|
-
const bitmetrics2 = new Float64Array(2 * NN$1);
|
|
1561
|
-
const bitmetrics3 = new Float64Array(2 * NN$1);
|
|
1562
|
-
if (nsync < 6) {
|
|
1563
|
-
return { bitmetrics1, bitmetrics2, bitmetrics3, badsync: true };
|
|
1564
1587
|
}
|
|
1588
|
+
bitmetrics1.fill(0);
|
|
1589
|
+
bitmetrics2.fill(0);
|
|
1590
|
+
bitmetrics3.fill(0);
|
|
1591
|
+
if (nsync < 6)
|
|
1592
|
+
return true;
|
|
1565
1593
|
for (let nseq = 1; nseq <= 3; nseq++) {
|
|
1566
1594
|
const nsym = nseq === 1 ? 1 : nseq === 2 ? 2 : 4;
|
|
1567
|
-
const nt = 1 << (2 * nsym);
|
|
1595
|
+
const nt = 1 << (2 * nsym);
|
|
1568
1596
|
const ibmax = nseq === 1 ? 1 : nseq === 2 ? 3 : 7;
|
|
1569
|
-
const s2 = new Float64Array(nt);
|
|
1570
1597
|
for (let ks = 1; ks <= NN$1 - nsym + 1; ks += nsym) {
|
|
1571
1598
|
for (let i = 0; i < nt; i++) {
|
|
1572
1599
|
const i1 = Math.floor(i / 64);
|
|
@@ -1609,18 +1636,16 @@ function getFt4Bitmetrics(cdRe, cdIm) {
|
|
|
1609
1636
|
for (let i = 0; i < nt; i++) {
|
|
1610
1637
|
const v = s2[i];
|
|
1611
1638
|
if ((i & mask) !== 0) {
|
|
1612
|
-
if (v > max1)
|
|
1639
|
+
if (v > max1)
|
|
1613
1640
|
max1 = v;
|
|
1614
|
-
}
|
|
1615
1641
|
}
|
|
1616
1642
|
else if (v > max0) {
|
|
1617
1643
|
max0 = v;
|
|
1618
1644
|
}
|
|
1619
1645
|
}
|
|
1620
1646
|
const idx = ipt + ib;
|
|
1621
|
-
if (idx >
|
|
1647
|
+
if (idx > BITMETRIC_LEN)
|
|
1622
1648
|
continue;
|
|
1623
|
-
}
|
|
1624
1649
|
const bm = max1 - max0;
|
|
1625
1650
|
if (nseq === 1) {
|
|
1626
1651
|
bitmetrics1[idx - 1] = bm;
|
|
@@ -1641,7 +1666,7 @@ function getFt4Bitmetrics(cdRe, cdIm) {
|
|
|
1641
1666
|
normalizeBitMetrics(bitmetrics1);
|
|
1642
1667
|
normalizeBitMetrics(bitmetrics2);
|
|
1643
1668
|
normalizeBitMetrics(bitmetrics3);
|
|
1644
|
-
return
|
|
1669
|
+
return false;
|
|
1645
1670
|
}
|
|
1646
1671
|
function maxTone(s4, symbolIndex) {
|
|
1647
1672
|
let bestTone = 0;
|
|
@@ -1666,32 +1691,26 @@ function normalizeBitMetrics(bmet) {
|
|
|
1666
1691
|
const avg2 = sum2 / bmet.length;
|
|
1667
1692
|
const variance = avg2 - avg * avg;
|
|
1668
1693
|
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
1669
|
-
if (sigma <= 0)
|
|
1694
|
+
if (sigma <= 0)
|
|
1670
1695
|
return;
|
|
1671
|
-
|
|
1672
|
-
for (let i = 0; i < bmet.length; i++) {
|
|
1696
|
+
for (let i = 0; i < bmet.length; i++)
|
|
1673
1697
|
bmet[i] = bmet[i] / sigma;
|
|
1674
|
-
}
|
|
1675
1698
|
}
|
|
1676
1699
|
function passesHardSyncQuality(bitmetrics1) {
|
|
1677
1700
|
const hard = new Uint8Array(bitmetrics1.length);
|
|
1678
|
-
for (let i = 0; i < bitmetrics1.length; i++)
|
|
1701
|
+
for (let i = 0; i < bitmetrics1.length; i++)
|
|
1679
1702
|
hard[i] = bitmetrics1[i] >= 0 ? 1 : 0;
|
|
1680
|
-
}
|
|
1681
1703
|
let score = 0;
|
|
1682
1704
|
for (const pattern of HARD_SYNC_PATTERNS) {
|
|
1683
1705
|
for (let i = 0; i < pattern.bits.length; i++) {
|
|
1684
|
-
if (hard[pattern.offset + i] === pattern.bits[i])
|
|
1706
|
+
if (hard[pattern.offset + i] === pattern.bits[i])
|
|
1685
1707
|
score++;
|
|
1686
|
-
}
|
|
1687
1708
|
}
|
|
1688
1709
|
}
|
|
1689
1710
|
return score >= 10;
|
|
1690
1711
|
}
|
|
1691
|
-
function buildLlrs(
|
|
1692
|
-
const llra =
|
|
1693
|
-
const llrb = new Float64Array(174);
|
|
1694
|
-
const llrc = new Float64Array(174);
|
|
1712
|
+
function buildLlrs(workspace) {
|
|
1713
|
+
const { bitmetrics1, bitmetrics2, bitmetrics3, llra, llrb, llrc } = workspace;
|
|
1695
1714
|
for (let i = 0; i < 58; i++) {
|
|
1696
1715
|
llra[i] = bitmetrics1[8 + i];
|
|
1697
1716
|
llra[58 + i] = bitmetrics1[74 + i];
|
|
@@ -1703,13 +1722,25 @@ function buildLlrs(bitmetrics1, bitmetrics2, bitmetrics3) {
|
|
|
1703
1722
|
llrc[58 + i] = bitmetrics3[74 + i];
|
|
1704
1723
|
llrc[116 + i] = bitmetrics3[140 + i];
|
|
1705
1724
|
}
|
|
1706
|
-
|
|
1725
|
+
}
|
|
1726
|
+
function tryDecodePasses$1(workspace, depth) {
|
|
1727
|
+
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
1728
|
+
const scalefac = 2.83;
|
|
1729
|
+
const sources = [workspace.llra, workspace.llrb, workspace.llrc];
|
|
1730
|
+
workspace.apmask.fill(0);
|
|
1731
|
+
for (const src of sources) {
|
|
1732
|
+
for (let i = 0; i < LDPC_BITS; i++)
|
|
1733
|
+
workspace.llr[i] = scalefac * src[i];
|
|
1734
|
+
const result = decode174_91(workspace.llr, workspace.apmask, maxosd);
|
|
1735
|
+
if (result)
|
|
1736
|
+
return result;
|
|
1737
|
+
}
|
|
1738
|
+
return null;
|
|
1707
1739
|
}
|
|
1708
1740
|
function hasNonZeroBit(bits) {
|
|
1709
1741
|
for (const bit of bits) {
|
|
1710
|
-
if (bit !== 0)
|
|
1742
|
+
if (bit !== 0)
|
|
1711
1743
|
return true;
|
|
1712
|
-
}
|
|
1713
1744
|
}
|
|
1714
1745
|
return false;
|
|
1715
1746
|
}
|
|
@@ -1873,7 +1904,6 @@ function parseCallsign(raw) {
|
|
|
1873
1904
|
iarea <= 2 && // Fortran: iarea (1-indexed) must be 2 or 3 → 0-indexed: 1 or 2
|
|
1874
1905
|
nplet >= 1 && // at least one letter before area digit
|
|
1875
1906
|
npdig < iarea && // not all digits before area
|
|
1876
|
-
nslet >= 1 && // must have at least one letter after area digit
|
|
1877
1907
|
nslet <= 3; // at most 3 suffix letters
|
|
1878
1908
|
return { basecall: call, isStandard: standard, suffix };
|
|
1879
1909
|
}
|
|
@@ -1917,7 +1947,22 @@ function pack28(token) {
|
|
|
1917
1947
|
// Standard callsign
|
|
1918
1948
|
const { basecall, isStandard } = parseCallsign(t);
|
|
1919
1949
|
if (isStandard) {
|
|
1920
|
-
|
|
1950
|
+
// Fortran pack28 layout:
|
|
1951
|
+
// iarea==2 (0-based 1): callsign=' '//c13(1:5)
|
|
1952
|
+
// iarea==3 (0-based 2): callsign= c13(1:6)
|
|
1953
|
+
let iareaD = -1;
|
|
1954
|
+
for (let ii = basecall.length - 1; ii >= 1; ii--) {
|
|
1955
|
+
const c = basecall[ii] ?? "";
|
|
1956
|
+
if (c >= "0" && c <= "9") {
|
|
1957
|
+
iareaD = ii;
|
|
1958
|
+
break;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
let cs = basecall;
|
|
1962
|
+
if (iareaD === 1)
|
|
1963
|
+
cs = ` ${basecall.slice(0, 5)}`;
|
|
1964
|
+
if (iareaD === 2)
|
|
1965
|
+
cs = basecall.slice(0, 6);
|
|
1921
1966
|
const i1 = A1.indexOf(cs[0] ?? " ");
|
|
1922
1967
|
const i2 = A2.indexOf(cs[1] ?? "0");
|
|
1923
1968
|
const i3 = A3.indexOf(cs[2] ?? "0");
|
|
@@ -2242,12 +2287,12 @@ function packFreeText(msg) {
|
|
|
2242
2287
|
return bits; // 77 bits
|
|
2243
2288
|
}
|
|
2244
2289
|
|
|
2245
|
-
const TWO_PI = 2 * Math.PI;
|
|
2290
|
+
const TWO_PI$1 = 2 * Math.PI;
|
|
2246
2291
|
const FT8_DEFAULT_SAMPLE_RATE = 12_000;
|
|
2247
2292
|
const FT8_DEFAULT_SAMPLES_PER_SYMBOL = 1_920;
|
|
2248
2293
|
const FT8_DEFAULT_BT = 2.0;
|
|
2249
2294
|
const FT4_DEFAULT_SAMPLE_RATE = 12_000;
|
|
2250
|
-
const FT4_DEFAULT_SAMPLES_PER_SYMBOL =
|
|
2295
|
+
const FT4_DEFAULT_SAMPLES_PER_SYMBOL = 576;
|
|
2251
2296
|
const FT4_DEFAULT_BT = 1.0;
|
|
2252
2297
|
const MODULATION_INDEX = 1.0;
|
|
2253
2298
|
function assertPositiveFinite(value, name) {
|
|
@@ -2280,12 +2325,16 @@ function generateGfskWaveform(tones, options, defaults, shape) {
|
|
|
2280
2325
|
const nsps = options.samplesPerSymbol ?? defaults.samplesPerSymbol;
|
|
2281
2326
|
const bt = options.bt ?? defaults.bt;
|
|
2282
2327
|
const f0 = options.baseFrequency ?? 0;
|
|
2328
|
+
const initialPhase = options.initialPhase ?? 0;
|
|
2283
2329
|
assertPositiveFinite(sampleRate, "sampleRate");
|
|
2284
2330
|
assertPositiveFinite(nsps, "samplesPerSymbol");
|
|
2285
2331
|
assertPositiveFinite(bt, "bt");
|
|
2286
2332
|
if (!Number.isFinite(f0)) {
|
|
2287
2333
|
throw new Error("baseFrequency must be finite");
|
|
2288
2334
|
}
|
|
2335
|
+
if (!Number.isFinite(initialPhase)) {
|
|
2336
|
+
throw new Error("initialPhase must be finite");
|
|
2337
|
+
}
|
|
2289
2338
|
if (!Number.isInteger(nsps)) {
|
|
2290
2339
|
throw new Error("samplesPerSymbol must be an integer");
|
|
2291
2340
|
}
|
|
@@ -2296,7 +2345,7 @@ function generateGfskWaveform(tones, options, defaults, shape) {
|
|
|
2296
2345
|
pulse[i] = gfskPulse(bt, tt);
|
|
2297
2346
|
}
|
|
2298
2347
|
const dphi = new Float64Array((nsym + 2) * nsps);
|
|
2299
|
-
const dphiPeak = (TWO_PI * MODULATION_INDEX) / nsps;
|
|
2348
|
+
const dphiPeak = (TWO_PI$1 * MODULATION_INDEX) / nsps;
|
|
2300
2349
|
for (let j = 0; j < nsym; j++) {
|
|
2301
2350
|
const tone = tones[j];
|
|
2302
2351
|
const ib = j * nsps;
|
|
@@ -2311,42 +2360,44 @@ function generateGfskWaveform(tones, options, defaults, shape) {
|
|
|
2311
2360
|
dphi[i] += dphiPeak * firstTone * pulse[nsps + i];
|
|
2312
2361
|
dphi[tailBase + i] += dphiPeak * lastTone * pulse[i];
|
|
2313
2362
|
}
|
|
2314
|
-
const carrierDphi = (TWO_PI * f0) / sampleRate;
|
|
2363
|
+
const carrierDphi = (TWO_PI$1 * f0) / sampleRate;
|
|
2315
2364
|
for (let i = 0; i < dphi.length; i++) {
|
|
2316
2365
|
dphi[i] += carrierDphi;
|
|
2317
2366
|
}
|
|
2318
2367
|
const wave = new Float32Array(nwave);
|
|
2319
|
-
let phi =
|
|
2368
|
+
let phi = initialPhase % TWO_PI$1;
|
|
2369
|
+
if (phi < 0)
|
|
2370
|
+
phi += TWO_PI$1;
|
|
2320
2371
|
const phaseStart = shape.includeRampSymbols ? 0 : nsps;
|
|
2321
2372
|
for (let k = 0; k < nwave; k++) {
|
|
2322
2373
|
const j = phaseStart + k;
|
|
2323
2374
|
wave[k] = Math.sin(phi);
|
|
2324
2375
|
phi += dphi[j];
|
|
2325
|
-
phi %= TWO_PI;
|
|
2376
|
+
phi %= TWO_PI$1;
|
|
2326
2377
|
if (phi < 0) {
|
|
2327
|
-
phi += TWO_PI;
|
|
2378
|
+
phi += TWO_PI$1;
|
|
2328
2379
|
}
|
|
2329
2380
|
}
|
|
2330
2381
|
if (shape.fullSymbolRamp) {
|
|
2331
2382
|
for (let i = 0; i < nsps; i++) {
|
|
2332
|
-
const up = (1 - Math.cos((TWO_PI * i) / (2 * nsps))) / 2;
|
|
2383
|
+
const up = (1 - Math.cos((TWO_PI$1 * i) / (2 * nsps))) / 2;
|
|
2333
2384
|
wave[i] *= up;
|
|
2334
2385
|
}
|
|
2335
2386
|
const tailStart = (nsym + 1) * nsps;
|
|
2336
2387
|
for (let i = 0; i < nsps; i++) {
|
|
2337
|
-
const down = (1 + Math.cos((TWO_PI * i) / (2 * nsps))) / 2;
|
|
2388
|
+
const down = (1 + Math.cos((TWO_PI$1 * i) / (2 * nsps))) / 2;
|
|
2338
2389
|
wave[tailStart + i] *= down;
|
|
2339
2390
|
}
|
|
2340
2391
|
}
|
|
2341
2392
|
else {
|
|
2342
2393
|
const nramp = Math.round(nsps / 8);
|
|
2343
2394
|
for (let i = 0; i < nramp; i++) {
|
|
2344
|
-
const up = (1 - Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
|
|
2395
|
+
const up = (1 - Math.cos((TWO_PI$1 * i) / (2 * nramp))) / 2;
|
|
2345
2396
|
wave[i] *= up;
|
|
2346
2397
|
}
|
|
2347
2398
|
const tailStart = nwave - nramp;
|
|
2348
2399
|
for (let i = 0; i < nramp; i++) {
|
|
2349
|
-
const down = (1 + Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
|
|
2400
|
+
const down = (1 + Math.cos((TWO_PI$1 * i) / (2 * nramp))) / 2;
|
|
2350
2401
|
wave[tailStart + i] *= down;
|
|
2351
2402
|
}
|
|
2352
2403
|
}
|
|
@@ -2376,13 +2427,6 @@ function generateFT4Waveform(tones, options = {}) {
|
|
|
2376
2427
|
}
|
|
2377
2428
|
|
|
2378
2429
|
/** FT8-specific constants (lib/ft8/ft8_params.f90). */
|
|
2379
|
-
const NSPS = 1920;
|
|
2380
|
-
const NFFT1 = 2 * NSPS; // 3840
|
|
2381
|
-
const NSTEP = NSPS / 4; // 480
|
|
2382
|
-
const NMAX = 15 * 12_000; // 180000
|
|
2383
|
-
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
2384
|
-
const NDOWN = 60;
|
|
2385
|
-
const NN = 79;
|
|
2386
2430
|
/** 7-symbol Costas array for sync. */
|
|
2387
2431
|
const COSTAS = [3, 1, 4, 0, 6, 5, 2];
|
|
2388
2432
|
/** 8-tone Gray mapping. */
|
|
@@ -2465,6 +2509,10 @@ function encode$1(msg, options = {}) {
|
|
|
2465
2509
|
return generateFT8Waveform(encodeMessage$1(msg), options);
|
|
2466
2510
|
}
|
|
2467
2511
|
|
|
2512
|
+
const COSTAS_A = [0, 1, 3, 2];
|
|
2513
|
+
const COSTAS_B = [1, 0, 2, 3];
|
|
2514
|
+
const COSTAS_C = [2, 3, 1, 0];
|
|
2515
|
+
const COSTAS_D = [3, 2, 0, 1];
|
|
2468
2516
|
/**
|
|
2469
2517
|
* Convert FT4 LDPC codeword bits into 103 channel tones.
|
|
2470
2518
|
* Port of lib/ft4/genft4.f90.
|
|
@@ -2497,6 +2545,34 @@ function encode(msg, options = {}) {
|
|
|
2497
2545
|
return generateFT4Waveform(encodeMessage(msg), options);
|
|
2498
2546
|
}
|
|
2499
2547
|
|
|
2548
|
+
const NSPS = 1920;
|
|
2549
|
+
const NFFT1 = 2 * NSPS; // 3840
|
|
2550
|
+
const NSTEP = NSPS / 4; // 480
|
|
2551
|
+
const NMAX = 15 * 12_000; // 180000
|
|
2552
|
+
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
2553
|
+
const NDOWN = 60;
|
|
2554
|
+
const NN = 79;
|
|
2555
|
+
const NFFT1_LONG = 192000;
|
|
2556
|
+
const NFFT2 = 3200;
|
|
2557
|
+
const NP2 = 2812;
|
|
2558
|
+
const COSTAS_BLOCKS = 7;
|
|
2559
|
+
const COSTAS_SYMBOL_LEN = 32;
|
|
2560
|
+
const SYNC_TIME_SHIFTS = [0, 36, 72];
|
|
2561
|
+
const TAPER_SIZE = 101;
|
|
2562
|
+
const TAPER_LAST = TAPER_SIZE - 1;
|
|
2563
|
+
const TWO_PI = 2 * Math.PI;
|
|
2564
|
+
const MAX_DECODE_PASSES_DEPTH3 = 2;
|
|
2565
|
+
const SUBTRACTION_GAIN = 0.95;
|
|
2566
|
+
const SUBTRACTION_PHASE_SHIFT = Math.PI / 2;
|
|
2567
|
+
const MIN_SUBTRACTION_SNR = -22;
|
|
2568
|
+
const FS2 = SAMPLE_RATE / NDOWN;
|
|
2569
|
+
const DT2 = 1.0 / FS2;
|
|
2570
|
+
const DOWNSAMPLE_DF = SAMPLE_RATE / NFFT1_LONG;
|
|
2571
|
+
const DOWNSAMPLE_BAUD = SAMPLE_RATE / NSPS;
|
|
2572
|
+
const DOWNSAMPLE_SCALE = Math.sqrt(NFFT2 / NFFT1_LONG);
|
|
2573
|
+
const TAPER = buildTaper(TAPER_SIZE);
|
|
2574
|
+
const COSTAS_SYNC = buildCostasSyncTemplates();
|
|
2575
|
+
const FREQ_SHIFT_SYNC = buildFrequencyShiftSyncTemplates();
|
|
2500
2576
|
/**
|
|
2501
2577
|
* Decode all FT8 signals in an audio buffer.
|
|
2502
2578
|
* Input: mono audio samples at `sampleRate` Hz, duration ~15s.
|
|
@@ -2509,55 +2585,96 @@ function decode(samples, options = {}) {
|
|
|
2509
2585
|
const depth = options.depth ?? 2;
|
|
2510
2586
|
const maxCandidates = options.maxCandidates ?? 300;
|
|
2511
2587
|
const book = options.hashCallBook;
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
const len = Math.min(samples.length, NMAX);
|
|
2517
|
-
for (let i = 0; i < len; i++)
|
|
2518
|
-
dd[i] = samples[i];
|
|
2519
|
-
}
|
|
2520
|
-
else {
|
|
2521
|
-
dd = resample(samples, sampleRate, SAMPLE_RATE, NMAX);
|
|
2522
|
-
}
|
|
2523
|
-
// Compute huge FFT for downsampling caching
|
|
2524
|
-
const NFFT1_LONG = 192000;
|
|
2588
|
+
const dd = sampleRate === SAMPLE_RATE
|
|
2589
|
+
? copySamplesToDecodeWindow(samples)
|
|
2590
|
+
: resample(samples, sampleRate, SAMPLE_RATE, NMAX);
|
|
2591
|
+
const residual = new Float64Array(dd);
|
|
2525
2592
|
const cxRe = new Float64Array(NFFT1_LONG);
|
|
2526
2593
|
const cxIm = new Float64Array(NFFT1_LONG);
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
}
|
|
2530
|
-
fftComplex(cxRe, cxIm, false);
|
|
2531
|
-
// Compute spectrogram and find sync candidates
|
|
2532
|
-
const { candidates, sbase } = sync8(dd, nfa, nfb, syncmin, maxCandidates);
|
|
2594
|
+
const workspace = createDecodeWorkspace();
|
|
2595
|
+
const toneCache = new Map();
|
|
2533
2596
|
const decoded = [];
|
|
2534
2597
|
const seenMessages = new Set();
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2598
|
+
const maxPasses = depth >= 3 ? MAX_DECODE_PASSES_DEPTH3 : 1;
|
|
2599
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
2600
|
+
cxRe.fill(0);
|
|
2601
|
+
cxIm.fill(0);
|
|
2602
|
+
cxRe.set(residual);
|
|
2603
|
+
fftComplex(cxRe, cxIm, false);
|
|
2604
|
+
const { candidates, sbase } = sync8(residual, nfa, nfb, syncmin, maxCandidates);
|
|
2605
|
+
const coarseFrequencyUses = countCandidateFrequencies(candidates);
|
|
2606
|
+
const coarseDownsampleCache = new Map();
|
|
2607
|
+
let decodedInPass = 0;
|
|
2608
|
+
for (const cand of candidates) {
|
|
2609
|
+
const result = ft8b(residual, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book, workspace, coarseDownsampleCache, coarseFrequencyUses);
|
|
2610
|
+
if (!result)
|
|
2611
|
+
continue;
|
|
2612
|
+
const messageKey = normalizeMessageKey(result.msg);
|
|
2613
|
+
if (seenMessages.has(messageKey))
|
|
2614
|
+
continue;
|
|
2615
|
+
seenMessages.add(messageKey);
|
|
2616
|
+
decoded.push({
|
|
2617
|
+
freq: result.freq,
|
|
2618
|
+
dt: result.dt - 0.5,
|
|
2619
|
+
snr: result.snr,
|
|
2620
|
+
msg: result.msg,
|
|
2621
|
+
sync: cand.sync,
|
|
2622
|
+
});
|
|
2623
|
+
decodedInPass++;
|
|
2624
|
+
if (pass + 1 < maxPasses) {
|
|
2625
|
+
subtractDecodedSignal(residual, result, toneCache);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (decodedInPass === 0)
|
|
2629
|
+
break;
|
|
2549
2630
|
}
|
|
2550
2631
|
return decoded;
|
|
2551
2632
|
}
|
|
2633
|
+
function normalizeMessageKey(msg) {
|
|
2634
|
+
return msg.trim().replace(/\s+/g, " ").toUpperCase();
|
|
2635
|
+
}
|
|
2636
|
+
function countCandidateFrequencies(candidates) {
|
|
2637
|
+
const counts = new Map();
|
|
2638
|
+
for (const c of candidates) {
|
|
2639
|
+
counts.set(c.freq, (counts.get(c.freq) ?? 0) + 1);
|
|
2640
|
+
}
|
|
2641
|
+
return counts;
|
|
2642
|
+
}
|
|
2643
|
+
function createDecodeWorkspace() {
|
|
2644
|
+
return {
|
|
2645
|
+
cd0Re: new Float64Array(NFFT2),
|
|
2646
|
+
cd0Im: new Float64Array(NFFT2),
|
|
2647
|
+
shiftRe: new Float64Array(NFFT2),
|
|
2648
|
+
shiftIm: new Float64Array(NFFT2),
|
|
2649
|
+
s8: new Float64Array(8 * NN),
|
|
2650
|
+
csRe: new Float64Array(8 * NN),
|
|
2651
|
+
csIm: new Float64Array(8 * NN),
|
|
2652
|
+
symbRe: new Float64Array(COSTAS_SYMBOL_LEN),
|
|
2653
|
+
symbIm: new Float64Array(COSTAS_SYMBOL_LEN),
|
|
2654
|
+
s2: new Float64Array(1 << 9),
|
|
2655
|
+
bmeta: new Float64Array(N_LDPC),
|
|
2656
|
+
bmetb: new Float64Array(N_LDPC),
|
|
2657
|
+
bmetc: new Float64Array(N_LDPC),
|
|
2658
|
+
bmetd: new Float64Array(N_LDPC),
|
|
2659
|
+
llr: new Float64Array(N_LDPC),
|
|
2660
|
+
apmask: new Int8Array(N_LDPC),
|
|
2661
|
+
ss: new Float64Array(9),
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
function copySamplesToDecodeWindow(samples) {
|
|
2665
|
+
const out = new Float64Array(NMAX);
|
|
2666
|
+
const len = Math.min(samples.length, NMAX);
|
|
2667
|
+
for (let i = 0; i < len; i++)
|
|
2668
|
+
out[i] = samples[i];
|
|
2669
|
+
return out;
|
|
2670
|
+
}
|
|
2552
2671
|
function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
2553
2672
|
const JZ = 62;
|
|
2554
|
-
// Fortran uses NFFT1=3840 for the spectrogram FFT; we need a power of 2
|
|
2555
2673
|
const fftSize = nextPow2(NFFT1); // 4096
|
|
2556
|
-
const halfSize = fftSize / 2;
|
|
2674
|
+
const halfSize = fftSize / 2;
|
|
2557
2675
|
const tstep = NSTEP / SAMPLE_RATE;
|
|
2558
2676
|
const df = SAMPLE_RATE / fftSize;
|
|
2559
2677
|
const fac = 1.0 / 300.0;
|
|
2560
|
-
// Compute symbol spectra, stepping by NSTEP
|
|
2561
2678
|
const s = new Float64Array(halfSize * NHSYM);
|
|
2562
2679
|
const savg = new Float64Array(halfSize);
|
|
2563
2680
|
const xRe = new Float64Array(fftSize);
|
|
@@ -2566,31 +2683,32 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2566
2683
|
const ia = j * NSTEP;
|
|
2567
2684
|
xRe.fill(0);
|
|
2568
2685
|
xIm.fill(0);
|
|
2569
|
-
for (let i = 0; i < NSPS && ia + i < dd.length; i++)
|
|
2686
|
+
for (let i = 0; i < NSPS && ia + i < dd.length; i++)
|
|
2570
2687
|
xRe[i] = fac * dd[ia + i];
|
|
2571
|
-
}
|
|
2572
2688
|
fftComplex(xRe, xIm, false);
|
|
2573
2689
|
for (let i = 0; i < halfSize; i++) {
|
|
2574
2690
|
const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
|
|
2575
2691
|
s[i * NHSYM + j] = power;
|
|
2576
|
-
savg[i] =
|
|
2692
|
+
savg[i] = savg[i] + power;
|
|
2577
2693
|
}
|
|
2578
2694
|
}
|
|
2579
|
-
// Compute baseline
|
|
2580
2695
|
const sbase = computeBaseline(savg, nfa, nfb, df, halfSize);
|
|
2581
2696
|
const ia = Math.max(1, Math.round(nfa / df));
|
|
2582
2697
|
const ib = Math.min(halfSize - 14, Math.round(nfb / df));
|
|
2583
2698
|
const nssy = Math.floor(NSPS / NSTEP);
|
|
2584
|
-
const nfos = Math.round(SAMPLE_RATE / NSPS / df);
|
|
2699
|
+
const nfos = Math.round(SAMPLE_RATE / NSPS / df);
|
|
2585
2700
|
const jstrt = Math.round(0.5 / tstep);
|
|
2586
|
-
// 2D sync correlation
|
|
2587
|
-
const sync2d = new Float64Array((ib - ia + 1) * (2 * JZ + 1));
|
|
2588
2701
|
const width = 2 * JZ + 1;
|
|
2702
|
+
const sync2d = new Float64Array((ib - ia + 1) * width);
|
|
2589
2703
|
for (let i = ia; i <= ib; i++) {
|
|
2590
2704
|
for (let jj = -JZ; jj <= JZ; jj++) {
|
|
2591
|
-
let ta = 0
|
|
2592
|
-
let
|
|
2593
|
-
|
|
2705
|
+
let ta = 0;
|
|
2706
|
+
let tb = 0;
|
|
2707
|
+
let tc = 0;
|
|
2708
|
+
let t0a = 0;
|
|
2709
|
+
let t0b = 0;
|
|
2710
|
+
let t0c = 0;
|
|
2711
|
+
for (let n = 0; n < COSTAS_BLOCKS; n++) {
|
|
2594
2712
|
const m = jj + jstrt + nssy * n;
|
|
2595
2713
|
const iCostas = i + nfos * COSTAS[n];
|
|
2596
2714
|
if (m >= 0 && m < NHSYM && iCostas < halfSize) {
|
|
@@ -2621,17 +2739,14 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2621
2739
|
}
|
|
2622
2740
|
}
|
|
2623
2741
|
const t = ta + tb + tc;
|
|
2624
|
-
const
|
|
2625
|
-
const t0 = (t0total - t) / 6.0;
|
|
2742
|
+
const t0 = (t0a + t0b + t0c - t) / 6.0;
|
|
2626
2743
|
const syncVal = t0 > 0 ? t / t0 : 0;
|
|
2627
2744
|
const tbc = tb + tc;
|
|
2628
|
-
const t0bc = t0b + t0c;
|
|
2629
|
-
const
|
|
2630
|
-
const syncBc = t0bc2 > 0 ? tbc / t0bc2 : 0;
|
|
2745
|
+
const t0bc = (t0b + t0c - tbc) / 6.0;
|
|
2746
|
+
const syncBc = t0bc > 0 ? tbc / t0bc : 0;
|
|
2631
2747
|
sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
|
|
2632
2748
|
}
|
|
2633
2749
|
}
|
|
2634
|
-
// Find peaks
|
|
2635
2750
|
const candidates0 = [];
|
|
2636
2751
|
const mlag = 10;
|
|
2637
2752
|
for (let i = ia; i <= ib; i++) {
|
|
@@ -2644,7 +2759,6 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2644
2759
|
bestJ = j;
|
|
2645
2760
|
}
|
|
2646
2761
|
}
|
|
2647
|
-
// Also check wider range
|
|
2648
2762
|
let bestSync2 = -1;
|
|
2649
2763
|
let bestJ2 = 0;
|
|
2650
2764
|
for (let j = -JZ; j <= JZ; j++) {
|
|
@@ -2661,7 +2775,7 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2661
2775
|
sync: bestSync,
|
|
2662
2776
|
});
|
|
2663
2777
|
}
|
|
2664
|
-
if (
|
|
2778
|
+
if (bestJ2 !== bestJ && bestSync2 >= syncmin) {
|
|
2665
2779
|
candidates0.push({
|
|
2666
2780
|
freq: i * df,
|
|
2667
2781
|
dt: (bestJ2 - 0.5) * tstep,
|
|
@@ -2669,7 +2783,6 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2669
2783
|
});
|
|
2670
2784
|
}
|
|
2671
2785
|
}
|
|
2672
|
-
// Compute baseline normalization for sync values
|
|
2673
2786
|
const syncValues = candidates0.map((c) => c.sync);
|
|
2674
2787
|
syncValues.sort((a, b) => a - b);
|
|
2675
2788
|
const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
|
|
@@ -2678,7 +2791,6 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2678
2791
|
for (const c of candidates0)
|
|
2679
2792
|
c.sync /= base;
|
|
2680
2793
|
}
|
|
2681
|
-
// Remove near-duplicate candidates
|
|
2682
2794
|
for (let i = 0; i < candidates0.length; i++) {
|
|
2683
2795
|
for (let j = 0; j < i; j++) {
|
|
2684
2796
|
const fdiff = Math.abs(candidates0[i].freq - candidates0[j].freq);
|
|
@@ -2693,7 +2805,6 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
2693
2805
|
}
|
|
2694
2806
|
}
|
|
2695
2807
|
}
|
|
2696
|
-
// Sort by sync descending, take top maxcand
|
|
2697
2808
|
const filtered = candidates0.filter((c) => c.sync >= syncmin);
|
|
2698
2809
|
filtered.sort((a, b) => b.sync - a.sync);
|
|
2699
2810
|
return { candidates: filtered.slice(0, maxcand), sbase };
|
|
@@ -2702,8 +2813,7 @@ function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
|
2702
2813
|
const sbase = new Float64Array(nh1);
|
|
2703
2814
|
const ia = Math.max(1, Math.round(nfa / df));
|
|
2704
2815
|
const ib = Math.min(nh1 - 1, Math.round(nfb / df));
|
|
2705
|
-
|
|
2706
|
-
const window = 50; // bins
|
|
2816
|
+
const window = 50;
|
|
2707
2817
|
for (let i = 0; i < nh1; i++) {
|
|
2708
2818
|
let sum = 0;
|
|
2709
2819
|
let count = 0;
|
|
@@ -2717,54 +2827,86 @@ function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
|
2717
2827
|
}
|
|
2718
2828
|
return sbase;
|
|
2719
2829
|
}
|
|
2720
|
-
function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
const
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2830
|
+
function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book, workspace, coarseDownsampleCache, coarseFrequencyUses) {
|
|
2831
|
+
loadCoarseDownsample(cxRe, cxIm, f1, workspace, coarseDownsampleCache, coarseFrequencyUses);
|
|
2832
|
+
let ibest = findBestTimeOffset(workspace.cd0Re, workspace.cd0Im, xdt);
|
|
2833
|
+
const delfbest = findBestFrequencyShift(workspace.cd0Re, workspace.cd0Im, ibest);
|
|
2834
|
+
f1 += delfbest;
|
|
2835
|
+
ft8Downsample(cxRe, cxIm, f1, workspace);
|
|
2836
|
+
ibest = refineTimeOffset(workspace.cd0Re, workspace.cd0Im, ibest, workspace.ss);
|
|
2837
|
+
xdt = (ibest - 1) * DT2;
|
|
2838
|
+
extractSoftSymbols(workspace.cd0Re, workspace.cd0Im, ibest, workspace);
|
|
2839
|
+
const minCostasHits = depth >= 3 ? 6 : 7;
|
|
2840
|
+
if (!passesSyncGate(workspace.s8, minCostasHits))
|
|
2841
|
+
return null;
|
|
2842
|
+
buildBitMetrics(workspace);
|
|
2843
|
+
const result = tryDecodePasses(workspace, depth);
|
|
2844
|
+
if (!result)
|
|
2845
|
+
return null;
|
|
2846
|
+
if (result.cw.every((b) => b === 0))
|
|
2847
|
+
return null;
|
|
2848
|
+
const message77 = result.message91.slice(0, 77);
|
|
2849
|
+
if (!isValidMessageType(message77))
|
|
2850
|
+
return null;
|
|
2851
|
+
const { msg, success } = unpack77(message77, book);
|
|
2852
|
+
if (!success || msg.trim().length === 0)
|
|
2853
|
+
return null;
|
|
2854
|
+
const snr = estimateSnr(workspace.s8, result.cw);
|
|
2855
|
+
return { msg, freq: f1, dt: xdt, snr };
|
|
2856
|
+
}
|
|
2857
|
+
function loadCoarseDownsample(cxRe, cxIm, f0, workspace, coarseDownsampleCache, coarseFrequencyUses) {
|
|
2858
|
+
const cached = coarseDownsampleCache.get(f0);
|
|
2859
|
+
if (cached) {
|
|
2860
|
+
workspace.cd0Re.set(cached.re);
|
|
2861
|
+
workspace.cd0Im.set(cached.im);
|
|
2862
|
+
}
|
|
2863
|
+
else {
|
|
2864
|
+
ft8Downsample(cxRe, cxIm, f0, workspace);
|
|
2865
|
+
const uses = coarseFrequencyUses.get(f0) ?? 0;
|
|
2866
|
+
if (uses > 1) {
|
|
2867
|
+
coarseDownsampleCache.set(f0, {
|
|
2868
|
+
re: new Float64Array(workspace.cd0Re),
|
|
2869
|
+
im: new Float64Array(workspace.cd0Im),
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
const remaining = (coarseFrequencyUses.get(f0) ?? 1) - 1;
|
|
2874
|
+
if (remaining <= 0) {
|
|
2875
|
+
coarseFrequencyUses.delete(f0);
|
|
2876
|
+
coarseDownsampleCache.delete(f0);
|
|
2877
|
+
}
|
|
2878
|
+
else {
|
|
2879
|
+
coarseFrequencyUses.set(f0, remaining);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function findBestTimeOffset(cd0Re, cd0Im, xdt) {
|
|
2883
|
+
const i0 = Math.round((xdt + 0.5) * FS2);
|
|
2732
2884
|
let smax = 0;
|
|
2733
2885
|
let ibest = i0;
|
|
2734
2886
|
for (let idt = i0 - 10; idt <= i0 + 10; idt++) {
|
|
2735
|
-
const sync = sync8d(cd0Re, cd0Im, idt,
|
|
2887
|
+
const sync = sync8d(cd0Re, cd0Im, idt, COSTAS_SYNC.re, COSTAS_SYNC.im);
|
|
2736
2888
|
if (sync > smax) {
|
|
2737
2889
|
smax = sync;
|
|
2738
2890
|
ibest = idt;
|
|
2739
2891
|
}
|
|
2740
2892
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2893
|
+
return ibest;
|
|
2894
|
+
}
|
|
2895
|
+
function findBestFrequencyShift(cd0Re, cd0Im, ibest) {
|
|
2896
|
+
let smax = 0;
|
|
2743
2897
|
let delfbest = 0;
|
|
2744
|
-
for (
|
|
2745
|
-
const
|
|
2746
|
-
const dphi = twopi * delf * dt2;
|
|
2747
|
-
const twkRe = new Float64Array(32);
|
|
2748
|
-
const twkIm = new Float64Array(32);
|
|
2749
|
-
let phi = 0;
|
|
2750
|
-
for (let i = 0; i < 32; i++) {
|
|
2751
|
-
twkRe[i] = Math.cos(phi);
|
|
2752
|
-
twkIm[i] = Math.sin(phi);
|
|
2753
|
-
phi = (phi + dphi) % twopi;
|
|
2754
|
-
}
|
|
2755
|
-
const sync = sync8d(cd0Re, cd0Im, ibest, twkRe, twkIm, true);
|
|
2898
|
+
for (const tpl of FREQ_SHIFT_SYNC) {
|
|
2899
|
+
const sync = sync8d(cd0Re, cd0Im, ibest, tpl.re, tpl.im);
|
|
2756
2900
|
if (sync > smax) {
|
|
2757
2901
|
smax = sync;
|
|
2758
|
-
delfbest = delf;
|
|
2902
|
+
delfbest = tpl.delf;
|
|
2759
2903
|
}
|
|
2760
2904
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
// Refine time offset
|
|
2765
|
-
const ss = new Float64Array(9);
|
|
2905
|
+
return delfbest;
|
|
2906
|
+
}
|
|
2907
|
+
function refineTimeOffset(cd0Re, cd0Im, ibest, ss) {
|
|
2766
2908
|
for (let idt = -4; idt <= 4; idt++) {
|
|
2767
|
-
ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt,
|
|
2909
|
+
ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt, COSTAS_SYNC.re, COSTAS_SYNC.im);
|
|
2768
2910
|
}
|
|
2769
2911
|
let maxss = -1;
|
|
2770
2912
|
let maxIdx = 4;
|
|
@@ -2774,20 +2916,16 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2774
2916
|
maxIdx = i;
|
|
2775
2917
|
}
|
|
2776
2918
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
const s8
|
|
2781
|
-
const csRe = new Float64Array(8 * NN);
|
|
2782
|
-
const csIm = new Float64Array(8 * NN);
|
|
2783
|
-
const symbRe = new Float64Array(32);
|
|
2784
|
-
const symbIm = new Float64Array(32);
|
|
2919
|
+
return ibest + maxIdx - 4;
|
|
2920
|
+
}
|
|
2921
|
+
function extractSoftSymbols(cd0Re, cd0Im, ibest, workspace) {
|
|
2922
|
+
const { s8, csRe, csIm, symbRe, symbIm } = workspace;
|
|
2785
2923
|
for (let k = 0; k < NN; k++) {
|
|
2786
|
-
const i1 = ibest + k *
|
|
2924
|
+
const i1 = ibest + k * COSTAS_SYMBOL_LEN;
|
|
2787
2925
|
symbRe.fill(0);
|
|
2788
2926
|
symbIm.fill(0);
|
|
2789
|
-
if (i1 >= 0 && i1 +
|
|
2790
|
-
for (let j = 0; j <
|
|
2927
|
+
if (i1 >= 0 && i1 + COSTAS_SYMBOL_LEN - 1 < NP2) {
|
|
2928
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
2791
2929
|
symbRe[j] = cd0Re[i1 + j];
|
|
2792
2930
|
symbIm[j] = cd0Im[i1 + j];
|
|
2793
2931
|
}
|
|
@@ -2796,15 +2934,17 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2796
2934
|
for (let tone = 0; tone < 8; tone++) {
|
|
2797
2935
|
const re = symbRe[tone] / 1000;
|
|
2798
2936
|
const im = symbIm[tone] / 1000;
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2937
|
+
const idx = tone * NN + k;
|
|
2938
|
+
csRe[idx] = re;
|
|
2939
|
+
csIm[idx] = im;
|
|
2940
|
+
s8[idx] = Math.sqrt(re * re + im * im);
|
|
2802
2941
|
}
|
|
2803
2942
|
}
|
|
2804
|
-
|
|
2943
|
+
}
|
|
2944
|
+
function passesSyncGate(s8, minCostasHits) {
|
|
2805
2945
|
let nsync = 0;
|
|
2806
|
-
for (let k = 0; k <
|
|
2807
|
-
for (const offset of
|
|
2946
|
+
for (let k = 0; k < COSTAS_BLOCKS; k++) {
|
|
2947
|
+
for (const offset of SYNC_TIME_SHIFTS) {
|
|
2808
2948
|
let maxTone = 0;
|
|
2809
2949
|
let maxVal = -1;
|
|
2810
2950
|
for (let t = 0; t < 8; t++) {
|
|
@@ -2818,21 +2958,20 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2818
2958
|
nsync++;
|
|
2819
2959
|
}
|
|
2820
2960
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2961
|
+
return nsync >= minCostasHits;
|
|
2962
|
+
}
|
|
2963
|
+
function buildBitMetrics(workspace) {
|
|
2964
|
+
const { csRe, csIm, bmeta, bmetb, bmetc, bmetd, s2 } = workspace;
|
|
2965
|
+
bmeta.fill(0);
|
|
2966
|
+
bmetb.fill(0);
|
|
2967
|
+
bmetc.fill(0);
|
|
2968
|
+
bmetd.fill(0);
|
|
2829
2969
|
for (let nsym = 1; nsym <= 3; nsym++) {
|
|
2830
|
-
const nt = 1 << (3 * nsym);
|
|
2970
|
+
const nt = 1 << (3 * nsym);
|
|
2831
2971
|
const ibmax = nsym === 1 ? 2 : nsym === 2 ? 5 : 8;
|
|
2832
2972
|
for (let ihalf = 1; ihalf <= 2; ihalf++) {
|
|
2833
2973
|
for (let k = 1; k <= 29; k += nsym) {
|
|
2834
2974
|
const ks = ihalf === 1 ? k + 7 : k + 43;
|
|
2835
|
-
const s2 = new Float64Array(nt);
|
|
2836
2975
|
for (let i = 0; i < nt; i++) {
|
|
2837
2976
|
const i1 = Math.floor(i / 64);
|
|
2838
2977
|
const i2 = Math.floor((i & 63) / 8);
|
|
@@ -2857,11 +2996,10 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2857
2996
|
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
2858
2997
|
}
|
|
2859
2998
|
}
|
|
2860
|
-
// Fortran: i32 = 1 + (k-1)*3 + (ihalf-1)*87 (1-based)
|
|
2861
2999
|
const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
|
|
2862
3000
|
for (let ib = 0; ib <= ibmax; ib++) {
|
|
2863
|
-
|
|
2864
|
-
let
|
|
3001
|
+
let max1 = -1e30;
|
|
3002
|
+
let max0 = -1e30;
|
|
2865
3003
|
for (let i = 0; i < nt; i++) {
|
|
2866
3004
|
const bitSet = (i & (1 << (ibmax - ib))) !== 0;
|
|
2867
3005
|
if (bitSet) {
|
|
@@ -2873,20 +3011,20 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2873
3011
|
max0 = s2[i];
|
|
2874
3012
|
}
|
|
2875
3013
|
}
|
|
2876
|
-
const idx = i32 + ib - 1;
|
|
2877
|
-
if (idx
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3014
|
+
const idx = i32 + ib - 1;
|
|
3015
|
+
if (idx < 0 || idx >= N_LDPC)
|
|
3016
|
+
continue;
|
|
3017
|
+
const bm = max1 - max0;
|
|
3018
|
+
if (nsym === 1) {
|
|
3019
|
+
bmeta[idx] = bm;
|
|
3020
|
+
const den = Math.max(max1, max0);
|
|
3021
|
+
bmetd[idx] = den > 0 ? bm / den : 0;
|
|
3022
|
+
}
|
|
3023
|
+
else if (nsym === 2) {
|
|
3024
|
+
bmetb[idx] = bm;
|
|
3025
|
+
}
|
|
3026
|
+
else {
|
|
3027
|
+
bmetc[idx] = bm;
|
|
2890
3028
|
}
|
|
2891
3029
|
}
|
|
2892
3030
|
}
|
|
@@ -2896,42 +3034,35 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2896
3034
|
normalizeBmet(bmetb);
|
|
2897
3035
|
normalizeBmet(bmetc);
|
|
2898
3036
|
normalizeBmet(bmetd);
|
|
2899
|
-
|
|
3037
|
+
}
|
|
3038
|
+
function tryDecodePasses(workspace, depth) {
|
|
2900
3039
|
const scalefac = 2.83;
|
|
2901
3040
|
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
2902
|
-
const
|
|
2903
|
-
|
|
2904
|
-
let result = null;
|
|
3041
|
+
const bmetrics = [workspace.bmeta, workspace.bmetb, workspace.bmetc, workspace.bmetd];
|
|
3042
|
+
workspace.apmask.fill(0);
|
|
2905
3043
|
for (let ipass = 0; ipass < 4; ipass++) {
|
|
2906
|
-
const
|
|
3044
|
+
const metric = bmetrics[ipass];
|
|
2907
3045
|
for (let i = 0; i < N_LDPC; i++)
|
|
2908
|
-
llr[i] = scalefac *
|
|
2909
|
-
result = decode174_91(llr, apmask, maxosd);
|
|
3046
|
+
workspace.llr[i] = scalefac * metric[i];
|
|
3047
|
+
const result = decode174_91(workspace.llr, workspace.apmask, maxosd);
|
|
2910
3048
|
if (result && result.nharderrors >= 0 && result.nharderrors <= 36)
|
|
2911
|
-
|
|
2912
|
-
result = null;
|
|
3049
|
+
return result;
|
|
2913
3050
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
if (result.cw.every((b) => b === 0))
|
|
2918
|
-
return null;
|
|
2919
|
-
const message77 = result.message91.slice(0, 77);
|
|
2920
|
-
// Validate message type
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
function isValidMessageType(message77) {
|
|
2921
3054
|
const n3v = (message77[71] << 2) | (message77[72] << 1) | message77[73];
|
|
2922
3055
|
const i3v = (message77[74] << 2) | (message77[75] << 1) | message77[76];
|
|
2923
3056
|
if (i3v > 5 || (i3v === 0 && n3v > 6))
|
|
2924
|
-
return
|
|
3057
|
+
return false;
|
|
2925
3058
|
if (i3v === 0 && n3v === 2)
|
|
2926
|
-
return
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
return null;
|
|
2931
|
-
// Estimate SNR
|
|
3059
|
+
return false;
|
|
3060
|
+
return true;
|
|
3061
|
+
}
|
|
3062
|
+
function estimateSnr(s8, cw) {
|
|
2932
3063
|
let xsig = 0;
|
|
2933
3064
|
let xnoi = 0;
|
|
2934
|
-
const itone = getTones(
|
|
3065
|
+
const itone = getTones(cw);
|
|
2935
3066
|
for (let i = 0; i < 79; i++) {
|
|
2936
3067
|
xsig += s8[itone[i] * NN + i] ** 2;
|
|
2937
3068
|
const ios = (itone[i] + 4) % 7;
|
|
@@ -2942,9 +3073,7 @@ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
|
2942
3073
|
if (arg > 0.1)
|
|
2943
3074
|
snr = arg;
|
|
2944
3075
|
snr = 10 * Math.log10(snr) - 27.0;
|
|
2945
|
-
|
|
2946
|
-
snr = -24;
|
|
2947
|
-
return { msg, freq: f1, dt: xdt, snr };
|
|
3076
|
+
return snr < -24 ? -24 : snr;
|
|
2948
3077
|
}
|
|
2949
3078
|
function getTones(cw) {
|
|
2950
3079
|
const tones = new Array(79).fill(0);
|
|
@@ -2969,112 +3098,77 @@ function getTones(cw) {
|
|
|
2969
3098
|
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
2970
3099
|
* Identical to Fortran ft8_downsample.
|
|
2971
3100
|
*/
|
|
2972
|
-
function ft8Downsample(cxRe, cxIm, f0,
|
|
2973
|
-
const
|
|
2974
|
-
const
|
|
2975
|
-
const
|
|
2976
|
-
// NSPS is imported, should be 1920
|
|
2977
|
-
const baud = 12000.0 / NSPS; // 6.25
|
|
3101
|
+
function ft8Downsample(cxRe, cxIm, f0, workspace) {
|
|
3102
|
+
const { cd0Re, cd0Im, shiftRe, shiftIm } = workspace;
|
|
3103
|
+
const df = DOWNSAMPLE_DF;
|
|
3104
|
+
const baud = DOWNSAMPLE_BAUD;
|
|
2978
3105
|
const i0 = Math.round(f0 / df);
|
|
2979
3106
|
const ft = f0 + 8.5 * baud;
|
|
2980
|
-
const it = Math.min(Math.round(ft / df),
|
|
3107
|
+
const it = Math.min(Math.round(ft / df), NFFT1_LONG / 2);
|
|
2981
3108
|
const fb = f0 - 1.5 * baud;
|
|
2982
3109
|
const ib = Math.max(1, Math.round(fb / df));
|
|
2983
|
-
|
|
2984
|
-
|
|
3110
|
+
cd0Re.fill(0);
|
|
3111
|
+
cd0Im.fill(0);
|
|
2985
3112
|
let k = 0;
|
|
2986
3113
|
for (let i = ib; i <= it; i++) {
|
|
2987
3114
|
if (k >= NFFT2)
|
|
2988
3115
|
break;
|
|
2989
|
-
|
|
2990
|
-
|
|
3116
|
+
cd0Re[k] = cxRe[i];
|
|
3117
|
+
cd0Im[k] = cxIm[i];
|
|
2991
3118
|
k++;
|
|
2992
3119
|
}
|
|
2993
|
-
|
|
2994
|
-
const pi = Math.PI;
|
|
2995
|
-
const taper = new Float64Array(101);
|
|
2996
|
-
for (let i = 0; i <= 100; i++) {
|
|
2997
|
-
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
2998
|
-
}
|
|
2999
|
-
for (let i = 0; i <= 100; i++) {
|
|
3120
|
+
for (let i = 0; i <= TAPER_LAST; i++) {
|
|
3000
3121
|
if (i >= NFFT2)
|
|
3001
3122
|
break;
|
|
3002
|
-
const tap =
|
|
3003
|
-
|
|
3004
|
-
|
|
3123
|
+
const tap = TAPER[TAPER_LAST - i];
|
|
3124
|
+
cd0Re[i] = cd0Re[i] * tap;
|
|
3125
|
+
cd0Im[i] = cd0Im[i] * tap;
|
|
3005
3126
|
}
|
|
3006
3127
|
const endTap = k - 1;
|
|
3007
|
-
for (let i = 0; i <=
|
|
3008
|
-
const idx = endTap -
|
|
3128
|
+
for (let i = 0; i <= TAPER_LAST; i++) {
|
|
3129
|
+
const idx = endTap - TAPER_LAST + i;
|
|
3009
3130
|
if (idx >= 0 && idx < NFFT2) {
|
|
3010
|
-
const tap =
|
|
3011
|
-
|
|
3012
|
-
|
|
3131
|
+
const tap = TAPER[i];
|
|
3132
|
+
cd0Re[idx] = cd0Re[idx] * tap;
|
|
3133
|
+
cd0Im[idx] = cd0Im[idx] * tap;
|
|
3013
3134
|
}
|
|
3014
3135
|
}
|
|
3015
|
-
// CSHIFT
|
|
3016
3136
|
const shift = i0 - ib;
|
|
3017
|
-
const tempRe = new Float64Array(NFFT2);
|
|
3018
|
-
const tempIm = new Float64Array(NFFT2);
|
|
3019
3137
|
for (let i = 0; i < NFFT2; i++) {
|
|
3020
3138
|
let srcIdx = (i + shift) % NFFT2;
|
|
3021
3139
|
if (srcIdx < 0)
|
|
3022
3140
|
srcIdx += NFFT2;
|
|
3023
|
-
|
|
3024
|
-
|
|
3141
|
+
shiftRe[i] = cd0Re[srcIdx];
|
|
3142
|
+
shiftIm[i] = cd0Im[srcIdx];
|
|
3025
3143
|
}
|
|
3026
3144
|
for (let i = 0; i < NFFT2; i++) {
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
}
|
|
3030
|
-
|
|
3031
|
-
fftComplex(c1Re, c1Im, true);
|
|
3032
|
-
// Scale
|
|
3033
|
-
// Fortran uses 1.0/sqrt(NFFT1 * NFFT2), but our fftComplex(true) scales by 1/NFFT2
|
|
3034
|
-
const scale = Math.sqrt(NFFT2 / NFFT1);
|
|
3145
|
+
cd0Re[i] = shiftRe[i];
|
|
3146
|
+
cd0Im[i] = shiftIm[i];
|
|
3147
|
+
}
|
|
3148
|
+
fftComplex(cd0Re, cd0Im, true);
|
|
3035
3149
|
for (let i = 0; i < NFFT2; i++) {
|
|
3036
|
-
|
|
3037
|
-
|
|
3150
|
+
cd0Re[i] = cd0Re[i] * DOWNSAMPLE_SCALE;
|
|
3151
|
+
cd0Im[i] = cd0Im[i] * DOWNSAMPLE_SCALE;
|
|
3038
3152
|
}
|
|
3039
3153
|
}
|
|
3040
|
-
function sync8d(cd0Re, cd0Im, i0,
|
|
3041
|
-
const NP2 = 2812;
|
|
3042
|
-
const twopi = 2 * Math.PI;
|
|
3043
|
-
// Precompute Costas sync waveforms
|
|
3044
|
-
const csyncRe = new Float64Array(7 * 32);
|
|
3045
|
-
const csyncIm = new Float64Array(7 * 32);
|
|
3046
|
-
for (let i = 0; i < 7; i++) {
|
|
3047
|
-
let phi = 0;
|
|
3048
|
-
const dphi = (twopi * COSTAS[i]) / 32;
|
|
3049
|
-
for (let j = 0; j < 32; j++) {
|
|
3050
|
-
csyncRe[i * 32 + j] = Math.cos(phi);
|
|
3051
|
-
csyncIm[i * 32 + j] = Math.sin(phi);
|
|
3052
|
-
phi = (phi + dphi) % twopi;
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3154
|
+
function sync8d(cd0Re, cd0Im, i0, syncRe, syncIm) {
|
|
3055
3155
|
let sync = 0;
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3060
|
-
for (
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
// Conjugate multiply: cd0 * conj(csync)
|
|
3073
|
-
const dRe = cd0Re[iStart + j];
|
|
3074
|
-
const dIm = cd0Im[iStart + j];
|
|
3075
|
-
zRe += dRe * sRe + dIm * sIm;
|
|
3076
|
-
zIm += dIm * sRe - dRe * sIm;
|
|
3077
|
-
}
|
|
3156
|
+
const stride = 36 * COSTAS_SYMBOL_LEN;
|
|
3157
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3158
|
+
const base = i * COSTAS_SYMBOL_LEN;
|
|
3159
|
+
let iStart = i0 + i * COSTAS_SYMBOL_LEN;
|
|
3160
|
+
for (let block = 0; block < 3; block++, iStart += stride) {
|
|
3161
|
+
if (iStart < 0 || iStart + COSTAS_SYMBOL_LEN - 1 >= NP2)
|
|
3162
|
+
continue;
|
|
3163
|
+
let zRe = 0;
|
|
3164
|
+
let zIm = 0;
|
|
3165
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3166
|
+
const sRe = syncRe[base + j];
|
|
3167
|
+
const sIm = syncIm[base + j];
|
|
3168
|
+
const dRe = cd0Re[iStart + j];
|
|
3169
|
+
const dIm = cd0Im[iStart + j];
|
|
3170
|
+
zRe += dRe * sRe + dIm * sIm;
|
|
3171
|
+
zIm += dIm * sRe - dRe * sIm;
|
|
3078
3172
|
}
|
|
3079
3173
|
sync += zRe * zRe + zIm * zIm;
|
|
3080
3174
|
}
|
|
@@ -3083,7 +3177,8 @@ function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
|
|
|
3083
3177
|
}
|
|
3084
3178
|
function normalizeBmet(bmet) {
|
|
3085
3179
|
const n = bmet.length;
|
|
3086
|
-
let sum = 0
|
|
3180
|
+
let sum = 0;
|
|
3181
|
+
let sum2 = 0;
|
|
3087
3182
|
for (let i = 0; i < n; i++) {
|
|
3088
3183
|
sum += bmet[i];
|
|
3089
3184
|
sum2 += bmet[i] * bmet[i];
|
|
@@ -3110,6 +3205,121 @@ function resample(input, fromRate, toRate, outLen) {
|
|
|
3110
3205
|
}
|
|
3111
3206
|
return out;
|
|
3112
3207
|
}
|
|
3208
|
+
function subtractDecodedSignal(residual, result, toneCache) {
|
|
3209
|
+
if (result.snr < MIN_SUBTRACTION_SNR)
|
|
3210
|
+
return;
|
|
3211
|
+
const msgKey = normalizeMessageKey(result.msg);
|
|
3212
|
+
let tones = toneCache.get(msgKey);
|
|
3213
|
+
if (!tones) {
|
|
3214
|
+
try {
|
|
3215
|
+
tones = encodeMessage$1(result.msg);
|
|
3216
|
+
}
|
|
3217
|
+
catch {
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
toneCache.set(msgKey, tones);
|
|
3221
|
+
}
|
|
3222
|
+
const waveI = generateFT8Waveform(tones, {
|
|
3223
|
+
sampleRate: SAMPLE_RATE,
|
|
3224
|
+
samplesPerSymbol: NSPS,
|
|
3225
|
+
baseFrequency: result.freq,
|
|
3226
|
+
initialPhase: 0,
|
|
3227
|
+
});
|
|
3228
|
+
const waveQ = generateFT8Waveform(tones, {
|
|
3229
|
+
sampleRate: SAMPLE_RATE,
|
|
3230
|
+
samplesPerSymbol: NSPS,
|
|
3231
|
+
baseFrequency: result.freq,
|
|
3232
|
+
initialPhase: SUBTRACTION_PHASE_SHIFT,
|
|
3233
|
+
});
|
|
3234
|
+
const start = Math.round(result.dt * SAMPLE_RATE);
|
|
3235
|
+
let srcStart = start;
|
|
3236
|
+
let tplStart = 0;
|
|
3237
|
+
if (srcStart < 0) {
|
|
3238
|
+
tplStart = -srcStart;
|
|
3239
|
+
srcStart = 0;
|
|
3240
|
+
}
|
|
3241
|
+
const maxLen = Math.min(residual.length - srcStart, waveI.length - tplStart, waveQ.length - tplStart);
|
|
3242
|
+
if (maxLen <= 0)
|
|
3243
|
+
return;
|
|
3244
|
+
let sii = 0;
|
|
3245
|
+
let sqq = 0;
|
|
3246
|
+
let siq = 0;
|
|
3247
|
+
let sri = 0;
|
|
3248
|
+
let srq = 0;
|
|
3249
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3250
|
+
const wi = waveI[tplStart + i];
|
|
3251
|
+
const wq = waveQ[tplStart + i];
|
|
3252
|
+
const rv = residual[srcStart + i];
|
|
3253
|
+
sii += wi * wi;
|
|
3254
|
+
sqq += wq * wq;
|
|
3255
|
+
siq += wi * wq;
|
|
3256
|
+
sri += rv * wi;
|
|
3257
|
+
srq += rv * wq;
|
|
3258
|
+
}
|
|
3259
|
+
const det = sii * sqq - siq * siq;
|
|
3260
|
+
if (det <= 1e-9)
|
|
3261
|
+
return;
|
|
3262
|
+
const ampI = (sri * sqq - srq * siq) / det;
|
|
3263
|
+
const ampQ = (srq * sii - sri * siq) / det;
|
|
3264
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3265
|
+
const wi = waveI[tplStart + i];
|
|
3266
|
+
const wq = waveQ[tplStart + i];
|
|
3267
|
+
const idx = srcStart + i;
|
|
3268
|
+
residual[idx] = residual[idx] - SUBTRACTION_GAIN * (ampI * wi + ampQ * wq);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
function buildTaper(size) {
|
|
3272
|
+
const taper = new Float64Array(size);
|
|
3273
|
+
const last = size - 1;
|
|
3274
|
+
for (let i = 0; i < size; i++)
|
|
3275
|
+
taper[i] = 0.5 * (1.0 + Math.cos((i * Math.PI) / last));
|
|
3276
|
+
return taper;
|
|
3277
|
+
}
|
|
3278
|
+
function buildCostasSyncTemplates() {
|
|
3279
|
+
const re = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3280
|
+
const im = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3281
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3282
|
+
let phi = 0;
|
|
3283
|
+
const dphi = (TWO_PI * COSTAS[i]) / COSTAS_SYMBOL_LEN;
|
|
3284
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3285
|
+
re[i * COSTAS_SYMBOL_LEN + j] = Math.cos(phi);
|
|
3286
|
+
im[i * COSTAS_SYMBOL_LEN + j] = Math.sin(phi);
|
|
3287
|
+
phi = (phi + dphi) % TWO_PI;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return { re, im };
|
|
3291
|
+
}
|
|
3292
|
+
function buildFrequencyShiftSyncTemplates() {
|
|
3293
|
+
const templates = [];
|
|
3294
|
+
for (let ifr = -5; ifr <= 5; ifr++) {
|
|
3295
|
+
const delf = ifr * 0.5;
|
|
3296
|
+
const dphi = TWO_PI * delf * DT2;
|
|
3297
|
+
const twkRe = new Float64Array(COSTAS_SYMBOL_LEN);
|
|
3298
|
+
const twkIm = new Float64Array(COSTAS_SYMBOL_LEN);
|
|
3299
|
+
let phi = 0;
|
|
3300
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3301
|
+
twkRe[j] = Math.cos(phi);
|
|
3302
|
+
twkIm[j] = Math.sin(phi);
|
|
3303
|
+
phi = (phi + dphi) % TWO_PI;
|
|
3304
|
+
}
|
|
3305
|
+
const re = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3306
|
+
const im = new Float64Array(COSTAS_BLOCKS * COSTAS_SYMBOL_LEN);
|
|
3307
|
+
for (let i = 0; i < COSTAS_BLOCKS; i++) {
|
|
3308
|
+
const base = i * COSTAS_SYMBOL_LEN;
|
|
3309
|
+
for (let j = 0; j < COSTAS_SYMBOL_LEN; j++) {
|
|
3310
|
+
const idx = base + j;
|
|
3311
|
+
const csRe = COSTAS_SYNC.re[idx];
|
|
3312
|
+
const csIm = COSTAS_SYNC.im[idx];
|
|
3313
|
+
const tRe = twkRe[j] * csRe - twkIm[j] * csIm;
|
|
3314
|
+
const tIm = twkRe[j] * csIm + twkIm[j] * csRe;
|
|
3315
|
+
re[idx] = tRe;
|
|
3316
|
+
im[idx] = tIm;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
templates.push({ delf, re, im });
|
|
3320
|
+
}
|
|
3321
|
+
return templates;
|
|
3322
|
+
}
|
|
3113
3323
|
|
|
3114
3324
|
/**
|
|
3115
3325
|
* Hash call table – TypeScript port of the hash call storage from packjt77.f90
|