@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/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.from({ length: N }, (_, i) => i);
383
- indices.sort((a, b) => Math.abs(llr[b]) - Math.abs(llr[a]));
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 i = 0; i < N; i++) {
387
- for (let k = 0; k < K; k++) {
388
- genmrb[k * N + i] = gen[k * N + indices[i]];
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
- for (let icol = id; icol < Math.min(K + 20, N); icol++) {
395
- if (genmrb[id * N + icol] === 1) {
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 tmp = genmrb[k * N + id];
400
- genmrb[k * N + id] = genmrb[k * N + icol];
401
- genmrb[k * N + icol] = tmp;
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 !== id && genmrb[ii * N + id] === 1) {
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[ii * N + c] ^= genmrb[id * N + c];
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
- hdec[i] = llr[indices[i]] >= 0 ? 1 : 0;
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] = Math.abs(llr[indices[i]]);
441
+ absrx[i] = absllr[indices[i]];
429
442
  }
430
- // Transpose of reordered gen matrix
431
- const g2 = new Uint8Array(N * K);
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
- g2[j * K + i] = genmrb[i * N + j];
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 me = new Int8Array(m0);
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 = ce[j] ^ hdec[j];
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
- bestCw.set(ce);
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(40, K);
476
- for (let i1 = K - 1; i1 >= K - ntry; i1--) {
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
- for (let i2 = i1 - 1; i2 >= K - ntry; i2--) {
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 me = new Int8Array(m0);
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 = ce[j] ^ hdec[j];
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
- bestCw.set(ce);
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 x = finalCw[i] ^ hdecOrig[i];
524
+ const hard = llr[i] >= 0 ? 1 : 0;
525
+ const x = finalCw[i] ^ hard;
514
526
  nhe += x;
515
- dminOrig += x * Math.abs(llr[i]);
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] /= n;
613
- im[i] /= n;
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 = nextPow2(n * 2 - 1);
620
- const s = inverse ? 1 : -1;
621
- const aRe = new Float64Array(m);
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 angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
627
- const cosA = Math.cos(angle);
628
- const sinA = Math.sin(angle);
629
- aRe[i] = re[i] * cosA - im[i] * sinA;
630
- aIm[i] = re[i] * sinA + im[i] * cosA;
631
- bRe[i] = cosA;
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 r = aRe[i] * bRe[i] - aIm[i] * bIm[i];
642
- const iIm = aRe[i] * bIm[i] + aIm[i] * bRe[i];
643
- aRe[i] = r;
644
- aIm[i] = iIm;
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 angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
650
- const cosA = Math.cos(angle);
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) / NSTEP$1); // 122
996
+ const NHSYM$1 = Math.floor((NMAX$1 - NFFT1$1) / NSPS$1); // 122
935
997
  const NDOWN$1 = 18;
936
- const ND = 87;
937
- const NS = 16;
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$1 = 2 * Math.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
- function xorWithScrambler(bits77) {
953
- const out = new Array(77);
954
- for (let i = 0; i < 77; i++) {
955
- out[i] = ((bits77[i] ?? 0) + RVEC[i]) & 1;
956
- }
957
- return out;
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
- ? copyIntoFt4Buffer(samples)
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, downsampleCtx, tweakedSyncTemplates, depth, book, apmask);
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 decodeCandidate(candidate, cxRe, cxIm, downsampleCtx, tweakedSyncTemplates, depth, book, apmask) {
1004
- const cd2 = ft4Downsample(cxRe, cxIm, candidate.freq, downsampleCtx);
1005
- normalizeComplexPower(cd2.re, cd2.im, NMAX$1 / NDOWN$1);
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
- let ibest = -1;
1008
- let idfbest = 0;
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
- const f1 = candidate.freq + idfbest;
1064
- if (f1 <= 10 || f1 >= 4990) {
1095
+ const f1 = candidate.freq + coarse.idfbest;
1096
+ if (f1 <= 10 || f1 >= 4990)
1065
1097
  continue;
1066
- }
1067
- const cb = ft4Downsample(cxRe, cxIm, f1, downsampleCtx);
1068
- normalizeComplexPower(cb.re, cb.im, NSS * NN$1);
1069
- const frame = extractFrame(cb.re, cb.im, ibest);
1070
- const metrics = getFt4Bitmetrics(frame.re, frame.im);
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 [llra, llrb, llrc] = buildLlrs(metrics.bitmetrics1, metrics.bitmetrics2, metrics.bitmetrics3);
1078
- const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
1079
- const scalefac = 2.83;
1080
- for (const src of [llra, llrb, llrc]) {
1081
- const llr = new Float64Array(174);
1082
- for (let i = 0; i < 174; i++) {
1083
- llr[i] = scalefac * src[i];
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
- const result = decode174_91(llr, apmask, maxosd);
1086
- if (!result) {
1087
- continue;
1148
+ else if (segment === 2) {
1149
+ ibmin = 560;
1150
+ ibmax = 1012;
1088
1151
  }
1089
- const message77Scrambled = result.message91.slice(0, 77);
1090
- if (!hasNonZeroBit(message77Scrambled)) {
1091
- continue;
1152
+ else {
1153
+ ibmin = -344;
1154
+ ibmax = 108;
1092
1155
  }
1093
- const message77 = xorWithScrambler(message77Scrambled);
1094
- const { msg, success } = unpack77(message77, book);
1095
- if (!success || msg.trim().length === 0) {
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 null;
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] * window[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
- const c1Re = new Float64Array(NFFT2);
1367
- const c1Im = new Float64Array(NFFT2);
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
- c1Re[0] = cxRe[i0] ?? 0;
1371
- c1Im[0] = cxIm[i0] ?? 0;
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
- c1Re[i] = cxRe[hi] ?? 0;
1377
- c1Im[i] = cxIm[hi] ?? 0;
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
- c1Re[idx] = cxRe[lo] ?? 0;
1383
- c1Im[idx] = cxIm[lo] ?? 0;
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
- c1Re[i] = c1Re[i] * w;
1390
- c1Im[i] = c1Im[i] * w;
1430
+ outRe[i] = outRe[i] * w;
1431
+ outIm[i] = outIm[i] * w;
1391
1432
  }
1392
- fftComplex(c1Re, c1Im, true);
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 = -16; idf <= 16; 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$1 * tone * 2) / NSS;
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$1;
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$1 * idf) / fsample;
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 < 4; i++) {
1495
- const z = correlateStride2(cdRe, cdIm, starts[i], templates[i].re, templates[i].im);
1496
- if (z.count <= 16) {
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 getFt4Bitmetrics(cdRe, cdIm) {
1523
- const csRe = new Float64Array(4 * NN$1);
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); // 4, 16, 256
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 > 2 * NN$1) {
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 { bitmetrics1, bitmetrics2, bitmetrics3, badsync: false };
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(bitmetrics1, bitmetrics2, bitmetrics3) {
1692
- const llra = new Float64Array(174);
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
- return [llra, llrb, llrc];
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
- const cs = basecall.length === 5 ? ` ${basecall}` : basecall;
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 = NSPS$1;
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 = 0;
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
- // Resample to 12000 Hz if needed
2513
- let dd;
2514
- if (sampleRate === SAMPLE_RATE) {
2515
- dd = new Float64Array(NMAX);
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
- for (let i = 0; i < NMAX; i++) {
2528
- cxRe[i] = dd[i] ?? 0;
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
- for (const cand of candidates) {
2536
- const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book);
2537
- if (!result)
2538
- continue;
2539
- if (seenMessages.has(result.msg))
2540
- continue;
2541
- seenMessages.add(result.msg);
2542
- decoded.push({
2543
- freq: result.freq,
2544
- dt: result.dt - 0.5,
2545
- snr: result.snr,
2546
- msg: result.msg,
2547
- sync: cand.sync,
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; // 2048
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] = (savg[i] ?? 0) + power;
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); // ~2 bins per tone spacing
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, tb = 0, tc = 0;
2592
- let t0a = 0, t0b = 0, t0c = 0;
2593
- for (let n = 0; n < 7; n++) {
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 t0total = t0a + t0b + t0c;
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 t0bc2 = (t0bc - tbc) / 6.0;
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 (Math.abs(bestJ2 - bestJ) > 0 && bestSync2 >= syncmin) {
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
- // Smooth the spectrum to get baseline
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
- const NFFT2 = 3200;
2722
- const NP2 = 2812;
2723
- const fs2 = SAMPLE_RATE / NDOWN;
2724
- const dt2 = 1.0 / fs2;
2725
- const twopi = 2 * Math.PI;
2726
- // Downsample: mix to baseband and filter
2727
- const cd0Re = new Float64Array(NFFT2);
2728
- const cd0Im = new Float64Array(NFFT2);
2729
- ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
2730
- // Find best time offset
2731
- const i0 = Math.round((xdt + 0.5) * fs2);
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, null, null, false);
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
- // Fine frequency search
2742
- smax = 0;
2893
+ return ibest;
2894
+ }
2895
+ function findBestFrequencyShift(cd0Re, cd0Im, ibest) {
2896
+ let smax = 0;
2743
2897
  let delfbest = 0;
2744
- for (let ifr = -5; ifr <= 5; ifr++) {
2745
- const delf = ifr * 0.5;
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
- // Apply frequency correction and re-downsample
2762
- f1 += delfbest;
2763
- ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
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, null, null, false);
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
- ibest = ibest + maxIdx - 4;
2778
- xdt = (ibest - 1) * dt2;
2779
- // Extract 8-tone soft symbols for each of NN=79 symbols
2780
- const s8 = new Float64Array(8 * NN);
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 * 32;
2924
+ const i1 = ibest + k * COSTAS_SYMBOL_LEN;
2787
2925
  symbRe.fill(0);
2788
2926
  symbIm.fill(0);
2789
- if (i1 >= 0 && i1 + 31 < NP2) {
2790
- for (let j = 0; j < 32; 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
- csRe[tone * NN + k] = re;
2800
- csIm[tone * NN + k] = im;
2801
- s8[tone * NN + k] = Math.sqrt(re * re + im * im);
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
- // Sync quality check
2943
+ }
2944
+ function passesSyncGate(s8, minCostasHits) {
2805
2945
  let nsync = 0;
2806
- for (let k = 0; k < 7; k++) {
2807
- for (const offset of [0, 36, 72]) {
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
- if (nsync <= 6)
2822
- return null;
2823
- // Compute soft bit metrics for multiple nsym values (1, 2, 3)
2824
- // and a normalized version, matching the Fortran ft8b passes 1-4
2825
- const bmeta = new Float64Array(N_LDPC); // nsym=1
2826
- const bmetb = new Float64Array(N_LDPC); // nsym=2
2827
- const bmetc = new Float64Array(N_LDPC); // nsym=3
2828
- const bmetd = new Float64Array(N_LDPC); // nsym=1 normalized
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); // 8, 64, 512
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
- // max of s2 where bit (ibmax-ib) of index is 1
2864
- let max1 = -1e30, max0 = -1e30;
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; // Convert to 0-based
2877
- if (idx >= 0 && idx < N_LDPC) {
2878
- const bm = max1 - max0;
2879
- if (nsym === 1) {
2880
- bmeta[idx] = bm;
2881
- const den = Math.max(max1, max0);
2882
- bmetd[idx] = den > 0 ? bm / den : 0;
2883
- }
2884
- else if (nsym === 2) {
2885
- bmetb[idx] = bm;
2886
- }
2887
- else {
2888
- bmetc[idx] = bm;
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
- const bmetrics = [bmeta, bmetb, bmetc, bmetd];
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 apmask = new Int8Array(N_LDPC);
2903
- // Try 4 passes with different soft-symbol metrics (matching Fortran)
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 llr = new Float64Array(N_LDPC);
3044
+ const metric = bmetrics[ipass];
2907
3045
  for (let i = 0; i < N_LDPC; i++)
2908
- llr[i] = scalefac * bmetrics[ipass][i];
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
- break;
2912
- result = null;
3049
+ return result;
2913
3050
  }
2914
- if (!result || result.nharderrors < 0 || result.nharderrors > 36)
2915
- return null;
2916
- // Check for all-zero codeword
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 null;
3057
+ return false;
2925
3058
  if (i3v === 0 && n3v === 2)
2926
- return null;
2927
- // Unpack
2928
- const { msg, success } = unpack77(message77, book);
2929
- if (!success || msg.trim().length === 0)
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(result.cw);
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
- if (snr < -24)
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, c1Re, c1Im) {
2973
- const NFFT1 = 192000;
2974
- const NFFT2 = 3200;
2975
- const df = 12000.0 / NFFT1;
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), NFFT1 / 2);
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
- c1Re.fill(0);
2984
- c1Im.fill(0);
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
- c1Re[k] = cxRe[i] ?? 0;
2990
- c1Im[k] = cxIm[i] ?? 0;
3116
+ cd0Re[k] = cxRe[i];
3117
+ cd0Im[k] = cxIm[i];
2991
3118
  k++;
2992
3119
  }
2993
- // Taper
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 = taper[100 - i];
3003
- c1Re[i] = c1Re[i] * tap;
3004
- c1Im[i] = c1Im[i] * tap;
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 <= 100; i++) {
3008
- const idx = endTap - 100 + i;
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 = taper[i];
3011
- c1Re[idx] = c1Re[idx] * tap;
3012
- c1Im[idx] = c1Im[idx] * tap;
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
- tempRe[i] = c1Re[srcIdx];
3024
- tempIm[i] = c1Im[srcIdx];
3141
+ shiftRe[i] = cd0Re[srcIdx];
3142
+ shiftIm[i] = cd0Im[srcIdx];
3025
3143
  }
3026
3144
  for (let i = 0; i < NFFT2; i++) {
3027
- c1Re[i] = tempRe[i];
3028
- c1Im[i] = tempIm[i];
3029
- }
3030
- // iFFT
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
- c1Re[i] = c1Re[i] * scale;
3037
- c1Im[i] = c1Im[i] * scale;
3150
+ cd0Re[i] = cd0Re[i] * DOWNSAMPLE_SCALE;
3151
+ cd0Im[i] = cd0Im[i] * DOWNSAMPLE_SCALE;
3038
3152
  }
3039
3153
  }
3040
- function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
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
- for (let i = 0; i < 7; i++) {
3057
- const i1 = i0 + i * 32;
3058
- const i2 = i1 + 36 * 32;
3059
- const i3 = i1 + 72 * 32;
3060
- for (const iStart of [i1, i2, i3]) {
3061
- let zRe = 0, zIm = 0;
3062
- if (iStart >= 0 && iStart + 31 < NP2) {
3063
- for (let j = 0; j < 32; j++) {
3064
- let sRe = csyncRe[i * 32 + j];
3065
- let sIm = csyncIm[i * 32 + j];
3066
- if (useTwk && twkRe && twkIm) {
3067
- const tRe = twkRe[j] * sRe - twkIm[j] * sIm;
3068
- const tIm = twkRe[j] * sIm + twkIm[j] * sRe;
3069
- sRe = tRe;
3070
- sIm = tIm;
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, sum2 = 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