@e04/ft8ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ft8ts.cjs ADDED
@@ -0,0 +1,2119 @@
1
+ 'use strict';
2
+
3
+ const SAMPLE_RATE = 12_000;
4
+ const NSPS = 1920;
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;
11
+ const KK = 91;
12
+ const N_LDPC = 174;
13
+ 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
+ const gHex = [
17
+ "8329ce11bf31eaf509f27fc",
18
+ "761c264e25c259335493132",
19
+ "dc265902fb277c6410a1bdc",
20
+ "1b3f417858cd2dd33ec7f62",
21
+ "09fda4fee04195fd034783a",
22
+ "077cccc11b8873ed5c3d48a",
23
+ "29b62afe3ca036f4fe1a9da",
24
+ "6054faf5f35d96d3b0c8c3e",
25
+ "e20798e4310eed27884ae90",
26
+ "775c9c08e80e26ddae56318",
27
+ "b0b811028c2bf997213487c",
28
+ "18a0c9231fc60adf5c5ea32",
29
+ "76471e8302a0721e01b12b8",
30
+ "ffbccb80ca8341fafb47b2e",
31
+ "66a72a158f9325a2bf67170",
32
+ "c4243689fe85b1c51363a18",
33
+ "0dff739414d1a1b34b1c270",
34
+ "15b48830636c8b99894972e",
35
+ "29a89c0d3de81d665489b0e",
36
+ "4f126f37fa51cbe61bd6b94",
37
+ "99c47239d0d97d3c84e0940",
38
+ "1919b75119765621bb4f1e8",
39
+ "09db12d731faee0b86df6b8",
40
+ "488fc33df43fbdeea4eafb4",
41
+ "827423ee40b675f756eb5fe",
42
+ "abe197c484cb74757144a9a",
43
+ "2b500e4bc0ec5a6d2bdbdd0",
44
+ "c474aa53d70218761669360",
45
+ "8eba1a13db3390bd6718cec",
46
+ "753844673a27782cc42012e",
47
+ "06ff83a145c37035a5c1268",
48
+ "3b37417858cc2dd33ec3f62",
49
+ "9a4a5a28ee17ca9c324842c",
50
+ "bc29f465309c977e89610a4",
51
+ "2663ae6ddf8b5ce2bb29488",
52
+ "46f231efe457034c1814418",
53
+ "3fb2ce85abe9b0c72e06fbe",
54
+ "de87481f282c153971a0a2e",
55
+ "fcd7ccf23c69fa99bba1412",
56
+ "f0261447e9490ca8e474cec",
57
+ "4410115818196f95cdd7012",
58
+ "088fc31df4bfbde2a4eafb4",
59
+ "b8fef1b6307729fb0a078c0",
60
+ "5afea7acccb77bbc9d99a90",
61
+ "49a7016ac653f65ecdc9076",
62
+ "1944d085be4e7da8d6cc7d0",
63
+ "251f62adc4032f0ee714002",
64
+ "56471f8702a0721e00b12b8",
65
+ "2b8e4923f2dd51e2d537fa0",
66
+ "6b550a40a66f4755de95c26",
67
+ "a18ad28d4e27fe92a4f6c84",
68
+ "10c2e586388cb82a3d80758",
69
+ "ef34a41817ee02133db2eb0",
70
+ "7e9c0c54325a9c15836e000",
71
+ "3693e572d1fde4cdf079e86",
72
+ "bfb2cec5abe1b0c72e07fbe",
73
+ "7ee18230c583cccc57d4b08",
74
+ "a066cb2fedafc9f52664126",
75
+ "bb23725abc47cc5f4cc4cd2",
76
+ "ded9dba3bee40c59b5609b4",
77
+ "d9a7016ac653e6decdc9036",
78
+ "9ad46aed5f707f280ab5fc4",
79
+ "e5921c77822587316d7d3c2",
80
+ "4f14da8242a8b86dca73352",
81
+ "8b8b507ad467d4441df770e",
82
+ "22831c9cf1169467ad04b68",
83
+ "213b838fe2ae54c38ee7180",
84
+ "5d926b6dd71f085181a4e12",
85
+ "66ab79d4b29ee6e69509e56",
86
+ "958148682d748a38dd68baa",
87
+ "b8ce020cf069c32a723ab14",
88
+ "f4331d6d461607e95752746",
89
+ "6da23ba424b9596133cf9c8",
90
+ "a636bcbc7b30c5fbeae67fe",
91
+ "5cb0d86a07df654a9089a20",
92
+ "f11f106848780fc9ecdd80a",
93
+ "1fbb5364fb8d2c9d730d5ba",
94
+ "fcb86bc70a50c9d02a5d034",
95
+ "a534433029eac15f322e34c",
96
+ "c989d9c7c3d3b8c55d75130",
97
+ "7bb38b2f0186d46643ae962",
98
+ "2644ebadeb44b9467d1f42c",
99
+ "608cc857594bfbb55d69600",
100
+ ];
101
+ const FTALPH = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
102
+ const A1 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
103
+ const A2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
104
+ const A3 = "0123456789";
105
+ const A4 = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
106
+ const C38 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
107
+ const NTOKENS = 2063592;
108
+ const MAX22 = 4194304; // 2^22
109
+ const MAX28 = 268435456; // 2^28
110
+ const MAXGRID4 = 32400;
111
+
112
+ /**
113
+ * CRC-14 computation and checking, shared between encoder and decoder.
114
+ * Polynomial: 0x2757 (x^14 + x^13 + x^10 + x^9 + x^8 + x^6 + x^4 + x^2 + x + 1)
115
+ */
116
+ function computeCRC14(msg77) {
117
+ const poly = 0x2757;
118
+ let crc = 0;
119
+ const bitArray = [...msg77, 0, 0, 0, ...new Array(16).fill(0)];
120
+ for (let bit = 0; bit < 96; bit++) {
121
+ const nextBit = bitArray[bit];
122
+ if ((crc & 0x2000) !== 0) {
123
+ crc = ((crc << 1) | nextBit) ^ poly;
124
+ }
125
+ else {
126
+ crc = (crc << 1) | nextBit;
127
+ }
128
+ crc &= 0x3fff;
129
+ }
130
+ return crc;
131
+ }
132
+ /**
133
+ * Check CRC-14 of a 91-bit decoded message (77 message + 14 CRC).
134
+ * Returns true if CRC is valid.
135
+ */
136
+ function checkCRC14(bits91) {
137
+ const msg77 = bits91.slice(0, 77);
138
+ const receivedCRC = bitsToInt(bits91, 77, 14);
139
+ const computedCRC = computeCRC14(msg77);
140
+ return receivedCRC === computedCRC;
141
+ }
142
+ function bitsToInt(bits, offset, count) {
143
+ let val = 0;
144
+ for (let i = 0; i < count; i++) {
145
+ val = (val << 1) | (bits[offset + i] ?? 0);
146
+ }
147
+ return val;
148
+ }
149
+
150
+ /**
151
+ * LDPC (174,91) parity check matrix data from ldpc_174_91_c_parity.f90
152
+ *
153
+ * Mn[j] = list of 3 check-node indices (1-based) for bit j (j=0..173)
154
+ * Nm[i] = list of variable-node indices (1-based) for check i (i=0..82), padded with 0
155
+ * nrw[i] = row weight for check i
156
+ * ncw = 3 (column weight – every bit participates in exactly 3 checks)
157
+ */
158
+ // Mn: 174 rows, each with 3 check-node indices (1-based, from Fortran)
159
+ const MnFlat = [
160
+ 16, 45, 73, 25, 51, 62, 33, 58, 78, 1, 44, 45, 2, 7, 61, 3, 6, 54, 4, 35, 48, 5, 13, 21, 8, 56,
161
+ 79, 9, 64, 69, 10, 19, 66, 11, 36, 60, 12, 37, 58, 14, 32, 43, 15, 63, 80, 17, 28, 77, 18, 74, 83,
162
+ 22, 53, 81, 23, 30, 34, 24, 31, 40, 26, 41, 76, 27, 57, 70, 29, 49, 65, 3, 38, 78, 5, 39, 82, 46,
163
+ 50, 73, 51, 52, 74, 55, 71, 72, 44, 67, 72, 43, 68, 78, 1, 32, 59, 2, 6, 71, 4, 16, 54, 7, 65, 67,
164
+ 8, 30, 42, 9, 22, 31, 10, 18, 76, 11, 23, 82, 12, 28, 61, 13, 52, 79, 14, 50, 51, 15, 81, 83, 17,
165
+ 29, 60, 19, 33, 64, 20, 26, 73, 21, 34, 40, 24, 27, 77, 25, 55, 58, 35, 53, 66, 36, 48, 68, 37,
166
+ 46, 75, 38, 45, 47, 39, 57, 69, 41, 56, 62, 20, 49, 53, 46, 52, 63, 45, 70, 75, 27, 35, 80, 1, 15,
167
+ 30, 2, 68, 80, 3, 36, 51, 4, 28, 51, 5, 31, 56, 6, 20, 37, 7, 40, 82, 8, 60, 69, 9, 10, 49, 11,
168
+ 44, 57, 12, 39, 59, 13, 24, 55, 14, 21, 65, 16, 71, 78, 17, 30, 76, 18, 25, 80, 19, 61, 83, 22,
169
+ 38, 77, 23, 41, 50, 7, 26, 58, 29, 32, 81, 33, 40, 73, 18, 34, 48, 13, 42, 64, 5, 26, 43, 47, 69,
170
+ 72, 54, 55, 70, 45, 62, 68, 10, 63, 67, 14, 66, 72, 22, 60, 74, 35, 39, 79, 1, 46, 64, 1, 24, 66,
171
+ 2, 5, 70, 3, 31, 65, 4, 49, 58, 1, 4, 5, 6, 60, 67, 7, 32, 75, 8, 48, 82, 9, 35, 41, 10, 39, 62,
172
+ 11, 14, 61, 12, 71, 74, 13, 23, 78, 11, 35, 55, 15, 16, 79, 7, 9, 16, 17, 54, 63, 18, 50, 57, 19,
173
+ 30, 47, 20, 64, 80, 21, 28, 69, 22, 25, 43, 13, 22, 37, 2, 47, 51, 23, 54, 74, 26, 34, 72, 27, 36,
174
+ 37, 21, 36, 63, 29, 40, 44, 19, 26, 57, 3, 46, 82, 14, 15, 58, 33, 52, 53, 30, 43, 52, 6, 9, 52,
175
+ 27, 33, 65, 25, 69, 73, 38, 55, 83, 20, 39, 77, 18, 29, 56, 32, 48, 71, 42, 51, 59, 28, 44, 79,
176
+ 34, 60, 62, 31, 45, 61, 46, 68, 77, 6, 24, 76, 8, 10, 78, 40, 41, 70, 17, 50, 53, 42, 66, 68, 4,
177
+ 22, 72, 36, 64, 81, 13, 29, 47, 2, 8, 81, 56, 67, 73, 5, 38, 50, 12, 38, 64, 59, 72, 80, 3, 26,
178
+ 79, 45, 76, 81, 1, 65, 74, 7, 18, 77, 11, 56, 59, 14, 39, 54, 16, 37, 66, 10, 28, 55, 15, 60, 70,
179
+ 17, 25, 82, 20, 30, 31, 12, 67, 68, 23, 75, 80, 27, 32, 62, 24, 69, 75, 19, 21, 71, 34, 53, 61,
180
+ 35, 46, 47, 33, 59, 76, 40, 43, 83, 41, 42, 63, 49, 75, 83, 20, 44, 48, 42, 49, 57,
181
+ ];
182
+ // Nm: 83 rows, each with up to 7 variable-node indices (1-based, 0-padded)
183
+ const NmFlat = [
184
+ 4, 31, 59, 91, 92, 96, 153, 5, 32, 60, 93, 115, 146, 0, 6, 24, 61, 94, 122, 151, 0, 7, 33, 62, 95,
185
+ 96, 143, 0, 8, 25, 63, 83, 93, 96, 148, 6, 32, 64, 97, 126, 138, 0, 5, 34, 65, 78, 98, 107, 154,
186
+ 9, 35, 66, 99, 139, 146, 0, 10, 36, 67, 100, 107, 126, 0, 11, 37, 67, 87, 101, 139, 158, 12, 38,
187
+ 68, 102, 105, 155, 0, 13, 39, 69, 103, 149, 162, 0, 8, 40, 70, 82, 104, 114, 145, 14, 41, 71, 88,
188
+ 102, 123, 156, 15, 42, 59, 106, 123, 159, 0, 1, 33, 72, 106, 107, 157, 0, 16, 43, 73, 108, 141,
189
+ 160, 0, 17, 37, 74, 81, 109, 131, 154, 11, 44, 75, 110, 121, 166, 0, 45, 55, 64, 111, 130, 161,
190
+ 173, 8, 46, 71, 112, 119, 166, 0, 18, 36, 76, 89, 113, 114, 143, 19, 38, 77, 104, 116, 163, 0, 20,
191
+ 47, 70, 92, 138, 165, 0, 2, 48, 74, 113, 128, 160, 0, 21, 45, 78, 83, 117, 121, 151, 22, 47, 58,
192
+ 118, 127, 164, 0, 16, 39, 62, 112, 134, 158, 0, 23, 43, 79, 120, 131, 145, 0, 19, 35, 59, 73, 110,
193
+ 125, 161, 20, 36, 63, 94, 136, 161, 0, 14, 31, 79, 98, 132, 164, 0, 3, 44, 80, 124, 127, 169, 0,
194
+ 19, 46, 81, 117, 135, 167, 0, 7, 49, 58, 90, 100, 105, 168, 12, 50, 61, 118, 119, 144, 0, 13, 51,
195
+ 64, 114, 118, 157, 0, 24, 52, 76, 129, 148, 149, 0, 25, 53, 69, 90, 101, 130, 156, 20, 46, 65, 80,
196
+ 120, 140, 170, 21, 54, 77, 100, 140, 171, 0, 35, 82, 133, 142, 171, 174, 0, 14, 30, 83, 113, 125,
197
+ 170, 0, 4, 29, 68, 120, 134, 173, 0, 1, 4, 52, 57, 86, 136, 152, 26, 51, 56, 91, 122, 137, 168,
198
+ 52, 84, 110, 115, 145, 168, 0, 7, 50, 81, 99, 132, 173, 0, 23, 55, 67, 95, 172, 174, 0, 26, 41,
199
+ 77, 109, 141, 148, 0, 2, 27, 41, 61, 62, 115, 133, 27, 40, 56, 124, 125, 126, 0, 18, 49, 55, 124,
200
+ 141, 167, 0, 6, 33, 85, 108, 116, 156, 0, 28, 48, 70, 85, 105, 129, 158, 9, 54, 63, 131, 147, 155,
201
+ 0, 22, 53, 68, 109, 121, 174, 0, 3, 13, 48, 78, 95, 123, 0, 31, 69, 133, 150, 155, 169, 0, 12, 43,
202
+ 66, 89, 97, 135, 159, 5, 39, 75, 102, 136, 167, 0, 2, 54, 86, 101, 135, 164, 0, 15, 56, 87, 108,
203
+ 119, 171, 0, 10, 44, 82, 91, 111, 144, 149, 23, 34, 71, 94, 127, 153, 0, 11, 49, 88, 92, 142, 157,
204
+ 0, 29, 34, 87, 97, 147, 162, 0, 30, 50, 60, 86, 137, 142, 162, 10, 53, 66, 84, 112, 128, 165, 22,
205
+ 57, 85, 93, 140, 159, 0, 28, 32, 72, 103, 132, 166, 0, 28, 29, 84, 88, 117, 143, 150, 1, 26, 45,
206
+ 80, 128, 147, 0, 17, 27, 89, 103, 116, 153, 0, 51, 57, 98, 163, 165, 172, 0, 21, 37, 73, 138, 152,
207
+ 169, 0, 16, 47, 76, 130, 137, 154, 0, 3, 24, 30, 72, 104, 139, 0, 9, 40, 90, 106, 134, 151, 0, 15,
208
+ 58, 60, 74, 111, 150, 163, 18, 42, 79, 144, 146, 152, 0, 25, 38, 65, 99, 122, 160, 0, 17, 42, 75,
209
+ 129, 170, 172, 0,
210
+ ];
211
+ const nrwData = [
212
+ 7, 6, 6, 6, 7, 6, 7, 6, 6, 7, 6, 6, 7, 7, 6, 6, 6, 7, 6, 7, 6, 7, 6, 6, 6, 7, 6, 6, 6, 7, 6, 6, 6,
213
+ 6, 7, 6, 6, 6, 7, 7, 6, 6, 6, 6, 7, 7, 6, 6, 6, 6, 7, 6, 6, 6, 7, 6, 6, 6, 6, 7, 6, 6, 6, 7, 6, 6,
214
+ 6, 7, 7, 6, 6, 7, 6, 6, 6, 6, 6, 6, 6, 7, 6, 6, 6,
215
+ ];
216
+ const ncw = 3;
217
+ /** Mn[j] = check indices (0-based) for bit j (0..173). Each entry has exactly 3 elements. */
218
+ const Mn = [];
219
+ for (let j = 0; j < 174; j++) {
220
+ Mn.push([MnFlat[j * 3] - 1, MnFlat[j * 3 + 1] - 1, MnFlat[j * 3 + 2] - 1]);
221
+ }
222
+ /** Nm[i] = bit indices (0-based) for check i (0..82). Variable length (nrw[i] elements). */
223
+ const Nm = [];
224
+ /** nrw[i] = row weight for check i */
225
+ const nrw = nrwData.slice();
226
+ for (let i = 0; i < 83; i++) {
227
+ const row = [];
228
+ for (let k = 0; k < 7; k++) {
229
+ const v = NmFlat[i * 7 + k];
230
+ if (v !== 0)
231
+ row.push(v - 1);
232
+ }
233
+ Nm.push(row);
234
+ }
235
+
236
+ /**
237
+ * LDPC (174,91) Belief Propagation decoder for FT8.
238
+ * Port of bpdecode174_91.f90 and decode174_91.f90.
239
+ */
240
+ function platanh(x) {
241
+ if (x > 0.9999999)
242
+ return 18.71;
243
+ if (x < -0.9999999)
244
+ return -18.71;
245
+ return 0.5 * Math.log((1 + x) / (1 - x));
246
+ }
247
+ /**
248
+ * BP decoder for (174,91) LDPC code.
249
+ * llr: log-likelihood ratios (174 values, positive = bit more likely 0)
250
+ * apmask: AP mask (174 values, 1 = a priori bit, don't update from check messages)
251
+ * maxIterations: max BP iterations
252
+ * Returns null if decoding fails, otherwise { message91, cw, nharderrors }
253
+ */
254
+ function bpDecode174_91(llr, apmask, maxIterations) {
255
+ const N = N_LDPC;
256
+ const M = M_LDPC;
257
+ const tov = new Float64Array(ncw * N);
258
+ const toc = new Float64Array(7 * M);
259
+ const tanhtoc = new Float64Array(7 * M);
260
+ const zn = new Float64Array(N);
261
+ const cw = new Int8Array(N);
262
+ // Initialize messages to checks
263
+ for (let j = 0; j < M; j++) {
264
+ const w = nrw[j];
265
+ for (let i = 0; i < w; i++) {
266
+ toc[i * M + j] = llr[Nm[j][i]];
267
+ }
268
+ }
269
+ let nclast = 0;
270
+ let ncnt = 0;
271
+ for (let iter = 0; iter <= maxIterations; iter++) {
272
+ // Update bit LLRs
273
+ for (let i = 0; i < N; i++) {
274
+ if (apmask[i] !== 1) {
275
+ let sum = 0;
276
+ for (let k = 0; k < ncw; k++)
277
+ sum += tov[k * N + i];
278
+ zn[i] = llr[i] + sum;
279
+ }
280
+ else {
281
+ zn[i] = llr[i];
282
+ }
283
+ }
284
+ // Hard decision
285
+ for (let i = 0; i < N; i++)
286
+ cw[i] = zn[i] > 0 ? 1 : 0;
287
+ // Check parity
288
+ let ncheck = 0;
289
+ for (let i = 0; i < M; i++) {
290
+ const w = nrw[i];
291
+ let s = 0;
292
+ for (let k = 0; k < w; k++)
293
+ s += cw[Nm[i][k]];
294
+ if (s % 2 !== 0)
295
+ ncheck++;
296
+ }
297
+ if (ncheck === 0) {
298
+ const bits91 = Array.from(cw.slice(0, KK));
299
+ if (checkCRC14(bits91)) {
300
+ let nharderrors = 0;
301
+ for (let i = 0; i < N; i++) {
302
+ if ((2 * cw[i] - 1) * llr[i] < 0)
303
+ nharderrors++;
304
+ }
305
+ return {
306
+ message91: bits91,
307
+ cw: Array.from(cw),
308
+ nharderrors,
309
+ dmin: 0,
310
+ ntype: 1,
311
+ };
312
+ }
313
+ }
314
+ // Early stopping
315
+ if (iter > 0) {
316
+ const nd = ncheck - nclast;
317
+ if (nd < 0) {
318
+ ncnt = 0;
319
+ }
320
+ else {
321
+ ncnt++;
322
+ }
323
+ if (ncnt >= 5 && iter >= 10 && ncheck > 15)
324
+ return null;
325
+ }
326
+ nclast = ncheck;
327
+ // Send messages from bits to check nodes
328
+ for (let j = 0; j < M; j++) {
329
+ const w = nrw[j];
330
+ for (let i = 0; i < w; i++) {
331
+ const ibj = Nm[j][i];
332
+ let val = zn[ibj];
333
+ for (let kk = 0; kk < ncw; kk++) {
334
+ if (Mn[ibj][kk] === j) {
335
+ val -= tov[kk * N + ibj];
336
+ }
337
+ }
338
+ toc[i * M + j] = val;
339
+ }
340
+ }
341
+ // Send messages from check nodes to variable nodes
342
+ for (let i = 0; i < M; i++) {
343
+ for (let k = 0; k < 7; k++) {
344
+ tanhtoc[k * M + i] = Math.tanh(-toc[k * M + i] / 2);
345
+ }
346
+ }
347
+ for (let j = 0; j < N; j++) {
348
+ for (let i = 0; i < ncw; i++) {
349
+ const ichk = Mn[j][i];
350
+ const w = nrw[ichk];
351
+ let Tmn = 1.0;
352
+ for (let k = 0; k < w; k++) {
353
+ if (Nm[ichk][k] !== j) {
354
+ Tmn *= tanhtoc[k * M + ichk];
355
+ }
356
+ }
357
+ tov[i * N + j] = 2 * platanh(-Tmn);
358
+ }
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ /**
364
+ * Hybrid BP + OSD-like decoder for (174,91) code.
365
+ * Tries BP first, then falls back to OSD approach for deeper decoding.
366
+ */
367
+ function decode174_91(llr, apmask, maxosd) {
368
+ const maxIterations = 30;
369
+ // Try BP decoding
370
+ const bpResult = bpDecode174_91(llr, apmask, maxIterations);
371
+ if (bpResult)
372
+ return bpResult;
373
+ // OSD-0 fallback: try hard-decision with bit flipping for most unreliable bits
374
+ if (maxosd >= 0) {
375
+ return osdDecode174_91(llr, apmask, maxosd >= 1 ? 2 : 1);
376
+ }
377
+ return null;
378
+ }
379
+ /**
380
+ * Simplified OSD decoder for (174,91) code.
381
+ * Uses ordered statistics approach: sort bits by reliability,
382
+ * do Gaussian elimination, try flipping least reliable info bits.
383
+ */
384
+ function osdDecode174_91(llr, apmask, norder) {
385
+ const N = N_LDPC;
386
+ const K = KK;
387
+ const gen = getGenerator();
388
+ // Sort by reliability (descending)
389
+ const indices = Array.from({ length: N }, (_, i) => i);
390
+ indices.sort((a, b) => Math.abs(llr[b]) - Math.abs(llr[a]));
391
+ // Reorder generator matrix columns
392
+ const genmrb = new Uint8Array(K * N);
393
+ for (let i = 0; i < N; i++) {
394
+ for (let k = 0; k < K; k++) {
395
+ genmrb[k * N + i] = gen[k * N + indices[i]];
396
+ }
397
+ }
398
+ // Gaussian elimination to get systematic form on the K most-reliable bits
399
+ for (let id = 0; id < K; id++) {
400
+ let found = false;
401
+ for (let icol = id; icol < Math.min(K + 20, N); icol++) {
402
+ if (genmrb[id * N + icol] === 1) {
403
+ if (icol !== id) {
404
+ // Swap columns
405
+ for (let k = 0; k < K; k++) {
406
+ const tmp = genmrb[k * N + id];
407
+ genmrb[k * N + id] = genmrb[k * N + icol];
408
+ genmrb[k * N + icol] = tmp;
409
+ }
410
+ const tmp = indices[id];
411
+ indices[id] = indices[icol];
412
+ indices[icol] = tmp;
413
+ }
414
+ for (let ii = 0; ii < K; ii++) {
415
+ if (ii !== id && genmrb[ii * N + id] === 1) {
416
+ for (let c = 0; c < N; c++) {
417
+ genmrb[ii * N + c] ^= genmrb[id * N + c];
418
+ }
419
+ }
420
+ }
421
+ found = true;
422
+ break;
423
+ }
424
+ }
425
+ if (!found)
426
+ return null;
427
+ }
428
+ // Hard decisions on reordered received word
429
+ const hdec = new Int8Array(N);
430
+ for (let i = 0; i < N; i++) {
431
+ hdec[i] = llr[indices[i]] >= 0 ? 1 : 0;
432
+ }
433
+ const absrx = new Float64Array(N);
434
+ for (let i = 0; i < N; i++) {
435
+ absrx[i] = Math.abs(llr[indices[i]]);
436
+ }
437
+ // Transpose of reordered gen matrix
438
+ const g2 = new Uint8Array(N * K);
439
+ for (let i = 0; i < K; i++) {
440
+ for (let j = 0; j < N; j++) {
441
+ g2[j * K + i] = genmrb[i * N + j];
442
+ }
443
+ }
444
+ function mrbencode(me) {
445
+ const codeword = new Int8Array(N);
446
+ for (let i = 0; i < K; i++) {
447
+ if (me[i] === 1) {
448
+ for (let j = 0; j < N; j++) {
449
+ codeword[j] ^= g2[j * K + i];
450
+ }
451
+ }
452
+ }
453
+ return codeword;
454
+ }
455
+ const m0 = hdec.slice(0, K);
456
+ const c0 = mrbencode(m0);
457
+ const bestCw = new Int8Array(c0);
458
+ let dmin = 0;
459
+ for (let i = 0; i < N; i++) {
460
+ const x = c0[i] ^ hdec[i];
461
+ dmin += x * absrx[i];
462
+ }
463
+ // Order-1: flip single bits in the info portion
464
+ for (let i1 = K - 1; i1 >= 0; i1--) {
465
+ if (apmask[indices[i1]] === 1)
466
+ continue;
467
+ const me = new Int8Array(m0);
468
+ me[i1] ^= 1;
469
+ const ce = mrbencode(me);
470
+ let dd = 0;
471
+ for (let j = 0; j < N; j++) {
472
+ const x = ce[j] ^ hdec[j];
473
+ dd += x * absrx[j];
474
+ }
475
+ if (dd < dmin) {
476
+ dmin = dd;
477
+ bestCw.set(ce);
478
+ }
479
+ }
480
+ // Order-2: flip pairs of least-reliable info bits (limited search)
481
+ if (norder >= 2) {
482
+ const ntry = Math.min(40, K);
483
+ for (let i1 = K - 1; i1 >= K - ntry; i1--) {
484
+ if (apmask[indices[i1]] === 1)
485
+ continue;
486
+ for (let i2 = i1 - 1; i2 >= K - ntry; i2--) {
487
+ if (apmask[indices[i2]] === 1)
488
+ continue;
489
+ const me = new Int8Array(m0);
490
+ me[i1] ^= 1;
491
+ me[i2] ^= 1;
492
+ const ce = mrbencode(me);
493
+ let dd = 0;
494
+ for (let j = 0; j < N; j++) {
495
+ const x = ce[j] ^ hdec[j];
496
+ dd += x * absrx[j];
497
+ }
498
+ if (dd < dmin) {
499
+ dmin = dd;
500
+ bestCw.set(ce);
501
+ }
502
+ }
503
+ }
504
+ }
505
+ // Reorder codeword back to original order
506
+ const finalCw = new Int8Array(N);
507
+ for (let i = 0; i < N; i++) {
508
+ finalCw[indices[i]] = bestCw[i];
509
+ }
510
+ const bits91 = Array.from(finalCw.slice(0, KK));
511
+ if (!checkCRC14(bits91))
512
+ return null;
513
+ // Compute dmin in original order
514
+ let dminOrig = 0;
515
+ const hdecOrig = new Int8Array(N);
516
+ for (let i = 0; i < N; i++)
517
+ hdecOrig[i] = llr[i] >= 0 ? 1 : 0;
518
+ let nhe = 0;
519
+ for (let i = 0; i < N; i++) {
520
+ const x = finalCw[i] ^ hdecOrig[i];
521
+ nhe += x;
522
+ dminOrig += x * Math.abs(llr[i]);
523
+ }
524
+ return {
525
+ message91: bits91,
526
+ cw: Array.from(finalCw),
527
+ nharderrors: nhe,
528
+ dmin: dminOrig,
529
+ ntype: 2,
530
+ };
531
+ }
532
+ let _generator = null;
533
+ function getGenerator() {
534
+ if (_generator)
535
+ return _generator;
536
+ const K = KK;
537
+ const N = N_LDPC;
538
+ const M = M_LDPC;
539
+ // Build full generator matrix (K×N) where first K columns are identity
540
+ const gen = new Uint8Array(K * N);
541
+ for (let i = 0; i < K; i++)
542
+ gen[i * N + i] = 1;
543
+ // gHex encodes the M×K generator parity matrix
544
+ // gen_parity[m][k] = 1 means info bit k contributes to parity bit m
545
+ for (let m = 0; m < M; m++) {
546
+ const hexStr = gHex[m];
547
+ for (let j = 0; j < 23; j++) {
548
+ const val = parseInt(hexStr[j], 16);
549
+ const limit = j === 22 ? 3 : 4;
550
+ for (let jj = 1; jj <= limit; jj++) {
551
+ const col = j * 4 + jj - 1;
552
+ if (col < K && (val & (1 << (4 - jj))) !== 0) {
553
+ // For info bit `col`, parity bit `m` is set
554
+ gen[col * N + K + m] = 1;
555
+ }
556
+ }
557
+ }
558
+ }
559
+ _generator = gen;
560
+ return gen;
561
+ }
562
+
563
+ /**
564
+ * Radix-2 Cooley-Tukey FFT for FT8 decoding.
565
+ * Supports real-to-complex, complex-to-complex, and inverse transforms.
566
+ */
567
+ function fftComplex(re, im, inverse) {
568
+ const n = re.length;
569
+ if (n <= 1)
570
+ return;
571
+ // Bit-reversal permutation
572
+ let j = 0;
573
+ for (let i = 0; i < n; i++) {
574
+ if (j > i) {
575
+ let tmp = re[i];
576
+ re[i] = re[j];
577
+ re[j] = tmp;
578
+ tmp = im[i];
579
+ im[i] = im[j];
580
+ im[j] = tmp;
581
+ }
582
+ let m = n >> 1;
583
+ while (m >= 1 && j >= m) {
584
+ j -= m;
585
+ m >>= 1;
586
+ }
587
+ j += m;
588
+ }
589
+ const sign = -1;
590
+ for (let size = 2; size <= n; size <<= 1) {
591
+ const halfsize = size >> 1;
592
+ const step = (sign * Math.PI) / halfsize;
593
+ const wRe = Math.cos(step);
594
+ const wIm = Math.sin(step);
595
+ for (let i = 0; i < n; i += size) {
596
+ let curRe = 1;
597
+ let curIm = 0;
598
+ for (let k = 0; k < halfsize; k++) {
599
+ const evenIdx = i + k;
600
+ const oddIdx = i + k + halfsize;
601
+ const tRe = curRe * re[oddIdx] - curIm * im[oddIdx];
602
+ const tIm = curRe * im[oddIdx] + curIm * re[oddIdx];
603
+ re[oddIdx] = re[evenIdx] - tRe;
604
+ im[oddIdx] = im[evenIdx] - tIm;
605
+ re[evenIdx] = re[evenIdx] + tRe;
606
+ im[evenIdx] = im[evenIdx] + tIm;
607
+ const newCurRe = curRe * wRe - curIm * wIm;
608
+ curIm = curRe * wIm + curIm * wRe;
609
+ curRe = newCurRe;
610
+ }
611
+ }
612
+ }
613
+ }
614
+ /** Next power of 2 >= n */
615
+ function nextPow2(n) {
616
+ let v = 1;
617
+ while (v < n)
618
+ v <<= 1;
619
+ return v;
620
+ }
621
+
622
+ /**
623
+ * FT8 message unpacking – TypeScript port of unpack77 from packjt77.f90
624
+ *
625
+ * Supported message types:
626
+ * Type 0.0 Free text
627
+ * Type 1 Standard (two callsigns + grid/report/RR73/73)
628
+ * Type 2 /P form for EU VHF contest
629
+ * Type 4 One nonstandard call and one hashed call
630
+ */
631
+ function bitsToUint(bits, start, len) {
632
+ let val = 0;
633
+ for (let i = 0; i < len; i++) {
634
+ val = val * 2 + (bits[start + i] ?? 0);
635
+ }
636
+ return val;
637
+ }
638
+ function unpack28(n28) {
639
+ if (n28 < 0 || n28 >= 268435456)
640
+ return { call: "", success: false };
641
+ if (n28 === 0)
642
+ return { call: "DE", success: true };
643
+ if (n28 === 1)
644
+ return { call: "QRZ", success: true };
645
+ if (n28 === 2)
646
+ return { call: "CQ", success: true };
647
+ if (n28 >= 3 && n28 < 3 + 1000) {
648
+ const nqsy = n28 - 3;
649
+ return { call: `CQ ${nqsy.toString().padStart(3, "0")}`, success: true };
650
+ }
651
+ if (n28 >= 1003 && n28 < NTOKENS) {
652
+ // CQ with 4-letter directed call
653
+ let m = n28 - 1003;
654
+ let chars = "";
655
+ for (let i = 3; i >= 0; i--) {
656
+ const j = m % 27;
657
+ m = Math.floor(m / 27);
658
+ chars = (j === 0 ? " " : String.fromCharCode(64 + j)) + chars;
659
+ }
660
+ const directed = chars.trim();
661
+ if (directed.length > 0)
662
+ return { call: `CQ ${directed}`, success: true };
663
+ return { call: "CQ", success: true };
664
+ }
665
+ if (n28 >= NTOKENS && n28 < NTOKENS + MAX22) {
666
+ // Hashed call – we don't have a hash table, so show <...>
667
+ return { call: "<...>", success: true };
668
+ }
669
+ // Standard callsign
670
+ let n = n28 - NTOKENS - MAX22;
671
+ if (n < 0)
672
+ return { call: "", success: false };
673
+ const i6 = n % 27;
674
+ n = Math.floor(n / 27);
675
+ const i5 = n % 27;
676
+ n = Math.floor(n / 27);
677
+ const i4 = n % 27;
678
+ n = Math.floor(n / 27);
679
+ const i3 = n % 10;
680
+ n = Math.floor(n / 10);
681
+ const i2 = n % 36;
682
+ n = Math.floor(n / 36);
683
+ const i1 = n;
684
+ if (i1 < 0 || i1 >= A1.length)
685
+ return { call: "", success: false };
686
+ if (i2 < 0 || i2 >= A2.length)
687
+ return { call: "", success: false };
688
+ if (i3 < 0 || i3 >= A3.length)
689
+ return { call: "", success: false };
690
+ if (i4 < 0 || i4 >= A4.length)
691
+ return { call: "", success: false };
692
+ if (i5 < 0 || i5 >= A4.length)
693
+ return { call: "", success: false };
694
+ if (i6 < 0 || i6 >= A4.length)
695
+ return { call: "", success: false };
696
+ const call = (A1[i1] + A2[i2] + A3[i3] + A4[i4] + A4[i5] + A4[i6]).trim();
697
+ return { call, success: call.length > 0 };
698
+ }
699
+ function toGrid4(igrid4) {
700
+ if (igrid4 < 0 || igrid4 > MAXGRID4)
701
+ return { grid: "", success: false };
702
+ let n = igrid4;
703
+ const j4 = n % 10;
704
+ n = Math.floor(n / 10);
705
+ const j3 = n % 10;
706
+ n = Math.floor(n / 10);
707
+ const j2 = n % 18;
708
+ n = Math.floor(n / 18);
709
+ const j1 = n;
710
+ if (j1 < 0 || j1 > 17 || j2 < 0 || j2 > 17)
711
+ return { grid: "", success: false };
712
+ const grid = String.fromCharCode(65 + j1) + String.fromCharCode(65 + j2) + j3.toString() + j4.toString();
713
+ return { grid, success: true };
714
+ }
715
+ function unpackText77(bits71) {
716
+ // Reconstruct 9 bytes from 71 bits (7 + 8*8)
717
+ const qa = new Uint8Array(9);
718
+ let val = 0;
719
+ for (let b = 6; b >= 0; b--) {
720
+ val = (val << 1) | (bits71[6 - b] ?? 0);
721
+ }
722
+ qa[0] = val;
723
+ for (let li = 1; li <= 8; li++) {
724
+ val = 0;
725
+ for (let b = 7; b >= 0; b--) {
726
+ val = (val << 1) | (bits71[7 + (li - 1) * 8 + (7 - b)] ?? 0);
727
+ }
728
+ qa[li] = val;
729
+ }
730
+ // Decode from base-42 big-endian
731
+ // Convert qa (9 bytes) to a bigint, then repeatedly divide by 42
732
+ let n = 0n;
733
+ for (let i = 0; i < 9; i++) {
734
+ n = (n << 8n) | BigInt(qa[i]);
735
+ }
736
+ const chars = [];
737
+ for (let i = 0; i < 13; i++) {
738
+ const j = Number(n % 42n);
739
+ n = n / 42n;
740
+ chars.unshift(FTALPH[j] ?? " ");
741
+ }
742
+ return chars.join("").trimStart();
743
+ }
744
+ /**
745
+ * Unpack a 77-bit FT8 message into a human-readable string.
746
+ */
747
+ function unpack77(bits77) {
748
+ const n3 = bitsToUint(bits77, 71, 3);
749
+ const i3 = bitsToUint(bits77, 74, 3);
750
+ if (i3 === 0 && n3 === 0) {
751
+ // Type 0.0: Free text
752
+ const msg = unpackText77(bits77.slice(0, 71));
753
+ if (msg.trim().length === 0)
754
+ return { msg: "", success: false };
755
+ return { msg: msg.trim(), success: true };
756
+ }
757
+ if (i3 === 1 || i3 === 2) {
758
+ // Type 1/2: Standard message
759
+ const n28a = bitsToUint(bits77, 0, 28);
760
+ const ipa = bits77[28];
761
+ const n28b = bitsToUint(bits77, 29, 28);
762
+ const ipb = bits77[57];
763
+ const ir = bits77[58];
764
+ const igrid4 = bitsToUint(bits77, 59, 15);
765
+ const { call: call1, success: ok1 } = unpack28(n28a);
766
+ const { call: call2Raw, success: ok2 } = unpack28(n28b);
767
+ if (!ok1 || !ok2)
768
+ return { msg: "", success: false };
769
+ let c1 = call1;
770
+ let c2 = call2Raw;
771
+ if (c1.startsWith("CQ_"))
772
+ c1 = c1.replace("_", " ");
773
+ if (c1.indexOf("<") < 0) {
774
+ if (ipa === 1 && i3 === 1 && c1.length >= 3)
775
+ c1 += "/R";
776
+ if (ipa === 1 && i3 === 2 && c1.length >= 3)
777
+ c1 += "/P";
778
+ }
779
+ if (c2.indexOf("<") < 0) {
780
+ if (ipb === 1 && i3 === 1 && c2.length >= 3)
781
+ c2 += "/R";
782
+ if (ipb === 1 && i3 === 2 && c2.length >= 3)
783
+ c2 += "/P";
784
+ }
785
+ if (igrid4 <= MAXGRID4) {
786
+ const { grid, success: gridOk } = toGrid4(igrid4);
787
+ if (!gridOk)
788
+ return { msg: "", success: false };
789
+ const msg = ir === 0 ? `${c1} ${c2} ${grid}` : `${c1} ${c2} R ${grid}`;
790
+ return { msg, success: true };
791
+ }
792
+ else {
793
+ const irpt = igrid4 - MAXGRID4;
794
+ if (irpt === 1)
795
+ return { msg: `${c1} ${c2}`, success: true };
796
+ if (irpt === 2)
797
+ return { msg: `${c1} ${c2} RRR`, success: true };
798
+ if (irpt === 3)
799
+ return { msg: `${c1} ${c2} RR73`, success: true };
800
+ if (irpt === 4)
801
+ return { msg: `${c1} ${c2} 73`, success: true };
802
+ if (irpt >= 5) {
803
+ let isnr = irpt - 35;
804
+ if (isnr > 50)
805
+ isnr -= 101;
806
+ const absStr = Math.abs(isnr).toString().padStart(2, "0");
807
+ const crpt = (isnr >= 0 ? "+" : "-") + absStr;
808
+ const msg = ir === 0 ? `${c1} ${c2} ${crpt}` : `${c1} ${c2} R${crpt}`;
809
+ return { msg, success: true };
810
+ }
811
+ return { msg: "", success: false };
812
+ }
813
+ }
814
+ if (i3 === 4) {
815
+ // Type 4: One nonstandard call
816
+ let n58 = 0n;
817
+ for (let i = 0; i < 58; i++) {
818
+ n58 = n58 * 2n + BigInt(bits77[12 + i] ?? 0);
819
+ }
820
+ const iflip = bits77[70];
821
+ const nrpt = bitsToUint(bits77, 71, 2);
822
+ const icq = bits77[73];
823
+ // Decode n58 to 11-char string using C38 alphabet
824
+ const c11chars = [];
825
+ let remain = n58;
826
+ for (let i = 10; i >= 0; i--) {
827
+ const j = Number(remain % 38n);
828
+ remain = remain / 38n;
829
+ c11chars.unshift(C38[j] ?? " ");
830
+ }
831
+ const c11 = c11chars.join("").trim();
832
+ const call3 = "<...>"; // We don't have a hash table for n12
833
+ let call1;
834
+ let call2;
835
+ if (iflip === 0) {
836
+ call1 = call3;
837
+ call2 = c11;
838
+ }
839
+ else {
840
+ call1 = c11;
841
+ call2 = call3;
842
+ }
843
+ let msg;
844
+ if (icq === 1) {
845
+ msg = `CQ ${call2}`;
846
+ }
847
+ else {
848
+ if (nrpt === 0)
849
+ msg = `${call1} ${call2}`;
850
+ else if (nrpt === 1)
851
+ msg = `${call1} ${call2} RRR`;
852
+ else if (nrpt === 2)
853
+ msg = `${call1} ${call2} RR73`;
854
+ else
855
+ msg = `${call1} ${call2} 73`;
856
+ }
857
+ return { msg, success: true };
858
+ }
859
+ return { msg: "", success: false };
860
+ }
861
+
862
+ /**
863
+ * Decode all FT8 signals in an audio buffer.
864
+ * Input: mono audio samples at `sampleRate` Hz, duration ~15s.
865
+ */
866
+ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
867
+ const nfa = options.freqLow ?? 200;
868
+ const nfb = options.freqHigh ?? 3000;
869
+ const syncmin = options.syncMin ?? 1.2;
870
+ const depth = options.depth ?? 2;
871
+ const maxCandidates = options.maxCandidates ?? 300;
872
+ // Resample to 12000 Hz if needed
873
+ let dd;
874
+ if (sampleRate === SAMPLE_RATE) {
875
+ dd = new Float64Array(NMAX);
876
+ const len = Math.min(samples.length, NMAX);
877
+ for (let i = 0; i < len; i++)
878
+ dd[i] = samples[i];
879
+ }
880
+ else {
881
+ dd = resample(samples, sampleRate, SAMPLE_RATE, NMAX);
882
+ }
883
+ // Compute spectrogram and find sync candidates
884
+ const { candidates, sbase } = sync8(dd, nfa, nfb, syncmin, maxCandidates);
885
+ const decoded = [];
886
+ const seenMessages = new Set();
887
+ for (const cand of candidates) {
888
+ const result = ft8b(dd, cand.freq, cand.dt, sbase, depth);
889
+ if (!result)
890
+ continue;
891
+ if (seenMessages.has(result.msg))
892
+ continue;
893
+ seenMessages.add(result.msg);
894
+ decoded.push({
895
+ freq: result.freq,
896
+ dt: result.dt - 0.5,
897
+ snr: result.snr,
898
+ msg: result.msg,
899
+ sync: cand.sync,
900
+ });
901
+ }
902
+ return decoded;
903
+ }
904
+ function sync8(dd, nfa, nfb, syncmin, maxcand) {
905
+ const JZ = 62;
906
+ // Fortran uses NFFT1=3840 for the spectrogram FFT; we need a power of 2
907
+ const fftSize = nextPow2(NFFT1); // 4096
908
+ const halfSize = fftSize / 2; // 2048
909
+ const tstep = NSTEP / SAMPLE_RATE;
910
+ const df = SAMPLE_RATE / fftSize;
911
+ const fac = 1.0 / 300.0;
912
+ // Compute symbol spectra, stepping by NSTEP
913
+ const s = new Float64Array(halfSize * NHSYM);
914
+ const savg = new Float64Array(halfSize);
915
+ const xRe = new Float64Array(fftSize);
916
+ const xIm = new Float64Array(fftSize);
917
+ for (let j = 0; j < NHSYM; j++) {
918
+ const ia = j * NSTEP;
919
+ xRe.fill(0);
920
+ xIm.fill(0);
921
+ for (let i = 0; i < NSPS && ia + i < dd.length; i++) {
922
+ xRe[i] = fac * dd[ia + i];
923
+ }
924
+ fftComplex(xRe, xIm);
925
+ for (let i = 0; i < halfSize; i++) {
926
+ const power = xRe[i] * xRe[i] + xIm[i] * xIm[i];
927
+ s[i * NHSYM + j] = power;
928
+ savg[i] = (savg[i] ?? 0) + power;
929
+ }
930
+ }
931
+ // Compute baseline
932
+ const sbase = computeBaseline(savg, nfa, nfb, df, halfSize);
933
+ const ia = Math.max(1, Math.round(nfa / df));
934
+ const ib = Math.min(halfSize - 14, Math.round(nfb / df));
935
+ const nssy = Math.floor(NSPS / NSTEP);
936
+ const nfos = Math.round(SAMPLE_RATE / NSPS / df); // ~2 bins per tone spacing
937
+ const jstrt = Math.round(0.5 / tstep);
938
+ // 2D sync correlation
939
+ const sync2d = new Float64Array((ib - ia + 1) * (2 * JZ + 1));
940
+ const width = 2 * JZ + 1;
941
+ for (let i = ia; i <= ib; i++) {
942
+ for (let jj = -JZ; jj <= JZ; jj++) {
943
+ let ta = 0, tb = 0, tc = 0;
944
+ let t0a = 0, t0b = 0, t0c = 0;
945
+ for (let n = 0; n < 7; n++) {
946
+ const m = jj + jstrt + nssy * n;
947
+ const iCostas = i + nfos * icos7[n];
948
+ if (m >= 0 && m < NHSYM && iCostas < halfSize) {
949
+ ta += s[iCostas * NHSYM + m];
950
+ for (let tone = 0; tone <= 6; tone++) {
951
+ const idx = i + nfos * tone;
952
+ if (idx < halfSize)
953
+ t0a += s[idx * NHSYM + m];
954
+ }
955
+ }
956
+ const m36 = m + nssy * 36;
957
+ if (m36 >= 0 && m36 < NHSYM && iCostas < halfSize) {
958
+ tb += s[iCostas * NHSYM + m36];
959
+ for (let tone = 0; tone <= 6; tone++) {
960
+ const idx = i + nfos * tone;
961
+ if (idx < halfSize)
962
+ t0b += s[idx * NHSYM + m36];
963
+ }
964
+ }
965
+ const m72 = m + nssy * 72;
966
+ if (m72 >= 0 && m72 < NHSYM && iCostas < halfSize) {
967
+ tc += s[iCostas * NHSYM + m72];
968
+ for (let tone = 0; tone <= 6; tone++) {
969
+ const idx = i + nfos * tone;
970
+ if (idx < halfSize)
971
+ t0c += s[idx * NHSYM + m72];
972
+ }
973
+ }
974
+ }
975
+ const t = ta + tb + tc;
976
+ const t0total = t0a + t0b + t0c;
977
+ const t0 = (t0total - t) / 6.0;
978
+ const syncVal = t0 > 0 ? t / t0 : 0;
979
+ const tbc = tb + tc;
980
+ const t0bc = t0b + t0c;
981
+ const t0bc2 = (t0bc - tbc) / 6.0;
982
+ const syncBc = t0bc2 > 0 ? tbc / t0bc2 : 0;
983
+ sync2d[(i - ia) * width + (jj + JZ)] = Math.max(syncVal, syncBc);
984
+ }
985
+ }
986
+ // Find peaks
987
+ const candidates0 = [];
988
+ const mlag = 10;
989
+ for (let i = ia; i <= ib; i++) {
990
+ let bestSync = -1;
991
+ let bestJ = 0;
992
+ for (let j = -mlag; j <= mlag; j++) {
993
+ const v = sync2d[(i - ia) * width + (j + JZ)];
994
+ if (v > bestSync) {
995
+ bestSync = v;
996
+ bestJ = j;
997
+ }
998
+ }
999
+ // Also check wider range
1000
+ let bestSync2 = -1;
1001
+ let bestJ2 = 0;
1002
+ for (let j = -JZ; j <= JZ; j++) {
1003
+ const v = sync2d[(i - ia) * width + (j + JZ)];
1004
+ if (v > bestSync2) {
1005
+ bestSync2 = v;
1006
+ bestJ2 = j;
1007
+ }
1008
+ }
1009
+ if (bestSync >= syncmin) {
1010
+ candidates0.push({
1011
+ freq: i * df,
1012
+ dt: (bestJ - 0.5) * tstep,
1013
+ sync: bestSync,
1014
+ });
1015
+ }
1016
+ if (Math.abs(bestJ2 - bestJ) > 0 && bestSync2 >= syncmin) {
1017
+ candidates0.push({
1018
+ freq: i * df,
1019
+ dt: (bestJ2 - 0.5) * tstep,
1020
+ sync: bestSync2,
1021
+ });
1022
+ }
1023
+ }
1024
+ // Compute baseline normalization for sync values
1025
+ const syncValues = candidates0.map((c) => c.sync);
1026
+ syncValues.sort((a, b) => a - b);
1027
+ const pctileIdx = Math.max(0, Math.round(0.4 * syncValues.length) - 1);
1028
+ const base = syncValues[pctileIdx] ?? 1;
1029
+ if (base > 0) {
1030
+ for (const c of candidates0)
1031
+ c.sync /= base;
1032
+ }
1033
+ // Remove near-duplicate candidates
1034
+ for (let i = 0; i < candidates0.length; i++) {
1035
+ for (let j = 0; j < i; j++) {
1036
+ const fdiff = Math.abs(candidates0[i].freq - candidates0[j].freq);
1037
+ const tdiff = Math.abs(candidates0[i].dt - candidates0[j].dt);
1038
+ if (fdiff < 4.0 && tdiff < 0.04) {
1039
+ if (candidates0[i].sync >= candidates0[j].sync) {
1040
+ candidates0[j].sync = 0;
1041
+ }
1042
+ else {
1043
+ candidates0[i].sync = 0;
1044
+ }
1045
+ }
1046
+ }
1047
+ }
1048
+ // Sort by sync descending, take top maxcand
1049
+ const filtered = candidates0.filter((c) => c.sync >= syncmin);
1050
+ filtered.sort((a, b) => b.sync - a.sync);
1051
+ return { candidates: filtered.slice(0, maxcand), sbase };
1052
+ }
1053
+ function computeBaseline(savg, nfa, nfb, df, nh1) {
1054
+ const sbase = new Float64Array(nh1);
1055
+ const ia = Math.max(1, Math.round(nfa / df));
1056
+ const ib = Math.min(nh1 - 1, Math.round(nfb / df));
1057
+ // Smooth the spectrum to get baseline
1058
+ const window = 50; // bins
1059
+ for (let i = 0; i < nh1; i++) {
1060
+ let sum = 0;
1061
+ let count = 0;
1062
+ const lo = Math.max(ia, i - window);
1063
+ const hi = Math.min(ib, i + window);
1064
+ for (let j = lo; j <= hi; j++) {
1065
+ sum += savg[j];
1066
+ count++;
1067
+ }
1068
+ sbase[i] = count > 0 ? 10 * Math.log10(Math.max(1e-30, sum / count)) : 0;
1069
+ }
1070
+ return sbase;
1071
+ }
1072
+ function ft8b(dd0, f1, xdt, _sbase, depth) {
1073
+ const NFFT2 = 3200;
1074
+ const NP2 = 2812;
1075
+ const NFFT1_LONG = 192000;
1076
+ const fs2 = SAMPLE_RATE / NDOWN;
1077
+ const dt2 = 1.0 / fs2;
1078
+ const twopi = 2 * Math.PI;
1079
+ // Downsample: mix to baseband and filter
1080
+ const cd0Re = new Float64Array(NFFT2);
1081
+ const cd0Im = new Float64Array(NFFT2);
1082
+ ft8Downsample(dd0, f1, cd0Re, cd0Im, NFFT1_LONG, NFFT2);
1083
+ // Find best time offset
1084
+ const i0 = Math.round((xdt + 0.5) * fs2);
1085
+ let smax = 0;
1086
+ let ibest = i0;
1087
+ for (let idt = i0 - 10; idt <= i0 + 10; idt++) {
1088
+ const sync = sync8d(cd0Re, cd0Im, idt, null, null, false);
1089
+ if (sync > smax) {
1090
+ smax = sync;
1091
+ ibest = idt;
1092
+ }
1093
+ }
1094
+ // Fine frequency search
1095
+ smax = 0;
1096
+ let delfbest = 0;
1097
+ for (let ifr = -5; ifr <= 5; ifr++) {
1098
+ const delf = ifr * 0.5;
1099
+ const dphi = twopi * delf * dt2;
1100
+ const twkRe = new Float64Array(32);
1101
+ const twkIm = new Float64Array(32);
1102
+ let phi = 0;
1103
+ for (let i = 0; i < 32; i++) {
1104
+ twkRe[i] = Math.cos(phi);
1105
+ twkIm[i] = Math.sin(phi);
1106
+ phi = (phi + dphi) % twopi;
1107
+ }
1108
+ const sync = sync8d(cd0Re, cd0Im, ibest, twkRe, twkIm, true);
1109
+ if (sync > smax) {
1110
+ smax = sync;
1111
+ delfbest = delf;
1112
+ }
1113
+ }
1114
+ // Apply frequency correction and re-downsample
1115
+ f1 += delfbest;
1116
+ ft8Downsample(dd0, f1, cd0Re, cd0Im, NFFT1_LONG, NFFT2);
1117
+ // Refine time offset
1118
+ const ss = new Float64Array(9);
1119
+ for (let idt = -4; idt <= 4; idt++) {
1120
+ ss[idt + 4] = sync8d(cd0Re, cd0Im, ibest + idt, null, null, false);
1121
+ }
1122
+ let maxss = -1;
1123
+ let maxIdx = 4;
1124
+ for (let i = 0; i < 9; i++) {
1125
+ if (ss[i] > maxss) {
1126
+ maxss = ss[i];
1127
+ maxIdx = i;
1128
+ }
1129
+ }
1130
+ ibest = ibest + maxIdx - 4;
1131
+ xdt = (ibest - 1) * dt2;
1132
+ // Extract 8-tone soft symbols for each of NN=79 symbols
1133
+ const s8 = new Float64Array(8 * NN);
1134
+ const csRe = new Float64Array(8 * NN);
1135
+ const csIm = new Float64Array(8 * NN);
1136
+ const symbRe = new Float64Array(32);
1137
+ const symbIm = new Float64Array(32);
1138
+ for (let k = 0; k < NN; k++) {
1139
+ const i1 = ibest + k * 32;
1140
+ symbRe.fill(0);
1141
+ symbIm.fill(0);
1142
+ if (i1 >= 0 && i1 + 31 < NP2) {
1143
+ for (let j = 0; j < 32; j++) {
1144
+ symbRe[j] = cd0Re[i1 + j];
1145
+ symbIm[j] = cd0Im[i1 + j];
1146
+ }
1147
+ }
1148
+ fftComplex(symbRe, symbIm);
1149
+ for (let tone = 0; tone < 8; tone++) {
1150
+ const re = symbRe[tone] / 1000;
1151
+ const im = symbIm[tone] / 1000;
1152
+ csRe[tone * NN + k] = re;
1153
+ csIm[tone * NN + k] = im;
1154
+ s8[tone * NN + k] = Math.sqrt(re * re + im * im);
1155
+ }
1156
+ }
1157
+ // Sync quality check
1158
+ let nsync = 0;
1159
+ for (let k = 0; k < 7; k++) {
1160
+ for (const offset of [0, 36, 72]) {
1161
+ let maxTone = 0;
1162
+ let maxVal = -1;
1163
+ for (let t = 0; t < 8; t++) {
1164
+ const v = s8[t * NN + k + offset];
1165
+ if (v > maxVal) {
1166
+ maxVal = v;
1167
+ maxTone = t;
1168
+ }
1169
+ }
1170
+ if (maxTone === icos7[k])
1171
+ nsync++;
1172
+ }
1173
+ }
1174
+ if (nsync <= 6)
1175
+ return null;
1176
+ // Compute soft bit metrics for multiple nsym values (1, 2, 3)
1177
+ // and a normalized version, matching the Fortran ft8b passes 1-4
1178
+ const bmeta = new Float64Array(N_LDPC); // nsym=1
1179
+ const bmetb = new Float64Array(N_LDPC); // nsym=2
1180
+ const bmetc = new Float64Array(N_LDPC); // nsym=3
1181
+ const bmetd = new Float64Array(N_LDPC); // nsym=1 normalized
1182
+ for (let nsym = 1; nsym <= 3; nsym++) {
1183
+ const nt = 1 << (3 * nsym); // 8, 64, 512
1184
+ const ibmax = nsym === 1 ? 2 : nsym === 2 ? 5 : 8;
1185
+ for (let ihalf = 1; ihalf <= 2; ihalf++) {
1186
+ for (let k = 1; k <= 29; k += nsym) {
1187
+ const ks = ihalf === 1 ? k + 7 : k + 43;
1188
+ const s2 = new Float64Array(nt);
1189
+ for (let i = 0; i < nt; i++) {
1190
+ const i1 = Math.floor(i / 64);
1191
+ const i2 = Math.floor((i & 63) / 8);
1192
+ const i3 = i & 7;
1193
+ if (nsym === 1) {
1194
+ const re = csRe[graymap[i3] * NN + ks - 1];
1195
+ const im = csIm[graymap[i3] * NN + ks - 1];
1196
+ s2[i] = Math.sqrt(re * re + im * im);
1197
+ }
1198
+ else if (nsym === 2) {
1199
+ const sRe = csRe[graymap[i2] * NN + ks - 1] + csRe[graymap[i3] * NN + ks];
1200
+ const sIm = csIm[graymap[i2] * NN + ks - 1] + csIm[graymap[i3] * NN + ks];
1201
+ s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
1202
+ }
1203
+ else {
1204
+ const sRe = csRe[graymap[i1] * NN + ks - 1] +
1205
+ csRe[graymap[i2] * NN + ks] +
1206
+ csRe[graymap[i3] * NN + ks + 1];
1207
+ const sIm = csIm[graymap[i1] * NN + ks - 1] +
1208
+ csIm[graymap[i2] * NN + ks] +
1209
+ csIm[graymap[i3] * NN + ks + 1];
1210
+ s2[i] = Math.sqrt(sRe * sRe + sIm * sIm);
1211
+ }
1212
+ }
1213
+ // Fortran: i32 = 1 + (k-1)*3 + (ihalf-1)*87 (1-based)
1214
+ const i32 = 1 + (k - 1) * 3 + (ihalf - 1) * 87;
1215
+ for (let ib = 0; ib <= ibmax; ib++) {
1216
+ // max of s2 where bit (ibmax-ib) of index is 1
1217
+ let max1 = -1e30, max0 = -1e30;
1218
+ for (let i = 0; i < nt; i++) {
1219
+ const bitSet = (i & (1 << (ibmax - ib))) !== 0;
1220
+ if (bitSet) {
1221
+ if (s2[i] > max1)
1222
+ max1 = s2[i];
1223
+ }
1224
+ else {
1225
+ if (s2[i] > max0)
1226
+ max0 = s2[i];
1227
+ }
1228
+ }
1229
+ const idx = i32 + ib - 1; // Convert to 0-based
1230
+ if (idx >= 0 && idx < N_LDPC) {
1231
+ const bm = max1 - max0;
1232
+ if (nsym === 1) {
1233
+ bmeta[idx] = bm;
1234
+ const den = Math.max(max1, max0);
1235
+ bmetd[idx] = den > 0 ? bm / den : 0;
1236
+ }
1237
+ else if (nsym === 2) {
1238
+ bmetb[idx] = bm;
1239
+ }
1240
+ else {
1241
+ bmetc[idx] = bm;
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ normalizeBmet(bmeta);
1249
+ normalizeBmet(bmetb);
1250
+ normalizeBmet(bmetc);
1251
+ normalizeBmet(bmetd);
1252
+ const bmetrics = [bmeta, bmetb, bmetc, bmetd];
1253
+ const scalefac = 2.83;
1254
+ const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
1255
+ const apmask = new Int8Array(N_LDPC);
1256
+ // Try 4 passes with different soft-symbol metrics (matching Fortran)
1257
+ let result = null;
1258
+ for (let ipass = 0; ipass < 4; ipass++) {
1259
+ const llr = new Float64Array(N_LDPC);
1260
+ for (let i = 0; i < N_LDPC; i++)
1261
+ llr[i] = scalefac * bmetrics[ipass][i];
1262
+ result = decode174_91(llr, apmask, maxosd);
1263
+ if (result && result.nharderrors >= 0 && result.nharderrors <= 36)
1264
+ break;
1265
+ result = null;
1266
+ }
1267
+ if (!result || result.nharderrors < 0 || result.nharderrors > 36)
1268
+ return null;
1269
+ // Check for all-zero codeword
1270
+ if (result.cw.every((b) => b === 0))
1271
+ return null;
1272
+ const message77 = result.message91.slice(0, 77);
1273
+ // Validate message type
1274
+ const n3v = (message77[71] << 2) | (message77[72] << 1) | message77[73];
1275
+ const i3v = (message77[74] << 2) | (message77[75] << 1) | message77[76];
1276
+ if (i3v > 5 || (i3v === 0 && n3v > 6))
1277
+ return null;
1278
+ if (i3v === 0 && n3v === 2)
1279
+ return null;
1280
+ // Unpack
1281
+ const { msg, success } = unpack77(message77);
1282
+ if (!success || msg.trim().length === 0)
1283
+ return null;
1284
+ // Estimate SNR
1285
+ let xsig = 0;
1286
+ let xnoi = 0;
1287
+ const itone = getTones$1(result.cw);
1288
+ for (let i = 0; i < 79; i++) {
1289
+ xsig += s8[itone[i] * NN + i] ** 2;
1290
+ const ios = (itone[i] + 4) % 7;
1291
+ xnoi += s8[ios * NN + i] ** 2;
1292
+ }
1293
+ let snr = 0.001;
1294
+ const arg = xsig / Math.max(xnoi, 1e-30) - 1.0;
1295
+ if (arg > 0.1)
1296
+ snr = arg;
1297
+ snr = 10 * Math.log10(snr) - 27.0;
1298
+ if (snr < -24)
1299
+ snr = -24;
1300
+ return { msg, freq: f1, dt: xdt, snr };
1301
+ }
1302
+ function getTones$1(cw) {
1303
+ const tones = new Array(79).fill(0);
1304
+ for (let i = 0; i < 7; i++)
1305
+ tones[i] = icos7[i];
1306
+ for (let i = 0; i < 7; i++)
1307
+ tones[36 + i] = icos7[i];
1308
+ for (let i = 0; i < 7; i++)
1309
+ tones[72 + i] = icos7[i];
1310
+ let k = 7;
1311
+ for (let j = 1; j <= 58; j++) {
1312
+ const i = (j - 1) * 3;
1313
+ if (j === 30)
1314
+ k += 7;
1315
+ const indx = cw[i] * 4 + cw[i + 1] * 2 + cw[i + 2];
1316
+ tones[k] = graymap[indx];
1317
+ k++;
1318
+ }
1319
+ return tones;
1320
+ }
1321
+ /**
1322
+ * Mix f0 to baseband and decimate by NDOWN (60x).
1323
+ * Time-domain approach: mix down, low-pass filter via moving average, decimate.
1324
+ * Output: complex baseband signal at 200 Hz sample rate (32 samples/symbol).
1325
+ */
1326
+ function ft8Downsample(dd, f0, outRe, outIm, _nfft1Long, nfft2) {
1327
+ const twopi = 2 * Math.PI;
1328
+ const len = Math.min(dd.length, NMAX);
1329
+ const dphi = (twopi * f0) / SAMPLE_RATE;
1330
+ // Mix to baseband
1331
+ const mixRe = new Float64Array(len);
1332
+ const mixIm = new Float64Array(len);
1333
+ let phi = 0;
1334
+ for (let i = 0; i < len; i++) {
1335
+ mixRe[i] = dd[i] * Math.cos(phi);
1336
+ mixIm[i] = -dd[i] * Math.sin(phi);
1337
+ phi += dphi;
1338
+ if (phi > twopi)
1339
+ phi -= twopi;
1340
+ }
1341
+ // Low-pass filter: simple moving-average with window = NDOWN
1342
+ // then decimate by NDOWN to get 200 Hz sample rate
1343
+ const outLen = Math.min(nfft2, Math.floor(len / NDOWN));
1344
+ outRe.fill(0);
1345
+ outIm.fill(0);
1346
+ // Running sum filter
1347
+ const halfWin = NDOWN >> 1;
1348
+ for (let k = 0; k < outLen; k++) {
1349
+ const center = k * NDOWN + halfWin;
1350
+ let sumRe = 0, sumIm = 0;
1351
+ const start = Math.max(0, center - halfWin);
1352
+ const end = Math.min(len, center + halfWin);
1353
+ for (let j = start; j < end; j++) {
1354
+ sumRe += mixRe[j];
1355
+ sumIm += mixIm[j];
1356
+ }
1357
+ const n = end - start;
1358
+ outRe[k] = sumRe / n;
1359
+ outIm[k] = sumIm / n;
1360
+ }
1361
+ }
1362
+ function sync8d(cd0Re, cd0Im, i0, twkRe, twkIm, useTwk) {
1363
+ const NP2 = 2812;
1364
+ const twopi = 2 * Math.PI;
1365
+ // Precompute Costas sync waveforms
1366
+ const csyncRe = new Float64Array(7 * 32);
1367
+ const csyncIm = new Float64Array(7 * 32);
1368
+ for (let i = 0; i < 7; i++) {
1369
+ let phi = 0;
1370
+ const dphi = (twopi * icos7[i]) / 32;
1371
+ for (let j = 0; j < 32; j++) {
1372
+ csyncRe[i * 32 + j] = Math.cos(phi);
1373
+ csyncIm[i * 32 + j] = Math.sin(phi);
1374
+ phi = (phi + dphi) % twopi;
1375
+ }
1376
+ }
1377
+ let sync = 0;
1378
+ for (let i = 0; i < 7; i++) {
1379
+ const i1 = i0 + i * 32;
1380
+ const i2 = i1 + 36 * 32;
1381
+ const i3 = i1 + 72 * 32;
1382
+ for (const iStart of [i1, i2, i3]) {
1383
+ let zRe = 0, zIm = 0;
1384
+ if (iStart >= 0 && iStart + 31 < NP2) {
1385
+ for (let j = 0; j < 32; j++) {
1386
+ let sRe = csyncRe[i * 32 + j];
1387
+ let sIm = csyncIm[i * 32 + j];
1388
+ if (useTwk && twkRe && twkIm) {
1389
+ const tRe = twkRe[j] * sRe - twkIm[j] * sIm;
1390
+ const tIm = twkRe[j] * sIm + twkIm[j] * sRe;
1391
+ sRe = tRe;
1392
+ sIm = tIm;
1393
+ }
1394
+ // Conjugate multiply: cd0 * conj(csync)
1395
+ const dRe = cd0Re[iStart + j];
1396
+ const dIm = cd0Im[iStart + j];
1397
+ zRe += dRe * sRe + dIm * sIm;
1398
+ zIm += dIm * sRe - dRe * sIm;
1399
+ }
1400
+ }
1401
+ sync += zRe * zRe + zIm * zIm;
1402
+ }
1403
+ }
1404
+ return sync;
1405
+ }
1406
+ function normalizeBmet(bmet) {
1407
+ const n = bmet.length;
1408
+ let sum = 0, sum2 = 0;
1409
+ for (let i = 0; i < n; i++) {
1410
+ sum += bmet[i];
1411
+ sum2 += bmet[i] * bmet[i];
1412
+ }
1413
+ const avg = sum / n;
1414
+ const avg2 = sum2 / n;
1415
+ const variance = avg2 - avg * avg;
1416
+ const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
1417
+ if (sigma > 0) {
1418
+ for (let i = 0; i < n; i++)
1419
+ bmet[i] = bmet[i] / sigma;
1420
+ }
1421
+ }
1422
+ function resample(input, fromRate, toRate, outLen) {
1423
+ const out = new Float64Array(outLen);
1424
+ const ratio = fromRate / toRate;
1425
+ for (let i = 0; i < outLen; i++) {
1426
+ const srcIdx = i * ratio;
1427
+ const lo = Math.floor(srcIdx);
1428
+ const frac = srcIdx - lo;
1429
+ const v0 = lo < input.length ? (input[lo] ?? 0) : 0;
1430
+ const v1 = lo + 1 < input.length ? (input[lo + 1] ?? 0) : 0;
1431
+ out[i] = v0 * (1 - frac) + v1 * frac;
1432
+ }
1433
+ return out;
1434
+ }
1435
+
1436
+ /**
1437
+ * FT8 message packing – TypeScript port of packjt77.f90
1438
+ *
1439
+ * Implemented message types
1440
+ * ─────────────────────────
1441
+ * 0.0 Free text (≤13 chars from the 42-char FT8 alphabet)
1442
+ * 1 Standard (two callsigns + grid/report/RR73/73)
1443
+ * /R and /P suffixes on either callsign → ipa/ipb = 1 (triggers i3=2 for /P)
1444
+ * 4 One nonstandard (<hash>) call + one standard call
1445
+ * e.g. <YW18FIFA> KA1ABC 73
1446
+ * KA1ABC <YW18FIFA> -11
1447
+ * CQ YW18FIFA
1448
+ *
1449
+ * Reference: lib/77bit/packjt77.f90 (subroutines pack77, pack28, pack77_1,
1450
+ * pack77_4, packtext77, ihashcall)
1451
+ */
1452
+ function mpZero() {
1453
+ return new Uint8Array(9);
1454
+ }
1455
+ /** qa = 42 * qb + carry from high limbs, working with 9 limbs (indices 0..8) */
1456
+ function mpMult42(a) {
1457
+ const b = mpZero();
1458
+ let carry = 0;
1459
+ for (let i = 8; i >= 0; i--) {
1460
+ const v = 42 * (a[i] ?? 0) + carry;
1461
+ b[i] = v & 0xff;
1462
+ carry = v >>> 8;
1463
+ }
1464
+ return b;
1465
+ }
1466
+ /** qa = qb + j */
1467
+ function mpAdd(a, j) {
1468
+ const b = new Uint8Array(a);
1469
+ let carry = j;
1470
+ for (let i = 8; i >= 0 && carry > 0; i--) {
1471
+ const v = (b[i] ?? 0) + carry;
1472
+ b[i] = v & 0xff;
1473
+ carry = v >>> 8;
1474
+ }
1475
+ return b;
1476
+ }
1477
+ /**
1478
+ * Pack a 13-char free-text string (42-char alphabet) into 71 bits.
1479
+ * Mirrors Fortran packtext77 / mp_short_* logic.
1480
+ * Alphabet: ' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?' (42 chars)
1481
+ */
1482
+ function packtext77(c13) {
1483
+ // Right-justify in 13 chars
1484
+ const w = c13.padStart(13, " ");
1485
+ let qa = mpZero();
1486
+ for (let i = 0; i < 13; i++) {
1487
+ let j = FTALPH.indexOf(w[i] ?? " ");
1488
+ if (j < 0)
1489
+ j = 0;
1490
+ qa = mpMult42(qa);
1491
+ qa = mpAdd(qa, j);
1492
+ }
1493
+ // Extract 71 bits: first 7 then 8*8
1494
+ const bits = [];
1495
+ // limb 0 gives 7 bits (high), limbs 1..8 give 8 bits each → 7 + 64 = 71
1496
+ // But we need exactly 71 bits. The Fortran writes b7.7 then 8*b8.8 for 71 total.
1497
+ // That equals: 7 + 8*8 = 71 bits from the 9 bytes (72 bits), skipping the top bit of byte 0.
1498
+ const byte0 = qa[0] ?? 0;
1499
+ for (let b = 6; b >= 0; b--)
1500
+ bits.push((byte0 >> b) & 1);
1501
+ for (let li = 1; li <= 8; li++) {
1502
+ const byte = qa[li] ?? 0;
1503
+ for (let b = 7; b >= 0; b--)
1504
+ bits.push((byte >> b) & 1);
1505
+ }
1506
+ return bits; // 71 bits
1507
+ }
1508
+ /**
1509
+ * ihashcall(c0, m): compute a hash of c0 and return bits [m-1 .. 63-m] of
1510
+ * (47055833459n * n8) shifted right by (64 - m).
1511
+ *
1512
+ * Fortran: ishft(47055833459_8 * n8, m - 64)
1513
+ * → arithmetic right-shift of 64-bit product by (64 - m), keeping low m bits.
1514
+ *
1515
+ * Here we only ever call with m=22 (per pack28 for <...> callsigns).
1516
+ */
1517
+ function ihashcall22(c0) {
1518
+ const C = C38;
1519
+ let n8 = 0n;
1520
+ const s = c0.padEnd(11, " ").slice(0, 11).toUpperCase();
1521
+ for (let i = 0; i < 11; i++) {
1522
+ const j = C.indexOf(s[i] ?? " ");
1523
+ n8 = 38n * n8 + BigInt(j < 0 ? 0 : j);
1524
+ }
1525
+ const MAGIC = 47055833459n;
1526
+ const prod = BigInt.asUintN(64, MAGIC * n8);
1527
+ // arithmetic right-shift by (64 - 22) = 42 bits → take top 22 bits
1528
+ const result = Number(prod >> 42n) & 0x3fffff; // 22 bits
1529
+ return result;
1530
+ }
1531
+ /**
1532
+ * Checks whether c0 is a valid standard callsign (may also have /R or /P suffix).
1533
+ * Returns { basecall, isStandard, hasSuffix: '/R'|'/P'|null }
1534
+ */
1535
+ function parseCallsign(raw) {
1536
+ let call = raw.trim().toUpperCase();
1537
+ let suffix = null;
1538
+ if (call.endsWith("/R")) {
1539
+ suffix = "/R";
1540
+ call = call.slice(0, -2);
1541
+ }
1542
+ if (call.endsWith("/P")) {
1543
+ suffix = "/P";
1544
+ call = call.slice(0, -2);
1545
+ }
1546
+ const isLetter = (c) => c >= "A" && c <= "Z";
1547
+ const isDigit = (c) => c >= "0" && c <= "9";
1548
+ // Find the call-area digit (last digit in the call)
1549
+ let iarea = -1;
1550
+ for (let i = call.length - 1; i >= 1; i--) {
1551
+ if (isDigit(call[i] ?? "")) {
1552
+ iarea = i;
1553
+ break;
1554
+ }
1555
+ }
1556
+ if (iarea < 1)
1557
+ return { basecall: call, isStandard: false, suffix };
1558
+ // Count letters/digits before the call-area digit
1559
+ let npdig = 0, nplet = 0;
1560
+ for (let i = 0; i < iarea; i++) {
1561
+ if (isDigit(call[i] ?? ""))
1562
+ npdig++;
1563
+ if (isLetter(call[i] ?? ""))
1564
+ nplet++;
1565
+ }
1566
+ // Count suffix letters after call-area digit
1567
+ let nslet = 0;
1568
+ for (let i = iarea + 1; i < call.length; i++) {
1569
+ if (isLetter(call[i] ?? ""))
1570
+ nslet++;
1571
+ }
1572
+ const standard = iarea >= 1 &&
1573
+ iarea <= 2 && // Fortran: iarea (1-indexed) must be 2 or 3 → 0-indexed: 1 or 2
1574
+ nplet >= 1 && // at least one letter before area digit
1575
+ npdig < iarea && // not all digits before area
1576
+ nslet >= 1 && // must have at least one letter after area digit
1577
+ nslet <= 3; // at most 3 suffix letters
1578
+ return { basecall: call, isStandard: standard, suffix };
1579
+ }
1580
+ /**
1581
+ * pack28: pack a single callsign/token to a 28-bit integer.
1582
+ * Mirrors Fortran pack28 subroutine.
1583
+ */
1584
+ function pack28(token) {
1585
+ const t = token.trim().toUpperCase();
1586
+ // Special tokens
1587
+ if (t === "DE")
1588
+ return 0;
1589
+ if (t === "QRZ")
1590
+ return 1;
1591
+ if (t === "CQ")
1592
+ return 2;
1593
+ // CQ_nnn (CQ with frequency offset in kHz)
1594
+ if (t.startsWith("CQ_")) {
1595
+ const rest = t.slice(3);
1596
+ const nqsy = parseInt(rest, 10);
1597
+ if (!Number.isNaN(nqsy) && /^\d{3}$/.test(rest))
1598
+ return 3 + nqsy;
1599
+ // CQ_aaaa (up to 4 letters)
1600
+ if (/^[A-Z]{1,4}$/.test(rest)) {
1601
+ const padded = rest.padStart(4, " ");
1602
+ let m = 0;
1603
+ for (let i = 0; i < 4; i++) {
1604
+ const c = padded[i] ?? " ";
1605
+ const j = c >= "A" && c <= "Z" ? c.charCodeAt(0) - 64 : 0;
1606
+ m = 27 * m + j;
1607
+ }
1608
+ return 3 + 1000 + m;
1609
+ }
1610
+ }
1611
+ // <...> hash calls
1612
+ if (t.startsWith("<") && t.endsWith(">")) {
1613
+ const inner = t.slice(1, -1);
1614
+ const n22 = ihashcall22(inner);
1615
+ return (NTOKENS + n22) & (MAX28 - 1);
1616
+ }
1617
+ // Standard callsign
1618
+ const { basecall, isStandard } = parseCallsign(t);
1619
+ if (isStandard) {
1620
+ const cs = basecall.length === 5 ? ` ${basecall}` : basecall;
1621
+ const i1 = A1.indexOf(cs[0] ?? " ");
1622
+ const i2 = A2.indexOf(cs[1] ?? "0");
1623
+ const i3 = A3.indexOf(cs[2] ?? "0");
1624
+ const i4 = A4.indexOf(cs[3] ?? " ");
1625
+ const i5 = A4.indexOf(cs[4] ?? " ");
1626
+ const i6 = A4.indexOf(cs[5] ?? " ");
1627
+ const n28 = 36 * 10 * 27 * 27 * 27 * i1 +
1628
+ 10 * 27 * 27 * 27 * i2 +
1629
+ 27 * 27 * 27 * i3 +
1630
+ 27 * 27 * i4 +
1631
+ 27 * i5 +
1632
+ i6;
1633
+ return (n28 + NTOKENS + MAX22) & (MAX28 - 1);
1634
+ }
1635
+ // Non-standard → 22-bit hash
1636
+ const n22 = ihashcall22(basecall);
1637
+ return (NTOKENS + n22) & (MAX28 - 1);
1638
+ }
1639
+ function packgrid4(s) {
1640
+ if (s === "RRR")
1641
+ return MAXGRID4 + 2;
1642
+ if (s === "73")
1643
+ return MAXGRID4 + 4;
1644
+ // Numeric report (+NN / -NN)
1645
+ const r = /^(R?)([+-]\d+)$/.exec(s);
1646
+ if (r) {
1647
+ let irpt = parseInt(r[2], 10);
1648
+ if (irpt >= -50 && irpt <= -31)
1649
+ irpt += 101;
1650
+ irpt += 35; // encode in range 5..85
1651
+ return MAXGRID4 + irpt;
1652
+ }
1653
+ // 4-char grid locator
1654
+ const j1 = (s.charCodeAt(0) - 65) * 18 * 10 * 10;
1655
+ const j2 = (s.charCodeAt(1) - 65) * 10 * 10;
1656
+ const j3 = (s.charCodeAt(2) - 48) * 10;
1657
+ const j4 = s.charCodeAt(3) - 48;
1658
+ return j1 + j2 + j3 + j4;
1659
+ }
1660
+ function appendBits(bits, val, width) {
1661
+ for (let i = width - 1; i >= 0; i--) {
1662
+ bits.push(Math.floor(val / 2 ** i) % 2);
1663
+ }
1664
+ }
1665
+ /**
1666
+ * Pack an FT8 message into 77 bits.
1667
+ * Returns an array of 0/1 values, length 77.
1668
+ *
1669
+ * Supported message types:
1670
+ * Type 1/2 Standard two-callsign messages including /R and /P suffixes
1671
+ * Type 4 One nonstandard (<hash>) call + one standard or nonstandard call
1672
+ * Type 0.0 Free text (≤13 chars from FTALPH)
1673
+ */
1674
+ /**
1675
+ * Preprocess a message in the same way as Fortran split77:
1676
+ * - Collapse multiple spaces, force uppercase
1677
+ * - If the first word is "CQ" and there are ≥3 words and the 3rd word is a
1678
+ * valid base callsign, merge words 1+2 into "CQ_<word2>" and shift the rest.
1679
+ */
1680
+ function split77(msg) {
1681
+ const parts = msg.trim().toUpperCase().replace(/\s+/g, " ").split(" ").filter(Boolean);
1682
+ if (parts.length >= 3 && parts[0] === "CQ") {
1683
+ // Check if word 3 (index 2) is a valid base callsign
1684
+ const w3 = parts[2].replace(/\/[RP]$/, ""); // strip /R or /P for check
1685
+ const { isStandard } = parseCallsign(w3);
1686
+ if (isStandard) {
1687
+ // merge CQ + word2 → CQ_word2
1688
+ const merged = [`CQ_${parts[1]}`, ...parts.slice(2)];
1689
+ return merged;
1690
+ }
1691
+ }
1692
+ return parts;
1693
+ }
1694
+ function pack77(msg) {
1695
+ const parts = split77(msg);
1696
+ if (parts.length < 1)
1697
+ throw new Error("Empty message");
1698
+ // ── Try Type 1/2: standard message ────────────────────────────────────────
1699
+ const t1 = tryPackType1(parts);
1700
+ if (t1)
1701
+ return t1;
1702
+ // ── Try Type 4: one hash call ──────────────────────────────────────────────
1703
+ const t4 = tryPackType4(parts);
1704
+ if (t4)
1705
+ return t4;
1706
+ // ── Default: Type 0.0 free text ───────────────────────────────────────────
1707
+ return packFreeText(msg);
1708
+ }
1709
+ function tryPackType1(parts) {
1710
+ // Minimum 2 words, maximum 4
1711
+ if (parts.length < 2 || parts.length > 4)
1712
+ return null;
1713
+ const w1 = parts[0];
1714
+ const w2 = parts[1];
1715
+ const wLast = parts[parts.length - 1];
1716
+ // Neither word may be a hash call if the other has a slash
1717
+ if (w1.startsWith("<") && w2.includes("/"))
1718
+ return null;
1719
+ if (w2.startsWith("<") && w1.includes("/"))
1720
+ return null;
1721
+ // Parse callsign 1
1722
+ let call1;
1723
+ let ipa = 0;
1724
+ let ok1;
1725
+ if (w1 === "CQ" || w1 === "DE" || w1 === "QRZ" || w1.startsWith("CQ_")) {
1726
+ call1 = w1;
1727
+ ok1 = true;
1728
+ ipa = 0;
1729
+ }
1730
+ else if (w1.startsWith("<") && w1.endsWith(">")) {
1731
+ call1 = w1;
1732
+ ok1 = true;
1733
+ ipa = 0;
1734
+ }
1735
+ else {
1736
+ const p1 = parseCallsign(w1);
1737
+ call1 = p1.basecall;
1738
+ ok1 = p1.isStandard;
1739
+ if (p1.suffix === "/R" || p1.suffix === "/P")
1740
+ ipa = 1;
1741
+ }
1742
+ // Parse callsign 2
1743
+ let call2;
1744
+ let ipb = 0;
1745
+ let ok2;
1746
+ if (w2.startsWith("<") && w2.endsWith(">")) {
1747
+ call2 = w2;
1748
+ ok2 = true;
1749
+ ipb = 0;
1750
+ }
1751
+ else {
1752
+ const p2 = parseCallsign(w2);
1753
+ call2 = p2.basecall;
1754
+ ok2 = p2.isStandard;
1755
+ if (p2.suffix === "/R" || p2.suffix === "/P")
1756
+ ipb = 1;
1757
+ }
1758
+ if (!ok1 || !ok2)
1759
+ return null;
1760
+ // Determine message type (1 or 2)
1761
+ const i1psfx = ipa === 1 && (w1.endsWith("/P") || w1.includes("/P "));
1762
+ const i2psfx = ipb === 1 && (w2.endsWith("/P") || w2.includes("/P "));
1763
+ const i3 = i1psfx || i2psfx ? 2 : 1;
1764
+ // Decode the grid/report/special from the last word
1765
+ let igrid4;
1766
+ let ir = 0;
1767
+ if (parts.length === 2) {
1768
+ // Two-word message: <call1> <call2> → special irpt=1
1769
+ igrid4 = MAXGRID4 + 1;
1770
+ ir = 0;
1771
+ }
1772
+ else {
1773
+ // Check whether wLast is a grid, report, or special
1774
+ const lastUpper = wLast.toUpperCase();
1775
+ if (isGrid4(lastUpper)) {
1776
+ igrid4 = packgrid4(lastUpper);
1777
+ ir = parts.length === 4 && parts[2] === "R" ? 1 : 0;
1778
+ }
1779
+ else if (lastUpper === "RRR") {
1780
+ igrid4 = MAXGRID4 + 2;
1781
+ ir = 0;
1782
+ }
1783
+ else if (lastUpper === "RR73") {
1784
+ igrid4 = MAXGRID4 + 3;
1785
+ ir = 0;
1786
+ }
1787
+ else if (lastUpper === "73") {
1788
+ igrid4 = MAXGRID4 + 4;
1789
+ ir = 0;
1790
+ }
1791
+ else if (/^R[+-]\d+$/.test(lastUpper)) {
1792
+ ir = 1;
1793
+ const reportStr = lastUpper.slice(1); // strip leading R
1794
+ let irpt = parseInt(reportStr, 10);
1795
+ if (irpt >= -50 && irpt <= -31)
1796
+ irpt += 101;
1797
+ irpt += 35;
1798
+ igrid4 = MAXGRID4 + irpt;
1799
+ }
1800
+ else if (/^[+-]\d+$/.test(lastUpper)) {
1801
+ ir = 0;
1802
+ let irpt = parseInt(lastUpper, 10);
1803
+ if (irpt >= -50 && irpt <= -31)
1804
+ irpt += 101;
1805
+ irpt += 35;
1806
+ igrid4 = MAXGRID4 + irpt;
1807
+ }
1808
+ else {
1809
+ return null; // Not a valid Type 1 last word
1810
+ }
1811
+ }
1812
+ const n28a = pack28(call1);
1813
+ const n28b = pack28(call2);
1814
+ const bits = [];
1815
+ appendBits(bits, n28a, 28);
1816
+ appendBits(bits, ipa, 1);
1817
+ appendBits(bits, n28b, 28);
1818
+ appendBits(bits, ipb, 1);
1819
+ appendBits(bits, ir, 1);
1820
+ appendBits(bits, igrid4, 15);
1821
+ appendBits(bits, i3, 3);
1822
+ return bits;
1823
+ }
1824
+ function isGrid4(s) {
1825
+ return (s.length === 4 &&
1826
+ s[0] >= "A" &&
1827
+ s[0] <= "R" &&
1828
+ s[1] >= "A" &&
1829
+ s[1] <= "R" &&
1830
+ s[2] >= "0" &&
1831
+ s[2] <= "9" &&
1832
+ s[3] >= "0" &&
1833
+ s[3] <= "9");
1834
+ }
1835
+ /**
1836
+ * Type 4: one nonstandard (or hashed <...>) call + one standard call.
1837
+ * Format: <HASH> CALL [RRR|RR73|73]
1838
+ * CALL <HASH> [RRR|RR73|73]
1839
+ * CQ NONSTDCALL
1840
+ *
1841
+ * Bit layout: n12(12) n58(58) iflip(1) nrpt(2) icq(1) i3=4(3) → 77 bits
1842
+ */
1843
+ function tryPackType4(parts) {
1844
+ if (parts.length < 2 || parts.length > 3)
1845
+ return null;
1846
+ const w1 = parts[0];
1847
+ const w2 = parts[1];
1848
+ const w3 = parts[2]; // optional
1849
+ let icq = 0;
1850
+ let iflip = 0;
1851
+ let n12 = 0;
1852
+ let n58 = 0n;
1853
+ let nrpt = 0;
1854
+ const parsedW1 = parseCallsign(w1);
1855
+ const parsedW2 = parseCallsign(w2);
1856
+ // If both are standard callsigns (no hash), type 4 doesn't apply
1857
+ if (parsedW1.isStandard && parsedW2.isStandard && !w1.startsWith("<") && !w2.startsWith("<"))
1858
+ return null;
1859
+ if (w1 === "CQ") {
1860
+ // CQ <nonstdcall>
1861
+ if (w2.length <= 4)
1862
+ return null; // too short for type 4
1863
+ icq = 1;
1864
+ iflip = 0;
1865
+ // save_hash_call updates n12 with ihashcall12 of the callsign
1866
+ n12 = ihashcall12(w2);
1867
+ const c11 = w2.padStart(11, " ");
1868
+ n58 = encodeC11(c11);
1869
+ nrpt = 0;
1870
+ }
1871
+ else if (w1.startsWith("<") && w1.endsWith(">")) {
1872
+ // <HASH> CALL [rpt]
1873
+ iflip = 0;
1874
+ const inner = w1.slice(1, -1);
1875
+ n12 = ihashcall12(inner);
1876
+ const c11 = w2.padStart(11, " ");
1877
+ n58 = encodeC11(c11);
1878
+ nrpt = decodeRpt(w3);
1879
+ }
1880
+ else if (w2.startsWith("<") && w2.endsWith(">")) {
1881
+ // CALL <HASH> [rpt]
1882
+ iflip = 1;
1883
+ const inner = w2.slice(1, -1);
1884
+ n12 = ihashcall12(inner);
1885
+ const c11 = w1.padStart(11, " ");
1886
+ n58 = encodeC11(c11);
1887
+ nrpt = decodeRpt(w3);
1888
+ }
1889
+ else {
1890
+ return null;
1891
+ }
1892
+ const i3 = 4;
1893
+ const bits = [];
1894
+ appendBits(bits, n12, 12);
1895
+ // n58 is a BigInt, need 58 bits
1896
+ for (let b = 57; b >= 0; b--) {
1897
+ bits.push(Number((n58 >> BigInt(b)) & 1n));
1898
+ }
1899
+ appendBits(bits, iflip, 1);
1900
+ appendBits(bits, nrpt, 2);
1901
+ appendBits(bits, icq, 1);
1902
+ appendBits(bits, i3, 3);
1903
+ return bits;
1904
+ }
1905
+ function ihashcall12(c0) {
1906
+ let n8 = 0n;
1907
+ const s = c0.padEnd(11, " ").slice(0, 11).toUpperCase();
1908
+ for (let i = 0; i < 11; i++) {
1909
+ const j = C38.indexOf(s[i] ?? " ");
1910
+ n8 = 38n * n8 + BigInt(j < 0 ? 0 : j);
1911
+ }
1912
+ const MAGIC = 47055833459n;
1913
+ const prod = BigInt.asUintN(64, MAGIC * n8);
1914
+ return Number(prod >> 52n) & 0xfff; // 12 bits
1915
+ }
1916
+ function encodeC11(c11) {
1917
+ const padded = c11.padStart(11, " ");
1918
+ let n = 0n;
1919
+ for (let i = 0; i < 11; i++) {
1920
+ const j = C38.indexOf(padded[i].toUpperCase());
1921
+ n = n * 38n + BigInt(j < 0 ? 0 : j);
1922
+ }
1923
+ return n;
1924
+ }
1925
+ function decodeRpt(w) {
1926
+ if (!w)
1927
+ return 0;
1928
+ if (w === "RRR")
1929
+ return 1;
1930
+ if (w === "RR73")
1931
+ return 2;
1932
+ if (w === "73")
1933
+ return 3;
1934
+ return 0;
1935
+ }
1936
+ function packFreeText(msg) {
1937
+ // Truncate to 13 chars, only characters from FTALPH
1938
+ const raw = msg.slice(0, 13).toUpperCase();
1939
+ const bits71 = packtext77(raw);
1940
+ // Type 0.0: n3=0, i3=0 → last 6 bits are 000 000
1941
+ const bits = [...bits71, 0, 0, 0, 0, 0, 0];
1942
+ return bits; // 77 bits
1943
+ }
1944
+
1945
+ const TWO_PI = 2 * Math.PI;
1946
+ const DEFAULT_SAMPLE_RATE = 12_000;
1947
+ const DEFAULT_SAMPLES_PER_SYMBOL = 1_920;
1948
+ const DEFAULT_BT = 2.0;
1949
+ const MODULATION_INDEX = 1.0;
1950
+ function assertPositiveFinite(value, name) {
1951
+ if (!Number.isFinite(value) || value <= 0) {
1952
+ throw new Error(`${name} must be a positive finite number`);
1953
+ }
1954
+ }
1955
+ // Abramowitz and Stegun 7.1.26 approximation.
1956
+ function erfApprox(x) {
1957
+ const sign = x < 0 ? -1 : 1;
1958
+ const ax = Math.abs(x);
1959
+ const t = 1 / (1 + 0.3275911 * ax);
1960
+ const y = 1 -
1961
+ ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *
1962
+ t *
1963
+ Math.exp(-ax * ax);
1964
+ return sign * y;
1965
+ }
1966
+ function gfskPulse(bt, tt) {
1967
+ // Same expression used by lib/ft2/gfsk_pulse.f90.
1968
+ const scale = Math.PI * Math.sqrt(2 / Math.log(2)) * bt;
1969
+ return 0.5 * (erfApprox(scale * (tt + 0.5)) - erfApprox(scale * (tt - 0.5)));
1970
+ }
1971
+ function generateFT8Waveform(tones, options = {}) {
1972
+ // Mirrors the FT8 path in lib/ft8/gen_ft8wave.f90.
1973
+ const nsym = tones.length;
1974
+ if (nsym === 0) {
1975
+ return new Float32Array(0);
1976
+ }
1977
+ const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
1978
+ const nsps = options.samplesPerSymbol ?? DEFAULT_SAMPLES_PER_SYMBOL;
1979
+ const bt = options.bt ?? DEFAULT_BT;
1980
+ const f0 = options.baseFrequency ?? 0;
1981
+ assertPositiveFinite(sampleRate, "sampleRate");
1982
+ assertPositiveFinite(nsps, "samplesPerSymbol");
1983
+ assertPositiveFinite(bt, "bt");
1984
+ if (!Number.isFinite(f0)) {
1985
+ throw new Error("baseFrequency must be finite");
1986
+ }
1987
+ if (!Number.isInteger(nsps)) {
1988
+ throw new Error("samplesPerSymbol must be an integer");
1989
+ }
1990
+ const nwave = nsym * nsps;
1991
+ const pulse = new Float64Array(3 * nsps);
1992
+ for (let i = 0; i < pulse.length; i++) {
1993
+ const tt = (i + 1 - 1.5 * nsps) / nsps;
1994
+ pulse[i] = gfskPulse(bt, tt);
1995
+ }
1996
+ const dphi = new Float64Array((nsym + 2) * nsps);
1997
+ const dphiPeak = (TWO_PI * MODULATION_INDEX) / nsps;
1998
+ for (let j = 0; j < nsym; j++) {
1999
+ const tone = tones[j];
2000
+ const ib = j * nsps;
2001
+ for (let i = 0; i < pulse.length; i++) {
2002
+ dphi[ib + i] += dphiPeak * pulse[i] * tone;
2003
+ }
2004
+ }
2005
+ const firstTone = tones[0];
2006
+ const lastTone = tones[nsym - 1];
2007
+ const tailBase = nsym * nsps;
2008
+ for (let i = 0; i < 2 * nsps; i++) {
2009
+ dphi[i] += dphiPeak * firstTone * pulse[nsps + i];
2010
+ dphi[tailBase + i] += dphiPeak * lastTone * pulse[i];
2011
+ }
2012
+ const carrierDphi = (TWO_PI * f0) / sampleRate;
2013
+ for (let i = 0; i < dphi.length; i++) {
2014
+ dphi[i] += carrierDphi;
2015
+ }
2016
+ const wave = new Float32Array(nwave);
2017
+ let phi = 0;
2018
+ for (let k = 0; k < nwave; k++) {
2019
+ const j = nsps + k; // skip the leading dummy symbol
2020
+ wave[k] = Math.sin(phi);
2021
+ phi += dphi[j];
2022
+ phi %= TWO_PI;
2023
+ if (phi < 0) {
2024
+ phi += TWO_PI;
2025
+ }
2026
+ }
2027
+ const nramp = Math.round(nsps / 8);
2028
+ for (let i = 0; i < nramp; i++) {
2029
+ const up = (1 - Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
2030
+ wave[i] *= up;
2031
+ }
2032
+ const tailStart = nwave - nramp;
2033
+ for (let i = 0; i < nramp; i++) {
2034
+ const down = (1 + Math.cos((TWO_PI * i) / (2 * nramp))) / 2;
2035
+ wave[tailStart + i] *= down;
2036
+ }
2037
+ return wave;
2038
+ }
2039
+
2040
+ function generateLdpcGMatrix() {
2041
+ const K = 91;
2042
+ const M = 83; // 174 - 91
2043
+ const gen = Array.from({ length: M }, () => new Array(K).fill(0));
2044
+ for (let i = 0; i < M; i++) {
2045
+ const hexStr = gHex[i];
2046
+ for (let j = 0; j < 23; j++) {
2047
+ const val = parseInt(hexStr[j], 16);
2048
+ const limit = j === 22 ? 3 : 4;
2049
+ for (let jj = 1; jj <= limit; jj++) {
2050
+ const col = j * 4 + jj - 1; // 0-indexed
2051
+ if ((val & (1 << (4 - jj))) !== 0) {
2052
+ gen[i][col] = 1;
2053
+ }
2054
+ }
2055
+ }
2056
+ }
2057
+ return gen;
2058
+ }
2059
+ const G = generateLdpcGMatrix();
2060
+ function encode174_91(msg77) {
2061
+ const poly = 0x2757;
2062
+ let crc = 0;
2063
+ // padded with 19 zeros (3 zeros + 16 zero-bits for flush)
2064
+ const bitArray = [...msg77, 0, 0, 0, ...new Array(16).fill(0)];
2065
+ for (let bit = 0; bit < 96; bit++) {
2066
+ const nextBit = bitArray[bit];
2067
+ if ((crc & 0x2000) !== 0) {
2068
+ crc = ((crc << 1) | nextBit) ^ poly;
2069
+ }
2070
+ else {
2071
+ crc = (crc << 1) | nextBit;
2072
+ }
2073
+ crc &= 0x3fff;
2074
+ }
2075
+ const msg91 = [...msg77];
2076
+ for (let i = 0; i < 14; i++) {
2077
+ msg91.push((crc >> (13 - i)) & 1);
2078
+ }
2079
+ const codeword = [...msg91];
2080
+ for (let i = 0; i < 83; i++) {
2081
+ let sum = 0;
2082
+ for (let j = 0; j < 91; j++) {
2083
+ sum += msg91[j] * G[i][j];
2084
+ }
2085
+ codeword.push(sum % 2);
2086
+ }
2087
+ return codeword;
2088
+ }
2089
+ function getTones(codeword) {
2090
+ const tones = new Array(79).fill(0);
2091
+ for (let i = 0; i < 7; i++)
2092
+ tones[i] = icos7[i];
2093
+ for (let i = 0; i < 7; i++)
2094
+ tones[36 + i] = icos7[i];
2095
+ for (let i = 0; i < 7; i++)
2096
+ tones[72 + i] = icos7[i];
2097
+ let k = 7;
2098
+ for (let j = 1; j <= 58; j++) {
2099
+ const i = j * 3 - 3; // codeword is 0-indexed in JS, but the loop was j=1 to 58
2100
+ if (j === 30)
2101
+ k += 7;
2102
+ const indx = codeword[i] * 4 + codeword[i + 1] * 2 + codeword[i + 2];
2103
+ tones[k] = graymap[indx];
2104
+ k++;
2105
+ }
2106
+ return tones;
2107
+ }
2108
+ function encodeMessage(msg) {
2109
+ const bits77 = pack77(msg);
2110
+ const codeword = encode174_91(bits77);
2111
+ return getTones(codeword);
2112
+ }
2113
+ function encode(msg, options = {}) {
2114
+ return generateFT8Waveform(encodeMessage(msg), options);
2115
+ }
2116
+
2117
+ exports.decodeFT8 = decode;
2118
+ exports.encodeFT8 = encode;
2119
+ //# sourceMappingURL=ft8ts.cjs.map