@e04/ft8ts 0.0.9 → 0.0.10

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/src/ft4/decode.ts CHANGED
@@ -3,29 +3,44 @@ import { decode174_91 } from "../util/decode174_91.js";
3
3
  import { fftComplex } from "../util/fft.js";
4
4
  import type { HashCallBook } from "../util/hashcall.js";
5
5
  import { unpack77 } from "../util/unpack_jt77.js";
6
- import {
7
- COSTAS_A,
8
- COSTAS_B,
9
- COSTAS_C,
10
- COSTAS_D,
11
- FS2,
12
- GRAYMAP,
13
- HARD_SYNC_PATTERNS,
14
- MAX_FREQ,
15
- NDOWN,
16
- NFFT1,
17
- NFFT2,
18
- NH1,
19
- NHSYM,
20
- NMAX,
21
- NN,
22
- NSPS,
23
- NSS,
24
- SYNC_PASS_MIN,
25
- TWO_PI,
26
- } from "./constants.js";
6
+ import { GRAYMAP } from "./constants.js";
27
7
  import { xorWithScrambler } from "./scramble.js";
28
8
 
9
+ const COSTAS_A = [0, 1, 3, 2] as const;
10
+ const COSTAS_B = [1, 0, 2, 3] as const;
11
+ const COSTAS_C = [2, 3, 1, 0] as const;
12
+ const COSTAS_D = [3, 2, 0, 1] as const;
13
+ const NSPS = 576;
14
+ const NFFT1 = 4 * NSPS; // 2304
15
+ const NH1 = NFFT1 / 2; // 1152
16
+ const NMAX = 21 * 3456; // 72576
17
+ const NHSYM = Math.floor((NMAX - NFFT1) / NSPS); // 122
18
+ const NDOWN = 18;
19
+ const NN = 103;
20
+ const NFFT2 = NMAX / NDOWN; // 4032
21
+ const NSS = NSPS / NDOWN; // 32
22
+ const FS2 = SAMPLE_RATE / NDOWN; // 666.67 Hz
23
+ const MAX_FREQ = 4910;
24
+ const SYNC_PASS_MIN = 1.2;
25
+ const TWO_PI = 2 * Math.PI;
26
+ const HARD_SYNC_PATTERNS = [
27
+ { offset: 0, bits: [0, 0, 0, 1, 1, 0, 1, 1] as const },
28
+ { offset: 66, bits: [0, 1, 0, 0, 1, 1, 1, 0] as const },
29
+ { offset: 132, bits: [1, 1, 1, 0, 0, 1, 0, 0] as const },
30
+ { offset: 198, bits: [1, 0, 1, 1, 0, 0, 0, 1] as const },
31
+ ] as const;
32
+
33
+ const COSTAS_BLOCKS = 4;
34
+ const FT4_SYNC_STRIDE = 33 * NSS;
35
+ const FT4_MAX_TWEAK = 16;
36
+ const LDPC_BITS = 174;
37
+ const BITMETRIC_LEN = 2 * NN;
38
+ const FRAME_LEN = NN * NSS;
39
+
40
+ const NUTTALL_WINDOW = makeNuttallWindow(NFFT1);
41
+ const DOWNSAMPLE_CTX = createDownsampleContext();
42
+ const TWEAKED_SYNC_TEMPLATES = createTweakedSyncTemplates();
43
+
29
44
  export interface DecodedMessage {
30
45
  freq: number;
31
46
  dt: number;
@@ -59,11 +74,6 @@ interface Candidate {
59
74
  sync: number;
60
75
  }
61
76
 
62
- interface ComplexBuffer {
63
- re: Float64Array;
64
- im: Float64Array;
65
- }
66
-
67
77
  interface DownsampleContext {
68
78
  df: number;
69
79
  window: Float64Array;
@@ -76,11 +86,33 @@ interface SyncTemplate {
76
86
 
77
87
  type SyncTemplates = [SyncTemplate, SyncTemplate, SyncTemplate, SyncTemplate];
78
88
 
79
- interface Ft4BitMetrics {
89
+ interface DecodeWorkspace {
90
+ coarseRe: Float64Array;
91
+ coarseIm: Float64Array;
92
+ fineRe: Float64Array;
93
+ fineIm: Float64Array;
94
+ frameRe: Float64Array;
95
+ frameIm: Float64Array;
96
+ symbRe: Float64Array;
97
+ symbIm: Float64Array;
98
+ csRe: Float64Array;
99
+ csIm: Float64Array;
100
+ s4: Float64Array;
101
+ s2: Float64Array;
80
102
  bitmetrics1: Float64Array;
81
103
  bitmetrics2: Float64Array;
82
104
  bitmetrics3: Float64Array;
83
- badsync: boolean;
105
+ llra: Float64Array;
106
+ llrb: Float64Array;
107
+ llrc: Float64Array;
108
+ llr: Float64Array;
109
+ apmask: Int8Array;
110
+ }
111
+
112
+ interface CandidateSearchResult {
113
+ ibest: number;
114
+ idfbest: number;
115
+ smax: number;
84
116
  }
85
117
 
86
118
  /**
@@ -101,44 +133,26 @@ export function decode(
101
133
 
102
134
  const dd =
103
135
  sampleRate === SAMPLE_RATE
104
- ? copyIntoFt4Buffer(samples)
136
+ ? copySamplesToDecodeWindow(samples)
105
137
  : resample(samples, sampleRate, SAMPLE_RATE, NMAX);
106
138
 
107
139
  const cxRe = new Float64Array(NMAX);
108
140
  const cxIm = new Float64Array(NMAX);
109
- for (let i = 0; i < NMAX; i++) {
110
- cxRe[i] = dd[i] ?? 0;
111
- }
141
+ for (let i = 0; i < NMAX; i++) cxRe[i] = dd[i] ?? 0;
112
142
  fftComplex(cxRe, cxIm, false);
113
143
 
114
144
  const candidates = getCandidates4(dd, freqLow, freqHigh, syncMin, maxCandidates);
115
- if (candidates.length === 0) {
116
- return [];
117
- }
145
+ if (candidates.length === 0) return [];
118
146
 
119
- const downsampleCtx = createDownsampleContext();
120
- const tweakedSyncTemplates = createTweakedSyncTemplates();
147
+ const workspace = createDecodeWorkspace();
121
148
  const decoded: DecodedMessage[] = [];
122
149
  const seenMessages = new Set<string>();
123
- const apmask = new Int8Array(174);
124
150
 
125
151
  for (const candidate of candidates) {
126
- const one = decodeCandidate(
127
- candidate,
128
- cxRe,
129
- cxIm,
130
- downsampleCtx,
131
- tweakedSyncTemplates,
132
- depth,
133
- book,
134
- apmask,
135
- );
136
- if (!one) {
137
- continue;
138
- }
139
- if (seenMessages.has(one.msg)) {
140
- continue;
141
- }
152
+ const one = decodeCandidate(candidate, cxRe, cxIm, depth, book, workspace);
153
+ if (!one) continue;
154
+ if (seenMessages.has(one.msg)) continue;
155
+
142
156
  seenMessages.add(one.msg);
143
157
  decoded.push(one);
144
158
  }
@@ -146,145 +160,152 @@ export function decode(
146
160
  return decoded;
147
161
  }
148
162
 
163
+ function createDecodeWorkspace(): DecodeWorkspace {
164
+ return {
165
+ coarseRe: new Float64Array(NFFT2),
166
+ coarseIm: new Float64Array(NFFT2),
167
+ fineRe: new Float64Array(NFFT2),
168
+ fineIm: new Float64Array(NFFT2),
169
+ frameRe: new Float64Array(FRAME_LEN),
170
+ frameIm: new Float64Array(FRAME_LEN),
171
+ symbRe: new Float64Array(NSS),
172
+ symbIm: new Float64Array(NSS),
173
+ csRe: new Float64Array(4 * NN),
174
+ csIm: new Float64Array(4 * NN),
175
+ s4: new Float64Array(4 * NN),
176
+ s2: new Float64Array(1 << 8),
177
+ bitmetrics1: new Float64Array(BITMETRIC_LEN),
178
+ bitmetrics2: new Float64Array(BITMETRIC_LEN),
179
+ bitmetrics3: new Float64Array(BITMETRIC_LEN),
180
+ llra: new Float64Array(LDPC_BITS),
181
+ llrb: new Float64Array(LDPC_BITS),
182
+ llrc: new Float64Array(LDPC_BITS),
183
+ llr: new Float64Array(LDPC_BITS),
184
+ apmask: new Int8Array(LDPC_BITS),
185
+ };
186
+ }
187
+
188
+ function copySamplesToDecodeWindow(samples: Float32Array | Float64Array): Float64Array {
189
+ const out = new Float64Array(NMAX);
190
+ const len = Math.min(samples.length, NMAX);
191
+ for (let i = 0; i < len; i++) out[i] = samples[i]!;
192
+ return out;
193
+ }
194
+
149
195
  function decodeCandidate(
150
196
  candidate: Candidate,
151
197
  cxRe: Float64Array,
152
198
  cxIm: Float64Array,
153
- downsampleCtx: DownsampleContext,
154
- tweakedSyncTemplates: Map<number, SyncTemplates>,
155
199
  depth: number,
156
200
  book: HashCallBook | undefined,
157
- apmask: Int8Array,
201
+ workspace: DecodeWorkspace,
158
202
  ): DecodedMessage | null {
159
- const cd2 = ft4Downsample(cxRe, cxIm, candidate.freq, downsampleCtx);
160
- normalizeComplexPower(cd2.re, cd2.im, NMAX / NDOWN);
203
+ ft4Downsample(cxRe, cxIm, candidate.freq, DOWNSAMPLE_CTX, workspace.coarseRe, workspace.coarseIm);
204
+ normalizeComplexPower(workspace.coarseRe, workspace.coarseIm, NMAX / NDOWN);
161
205
 
162
206
  for (let segment = 1; segment <= 3; segment++) {
163
- let ibest = -1;
164
- let idfbest = 0;
165
- let smax = -99;
166
-
167
- for (let isync = 1; isync <= 2; isync++) {
168
- let idfmin: number;
169
- let idfmax: number;
170
- let idfstp: number;
171
- let ibmin: number;
172
- let ibmax: number;
173
- let ibstp: number;
174
-
175
- if (isync === 1) {
176
- idfmin = -12;
177
- idfmax = 12;
178
- idfstp = 3;
179
- ibmin = -344;
180
- ibmax = 1012;
181
- if (segment === 1) {
182
- ibmin = 108;
183
- ibmax = 560;
184
- } else if (segment === 2) {
185
- ibmin = 560;
186
- ibmax = 1012;
187
- } else {
188
- ibmin = -344;
189
- ibmax = 108;
190
- }
191
- ibstp = 4;
192
- } else {
193
- idfmin = idfbest - 4;
194
- idfmax = idfbest + 4;
195
- idfstp = 1;
196
- ibmin = ibest - 5;
197
- ibmax = ibest + 5;
198
- ibstp = 1;
199
- }
200
-
201
- for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
202
- const templates = tweakedSyncTemplates.get(idf);
203
- if (!templates) {
204
- continue;
205
- }
206
- for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
207
- const sync = sync4d(cd2.re, cd2.im, istart, templates);
208
- if (sync > smax) {
209
- smax = sync;
210
- ibest = istart;
211
- idfbest = idf;
212
- }
213
- }
214
- }
215
- }
216
-
217
- if (smax < SYNC_PASS_MIN) {
218
- continue;
219
- }
220
-
221
- const f1 = candidate.freq + idfbest;
222
- if (f1 <= 10 || f1 >= 4990) {
223
- continue;
224
- }
225
-
226
- const cb = ft4Downsample(cxRe, cxIm, f1, downsampleCtx);
227
- normalizeComplexPower(cb.re, cb.im, NSS * NN);
228
- const frame = extractFrame(cb.re, cb.im, ibest);
229
- const metrics = getFt4Bitmetrics(frame.re, frame.im);
230
-
231
- if (metrics.badsync) {
232
- continue;
233
- }
234
- if (!passesHardSyncQuality(metrics.bitmetrics1)) {
235
- continue;
236
- }
237
-
238
- const [llra, llrb, llrc] = buildLlrs(
239
- metrics.bitmetrics1,
240
- metrics.bitmetrics2,
241
- metrics.bitmetrics3,
207
+ const coarse = findBestSyncLocation(workspace.coarseRe, workspace.coarseIm, segment);
208
+ if (coarse.smax < SYNC_PASS_MIN) continue;
209
+
210
+ const f1 = candidate.freq + coarse.idfbest;
211
+ if (f1 <= 10 || f1 >= 4990) continue;
212
+
213
+ ft4Downsample(cxRe, cxIm, f1, DOWNSAMPLE_CTX, workspace.fineRe, workspace.fineIm);
214
+ normalizeComplexPower(workspace.fineRe, workspace.fineIm, NSS * NN);
215
+ extractFrame(
216
+ workspace.fineRe,
217
+ workspace.fineIm,
218
+ coarse.ibest,
219
+ workspace.frameRe,
220
+ workspace.frameIm,
242
221
  );
243
- const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
244
- const scalefac = 2.83;
245
222
 
246
- for (const src of [llra, llrb, llrc]) {
247
- const llr = new Float64Array(174);
248
- for (let i = 0; i < 174; i++) {
249
- llr[i] = scalefac * src[i]!;
250
- }
223
+ const badsync = buildBitMetrics(workspace.frameRe, workspace.frameIm, workspace);
224
+ if (badsync) continue;
225
+ if (!passesHardSyncQuality(workspace.bitmetrics1)) continue;
251
226
 
252
- const result = decode174_91(llr, apmask, maxosd);
253
- if (!result) {
254
- continue;
255
- }
227
+ buildLlrs(workspace);
228
+ const result = tryDecodePasses(workspace, depth);
229
+ if (!result) continue;
256
230
 
257
- const message77Scrambled = result.message91.slice(0, 77);
258
- if (!hasNonZeroBit(message77Scrambled)) {
259
- continue;
260
- }
231
+ const message77Scrambled = result.message91.slice(0, 77);
232
+ if (!hasNonZeroBit(message77Scrambled)) continue;
261
233
 
262
- const message77 = xorWithScrambler(message77Scrambled);
263
- const { msg, success } = unpack77(message77, book);
264
- if (!success || msg.trim().length === 0) {
265
- continue;
266
- }
234
+ const message77 = xorWithScrambler(message77Scrambled);
235
+ const { msg, success } = unpack77(message77, book);
236
+ if (!success || msg.trim().length === 0) continue;
267
237
 
268
- return {
269
- freq: f1,
270
- dt: ibest / FS2 - 0.5,
271
- snr: toFt4Snr(candidate.sync - 1.0),
272
- msg,
273
- sync: smax,
274
- };
275
- }
238
+ return {
239
+ freq: f1,
240
+ dt: coarse.ibest / FS2 - 0.5,
241
+ snr: toFt4Snr(candidate.sync - 1.0),
242
+ msg,
243
+ sync: coarse.smax,
244
+ };
276
245
  }
277
246
 
278
247
  return null;
279
248
  }
280
249
 
281
- function copyIntoFt4Buffer(samples: Float32Array | Float64Array): Float64Array {
282
- const out = new Float64Array(NMAX);
283
- const len = Math.min(samples.length, NMAX);
284
- for (let i = 0; i < len; i++) {
285
- out[i] = samples[i]!;
250
+ function findBestSyncLocation(
251
+ cdRe: Float64Array,
252
+ cdIm: Float64Array,
253
+ segment: number,
254
+ ): CandidateSearchResult {
255
+ let ibest = -1;
256
+ let idfbest = 0;
257
+ let smax = -99;
258
+
259
+ for (let isync = 1; isync <= 2; isync++) {
260
+ let idfmin: number;
261
+ let idfmax: number;
262
+ let idfstp: number;
263
+ let ibmin: number;
264
+ let ibmax: number;
265
+ let ibstp: number;
266
+
267
+ if (isync === 1) {
268
+ idfmin = -12;
269
+ idfmax = 12;
270
+ idfstp = 3;
271
+ ibmin = -344;
272
+ ibmax = 1012;
273
+ if (segment === 1) {
274
+ ibmin = 108;
275
+ ibmax = 560;
276
+ } else if (segment === 2) {
277
+ ibmin = 560;
278
+ ibmax = 1012;
279
+ } else {
280
+ ibmin = -344;
281
+ ibmax = 108;
282
+ }
283
+ ibstp = 4;
284
+ } else {
285
+ idfmin = idfbest - 4;
286
+ idfmax = idfbest + 4;
287
+ idfstp = 1;
288
+ ibmin = ibest - 5;
289
+ ibmax = ibest + 5;
290
+ ibstp = 1;
291
+ }
292
+
293
+ for (let idf = idfmin; idf <= idfmax; idf += idfstp) {
294
+ const templates = TWEAKED_SYNC_TEMPLATES.get(idf);
295
+ if (!templates) continue;
296
+
297
+ for (let istart = ibmin; istart <= ibmax; istart += ibstp) {
298
+ const sync = sync4d(cdRe, cdIm, istart, templates);
299
+ if (sync > smax) {
300
+ smax = sync;
301
+ ibest = istart;
302
+ idfbest = idf;
303
+ }
304
+ }
305
+ }
286
306
  }
287
- return out;
307
+
308
+ return { ibest, idfbest, smax };
288
309
  }
289
310
 
290
311
  function getCandidates4(
@@ -296,7 +317,6 @@ function getCandidates4(
296
317
  ): Candidate[] {
297
318
  const df = SAMPLE_RATE / NFFT1;
298
319
  const fac = 1 / 300;
299
- const window = makeNuttallWindow(NFFT1);
300
320
  const savg = new Float64Array(NH1);
301
321
  const s = new Float64Array(NH1 * NHSYM);
302
322
  const savsm = new Float64Array(NH1);
@@ -307,13 +327,10 @@ function getCandidates4(
307
327
  for (let j = 0; j < NHSYM; j++) {
308
328
  const ia = j * NSPS;
309
329
  const ib = ia + NFFT1;
310
- if (ib > NMAX) {
311
- break;
312
- }
330
+ if (ib > NMAX) break;
331
+
313
332
  xIm.fill(0);
314
- for (let i = 0; i < NFFT1; i++) {
315
- xRe[i] = fac * dd[ia + i]! * window[i]!;
316
- }
333
+ for (let i = 0; i < NFFT1; i++) xRe[i] = fac * dd[ia + i]! * NUTTALL_WINDOW[i]!;
317
334
  fftComplex(xRe, xIm, false);
318
335
 
319
336
  for (let bin = 1; bin <= NH1; bin++) {
@@ -326,32 +343,22 @@ function getCandidates4(
326
343
  }
327
344
  }
328
345
 
329
- for (let i = 0; i < NH1; i++) {
330
- savg[i] = (savg[i] ?? 0) / NHSYM;
331
- }
346
+ for (let i = 0; i < NH1; i++) savg[i] = (savg[i] ?? 0) / NHSYM;
332
347
 
333
348
  for (let i = 7; i < NH1 - 7; i++) {
334
349
  let sum = 0;
335
- for (let j = i - 7; j <= i + 7; j++) {
336
- sum += savg[j]!;
337
- }
350
+ for (let j = i - 7; j <= i + 7; j++) sum += savg[j]!;
338
351
  savsm[i] = sum / 15;
339
352
  }
340
353
 
341
354
  let nfa = Math.round(freqLow / df);
342
- if (nfa < Math.round(200 / df)) {
343
- nfa = Math.round(200 / df);
344
- }
355
+ if (nfa < Math.round(200 / df)) nfa = Math.round(200 / df);
345
356
  let nfb = Math.round(freqHigh / df);
346
- if (nfb > Math.round(MAX_FREQ / df)) {
347
- nfb = Math.round(MAX_FREQ / df);
348
- }
357
+ if (nfb > Math.round(MAX_FREQ / df)) nfb = Math.round(MAX_FREQ / df);
349
358
 
350
359
  const sbase = ft4Baseline(savg, nfa, nfb, df);
351
360
  for (let bin = nfa; bin <= nfb; bin++) {
352
- if ((sbase[bin - 1] ?? 0) <= 0) {
353
- return [];
354
- }
361
+ if ((sbase[bin - 1] ?? 0) <= 0) return [];
355
362
  }
356
363
 
357
364
  for (let bin = nfa; bin <= nfb; bin++) {
@@ -370,9 +377,7 @@ function getCandidates4(
370
377
  const den = left - 2 * center + right;
371
378
  const del = den !== 0 ? (0.5 * (left - right)) / den : 0;
372
379
  const fpeak = (i + del) * df + fOffset;
373
- if (fpeak < 200 || fpeak > MAX_FREQ) {
374
- continue;
375
- }
380
+ if (fpeak < 200 || fpeak > MAX_FREQ) continue;
376
381
  const speak = center - 0.25 * (left - right) * del;
377
382
  candidates.push({ freq: fpeak, sync: speak });
378
383
  }
@@ -404,14 +409,10 @@ function ft4Baseline(savg: Float64Array, nfa: number, nfb: number, df: number):
404
409
 
405
410
  const ia = Math.max(Math.round(200 / df), nfa);
406
411
  const ib = Math.min(NH1, nfb);
407
- if (ib <= ia) {
408
- return sbase;
409
- }
412
+ if (ib <= ia) return sbase;
410
413
 
411
414
  const sDb = new Float64Array(NH1);
412
- for (let i = ia; i <= ib; i++) {
413
- sDb[i - 1] = 10 * Math.log10(Math.max(1e-30, savg[i - 1]!));
414
- }
415
+ for (let i = ia; i <= ib; i++) sDb[i - 1] = 10 * Math.log10(Math.max(1e-30, savg[i - 1]!));
415
416
 
416
417
  const nseg = 10;
417
418
  const npct = 10;
@@ -420,18 +421,13 @@ function ft4Baseline(savg: Float64Array, nfa: number, nfb: number, df: number):
420
421
 
421
422
  const x: number[] = [];
422
423
  const y: number[] = [];
423
-
424
424
  for (let seg = 0; seg < nseg; seg++) {
425
425
  const ja = ia + seg * nlen;
426
- if (ja > ib) {
427
- break;
428
- }
426
+ if (ja > ib) break;
429
427
  const jb = Math.min(ib, ja + nlen - 1);
430
428
 
431
429
  const vals: number[] = [];
432
- for (let i = ja; i <= jb; i++) {
433
- vals.push(sDb[i - 1]!);
434
- }
430
+ for (let i = ja; i <= jb; i++) vals.push(sDb[i - 1]!);
435
431
  const base = percentile(vals, npct);
436
432
 
437
433
  for (let i = ja; i <= jb; i++) {
@@ -444,7 +440,6 @@ function ft4Baseline(savg: Float64Array, nfa: number, nfb: number, df: number):
444
440
  }
445
441
 
446
442
  const coeff = x.length >= 5 ? polyfitLeastSquares(x, y, 4) : null;
447
-
448
443
  if (coeff) {
449
444
  for (let i = ia; i <= ib; i++) {
450
445
  const t = i - i0;
@@ -471,9 +466,7 @@ function ft4Baseline(savg: Float64Array, nfa: number, nfb: number, df: number):
471
466
  }
472
467
 
473
468
  function percentile(values: readonly number[], pct: number): number {
474
- if (values.length === 0) {
475
- return 0;
476
- }
469
+ if (values.length === 0) return 0;
477
470
  const sorted = [...values].sort((a, b) => a - b);
478
471
  const idx = Math.max(
479
472
  0,
@@ -493,20 +486,14 @@ function polyfitLeastSquares(
493
486
  const xPows = new Float64Array(2 * degree + 1);
494
487
  for (let p = 0; p <= 2 * degree; p++) {
495
488
  let sum = 0;
496
- for (let i = 0; i < x.length; i++) {
497
- sum += x[i]! ** p;
498
- }
489
+ for (let i = 0; i < x.length; i++) sum += x[i]! ** p;
499
490
  xPows[p] = sum;
500
491
  }
501
492
 
502
493
  for (let row = 0; row < n; row++) {
503
- for (let col = 0; col < n; col++) {
504
- mat[row]![col] = xPows[row + col]!;
505
- }
494
+ for (let col = 0; col < n; col++) mat[row]![col] = xPows[row + col]!;
506
495
  let rhs = 0;
507
- for (let i = 0; i < x.length; i++) {
508
- rhs += y[i]! * x[i]! ** row;
509
- }
496
+ for (let i = 0; i < x.length; i++) rhs += y[i]! * x[i]! ** row;
510
497
  mat[row]![n] = rhs;
511
498
  }
512
499
 
@@ -520,9 +507,7 @@ function polyfitLeastSquares(
520
507
  pivot = row;
521
508
  }
522
509
  }
523
- if (maxAbs < 1e-12) {
524
- return null;
525
- }
510
+ if (maxAbs < 1e-12) return null;
526
511
  if (pivot !== col) {
527
512
  const tmp = mat[col]!;
528
513
  mat[col] = mat[pivot]!;
@@ -530,28 +515,18 @@ function polyfitLeastSquares(
530
515
  }
531
516
 
532
517
  const pivotVal = mat[col]![col]!;
533
- for (let c = col; c <= n; c++) {
534
- mat[col]![c] = mat[col]![c]! / pivotVal;
535
- }
518
+ for (let c = col; c <= n; c++) mat[col]![c] = mat[col]![c]! / pivotVal;
536
519
 
537
520
  for (let row = 0; row < n; row++) {
538
- if (row === col) {
539
- continue;
540
- }
521
+ if (row === col) continue;
541
522
  const factor = mat[row]![col]!;
542
- if (factor === 0) {
543
- continue;
544
- }
545
- for (let c = col; c <= n; c++) {
546
- mat[row]![c] = mat[row]![c]! - factor * mat[col]![c]!;
547
- }
523
+ if (factor === 0) continue;
524
+ for (let c = col; c <= n; c++) mat[row]![c] = mat[row]![c]! - factor * mat[col]![c]!;
548
525
  }
549
526
  }
550
527
 
551
528
  const coeff = new Array<number>(n);
552
- for (let i = 0; i < n; i++) {
553
- coeff[i] = mat[i]![n]!;
554
- }
529
+ for (let i = 0; i < n; i++) coeff[i] = mat[i]![n]!;
555
530
  return coeff;
556
531
  }
557
532
 
@@ -568,9 +543,7 @@ function createDownsampleContext(): DownsampleContext {
568
543
  for (let i = 0; i < iwt && i < raw.length; i++) {
569
544
  raw[i] = 0.5 * (1 + Math.cos((Math.PI * (iwt - 1 - i)) / iwt));
570
545
  }
571
- for (let i = iwt; i < iwt + iwf && i < raw.length; i++) {
572
- raw[i] = 1;
573
- }
546
+ for (let i = iwt; i < iwt + iwf && i < raw.length; i++) raw[i] = 1;
574
547
  for (let i = iwt + iwf; i < 2 * iwt + iwf && i < raw.length; i++) {
575
548
  raw[i] = 0.5 * (1 + Math.cos((Math.PI * (i - (iwt + iwf))) / iwt));
576
549
  }
@@ -589,49 +562,47 @@ function ft4Downsample(
589
562
  cxIm: Float64Array,
590
563
  f0: number,
591
564
  ctx: DownsampleContext,
592
- ): ComplexBuffer {
593
- const c1Re = new Float64Array(NFFT2);
594
- const c1Im = new Float64Array(NFFT2);
565
+ outRe: Float64Array,
566
+ outIm: Float64Array,
567
+ ): void {
568
+ outRe.fill(0);
569
+ outIm.fill(0);
595
570
  const i0 = Math.round(f0 / ctx.df);
596
571
 
597
572
  if (i0 >= 0 && i0 <= NMAX / 2) {
598
- c1Re[0] = cxRe[i0] ?? 0;
599
- c1Im[0] = cxIm[i0] ?? 0;
573
+ outRe[0] = cxRe[i0] ?? 0;
574
+ outIm[0] = cxIm[i0] ?? 0;
600
575
  }
601
576
 
602
577
  for (let i = 1; i <= NFFT2 / 2; i++) {
603
578
  const hi = i0 + i;
604
579
  if (hi >= 0 && hi <= NMAX / 2) {
605
- c1Re[i] = cxRe[hi] ?? 0;
606
- c1Im[i] = cxIm[hi] ?? 0;
580
+ outRe[i] = cxRe[hi] ?? 0;
581
+ outIm[i] = cxIm[hi] ?? 0;
607
582
  }
608
583
  const lo = i0 - i;
609
584
  if (lo >= 0 && lo <= NMAX / 2) {
610
585
  const idx = NFFT2 - i;
611
- c1Re[idx] = cxRe[lo] ?? 0;
612
- c1Im[idx] = cxIm[lo] ?? 0;
586
+ outRe[idx] = cxRe[lo] ?? 0;
587
+ outIm[idx] = cxIm[lo] ?? 0;
613
588
  }
614
589
  }
615
590
 
616
591
  const scale = 1 / NFFT2;
617
592
  for (let i = 0; i < NFFT2; i++) {
618
593
  const w = (ctx.window[i] ?? 0) * scale;
619
- c1Re[i] = c1Re[i]! * w;
620
- c1Im[i] = c1Im[i]! * w;
594
+ outRe[i] = outRe[i]! * w;
595
+ outIm[i] = outIm[i]! * w;
621
596
  }
622
597
 
623
- fftComplex(c1Re, c1Im, true);
624
- return { re: c1Re, im: c1Im };
598
+ fftComplex(outRe, outIm, true);
625
599
  }
626
600
 
627
601
  function normalizeComplexPower(re: Float64Array, im: Float64Array, denom: number): void {
628
602
  let sum = 0;
629
- for (let i = 0; i < re.length; i++) {
630
- sum += re[i]! * re[i]! + im[i]! * im[i]!;
631
- }
632
- if (sum <= 0) {
633
- return;
634
- }
603
+ for (let i = 0; i < re.length; i++) sum += re[i]! * re[i]! + im[i]! * im[i]!;
604
+ if (sum <= 0) return;
605
+
635
606
  const scale = 1 / Math.sqrt(sum / denom);
636
607
  for (let i = 0; i < re.length; i++) {
637
608
  re[i] = re[i]! * scale;
@@ -639,17 +610,23 @@ function normalizeComplexPower(re: Float64Array, im: Float64Array, denom: number
639
610
  }
640
611
  }
641
612
 
642
- function extractFrame(cbRe: Float64Array, cbIm: Float64Array, ibest: number): ComplexBuffer {
643
- const outRe = new Float64Array(NN * NSS);
644
- const outIm = new Float64Array(NN * NSS);
613
+ function extractFrame(
614
+ cbRe: Float64Array,
615
+ cbIm: Float64Array,
616
+ ibest: number,
617
+ outRe: Float64Array,
618
+ outIm: Float64Array,
619
+ ): void {
645
620
  for (let i = 0; i < outRe.length; i++) {
646
621
  const src = ibest + i;
647
622
  if (src >= 0 && src < cbRe.length) {
648
623
  outRe[i] = cbRe[src]!;
649
624
  outIm[i] = cbIm[src]!;
625
+ } else {
626
+ outRe[i] = 0;
627
+ outIm[i] = 0;
650
628
  }
651
629
  }
652
- return { re: outRe, im: outIm };
653
630
  }
654
631
 
655
632
  function createTweakedSyncTemplates(): Map<number, SyncTemplates> {
@@ -657,7 +634,7 @@ function createTweakedSyncTemplates(): Map<number, SyncTemplates> {
657
634
  const fsample = FS2 / 2;
658
635
  const out = new Map<number, SyncTemplates>();
659
636
 
660
- for (let idf = -16; idf <= 16; idf++) {
637
+ for (let idf = -FT4_MAX_TWEAK; idf <= FT4_MAX_TWEAK; idf++) {
661
638
  const tweak = createFrequencyTweak(idf, 2 * NSS, fsample);
662
639
  out.set(idf, [
663
640
  applyTweak(base[0], tweak),
@@ -684,6 +661,7 @@ function buildSyncTemplate(tones: readonly number[]): SyncTemplate {
684
661
  const im = new Float64Array(2 * NSS);
685
662
  let k = 0;
686
663
  let phi = 0;
664
+
687
665
  for (const tone of tones) {
688
666
  const dphi = (TWO_PI * tone * 2) / NSS;
689
667
  for (let j = 0; j < NSS / 2; j++) {
@@ -693,6 +671,7 @@ function buildSyncTemplate(tones: readonly number[]): SyncTemplate {
693
671
  k++;
694
672
  }
695
673
  }
674
+
696
675
  return { re, im };
697
676
  }
698
677
 
@@ -704,6 +683,7 @@ function createFrequencyTweak(idf: number, npts: number, fsample: number): SyncT
704
683
  const stepIm = Math.sin(dphi);
705
684
  let wRe = 1;
706
685
  let wIm = 0;
686
+
707
687
  for (let i = 0; i < npts; i++) {
708
688
  const newRe = wRe * stepRe - wIm * stepIm;
709
689
  const newIm = wRe * stepIm + wIm * stepRe;
@@ -712,6 +692,7 @@ function createFrequencyTweak(idf: number, npts: number, fsample: number): SyncT
712
692
  re[i] = wRe;
713
693
  im[i] = wIm;
714
694
  }
695
+
715
696
  return { re, im };
716
697
  }
717
698
 
@@ -735,13 +716,11 @@ function sync4d(
735
716
  i0: number,
736
717
  templates: SyncTemplates,
737
718
  ): number {
738
- const starts = [i0, i0 + 33 * NSS, i0 + 66 * NSS, i0 + 99 * NSS];
739
719
  let sync = 0;
740
- for (let i = 0; i < 4; i++) {
741
- const z = correlateStride2(cdRe, cdIm, starts[i]!, templates[i]!.re, templates[i]!.im);
742
- if (z.count <= 16) {
743
- continue;
744
- }
720
+ for (let i = 0; i < COSTAS_BLOCKS; i++) {
721
+ const start = i0 + i * FT4_SYNC_STRIDE;
722
+ const z = correlateStride2(cdRe, cdIm, start, templates[i]!.re, templates[i]!.im);
723
+ if (z.count <= 16) continue;
745
724
  sync += Math.hypot(z.re, z.im) / (2 * NSS);
746
725
  }
747
726
  return sync;
@@ -759,9 +738,7 @@ function correlateStride2(
759
738
  let count = 0;
760
739
  for (let i = 0; i < templateRe.length; i++) {
761
740
  const idx = start + 2 * i;
762
- if (idx < 0 || idx >= cdRe.length) {
763
- continue;
764
- }
741
+ if (idx < 0 || idx >= cdRe.length) continue;
765
742
  const sRe = templateRe[i]!;
766
743
  const sIm = templateIm[i]!;
767
744
  const dRe = cdRe[idx]!;
@@ -773,13 +750,12 @@ function correlateStride2(
773
750
  return { re: zRe, im: zIm, count };
774
751
  }
775
752
 
776
- function getFt4Bitmetrics(cdRe: Float64Array, cdIm: Float64Array): Ft4BitMetrics {
777
- const csRe = new Float64Array(4 * NN);
778
- const csIm = new Float64Array(4 * NN);
779
- const s4 = new Float64Array(4 * NN);
780
-
781
- const symbRe = new Float64Array(NSS);
782
- const symbIm = new Float64Array(NSS);
753
+ function buildBitMetrics(
754
+ cdRe: Float64Array,
755
+ cdIm: Float64Array,
756
+ workspace: DecodeWorkspace,
757
+ ): boolean {
758
+ const { csRe, csIm, s4, symbRe, symbIm, bitmetrics1, bitmetrics2, bitmetrics3, s2 } = workspace;
783
759
 
784
760
  for (let k = 0; k < NN; k++) {
785
761
  const i1 = k * NSS;
@@ -801,33 +777,21 @@ function getFt4Bitmetrics(cdRe: Float64Array, cdIm: Float64Array): Ft4BitMetrics
801
777
 
802
778
  let nsync = 0;
803
779
  for (let k = 0; k < 4; k++) {
804
- if (maxTone(s4, k) === COSTAS_A[k]) {
805
- nsync++;
806
- }
807
- if (maxTone(s4, 33 + k) === COSTAS_B[k]) {
808
- nsync++;
809
- }
810
- if (maxTone(s4, 66 + k) === COSTAS_C[k]) {
811
- nsync++;
812
- }
813
- if (maxTone(s4, 99 + k) === COSTAS_D[k]) {
814
- nsync++;
815
- }
780
+ if (maxTone(s4, k) === COSTAS_A[k]) nsync++;
781
+ if (maxTone(s4, 33 + k) === COSTAS_B[k]) nsync++;
782
+ if (maxTone(s4, 66 + k) === COSTAS_C[k]) nsync++;
783
+ if (maxTone(s4, 99 + k) === COSTAS_D[k]) nsync++;
816
784
  }
817
785
 
818
- const bitmetrics1 = new Float64Array(2 * NN);
819
- const bitmetrics2 = new Float64Array(2 * NN);
820
- const bitmetrics3 = new Float64Array(2 * NN);
821
-
822
- if (nsync < 6) {
823
- return { bitmetrics1, bitmetrics2, bitmetrics3, badsync: true };
824
- }
786
+ bitmetrics1.fill(0);
787
+ bitmetrics2.fill(0);
788
+ bitmetrics3.fill(0);
789
+ if (nsync < 6) return true;
825
790
 
826
791
  for (let nseq = 1; nseq <= 3; nseq++) {
827
792
  const nsym = nseq === 1 ? 1 : nseq === 2 ? 2 : 4;
828
- const nt = 1 << (2 * nsym); // 4, 16, 256
793
+ const nt = 1 << (2 * nsym);
829
794
  const ibmax = nseq === 1 ? 1 : nseq === 2 ? 3 : 7;
830
- const s2 = new Float64Array(nt);
831
795
 
832
796
  for (let ks = 1; ks <= NN - nsym + 1; ks += nsym) {
833
797
  for (let i = 0; i < nt; i++) {
@@ -871,18 +835,15 @@ function getFt4Bitmetrics(cdRe: Float64Array, cdIm: Float64Array): Ft4BitMetrics
871
835
  for (let i = 0; i < nt; i++) {
872
836
  const v = s2[i]!;
873
837
  if ((i & mask) !== 0) {
874
- if (v > max1) {
875
- max1 = v;
876
- }
838
+ if (v > max1) max1 = v;
877
839
  } else if (v > max0) {
878
840
  max0 = v;
879
841
  }
880
842
  }
881
843
 
882
844
  const idx = ipt + ib;
883
- if (idx > 2 * NN) {
884
- continue;
885
- }
845
+ if (idx > BITMETRIC_LEN) continue;
846
+
886
847
  const bm = max1 - max0;
887
848
  if (nseq === 1) {
888
849
  bitmetrics1[idx - 1] = bm;
@@ -903,8 +864,7 @@ function getFt4Bitmetrics(cdRe: Float64Array, cdIm: Float64Array): Ft4BitMetrics
903
864
  normalizeBitMetrics(bitmetrics1);
904
865
  normalizeBitMetrics(bitmetrics2);
905
866
  normalizeBitMetrics(bitmetrics3);
906
-
907
- return { bitmetrics1, bitmetrics2, bitmetrics3, badsync: false };
867
+ return false;
908
868
  }
909
869
 
910
870
  function maxTone(s4: Float64Array, symbolIndex: number): number {
@@ -931,40 +891,25 @@ function normalizeBitMetrics(bmet: Float64Array): void {
931
891
  const avg2 = sum2 / bmet.length;
932
892
  const variance = avg2 - avg * avg;
933
893
  const sigma = variance > 0 ? Math.sqrt(variance) : Math.sqrt(avg2);
934
- if (sigma <= 0) {
935
- return;
936
- }
937
- for (let i = 0; i < bmet.length; i++) {
938
- bmet[i] = bmet[i]! / sigma;
939
- }
894
+ if (sigma <= 0) return;
895
+ for (let i = 0; i < bmet.length; i++) bmet[i] = bmet[i]! / sigma;
940
896
  }
941
897
 
942
898
  function passesHardSyncQuality(bitmetrics1: Float64Array): boolean {
943
899
  const hard = new Uint8Array(bitmetrics1.length);
944
- for (let i = 0; i < bitmetrics1.length; i++) {
945
- hard[i] = bitmetrics1[i]! >= 0 ? 1 : 0;
946
- }
900
+ for (let i = 0; i < bitmetrics1.length; i++) hard[i] = bitmetrics1[i]! >= 0 ? 1 : 0;
947
901
 
948
902
  let score = 0;
949
903
  for (const pattern of HARD_SYNC_PATTERNS) {
950
904
  for (let i = 0; i < pattern.bits.length; i++) {
951
- if (hard[pattern.offset + i] === pattern.bits[i]) {
952
- score++;
953
- }
905
+ if (hard[pattern.offset + i] === pattern.bits[i]) score++;
954
906
  }
955
907
  }
956
908
  return score >= 10;
957
909
  }
958
910
 
959
- function buildLlrs(
960
- bitmetrics1: Float64Array,
961
- bitmetrics2: Float64Array,
962
- bitmetrics3: Float64Array,
963
- ): [Float64Array, Float64Array, Float64Array] {
964
- const llra = new Float64Array(174);
965
- const llrb = new Float64Array(174);
966
- const llrc = new Float64Array(174);
967
-
911
+ function buildLlrs(workspace: DecodeWorkspace): void {
912
+ const { bitmetrics1, bitmetrics2, bitmetrics3, llra, llrb, llrc } = workspace;
968
913
  for (let i = 0; i < 58; i++) {
969
914
  llra[i] = bitmetrics1[8 + i]!;
970
915
  llra[58 + i] = bitmetrics1[74 + i]!;
@@ -978,15 +923,29 @@ function buildLlrs(
978
923
  llrc[58 + i] = bitmetrics3[74 + i]!;
979
924
  llrc[116 + i] = bitmetrics3[140 + i]!;
980
925
  }
926
+ }
927
+
928
+ function tryDecodePasses(
929
+ workspace: DecodeWorkspace,
930
+ depth: number,
931
+ ): import("../util/decode174_91.js").DecodeResult | null {
932
+ const maxosd = depth >= 3 ? 2 : depth >= 2 ? 0 : -1;
933
+ const scalefac = 2.83;
934
+ const sources = [workspace.llra, workspace.llrb, workspace.llrc];
981
935
 
982
- return [llra, llrb, llrc];
936
+ workspace.apmask.fill(0);
937
+ for (const src of sources) {
938
+ for (let i = 0; i < LDPC_BITS; i++) workspace.llr[i] = scalefac * src[i]!;
939
+ const result = decode174_91(workspace.llr, workspace.apmask, maxosd);
940
+ if (result) return result;
941
+ }
942
+
943
+ return null;
983
944
  }
984
945
 
985
946
  function hasNonZeroBit(bits: readonly number[]): boolean {
986
947
  for (const bit of bits) {
987
- if (bit !== 0) {
988
- return true;
989
- }
948
+ if (bit !== 0) return true;
990
949
  }
991
950
  return false;
992
951
  }