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