@e04/ft8ts 0.0.1 → 0.0.3
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 +69 -9
- package/dist/ft8ts.cjs +262 -54
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +57 -1
- package/dist/ft8ts.mjs +262 -55
- package/dist/ft8ts.mjs.map +1 -1
- package/example/browser/index.html +4 -4
- package/package.json +51 -51
- package/src/ft8/decode.ts +101 -47
- package/src/index.ts +1 -0
- package/src/util/fft.ts +56 -0
- package/src/util/hashcall.ts +110 -0
- package/src/util/unpack_jt77.ts +17 -8
- package/src/__test__/190227_155815.wav +0 -0
- package/src/__test__/decode.test.ts +0 -117
- package/src/__test__/encode.test.ts +0 -52
- package/src/__test__/test_vectors.ts +0 -221
- package/src/__test__/wav.test.ts +0 -45
- package/src/__test__/waveform.test.ts +0 -28
package/dist/ft8ts.mjs
CHANGED
|
@@ -566,6 +566,10 @@ function fftComplex(re, im, inverse) {
|
|
|
566
566
|
const n = re.length;
|
|
567
567
|
if (n <= 1)
|
|
568
568
|
return;
|
|
569
|
+
if ((n & (n - 1)) !== 0) {
|
|
570
|
+
bluestein(re, im, inverse);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
569
573
|
// Bit-reversal permutation
|
|
570
574
|
let j = 0;
|
|
571
575
|
for (let i = 0; i < n; i++) {
|
|
@@ -584,7 +588,7 @@ function fftComplex(re, im, inverse) {
|
|
|
584
588
|
}
|
|
585
589
|
j += m;
|
|
586
590
|
}
|
|
587
|
-
const sign = -1;
|
|
591
|
+
const sign = inverse ? 1 : -1;
|
|
588
592
|
for (let size = 2; size <= n; size <<= 1) {
|
|
589
593
|
const halfsize = size >> 1;
|
|
590
594
|
const step = (sign * Math.PI) / halfsize;
|
|
@@ -608,6 +612,53 @@ function fftComplex(re, im, inverse) {
|
|
|
608
612
|
}
|
|
609
613
|
}
|
|
610
614
|
}
|
|
615
|
+
if (inverse) {
|
|
616
|
+
for (let i = 0; i < n; i++) {
|
|
617
|
+
re[i] /= n;
|
|
618
|
+
im[i] /= n;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function bluestein(re, im, inverse) {
|
|
623
|
+
const n = re.length;
|
|
624
|
+
const m = nextPow2(n * 2 - 1);
|
|
625
|
+
const s = inverse ? 1 : -1;
|
|
626
|
+
const aRe = new Float64Array(m);
|
|
627
|
+
const aIm = new Float64Array(m);
|
|
628
|
+
const bRe = new Float64Array(m);
|
|
629
|
+
const bIm = new Float64Array(m);
|
|
630
|
+
for (let i = 0; i < n; i++) {
|
|
631
|
+
const angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
|
|
632
|
+
const cosA = Math.cos(angle);
|
|
633
|
+
const sinA = Math.sin(angle);
|
|
634
|
+
aRe[i] = re[i] * cosA - im[i] * sinA;
|
|
635
|
+
aIm[i] = re[i] * sinA + im[i] * cosA;
|
|
636
|
+
bRe[i] = cosA;
|
|
637
|
+
bIm[i] = -sinA;
|
|
638
|
+
}
|
|
639
|
+
for (let i = 1; i < n; i++) {
|
|
640
|
+
bRe[m - i] = bRe[i];
|
|
641
|
+
bIm[m - i] = bIm[i];
|
|
642
|
+
}
|
|
643
|
+
fftComplex(aRe, aIm, false);
|
|
644
|
+
fftComplex(bRe, bIm, false);
|
|
645
|
+
for (let i = 0; i < m; i++) {
|
|
646
|
+
const r = aRe[i] * bRe[i] - aIm[i] * bIm[i];
|
|
647
|
+
const iIm = aRe[i] * bIm[i] + aIm[i] * bRe[i];
|
|
648
|
+
aRe[i] = r;
|
|
649
|
+
aIm[i] = iIm;
|
|
650
|
+
}
|
|
651
|
+
fftComplex(aRe, aIm, true);
|
|
652
|
+
const scale = inverse ? 1 / n : 1;
|
|
653
|
+
for (let i = 0; i < n; i++) {
|
|
654
|
+
const angle = (s * Math.PI * ((i * i) % (2 * n))) / n;
|
|
655
|
+
const cosA = Math.cos(angle);
|
|
656
|
+
const sinA = Math.sin(angle);
|
|
657
|
+
const r = aRe[i] * cosA - aIm[i] * sinA;
|
|
658
|
+
const iIm = aRe[i] * sinA + aIm[i] * cosA;
|
|
659
|
+
re[i] = r * scale;
|
|
660
|
+
im[i] = iIm * scale;
|
|
661
|
+
}
|
|
611
662
|
}
|
|
612
663
|
/** Next power of 2 >= n */
|
|
613
664
|
function nextPow2(n) {
|
|
@@ -633,7 +684,7 @@ function bitsToUint(bits, start, len) {
|
|
|
633
684
|
}
|
|
634
685
|
return val;
|
|
635
686
|
}
|
|
636
|
-
function unpack28(n28) {
|
|
687
|
+
function unpack28(n28, book) {
|
|
637
688
|
if (n28 < 0 || n28 >= 268435456)
|
|
638
689
|
return { call: "", success: false };
|
|
639
690
|
if (n28 === 0)
|
|
@@ -647,7 +698,6 @@ function unpack28(n28) {
|
|
|
647
698
|
return { call: `CQ ${nqsy.toString().padStart(3, "0")}`, success: true };
|
|
648
699
|
}
|
|
649
700
|
if (n28 >= 1003 && n28 < NTOKENS) {
|
|
650
|
-
// CQ with 4-letter directed call
|
|
651
701
|
let m = n28 - 1003;
|
|
652
702
|
let chars = "";
|
|
653
703
|
for (let i = 3; i >= 0; i--) {
|
|
@@ -661,7 +711,10 @@ function unpack28(n28) {
|
|
|
661
711
|
return { call: "CQ", success: true };
|
|
662
712
|
}
|
|
663
713
|
if (n28 >= NTOKENS && n28 < NTOKENS + MAX22) {
|
|
664
|
-
|
|
714
|
+
const n22 = n28 - NTOKENS;
|
|
715
|
+
const resolved = book?.lookup22(n22);
|
|
716
|
+
if (resolved)
|
|
717
|
+
return { call: `<${resolved}>`, success: true };
|
|
665
718
|
return { call: "<...>", success: true };
|
|
666
719
|
}
|
|
667
720
|
// Standard callsign
|
|
@@ -741,8 +794,11 @@ function unpackText77(bits71) {
|
|
|
741
794
|
}
|
|
742
795
|
/**
|
|
743
796
|
* Unpack a 77-bit FT8 message into a human-readable string.
|
|
797
|
+
*
|
|
798
|
+
* When a {@link HashCallBook} is provided, hashed callsigns are resolved from
|
|
799
|
+
* the book, and newly decoded standard callsigns are saved into it.
|
|
744
800
|
*/
|
|
745
|
-
function unpack77(bits77) {
|
|
801
|
+
function unpack77(bits77, book) {
|
|
746
802
|
const n3 = bitsToUint(bits77, 71, 3);
|
|
747
803
|
const i3 = bitsToUint(bits77, 74, 3);
|
|
748
804
|
if (i3 === 0 && n3 === 0) {
|
|
@@ -760,8 +816,8 @@ function unpack77(bits77) {
|
|
|
760
816
|
const ipb = bits77[57];
|
|
761
817
|
const ir = bits77[58];
|
|
762
818
|
const igrid4 = bitsToUint(bits77, 59, 15);
|
|
763
|
-
const { call: call1, success: ok1 } = unpack28(n28a);
|
|
764
|
-
const { call: call2Raw, success: ok2 } = unpack28(n28b);
|
|
819
|
+
const { call: call1, success: ok1 } = unpack28(n28a, book);
|
|
820
|
+
const { call: call2Raw, success: ok2 } = unpack28(n28b, book);
|
|
765
821
|
if (!ok1 || !ok2)
|
|
766
822
|
return { msg: "", success: false };
|
|
767
823
|
let c1 = call1;
|
|
@@ -779,6 +835,9 @@ function unpack77(bits77) {
|
|
|
779
835
|
c2 += "/R";
|
|
780
836
|
if (ipb === 1 && i3 === 2 && c2.length >= 3)
|
|
781
837
|
c2 += "/P";
|
|
838
|
+
// Save the "from" call (call_2) into the hash book
|
|
839
|
+
if (book && c2.length >= 3)
|
|
840
|
+
book.save(c2);
|
|
782
841
|
}
|
|
783
842
|
if (igrid4 <= MAXGRID4) {
|
|
784
843
|
const { grid, success: gridOk } = toGrid4(igrid4);
|
|
@@ -811,6 +870,7 @@ function unpack77(bits77) {
|
|
|
811
870
|
}
|
|
812
871
|
if (i3 === 4) {
|
|
813
872
|
// Type 4: One nonstandard call
|
|
873
|
+
const n12 = bitsToUint(bits77, 0, 12);
|
|
814
874
|
let n58 = 0n;
|
|
815
875
|
for (let i = 0; i < 58; i++) {
|
|
816
876
|
n58 = n58 * 2n + BigInt(bits77[12 + i] ?? 0);
|
|
@@ -818,7 +878,6 @@ function unpack77(bits77) {
|
|
|
818
878
|
const iflip = bits77[70];
|
|
819
879
|
const nrpt = bitsToUint(bits77, 71, 2);
|
|
820
880
|
const icq = bits77[73];
|
|
821
|
-
// Decode n58 to 11-char string using C38 alphabet
|
|
822
881
|
const c11chars = [];
|
|
823
882
|
let remain = n58;
|
|
824
883
|
for (let i = 10; i >= 0; i--) {
|
|
@@ -827,12 +886,15 @@ function unpack77(bits77) {
|
|
|
827
886
|
c11chars.unshift(C38[j] ?? " ");
|
|
828
887
|
}
|
|
829
888
|
const c11 = c11chars.join("").trim();
|
|
830
|
-
const
|
|
889
|
+
const resolved = book?.lookup12(n12);
|
|
890
|
+
const call3 = resolved ? `<${resolved}>` : "<...>";
|
|
831
891
|
let call1;
|
|
832
892
|
let call2;
|
|
833
893
|
if (iflip === 0) {
|
|
834
894
|
call1 = call3;
|
|
835
895
|
call2 = c11;
|
|
896
|
+
if (book)
|
|
897
|
+
book.save(c11);
|
|
836
898
|
}
|
|
837
899
|
else {
|
|
838
900
|
call1 = c11;
|
|
@@ -867,6 +929,7 @@ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
|
|
|
867
929
|
const syncmin = options.syncMin ?? 1.2;
|
|
868
930
|
const depth = options.depth ?? 2;
|
|
869
931
|
const maxCandidates = options.maxCandidates ?? 300;
|
|
932
|
+
const book = options.hashCallBook;
|
|
870
933
|
// Resample to 12000 Hz if needed
|
|
871
934
|
let dd;
|
|
872
935
|
if (sampleRate === SAMPLE_RATE) {
|
|
@@ -878,12 +941,20 @@ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
|
|
|
878
941
|
else {
|
|
879
942
|
dd = resample(samples, sampleRate, SAMPLE_RATE, NMAX);
|
|
880
943
|
}
|
|
944
|
+
// Compute huge FFT for downsampling caching
|
|
945
|
+
const NFFT1_LONG = 192000;
|
|
946
|
+
const cxRe = new Float64Array(NFFT1_LONG);
|
|
947
|
+
const cxIm = new Float64Array(NFFT1_LONG);
|
|
948
|
+
for (let i = 0; i < NMAX; i++) {
|
|
949
|
+
cxRe[i] = dd[i] ?? 0;
|
|
950
|
+
}
|
|
951
|
+
fftComplex(cxRe, cxIm, false);
|
|
881
952
|
// Compute spectrogram and find sync candidates
|
|
882
953
|
const { candidates, sbase } = sync8(dd, nfa, nfb, syncmin, maxCandidates);
|
|
883
954
|
const decoded = [];
|
|
884
955
|
const seenMessages = new Set();
|
|
885
956
|
for (const cand of candidates) {
|
|
886
|
-
const result = ft8b(dd, cand.freq, cand.dt, sbase, depth);
|
|
957
|
+
const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book);
|
|
887
958
|
if (!result)
|
|
888
959
|
continue;
|
|
889
960
|
if (seenMessages.has(result.msg))
|
|
@@ -919,7 +990,7 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
919
990
|
for (let i = 0; i < NSPS && ia + i < dd.length; i++) {
|
|
920
991
|
xRe[i] = fac * dd[ia + i];
|
|
921
992
|
}
|
|
922
|
-
fftComplex(xRe, xIm);
|
|
993
|
+
fftComplex(xRe, xIm, false);
|
|
923
994
|
for (let i = 0; i < halfSize; i++) {
|
|
924
995
|
const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
|
|
925
996
|
s[i * NHSYM + j] = power;
|
|
@@ -1067,17 +1138,16 @@ function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
|
1067
1138
|
}
|
|
1068
1139
|
return sbase;
|
|
1069
1140
|
}
|
|
1070
|
-
function ft8b(
|
|
1141
|
+
function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
1071
1142
|
const NFFT2 = 3200;
|
|
1072
1143
|
const NP2 = 2812;
|
|
1073
|
-
const NFFT1_LONG = 192000;
|
|
1074
1144
|
const fs2 = SAMPLE_RATE / NDOWN;
|
|
1075
1145
|
const dt2 = 1.0 / fs2;
|
|
1076
1146
|
const twopi = 2 * Math.PI;
|
|
1077
1147
|
// Downsample: mix to baseband and filter
|
|
1078
1148
|
const cd0Re = new Float64Array(NFFT2);
|
|
1079
1149
|
const cd0Im = new Float64Array(NFFT2);
|
|
1080
|
-
ft8Downsample(
|
|
1150
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
1081
1151
|
// Find best time offset
|
|
1082
1152
|
const i0 = Math.round((xdt + 0.5) * fs2);
|
|
1083
1153
|
let smax = 0;
|
|
@@ -1111,7 +1181,7 @@ function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
|
1111
1181
|
}
|
|
1112
1182
|
// Apply frequency correction and re-downsample
|
|
1113
1183
|
f1 += delfbest;
|
|
1114
|
-
ft8Downsample(
|
|
1184
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
1115
1185
|
// Refine time offset
|
|
1116
1186
|
const ss = new Float64Array(9);
|
|
1117
1187
|
for (let idt = -4; idt <= 4; idt++) {
|
|
@@ -1143,7 +1213,7 @@ function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
|
1143
1213
|
symbIm[j] = cd0Im[i1 + j];
|
|
1144
1214
|
}
|
|
1145
1215
|
}
|
|
1146
|
-
fftComplex(symbRe, symbIm);
|
|
1216
|
+
fftComplex(symbRe, symbIm, false);
|
|
1147
1217
|
for (let tone = 0; tone < 8; tone++) {
|
|
1148
1218
|
const re = symbRe[tone] / 1000;
|
|
1149
1219
|
const im = symbIm[tone] / 1000;
|
|
@@ -1276,7 +1346,7 @@ function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
|
1276
1346
|
if (i3v === 0 && n3v === 2)
|
|
1277
1347
|
return null;
|
|
1278
1348
|
// Unpack
|
|
1279
|
-
const { msg, success } = unpack77(message77);
|
|
1349
|
+
const { msg, success } = unpack77(message77, book);
|
|
1280
1350
|
if (!success || msg.trim().length === 0)
|
|
1281
1351
|
return null;
|
|
1282
1352
|
// Estimate SNR
|
|
@@ -1317,44 +1387,75 @@ function getTones$1(cw) {
|
|
|
1317
1387
|
return tones;
|
|
1318
1388
|
}
|
|
1319
1389
|
/**
|
|
1320
|
-
* Mix f0 to baseband and decimate by NDOWN (60x).
|
|
1321
|
-
*
|
|
1322
|
-
* Output: complex baseband signal at 200 Hz sample rate (32 samples/symbol).
|
|
1390
|
+
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
1391
|
+
* Identical to Fortran ft8_downsample.
|
|
1323
1392
|
*/
|
|
1324
|
-
function ft8Downsample(
|
|
1325
|
-
const
|
|
1326
|
-
const
|
|
1327
|
-
const
|
|
1328
|
-
//
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1393
|
+
function ft8Downsample(cxRe, cxIm, f0, c1Re, c1Im) {
|
|
1394
|
+
const NFFT1 = 192000;
|
|
1395
|
+
const NFFT2 = 3200;
|
|
1396
|
+
const df = 12000.0 / NFFT1;
|
|
1397
|
+
// NSPS is imported, should be 1920
|
|
1398
|
+
const baud = 12000.0 / NSPS; // 6.25
|
|
1399
|
+
const i0 = Math.round(f0 / df);
|
|
1400
|
+
const ft = f0 + 8.5 * baud;
|
|
1401
|
+
const it = Math.min(Math.round(ft / df), NFFT1 / 2);
|
|
1402
|
+
const fb = f0 - 1.5 * baud;
|
|
1403
|
+
const ib = Math.max(1, Math.round(fb / df));
|
|
1404
|
+
c1Re.fill(0);
|
|
1405
|
+
c1Im.fill(0);
|
|
1406
|
+
let k = 0;
|
|
1407
|
+
for (let i = ib; i <= it; i++) {
|
|
1408
|
+
if (k >= NFFT2)
|
|
1409
|
+
break;
|
|
1410
|
+
c1Re[k] = cxRe[i] ?? 0;
|
|
1411
|
+
c1Im[k] = cxIm[i] ?? 0;
|
|
1412
|
+
k++;
|
|
1413
|
+
}
|
|
1414
|
+
// Taper
|
|
1415
|
+
const pi = Math.PI;
|
|
1416
|
+
const taper = new Float64Array(101);
|
|
1417
|
+
for (let i = 0; i <= 100; i++) {
|
|
1418
|
+
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
1419
|
+
}
|
|
1420
|
+
for (let i = 0; i <= 100; i++) {
|
|
1421
|
+
if (i >= NFFT2)
|
|
1422
|
+
break;
|
|
1423
|
+
const tap = taper[100 - i];
|
|
1424
|
+
c1Re[i] = c1Re[i] * tap;
|
|
1425
|
+
c1Im[i] = c1Im[i] * tap;
|
|
1426
|
+
}
|
|
1427
|
+
const endTap = k - 1;
|
|
1428
|
+
for (let i = 0; i <= 100; i++) {
|
|
1429
|
+
const idx = endTap - 100 + i;
|
|
1430
|
+
if (idx >= 0 && idx < NFFT2) {
|
|
1431
|
+
const tap = taper[i];
|
|
1432
|
+
c1Re[idx] = c1Re[idx] * tap;
|
|
1433
|
+
c1Im[idx] = c1Im[idx] * tap;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
// CSHIFT
|
|
1437
|
+
const shift = i0 - ib;
|
|
1438
|
+
const tempRe = new Float64Array(NFFT2);
|
|
1439
|
+
const tempIm = new Float64Array(NFFT2);
|
|
1440
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1441
|
+
let srcIdx = (i + shift) % NFFT2;
|
|
1442
|
+
if (srcIdx < 0)
|
|
1443
|
+
srcIdx += NFFT2;
|
|
1444
|
+
tempRe[i] = c1Re[srcIdx];
|
|
1445
|
+
tempIm[i] = c1Im[srcIdx];
|
|
1446
|
+
}
|
|
1447
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1448
|
+
c1Re[i] = tempRe[i];
|
|
1449
|
+
c1Im[i] = tempIm[i];
|
|
1450
|
+
}
|
|
1451
|
+
// iFFT
|
|
1452
|
+
fftComplex(c1Re, c1Im, true);
|
|
1453
|
+
// Scale
|
|
1454
|
+
// Fortran uses 1.0/sqrt(NFFT1 * NFFT2), but our fftComplex(true) scales by 1/NFFT2
|
|
1455
|
+
const scale = Math.sqrt(NFFT2 / NFFT1);
|
|
1456
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1457
|
+
c1Re[i] = c1Re[i] * scale;
|
|
1458
|
+
c1Im[i] = c1Im[i] * scale;
|
|
1358
1459
|
}
|
|
1359
1460
|
}
|
|
1360
1461
|
function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
|
|
@@ -2112,5 +2213,111 @@ function encode(msg, options = {}) {
|
|
|
2112
2213
|
return generateFT8Waveform(encodeMessage(msg), options);
|
|
2113
2214
|
}
|
|
2114
2215
|
|
|
2115
|
-
|
|
2216
|
+
/**
|
|
2217
|
+
* Hash call table – TypeScript port of the hash call storage from packjt77.f90
|
|
2218
|
+
*
|
|
2219
|
+
* In FT8, nonstandard callsigns are transmitted as hashes (10-, 12-, or 22-bit).
|
|
2220
|
+
* When a full callsign is decoded from a standard message, it is stored in this
|
|
2221
|
+
* table so that future hashed references to it can be resolved.
|
|
2222
|
+
*
|
|
2223
|
+
* Mirrors Fortran: save_hash_call, hash10, hash12, hash22, ihashcall
|
|
2224
|
+
*/
|
|
2225
|
+
const MAGIC = 47055833459n;
|
|
2226
|
+
const MAX_HASH22_ENTRIES = 1000;
|
|
2227
|
+
function ihashcall(c0, m) {
|
|
2228
|
+
const s = c0.padEnd(11, " ").slice(0, 11).toUpperCase();
|
|
2229
|
+
let n8 = 0n;
|
|
2230
|
+
for (let i = 0; i < 11; i++) {
|
|
2231
|
+
const j = C38.indexOf(s[i] ?? " ");
|
|
2232
|
+
n8 = 38n * n8 + BigInt(j < 0 ? 0 : j);
|
|
2233
|
+
}
|
|
2234
|
+
const prod = BigInt.asUintN(64, MAGIC * n8);
|
|
2235
|
+
return Number(prod >> BigInt(64 - m)) & ((1 << m) - 1);
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Maintains a callsign ↔ hash lookup table for resolving hashed FT8 callsigns.
|
|
2239
|
+
*
|
|
2240
|
+
* Usage:
|
|
2241
|
+
* ```ts
|
|
2242
|
+
* const book = new HashCallBook();
|
|
2243
|
+
* const decoded = decodeFT8(samples, sampleRate, { hashCallBook: book });
|
|
2244
|
+
* // `book` now contains callsigns learned from decoded messages.
|
|
2245
|
+
* // Subsequent calls reuse the same book to resolve hashed callsigns:
|
|
2246
|
+
* const decoded2 = decodeFT8(samples2, sampleRate, { hashCallBook: book });
|
|
2247
|
+
* ```
|
|
2248
|
+
*
|
|
2249
|
+
* You can also pre-populate the book with known callsigns:
|
|
2250
|
+
* ```ts
|
|
2251
|
+
* book.save("W9XYZ");
|
|
2252
|
+
* book.save("PJ4/K1ABC");
|
|
2253
|
+
* ```
|
|
2254
|
+
*/
|
|
2255
|
+
class HashCallBook {
|
|
2256
|
+
calls10 = new Map();
|
|
2257
|
+
calls12 = new Map();
|
|
2258
|
+
hash22Entries = [];
|
|
2259
|
+
/**
|
|
2260
|
+
* Store a callsign in all three hash tables (10, 12, 22-bit).
|
|
2261
|
+
* Strips angle brackets if present. Ignores `<...>` and blank/short strings.
|
|
2262
|
+
*/
|
|
2263
|
+
save(callsign) {
|
|
2264
|
+
let cw = callsign.trim().toUpperCase();
|
|
2265
|
+
if (cw === "" || cw === "<...>")
|
|
2266
|
+
return;
|
|
2267
|
+
if (cw.startsWith("<"))
|
|
2268
|
+
cw = cw.slice(1);
|
|
2269
|
+
const gt = cw.indexOf(">");
|
|
2270
|
+
if (gt >= 0)
|
|
2271
|
+
cw = cw.slice(0, gt);
|
|
2272
|
+
cw = cw.trim();
|
|
2273
|
+
if (cw.length < 3)
|
|
2274
|
+
return;
|
|
2275
|
+
const n10 = ihashcall(cw, 10);
|
|
2276
|
+
if (n10 >= 0 && n10 <= 1023)
|
|
2277
|
+
this.calls10.set(n10, cw);
|
|
2278
|
+
const n12 = ihashcall(cw, 12);
|
|
2279
|
+
if (n12 >= 0 && n12 <= 4095)
|
|
2280
|
+
this.calls12.set(n12, cw);
|
|
2281
|
+
const n22 = ihashcall(cw, 22);
|
|
2282
|
+
const existing = this.hash22Entries.findIndex((e) => e.hash === n22);
|
|
2283
|
+
if (existing >= 0) {
|
|
2284
|
+
this.hash22Entries[existing].call = cw;
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
if (this.hash22Entries.length >= MAX_HASH22_ENTRIES) {
|
|
2288
|
+
this.hash22Entries.pop();
|
|
2289
|
+
}
|
|
2290
|
+
this.hash22Entries.unshift({ hash: n22, call: cw });
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
/** Look up a callsign by its 10-bit hash. Returns `null` if not found. */
|
|
2294
|
+
lookup10(n10) {
|
|
2295
|
+
if (n10 < 0 || n10 > 1023)
|
|
2296
|
+
return null;
|
|
2297
|
+
return this.calls10.get(n10) ?? null;
|
|
2298
|
+
}
|
|
2299
|
+
/** Look up a callsign by its 12-bit hash. Returns `null` if not found. */
|
|
2300
|
+
lookup12(n12) {
|
|
2301
|
+
if (n12 < 0 || n12 > 4095)
|
|
2302
|
+
return null;
|
|
2303
|
+
return this.calls12.get(n12) ?? null;
|
|
2304
|
+
}
|
|
2305
|
+
/** Look up a callsign by its 22-bit hash. Returns `null` if not found. */
|
|
2306
|
+
lookup22(n22) {
|
|
2307
|
+
const entry = this.hash22Entries.find((e) => e.hash === n22);
|
|
2308
|
+
return entry?.call ?? null;
|
|
2309
|
+
}
|
|
2310
|
+
/** Number of entries in the 22-bit hash table. */
|
|
2311
|
+
get size() {
|
|
2312
|
+
return this.hash22Entries.length;
|
|
2313
|
+
}
|
|
2314
|
+
/** Remove all stored entries. */
|
|
2315
|
+
clear() {
|
|
2316
|
+
this.calls10.clear();
|
|
2317
|
+
this.calls12.clear();
|
|
2318
|
+
this.hash22Entries.length = 0;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
export { HashCallBook, decode as decodeFT8, encode as encodeFT8 };
|
|
2116
2323
|
//# sourceMappingURL=ft8ts.mjs.map
|