@e04/ft8ts 0.0.8 → 0.0.9
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 +29 -11
- package/dist/cli.js +1451 -603
- package/dist/cli.js.map +1 -1
- package/dist/ft8ts.cjs +1464 -567
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +43 -9
- package/dist/ft8ts.mjs +1462 -567
- package/dist/ft8ts.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +15 -2
- package/src/ft4/constants.ts +41 -0
- package/src/ft4/decode.ts +1018 -0
- package/src/ft4/encode.ts +40 -0
- package/src/ft4/scramble.ts +9 -0
- package/src/ft8/constants.ts +18 -0
- package/src/ft8/decode.ts +19 -30
- package/src/ft8/encode.ts +6 -5
- package/src/index.ts +6 -0
- package/src/util/constants.ts +4 -13
- package/src/util/waveform.ts +92 -20
package/dist/cli.js
CHANGED
|
@@ -2,19 +2,12 @@
|
|
|
2
2
|
import { writeFileSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
|
|
5
|
+
/** Shared constants used by FT8, FT4, pack77, etc. */
|
|
5
6
|
const SAMPLE_RATE$1 = 12_000;
|
|
6
|
-
|
|
7
|
-
const NFFT1 = 2 * NSPS; // 3840
|
|
8
|
-
const NSTEP = NSPS / 4; // 480
|
|
9
|
-
const NMAX = 15 * SAMPLE_RATE$1; // 180000
|
|
10
|
-
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
11
|
-
const NDOWN = 60;
|
|
12
|
-
const NN = 79;
|
|
7
|
+
/** LDPC(174,91) code (shared by FT8 and FT4). */
|
|
13
8
|
const KK = 91;
|
|
14
9
|
const N_LDPC = 174;
|
|
15
10
|
const M_LDPC = N_LDPC - KK; // 83
|
|
16
|
-
const icos7 = [3, 1, 4, 0, 6, 5, 2];
|
|
17
|
-
const graymap = [0, 1, 3, 2, 5, 6, 4, 7];
|
|
18
11
|
const gHex = [
|
|
19
12
|
"8329ce11bf31eaf509f27fc",
|
|
20
13
|
"761c264e25c259335493132",
|
|
@@ -923,607 +916,812 @@ function unpack77(bits77, book) {
|
|
|
923
916
|
return { msg: "", success: false };
|
|
924
917
|
}
|
|
925
918
|
|
|
919
|
+
/** FT4-specific constants (lib/ft4/ft4_params.f90). */
|
|
920
|
+
const COSTAS_A = [0, 1, 3, 2];
|
|
921
|
+
const COSTAS_B = [1, 0, 2, 3];
|
|
922
|
+
const COSTAS_C = [2, 3, 1, 0];
|
|
923
|
+
const COSTAS_D = [3, 2, 0, 1];
|
|
924
|
+
const GRAYMAP = [0, 1, 3, 2];
|
|
925
|
+
// Message scrambling vector (rvec) from WSJT-X.
|
|
926
|
+
const RVEC = [
|
|
927
|
+
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,
|
|
928
|
+
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,
|
|
929
|
+
1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1,
|
|
930
|
+
];
|
|
931
|
+
const NSPS$1 = 576;
|
|
932
|
+
const NFFT1$1 = 4 * NSPS$1; // 2304
|
|
933
|
+
const NH1 = NFFT1$1 / 2; // 1152
|
|
934
|
+
const NSTEP$1 = NSPS$1;
|
|
935
|
+
const NMAX$1 = 21 * 3456; // 72576
|
|
936
|
+
const NHSYM$1 = Math.floor((NMAX$1 - NFFT1$1) / NSTEP$1); // 122
|
|
937
|
+
const NDOWN$1 = 18;
|
|
938
|
+
const ND = 87;
|
|
939
|
+
const NS = 16;
|
|
940
|
+
const NN$1 = NS + ND; // 103
|
|
941
|
+
const NFFT2 = NMAX$1 / NDOWN$1; // 4032
|
|
942
|
+
const NSS = NSPS$1 / NDOWN$1; // 32
|
|
943
|
+
const FS2 = SAMPLE_RATE$1 / NDOWN$1; // 666.67 Hz
|
|
944
|
+
const MAX_FREQ = 4910;
|
|
945
|
+
const SYNC_PASS_MIN = 1.2;
|
|
946
|
+
const TWO_PI$1 = 2 * Math.PI;
|
|
947
|
+
const HARD_SYNC_PATTERNS = [
|
|
948
|
+
{ offset: 0, bits: [0, 0, 0, 1, 1, 0, 1, 1] },
|
|
949
|
+
{ offset: 66, bits: [0, 1, 0, 0, 1, 1, 1, 0] },
|
|
950
|
+
{ offset: 132, bits: [1, 1, 1, 0, 0, 1, 0, 0] },
|
|
951
|
+
{ offset: 198, bits: [1, 0, 1, 1, 0, 0, 0, 1] },
|
|
952
|
+
];
|
|
953
|
+
|
|
954
|
+
function xorWithScrambler(bits77) {
|
|
955
|
+
const out = new Array(77);
|
|
956
|
+
for (let i = 0; i < 77; i++) {
|
|
957
|
+
out[i] = ((bits77[i] ?? 0) + RVEC[i]) & 1;
|
|
958
|
+
}
|
|
959
|
+
return out;
|
|
960
|
+
}
|
|
961
|
+
|
|
926
962
|
/**
|
|
927
|
-
* Decode all
|
|
928
|
-
* Input: mono audio samples at `sampleRate` Hz, duration ~
|
|
963
|
+
* Decode all FT4 signals in a buffer.
|
|
964
|
+
* Input: mono audio samples at `sampleRate` Hz, duration ~6s.
|
|
929
965
|
*/
|
|
930
|
-
function decode(samples, options = {}) {
|
|
966
|
+
function decode$1(samples, options = {}) {
|
|
931
967
|
const sampleRate = options.sampleRate ?? SAMPLE_RATE$1;
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
const
|
|
968
|
+
const freqLow = options.freqLow ?? 200;
|
|
969
|
+
const freqHigh = options.freqHigh ?? 3000;
|
|
970
|
+
const syncMin = options.syncMin ?? 1.2;
|
|
935
971
|
const depth = options.depth ?? 2;
|
|
936
|
-
const maxCandidates = options.maxCandidates ??
|
|
972
|
+
const maxCandidates = options.maxCandidates ?? 100;
|
|
937
973
|
const book = options.hashCallBook;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
dd[i] = samples[i];
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
dd = resample(samples, sampleRate, SAMPLE_RATE$1, NMAX);
|
|
948
|
-
}
|
|
949
|
-
// Compute huge FFT for downsampling caching
|
|
950
|
-
const NFFT1_LONG = 192000;
|
|
951
|
-
const cxRe = new Float64Array(NFFT1_LONG);
|
|
952
|
-
const cxIm = new Float64Array(NFFT1_LONG);
|
|
953
|
-
for (let i = 0; i < NMAX; i++) {
|
|
974
|
+
const dd = sampleRate === SAMPLE_RATE$1
|
|
975
|
+
? copyIntoFt4Buffer(samples)
|
|
976
|
+
: resample$1(samples, sampleRate, SAMPLE_RATE$1, NMAX$1);
|
|
977
|
+
const cxRe = new Float64Array(NMAX$1);
|
|
978
|
+
const cxIm = new Float64Array(NMAX$1);
|
|
979
|
+
for (let i = 0; i < NMAX$1; i++) {
|
|
954
980
|
cxRe[i] = dd[i] ?? 0;
|
|
955
981
|
}
|
|
956
982
|
fftComplex(cxRe, cxIm, false);
|
|
957
|
-
|
|
958
|
-
|
|
983
|
+
const candidates = getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates);
|
|
984
|
+
if (candidates.length === 0) {
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
const downsampleCtx = createDownsampleContext();
|
|
988
|
+
const tweakedSyncTemplates = createTweakedSyncTemplates();
|
|
959
989
|
const decoded = [];
|
|
960
990
|
const seenMessages = new Set();
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
991
|
+
const apmask = new Int8Array(174);
|
|
992
|
+
for (const candidate of candidates) {
|
|
993
|
+
const one = decodeCandidate(candidate, cxRe, cxIm, downsampleCtx, tweakedSyncTemplates, depth, book, apmask);
|
|
994
|
+
if (!one) {
|
|
964
995
|
continue;
|
|
965
|
-
|
|
996
|
+
}
|
|
997
|
+
if (seenMessages.has(one.msg)) {
|
|
966
998
|
continue;
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
dt: result.dt - 0.5,
|
|
971
|
-
snr: result.snr,
|
|
972
|
-
msg: result.msg,
|
|
973
|
-
sync: cand.sync,
|
|
974
|
-
});
|
|
999
|
+
}
|
|
1000
|
+
seenMessages.add(one.msg);
|
|
1001
|
+
decoded.push(one);
|
|
975
1002
|
}
|
|
976
1003
|
return decoded;
|
|
977
1004
|
}
|
|
978
|
-
function
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
s[i * NHSYM + j] = power;
|
|
1002
|
-
savg[i] = (savg[i] ?? 0) + power;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// Compute baseline
|
|
1006
|
-
const sbase = computeBaseline(savg, nfa, nfb, df, halfSize);
|
|
1007
|
-
const ia = Math.max(1, Math.round(nfa / df));
|
|
1008
|
-
const ib = Math.min(halfSize - 14, Math.round(nfb / df));
|
|
1009
|
-
const nssy = Math.floor(NSPS / NSTEP);
|
|
1010
|
-
const nfos = Math.round(SAMPLE_RATE$1 / NSPS / df); // ~2 bins per tone spacing
|
|
1011
|
-
const jstrt = Math.round(0.5 / tstep);
|
|
1012
|
-
// 2D sync correlation
|
|
1013
|
-
const sync2d = new Float64Array((ib - ia + 1) * (2 * JZ + 1));
|
|
1014
|
-
const width = 2 * JZ + 1;
|
|
1015
|
-
for (let i = ia; i <= ib; i++) {
|
|
1016
|
-
for (let jj = -JZ; jj <= JZ; jj++) {
|
|
1017
|
-
let ta = 0, tb = 0, tc = 0;
|
|
1018
|
-
let t0a = 0, t0b = 0, t0c = 0;
|
|
1019
|
-
for (let n = 0; n < 7; n++) {
|
|
1020
|
-
const m = jj + jstrt + nssy * n;
|
|
1021
|
-
const iCostas = i + nfos * icos7[n];
|
|
1022
|
-
if (m >= 0 && m < NHSYM && iCostas < halfSize) {
|
|
1023
|
-
ta += s[iCostas * NHSYM + m];
|
|
1024
|
-
for (let tone = 0; tone <= 6; tone++) {
|
|
1025
|
-
const idx = i + nfos * tone;
|
|
1026
|
-
if (idx < halfSize)
|
|
1027
|
-
t0a += s[idx * NHSYM + m];
|
|
1028
|
-
}
|
|
1005
|
+
function decodeCandidate(candidate, cxRe, cxIm, downsampleCtx, tweakedSyncTemplates, depth, book, apmask) {
|
|
1006
|
+
const cd2 = ft4Downsample(cxRe, cxIm, candidate.freq, downsampleCtx);
|
|
1007
|
+
normalizeComplexPower(cd2.re, cd2.im, NMAX$1 / NDOWN$1);
|
|
1008
|
+
for (let segment = 1; segment <= 3; segment++) {
|
|
1009
|
+
let ibest = -1;
|
|
1010
|
+
let idfbest = 0;
|
|
1011
|
+
let smax = -99;
|
|
1012
|
+
for (let isync = 1; isync <= 2; isync++) {
|
|
1013
|
+
let idfmin;
|
|
1014
|
+
let idfmax;
|
|
1015
|
+
let idfstp;
|
|
1016
|
+
let ibmin;
|
|
1017
|
+
let ibmax;
|
|
1018
|
+
let ibstp;
|
|
1019
|
+
if (isync === 1) {
|
|
1020
|
+
idfmin = -12;
|
|
1021
|
+
idfmax = 12;
|
|
1022
|
+
idfstp = 3;
|
|
1023
|
+
ibmin = -344;
|
|
1024
|
+
ibmax = 1012;
|
|
1025
|
+
if (segment === 1) {
|
|
1026
|
+
ibmin = 108;
|
|
1027
|
+
ibmax = 560;
|
|
1029
1028
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
for (let tone = 0; tone <= 6; tone++) {
|
|
1034
|
-
const idx = i + nfos * tone;
|
|
1035
|
-
if (idx < halfSize)
|
|
1036
|
-
t0b += s[idx * NHSYM + m36];
|
|
1037
|
-
}
|
|
1029
|
+
else if (segment === 2) {
|
|
1030
|
+
ibmin = 560;
|
|
1031
|
+
ibmax = 1012;
|
|
1038
1032
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1033
|
+
else {
|
|
1034
|
+
ibmin = -344;
|
|
1035
|
+
ibmax = 108;
|
|
1036
|
+
}
|
|
1037
|
+
ibstp = 4;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
idfmin = idfbest - 4;
|
|
1041
|
+
idfmax = idfbest + 4;
|
|
1042
|
+
idfstp = 1;
|
|
1043
|
+
ibmin = ibest - 5;
|
|
1044
|
+
ibmax = ibest + 5;
|
|
1045
|
+
ibstp = 1;
|
|
1046
|
+
}
|
|
1047
|
+
for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
|
|
1048
|
+
const templates = tweakedSyncTemplates.get(idf);
|
|
1049
|
+
if (!templates) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
|
|
1053
|
+
const sync = sync4d(cd2.re, cd2.im, istart, templates);
|
|
1054
|
+
if (sync > smax) {
|
|
1055
|
+
smax = sync;
|
|
1056
|
+
ibest = istart;
|
|
1057
|
+
idfbest = idf;
|
|
1046
1058
|
}
|
|
1047
1059
|
}
|
|
1048
1060
|
}
|
|
1049
|
-
const t = ta + tb + tc;
|
|
1050
|
-
const t0total = t0a + t0b + t0c;
|
|
1051
|
-
const t0 = (t0total - t) / 6.0;
|
|
1052
|
-
const syncVal = t0 > 0 ? t / t0 : 0;
|
|
1053
|
-
const tbc = tb + tc;
|
|
1054
|
-
const t0bc = t0b + t0c;
|
|
1055
|
-
const t0bc2 = (t0bc - tbc) / 6.0;
|
|
1056
|
-
const syncBc = t0bc2 > 0 ? tbc / t0bc2 : 0;
|
|
1057
|
-
sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
|
|
1058
1061
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const candidates0 = [];
|
|
1062
|
-
const mlag = 10;
|
|
1063
|
-
for (let i = ia; i <= ib; i++) {
|
|
1064
|
-
let bestSync = -1;
|
|
1065
|
-
let bestJ = 0;
|
|
1066
|
-
for (let j = -mlag; j <= mlag; j++) {
|
|
1067
|
-
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
1068
|
-
if (v > bestSync) {
|
|
1069
|
-
bestSync = v;
|
|
1070
|
-
bestJ = j;
|
|
1071
|
-
}
|
|
1062
|
+
if (smax < SYNC_PASS_MIN) {
|
|
1063
|
+
continue;
|
|
1072
1064
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1065
|
+
const f1 = candidate.freq + idfbest;
|
|
1066
|
+
if (f1 <= 10 || f1 >= 4990) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const cb = ft4Downsample(cxRe, cxIm, f1, downsampleCtx);
|
|
1070
|
+
normalizeComplexPower(cb.re, cb.im, NSS * NN$1);
|
|
1071
|
+
const frame = extractFrame(cb.re, cb.im, ibest);
|
|
1072
|
+
const metrics = getFt4Bitmetrics(frame.re, frame.im);
|
|
1073
|
+
if (metrics.badsync) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (!passesHardSyncQuality(metrics.bitmetrics1)) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const [llra, llrb, llrc] = buildLlrs(metrics.bitmetrics1, metrics.bitmetrics2, metrics.bitmetrics3);
|
|
1080
|
+
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
1081
|
+
const scalefac = 2.83;
|
|
1082
|
+
for (const src of [llra, llrb, llrc]) {
|
|
1083
|
+
const llr = new Float64Array(174);
|
|
1084
|
+
for (let i = 0; i < 174; i++) {
|
|
1085
|
+
llr[i] = scalefac * src[i];
|
|
1086
|
+
}
|
|
1087
|
+
const result = decode174_91(llr, apmask, maxosd);
|
|
1088
|
+
if (!result) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
const message77Scrambled = result.message91.slice(0, 77);
|
|
1092
|
+
if (!hasNonZeroBit(message77Scrambled)) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const message77 = xorWithScrambler(message77Scrambled);
|
|
1096
|
+
const { msg, success } = unpack77(message77, book);
|
|
1097
|
+
if (!success || msg.trim().length === 0) {
|
|
1098
|
+
continue;
|
|
1081
1099
|
}
|
|
1100
|
+
return {
|
|
1101
|
+
freq: f1,
|
|
1102
|
+
dt: ibest / FS2 - 0.5,
|
|
1103
|
+
snr: toFt4Snr(candidate.sync - 1.0),
|
|
1104
|
+
msg,
|
|
1105
|
+
sync: smax,
|
|
1106
|
+
};
|
|
1082
1107
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1108
|
+
}
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
function copyIntoFt4Buffer(samples) {
|
|
1112
|
+
const out = new Float64Array(NMAX$1);
|
|
1113
|
+
const len = Math.min(samples.length, NMAX$1);
|
|
1114
|
+
for (let i = 0; i < len; i++) {
|
|
1115
|
+
out[i] = samples[i];
|
|
1116
|
+
}
|
|
1117
|
+
return out;
|
|
1118
|
+
}
|
|
1119
|
+
function getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates) {
|
|
1120
|
+
const df = SAMPLE_RATE$1 / NFFT1$1;
|
|
1121
|
+
const fac = 1 / 300;
|
|
1122
|
+
const window = makeNuttallWindow(NFFT1$1);
|
|
1123
|
+
const savg = new Float64Array(NH1);
|
|
1124
|
+
const s = new Float64Array(NH1 * NHSYM$1);
|
|
1125
|
+
const savsm = new Float64Array(NH1);
|
|
1126
|
+
const xRe = new Float64Array(NFFT1$1);
|
|
1127
|
+
const xIm = new Float64Array(NFFT1$1);
|
|
1128
|
+
for (let j = 0; j < NHSYM$1; j++) {
|
|
1129
|
+
const ia = j * NSPS$1;
|
|
1130
|
+
const ib = ia + NFFT1$1;
|
|
1131
|
+
if (ib > NMAX$1) {
|
|
1132
|
+
break;
|
|
1089
1133
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1134
|
+
xIm.fill(0);
|
|
1135
|
+
for (let i = 0; i < NFFT1$1; i++) {
|
|
1136
|
+
xRe[i] = fac * dd[ia + i] * window[i];
|
|
1137
|
+
}
|
|
1138
|
+
fftComplex(xRe, xIm, false);
|
|
1139
|
+
for (let bin = 1; bin <= NH1; bin++) {
|
|
1140
|
+
const idx = bin - 1;
|
|
1141
|
+
const re = xRe[bin] ?? 0;
|
|
1142
|
+
const im = xIm[bin] ?? 0;
|
|
1143
|
+
const power = re * re + im * im;
|
|
1144
|
+
s[idx * NHSYM$1 + j] = power;
|
|
1145
|
+
savg[idx] = (savg[idx] ?? 0) + power;
|
|
1096
1146
|
}
|
|
1097
1147
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
syncValues.sort((a, b) => a - b);
|
|
1101
|
-
const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
|
|
1102
|
-
const base = syncValues[pctileIdx] ?? 1;
|
|
1103
|
-
if (base > 0) {
|
|
1104
|
-
for (const c of candidates0)
|
|
1105
|
-
c.sync /= base;
|
|
1148
|
+
for (let i = 0; i < NH1; i++) {
|
|
1149
|
+
savg[i] = (savg[i] ?? 0) / NHSYM$1;
|
|
1106
1150
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
for (let j =
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1151
|
+
for (let i = 7; i < NH1 - 7; i++) {
|
|
1152
|
+
let sum = 0;
|
|
1153
|
+
for (let j = i - 7; j <= i + 7; j++) {
|
|
1154
|
+
sum += savg[j];
|
|
1155
|
+
}
|
|
1156
|
+
savsm[i] = sum / 15;
|
|
1157
|
+
}
|
|
1158
|
+
let nfa = Math.round(freqLow / df);
|
|
1159
|
+
if (nfa < Math.round(200 / df)) {
|
|
1160
|
+
nfa = Math.round(200 / df);
|
|
1161
|
+
}
|
|
1162
|
+
let nfb = Math.round(freqHigh / df);
|
|
1163
|
+
if (nfb > Math.round(MAX_FREQ / df)) {
|
|
1164
|
+
nfb = Math.round(MAX_FREQ / df);
|
|
1165
|
+
}
|
|
1166
|
+
const sbase = ft4Baseline(savg, nfa, nfb, df);
|
|
1167
|
+
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1168
|
+
if ((sbase[bin - 1] ?? 0) <= 0) {
|
|
1169
|
+
return [];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
for (let bin = nfa; bin <= nfb; bin++) {
|
|
1173
|
+
const idx = bin - 1;
|
|
1174
|
+
savsm[idx] = (savsm[idx] ?? 0) / sbase[idx];
|
|
1175
|
+
}
|
|
1176
|
+
const fOffset = (-1.5 * SAMPLE_RATE$1) / NSPS$1;
|
|
1177
|
+
const candidates = [];
|
|
1178
|
+
for (let i = nfa + 1; i <= nfb - 1; i++) {
|
|
1179
|
+
const left = savsm[i - 2] ?? 0;
|
|
1180
|
+
const center = savsm[i - 1] ?? 0;
|
|
1181
|
+
const right = savsm[i] ?? 0;
|
|
1182
|
+
if (center >= left && center >= right && center >= syncMin) {
|
|
1183
|
+
const den = left - 2 * center + right;
|
|
1184
|
+
const del = den !== 0 ? (0.5 * (left - right)) / den : 0;
|
|
1185
|
+
const fpeak = (i + del) * df + fOffset;
|
|
1186
|
+
if (fpeak < 200 || fpeak > MAX_FREQ) {
|
|
1187
|
+
continue;
|
|
1119
1188
|
}
|
|
1189
|
+
const speak = center - 0.25 * (left - right) * del;
|
|
1190
|
+
candidates.push({ freq: fpeak, sync: speak });
|
|
1120
1191
|
}
|
|
1121
1192
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
filtered.sort((a, b) => b.sync - a.sync);
|
|
1125
|
-
return { candidates: filtered.slice(0, maxcand), sbase };
|
|
1193
|
+
candidates.sort((a, b) => b.sync - a.sync);
|
|
1194
|
+
return candidates.slice(0, maxCandidates);
|
|
1126
1195
|
}
|
|
1127
|
-
function
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
for (let i = 0; i <
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1196
|
+
function makeNuttallWindow(n) {
|
|
1197
|
+
const out = new Float64Array(n);
|
|
1198
|
+
const a0 = 0.3635819;
|
|
1199
|
+
const a1 = -0.4891775;
|
|
1200
|
+
const a2 = 0.1365995;
|
|
1201
|
+
const a3 = -0.0106411;
|
|
1202
|
+
for (let i = 0; i < n; i++) {
|
|
1203
|
+
out[i] =
|
|
1204
|
+
a0 +
|
|
1205
|
+
a1 * Math.cos((2 * Math.PI * i) / n) +
|
|
1206
|
+
a2 * Math.cos((4 * Math.PI * i) / n) +
|
|
1207
|
+
a3 * Math.cos((6 * Math.PI * i) / n);
|
|
1208
|
+
}
|
|
1209
|
+
return out;
|
|
1210
|
+
}
|
|
1211
|
+
function ft4Baseline(savg, nfa, nfb, df) {
|
|
1212
|
+
const sbase = new Float64Array(NH1);
|
|
1213
|
+
sbase.fill(1);
|
|
1214
|
+
const ia = Math.max(Math.round(200 / df), nfa);
|
|
1215
|
+
const ib = Math.min(NH1, nfb);
|
|
1216
|
+
if (ib <= ia) {
|
|
1217
|
+
return sbase;
|
|
1218
|
+
}
|
|
1219
|
+
const sDb = new Float64Array(NH1);
|
|
1220
|
+
for (let i = ia; i <= ib; i++) {
|
|
1221
|
+
sDb[i - 1] = 10 * Math.log10(Math.max(1e-30, savg[i - 1]));
|
|
1222
|
+
}
|
|
1223
|
+
const nseg = 10;
|
|
1224
|
+
const npct = 10;
|
|
1225
|
+
const nlen = Math.max(1, Math.trunc((ib - ia + 1) / nseg));
|
|
1226
|
+
const i0 = Math.trunc((ib - ia + 1) / 2);
|
|
1227
|
+
const x = [];
|
|
1228
|
+
const y = [];
|
|
1229
|
+
for (let seg = 0; seg < nseg; seg++) {
|
|
1230
|
+
const ja = ia + seg * nlen;
|
|
1231
|
+
if (ja > ib) {
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
const jb = Math.min(ib, ja + nlen - 1);
|
|
1235
|
+
const vals = [];
|
|
1236
|
+
for (let i = ja; i <= jb; i++) {
|
|
1237
|
+
vals.push(sDb[i - 1]);
|
|
1238
|
+
}
|
|
1239
|
+
const base = percentile(vals, npct);
|
|
1240
|
+
for (let i = ja; i <= jb; i++) {
|
|
1241
|
+
const v = sDb[i - 1];
|
|
1242
|
+
if (v <= base) {
|
|
1243
|
+
x.push(i - i0);
|
|
1244
|
+
y.push(v);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const coeff = x.length >= 5 ? polyfitLeastSquares(x, y, 4) : null;
|
|
1249
|
+
if (coeff) {
|
|
1250
|
+
for (let i = ia; i <= ib; i++) {
|
|
1251
|
+
const t = i - i0;
|
|
1252
|
+
const db = coeff[0] + t * (coeff[1] + t * (coeff[2] + t * (coeff[3] + t * coeff[4]))) + 0.65;
|
|
1253
|
+
sbase[i - 1] = 10 ** (db / 10);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
const halfWindow = 25;
|
|
1258
|
+
for (let i = ia; i <= ib; i++) {
|
|
1259
|
+
const lo = Math.max(ia, i - halfWindow);
|
|
1260
|
+
const hi = Math.min(ib, i + halfWindow);
|
|
1261
|
+
let sum = 0;
|
|
1262
|
+
let count = 0;
|
|
1263
|
+
for (let j = lo; j <= hi; j++) {
|
|
1264
|
+
sum += savg[j - 1];
|
|
1265
|
+
count++;
|
|
1266
|
+
}
|
|
1267
|
+
sbase[i - 1] = count > 0 ? sum / count : 1;
|
|
1141
1268
|
}
|
|
1142
|
-
sbase[i] = count > 0 ? 10 * Math.log10(Math.max(1e-30, sum / count)) : 0;
|
|
1143
1269
|
}
|
|
1144
1270
|
return sbase;
|
|
1145
1271
|
}
|
|
1146
|
-
function
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
let
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (sync > smax) {
|
|
1163
|
-
smax = sync;
|
|
1164
|
-
ibest = idt;
|
|
1272
|
+
function percentile(values, pct) {
|
|
1273
|
+
if (values.length === 0) {
|
|
1274
|
+
return 0;
|
|
1275
|
+
}
|
|
1276
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1277
|
+
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((pct / 100) * (sorted.length - 1))));
|
|
1278
|
+
return sorted[idx];
|
|
1279
|
+
}
|
|
1280
|
+
function polyfitLeastSquares(x, y, degree) {
|
|
1281
|
+
const n = degree + 1;
|
|
1282
|
+
const mat = Array.from({ length: n }, () => new Float64Array(n + 1));
|
|
1283
|
+
const xPows = new Float64Array(2 * degree + 1);
|
|
1284
|
+
for (let p = 0; p <= 2 * degree; p++) {
|
|
1285
|
+
let sum = 0;
|
|
1286
|
+
for (let i = 0; i < x.length; i++) {
|
|
1287
|
+
sum += x[i] ** p;
|
|
1165
1288
|
}
|
|
1289
|
+
xPows[p] = sum;
|
|
1166
1290
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
for (let ifr = -5; ifr <= 5; ifr++) {
|
|
1171
|
-
const delf = ifr * 0.5;
|
|
1172
|
-
const dphi = twopi * delf * dt2;
|
|
1173
|
-
const twkRe = new Float64Array(32);
|
|
1174
|
-
const twkIm = new Float64Array(32);
|
|
1175
|
-
let phi = 0;
|
|
1176
|
-
for (let i = 0; i < 32; i++) {
|
|
1177
|
-
twkRe[i] = Math.cos(phi);
|
|
1178
|
-
twkIm[i] = Math.sin(phi);
|
|
1179
|
-
phi = (phi + dphi) % twopi;
|
|
1291
|
+
for (let row = 0; row < n; row++) {
|
|
1292
|
+
for (let col = 0; col < n; col++) {
|
|
1293
|
+
mat[row][col] = xPows[row + col];
|
|
1180
1294
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
delfbest = delf;
|
|
1295
|
+
let rhs = 0;
|
|
1296
|
+
for (let i = 0; i < x.length; i++) {
|
|
1297
|
+
rhs += y[i] * x[i] ** row;
|
|
1185
1298
|
}
|
|
1299
|
+
mat[row][n] = rhs;
|
|
1186
1300
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1301
|
+
for (let col = 0; col < n; col++) {
|
|
1302
|
+
let pivot = col;
|
|
1303
|
+
let maxAbs = Math.abs(mat[col][col]);
|
|
1304
|
+
for (let row = col + 1; row < n; row++) {
|
|
1305
|
+
const a = Math.abs(mat[row][col]);
|
|
1306
|
+
if (a > maxAbs) {
|
|
1307
|
+
maxAbs = a;
|
|
1308
|
+
pivot = row;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (maxAbs < 1e-12) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
if (pivot !== col) {
|
|
1315
|
+
const tmp = mat[col];
|
|
1316
|
+
mat[col] = mat[pivot];
|
|
1317
|
+
mat[pivot] = tmp;
|
|
1318
|
+
}
|
|
1319
|
+
const pivotVal = mat[col][col];
|
|
1320
|
+
for (let c = col; c <= n; c++) {
|
|
1321
|
+
mat[col][c] = mat[col][c] / pivotVal;
|
|
1322
|
+
}
|
|
1323
|
+
for (let row = 0; row < n; row++) {
|
|
1324
|
+
if (row === col) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const factor = mat[row][col];
|
|
1328
|
+
if (factor === 0) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
for (let c = col; c <= n; c++) {
|
|
1332
|
+
mat[row][c] = mat[row][c] - factor * mat[col][c];
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1194
1335
|
}
|
|
1195
|
-
|
|
1196
|
-
let
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1336
|
+
const coeff = new Array(n);
|
|
1337
|
+
for (let i = 0; i < n; i++) {
|
|
1338
|
+
coeff[i] = mat[i][n];
|
|
1339
|
+
}
|
|
1340
|
+
return coeff;
|
|
1341
|
+
}
|
|
1342
|
+
function createDownsampleContext() {
|
|
1343
|
+
const df = SAMPLE_RATE$1 / NMAX$1;
|
|
1344
|
+
const baud = SAMPLE_RATE$1 / NSPS$1;
|
|
1345
|
+
const bwTransition = 0.5 * baud;
|
|
1346
|
+
const bwFlat = 4 * baud;
|
|
1347
|
+
const iwt = Math.max(1, Math.trunc(bwTransition / df));
|
|
1348
|
+
const iwf = Math.max(1, Math.trunc(bwFlat / df));
|
|
1349
|
+
const iws = Math.trunc(baud / df);
|
|
1350
|
+
const raw = new Float64Array(NFFT2);
|
|
1351
|
+
for (let i = 0; i < iwt && i < raw.length; i++) {
|
|
1352
|
+
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (iwt - 1 - i)) / iwt));
|
|
1353
|
+
}
|
|
1354
|
+
for (let i = iwt; i < iwt + iwf && i < raw.length; i++) {
|
|
1355
|
+
raw[i] = 1;
|
|
1356
|
+
}
|
|
1357
|
+
for (let i = iwt + iwf; i < 2 * iwt + iwf && i < raw.length; i++) {
|
|
1358
|
+
raw[i] = 0.5 * (1 + Math.cos((Math.PI * (i - (iwt + iwf))) / iwt));
|
|
1359
|
+
}
|
|
1360
|
+
const window = new Float64Array(NFFT2);
|
|
1361
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1362
|
+
const src = (i + iws) % NFFT2;
|
|
1363
|
+
window[i] = raw[src];
|
|
1364
|
+
}
|
|
1365
|
+
return { df, window };
|
|
1366
|
+
}
|
|
1367
|
+
function ft4Downsample(cxRe, cxIm, f0, ctx) {
|
|
1368
|
+
const c1Re = new Float64Array(NFFT2);
|
|
1369
|
+
const c1Im = new Float64Array(NFFT2);
|
|
1370
|
+
const i0 = Math.round(f0 / ctx.df);
|
|
1371
|
+
if (i0 >= 0 && i0 <= NMAX$1 / 2) {
|
|
1372
|
+
c1Re[0] = cxRe[i0] ?? 0;
|
|
1373
|
+
c1Im[0] = cxIm[i0] ?? 0;
|
|
1374
|
+
}
|
|
1375
|
+
for (let i = 1; i <= NFFT2 / 2; i++) {
|
|
1376
|
+
const hi = i0 + i;
|
|
1377
|
+
if (hi >= 0 && hi <= NMAX$1 / 2) {
|
|
1378
|
+
c1Re[i] = cxRe[hi] ?? 0;
|
|
1379
|
+
c1Im[i] = cxIm[hi] ?? 0;
|
|
1380
|
+
}
|
|
1381
|
+
const lo = i0 - i;
|
|
1382
|
+
if (lo >= 0 && lo <= NMAX$1 / 2) {
|
|
1383
|
+
const idx = NFFT2 - i;
|
|
1384
|
+
c1Re[idx] = cxRe[lo] ?? 0;
|
|
1385
|
+
c1Im[idx] = cxIm[lo] ?? 0;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const scale = 1 / NFFT2;
|
|
1389
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
1390
|
+
const w = (ctx.window[i] ?? 0) * scale;
|
|
1391
|
+
c1Re[i] = c1Re[i] * w;
|
|
1392
|
+
c1Im[i] = c1Im[i] * w;
|
|
1393
|
+
}
|
|
1394
|
+
fftComplex(c1Re, c1Im, true);
|
|
1395
|
+
return { re: c1Re, im: c1Im };
|
|
1396
|
+
}
|
|
1397
|
+
function normalizeComplexPower(re, im, denom) {
|
|
1398
|
+
let sum = 0;
|
|
1399
|
+
for (let i = 0; i < re.length; i++) {
|
|
1400
|
+
sum += re[i] * re[i] + im[i] * im[i];
|
|
1401
|
+
}
|
|
1402
|
+
if (sum <= 0) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const scale = 1 / Math.sqrt(sum / denom);
|
|
1406
|
+
for (let i = 0; i < re.length; i++) {
|
|
1407
|
+
re[i] = re[i] * scale;
|
|
1408
|
+
im[i] = im[i] * scale;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
function extractFrame(cbRe, cbIm, ibest) {
|
|
1412
|
+
const outRe = new Float64Array(NN$1 * NSS);
|
|
1413
|
+
const outIm = new Float64Array(NN$1 * NSS);
|
|
1414
|
+
for (let i = 0; i < outRe.length; i++) {
|
|
1415
|
+
const src = ibest + i;
|
|
1416
|
+
if (src >= 0 && src < cbRe.length) {
|
|
1417
|
+
outRe[i] = cbRe[src];
|
|
1418
|
+
outIm[i] = cbIm[src];
|
|
1201
1419
|
}
|
|
1202
1420
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1421
|
+
return { re: outRe, im: outIm };
|
|
1422
|
+
}
|
|
1423
|
+
function createTweakedSyncTemplates() {
|
|
1424
|
+
const base = createBaseSyncTemplates();
|
|
1425
|
+
const fsample = FS2 / 2;
|
|
1426
|
+
const out = new Map();
|
|
1427
|
+
for (let idf = -16; idf <= 16; idf++) {
|
|
1428
|
+
const tweak = createFrequencyTweak(idf, 2 * NSS, fsample);
|
|
1429
|
+
out.set(idf, [
|
|
1430
|
+
applyTweak(base[0], tweak),
|
|
1431
|
+
applyTweak(base[1], tweak),
|
|
1432
|
+
applyTweak(base[2], tweak),
|
|
1433
|
+
applyTweak(base[3], tweak),
|
|
1434
|
+
]);
|
|
1435
|
+
}
|
|
1436
|
+
return out;
|
|
1437
|
+
}
|
|
1438
|
+
function createBaseSyncTemplates() {
|
|
1439
|
+
return [
|
|
1440
|
+
buildSyncTemplate(COSTAS_A),
|
|
1441
|
+
buildSyncTemplate(COSTAS_B),
|
|
1442
|
+
buildSyncTemplate(COSTAS_C),
|
|
1443
|
+
buildSyncTemplate(COSTAS_D),
|
|
1444
|
+
];
|
|
1445
|
+
}
|
|
1446
|
+
function buildSyncTemplate(tones) {
|
|
1447
|
+
const re = new Float64Array(2 * NSS);
|
|
1448
|
+
const im = new Float64Array(2 * NSS);
|
|
1449
|
+
let k = 0;
|
|
1450
|
+
let phi = 0;
|
|
1451
|
+
for (const tone of tones) {
|
|
1452
|
+
const dphi = (TWO_PI$1 * tone * 2) / NSS;
|
|
1453
|
+
for (let j = 0; j < NSS / 2; j++) {
|
|
1454
|
+
re[k] = Math.cos(phi);
|
|
1455
|
+
im[k] = Math.sin(phi);
|
|
1456
|
+
phi = (phi + dphi) % TWO_PI$1;
|
|
1457
|
+
k++;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return { re, im };
|
|
1461
|
+
}
|
|
1462
|
+
function createFrequencyTweak(idf, npts, fsample) {
|
|
1463
|
+
const re = new Float64Array(npts);
|
|
1464
|
+
const im = new Float64Array(npts);
|
|
1465
|
+
const dphi = (TWO_PI$1 * idf) / fsample;
|
|
1466
|
+
const stepRe = Math.cos(dphi);
|
|
1467
|
+
const stepIm = Math.sin(dphi);
|
|
1468
|
+
let wRe = 1;
|
|
1469
|
+
let wIm = 0;
|
|
1470
|
+
for (let i = 0; i < npts; i++) {
|
|
1471
|
+
const newRe = wRe * stepRe - wIm * stepIm;
|
|
1472
|
+
const newIm = wRe * stepIm + wIm * stepRe;
|
|
1473
|
+
wRe = newRe;
|
|
1474
|
+
wIm = newIm;
|
|
1475
|
+
re[i] = wRe;
|
|
1476
|
+
im[i] = wIm;
|
|
1477
|
+
}
|
|
1478
|
+
return { re, im };
|
|
1479
|
+
}
|
|
1480
|
+
function applyTweak(template, tweak) {
|
|
1481
|
+
const re = new Float64Array(template.re.length);
|
|
1482
|
+
const im = new Float64Array(template.im.length);
|
|
1483
|
+
for (let i = 0; i < template.re.length; i++) {
|
|
1484
|
+
const sr = template.re[i];
|
|
1485
|
+
const si = template.im[i];
|
|
1486
|
+
const tr = tweak.re[i];
|
|
1487
|
+
const ti = tweak.im[i];
|
|
1488
|
+
re[i] = tr * sr - ti * si;
|
|
1489
|
+
im[i] = tr * si + ti * sr;
|
|
1490
|
+
}
|
|
1491
|
+
return { re, im };
|
|
1492
|
+
}
|
|
1493
|
+
function sync4d(cdRe, cdIm, i0, templates) {
|
|
1494
|
+
const starts = [i0, i0 + 33 * NSS, i0 + 66 * NSS, i0 + 99 * NSS];
|
|
1495
|
+
let sync = 0;
|
|
1496
|
+
for (let i = 0; i < 4; i++) {
|
|
1497
|
+
const z = correlateStride2(cdRe, cdIm, starts[i], templates[i].re, templates[i].im);
|
|
1498
|
+
if (z.count <= 16) {
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
sync += Math.hypot(z.re, z.im) / (2 * NSS);
|
|
1502
|
+
}
|
|
1503
|
+
return sync;
|
|
1504
|
+
}
|
|
1505
|
+
function correlateStride2(cdRe, cdIm, start, templateRe, templateIm) {
|
|
1506
|
+
let zRe = 0;
|
|
1507
|
+
let zIm = 0;
|
|
1508
|
+
let count = 0;
|
|
1509
|
+
for (let i = 0; i < templateRe.length; i++) {
|
|
1510
|
+
const idx = start + 2 * i;
|
|
1511
|
+
if (idx < 0 || idx >= cdRe.length) {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
const sRe = templateRe[i];
|
|
1515
|
+
const sIm = templateIm[i];
|
|
1516
|
+
const dRe = cdRe[idx];
|
|
1517
|
+
const dIm = cdIm[idx];
|
|
1518
|
+
zRe += dRe * sRe + dIm * sIm;
|
|
1519
|
+
zIm += dIm * sRe - dRe * sIm;
|
|
1520
|
+
count++;
|
|
1521
|
+
}
|
|
1522
|
+
return { re: zRe, im: zIm, count };
|
|
1523
|
+
}
|
|
1524
|
+
function getFt4Bitmetrics(cdRe, cdIm) {
|
|
1525
|
+
const csRe = new Float64Array(4 * NN$1);
|
|
1526
|
+
const csIm = new Float64Array(4 * NN$1);
|
|
1527
|
+
const s4 = new Float64Array(4 * NN$1);
|
|
1528
|
+
const symbRe = new Float64Array(NSS);
|
|
1529
|
+
const symbIm = new Float64Array(NSS);
|
|
1530
|
+
for (let k = 0; k < NN$1; k++) {
|
|
1531
|
+
const i1 = k * NSS;
|
|
1532
|
+
for (let i = 0; i < NSS; i++) {
|
|
1533
|
+
symbRe[i] = cdRe[i1 + i];
|
|
1534
|
+
symbIm[i] = cdIm[i1 + i];
|
|
1220
1535
|
}
|
|
1221
1536
|
fftComplex(symbRe, symbIm, false);
|
|
1222
|
-
for (let tone = 0; tone <
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1537
|
+
for (let tone = 0; tone < 4; tone++) {
|
|
1538
|
+
const idx = tone * NN$1 + k;
|
|
1539
|
+
const re = symbRe[tone];
|
|
1540
|
+
const im = symbIm[tone];
|
|
1541
|
+
csRe[idx] = re;
|
|
1542
|
+
csIm[idx] = im;
|
|
1543
|
+
s4[idx] = Math.hypot(re, im);
|
|
1228
1544
|
}
|
|
1229
1545
|
}
|
|
1230
|
-
// Sync quality check
|
|
1231
1546
|
let nsync = 0;
|
|
1232
|
-
for (let k = 0; k <
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1547
|
+
for (let k = 0; k < 4; k++) {
|
|
1548
|
+
if (maxTone(s4, k) === COSTAS_A[k]) {
|
|
1549
|
+
nsync++;
|
|
1550
|
+
}
|
|
1551
|
+
if (maxTone(s4, 33 + k) === COSTAS_B[k]) {
|
|
1552
|
+
nsync++;
|
|
1553
|
+
}
|
|
1554
|
+
if (maxTone(s4, 66 + k) === COSTAS_C[k]) {
|
|
1555
|
+
nsync++;
|
|
1556
|
+
}
|
|
1557
|
+
if (maxTone(s4, 99 + k) === COSTAS_D[k]) {
|
|
1558
|
+
nsync++;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const bitmetrics1 = new Float64Array(2 * NN$1);
|
|
1562
|
+
const bitmetrics2 = new Float64Array(2 * NN$1);
|
|
1563
|
+
const bitmetrics3 = new Float64Array(2 * NN$1);
|
|
1564
|
+
if (nsync < 6) {
|
|
1565
|
+
return { bitmetrics1, bitmetrics2, bitmetrics3, badsync: true };
|
|
1566
|
+
}
|
|
1567
|
+
for (let nseq = 1; nseq <= 3; nseq++) {
|
|
1568
|
+
const nsym = nseq === 1 ? 1 : nseq === 2 ? 2 : 4;
|
|
1569
|
+
const nt = 1 << (2 * nsym); // 4, 16, 256
|
|
1570
|
+
const ibmax = nseq === 1 ? 1 : nseq === 2 ? 3 : 7;
|
|
1571
|
+
const s2 = new Float64Array(nt);
|
|
1572
|
+
for (let ks = 1; ks <= NN$1 - nsym + 1; ks += nsym) {
|
|
1573
|
+
for (let i = 0; i < nt; i++) {
|
|
1574
|
+
const i1 = Math.floor(i / 64);
|
|
1575
|
+
const i2 = Math.floor((i & 63) / 16);
|
|
1576
|
+
const i3 = Math.floor((i & 15) / 4);
|
|
1577
|
+
const i4 = i & 3;
|
|
1578
|
+
if (nsym === 1) {
|
|
1579
|
+
const t = GRAYMAP[i4];
|
|
1580
|
+
const idx = t * NN$1 + (ks - 1);
|
|
1581
|
+
s2[i] = Math.hypot(csRe[idx], csIm[idx]);
|
|
1582
|
+
}
|
|
1583
|
+
else if (nsym === 2) {
|
|
1584
|
+
const t3 = GRAYMAP[i3];
|
|
1585
|
+
const t4 = GRAYMAP[i4];
|
|
1586
|
+
const iA = t3 * NN$1 + (ks - 1);
|
|
1587
|
+
const iB = t4 * NN$1 + ks;
|
|
1588
|
+
const re = csRe[iA] + csRe[iB];
|
|
1589
|
+
const im = csIm[iA] + csIm[iB];
|
|
1590
|
+
s2[i] = Math.hypot(re, im);
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
const t1 = GRAYMAP[i1];
|
|
1594
|
+
const t2 = GRAYMAP[i2];
|
|
1595
|
+
const t3 = GRAYMAP[i3];
|
|
1596
|
+
const t4 = GRAYMAP[i4];
|
|
1597
|
+
const iA = t1 * NN$1 + (ks - 1);
|
|
1598
|
+
const iB = t2 * NN$1 + ks;
|
|
1599
|
+
const iC = t3 * NN$1 + (ks + 1);
|
|
1600
|
+
const iD = t4 * NN$1 + (ks + 2);
|
|
1601
|
+
const re = csRe[iA] + csRe[iB] + csRe[iC] + csRe[iD];
|
|
1602
|
+
const im = csIm[iA] + csIm[iB] + csIm[iC] + csIm[iD];
|
|
1603
|
+
s2[i] = Math.hypot(re, im);
|
|
1241
1604
|
}
|
|
1242
1605
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
return null;
|
|
1249
|
-
// Compute soft bit metrics for multiple nsym values (1, 2, 3)
|
|
1250
|
-
// and a normalized version, matching the Fortran ft8b passes 1-4
|
|
1251
|
-
const bmeta = new Float64Array(N_LDPC); // nsym=1
|
|
1252
|
-
const bmetb = new Float64Array(N_LDPC); // nsym=2
|
|
1253
|
-
const bmetc = new Float64Array(N_LDPC); // nsym=3
|
|
1254
|
-
const bmetd = new Float64Array(N_LDPC); // nsym=1 normalized
|
|
1255
|
-
for (let nsym = 1; nsym <= 3; nsym++) {
|
|
1256
|
-
const nt = 1 << (3 * nsym); // 8, 64, 512
|
|
1257
|
-
const ibmax = nsym === 1 ? 2 : nsym === 2 ? 5 : 8;
|
|
1258
|
-
for (let ihalf = 1; ihalf <= 2; ihalf++) {
|
|
1259
|
-
for (let k = 1; k <= 29; k += nsym) {
|
|
1260
|
-
const ks = ihalf === 1 ? k + 7 : k + 43;
|
|
1261
|
-
const s2 = new Float64Array(nt);
|
|
1606
|
+
const ipt = 1 + (ks - 1) * 2;
|
|
1607
|
+
for (let ib = 0; ib <= ibmax; ib++) {
|
|
1608
|
+
const mask = 1 << (ibmax - ib);
|
|
1609
|
+
let max1 = -1e30;
|
|
1610
|
+
let max0 = -1e30;
|
|
1262
1611
|
for (let i = 0; i < nt; i++) {
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const re = csRe[graymap[i3] * NN + ks - 1];
|
|
1268
|
-
const im = csIm[graymap[i3] * NN + ks - 1];
|
|
1269
|
-
s2[i] = Math.sqrt(re * re + im * im);
|
|
1270
|
-
}
|
|
1271
|
-
else if (nsym === 2) {
|
|
1272
|
-
const sRe = csRe[graymap[i2] * NN + ks - 1] + csRe[graymap[i3] * NN + ks];
|
|
1273
|
-
const sIm = csIm[graymap[i2] * NN + ks - 1] + csIm[graymap[i3] * NN + ks];
|
|
1274
|
-
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
1275
|
-
}
|
|
1276
|
-
else {
|
|
1277
|
-
const sRe = csRe[graymap[i1] * NN + ks - 1] +
|
|
1278
|
-
csRe[graymap[i2] * NN + ks] +
|
|
1279
|
-
csRe[graymap[i3] * NN + ks + 1];
|
|
1280
|
-
const sIm = csIm[graymap[i1] * NN + ks - 1] +
|
|
1281
|
-
csIm[graymap[i2] * NN + ks] +
|
|
1282
|
-
csIm[graymap[i3] * NN + ks + 1];
|
|
1283
|
-
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
// Fortran: i32 = 1 + (k-1)*3 + (ihalf-1)*87 (1-based)
|
|
1287
|
-
const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
|
|
1288
|
-
for (let ib = 0; ib <= ibmax; ib++) {
|
|
1289
|
-
// max of s2 where bit (ibmax-ib) of index is 1
|
|
1290
|
-
let max1 = -1e30, max0 = -1e30;
|
|
1291
|
-
for (let i = 0; i < nt; i++) {
|
|
1292
|
-
const bitSet = (i & (1 << (ibmax - ib))) !== 0;
|
|
1293
|
-
if (bitSet) {
|
|
1294
|
-
if (s2[i] > max1)
|
|
1295
|
-
max1 = s2[i];
|
|
1296
|
-
}
|
|
1297
|
-
else {
|
|
1298
|
-
if (s2[i] > max0)
|
|
1299
|
-
max0 = s2[i];
|
|
1612
|
+
const v = s2[i];
|
|
1613
|
+
if ((i & mask) !== 0) {
|
|
1614
|
+
if (v > max1) {
|
|
1615
|
+
max1 = v;
|
|
1300
1616
|
}
|
|
1301
1617
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
const bm = max1 - max0;
|
|
1305
|
-
if (nsym === 1) {
|
|
1306
|
-
bmeta[idx] = bm;
|
|
1307
|
-
const den = Math.max(max1, max0);
|
|
1308
|
-
bmetd[idx] = den > 0 ? bm / den : 0;
|
|
1309
|
-
}
|
|
1310
|
-
else if (nsym === 2) {
|
|
1311
|
-
bmetb[idx] = bm;
|
|
1312
|
-
}
|
|
1313
|
-
else {
|
|
1314
|
-
bmetc[idx] = bm;
|
|
1315
|
-
}
|
|
1618
|
+
else if (v > max0) {
|
|
1619
|
+
max0 = v;
|
|
1316
1620
|
}
|
|
1317
1621
|
}
|
|
1622
|
+
const idx = ipt + ib;
|
|
1623
|
+
if (idx > 2 * NN$1) {
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const bm = max1 - max0;
|
|
1627
|
+
if (nseq === 1) {
|
|
1628
|
+
bitmetrics1[idx - 1] = bm;
|
|
1629
|
+
}
|
|
1630
|
+
else if (nseq === 2) {
|
|
1631
|
+
bitmetrics2[idx - 1] = bm;
|
|
1632
|
+
}
|
|
1633
|
+
else {
|
|
1634
|
+
bitmetrics3[idx - 1] = bm;
|
|
1635
|
+
}
|
|
1318
1636
|
}
|
|
1319
1637
|
}
|
|
1320
1638
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1639
|
+
bitmetrics2[208] = bitmetrics1[208];
|
|
1640
|
+
bitmetrics2[209] = bitmetrics1[209];
|
|
1641
|
+
bitmetrics3[208] = bitmetrics1[208];
|
|
1642
|
+
bitmetrics3[209] = bitmetrics1[209];
|
|
1643
|
+
normalizeBitMetrics(bitmetrics1);
|
|
1644
|
+
normalizeBitMetrics(bitmetrics2);
|
|
1645
|
+
normalizeBitMetrics(bitmetrics3);
|
|
1646
|
+
return { bitmetrics1, bitmetrics2, bitmetrics3, badsync: false };
|
|
1647
|
+
}
|
|
1648
|
+
function maxTone(s4, symbolIndex) {
|
|
1649
|
+
let bestTone = 0;
|
|
1650
|
+
let bestValue = -1;
|
|
1651
|
+
for (let tone = 0; tone < 4; tone++) {
|
|
1652
|
+
const v = s4[tone * NN$1 + symbolIndex];
|
|
1653
|
+
if (v > bestValue) {
|
|
1654
|
+
bestValue = v;
|
|
1655
|
+
bestTone = tone;
|
|
1656
|
+
}
|
|
1339
1657
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
let xsig = 0;
|
|
1359
|
-
let xnoi = 0;
|
|
1360
|
-
const itone = getTones$1(result.cw);
|
|
1361
|
-
for (let i = 0; i < 79; i++) {
|
|
1362
|
-
xsig += s8[itone[i] * NN + i] ** 2;
|
|
1363
|
-
const ios = (itone[i] + 4) % 7;
|
|
1364
|
-
xnoi += s8[ios * NN + i] ** 2;
|
|
1658
|
+
return bestTone;
|
|
1659
|
+
}
|
|
1660
|
+
function normalizeBitMetrics(bmet) {
|
|
1661
|
+
let sum = 0;
|
|
1662
|
+
let sum2 = 0;
|
|
1663
|
+
for (let i = 0; i < bmet.length; i++) {
|
|
1664
|
+
sum += bmet[i];
|
|
1665
|
+
sum2 += bmet[i] * bmet[i];
|
|
1666
|
+
}
|
|
1667
|
+
const avg = sum / bmet.length;
|
|
1668
|
+
const avg2 = sum2 / bmet.length;
|
|
1669
|
+
const variance = avg2 - avg * avg;
|
|
1670
|
+
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
1671
|
+
if (sigma <= 0) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
for (let i = 0; i < bmet.length; i++) {
|
|
1675
|
+
bmet[i] = bmet[i] / sigma;
|
|
1365
1676
|
}
|
|
1366
|
-
let snr = 0.001;
|
|
1367
|
-
const arg = xsig / Math.max(xnoi, 1e-30) - 1.0;
|
|
1368
|
-
if (arg > 0.1)
|
|
1369
|
-
snr = arg;
|
|
1370
|
-
snr = 10 * Math.log10(snr) - 27.0;
|
|
1371
|
-
if (snr < -24)
|
|
1372
|
-
snr = -24;
|
|
1373
|
-
return { msg, freq: f1, dt: xdt, snr };
|
|
1374
1677
|
}
|
|
1375
|
-
function
|
|
1376
|
-
const
|
|
1377
|
-
for (let i = 0; i <
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
for (
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (j === 30)
|
|
1387
|
-
k += 7;
|
|
1388
|
-
const indx = cw[i] * 4 + cw[i + 1] * 2 + cw[i + 2];
|
|
1389
|
-
tones[k] = graymap[indx];
|
|
1390
|
-
k++;
|
|
1391
|
-
}
|
|
1392
|
-
return tones;
|
|
1393
|
-
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
1396
|
-
* Identical to Fortran ft8_downsample.
|
|
1397
|
-
*/
|
|
1398
|
-
function ft8Downsample(cxRe, cxIm, f0, c1Re, c1Im) {
|
|
1399
|
-
const NFFT1 = 192000;
|
|
1400
|
-
const NFFT2 = 3200;
|
|
1401
|
-
const df = 12000.0 / NFFT1;
|
|
1402
|
-
// NSPS is imported, should be 1920
|
|
1403
|
-
const baud = 12000.0 / NSPS; // 6.25
|
|
1404
|
-
const i0 = Math.round(f0 / df);
|
|
1405
|
-
const ft = f0 + 8.5 * baud;
|
|
1406
|
-
const it = Math.min(Math.round(ft / df), NFFT1 / 2);
|
|
1407
|
-
const fb = f0 - 1.5 * baud;
|
|
1408
|
-
const ib = Math.max(1, Math.round(fb / df));
|
|
1409
|
-
c1Re.fill(0);
|
|
1410
|
-
c1Im.fill(0);
|
|
1411
|
-
let k = 0;
|
|
1412
|
-
for (let i = ib; i <= it; i++) {
|
|
1413
|
-
if (k >= NFFT2)
|
|
1414
|
-
break;
|
|
1415
|
-
c1Re[k] = cxRe[i] ?? 0;
|
|
1416
|
-
c1Im[k] = cxIm[i] ?? 0;
|
|
1417
|
-
k++;
|
|
1418
|
-
}
|
|
1419
|
-
// Taper
|
|
1420
|
-
const pi = Math.PI;
|
|
1421
|
-
const taper = new Float64Array(101);
|
|
1422
|
-
for (let i = 0; i <= 100; i++) {
|
|
1423
|
-
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
1424
|
-
}
|
|
1425
|
-
for (let i = 0; i <= 100; i++) {
|
|
1426
|
-
if (i >= NFFT2)
|
|
1427
|
-
break;
|
|
1428
|
-
const tap = taper[100 - i];
|
|
1429
|
-
c1Re[i] = c1Re[i] * tap;
|
|
1430
|
-
c1Im[i] = c1Im[i] * tap;
|
|
1431
|
-
}
|
|
1432
|
-
const endTap = k - 1;
|
|
1433
|
-
for (let i = 0; i <= 100; i++) {
|
|
1434
|
-
const idx = endTap - 100 + i;
|
|
1435
|
-
if (idx >= 0 && idx < NFFT2) {
|
|
1436
|
-
const tap = taper[i];
|
|
1437
|
-
c1Re[idx] = c1Re[idx] * tap;
|
|
1438
|
-
c1Im[idx] = c1Im[idx] * tap;
|
|
1678
|
+
function passesHardSyncQuality(bitmetrics1) {
|
|
1679
|
+
const hard = new Uint8Array(bitmetrics1.length);
|
|
1680
|
+
for (let i = 0; i < bitmetrics1.length; i++) {
|
|
1681
|
+
hard[i] = bitmetrics1[i] >= 0 ? 1 : 0;
|
|
1682
|
+
}
|
|
1683
|
+
let score = 0;
|
|
1684
|
+
for (const pattern of HARD_SYNC_PATTERNS) {
|
|
1685
|
+
for (let i = 0; i < pattern.bits.length; i++) {
|
|
1686
|
+
if (hard[pattern.offset + i] === pattern.bits[i]) {
|
|
1687
|
+
score++;
|
|
1688
|
+
}
|
|
1439
1689
|
}
|
|
1440
1690
|
}
|
|
1441
|
-
|
|
1442
|
-
const shift = i0 - ib;
|
|
1443
|
-
const tempRe = new Float64Array(NFFT2);
|
|
1444
|
-
const tempIm = new Float64Array(NFFT2);
|
|
1445
|
-
for (let i = 0; i < NFFT2; i++) {
|
|
1446
|
-
let srcIdx = (i + shift) % NFFT2;
|
|
1447
|
-
if (srcIdx < 0)
|
|
1448
|
-
srcIdx += NFFT2;
|
|
1449
|
-
tempRe[i] = c1Re[srcIdx];
|
|
1450
|
-
tempIm[i] = c1Im[srcIdx];
|
|
1451
|
-
}
|
|
1452
|
-
for (let i = 0; i < NFFT2; i++) {
|
|
1453
|
-
c1Re[i] = tempRe[i];
|
|
1454
|
-
c1Im[i] = tempIm[i];
|
|
1455
|
-
}
|
|
1456
|
-
// iFFT
|
|
1457
|
-
fftComplex(c1Re, c1Im, true);
|
|
1458
|
-
// Scale
|
|
1459
|
-
// Fortran uses 1.0/sqrt(NFFT1 * NFFT2), but our fftComplex(true) scales by 1/NFFT2
|
|
1460
|
-
const scale = Math.sqrt(NFFT2 / NFFT1);
|
|
1461
|
-
for (let i = 0; i < NFFT2; i++) {
|
|
1462
|
-
c1Re[i] = c1Re[i] * scale;
|
|
1463
|
-
c1Im[i] = c1Im[i] * scale;
|
|
1464
|
-
}
|
|
1691
|
+
return score >= 10;
|
|
1465
1692
|
}
|
|
1466
|
-
function
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
let zRe = 0, zIm = 0;
|
|
1488
|
-
if (iStart >= 0 && iStart + 31 < NP2) {
|
|
1489
|
-
for (let j = 0; j < 32; j++) {
|
|
1490
|
-
let sRe = csyncRe[i * 32 + j];
|
|
1491
|
-
let sIm = csyncIm[i * 32 + j];
|
|
1492
|
-
if (useTwk && twkRe && twkIm) {
|
|
1493
|
-
const tRe = twkRe[j] * sRe - twkIm[j] * sIm;
|
|
1494
|
-
const tIm = twkRe[j] * sIm + twkIm[j] * sRe;
|
|
1495
|
-
sRe = tRe;
|
|
1496
|
-
sIm = tIm;
|
|
1497
|
-
}
|
|
1498
|
-
// Conjugate multiply: cd0 * conj(csync)
|
|
1499
|
-
const dRe = cd0Re[iStart + j];
|
|
1500
|
-
const dIm = cd0Im[iStart + j];
|
|
1501
|
-
zRe += dRe * sRe + dIm * sIm;
|
|
1502
|
-
zIm += dIm * sRe - dRe * sIm;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
sync += zRe * zRe + zIm * zIm;
|
|
1693
|
+
function buildLlrs(bitmetrics1, bitmetrics2, bitmetrics3) {
|
|
1694
|
+
const llra = new Float64Array(174);
|
|
1695
|
+
const llrb = new Float64Array(174);
|
|
1696
|
+
const llrc = new Float64Array(174);
|
|
1697
|
+
for (let i = 0; i < 58; i++) {
|
|
1698
|
+
llra[i] = bitmetrics1[8 + i];
|
|
1699
|
+
llra[58 + i] = bitmetrics1[74 + i];
|
|
1700
|
+
llra[116 + i] = bitmetrics1[140 + i];
|
|
1701
|
+
llrb[i] = bitmetrics2[8 + i];
|
|
1702
|
+
llrb[58 + i] = bitmetrics2[74 + i];
|
|
1703
|
+
llrb[116 + i] = bitmetrics2[140 + i];
|
|
1704
|
+
llrc[i] = bitmetrics3[8 + i];
|
|
1705
|
+
llrc[58 + i] = bitmetrics3[74 + i];
|
|
1706
|
+
llrc[116 + i] = bitmetrics3[140 + i];
|
|
1707
|
+
}
|
|
1708
|
+
return [llra, llrb, llrc];
|
|
1709
|
+
}
|
|
1710
|
+
function hasNonZeroBit(bits) {
|
|
1711
|
+
for (const bit of bits) {
|
|
1712
|
+
if (bit !== 0) {
|
|
1713
|
+
return true;
|
|
1506
1714
|
}
|
|
1507
1715
|
}
|
|
1508
|
-
return
|
|
1716
|
+
return false;
|
|
1509
1717
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
for (let i = 0; i < n; i++) {
|
|
1514
|
-
sum += bmet[i];
|
|
1515
|
-
sum2 += bmet[i] * bmet[i];
|
|
1516
|
-
}
|
|
1517
|
-
const avg = sum / n;
|
|
1518
|
-
const avg2 = sum2 / n;
|
|
1519
|
-
const variance = avg2 - avg * avg;
|
|
1520
|
-
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
1521
|
-
if (sigma > 0) {
|
|
1522
|
-
for (let i = 0; i < n; i++)
|
|
1523
|
-
bmet[i] = bmet[i] / sigma;
|
|
1718
|
+
function toFt4Snr(syncMinusOne) {
|
|
1719
|
+
if (syncMinusOne > 0) {
|
|
1720
|
+
return Math.round(Math.max(-21, 10 * Math.log10(syncMinusOne) - 14.8));
|
|
1524
1721
|
}
|
|
1722
|
+
return -21;
|
|
1525
1723
|
}
|
|
1526
|
-
function resample(input, fromRate, toRate, outLen) {
|
|
1724
|
+
function resample$1(input, fromRate, toRate, outLen) {
|
|
1527
1725
|
const out = new Float64Array(outLen);
|
|
1528
1726
|
const ratio = fromRate / toRate;
|
|
1529
1727
|
for (let i = 0; i < outLen; i++) {
|
|
@@ -2047,9 +2245,9 @@ function packFreeText(msg) {
|
|
|
2047
2245
|
}
|
|
2048
2246
|
|
|
2049
2247
|
const TWO_PI = 2 * Math.PI;
|
|
2050
|
-
const
|
|
2051
|
-
const
|
|
2052
|
-
const
|
|
2248
|
+
const FT8_DEFAULT_SAMPLE_RATE = 12_000;
|
|
2249
|
+
const FT8_DEFAULT_SAMPLES_PER_SYMBOL = 1_920;
|
|
2250
|
+
const FT8_DEFAULT_BT = 2.0;
|
|
2053
2251
|
const MODULATION_INDEX = 1.0;
|
|
2054
2252
|
function assertPositiveFinite(value, name) {
|
|
2055
2253
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -2072,15 +2270,14 @@ function gfskPulse(bt, tt) {
|
|
|
2072
2270
|
const scale = Math.PI * Math.sqrt(2 / Math.log(2)) * bt;
|
|
2073
2271
|
return 0.5 * (erfApprox(scale * (tt + 0.5)) - erfApprox(scale * (tt - 0.5)));
|
|
2074
2272
|
}
|
|
2075
|
-
function
|
|
2076
|
-
// Mirrors the FT8 path in lib/ft8/gen_ft8wave.f90.
|
|
2273
|
+
function generateGfskWaveform(tones, options, defaults, shape) {
|
|
2077
2274
|
const nsym = tones.length;
|
|
2078
2275
|
if (nsym === 0) {
|
|
2079
2276
|
return new Float32Array(0);
|
|
2080
2277
|
}
|
|
2081
|
-
const sampleRate = options.sampleRate ??
|
|
2082
|
-
const nsps = options.samplesPerSymbol ??
|
|
2083
|
-
const bt = options.bt ??
|
|
2278
|
+
const sampleRate = options.sampleRate ?? defaults.sampleRate;
|
|
2279
|
+
const nsps = options.samplesPerSymbol ?? defaults.samplesPerSymbol;
|
|
2280
|
+
const bt = options.bt ?? defaults.bt;
|
|
2084
2281
|
const f0 = options.baseFrequency ?? 0;
|
|
2085
2282
|
assertPositiveFinite(sampleRate, "sampleRate");
|
|
2086
2283
|
assertPositiveFinite(nsps, "samplesPerSymbol");
|
|
@@ -2091,7 +2288,7 @@ function generateFT8Waveform(tones, options = {}) {
|
|
|
2091
2288
|
if (!Number.isInteger(nsps)) {
|
|
2092
2289
|
throw new Error("samplesPerSymbol must be an integer");
|
|
2093
2290
|
}
|
|
2094
|
-
const nwave = nsym * nsps;
|
|
2291
|
+
const nwave = (nsym) * nsps;
|
|
2095
2292
|
const pulse = new Float64Array(3 * nsps);
|
|
2096
2293
|
for (let i = 0; i < pulse.length; i++) {
|
|
2097
2294
|
const tt = (i + 1 - 1.5 * nsps) / nsps;
|
|
@@ -2119,8 +2316,9 @@ function generateFT8Waveform(tones, options = {}) {
|
|
|
2119
2316
|
}
|
|
2120
2317
|
const wave = new Float32Array(nwave);
|
|
2121
2318
|
let phi = 0;
|
|
2319
|
+
const phaseStart = nsps;
|
|
2122
2320
|
for (let k = 0; k < nwave; k++) {
|
|
2123
|
-
const j =
|
|
2321
|
+
const j = phaseStart + k;
|
|
2124
2322
|
wave[k] = Math.sin(phi);
|
|
2125
2323
|
phi += dphi[j];
|
|
2126
2324
|
phi %= TWO_PI;
|
|
@@ -2128,18 +2326,41 @@ function generateFT8Waveform(tones, options = {}) {
|
|
|
2128
2326
|
phi += TWO_PI;
|
|
2129
2327
|
}
|
|
2130
2328
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2329
|
+
{
|
|
2330
|
+
const nramp = Math.round(nsps / 8);
|
|
2331
|
+
for (let i = 0; i < nramp; i++) {
|
|
2332
|
+
const up = (1 - Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
|
|
2333
|
+
wave[i] *= up;
|
|
2334
|
+
}
|
|
2335
|
+
const tailStart = nwave - nramp;
|
|
2336
|
+
for (let i = 0; i < nramp; i++) {
|
|
2337
|
+
const down = (1 + Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
|
|
2338
|
+
wave[tailStart + i] *= down;
|
|
2339
|
+
}
|
|
2140
2340
|
}
|
|
2141
2341
|
return wave;
|
|
2142
2342
|
}
|
|
2343
|
+
function generateFT8Waveform(tones, options = {}) {
|
|
2344
|
+
// Mirrors the FT8 path in lib/ft8/gen_ft8wave.f90.
|
|
2345
|
+
return generateGfskWaveform(tones, options, {
|
|
2346
|
+
sampleRate: FT8_DEFAULT_SAMPLE_RATE,
|
|
2347
|
+
samplesPerSymbol: FT8_DEFAULT_SAMPLES_PER_SYMBOL,
|
|
2348
|
+
bt: FT8_DEFAULT_BT,
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/** FT8-specific constants (lib/ft8/ft8_params.f90). */
|
|
2353
|
+
const NSPS = 1920;
|
|
2354
|
+
const NFFT1 = 2 * NSPS; // 3840
|
|
2355
|
+
const NSTEP = NSPS / 4; // 480
|
|
2356
|
+
const NMAX = 15 * 12_000; // 180000
|
|
2357
|
+
const NHSYM = Math.floor(NMAX / NSTEP) - 3; // 372
|
|
2358
|
+
const NDOWN = 60;
|
|
2359
|
+
const NN = 79;
|
|
2360
|
+
/** 7-symbol Costas array for sync. */
|
|
2361
|
+
const COSTAS = [3, 1, 4, 0, 6, 5, 2];
|
|
2362
|
+
/** 8-tone Gray mapping. */
|
|
2363
|
+
const GRAY_MAP = [0, 1, 3, 2, 5, 6, 4, 7];
|
|
2143
2364
|
|
|
2144
2365
|
function generateLdpcGMatrix() {
|
|
2145
2366
|
const K = 91;
|
|
@@ -2158,64 +2379,678 @@ function generateLdpcGMatrix() {
|
|
|
2158
2379
|
}
|
|
2159
2380
|
}
|
|
2160
2381
|
}
|
|
2161
|
-
return gen;
|
|
2162
|
-
}
|
|
2163
|
-
const G = generateLdpcGMatrix();
|
|
2164
|
-
function encode174_91(msg77) {
|
|
2165
|
-
const poly = 0x2757;
|
|
2166
|
-
let crc = 0;
|
|
2167
|
-
// padded with 19 zeros (3 zeros + 16 zero-bits for flush)
|
|
2168
|
-
const bitArray = [...msg77, 0, 0, 0, ...new Array(16).fill(0)];
|
|
2169
|
-
for (let bit = 0; bit < 96; bit++) {
|
|
2170
|
-
const nextBit = bitArray[bit];
|
|
2171
|
-
if ((crc & 0x2000) !== 0) {
|
|
2172
|
-
crc = ((crc << 1) | nextBit) ^ poly;
|
|
2173
|
-
}
|
|
2174
|
-
else {
|
|
2175
|
-
crc = (crc << 1) | nextBit;
|
|
2176
|
-
}
|
|
2177
|
-
crc &= 0x3fff;
|
|
2178
|
-
}
|
|
2179
|
-
const msg91 = [...msg77];
|
|
2180
|
-
for (let i = 0; i < 14; i++) {
|
|
2181
|
-
msg91.push((crc >> (13 - i)) & 1);
|
|
2382
|
+
return gen;
|
|
2383
|
+
}
|
|
2384
|
+
const G = generateLdpcGMatrix();
|
|
2385
|
+
function encode174_91(msg77) {
|
|
2386
|
+
const poly = 0x2757;
|
|
2387
|
+
let crc = 0;
|
|
2388
|
+
// padded with 19 zeros (3 zeros + 16 zero-bits for flush)
|
|
2389
|
+
const bitArray = [...msg77, 0, 0, 0, ...new Array(16).fill(0)];
|
|
2390
|
+
for (let bit = 0; bit < 96; bit++) {
|
|
2391
|
+
const nextBit = bitArray[bit];
|
|
2392
|
+
if ((crc & 0x2000) !== 0) {
|
|
2393
|
+
crc = ((crc << 1) | nextBit) ^ poly;
|
|
2394
|
+
}
|
|
2395
|
+
else {
|
|
2396
|
+
crc = (crc << 1) | nextBit;
|
|
2397
|
+
}
|
|
2398
|
+
crc &= 0x3fff;
|
|
2399
|
+
}
|
|
2400
|
+
const msg91 = [...msg77];
|
|
2401
|
+
for (let i = 0; i < 14; i++) {
|
|
2402
|
+
msg91.push((crc >> (13 - i)) & 1);
|
|
2403
|
+
}
|
|
2404
|
+
const codeword = [...msg91];
|
|
2405
|
+
for (let i = 0; i < 83; i++) {
|
|
2406
|
+
let sum = 0;
|
|
2407
|
+
for (let j = 0; j < 91; j++) {
|
|
2408
|
+
sum += msg91[j] * G[i][j];
|
|
2409
|
+
}
|
|
2410
|
+
codeword.push(sum % 2);
|
|
2411
|
+
}
|
|
2412
|
+
return codeword;
|
|
2413
|
+
}
|
|
2414
|
+
function getTones$1(codeword) {
|
|
2415
|
+
const tones = new Array(79).fill(0);
|
|
2416
|
+
for (let i = 0; i < 7; i++)
|
|
2417
|
+
tones[i] = COSTAS[i];
|
|
2418
|
+
for (let i = 0; i < 7; i++)
|
|
2419
|
+
tones[36 + i] = COSTAS[i];
|
|
2420
|
+
for (let i = 0; i < 7; i++)
|
|
2421
|
+
tones[72 + i] = COSTAS[i];
|
|
2422
|
+
let k = 7;
|
|
2423
|
+
for (let j = 1; j <= 58; j++) {
|
|
2424
|
+
const i = j * 3 - 3; // codeword is 0-indexed in JS, but the loop was j=1 to 58
|
|
2425
|
+
if (j === 30)
|
|
2426
|
+
k += 7;
|
|
2427
|
+
const indx = codeword[i] * 4 + codeword[i + 1] * 2 + codeword[i + 2];
|
|
2428
|
+
tones[k] = GRAY_MAP[indx];
|
|
2429
|
+
k++;
|
|
2430
|
+
}
|
|
2431
|
+
return tones;
|
|
2432
|
+
}
|
|
2433
|
+
function encodeMessage(msg) {
|
|
2434
|
+
const bits77 = pack77(msg);
|
|
2435
|
+
const codeword = encode174_91(bits77);
|
|
2436
|
+
return getTones$1(codeword);
|
|
2437
|
+
}
|
|
2438
|
+
function encode(msg, options = {}) {
|
|
2439
|
+
return generateFT8Waveform(encodeMessage(msg), options);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Decode all FT8 signals in an audio buffer.
|
|
2444
|
+
* Input: mono audio samples at `sampleRate` Hz, duration ~15s.
|
|
2445
|
+
*/
|
|
2446
|
+
function decode(samples, options = {}) {
|
|
2447
|
+
const sampleRate = options.sampleRate ?? SAMPLE_RATE$1;
|
|
2448
|
+
const nfa = options.freqLow ?? 200;
|
|
2449
|
+
const nfb = options.freqHigh ?? 3000;
|
|
2450
|
+
const syncmin = options.syncMin ?? 1.2;
|
|
2451
|
+
const depth = options.depth ?? 2;
|
|
2452
|
+
const maxCandidates = options.maxCandidates ?? 300;
|
|
2453
|
+
const book = options.hashCallBook;
|
|
2454
|
+
// Resample to 12000 Hz if needed
|
|
2455
|
+
let dd;
|
|
2456
|
+
if (sampleRate === SAMPLE_RATE$1) {
|
|
2457
|
+
dd = new Float64Array(NMAX);
|
|
2458
|
+
const len = Math.min(samples.length, NMAX);
|
|
2459
|
+
for (let i = 0; i < len; i++)
|
|
2460
|
+
dd[i] = samples[i];
|
|
2461
|
+
}
|
|
2462
|
+
else {
|
|
2463
|
+
dd = resample(samples, sampleRate, SAMPLE_RATE$1, NMAX);
|
|
2464
|
+
}
|
|
2465
|
+
// Compute huge FFT for downsampling caching
|
|
2466
|
+
const NFFT1_LONG = 192000;
|
|
2467
|
+
const cxRe = new Float64Array(NFFT1_LONG);
|
|
2468
|
+
const cxIm = new Float64Array(NFFT1_LONG);
|
|
2469
|
+
for (let i = 0; i < NMAX; i++) {
|
|
2470
|
+
cxRe[i] = dd[i] ?? 0;
|
|
2471
|
+
}
|
|
2472
|
+
fftComplex(cxRe, cxIm, false);
|
|
2473
|
+
// Compute spectrogram and find sync candidates
|
|
2474
|
+
const { candidates, sbase } = sync8(dd, nfa, nfb, syncmin, maxCandidates);
|
|
2475
|
+
const decoded = [];
|
|
2476
|
+
const seenMessages = new Set();
|
|
2477
|
+
for (const cand of candidates) {
|
|
2478
|
+
const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book);
|
|
2479
|
+
if (!result)
|
|
2480
|
+
continue;
|
|
2481
|
+
if (seenMessages.has(result.msg))
|
|
2482
|
+
continue;
|
|
2483
|
+
seenMessages.add(result.msg);
|
|
2484
|
+
decoded.push({
|
|
2485
|
+
freq: result.freq,
|
|
2486
|
+
dt: result.dt - 0.5,
|
|
2487
|
+
snr: result.snr,
|
|
2488
|
+
msg: result.msg,
|
|
2489
|
+
sync: cand.sync,
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return decoded;
|
|
2493
|
+
}
|
|
2494
|
+
function sync8(dd, nfa, nfb, syncmin, maxcand) {
|
|
2495
|
+
const JZ = 62;
|
|
2496
|
+
// Fortran uses NFFT1=3840 for the spectrogram FFT; we need a power of 2
|
|
2497
|
+
const fftSize = nextPow2(NFFT1); // 4096
|
|
2498
|
+
const halfSize = fftSize / 2; // 2048
|
|
2499
|
+
const tstep = NSTEP / SAMPLE_RATE$1;
|
|
2500
|
+
const df = SAMPLE_RATE$1 / fftSize;
|
|
2501
|
+
const fac = 1.0 / 300.0;
|
|
2502
|
+
// Compute symbol spectra, stepping by NSTEP
|
|
2503
|
+
const s = new Float64Array(halfSize * NHSYM);
|
|
2504
|
+
const savg = new Float64Array(halfSize);
|
|
2505
|
+
const xRe = new Float64Array(fftSize);
|
|
2506
|
+
const xIm = new Float64Array(fftSize);
|
|
2507
|
+
for (let j = 0; j < NHSYM; j++) {
|
|
2508
|
+
const ia = j * NSTEP;
|
|
2509
|
+
xRe.fill(0);
|
|
2510
|
+
xIm.fill(0);
|
|
2511
|
+
for (let i = 0; i < NSPS && ia + i < dd.length; i++) {
|
|
2512
|
+
xRe[i] = fac * dd[ia + i];
|
|
2513
|
+
}
|
|
2514
|
+
fftComplex(xRe, xIm, false);
|
|
2515
|
+
for (let i = 0; i < halfSize; i++) {
|
|
2516
|
+
const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
|
|
2517
|
+
s[i * NHSYM + j] = power;
|
|
2518
|
+
savg[i] = (savg[i] ?? 0) + power;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
// Compute baseline
|
|
2522
|
+
const sbase = computeBaseline(savg, nfa, nfb, df, halfSize);
|
|
2523
|
+
const ia = Math.max(1, Math.round(nfa / df));
|
|
2524
|
+
const ib = Math.min(halfSize - 14, Math.round(nfb / df));
|
|
2525
|
+
const nssy = Math.floor(NSPS / NSTEP);
|
|
2526
|
+
const nfos = Math.round(SAMPLE_RATE$1 / NSPS / df); // ~2 bins per tone spacing
|
|
2527
|
+
const jstrt = Math.round(0.5 / tstep);
|
|
2528
|
+
// 2D sync correlation
|
|
2529
|
+
const sync2d = new Float64Array((ib - ia + 1) * (2 * JZ + 1));
|
|
2530
|
+
const width = 2 * JZ + 1;
|
|
2531
|
+
for (let i = ia; i <= ib; i++) {
|
|
2532
|
+
for (let jj = -JZ; jj <= JZ; jj++) {
|
|
2533
|
+
let ta = 0, tb = 0, tc = 0;
|
|
2534
|
+
let t0a = 0, t0b = 0, t0c = 0;
|
|
2535
|
+
for (let n = 0; n < 7; n++) {
|
|
2536
|
+
const m = jj + jstrt + nssy * n;
|
|
2537
|
+
const iCostas = i + nfos * COSTAS[n];
|
|
2538
|
+
if (m >= 0 && m < NHSYM && iCostas < halfSize) {
|
|
2539
|
+
ta += s[iCostas * NHSYM + m];
|
|
2540
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2541
|
+
const idx = i + nfos * tone;
|
|
2542
|
+
if (idx < halfSize)
|
|
2543
|
+
t0a += s[idx * NHSYM + m];
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const m36 = m + nssy * 36;
|
|
2547
|
+
if (m36 >= 0 && m36 < NHSYM && iCostas < halfSize) {
|
|
2548
|
+
tb += s[iCostas * NHSYM + m36];
|
|
2549
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2550
|
+
const idx = i + nfos * tone;
|
|
2551
|
+
if (idx < halfSize)
|
|
2552
|
+
t0b += s[idx * NHSYM + m36];
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const m72 = m + nssy * 72;
|
|
2556
|
+
if (m72 >= 0 && m72 < NHSYM && iCostas < halfSize) {
|
|
2557
|
+
tc += s[iCostas * NHSYM + m72];
|
|
2558
|
+
for (let tone = 0; tone <= 6; tone++) {
|
|
2559
|
+
const idx = i + nfos * tone;
|
|
2560
|
+
if (idx < halfSize)
|
|
2561
|
+
t0c += s[idx * NHSYM + m72];
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const t = ta + tb + tc;
|
|
2566
|
+
const t0total = t0a + t0b + t0c;
|
|
2567
|
+
const t0 = (t0total - t) / 6.0;
|
|
2568
|
+
const syncVal = t0 > 0 ? t / t0 : 0;
|
|
2569
|
+
const tbc = tb + tc;
|
|
2570
|
+
const t0bc = t0b + t0c;
|
|
2571
|
+
const t0bc2 = (t0bc - tbc) / 6.0;
|
|
2572
|
+
const syncBc = t0bc2 > 0 ? tbc / t0bc2 : 0;
|
|
2573
|
+
sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
// Find peaks
|
|
2577
|
+
const candidates0 = [];
|
|
2578
|
+
const mlag = 10;
|
|
2579
|
+
for (let i = ia; i <= ib; i++) {
|
|
2580
|
+
let bestSync = -1;
|
|
2581
|
+
let bestJ = 0;
|
|
2582
|
+
for (let j = -mlag; j <= mlag; j++) {
|
|
2583
|
+
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
2584
|
+
if (v > bestSync) {
|
|
2585
|
+
bestSync = v;
|
|
2586
|
+
bestJ = j;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
// Also check wider range
|
|
2590
|
+
let bestSync2 = -1;
|
|
2591
|
+
let bestJ2 = 0;
|
|
2592
|
+
for (let j = -JZ; j <= JZ; j++) {
|
|
2593
|
+
const v = sync2d[(i - ia) * width + (j + JZ)];
|
|
2594
|
+
if (v > bestSync2) {
|
|
2595
|
+
bestSync2 = v;
|
|
2596
|
+
bestJ2 = j;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
if (bestSync >= syncmin) {
|
|
2600
|
+
candidates0.push({
|
|
2601
|
+
freq: i * df,
|
|
2602
|
+
dt: (bestJ - 0.5) * tstep,
|
|
2603
|
+
sync: bestSync,
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
if (Math.abs(bestJ2 - bestJ) > 0 && bestSync2 >= syncmin) {
|
|
2607
|
+
candidates0.push({
|
|
2608
|
+
freq: i * df,
|
|
2609
|
+
dt: (bestJ2 - 0.5) * tstep,
|
|
2610
|
+
sync: bestSync2,
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
// Compute baseline normalization for sync values
|
|
2615
|
+
const syncValues = candidates0.map((c) => c.sync);
|
|
2616
|
+
syncValues.sort((a, b) => a - b);
|
|
2617
|
+
const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
|
|
2618
|
+
const base = syncValues[pctileIdx] ?? 1;
|
|
2619
|
+
if (base > 0) {
|
|
2620
|
+
for (const c of candidates0)
|
|
2621
|
+
c.sync /= base;
|
|
2622
|
+
}
|
|
2623
|
+
// Remove near-duplicate candidates
|
|
2624
|
+
for (let i = 0; i < candidates0.length; i++) {
|
|
2625
|
+
for (let j = 0; j < i; j++) {
|
|
2626
|
+
const fdiff = Math.abs(candidates0[i].freq - candidates0[j].freq);
|
|
2627
|
+
const tdiff = Math.abs(candidates0[i].dt - candidates0[j].dt);
|
|
2628
|
+
if (fdiff < 4.0 && tdiff < 0.04) {
|
|
2629
|
+
if (candidates0[i].sync >= candidates0[j].sync) {
|
|
2630
|
+
candidates0[j].sync = 0;
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
candidates0[i].sync = 0;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
// Sort by sync descending, take top maxcand
|
|
2639
|
+
const filtered = candidates0.filter((c) => c.sync >= syncmin);
|
|
2640
|
+
filtered.sort((a, b) => b.sync - a.sync);
|
|
2641
|
+
return { candidates: filtered.slice(0, maxcand), sbase };
|
|
2642
|
+
}
|
|
2643
|
+
function computeBaseline(savg, nfa, nfb, df, nh1) {
|
|
2644
|
+
const sbase = new Float64Array(nh1);
|
|
2645
|
+
const ia = Math.max(1, Math.round(nfa / df));
|
|
2646
|
+
const ib = Math.min(nh1 - 1, Math.round(nfb / df));
|
|
2647
|
+
// Smooth the spectrum to get baseline
|
|
2648
|
+
const window = 50; // bins
|
|
2649
|
+
for (let i = 0; i < nh1; i++) {
|
|
2650
|
+
let sum = 0;
|
|
2651
|
+
let count = 0;
|
|
2652
|
+
const lo = Math.max(ia, i - window);
|
|
2653
|
+
const hi = Math.min(ib, i + window);
|
|
2654
|
+
for (let j = lo; j <= hi; j++) {
|
|
2655
|
+
sum += savg[j];
|
|
2656
|
+
count++;
|
|
2657
|
+
}
|
|
2658
|
+
sbase[i] = count > 0 ? 10 * Math.log10(Math.max(1e-30, sum / count)) : 0;
|
|
2659
|
+
}
|
|
2660
|
+
return sbase;
|
|
2661
|
+
}
|
|
2662
|
+
function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
|
|
2663
|
+
const NFFT2 = 3200;
|
|
2664
|
+
const NP2 = 2812;
|
|
2665
|
+
const fs2 = SAMPLE_RATE$1 / NDOWN;
|
|
2666
|
+
const dt2 = 1.0 / fs2;
|
|
2667
|
+
const twopi = 2 * Math.PI;
|
|
2668
|
+
// Downsample: mix to baseband and filter
|
|
2669
|
+
const cd0Re = new Float64Array(NFFT2);
|
|
2670
|
+
const cd0Im = new Float64Array(NFFT2);
|
|
2671
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
2672
|
+
// Find best time offset
|
|
2673
|
+
const i0 = Math.round((xdt + 0.5) * fs2);
|
|
2674
|
+
let smax = 0;
|
|
2675
|
+
let ibest = i0;
|
|
2676
|
+
for (let idt = i0 - 10; idt <= i0 + 10; idt++) {
|
|
2677
|
+
const sync = sync8d(cd0Re, cd0Im, idt, null, null, false);
|
|
2678
|
+
if (sync > smax) {
|
|
2679
|
+
smax = sync;
|
|
2680
|
+
ibest = idt;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
// Fine frequency search
|
|
2684
|
+
smax = 0;
|
|
2685
|
+
let delfbest = 0;
|
|
2686
|
+
for (let ifr = -5; ifr <= 5; ifr++) {
|
|
2687
|
+
const delf = ifr * 0.5;
|
|
2688
|
+
const dphi = twopi * delf * dt2;
|
|
2689
|
+
const twkRe = new Float64Array(32);
|
|
2690
|
+
const twkIm = new Float64Array(32);
|
|
2691
|
+
let phi = 0;
|
|
2692
|
+
for (let i = 0; i < 32; i++) {
|
|
2693
|
+
twkRe[i] = Math.cos(phi);
|
|
2694
|
+
twkIm[i] = Math.sin(phi);
|
|
2695
|
+
phi = (phi + dphi) % twopi;
|
|
2696
|
+
}
|
|
2697
|
+
const sync = sync8d(cd0Re, cd0Im, ibest, twkRe, twkIm, true);
|
|
2698
|
+
if (sync > smax) {
|
|
2699
|
+
smax = sync;
|
|
2700
|
+
delfbest = delf;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
// Apply frequency correction and re-downsample
|
|
2704
|
+
f1 += delfbest;
|
|
2705
|
+
ft8Downsample(cxRe, cxIm, f1, cd0Re, cd0Im);
|
|
2706
|
+
// Refine time offset
|
|
2707
|
+
const ss = new Float64Array(9);
|
|
2708
|
+
for (let idt = -4; idt <= 4; idt++) {
|
|
2709
|
+
ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt, null, null, false);
|
|
2710
|
+
}
|
|
2711
|
+
let maxss = -1;
|
|
2712
|
+
let maxIdx = 4;
|
|
2713
|
+
for (let i = 0; i < 9; i++) {
|
|
2714
|
+
if (ss[i] > maxss) {
|
|
2715
|
+
maxss = ss[i];
|
|
2716
|
+
maxIdx = i;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
ibest = ibest + maxIdx - 4;
|
|
2720
|
+
xdt = (ibest - 1) * dt2;
|
|
2721
|
+
// Extract 8-tone soft symbols for each of NN=79 symbols
|
|
2722
|
+
const s8 = new Float64Array(8 * NN);
|
|
2723
|
+
const csRe = new Float64Array(8 * NN);
|
|
2724
|
+
const csIm = new Float64Array(8 * NN);
|
|
2725
|
+
const symbRe = new Float64Array(32);
|
|
2726
|
+
const symbIm = new Float64Array(32);
|
|
2727
|
+
for (let k = 0; k < NN; k++) {
|
|
2728
|
+
const i1 = ibest + k * 32;
|
|
2729
|
+
symbRe.fill(0);
|
|
2730
|
+
symbIm.fill(0);
|
|
2731
|
+
if (i1 >= 0 && i1 + 31 < NP2) {
|
|
2732
|
+
for (let j = 0; j < 32; j++) {
|
|
2733
|
+
symbRe[j] = cd0Re[i1 + j];
|
|
2734
|
+
symbIm[j] = cd0Im[i1 + j];
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
fftComplex(symbRe, symbIm, false);
|
|
2738
|
+
for (let tone = 0; tone < 8; tone++) {
|
|
2739
|
+
const re = symbRe[tone] / 1000;
|
|
2740
|
+
const im = symbIm[tone] / 1000;
|
|
2741
|
+
csRe[tone * NN + k] = re;
|
|
2742
|
+
csIm[tone * NN + k] = im;
|
|
2743
|
+
s8[tone * NN + k] = Math.sqrt(re * re + im * im);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
// Sync quality check
|
|
2747
|
+
let nsync = 0;
|
|
2748
|
+
for (let k = 0; k < 7; k++) {
|
|
2749
|
+
for (const offset of [0, 36, 72]) {
|
|
2750
|
+
let maxTone = 0;
|
|
2751
|
+
let maxVal = -1;
|
|
2752
|
+
for (let t = 0; t < 8; t++) {
|
|
2753
|
+
const v = s8[t * NN + k + offset];
|
|
2754
|
+
if (v > maxVal) {
|
|
2755
|
+
maxVal = v;
|
|
2756
|
+
maxTone = t;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (maxTone === COSTAS[k])
|
|
2760
|
+
nsync++;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (nsync <= 6)
|
|
2764
|
+
return null;
|
|
2765
|
+
// Compute soft bit metrics for multiple nsym values (1, 2, 3)
|
|
2766
|
+
// and a normalized version, matching the Fortran ft8b passes 1-4
|
|
2767
|
+
const bmeta = new Float64Array(N_LDPC); // nsym=1
|
|
2768
|
+
const bmetb = new Float64Array(N_LDPC); // nsym=2
|
|
2769
|
+
const bmetc = new Float64Array(N_LDPC); // nsym=3
|
|
2770
|
+
const bmetd = new Float64Array(N_LDPC); // nsym=1 normalized
|
|
2771
|
+
for (let nsym = 1; nsym <= 3; nsym++) {
|
|
2772
|
+
const nt = 1 << (3 * nsym); // 8, 64, 512
|
|
2773
|
+
const ibmax = nsym === 1 ? 2 : nsym === 2 ? 5 : 8;
|
|
2774
|
+
for (let ihalf = 1; ihalf <= 2; ihalf++) {
|
|
2775
|
+
for (let k = 1; k <= 29; k += nsym) {
|
|
2776
|
+
const ks = ihalf === 1 ? k + 7 : k + 43;
|
|
2777
|
+
const s2 = new Float64Array(nt);
|
|
2778
|
+
for (let i = 0; i < nt; i++) {
|
|
2779
|
+
const i1 = Math.floor(i / 64);
|
|
2780
|
+
const i2 = Math.floor((i & 63) / 8);
|
|
2781
|
+
const i3 = i & 7;
|
|
2782
|
+
if (nsym === 1) {
|
|
2783
|
+
const re = csRe[GRAY_MAP[i3] * NN + ks - 1];
|
|
2784
|
+
const im = csIm[GRAY_MAP[i3] * NN + ks - 1];
|
|
2785
|
+
s2[i] = Math.sqrt(re * re + im * im);
|
|
2786
|
+
}
|
|
2787
|
+
else if (nsym === 2) {
|
|
2788
|
+
const sRe = csRe[GRAY_MAP[i2] * NN + ks - 1] + csRe[GRAY_MAP[i3] * NN + ks];
|
|
2789
|
+
const sIm = csIm[GRAY_MAP[i2] * NN + ks - 1] + csIm[GRAY_MAP[i3] * NN + ks];
|
|
2790
|
+
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
2791
|
+
}
|
|
2792
|
+
else {
|
|
2793
|
+
const sRe = csRe[GRAY_MAP[i1] * NN + ks - 1] +
|
|
2794
|
+
csRe[GRAY_MAP[i2] * NN + ks] +
|
|
2795
|
+
csRe[GRAY_MAP[i3] * NN + ks + 1];
|
|
2796
|
+
const sIm = csIm[GRAY_MAP[i1] * NN + ks - 1] +
|
|
2797
|
+
csIm[GRAY_MAP[i2] * NN + ks] +
|
|
2798
|
+
csIm[GRAY_MAP[i3] * NN + ks + 1];
|
|
2799
|
+
s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
// Fortran: i32 = 1 + (k-1)*3 + (ihalf-1)*87 (1-based)
|
|
2803
|
+
const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
|
|
2804
|
+
for (let ib = 0; ib <= ibmax; ib++) {
|
|
2805
|
+
// max of s2 where bit (ibmax-ib) of index is 1
|
|
2806
|
+
let max1 = -1e30, max0 = -1e30;
|
|
2807
|
+
for (let i = 0; i < nt; i++) {
|
|
2808
|
+
const bitSet = (i & (1 << (ibmax - ib))) !== 0;
|
|
2809
|
+
if (bitSet) {
|
|
2810
|
+
if (s2[i] > max1)
|
|
2811
|
+
max1 = s2[i];
|
|
2812
|
+
}
|
|
2813
|
+
else {
|
|
2814
|
+
if (s2[i] > max0)
|
|
2815
|
+
max0 = s2[i];
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
const idx = i32 + ib - 1; // Convert to 0-based
|
|
2819
|
+
if (idx >= 0 && idx < N_LDPC) {
|
|
2820
|
+
const bm = max1 - max0;
|
|
2821
|
+
if (nsym === 1) {
|
|
2822
|
+
bmeta[idx] = bm;
|
|
2823
|
+
const den = Math.max(max1, max0);
|
|
2824
|
+
bmetd[idx] = den > 0 ? bm / den : 0;
|
|
2825
|
+
}
|
|
2826
|
+
else if (nsym === 2) {
|
|
2827
|
+
bmetb[idx] = bm;
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
bmetc[idx] = bm;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
normalizeBmet(bmeta);
|
|
2838
|
+
normalizeBmet(bmetb);
|
|
2839
|
+
normalizeBmet(bmetc);
|
|
2840
|
+
normalizeBmet(bmetd);
|
|
2841
|
+
const bmetrics = [bmeta, bmetb, bmetc, bmetd];
|
|
2842
|
+
const scalefac = 2.83;
|
|
2843
|
+
const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
|
|
2844
|
+
const apmask = new Int8Array(N_LDPC);
|
|
2845
|
+
// Try 4 passes with different soft-symbol metrics (matching Fortran)
|
|
2846
|
+
let result = null;
|
|
2847
|
+
for (let ipass = 0; ipass < 4; ipass++) {
|
|
2848
|
+
const llr = new Float64Array(N_LDPC);
|
|
2849
|
+
for (let i = 0; i < N_LDPC; i++)
|
|
2850
|
+
llr[i] = scalefac * bmetrics[ipass][i];
|
|
2851
|
+
result = decode174_91(llr, apmask, maxosd);
|
|
2852
|
+
if (result && result.nharderrors >= 0 && result.nharderrors <= 36)
|
|
2853
|
+
break;
|
|
2854
|
+
result = null;
|
|
2182
2855
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2856
|
+
if (!result || result.nharderrors < 0 || result.nharderrors > 36)
|
|
2857
|
+
return null;
|
|
2858
|
+
// Check for all-zero codeword
|
|
2859
|
+
if (result.cw.every((b) => b === 0))
|
|
2860
|
+
return null;
|
|
2861
|
+
const message77 = result.message91.slice(0, 77);
|
|
2862
|
+
// Validate message type
|
|
2863
|
+
const n3v = (message77[71] << 2) | (message77[72] << 1) | message77[73];
|
|
2864
|
+
const i3v = (message77[74] << 2) | (message77[75] << 1) | message77[76];
|
|
2865
|
+
if (i3v > 5 || (i3v === 0 && n3v > 6))
|
|
2866
|
+
return null;
|
|
2867
|
+
if (i3v === 0 && n3v === 2)
|
|
2868
|
+
return null;
|
|
2869
|
+
// Unpack
|
|
2870
|
+
const { msg, success } = unpack77(message77, book);
|
|
2871
|
+
if (!success || msg.trim().length === 0)
|
|
2872
|
+
return null;
|
|
2873
|
+
// Estimate SNR
|
|
2874
|
+
let xsig = 0;
|
|
2875
|
+
let xnoi = 0;
|
|
2876
|
+
const itone = getTones(result.cw);
|
|
2877
|
+
for (let i = 0; i < 79; i++) {
|
|
2878
|
+
xsig += s8[itone[i] * NN + i] ** 2;
|
|
2879
|
+
const ios = (itone[i] + 4) % 7;
|
|
2880
|
+
xnoi += s8[ios * NN + i] ** 2;
|
|
2190
2881
|
}
|
|
2191
|
-
|
|
2882
|
+
let snr = 0.001;
|
|
2883
|
+
const arg = xsig / Math.max(xnoi, 1e-30) - 1.0;
|
|
2884
|
+
if (arg > 0.1)
|
|
2885
|
+
snr = arg;
|
|
2886
|
+
snr = 10 * Math.log10(snr) - 27.0;
|
|
2887
|
+
if (snr < -24)
|
|
2888
|
+
snr = -24;
|
|
2889
|
+
return { msg, freq: f1, dt: xdt, snr };
|
|
2192
2890
|
}
|
|
2193
|
-
function getTones(
|
|
2891
|
+
function getTones(cw) {
|
|
2194
2892
|
const tones = new Array(79).fill(0);
|
|
2195
2893
|
for (let i = 0; i < 7; i++)
|
|
2196
|
-
tones[i] =
|
|
2894
|
+
tones[i] = COSTAS[i];
|
|
2197
2895
|
for (let i = 0; i < 7; i++)
|
|
2198
|
-
tones[36 + i] =
|
|
2896
|
+
tones[36 + i] = COSTAS[i];
|
|
2199
2897
|
for (let i = 0; i < 7; i++)
|
|
2200
|
-
tones[72 + i] =
|
|
2898
|
+
tones[72 + i] = COSTAS[i];
|
|
2201
2899
|
let k = 7;
|
|
2202
2900
|
for (let j = 1; j <= 58; j++) {
|
|
2203
|
-
const i = j
|
|
2901
|
+
const i = (j - 1) * 3;
|
|
2204
2902
|
if (j === 30)
|
|
2205
2903
|
k += 7;
|
|
2206
|
-
const indx =
|
|
2207
|
-
tones[k] =
|
|
2904
|
+
const indx = cw[i] * 4 + cw[i + 1] * 2 + cw[i + 2];
|
|
2905
|
+
tones[k] = GRAY_MAP[indx];
|
|
2208
2906
|
k++;
|
|
2209
2907
|
}
|
|
2210
2908
|
return tones;
|
|
2211
2909
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2910
|
+
/**
|
|
2911
|
+
* Mix f0 to baseband and decimate by NDOWN (60x) by extracting frequency bins.
|
|
2912
|
+
* Identical to Fortran ft8_downsample.
|
|
2913
|
+
*/
|
|
2914
|
+
function ft8Downsample(cxRe, cxIm, f0, c1Re, c1Im) {
|
|
2915
|
+
const NFFT1 = 192000;
|
|
2916
|
+
const NFFT2 = 3200;
|
|
2917
|
+
const df = 12000.0 / NFFT1;
|
|
2918
|
+
// NSPS is imported, should be 1920
|
|
2919
|
+
const baud = 12000.0 / NSPS; // 6.25
|
|
2920
|
+
const i0 = Math.round(f0 / df);
|
|
2921
|
+
const ft = f0 + 8.5 * baud;
|
|
2922
|
+
const it = Math.min(Math.round(ft / df), NFFT1 / 2);
|
|
2923
|
+
const fb = f0 - 1.5 * baud;
|
|
2924
|
+
const ib = Math.max(1, Math.round(fb / df));
|
|
2925
|
+
c1Re.fill(0);
|
|
2926
|
+
c1Im.fill(0);
|
|
2927
|
+
let k = 0;
|
|
2928
|
+
for (let i = ib; i <= it; i++) {
|
|
2929
|
+
if (k >= NFFT2)
|
|
2930
|
+
break;
|
|
2931
|
+
c1Re[k] = cxRe[i] ?? 0;
|
|
2932
|
+
c1Im[k] = cxIm[i] ?? 0;
|
|
2933
|
+
k++;
|
|
2934
|
+
}
|
|
2935
|
+
// Taper
|
|
2936
|
+
const pi = Math.PI;
|
|
2937
|
+
const taper = new Float64Array(101);
|
|
2938
|
+
for (let i = 0; i <= 100; i++) {
|
|
2939
|
+
taper[i] = 0.5 * (1.0 + Math.cos((i * pi) / 100));
|
|
2940
|
+
}
|
|
2941
|
+
for (let i = 0; i <= 100; i++) {
|
|
2942
|
+
if (i >= NFFT2)
|
|
2943
|
+
break;
|
|
2944
|
+
const tap = taper[100 - i];
|
|
2945
|
+
c1Re[i] = c1Re[i] * tap;
|
|
2946
|
+
c1Im[i] = c1Im[i] * tap;
|
|
2947
|
+
}
|
|
2948
|
+
const endTap = k - 1;
|
|
2949
|
+
for (let i = 0; i <= 100; i++) {
|
|
2950
|
+
const idx = endTap - 100 + i;
|
|
2951
|
+
if (idx >= 0 && idx < NFFT2) {
|
|
2952
|
+
const tap = taper[i];
|
|
2953
|
+
c1Re[idx] = c1Re[idx] * tap;
|
|
2954
|
+
c1Im[idx] = c1Im[idx] * tap;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
// CSHIFT
|
|
2958
|
+
const shift = i0 - ib;
|
|
2959
|
+
const tempRe = new Float64Array(NFFT2);
|
|
2960
|
+
const tempIm = new Float64Array(NFFT2);
|
|
2961
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
2962
|
+
let srcIdx = (i + shift) % NFFT2;
|
|
2963
|
+
if (srcIdx < 0)
|
|
2964
|
+
srcIdx += NFFT2;
|
|
2965
|
+
tempRe[i] = c1Re[srcIdx];
|
|
2966
|
+
tempIm[i] = c1Im[srcIdx];
|
|
2967
|
+
}
|
|
2968
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
2969
|
+
c1Re[i] = tempRe[i];
|
|
2970
|
+
c1Im[i] = tempIm[i];
|
|
2971
|
+
}
|
|
2972
|
+
// iFFT
|
|
2973
|
+
fftComplex(c1Re, c1Im, true);
|
|
2974
|
+
// Scale
|
|
2975
|
+
// Fortran uses 1.0/sqrt(NFFT1 * NFFT2), but our fftComplex(true) scales by 1/NFFT2
|
|
2976
|
+
const scale = Math.sqrt(NFFT2 / NFFT1);
|
|
2977
|
+
for (let i = 0; i < NFFT2; i++) {
|
|
2978
|
+
c1Re[i] = c1Re[i] * scale;
|
|
2979
|
+
c1Im[i] = c1Im[i] * scale;
|
|
2980
|
+
}
|
|
2216
2981
|
}
|
|
2217
|
-
function
|
|
2218
|
-
|
|
2982
|
+
function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
|
|
2983
|
+
const NP2 = 2812;
|
|
2984
|
+
const twopi = 2 * Math.PI;
|
|
2985
|
+
// Precompute Costas sync waveforms
|
|
2986
|
+
const csyncRe = new Float64Array(7 * 32);
|
|
2987
|
+
const csyncIm = new Float64Array(7 * 32);
|
|
2988
|
+
for (let i = 0; i < 7; i++) {
|
|
2989
|
+
let phi = 0;
|
|
2990
|
+
const dphi = (twopi * COSTAS[i]) / 32;
|
|
2991
|
+
for (let j = 0; j < 32; j++) {
|
|
2992
|
+
csyncRe[i * 32 + j] = Math.cos(phi);
|
|
2993
|
+
csyncIm[i * 32 + j] = Math.sin(phi);
|
|
2994
|
+
phi = (phi + dphi) % twopi;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
let sync = 0;
|
|
2998
|
+
for (let i = 0; i < 7; i++) {
|
|
2999
|
+
const i1 = i0 + i * 32;
|
|
3000
|
+
const i2 = i1 + 36 * 32;
|
|
3001
|
+
const i3 = i1 + 72 * 32;
|
|
3002
|
+
for (const iStart of [i1, i2, i3]) {
|
|
3003
|
+
let zRe = 0, zIm = 0;
|
|
3004
|
+
if (iStart >= 0 && iStart + 31 < NP2) {
|
|
3005
|
+
for (let j = 0; j < 32; j++) {
|
|
3006
|
+
let sRe = csyncRe[i * 32 + j];
|
|
3007
|
+
let sIm = csyncIm[i * 32 + j];
|
|
3008
|
+
if (useTwk && twkRe && twkIm) {
|
|
3009
|
+
const tRe = twkRe[j] * sRe - twkIm[j] * sIm;
|
|
3010
|
+
const tIm = twkRe[j] * sIm + twkIm[j] * sRe;
|
|
3011
|
+
sRe = tRe;
|
|
3012
|
+
sIm = tIm;
|
|
3013
|
+
}
|
|
3014
|
+
// Conjugate multiply: cd0 * conj(csync)
|
|
3015
|
+
const dRe = cd0Re[iStart + j];
|
|
3016
|
+
const dIm = cd0Im[iStart + j];
|
|
3017
|
+
zRe += dRe * sRe + dIm * sIm;
|
|
3018
|
+
zIm += dIm * sRe - dRe * sIm;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
sync += zRe * zRe + zIm * zIm;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return sync;
|
|
3025
|
+
}
|
|
3026
|
+
function normalizeBmet(bmet) {
|
|
3027
|
+
const n = bmet.length;
|
|
3028
|
+
let sum = 0, sum2 = 0;
|
|
3029
|
+
for (let i = 0; i < n; i++) {
|
|
3030
|
+
sum += bmet[i];
|
|
3031
|
+
sum2 += bmet[i] * bmet[i];
|
|
3032
|
+
}
|
|
3033
|
+
const avg = sum / n;
|
|
3034
|
+
const avg2 = sum2 / n;
|
|
3035
|
+
const variance = avg2 - avg * avg;
|
|
3036
|
+
const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
|
|
3037
|
+
if (sigma > 0) {
|
|
3038
|
+
for (let i = 0; i < n; i++)
|
|
3039
|
+
bmet[i] = bmet[i] / sigma;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
function resample(input, fromRate, toRate, outLen) {
|
|
3043
|
+
const out = new Float64Array(outLen);
|
|
3044
|
+
const ratio = fromRate / toRate;
|
|
3045
|
+
for (let i = 0; i < outLen; i++) {
|
|
3046
|
+
const srcIdx = i * ratio;
|
|
3047
|
+
const lo = Math.floor(srcIdx);
|
|
3048
|
+
const frac = srcIdx - lo;
|
|
3049
|
+
const v0 = lo < input.length ? (input[lo] ?? 0) : 0;
|
|
3050
|
+
const v1 = lo + 1 < input.length ? (input[lo + 1] ?? 0) : 0;
|
|
3051
|
+
out[i] = v0 * (1 - frac) + v1 * frac;
|
|
3052
|
+
}
|
|
3053
|
+
return out;
|
|
2219
3054
|
}
|
|
2220
3055
|
|
|
2221
3056
|
/// <reference types="node" />
|
|
@@ -2341,6 +3176,7 @@ Usage:
|
|
|
2341
3176
|
ft8ts encode "<message>" [options]
|
|
2342
3177
|
|
|
2343
3178
|
Decode options:
|
|
3179
|
+
--mode <ft8|ft4> Mode: ft8 (default) or ft4
|
|
2344
3180
|
--low <hz> Lower frequency bound (default: 200)
|
|
2345
3181
|
--high <hz> Upper frequency bound (default: 3000)
|
|
2346
3182
|
--depth <1|2|3> Decoding depth (default: 2)
|
|
@@ -2364,9 +3200,19 @@ function runDecode(argv) {
|
|
|
2364
3200
|
}
|
|
2365
3201
|
const wavFile = argv[0];
|
|
2366
3202
|
const options = {};
|
|
3203
|
+
let mode = "ft8";
|
|
2367
3204
|
for (let i = 1; i < argv.length; i++) {
|
|
2368
3205
|
const arg = argv[i];
|
|
2369
|
-
if (arg === "--
|
|
3206
|
+
if (arg === "--mode") {
|
|
3207
|
+
const value = argv[++i];
|
|
3208
|
+
if (value === "ft8" || value === "ft4") {
|
|
3209
|
+
mode = value;
|
|
3210
|
+
}
|
|
3211
|
+
else {
|
|
3212
|
+
throw new Error(`Invalid --mode: ${value ?? "(missing)"}. Use ft8 or ft4`);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
else if (arg === "--low") {
|
|
2370
3216
|
options.freqLow = Number(argv[++i]);
|
|
2371
3217
|
}
|
|
2372
3218
|
else if (arg === "--high") {
|
|
@@ -2384,7 +3230,9 @@ function runDecode(argv) {
|
|
|
2384
3230
|
const { sampleRate, samples } = parseWavBuffer(readFileSync(filePath));
|
|
2385
3231
|
console.log(`WAV: ${sampleRate} Hz, ${samples.length} samples, ${(samples.length / sampleRate).toFixed(1)}s`);
|
|
2386
3232
|
const startTime = performance.now();
|
|
2387
|
-
const decoded =
|
|
3233
|
+
const decoded = mode === "ft4"
|
|
3234
|
+
? decode$1(samples, { ...options, sampleRate })
|
|
3235
|
+
: decode(samples, { ...options, sampleRate });
|
|
2388
3236
|
const elapsed = performance.now() - startTime;
|
|
2389
3237
|
console.log(`\nDecoded ${decoded.length} messages in ${(elapsed / 1000).toFixed(2)}s:\n`);
|
|
2390
3238
|
console.log(" dt snr freq message");
|