@e04/ft8ts 0.0.1 → 0.0.2
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 +58 -7
- package/dist/ft8ts.cjs +134 -45
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.mjs +134 -45
- package/dist/ft8ts.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ft8/decode.ts +87 -44
- package/src/util/fft.ts +56 -0
- 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) {
|
|
@@ -878,12 +929,20 @@ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
|
|
|
878
929
|
else {
|
|
879
930
|
dd = resample(samples, sampleRate, SAMPLE_RATE, NMAX);
|
|
880
931
|
}
|
|
932
|
+
// Compute huge FFT for downsampling caching
|
|
933
|
+
const NFFT1_LONG = 192000;
|
|
934
|
+
const cxRe = new Float64Array(NFFT1_LONG);
|
|
935
|
+
const cxIm = new Float64Array(NFFT1_LONG);
|
|
936
|
+
for (let i = 0; i < NMAX; i++) {
|
|
937
|
+
cxRe[i] = dd[i] ?? 0;
|
|
938
|
+
}
|
|
939
|
+
fftComplex(cxRe, cxIm, false);
|
|
881
940
|
// Compute spectrogram and find sync candidates
|
|
882
941
|
const { candidates, sbase } = sync8(dd, nfa, nfb, syncmin, maxCandidates);
|
|
883
942
|
const decoded = [];
|
|
884
943
|
const seenMessages = new Set();
|
|
885
944
|
for (const cand of candidates) {
|
|
886
|
-
const result = ft8b(dd, cand.freq, cand.dt, sbase, depth);
|
|
945
|
+
const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth);
|
|
887
946
|
if (!result)
|
|
888
947
|
continue;
|
|
889
948
|
if (seenMessages.has(result.msg))
|
|
@@ -919,7 +978,7 @@ function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
|
919
978
|
for (let i = 0; i < NSPS && ia + i < dd.length; i++) {
|
|
920
979
|
xRe[i] = fac * dd[ia + i];
|
|
921
980
|
}
|
|
922
|
-
fftComplex(xRe, xIm);
|
|
981
|
+
fftComplex(xRe, xIm, false);
|
|
923
982
|
for (let i = 0; i < halfSize; i++) {
|
|
924
983
|
const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
|
|
925
984
|
s[i * NHSYM + j] = power;
|
|
@@ -1067,17 +1126,16 @@ function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
|
1067
1126
|
}
|
|
1068
1127
|
return sbase;
|
|
1069
1128
|
}
|
|
1070
|
-
function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
1129
|
+
function ft8b(dd0, cxRe, cxIm, f1, xdt, _sbase, depth) {
|
|
1071
1130
|
const NFFT2 = 3200;
|
|
1072
1131
|
const NP2 = 2812;
|
|
1073
|
-
const NFFT1_LONG = 192000;
|
|
1074
1132
|
const fs2 = SAMPLE_RATE / NDOWN;
|
|
1075
1133
|
const dt2 = 1.0 / fs2;
|
|
1076
1134
|
const twopi = 2 * Math.PI;
|
|
1077
1135
|
// Downsample: mix to baseband and filter
|
|
1078
1136
|
const cd0Re = new Float64Array(NFFT2);
|
|
1079
1137
|
const cd0Im = new Float64Array(NFFT2);
|
|
1080
|
-
ft8Downsample(
|
|
1138
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
1081
1139
|
// Find best time offset
|
|
1082
1140
|
const i0 = Math.round((xdt + 0.5) * fs2);
|
|
1083
1141
|
let smax = 0;
|
|
@@ -1111,7 +1169,7 @@ function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
|
1111
1169
|
}
|
|
1112
1170
|
// Apply frequency correction and re-downsample
|
|
1113
1171
|
f1 += delfbest;
|
|
1114
|
-
ft8Downsample(
|
|
1172
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
1115
1173
|
// Refine time offset
|
|
1116
1174
|
const ss = new Float64Array(9);
|
|
1117
1175
|
for (let idt = -4; idt <= 4; idt++) {
|
|
@@ -1143,7 +1201,7 @@ function ft8b(dd0, f1, xdt, _sbase, depth) {
|
|
|
1143
1201
|
symbIm[j] = cd0Im[i1 + j];
|
|
1144
1202
|
}
|
|
1145
1203
|
}
|
|
1146
|
-
fftComplex(symbRe, symbIm);
|
|
1204
|
+
fftComplex(symbRe, symbIm, false);
|
|
1147
1205
|
for (let tone = 0; tone < 8; tone++) {
|
|
1148
1206
|
const re = symbRe[tone] / 1000;
|
|
1149
1207
|
const im = symbIm[tone] / 1000;
|
|
@@ -1317,44 +1375,75 @@ function getTones$1(cw) {
|
|
|
1317
1375
|
return tones;
|
|
1318
1376
|
}
|
|
1319
1377
|
/**
|
|
1320
|
-
* Mix f0 to baseband and decimate by NDOWN (60x).
|
|
1321
|
-
*
|
|
1322
|
-
* Output: complex baseband signal at 200 Hz sample rate (32 samples/symbol).
|
|
1378
|
+
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
1379
|
+
* Identical to Fortran ft8_downsample.
|
|
1323
1380
|
*/
|
|
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
|
-
|
|
1381
|
+
function ft8Downsample(cxRe, cxIm, f0, c1Re, c1Im) {
|
|
1382
|
+
const NFFT1 = 192000;
|
|
1383
|
+
const NFFT2 = 3200;
|
|
1384
|
+
const df = 12000.0 / NFFT1;
|
|
1385
|
+
// NSPS is imported, should be 1920
|
|
1386
|
+
const baud = 12000.0 / NSPS; // 6.25
|
|
1387
|
+
const i0 = Math.round(f0 / df);
|
|
1388
|
+
const ft = f0 + 8.5 * baud;
|
|
1389
|
+
const it = Math.min(Math.round(ft / df), NFFT1 / 2);
|
|
1390
|
+
const fb = f0 - 1.5 * baud;
|
|
1391
|
+
const ib = Math.max(1, Math.round(fb / df));
|
|
1392
|
+
c1Re.fill(0);
|
|
1393
|
+
c1Im.fill(0);
|
|
1394
|
+
let k = 0;
|
|
1395
|
+
for (let i = ib; i <= it; i++) {
|
|
1396
|
+
if (k >= NFFT2)
|
|
1397
|
+
break;
|
|
1398
|
+
c1Re[k] = cxRe[i] ?? 0;
|
|
1399
|
+
c1Im[k] = cxIm[i] ?? 0;
|
|
1400
|
+
k++;
|
|
1401
|
+
}
|
|
1402
|
+
// Taper
|
|
1403
|
+
const pi = Math.PI;
|
|
1404
|
+
const taper = new Float64Array(101);
|
|
1405
|
+
for (let i = 0; i <= 100; i++) {
|
|
1406
|
+
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
1407
|
+
}
|
|
1408
|
+
for (let i = 0; i <= 100; i++) {
|
|
1409
|
+
if (i >= NFFT2)
|
|
1410
|
+
break;
|
|
1411
|
+
const tap = taper[100 - i];
|
|
1412
|
+
c1Re[i] = c1Re[i] * tap;
|
|
1413
|
+
c1Im[i] = c1Im[i] * tap;
|
|
1414
|
+
}
|
|
1415
|
+
const endTap = k - 1;
|
|
1416
|
+
for (let i = 0; i <= 100; i++) {
|
|
1417
|
+
const idx = endTap - 100 + i;
|
|
1418
|
+
if (idx >= 0 && idx < NFFT2) {
|
|
1419
|
+
const tap = taper[i];
|
|
1420
|
+
c1Re[idx] = c1Re[idx] * tap;
|
|
1421
|
+
c1Im[idx] = c1Im[idx] * tap;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
// CSHIFT
|
|
1425
|
+
const shift = i0 - ib;
|
|
1426
|
+
const tempRe = new Float64Array(NFFT2);
|
|
1427
|
+
const tempIm = new Float64Array(NFFT2);
|
|
1428
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1429
|
+
let srcIdx = (i + shift) % NFFT2;
|
|
1430
|
+
if (srcIdx < 0)
|
|
1431
|
+
srcIdx += NFFT2;
|
|
1432
|
+
tempRe[i] = c1Re[srcIdx];
|
|
1433
|
+
tempIm[i] = c1Im[srcIdx];
|
|
1434
|
+
}
|
|
1435
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1436
|
+
c1Re[i] = tempRe[i];
|
|
1437
|
+
c1Im[i] = tempIm[i];
|
|
1438
|
+
}
|
|
1439
|
+
// iFFT
|
|
1440
|
+
fftComplex(c1Re, c1Im, true);
|
|
1441
|
+
// Scale
|
|
1442
|
+
// Fortran uses 1.0/sqrt(NFFT1 * NFFT2), but our fftComplex(true) scales by 1/NFFT2
|
|
1443
|
+
const scale = Math.sqrt(NFFT2 / NFFT1);
|
|
1444
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1445
|
+
c1Re[i] = c1Re[i] * scale;
|
|
1446
|
+
c1Im[i] = c1Im[i] * scale;
|
|
1358
1447
|
}
|
|
1359
1448
|
}
|
|
1360
1449
|
function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
|