JS8Call-Improved master
Loading...
Searching...
No Matches
JS8.cpp
Go to the documentation of this file.
1
7
8#include "JS8.h"
9#include "JS8_Include/commons.h"
10#include "JS8_Mode/FrequencyTracker.h"
11#include "JS8_Mode/whitening_processor.h"
12#include "ldpc_feedback.h"
13#include "soft_combiner.h"
14
15#include <QDebug>
16#include <QLoggingCategory>
17#include <QtGlobal>
18
19#include <algorithm>
20#include <atomic>
21#include <boost/crc.hpp>
22#include <boost/math/ccmath/round.hpp>
23#include <boost/multi_index/key.hpp>
24#include <boost/multi_index/ordered_index.hpp>
25#include <boost/multi_index/ranked_index.hpp>
26#include <boost/multi_index_container.hpp>
27#include <chrono>
28#include <cmath>
29#include <complex>
30#include <concepts>
31#include <cstddef>
32#include <cstdint>
33#include <cstdlib>
34#include <fftw3.h>
35#include <initializer_list>
36#include <limits>
37#include <memory>
38#include <mutex>
39#include <numbers>
40#include <numeric>
41#include <optional>
42#include <sstream>
43#include <stdexcept>
44#include <string_view>
45#include <unordered_map>
46#include <utility>
47#include <vector>
48#include <vendor/Eigen/Dense>
49
50Q_DECLARE_LOGGING_CATEGORY(decoder_js8);
51
52// A C++ conversion of the Fortran JS8 encoding and decoder function.
53// Some notes on the conversion:
54//
55// 1. Names of variables and functions as much as possible match those
56// of the Fortran routines, for ease in cross-referencing during the
57// debug comparison phase of testing. You don't have to like them; I
58// don't like them either, frankly, but it's the reasonable approach
59// to the problem as of this writing; we can make 'em pretty later.
60//
61// 2. The BP decoder should be a faithful reproduction of the Fortran
62// version, albeit modified for the column-major vs. row-major
63// differences between the two languages.
64//
65// 3. The OSD decoder is no longer used, and the depth is now fixed at
66// 2, instead of being variable 1 to 4.
67//
68// 4. The Fortran version didn't compute the 40% rank consistently in
69// syncjs8(); this version does. It wasn't typically off by much, but
70// it was reliably not going to be at 40%. Hopefully, this change will
71// result in more predictable first-pass candidate selection.
72//
73// 5. The Fortran version was very subject to Runge's phenomenon when
74// computing the baseline in baselinejs8(), and was using a ton of
75// data points below the 10% threshold for the polynomial determination.
76// Neither of these seemed to be helpful, so in contrast we're using
77// a number of Chebyshev nodes proportional to the desired polynomial
78// terms.
79//
80// 6. The Fortran version normalized `s1` by dividing by the median in
81// js8dec(), but did so in a naive manner, not checking for a median
82// of zero. Testing indicates that the normalization does not appear
83// to contribute to decoder yield, so it's been removed.
84//
85// 7. Translating array indices from the world of Fortran to that of C++
86// is no one's fun task. If you see things that aren't behaving as
87// expected, look at the Fortran code and compare the array indexing;
88// would not be surprised in the least to have off-by-one errors here.
89
90/******************************************************************************/
91// Compilation Utilities
92/******************************************************************************/
93
94namespace {
95// Full-range cosine function using symmetries of cos(x). std::cos
96// isn't constexpr until C++20, and we're targeting C++17 at the
97// moment. We only use this function during compilation; std::cos
98// is the better choice at runtime. Once we move to requiring a
99// C++20 compiler, we can just use std::cos.
100
101constexpr double cos_approx(double x) {
102 constexpr auto RAD_360 = std::numbers::pi * 2;
103 constexpr auto RAD_180 = std::numbers::pi;
104 constexpr auto RAD_90 = std::numbers::pi / 2;
105
106 // Polynomial approximation of cos(x) for x in [0, RAD_90],
107 // Accuracy here in theory is 1e-18, but double precision
108 // itself is only 1-e16, so within the domain of doubles,
109 // this should be extremely accurate.
110
111 constexpr auto poly = [](double const x) {
112 constexpr std::array coefficients = {
113 1.0, // Coefficient for x^0
114 -0.49999999999999994, // Coefficient for x^2
115 0.041666666666666664, // Coefficient for x^4
116 -0.001388888888888889, // Coefficient for x^6
117 0.000024801587301587, // Coefficient for x^8
118 -0.00000027557319223986, // Coefficient for x^10
119 0.00000000208767569878681, // Coefficient for x^12
120 -0.00000000001147074513875176, // Coefficient for x^14
121 0.0000000000000477947733238733 // Coefficient for x^16
122 };
123
124 auto const x2 = x * x;
125 auto const x4 = x2 * x2;
126 auto const x6 = x4 * x2;
127 auto const x8 = x4 * x4;
128 auto const x10 = x8 * x2;
129 auto const x12 = x8 * x4;
130 auto const x14 = x12 * x2;
131 auto const x16 = x8 * x8;
132
133 return coefficients[0] + coefficients[1] * x2 + coefficients[2] * x4 +
134 coefficients[3] * x6 + coefficients[4] * x8 +
135 coefficients[5] * x10 + coefficients[6] * x12 +
136 coefficients[7] * x14 + coefficients[8] * x16;
137 };
138
139 // Reduce x to [0, RAD_360)
140
141 x -= static_cast<long long>(x / RAD_360) * RAD_360;
142
143 // Map x to [0, RAD_180]
144
145 if (x > RAD_180)
146 x = RAD_360 - x;
147
148 // Map x to [0, RAD_90] and evaluate the polynomial;
149 // flip the sign for angles in the second quadrant.
150
151 return x > RAD_90 ? -poly(RAD_180 - x) : poly(x);
152};
153} // end anonymous namespace
154
155/******************************************************************************/
156// Constants
157/******************************************************************************/
158
159namespace {
160/* COMMON PARAMETERS */
161
162// !Common
163//
164// parameter (KK=87) !Information bits (75 + CRC12)
165// parameter (ND=58) !Data symbols
166// parameter (NS=21) !Sync symbols (3 @ Costas 7x7)
167// parameter (NN=NS+ND) !Total channel symbols (79)
168// parameter (ASYNCMIN=1.5) !Minimum Sync
169// parameter (NFSRCH=5) !Search frequency range in Hz (i.e.,
170// +/- 2.5 Hz) parameter (NMAXCAND=300) !Maximum number of
171// candidate signals
172
173// Parameter Value Description
174// KK 87 Number of information bits (75 message bits + 12 CRC bits).
175// ND 58 Number of data symbols in the JS8 transmission.
176// NS 21 Number of synchronization symbols (3 Costas arrays of size 7).
177// NN 79 Total number of channel symbols (NN = NS + ND).
178// ASYNCMIN 1.5 Minimum sync value for successful decoding.
179// NFSRCH 5 Search frequency range in Hz (±2.5 Hz).
180// NMAXCAND 300 Maximum number of candidate signals.
181
182constexpr int N = 174; // Total bits
183constexpr int K = 87; // Message bits
184constexpr int M = N - K; // Check bits
185constexpr int KK = 87; // Information bits (75 + CRC12)
186constexpr int ND = 58; // Data symbols
187constexpr int NS = 21; // Sync symbols (3 @ Costas 7x7)
188constexpr int NN = NS + ND; // Total channel symbols (79)
189constexpr float ASYNCMIN = 1.5f; // Minimum sync
190constexpr int NFSRCH = 5; // Search frequency range in Hz (i.e., +/- 2.5 Hz)
191constexpr std::size_t NMAXCAND = 300; // Maxiumum number of candidate signals
192constexpr int NFILT = 1400; // Filter length
193constexpr int NROWS = 8;
194constexpr int NFOS = 2;
195constexpr int NSSY = 4;
196constexpr int NP = 3200;
197constexpr int NP2 = 2812;
198constexpr float TAU = 2.0f * std::numbers::pi_v<float>;
199constexpr auto ZERO = std::complex<float>{0.0f, 0.0f};
200// Key for the constants that follow:
201// Key for the constants that follow:
202//
203// NSUBMODE - ID of the submode
204// NCOSTAS - Which JS8 Costas Arrays to use
205// NSPS - Number of samples per second
206// NTXDUR - Duration of the transmission in seconds.
207// NDOWNSPS - Number of samples per symbol after downsampling.
208// NDD - Parameter used in waveform tapering and related calculations.
209// XXX JZ - Range of symbol offsets considered during decoding. ASTART
210// - Start delay in seconds for decoding. BASESUB - XXX NMAX - Samples in
211// input wave NSTEP - Rough time-sync step size NHSYM - Number of symbol
212// spectra (1/4-sym steps) NDOW - Downsample factor to 32 samples per
213// symbol NQSYMBOL - Downsample factor of a quarter symbol
214
215/* A MODE DECODER */
216
217struct ModeA {
218 // Static constants
219 inline static constexpr int NSUBMODE = 0;
220 inline static constexpr auto NCOSTAS = JS8::Costas::Type::ORIGINAL;
221 inline static constexpr int NSPS = JS8A_SYMBOL_SAMPLES;
222 inline static constexpr int NTXDUR = JS8A_TX_SECONDS;
223 inline static constexpr int NDOWNSPS = 32;
224 inline static constexpr int NDD = 100;
225 inline static constexpr int JZ = 62;
226 inline static constexpr float ASTART = 0.5f;
227 inline static constexpr float BASESUB = 40.0f;
228
229 // Derived parameters
230 inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
231 inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
232 inline static constexpr int NFFT1 = NSPS * NFOS;
233 inline static constexpr int NSTEP = NSPS / NSSY;
234 inline static constexpr int NHSYM = NMAX / NSTEP - 3;
235 inline static constexpr int NDOWN = NSPS / NDOWNSPS;
236 inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
237 inline static constexpr int NDFFT1 = NSPS * NDD;
238 inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
239 inline static constexpr int NP2 = NN * NDOWNSPS;
240 inline static constexpr float TSTEP = NSTEP / 12000.0f;
241 inline static constexpr int JSTRT = ASTART / TSTEP;
242 inline static constexpr float DF = 12000.0f / NFFT1;
243};
244
245/* B MODE DECODER */
246
247struct ModeB {
248 // Static constants
249 inline static constexpr int NSUBMODE = 1;
250 inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
251 inline static constexpr int NSPS = JS8B_SYMBOL_SAMPLES;
252 inline static constexpr int NTXDUR = JS8B_TX_SECONDS;
253 inline static constexpr int NDOWNSPS = 20;
254 inline static constexpr int NDD = 100;
255 inline static constexpr int JZ = 144;
256 inline static constexpr float ASTART = 0.2f;
257 inline static constexpr float BASESUB = 39.0f;
258
259 // Derived parameters
260 inline static constexpr float AZ = (12000.0f / NSPS) * 0.8f;
261 inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
262 inline static constexpr int NFFT1 = NSPS * NFOS;
263 inline static constexpr int NSTEP = NSPS / NSSY;
264 inline static constexpr int NHSYM = NMAX / NSTEP - 3;
265 inline static constexpr int NDOWN = NSPS / NDOWNSPS;
266 inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
267 inline static constexpr int NDFFT1 = NSPS * NDD;
268 inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
269 inline static constexpr int NP2 = NN * NDOWNSPS;
270 inline static constexpr float TSTEP = NSTEP / 12000.0f;
271 inline static constexpr int JSTRT = ASTART / TSTEP;
272 inline static constexpr float DF = 12000.0f / NFFT1;
273};
274
275/* C MODE DECODER */
276
277struct ModeC {
278 // Static constants
279 inline static constexpr int NSUBMODE = 2;
280 inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
281 inline static constexpr int NSPS = JS8C_SYMBOL_SAMPLES;
282 inline static constexpr int NTXDUR = JS8C_TX_SECONDS;
283 inline static constexpr int NDOWNSPS = 12;
284 inline static constexpr int NDD = 120;
285 inline static constexpr int JZ = 172;
286 inline static constexpr float ASTART = 0.1f;
287 inline static constexpr float BASESUB = 38.0f;
288
289 // Derived parameters
290 inline static constexpr float AZ = (12000.0f / NSPS) * 0.6f;
291 inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
292 inline static constexpr int NFFT1 = NSPS * NFOS;
293 inline static constexpr int NSTEP = NSPS / NSSY;
294 inline static constexpr int NHSYM = NMAX / NSTEP - 3;
295 inline static constexpr int NDOWN = NSPS / NDOWNSPS;
296 inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
297 inline static constexpr int NDFFT1 = NSPS * NDD;
298 inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
299 inline static constexpr int NP2 = NN * NDOWNSPS;
300 inline static constexpr float TSTEP = NSTEP / 12000.0f;
301 inline static constexpr int JSTRT = ASTART / TSTEP;
302 inline static constexpr float DF = 12000.0f / NFFT1;
303};
304
305/* E MODE DECODER */
306
307// Note that the original used 28 for NTXDUR and 90 for NDD, but the
308// corresponding C++ mainline side used 30 for NTXDUR, so for the
309// moment, we're matching that here, which seems logical at present.
310
311struct ModeE {
312 // Static constants
313 inline static constexpr int NSUBMODE = 4;
314 inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
315 inline static constexpr int NSPS = JS8E_SYMBOL_SAMPLES;
316 inline static constexpr int NTXDUR =
317 JS8E_TX_SECONDS; // XXX was 28 in Fortran
318 inline static constexpr int NDOWNSPS = 32;
319 inline static constexpr int NDD = 94; // XXX was 90 in Fortran
320 inline static constexpr int JZ = 32;
321 inline static constexpr float ASTART = 0.5f;
322 inline static constexpr float BASESUB = 42.0f;
323
324 // Derived parameters
325 inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
326 inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
327 inline static constexpr int NFFT1 = NSPS * NFOS;
328 inline static constexpr int NSTEP = NSPS / NSSY;
329 inline static constexpr int NHSYM = NMAX / NSTEP - 3;
330 inline static constexpr int NDOWN = NSPS / NDOWNSPS;
331 inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
332 inline static constexpr int NDFFT1 = NSPS * NDD;
333 inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
334 inline static constexpr int NP2 = NN * NDOWNSPS;
335 inline static constexpr float TSTEP = NSTEP / 12000.0f;
336 inline static constexpr int JSTRT = ASTART / TSTEP;
337 inline static constexpr float DF = 12000.0f / NFFT1;
338};
339
340/* I MODE DECODER */
341
342struct ModeI {
343 // Static constants
344 inline static constexpr int NSUBMODE = 8;
345 inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
346 inline static constexpr int NSPS = JS8I_SYMBOL_SAMPLES;
347 inline static constexpr int NTXDUR = JS8I_TX_SECONDS;
348 inline static constexpr int NDOWNSPS = 12;
349 inline static constexpr int NDD = 125;
350 inline static constexpr int JZ = 250;
351 inline static constexpr float ASTART = 0.1f;
352 inline static constexpr float BASESUB = 36.0f;
353
354 // Derived parameters
355 inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
356 inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
357 inline static constexpr int NFFT1 = NSPS * NFOS;
358 inline static constexpr int NSTEP = NSPS / NSSY;
359 inline static constexpr int NHSYM = NMAX / NSTEP - 3;
360 inline static constexpr int NDOWN = NSPS / NDOWNSPS;
361 inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
362 inline static constexpr int NDFFT1 = NSPS * NDD;
363 inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
364 inline static constexpr int NP2 = NN * NDOWNSPS;
365 inline static constexpr float TSTEP = NSTEP / 12000.0f;
366 inline static constexpr int JSTRT = ASTART / TSTEP;
367 inline static constexpr float DF = 12000.0f / NFFT1;
368};
369
370// Tunable settings; degree of the polynomial used for the baseline
371// curve fit, and the percentile of the span at which to sample. In
372// general, a 5th degree polynomial and the 10th percentile should
373// be optimal.
374
375constexpr auto BASELINE_DEGREE = 5;
376constexpr auto BASELINE_SAMPLE = 10;
377
378// Define the closed range in Hz that we'll consider to be the window
379// for baseline determination.
380
381constexpr auto BASELINE_MIN = 500;
382constexpr auto BASELINE_MAX = 2500;
383
384// We're going to do a pairwise Estrin's evaluation of the polynomial
385// coefficients, so it's critical that the degree of the polynomial is
386// odd, resulting in an even number of coefficients.
387
388static_assert(BASELINE_DEGREE & 1, "Degree must be odd");
389static_assert(BASELINE_SAMPLE >= 0 && BASELINE_SAMPLE <= 100,
390 "Sample must be a percentage");
391
392// Since we know the degree of the polynomial, and thus the number of
393// nodes that we're going to use, we can do all the trigonometry work
394// required to calculate the Chebyshev nodes in advance, by computing
395// them over the range [0, 1]; we can then scale these at runtime to
396// a span of any size by simple multiplication.
397//
398// Downside to this with C++17 is that std::cos() is not yet constexpr,
399// as it is in C++20, so we must provide our own implementation until
400// then.
401
402constexpr auto BASELINE_NODES = []() {
403 // Down to the actual business of generating Chebyshev nodes
404 // suitable for scaling; once we move to C++20 as the minimum
405 // compiler, we can remove the cos() function above and instead
406 // call std::cos() here, as it's required to be constexpr in
407 // C++20 and above, and presumably it'll be of high quality.
408
409 auto nodes = std::array<double, BASELINE_DEGREE + 1>{};
410 constexpr auto slice = std::numbers::pi / (2.0 * nodes.size());
411
412 for (std::size_t i = 0; i < nodes.size(); ++i) {
413 nodes[i] = 0.5 * (1.0 - cos_approx(slice * (2.0 * i + 1)));
414 }
415
416 return nodes;
417}();
418} // end anonymous namespace
419
420/******************************************************************************/
421// Local Types
422/******************************************************************************/
423
424namespace {
425// Accumulation of rounding errors in IEEE 754 values can be a problem
426// when summing large numbers of small values; a Kahan summation class
427// by which to compensate for them.
428//
429// Fortran, or at least, gfortran, will use this technique under the
430// covers in various scenarios. While it'd be reasonable to expect it
431// to be used in sum(), that's typically not the case.
432//
433// However, for example, it'll use it here for the value that goes into
434// win(i), and naive summation in C++ will as a result not produce the
435// same values without using compensation.
436//
437// subroutine nuttal_window(win,n)
438// real win(n)
439// pi=4.0*atan(1.0)
440// a0=0.3635819
441// a1=-0.4891775;
442// a2=0.1365995;
443// a3=-0.0106411;
444// do i=1,n
445// win(i)=a0+a1*cos(2*pi*(i-1)/(n))+ &
446// a2*cos(4*pi*(i-1)/(n))+ &
447// a3*cos(6*pi*(i-1)/(n))
448// enddo
449// return
450// end subroutine nuttal_window
451
452template <std::floating_point T> class KahanSum {
453 T m_sum; // Accumulated sum
454 T m_compensation; // Compensation for lost low-order bits
455
456 public:
457 KahanSum(T sum = T(0)) : m_sum(sum), m_compensation(T(0)) {}
458
459 KahanSum &operator=(T const sum) {
460 m_sum = sum;
461 m_compensation = T(0);
462
463 return *this;
464 }
465
466 KahanSum &operator+=(T const value) {
467 T const y = value - m_compensation; // Correct the value
468 T const t = m_sum + y; // Perform the sum
469
470 m_compensation = (t - m_sum) - y; // Update compensation
471 m_sum = t; // Update the sum
472
473 return *this;
474 }
475
476 operator T() const { return m_sum; }
477};
478
479// Management of dynamic FFTW plan storage.
480
481class FFTWPlanManager {
482 public:
483 enum class Type { DS, BB, CF, CB, SD, CS, count };
484
485 // Disallow copying and moving
486
487 FFTWPlanManager(FFTWPlanManager const &) = delete;
488 FFTWPlanManager &operator=(FFTWPlanManager const &) = delete;
489 FFTWPlanManager(FFTWPlanManager &&) = delete;
490 FFTWPlanManager &operator=(FFTWPlanManager &&) = delete;
491
492 // Constructor
493
494 FFTWPlanManager() { m_plans.fill(nullptr); }
495
496 // Destructor
497
498 ~FFTWPlanManager() {
499 std::lock_guard<std::mutex> lock(fftw_mutex);
500
501 for (auto &plan : m_plans) {
502 if (plan)
503 fftwf_destroy_plan(plan);
504 }
505 }
506
507 // Accessor
508
509 fftwf_plan const &operator[](Type const type) const noexcept {
510 return m_plans[static_cast<std::size_t>(type)];
511 }
512
513 // Manipulator
514
515 fftwf_plan &operator[](Type const type) noexcept {
516 return m_plans[static_cast<std::size_t>(type)];
517 }
518
519 // Iteration support
520
521 auto begin() noexcept { return m_plans.begin(); }
522 auto end() noexcept { return m_plans.end(); }
523 auto begin() const noexcept { return m_plans.begin(); }
524 auto end() const noexcept { return m_plans.end(); }
525
526 private:
527 // Data members
528
529 std::array<fftwf_plan, static_cast<std::size_t>(Type::count)> m_plans;
530};
531
532// Encapsulates the first-order search results provided by syncjs8().
533
534struct Sync {
535 float freq;
536 float step;
537 float sync;
538
539 // Constructor for convenience.
540
541 Sync(float const freq, float const step, float const sync)
542 : freq(freq), step(step), sync(sync) {}
543};
544
545// Tag structs so that we can refer to multi index container indices
546// by a descriptive tag instead of by the index of the index. These
547// don't need to be anything but a name.
548
549namespace Tag {
550struct Freq {};
551struct Rank {};
552struct Sync {};
553} // namespace Tag
554
555// Container indexing Sync objects in useful ways, used by syncjs8().
556
557namespace MI = boost::multi_index;
558using SyncIndex = MI::multi_index_container<
559 Sync, MI::indexed_by<
560 MI::ordered_non_unique<MI::tag<Tag::Freq>, MI::key<&Sync::freq>>,
561 MI::ranked_non_unique<MI::tag<Tag::Rank>, MI::key<&Sync::sync>>,
562 MI::ordered_non_unique<MI::tag<Tag::Sync>, MI::key<&Sync::sync>,
563 std::greater<>>>>;
564
565// Represents a decoded message, i.e., the 3-bit message type
566// and the 12 bytes that result from decoding a message.
567
568class Decode {
569 public:
570 int type;
571 std::string data;
572
573 Decode(int type, std::string data) : type(type), data(std::move(data)) {}
574
575 bool operator==(Decode const &) const noexcept = default;
576
577 struct Hash {
578 std::size_t operator()(Decode const &decode) const noexcept {
579 std::size_t const h1 = std::hash<int>{}(decode.type);
580 std::size_t const h2 = std::hash<std::string>{}(decode.data);
581 return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
582 }
583 };
584
585 using Map = std::unordered_map<Decode, int, Hash>;
586};
587
588/******************************************************************************/
589// Belief Propagation Decoder
590/******************************************************************************/
591
592namespace {
593constexpr int BP_MAX_ROWS = 7; // Max rows per column in Nm
594constexpr int BP_MAX_CHECKS = 3; // Max checks per bit in Mn
595constexpr int BP_MAX_ITERATIONS = 30; // Max iterations in BP decoder
596
597constexpr std::array<std::array<int, BP_MAX_CHECKS>, N> Mn = {
598 {{0, 24, 68}, {1, 4, 72}, {2, 31, 67}, {3, 50, 60}, {5, 62, 69},
599 {6, 32, 78}, {7, 49, 85}, {8, 36, 42}, {9, 40, 64}, {10, 13, 63},
600 {11, 74, 76}, {12, 22, 80}, {14, 15, 81}, {16, 55, 65}, {17, 52, 59},
601 {18, 30, 51}, {19, 66, 83}, {20, 28, 71}, {21, 23, 43}, {25, 34, 75},
602 {26, 35, 37}, {27, 39, 41}, {29, 53, 54}, {33, 48, 86}, {38, 56, 57},
603 {44, 73, 82}, {45, 61, 79}, {46, 47, 84}, {58, 70, 77}, {0, 49, 52},
604 {1, 46, 83}, {2, 24, 78}, {3, 5, 13}, {4, 6, 79}, {7, 33, 54},
605 {8, 35, 68}, {9, 42, 82}, {10, 22, 73}, {11, 16, 43}, {12, 56, 75},
606 {14, 26, 55}, {15, 27, 28}, {17, 18, 58}, {19, 39, 62}, {20, 34, 51},
607 {21, 53, 63}, {23, 61, 77}, {25, 31, 76}, {29, 71, 84}, {30, 64, 86},
608 {32, 38, 50}, {36, 47, 74}, {37, 69, 70}, {40, 41, 67}, {44, 66, 85},
609 {45, 80, 81}, {48, 65, 72}, {57, 59, 65}, {60, 64, 84}, {0, 13, 20},
610 {1, 12, 58}, {2, 66, 81}, {3, 31, 72}, {4, 35, 53}, {5, 42, 45},
611 {6, 27, 74}, {7, 32, 70}, {8, 48, 75}, {9, 57, 63}, {10, 47, 67},
612 {11, 18, 44}, {14, 49, 60}, {15, 21, 25}, {16, 71, 79}, {17, 39, 54},
613 {19, 34, 50}, {22, 24, 33}, {23, 62, 86}, {26, 38, 73}, {28, 77, 82},
614 {29, 69, 76}, {30, 68, 83}, {21, 36, 85}, {37, 40, 80}, {41, 43, 56},
615 {46, 52, 61}, {51, 55, 78}, {59, 74, 80}, {0, 38, 76}, {1, 15, 40},
616 {2, 30, 53}, {3, 35, 77}, {4, 44, 64}, {5, 56, 84}, {6, 13, 48},
617 {7, 20, 45}, {8, 14, 71}, {9, 19, 61}, {10, 16, 70}, {11, 33, 46},
618 {12, 67, 85}, {17, 22, 42}, {18, 63, 72}, {23, 47, 78}, {24, 69, 82},
619 {25, 79, 86}, {26, 31, 39}, {27, 55, 68}, {28, 62, 65}, {29, 41, 49},
620 {32, 36, 81}, {34, 59, 73}, {37, 54, 83}, {43, 51, 60}, {50, 52, 71},
621 {57, 58, 66}, {46, 55, 75}, {0, 18, 36}, {1, 60, 74}, {2, 7, 65},
622 {3, 59, 83}, {4, 33, 38}, {5, 25, 52}, {6, 31, 56}, {8, 51, 66},
623 {9, 11, 14}, {10, 50, 68}, {12, 13, 64}, {15, 30, 42}, {16, 19, 35},
624 {17, 79, 85}, {20, 47, 58}, {21, 39, 45}, {22, 32, 61}, {23, 29, 73},
625 {24, 41, 63}, {26, 48, 84}, {27, 37, 72}, {28, 43, 80}, {34, 67, 69},
626 {40, 62, 75}, {44, 48, 70}, {49, 57, 86}, {47, 53, 82}, {12, 54, 78},
627 {76, 77, 81}, {0, 1, 23}, {2, 5, 74}, {3, 55, 86}, {4, 43, 52},
628 {6, 49, 82}, {7, 9, 27}, {8, 54, 61}, {10, 28, 66}, {11, 32, 39},
629 {13, 15, 19}, {14, 34, 72}, {16, 30, 38}, {17, 35, 56}, {18, 45, 75},
630 {20, 41, 83}, {21, 33, 58}, {22, 25, 60}, {24, 59, 64}, {26, 63, 79},
631 {29, 36, 65}, {31, 44, 71}, {37, 50, 85}, {40, 76, 78}, {42, 55, 67},
632 {46, 73, 81}, {39, 51, 77}, {53, 60, 70}, {45, 57, 68}}};
633
634struct CheckNode {
635 int valid_neighbors;
636 std::array<int, BP_MAX_ROWS> neighbors;
637};
638
639constexpr std::array<CheckNode, M> Nm = {{{6, {0, 29, 59, 88, 117, 146, 0}},
640 {6, {1, 30, 60, 89, 118, 146, 0}},
641 {6, {2, 31, 61, 90, 119, 147, 0}},
642 {6, {3, 32, 62, 91, 120, 148, 0}},
643 {6, {1, 33, 63, 92, 121, 149, 0}},
644 {6, {4, 32, 64, 93, 122, 147, 0}},
645 {6, {5, 33, 65, 94, 123, 150, 0}},
646 {6, {6, 34, 66, 95, 119, 151, 0}},
647 {6, {7, 35, 67, 96, 124, 152, 0}},
648 {6, {8, 36, 68, 97, 125, 151, 0}},
649 {6, {9, 37, 69, 98, 126, 153, 0}},
650 {6, {10, 38, 70, 99, 125, 154, 0}},
651 {6, {11, 39, 60, 100, 127, 144, 0}},
652 {6, {9, 32, 59, 94, 127, 155, 0}},
653 {6, {12, 40, 71, 96, 125, 156, 0}},
654 {6, {12, 41, 72, 89, 128, 155, 0}},
655 {6, {13, 38, 73, 98, 129, 157, 0}},
656 {6, {14, 42, 74, 101, 130, 158, 0}},
657 {6, {15, 42, 70, 102, 117, 159, 0}},
658 {6, {16, 43, 75, 97, 129, 155, 0}},
659 {6, {17, 44, 59, 95, 131, 160, 0}},
660 {6, {18, 45, 72, 82, 132, 161, 0}},
661 {6, {11, 37, 76, 101, 133, 162, 0}},
662 {6, {18, 46, 77, 103, 134, 146, 0}},
663 {6, {0, 31, 76, 104, 135, 163, 0}},
664 {6, {19, 47, 72, 105, 122, 162, 0}},
665 {6, {20, 40, 78, 106, 136, 164, 0}},
666 {6, {21, 41, 65, 107, 137, 151, 0}},
667 {6, {17, 41, 79, 108, 138, 153, 0}},
668 {6, {22, 48, 80, 109, 134, 165, 0}},
669 {6, {15, 49, 81, 90, 128, 157, 0}},
670 {6, {2, 47, 62, 106, 123, 166, 0}},
671 {6, {5, 50, 66, 110, 133, 154, 0}},
672 {6, {23, 34, 76, 99, 121, 161, 0}},
673 {6, {19, 44, 75, 111, 139, 156, 0}},
674 {6, {20, 35, 63, 91, 129, 158, 0}},
675 {6, {7, 51, 82, 110, 117, 165, 0}},
676 {6, {20, 52, 83, 112, 137, 167, 0}},
677 {6, {24, 50, 78, 88, 121, 157, 0}},
678 {7, {21, 43, 74, 106, 132, 154, 171}},
679 {6, {8, 53, 83, 89, 140, 168, 0}},
680 {6, {21, 53, 84, 109, 135, 160, 0}},
681 {6, {7, 36, 64, 101, 128, 169, 0}},
682 {6, {18, 38, 84, 113, 138, 149, 0}},
683 {6, {25, 54, 70, 92, 141, 166, 0}},
684 {7, {26, 55, 64, 95, 132, 159, 173}},
685 {6, {27, 30, 85, 99, 116, 170, 0}},
686 {6, {27, 51, 69, 103, 131, 143, 0}},
687 {6, {23, 56, 67, 94, 136, 141, 0}},
688 {6, {6, 29, 71, 109, 142, 150, 0}},
689 {6, {3, 50, 75, 114, 126, 167, 0}},
690 {6, {15, 44, 86, 113, 124, 171, 0}},
691 {6, {14, 29, 85, 114, 122, 149, 0}},
692 {6, {22, 45, 63, 90, 143, 172, 0}},
693 {6, {22, 34, 74, 112, 144, 152, 0}},
694 {7, {13, 40, 86, 107, 116, 148, 169}},
695 {6, {24, 39, 84, 93, 123, 158, 0}},
696 {6, {24, 57, 68, 115, 142, 173, 0}},
697 {6, {28, 42, 60, 115, 131, 161, 0}},
698 {6, {14, 57, 87, 111, 120, 163, 0}},
699 {7, {3, 58, 71, 113, 118, 162, 172}},
700 {6, {26, 46, 85, 97, 133, 152, 0}},
701 {5, {4, 43, 77, 108, 140, 0, 0}},
702 {6, {9, 45, 68, 102, 135, 164, 0}},
703 {6, {8, 49, 58, 92, 127, 163, 0}},
704 {6, {13, 56, 57, 108, 119, 165, 0}},
705 {6, {16, 54, 61, 115, 124, 153, 0}},
706 {6, {2, 53, 69, 100, 139, 169, 0}},
707 {6, {0, 35, 81, 107, 126, 173, 0}},
708 {5, {4, 52, 80, 104, 139, 0, 0}},
709 {6, {28, 52, 66, 98, 141, 172, 0}},
710 {6, {17, 48, 73, 96, 114, 166, 0}},
711 {6, {1, 56, 62, 102, 137, 156, 0}},
712 {6, {25, 37, 78, 111, 134, 170, 0}},
713 {6, {10, 51, 65, 87, 118, 147, 0}},
714 {6, {19, 39, 67, 116, 140, 159, 0}},
715 {6, {10, 47, 80, 88, 145, 168, 0}},
716 {6, {28, 46, 79, 91, 145, 171, 0}},
717 {6, {5, 31, 86, 103, 144, 168, 0}},
718 {6, {26, 33, 73, 105, 130, 164, 0}},
719 {5, {11, 55, 83, 87, 138, 0, 0}},
720 {6, {12, 55, 61, 110, 145, 170, 0}},
721 {6, {25, 36, 79, 104, 143, 150, 0}},
722 {6, {16, 30, 81, 112, 120, 160, 0}},
723 {5, {27, 48, 58, 93, 136, 0, 0}},
724 {6, {6, 54, 82, 100, 130, 167, 0}},
725 {6, {23, 49, 77, 105, 142, 148, 0}}}};
726
727// Belief Propagation Decoder
728
729int bpdecode174(std::array<float, N> const &llr, std::array<int8_t, K> &decoded,
730 std::array<int8_t, N> &cw) {
731 // Initialize messages and variables
732 std::array<std::array<float, BP_MAX_CHECKS>, N> tov =
733 {}; // Messages to variable nodes
734 std::array<std::array<float, BP_MAX_ROWS>, M> toc =
735 {}; // Messages to check nodes
736 std::array<std::array<float, BP_MAX_ROWS>, M> tanhtoc =
737 {}; // Tanh of messages
738
739 std::array<float, N> zn = {}; // Bit log likelihood ratios
740 std::array<int, M> synd = {}; // Syndrome for checks
741
742 int ncnt = 0;
743 int nclast = 0;
744
745 // Initialize toc (messages from bits to checks)
746 for (int i = 0; i < M; ++i) {
747 for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
748 toc[i][j] = llr[Nm[i].neighbors[j]];
749 }
750 }
751
752 // Iterative decoding
753 for (int iter = 0; iter <= BP_MAX_ITERATIONS; ++iter) {
754 // Update bit log likelihood ratios
755 for (int i = 0; i < N; ++i) {
756 zn[i] =
757 llr[i] + std::accumulate(tov[i].begin(),
758 tov[i].begin() + BP_MAX_CHECKS, 0.0f);
759 }
760
761 // Check if we have a valid codeword
762 for (int i = 0; i < N; ++i)
763 cw[i] = zn[i] > 0 ? 1 : 0;
764
765 int ncheck = 0;
766 for (int i = 0; i < M; ++i) {
767 synd[i] = 0;
768 for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
769 synd[i] += cw[Nm[i].neighbors[j]];
770 }
771 if (synd[i] % 2 != 0)
772 ++ncheck;
773 }
774
775 if (ncheck == 0) {
776 // Extract decoded bits (last N-M bits of codeword)
777 std::copy(cw.begin() + M, cw.end(), decoded.begin());
778
779 // Count errors
780 int nerr = 0;
781 for (int i = 0; i < N; ++i) {
782 if ((2 * cw[i] - 1) * llr[i] < 0.0f) {
783 ++nerr;
784 }
785 }
786
787 return nerr;
788 }
789
790 // Early stopping criterion
791 if (iter > 0) {
792 int nd = ncheck - nclast;
793 ncnt = (nd < 0) ? 0 : ncnt + 1;
794 if (ncnt >= 5 && iter >= 10 && ncheck > 15) {
795 return -1;
796 }
797 }
798 nclast = ncheck;
799
800 // Send messages from bits to check nodes
801 for (int i = 0; i < M; ++i) {
802 for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
803 int ibj = Nm[i].neighbors[j];
804 toc[i][j] = zn[ibj];
805 for (int k = 0; k < BP_MAX_CHECKS; ++k) {
806 if (Mn[ibj][k] == i) {
807 toc[i][j] -= tov[ibj][k];
808 }
809 }
810 }
811 }
812
813 // Send messages from check nodes to variable nodes
814 for (int i = 0; i < M; ++i) {
815 for (int j = 0; j < 7;
816 ++j) { // Fixed range [0, 7) to match Fortran's 1:7, could be
817 // nrw[j], or 7 logically
818 tanhtoc[i][j] = std::tanh(-toc[i][j] / 2.0f);
819 }
820 }
821
822 for (int i = 0; i < N; ++i) {
823 for (int j = 0; j < BP_MAX_CHECKS; ++j) {
824 int ichk = Mn[i][j];
825 if (ichk >= 0) {
826 float Tmn = 1.0f;
827 for (int k = 0; k < Nm[ichk].valid_neighbors; ++k) {
828 if (Nm[ichk].neighbors[k] != i) {
829 Tmn *= tanhtoc[ichk][k];
830 }
831 }
832 tov[i][j] = 2.0f * std::atanh(-Tmn);
833 }
834 }
835 }
836 }
837
838 return -1; // Decoding failed
839}
840} // namespace
841
842/******************************************************************************/
843// Local Routines
844/******************************************************************************/
845
846namespace {
847constexpr std::string_view alphabet =
848 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
849
850static_assert(alphabet.size() == 64);
851
852// Function that either translates valid JS8 message characters to their
853// corresponding 6-bit word value, or throws. This will end up doing a
854// direct index operation into a 256-byte table, the creation of which
855// must be constexpr under C++17.
856
857constexpr auto alphabetWord = []() {
858 constexpr std::uint8_t invalid = 0xff;
859
860 constexpr auto words = []() {
861 std::array<std::uint8_t, 256> words{};
862
863 for (auto &word : words)
864 word = invalid;
865
866 for (std::size_t i = 0; i < alphabet.size(); ++i) {
867 words[static_cast<std::uint8_t>(alphabet[i])] =
868 static_cast<std::uint8_t>(i);
869 }
870
871 return words;
872 }();
873
874 return [words](char const value) {
875 if (auto const word = words[value]; word != invalid) {
876 return word;
877 }
878
879 throw std::runtime_error("Invalid character in message");
880 };
881}();
882
883// Sanity check key bounds of the 6-bit encoding table.
884
885static_assert(alphabetWord('0') == 0);
886static_assert(alphabetWord('A') == 10);
887static_assert(alphabetWord('a') == 36);
888static_assert(alphabetWord('-') == 62);
889static_assert(alphabetWord('+') == 63);
890
891template <typename T> std::uint16_t CRC12(T const &range) {
892 return boost::augmented_crc<12, 0xc06>(range.data(), range.size()) ^ 42;
893}
894
895bool checkCRC12(std::array<std::int8_t, KK> const &decoded) {
896 std::array<uint8_t, 11> bits = {};
897
898 for (std::size_t i = 0; i < decoded.size(); ++i) {
899 if (decoded[i])
900 bits[i / 8] |= (1 << (7 - (i % 8)));
901 }
902
903 // Extract the received CRC-12.
904
905 uint16_t crc = (static_cast<uint16_t>(bits[9] & 0x1F) << 7) |
906 (static_cast<uint16_t>(bits[10]) >> 1);
907
908 // Clear bits that correspond to the CRC in the last bytes.
909
910 bits[9] &= 0xE0;
911 bits[10] = 0x00;
912
913 // Compute CRC and indicate if we have a match.
914
915 return crc == CRC12(bits);
916}
917
918std::string extractmessage174(std::array<int8_t, KK> const &decoded) {
919 std::string message;
920
921 // Ensure received CRC matches computed CRC.
922
923 if (checkCRC12(decoded)) {
924 message.reserve(12);
925
926 // Decode the message from the 72 data bits
927
928 std::array<uint8_t, 12> words;
929
930 for (std::size_t i = 0; i < 12; ++i) {
931 words[i] = (decoded[i * 6 + 0] << 5) | (decoded[i * 6 + 1] << 4) |
932 (decoded[i * 6 + 2] << 3) | (decoded[i * 6 + 3] << 2) |
933 (decoded[i * 6 + 4] << 1) | (decoded[i * 6 + 5] << 0);
934 }
935
936 // Map 6-bit words to the alphabet
937
938 for (auto const word : words)
939 message += alphabet[word];
940 }
941
942 return message;
943}
944
945// Parity matrix for JS8 message generation.
946//
947// This should be 952 bytes in size; to store an 87x87 matrix of bits,
948// you need 7569 bits, which requires 119 64-bit values, or 952 bytes.
949//
950// Background here is that this is a low-density parity check code (LDPC),
951// generated using the PEG algorithm. In short, true values in a row i of
952// the matrix define which of the 87 message bits must be summed, modulo
953// 2, to produce the ith parity check bit. Decent references on this are:
954//
955// 1. https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf
956// 2. https://inference.org.uk/mackay/PEG_ECC.html
957// 3. https://github.com/Lcrypto/classic-PEG-
958//
959// The data used was harvested from the original 'ldpc_174_87_params.f90',
960// but you'll note that the rows have been reordered here, because this
961// isn't Fortran; C++ is row-major, not column-major.
962
963constexpr auto parity = []() {
964 constexpr std::size_t Rows = 87;
965 constexpr std::size_t Cols = 87;
966
967 using ElementType = std::uint64_t;
968 constexpr std::size_t ElementSize =
969 std::numeric_limits<ElementType>::digits;
970
971 constexpr auto matrix = []() {
972 constexpr std::array<std::string_view, Rows> Data = {
973 "23bba830e23b6b6f50982e", "1f8e55da218c5df3309052",
974 "ca7b3217cd92bd59a5ae20", "56f78313537d0f4382964e",
975 "6be396b5e2e819e373340c", "293548a138858328af4210",
976 "cb6c6afcdc28bb3f7c6e86", "3f2a86f5c5bd225c961150",
977 "849dd2d63673481860f62c", "56cdaec6e7ae14b43feeee",
978 "04ef5cfa3766ba778f45a4", "c525ae4bd4f627320a3974",
979 "41fd9520b2e4abeb2f989c", "7fb36c24085a34d8c1dbc4",
980 "40fc3e44bb7d2bb2756e44", "d38ab0a1d2e52a8ec3bc76",
981 "3d0f929ef3949bd84d4734", "45d3814f504064f80549ae",
982 "f14dbf263825d0bd04b05e", "db714f8f64e8ac7af1a76e",
983 "8d0274de71e7c1a8055eb0", "51f81573dd4049b082de14",
984 "d8f937f31822e57c562370", "b6537f417e61d1a7085336",
985 "ecbd7c73b9cd34c3720c8a", "3d188ea477f6fa41317a4e",
986 "1ac4672b549cd6dba79bcc", "a377253773ea678367c3f6",
987 "0dbd816fba1543f721dc72", "ca4186dd44c3121565cf5c",
988 "29c29dba9c545e267762fe", "1616d78018d0b4745ca0f2",
989 "fe37802941d66dde02b99c", "a9fa8e50bcb032c85e3304",
990 "83f640f1a48a8ebc0443ea", "3776af54ccfbae916afde6",
991 "a8fc906976c35669e79ce0", "f08a91fb2e1f78290619a8",
992 "cc9da55fe046d0cb3a770c", "d36d662a69ae24b74dcbd8",
993 "40907b01280f03c0323946", "d037db825175d851f3af00",
994 "1bf1490607c54032660ede", "0af7723161ec223080be86",
995 "eca9afa0f6b01d92305edc", "7a8dec79a51e8ac5388022",
996 "9059dfa2bb20ef7ef73ad4", "6abb212d9739dfc02580f2",
997 "f6ad4824b87c80ebfce466", "d747bfc5fd65ef70fbd9bc",
998 "612f63acc025b6ab476f7c", "05209a0abb530b9e7e34b0",
999 "45b7ab6242b77474d9f11a", "6c280d2a0523d9c4bc5946",
1000 "f1627701a2d692fd9449e6", "8d9071b7e7a6a2eed6965e",
1001 "bf4f56e073271f6ab4bf80", "c0fc3ec4fb7d2bb2756644",
1002 "57da6d13cb96a7689b2790", "a9fa2eefa6f8796a355772",
1003 "164cc861bdd803c547f2ac", "cc6de59755420925f90ed2",
1004 "a0c0033a52ab6299802fd2", "b274db8abd3c6f396ea356",
1005 "97d4169cb33e7435718d90", "81cfc6f18c35b1e1f17114",
1006 "481a2a0df8a23583f82d6c", "081c29a10d468ccdbcecb6",
1007 "2c4142bf42b01e71076acc", "a6573f3dc8b16c9d19f746",
1008 "c87af9a5d5206abca532a8", "012dee2198eba82b19a1da",
1009 "b1ca4ea2e3d173bad4379c", "b33ec97be83ce413f9acc8",
1010 "5b0f7742bca86b8012609a", "37d8e0af9258b9e8c5f9b2",
1011 "35ad3fb0faeb5f1b0c30dc", "6114e08483043fd3f38a8a",
1012 "cd921fdf59e882683763f6", "95e45ecd0135aca9d6e6ae",
1013 "2e547dd7a05f6597aac516", "14cd0f642fc0c5fe3a65ca",
1014 "3a0a1dfd7eee29c2e827e0", "c8b5dffc335095dcdcaf2a",
1015 "3dd01a59d86310743ec752", "8abdb889efbe39a510a118",
1016 "3f231f212055371cf3e2a2"};
1017
1018 constexpr std::size_t Total = (Rows * Cols + ElementSize - 1);
1019 constexpr std::size_t Count = Total / ElementSize;
1020 constexpr std::array<std::uint8_t, 4> Masks = {0x8, 0x4, 0x2, 0x1};
1021
1022 std::array<ElementType, Count> data{};
1023
1024 for (std::size_t row = 0; row < Rows; ++row) {
1025 std::size_t col = 0;
1026
1027 for (auto const c : Data[row]) {
1028 std::uint8_t const value =
1029 (c >= '0' && c <= '9') ? c - '0'
1030 : (c >= 'a' && c <= 'f') ? c - 'a' + 10
1031 : (c >= 'A' && c <= 'F') ? c - 'A' + 10
1032 : throw "Invalid hex";
1033
1034 for (auto const mask : Masks) {
1035 if (col >= Cols)
1036 break;
1037 if (value & mask) {
1038 auto const index = row * Cols + col;
1039 data[index / ElementSize] |=
1040 (ElementType(1) << (index % ElementSize));
1041 }
1042 ++col;
1043 }
1044 }
1045 }
1046 return data;
1047 }();
1048
1049 return [matrix](std::size_t const row, std::size_t const col) {
1050 auto const index = row * Cols + col;
1051 return (matrix[index / ElementSize] >> (index % ElementSize)) & 1;
1052 };
1053}();
1054} // namespace
1055
1056/******************************************************************************/
1057// DecodeMode Template Class
1058/******************************************************************************/
1059
1060// Mode-parameterized decode class.
1061
1062namespace {
1063template <typename Mode> class DecodeMode {
1064 // Data members
1065
1066 std::array<float, Mode::NFFT1> nuttal;
1067 std::array<std::array<std::array<std::complex<float>, Mode::NDOWNSPS>, 7>,
1068 3>
1069 csyncs;
1070 alignas(64) std::array<std::complex<float>, Mode::NDOWNSPS> csymb;
1071 alignas(64) std::array<std::complex<float>, Mode::NMAX> filter;
1072 alignas(64) std::array<std::complex<float>, Mode::NMAX> cfilt;
1073 alignas(64) std::array<std::complex<float>, Mode::NDFFT1 / 2 + 1> ds_cx;
1074 alignas(64) std::array<std::complex<float>, Mode::NFFT1 / 2 + 1> sd;
1075 alignas(64) std::array<std::complex<float>, NP> cd0;
1076 std::array<float, Mode::NMAX> dd;
1077 std::array<std::array<float, Mode::NHSYM>, Mode::NSPS> s;
1078 std::array<float, Mode::NSPS> savg;
1079 FFTWPlanManager plans;
1080 SyncIndex sync;
1081 js8::SoftCombiner<N> m_softCombiner;
1082 bool m_enableFreqTracking = true;
1083 bool m_enableTimingTracking = true;
1084 float m_llrErasureThreshold = js8::llrErasureThreshold();
1085 bool m_enableLdpcFeedback = js8::ldpcFeedbackEnabled();
1086 int m_maxLdpcPasses = js8::ldpcFeedbackMaxPasses();
1087
1088 using Plan = FFTWPlanManager::Type;
1089
1090 static constexpr auto Costas = JS8::Costas::array(Mode::NCOSTAS);
1091
1092 // Fore and aft tapers to reduce spectral leakage during the
1093 // downsampling process. We can compute these at compile time.
1094
1095 static constexpr auto Taper = [] {
1096 std::array<std::array<float, Mode::NDD + 1>, 2> taper{};
1097
1098 for (size_t i = 0; i <= Mode::NDD; ++i) {
1099 float const value =
1100 0.5f *
1101 (1.0f + cos_approx(i * std::numbers::pi_v<float> / Mode::NDD));
1102
1103 taper[1][i] = value; // TailTaper (original taper)
1104 taper[0][Mode::NDD - i] = value; // HeadTaper (reversed taper)
1105 }
1106
1107 return taper;
1108 }();
1109
1110 // Baseline computation support.
1111
1112 using Points = Eigen::Matrix<double, BASELINE_NODES.size(), 2>;
1113 using Vandermonde =
1114 Eigen::Matrix<double, BASELINE_NODES.size(), BASELINE_NODES.size()>;
1115 using Coefficients = Eigen::Vector<double, BASELINE_NODES.size()>;
1116
1117 Points p;
1118 Vandermonde V;
1119 Coefficients c;
1120
1121 // Polynomial evaluation using Estrin's method, loop is unrolled at
1122 // compile time. A compiler should emit SIMD instructions from what
1123 // it sees here when the optimizer is involved, but even without it,
1124 // we'll likely see fused multiply-add instructions.
1125
1126 inline auto evaluate(float const x) const {
1127 return [this]<Eigen::Index... I>(
1128 float const x, std::integer_sequence<Eigen::Index, I...>) {
1129 auto baseline = 0.0;
1130 auto exponent = 1.0;
1131
1132 ((baseline += (c[I * 2] + c[I * 2 + 1] * x) * exponent,
1133 exponent *= x * x),
1134 ...);
1135
1136 return static_cast<float>(baseline);
1137 }(x, std::make_integer_sequence<Eigen::Index,
1138 Coefficients::SizeAtCompileTime / 2>{});
1139 }
1140
1141 std::optional<Decode> js8dec(bool const syncStats, bool const lsubtract,
1142 float &f1, float &xdt, int &nharderrors,
1143 float &xsnr, JS8::Event::Emitter emitEvent) {
1144 constexpr float FR = 12000.0f / Mode::NFFT1; // Frequency resolution
1145 constexpr float FS2 = 12000.0f / Mode::NDOWN;
1146 constexpr float DT2 = 1.0f / FS2;
1147
1148 float const coarseStartHz = f1;
1149 float const coarseStartDt = xdt;
1150
1151 auto const index =
1152 static_cast<int>(std::round(f1 / FR)); // Closest index
1153 float const scaled_value =
1154 0.1f * (savg[index] - Mode::BASESUB); // Adjust and scale
1155 float const xbase =
1156 std::pow(10.0f, scaled_value); // Convert to linear scale
1157
1158 float delfbest = 0.0f;
1159 int ibest = 0;
1160
1161 // Downsample the signal and prepare for processing.
1162
1163 js8_downsample(f1);
1164
1165 // Initial guess for the start of the signal.
1166
1167 int i0 = static_cast<int>(std::round((xdt + Mode::ASTART) * FS2));
1168 float smax = 0.0f;
1169
1170 // Search for the best synchronization offset.
1171
1172 for (int idt = i0 - Mode::NQSYMBOL; idt <= i0 + Mode::NQSYMBOL; ++idt) {
1173 float const sync = syncjs8d(idt, 0.0f);
1174
1175 if (sync > smax) {
1176 smax = sync;
1177 ibest = idt;
1178 }
1179 }
1180
1181 // Improved estimate for DT.
1182
1183 float const xdt2 = ibest * DT2;
1184
1185 // Fine frequency synchronization
1186
1187 i0 = static_cast<int>(std::round(xdt2 * FS2));
1188 smax = 0.0f;
1189
1190 for (int ifr = -NFSRCH; ifr <= NFSRCH; ++ifr) {
1191 float const delf = ifr * 0.5f;
1192 float const sync = syncjs8d(i0, delf);
1193
1194 if (sync > smax) {
1195 smax = sync;
1196 delfbest = delf;
1197 }
1198 }
1199
1200 // Frequency tweaking.
1201
1202 float const dphi = -delfbest * ((2.0f * std::numbers::pi_v<float>) /
1203 FS2); // Phase increment
1204 std::complex<float> const wstep =
1205 std::polar(1.0f, dphi); // Step for phase rotation
1206 std::complex<float> w = {1.0f, 0.0f}; // Cumlative phase
1207
1208 for (int i = 0; i < NP2; ++i) {
1209 w *= wstep; // Update cumulative phase
1210 cd0[i] *= w; // Apply phase shift
1211 }
1212
1213 // Adjust the frequency and time offset.
1214
1215 xdt = xdt2;
1216 f1 += delfbest;
1217
1218 float const sync = syncjs8d(i0, 0.0f);
1219
1220 std::array<std::array<float, NN>, NROWS> s2;
1221
1222 js8::FrequencyTracker freqTracker;
1223 if (m_enableFreqTracking) {
1224 freqTracker.reset(0.0, FS2);
1225 } else {
1226 freqTracker.disable();
1227 }
1228
1229 // Scale timing clamp relative to samples per symbol so short-symbol
1230 // modes aren't allowed outsized shifts (e.g., 12-sample JS8 40 formerly "Turbo").
1231 double const timingMaxShift =
1232 std::clamp(0.08 * static_cast<double>(Mode::NDOWNSPS), 0.5, 2.0);
1233
1234 js8::TimingTracker timingTracker;
1235 if (m_enableTimingTracking) {
1236 timingTracker.reset(0.0, 0.15, 0.35, timingMaxShift);
1237 } else {
1238 timingTracker.disable();
1239 }
1240
1241 auto const estimateResidualHz =
1242 [&](int expectedTone) -> std::optional<float> {
1243 if (!freqTracker.enabled())
1244 return std::nullopt;
1245
1246 if (expectedTone < 0 || expectedTone + 1 >= Mode::NDOWNSPS)
1247 return std::nullopt;
1248
1249 float const m0 = std::norm(csymb[expectedTone]);
1250 float const mplus = std::norm(csymb[expectedTone + 1]);
1251 float const mminus =
1252 expectedTone > 0 ? std::norm(csymb[expectedTone - 1]) : 0.0f;
1253
1254 if (m0 <= 0.0f)
1255 return std::nullopt;
1256
1257 float const ratio = m0 / (mplus + mminus + 1e-12f);
1258 if (ratio < 1.5f)
1259 return std::nullopt;
1260
1261 float const denom = mminus - 2.0f * m0 + mplus;
1262 if (std::abs(denom) < 1e-9f)
1263 return std::nullopt;
1264
1265 float delta = 0.5f * (mminus - mplus) / denom;
1266 delta = std::clamp(delta, -0.5f, 0.5f);
1267
1268 return delta * (FS2 / Mode::NDOWNSPS);
1269 };
1270
1271 auto const logTracker = [&](char const *tag) {
1272 if (decoder_js8().isDebugEnabled()) {
1273 if (freqTracker.enabled()) {
1274 qCDebug(decoder_js8)
1275 << "freqTracker" << tag << "coarseHz" << coarseStartHz
1276 << "fineHz" << f1 << "refinedHz"
1277 << f1 + static_cast<float>(freqTracker.currentHz())
1278 << "avgStepHz"
1279 << static_cast<float>(freqTracker.averageStepHz());
1280 }
1281
1282 if (timingTracker.enabled()) {
1283 qCDebug(decoder_js8)
1284 << "timingTracker" << tag << "coarseDt" << coarseStartDt
1285 << "fineDt" << xdt << "refinedDt"
1286 << xdt + static_cast<float>(
1287 timingTracker.currentSamples() * DT2)
1288 << "avgStepSmpl"
1289 << static_cast<float>(
1290 timingTracker.averageStepSamples());
1291 }
1292 }
1293 };
1294
1295 auto const goertzelEnergy =
1296 [&](int start, int expectedTone) -> std::optional<float> {
1297 if (start < 0 || start + Mode::NDOWNSPS > NP2)
1298 return std::nullopt;
1299
1300 std::array<std::complex<float>, Mode::NDOWNSPS> tmp;
1301 std::copy(cd0.begin() + start, cd0.begin() + start + Mode::NDOWNSPS,
1302 tmp.begin());
1303
1304 if (freqTracker.enabled()) {
1305 freqTracker.apply(tmp.data(), Mode::NDOWNSPS);
1306 }
1307
1308 auto const wstep = std::polar(
1309 1.0f, static_cast<float>(-TAU * expectedTone / Mode::NDOWNSPS));
1310 auto phase = std::complex<float>{1.0f, 0.0f};
1311 std::complex<float> acc{0.0f, 0.0f};
1312
1313 for (auto const &sample : tmp) {
1314 acc += sample * std::conj(phase);
1315 phase *= wstep;
1316 }
1317
1318 return std::norm(acc);
1319 };
1320
1321 for (int k = 0; k < NN; ++k) {
1322 // Calculate the starting index for the current symbol.
1323
1324 int const i1Base = ibest + k * Mode::NDOWNSPS;
1325 int const timingShift = timingTracker.enabled()
1326 ? static_cast<int>(std::round(
1327 timingTracker.currentSamples()))
1328 : 0;
1329 int i1 = i1Base + timingShift;
1330
1331 if (i1 < 0) {
1332 i1 = 0;
1333 } else if (i1 + Mode::NDOWNSPS > NP2) {
1334 i1 = NP2 - Mode::NDOWNSPS;
1335 }
1336
1337 csymb.fill(ZERO);
1338
1339 if (i1 >= 0 && i1 + Mode::NDOWNSPS <= NP2) {
1340 std::copy(cd0.begin() + i1, cd0.begin() + i1 + Mode::NDOWNSPS,
1341 csymb.begin());
1342
1343 if (freqTracker.enabled()) {
1344 freqTracker.apply(csymb.data(), Mode::NDOWNSPS);
1345 }
1346 }
1347
1348 fftwf_execute(plans[Plan::CS]);
1349
1350 // Normalize and take the magnitude of the first 8 points.
1351
1352 for (int i = 0; i < NROWS; ++i) {
1353 s2[i][k] = std::abs(csymb[i]) / 1000.0f;
1354 }
1355
1356 if (freqTracker.enabled() || timingTracker.enabled()) {
1357 bool const isPilot =
1358 (k < 7) || (k >= 36 && k < 43) || (k >= 72 && k < 79);
1359
1360 if (isPilot) {
1361 int costasBlock = 0;
1362 int costasColumn = k;
1363
1364 if (k >= 36 && k < 43) {
1365 costasBlock = 1;
1366 costasColumn = k - 36;
1367 } else if (k >= 72 && k < 79) {
1368 costasBlock = 2;
1369 costasColumn = k - 72;
1370 }
1371
1372 int const expectedTone = Costas[costasBlock][costasColumn];
1373
1374 if (auto const residual =
1375 estimateResidualHz(expectedTone)) {
1376 freqTracker.update(*residual);
1377 }
1378
1379 if (timingTracker.enabled()) {
1380 auto const e0 = goertzelEnergy(i1, expectedTone);
1381 auto const eEarly =
1382 goertzelEnergy(i1 - 1, expectedTone);
1383 auto const eLate = goertzelEnergy(i1 + 1, expectedTone);
1384
1385 float const toneMag = s2[expectedTone][k];
1386
1387 if (e0 && eEarly && eLate && toneMag > 1e-6f) {
1388 float const denom = *e0 + 1e-6f;
1389 float const grad = (*eLate - *eEarly) / denom;
1390
1391 // Smaller steps; favor stability.
1392 double const weight = std::clamp(
1393 static_cast<double>(toneMag / 5.0f), 0.0, 1.0);
1394 double const errorSamples = std::clamp(
1395 0.25 * static_cast<double>(grad), -1.0, 1.0);
1396
1397 timingTracker.update(errorSamples, weight);
1398 }
1399 }
1400 }
1401 }
1402 }
1403
1404 // Sync quality check using Costas tone patterns.
1405
1406 int nsync = 0;
1407
1408 for (std::size_t costas = 0; costas < Costas.size(); ++costas) {
1409 auto const offset = costas * 36;
1410
1411 for (std::size_t column = 0; column < 7; ++column) {
1412 // Find the row containing the maximum value in the
1413 // current column.
1414
1415 auto const max_row = std::distance(
1416 s2.begin(),
1417 std::max_element(s2.begin(), s2.end(),
1418 [index = offset + column](
1419 auto const &rowA, auto const &rowB) {
1420 return rowA[index] < rowB[index];
1421 }));
1422
1423 // Check if the max row matches the Costas pattern.
1424
1425 if (Costas[costas][column] == max_row)
1426 ++nsync;
1427 }
1428 }
1429
1430 // If the sync quality isn't at least 7, this one's a loser.
1431
1432 if (nsync <= 6) {
1433 logTracker("sync_fail");
1434 return std::nullopt;
1435 }
1436
1437 if (syncStats)
1438 emitEvent(
1439 JS8::Event::SyncState{JS8::Event::SyncState::Type::CANDIDATE,
1440 Mode::NSUBMODE,
1441 f1,
1442 xdt,
1443 {.candidate = nsync}});
1444
1445 std::array<std::array<float, ND>, NROWS> s1;
1446
1447 // Fill s1 from s2, excluding the Costas arrays.
1448
1449 for (int row = 0; row < NROWS; ++row) {
1450 std::copy(s2[row].begin() + 7, s2[row].begin() + 36,
1451 s1[row].begin());
1452 std::copy(s2[row].begin() + 43, s2[row].begin() + 72,
1453 s1[row].begin() + 29);
1454 }
1455
1456 // Identify winning tones (max magnitude) for each data symbol.
1457 std::array<int, ND> symbolWinners = {};
1458
1459 for (int j = 0; j < ND; ++j) {
1460 int winner = 0;
1461 float best = s1[0][j];
1462
1463 for (int i = 1; i < NROWS; ++i) {
1464 if (s1[i][j] > best) {
1465 best = s1[i][j];
1466 winner = i;
1467 }
1468 }
1469
1470 symbolWinners[j] = winner;
1471 }
1472
1473 auto const whitening = js8::WhiteningProcessor<NROWS, ND, N>::process(
1474 s1, symbolWinners, m_llrErasureThreshold,
1475 decoder_js8().isDebugEnabled());
1476
1477 auto llr0 = whitening.llr0;
1478 auto llr1 = whitening.llr1;
1479
1480 // Only apply a second erasure threshold pass if whitening didn't
1481 // already zero low-magnitude LLRs using the configured threshold.
1482 if (!whitening.erasureApplied) {
1483 std::size_t erasuresAfterThreshold = 0;
1484 double sumAbsPreErasure = 0.0;
1485 double sumAbsPostErasure = 0.0;
1486
1487 auto const applyErasureThreshold = [&](auto &llr) {
1488 for (auto const value : llr)
1489 sumAbsPreErasure += std::abs(value);
1490
1491 if (m_llrErasureThreshold > 0.0f) {
1492 for (auto &value : llr) {
1493 if (std::abs(value) < m_llrErasureThreshold) {
1494 value = 0.0f;
1495 ++erasuresAfterThreshold;
1496 }
1497
1498 sumAbsPostErasure += std::abs(value);
1499 }
1500 } else {
1501 for (auto const value : llr)
1502 sumAbsPostErasure += std::abs(value);
1503 }
1504 };
1505
1506 applyErasureThreshold(llr0);
1507 applyErasureThreshold(llr1);
1508
1509 if (decoder_js8().isDebugEnabled()) {
1510 auto const total =
1511 static_cast<double>(llr0.size() + llr1.size());
1512 double const avgPre =
1513 total > 0.0 ? sumAbsPreErasure / total : 0.0;
1514 double const avgPost =
1515 total > 0.0 ? sumAbsPostErasure / total : 0.0;
1516
1517 qCDebug(decoder_js8)
1518 << "LLR erasure threshold" << m_llrErasureThreshold
1519 << "erasures:" << erasuresAfterThreshold
1520 << "avg|LLR| pre/post:" << avgPre << avgPost;
1521 }
1522 }
1523
1524 auto const ttl = std::chrono::seconds{Mode::NTXDUR * 2};
1525
1526 m_softCombiner.flush(ttl);
1527
1528 auto const key =
1529 m_softCombiner.makeKey(Mode::NSUBMODE, f1, xdt, llr0, llr1);
1530 auto combined = m_softCombiner.combine(key, llr0, llr1, ttl);
1531
1532 auto llr0Combined = combined.llr0;
1533 auto llr1Combined = combined.llr1;
1534
1535 std::array<int8_t, K> decoded;
1536 std::array<int8_t, N> cw;
1537
1538 int totalLdpcPasses = 0;
1539 bool usedFeedbackPass = false;
1540 bool feedbackTurnedSuccess = false;
1541 int feedbackConfident = 0;
1542 int feedbackUncertain = 0;
1543
1544 auto const tryDecode = [&](std::array<float, N> const &llrInput,
1545 int ipass) -> std::optional<Decode> {
1546 nharderrors = bpdecode174(llrInput, decoded, cw);
1547 xsnr = -99.0f;
1548
1549 if (std::all_of(cw.begin(), cw.end(),
1550 [](int x) { return x == 0; })) {
1551 return std::nullopt;
1552 }
1553
1554 if (nharderrors >= 0 && nharderrors < 60 &&
1555 !(sync < 2.0f && nharderrors > 35) &&
1556 !(ipass > 2 && nharderrors > 39) &&
1557 !(ipass == 4 && nharderrors > 30)) {
1558 if (checkCRC12(decoded)) {
1559 if (syncStats)
1560 emitEvent(JS8::Event::SyncState{
1561 JS8::Event::SyncState::Type::DECODED,
1562 Mode::NSUBMODE,
1563 f1,
1564 xdt2,
1565 {.decoded = sync}});
1566
1567 auto message = extractmessage174(decoded);
1568
1569 int const i3bit =
1570 (decoded[72] << 2) | (decoded[73] << 1) | decoded[74];
1571
1572 std::array<int, NN> itone;
1573
1574 JS8::encode(i3bit, Costas, message.data(), itone.data());
1575
1576 if (lsubtract)
1577 subtractjs8(genjs8refsig(itone, f1), xdt2);
1578
1579 float xsig = 0.0f;
1580
1581 for (std::size_t i = 0; i < itone.size(); ++i) {
1582 xsig += std::pow(s2[itone[i]][i], 2);
1583 }
1584
1585 xsnr =
1586 std::max(10.0f * std::log10(std::max(
1587 xsig / xbase - 1.0f, 1.259e-10f)) -
1588 32.0f,
1589 -60.0f); // XXX was -28.0f in Fortran
1590
1591 m_softCombiner.markDecoded(combined.key);
1592
1593 logTracker("decoded");
1594 return std::make_optional<Decode>(i3bit, message);
1595 }
1596 } else {
1597 nharderrors = -1;
1598 }
1599
1600 return std::nullopt;
1601 };
1602
1603 // Loop over decoding passes
1604 for (int ipass = 1; ipass <= 4 && totalLdpcPasses < m_maxLdpcPasses;
1605 ++ipass) {
1606 auto &llr = ipass == 2 ? llr1Combined : llr0Combined;
1607
1608 // Zero ranges for certain passes to mirror legacy behavior.
1609 if (ipass == 3)
1610 std::fill(llr0Combined.begin(), llr0Combined.begin() + 24,
1611 0.0f);
1612 else if (ipass == 4)
1613 std::fill(llr0Combined.begin() + 24, llr0Combined.begin() + 48,
1614 0.0f);
1615
1616 std::array<float, N> llrPrimary = llr;
1617 if (auto result = tryDecode(llrPrimary, ipass)) {
1618 ++totalLdpcPasses;
1619 return result;
1620 }
1621 ++totalLdpcPasses;
1622
1623 // Feedback refinement and second attempt, if enabled and budget
1624 // allows.
1625 if (m_enableLdpcFeedback && totalLdpcPasses < m_maxLdpcPasses) {
1626 std::array<float, N> llrRefined;
1627 int confident = 0;
1628 int uncertain = 0;
1629 js8::refineLlrsWithLdpcFeedback(
1630 llrPrimary, cw, m_llrErasureThreshold, llrRefined,
1631 confident, uncertain);
1632
1633 if (decoder_js8().isDebugEnabled()) {
1634 qCDebug(decoder_js8)
1635 << "LDPC feedback pass"
1636 << "ipass" << ipass << "confident" << confident
1637 << "uncertain" << uncertain;
1638 }
1639
1640 usedFeedbackPass = true;
1641 feedbackConfident += confident;
1642 feedbackUncertain += uncertain;
1643
1644 if (auto result = tryDecode(llrRefined, ipass)) {
1645 ++totalLdpcPasses;
1646 feedbackTurnedSuccess = true;
1647 if (decoder_js8().isDebugEnabled()) {
1648 qCDebug(decoder_js8)
1649 << "LDPC feedback succeeded on second pass"
1650 << "ipass" << ipass << "confident"
1651 << feedbackConfident << "uncertain"
1652 << feedbackUncertain << "passes" << totalLdpcPasses;
1653 }
1654 return result;
1655 }
1656
1657 ++totalLdpcPasses;
1658 }
1659 }
1660
1661 if (decoder_js8().isDebugEnabled()) {
1662 qCDebug(decoder_js8)
1663 << "LDPC feedback summary"
1664 << "used" << usedFeedbackPass << "success"
1665 << feedbackTurnedSuccess << "confident" << feedbackConfident
1666 << "uncertain" << feedbackUncertain << "passes"
1667 << totalLdpcPasses;
1668 }
1669
1670 logTracker("fail");
1671 return std::nullopt;
1672 }
1673
1674 // Compute noise baseline. We differ quite a bit from the Fortran
1675 // implementation here.
1676 //
1677 // The Fortran version took `savg` as input; power scaled data from
1678 // `syncjs8`, and produced `sbase`, the noise baseline. To accomplish
1679 // that, it used up to 1000 lower envelope points for the polynomial
1680 // determination, which caused some oddities when the matrix was
1681 // ill-conditioned; we're just looking for a low-order polynomial
1682 // here, and a massively tall matrix isn't in general going to be
1683 // helpful there. Additionally, the methodology seemed to be very
1684 // susceptible to Runge's phenomenon.
1685 //
1686 // This approach instead uses a number of Chebyshev nodes proportional
1687 // to the polynomial degree, and we evaluate 2 kHz of `savg`, centered
1688 // around 1.5 kHz, to determine the polynomial, figuring that that's
1689 // going to be an optimal place to measure the 10% noise floor. We then
1690 // map the [ia, ib] range to the domain of the polynomial to compute
1691 // the baseline. Since `savg` would otherwise no longer be referenced
1692 // beyond this function, we dispense with `sbase` and instead overwrite
1693 // `savg` with the baseline.
1694
1695 void baselinejs8(int const ia, int const ib) {
1696 // Data referenced in savg is defined by the closed range [bmin, bmax].
1697 // From this we can derive the size of the closed range and the number
1698 // of points in each of the arms on either side of a node. All of these
1699 // values can be computed at compile time.
1700
1701 using boost::math::ccmath::round;
1702
1703 constexpr auto bmin =
1704 static_cast<std::size_t>(round(BASELINE_MIN / Mode::DF));
1705 constexpr auto bmax =
1706 static_cast<std::size_t>(round(BASELINE_MAX / Mode::DF));
1707 constexpr auto size = bmax - bmin + 1;
1708 constexpr auto arm = size / (2 * BASELINE_NODES.size());
1709
1710 // Loop invariants; beginning of the data range, sentinel one past the
1711 // end of the range.
1712
1713 auto const data = savg.begin() + bmin;
1714 auto const end = data + size;
1715
1716 // Convert savg range of interest from power scale to dB scale.
1717
1718 std::transform(data, end, data, [](float const value) {
1719 return 10.0f * std::log10(value);
1720 });
1721
1722 // Collect lower envelope points; use Chebyshev node interpolants
1723 // to reduce Runge's phenomenon oscillations.
1724
1725 for (std::size_t i = 0; i < BASELINE_NODES.size(); ++i) {
1726 auto const node = size * BASELINE_NODES[i];
1727 auto const base = data + static_cast<int>(std::round(node));
1728 auto span = std::vector<float>(std::clamp(base - arm, data, end),
1729 std::clamp(base + arm, data, end));
1730
1731 auto const n = span.size() * BASELINE_SAMPLE / 100;
1732
1733 std::nth_element(span.begin(), span.begin() + n, span.end());
1734
1735 p.row(i) << node, span[n];
1736 }
1737
1738 // Extract x and y values from points and prepare the Vandermonde
1739 // matrix, initializing the first column with 1 (x^0); remaining
1740 // columns are filled with the Schur product.
1741
1742 Eigen::VectorXd x = p.col(0);
1743 Eigen::VectorXd y = p.col(1);
1744
1745 V.col(0).setOnes();
1746 for (Eigen::Index i = 1; i < V.cols(); ++i) {
1747 V.col(i) = V.col(i - 1).cwiseProduct(x);
1748 }
1749
1750 // Solve the least squares problem for polynomial coefficients.
1751
1752 c = V.colPivHouseholderQr().solve(y);
1753
1754 // To map an index i in the range [ia, ib] to the polynomial's
1755 // input domain [0, size - 1]:
1756 //
1757 // i - ia
1758 // x = ------- * (size - 1)
1759 // ib - ia
1760
1761 auto const mapIndex = [ia, ib, last = size - 1](int const i) {
1762 return (i - ia) * last / float(ib - ia);
1763 };
1764
1765 // Replace savg with a computed baseline in the range [ia, ib].
1766 // This might be interpolation, which should be quite accurate,
1767 // or extrapolation, likely somewhat less so the further we get
1768 // from the polynomial fitting domain, but hopefully still good
1769 // enough for our purposes here.
1770
1771 savg.fill(0.0f);
1772
1773 for (int i = ia; i <= ib; ++i) {
1774 savg[i] = evaluate(mapIndex(i)) + 0.65f;
1775 }
1776 }
1777
1778 // Extracted from the downsampling process; this step is part of the
1779 // frequency-domain filtering process for downsampling the JS8 signal.
1780 // After the FFT, the resulting frequency-domain data (ds_cx) can be
1781 // manipulated (e.g., band-pass filtered or shifted). Subsequent inverse
1782 // FFT operations convert the filtered data back to the time domain at
1783 // a lower sample rate, achieving the desired downsampling.
1784
1785 void computeBasebandFFT() {
1786 // ds_dx is an array of complex<float>; we're going to do an in-place
1787 // FFT, so we'll interpret the first half of the array as if they were
1788 // floats, which they are.
1789
1790 float *fftw_real = reinterpret_cast<float *>(ds_cx.data());
1791
1792 // Copy in data and zero-pad any remainder; not all modes will have
1793 // a remainder.
1794
1795 std::copy(dd.begin(), dd.end(), fftw_real);
1796 std::fill(fftw_real + dd.size(), fftw_real + Mode::NDFFT1, 0.0f);
1797
1798 fftwf_execute(plans[Plan::BB]);
1799 }
1800
1801 // This function extracts a narrow frequency band around the target
1802 // frequency f0, applies tapering to reduce spectral artifacts, aligns the
1803 // signal to the center frequency, performs an inverse FFT to convert the
1804 // data back into the time domain, and normalizes the result for further
1805 // processing in the JS8 decoding pipeline.
1806
1807 void js8_downsample(float const f0) {
1808 // Frequency band extraction; identifies a narrow frequency band around
1809 // the target frequency (f0) based on a predefined range (8.5 baud above
1810 // and 1.5 baud below). The indices of this range in the
1811 // frequency-domain representation (ds_cx) are calculated (ib and it),
1812 // and the relevant frequency-domain samples are extracted into cd0.
1813
1814 constexpr float DF = 12000.0f / Mode::NDFFT1;
1815 constexpr float BAUD = 12000.0f / Mode::NSPS;
1816
1817 float const ft = f0 + 8.5f * BAUD;
1818 float const fb = f0 - 1.5f * BAUD;
1819 int const i0 = static_cast<int>(std::round(f0 / DF));
1820 int const it =
1821 std::min(static_cast<int>(std::round(ft / DF)), Mode::NDFFT1 / 2);
1822 int const ib = std::max(0, static_cast<int>(std::round(fb / DF)));
1823
1824 std::size_t const NDD_SIZE = Mode::NDD + 1;
1825 std::size_t const RANGE_SIZE = it - ib + 1;
1826
1827 std::fill_n(cd0.begin(), Mode::NDFFT2, ZERO);
1828
1829 std::copy(ds_cx.begin() + ib, ds_cx.begin() + ib + RANGE_SIZE,
1830 cd0.begin());
1831
1832 // Tapering is applied to smooth the edges of the frequency band,
1833 // reducing spectral leakage during the inverse FFT. Reversed taper at
1834 // the beginning, normal taper at the end.
1835
1836 auto const head = cd0.begin();
1837 auto const tail = cd0.begin() + RANGE_SIZE;
1838
1839 std::transform(head, head + NDD_SIZE, Taper[0].begin(), head,
1840 std::multiplies<>());
1841 std::transform(tail - NDD_SIZE, tail, Taper[1].begin(), tail - NDD_SIZE,
1842 std::multiplies<>());
1843
1844 // The extracted frequency band is aligned to the center of the
1845 // frequency domain representation (i0 - ib) via a cyclic shift using
1846 // std::rotate. This centers the desired signal.
1847
1848 std::rotate(cd0.begin(), cd0.begin() + (i0 - ib),
1849 cd0.begin() + Mode::NDFFT2);
1850
1851 // An inverse FFT is performed on the frequency-domain data (cd0) to
1852 // transform it back into the time domain, effectively yielding a
1853 // downsampled, time-domain signal focused on the extracted narrow
1854 // frequency band.
1855
1856 fftwf_execute(plans[Plan::DS]);
1857
1858 // The resulting time-domain samples are normalized by a factor derived
1859 // from the input and output FFT sizes (Mode::NDFFT1 and Mode::NDFFT2),
1860 // ensuring consistency in the signal’s amplitude.
1861
1862 float const factor =
1863 1.0f / std::sqrt(static_cast<float>(Mode::NDFFT1) * Mode::NDFFT2);
1864
1865 std::transform(cd0.begin(), cd0.end(), cd0.begin(),
1866 [factor](auto &value) { return value * factor; });
1867 }
1868
1869 // Evaluate the synchronization power of signal segments, ranks potential
1870 // candidates, and extracts the most promising ones for further decoding.
1871 //
1872 // Detailed Steps:
1873 //
1874 // 1. Compute Symbol Spectra:
1875 //
1876 // - The signal is processed in overlapping segments, with each segment
1877 // multiplied by
1878 // a Nuttall window to reduce spectral leakage.
1879 // - An FFT is performed on each windowed segment to obtain the
1880 // frequency-domain
1881 // representation.
1882 // - The power spectrum of each segment is computed, and the average
1883 // spectrum is
1884 // accumulated across segments.
1885 //
1886 // 2. Filter Edge Adjustments:
1887 //
1888 // - Adjusts the frequency bounds (nfa and nfb) to ensure the analysis
1889 // remains
1890 // within valid and meaningful regions of the signal.
1891 //
1892 // 3. Baseline Computation:
1893 //
1894 // - The average spectrum is converted to a dB scale.
1895 // - Baseline is computed to distinguish significant signal components
1896 // from
1897 // background noise.
1898 //
1899 // 4. Synchronization Metric Calculation:
1900 //
1901 // - For each frequency bin in the specified range, evaluates
1902 // synchronization
1903 // power using a Costas waveform.
1904 // - Sync metric is computed over the index range, considering all
1905 // combinations
1906 // of Costas patterns.
1907 // - The maximum sync value and its corresponding offset are recorded
1908 // for each
1909 // frequency bin.
1910 //
1911 // 5. Normalization:
1912 //
1913 // - The sync values are normalized to the 40th percentile value using a
1914 // ranked
1915 // index. This ensures a consistent scaling across different signals
1916 // and noise levels.
1917 //
1918 // 6. Candidate Extraction:
1919 //
1920 // - Candidates with a strong sync metric (above a defined threshold)
1921 // are extracted.
1922 // - Near-duplicate candidates of lesser synchronization power, based on
1923 // frequency
1924 // proximity, are eliminated.
1925 //
1926 // 7. Output:
1927 //
1928 // - Returns a vector of the most promising signal candidates, sorted by
1929 // their
1930 // synchronization power. It's expected that these will be re-sorted
1931 // by the caller into a desirable order, but synchronization power
1932 // order facilitates debugging this function.
1933 //
1934 // Note: The Fortran version of this routine would normalize `s` at the end
1935 // of this
1936 // function, but I'm unsure why; nothing beyond this function
1937 // references `s`, so it was effectively a somewhat expensive dead
1938 // store. It's been eliminated in this version.
1939
1940 std::vector<Sync> syncjs8(int nfa, int nfb) {
1941 // Compute symbol spectra
1942
1943 savg.fill(0.0f);
1944
1945 for (int j = 0; j < Mode::NHSYM; ++j) {
1946 int const ia = j * Mode::NSTEP;
1947 int const ib = ia + Mode::NFFT1;
1948
1949 if (ib > Mode::NMAX)
1950 break;
1951
1952 std::transform(dd.begin() + ia, dd.begin() + ib, nuttal.begin(),
1953 reinterpret_cast<float *>(sd.data()),
1954 std::multiplies<float>{});
1955
1956 fftwf_execute(plans[Plan::SD]);
1957
1958 // Compute power spectrum
1959
1960 for (int i = 0; i < Mode::NSPS; ++i) {
1961 auto const power = std::norm(sd[i]);
1962 s[i][j] = power;
1963 savg[i] += power;
1964 }
1965 }
1966
1967 // Filter edge sanity measures
1968
1969 int const nwin = nfb - nfa;
1970
1971 if (nfa < 100) {
1972 nfa = 100;
1973 if (nwin < 100)
1974 nfb = nfa + nwin;
1975 }
1976
1977 if (nfb > 4910) {
1978 nfb = 4910;
1979 if (nwin < 100)
1980 nfa = nfb - nwin;
1981 }
1982
1983 auto const ia =
1984 std::max(0, static_cast<int>(std::round(nfa / Mode::DF)));
1985 auto const ib = static_cast<int>(std::round(nfb / Mode::DF));
1986
1987 // Convert average spectrum from power to db scale and compute
1988 // baseline from it; baseline replaces average spectrum.
1989
1990 baselinejs8(ia, ib);
1991
1992 // Compute and populate the sync index.
1993
1994 sync.clear();
1995
1996 for (int i = ia; i <= ib; ++i) {
1997 float max_value = -std::numeric_limits<float>::infinity();
1998 int max_index = -Mode::JZ;
1999
2000 for (int j = -Mode::JZ; j <= Mode::JZ; ++j) {
2001 std::array<std::array<float, 3>, 2> t{};
2002
2003 for (int p = 0; p < 3; ++p) {
2004 for (int n = 0; n < 7; ++n) {
2005 int const offset =
2006 j + Mode::JSTRT + NSSY * n + p * 36 * NSSY;
2007
2008 if (offset >= 0 && offset < Mode::NHSYM) {
2009 // Accumulate Costas pattern contributions.
2010
2011 t[0][p] += s[i + NFOS * Costas[p][n]][offset];
2012
2013 // Accumulate sum over all frequencies for this
2014 // block.
2015
2016 for (int freq = 0; freq < 7; ++freq) {
2017 t[1][p] += s[i + NFOS * freq][offset];
2018 }
2019 }
2020 }
2021 }
2022
2023 // Compute sync metric over the index range. We are at the
2024 // moment maintaining the Fortran summation methodology for
2025 // compatibility testing; there are more efficient ways to do
2026 // this, but IEEE 754 addition is a touchy thing, so we'll need
2027 // to ensure that any changes don't negatively affect result
2028 // precision.
2029
2030 auto const compute_sync = [&t](int start, int end) {
2031 float tx = 0.0f;
2032 float t0 = 0.0f;
2033
2034 for (int i = start; i <= end; ++i) {
2035 tx += t[0][i];
2036 t0 += t[1][i];
2037 }
2038
2039 return tx / ((t0 - tx) / 6.0f);
2040 };
2041
2042 if (auto const sync_value =
2043 std::max({compute_sync(0, 2), compute_sync(0, 1),
2044 compute_sync(1, 2)});
2045 sync_value > max_value) {
2046 max_value = sync_value;
2047 max_index = j;
2048 }
2049 }
2050
2051 sync.emplace(Mode::DF * i, Mode::TSTEP * (max_index + 0.5f),
2052 max_value);
2053 }
2054
2055 // If we found nothing, we're done here.
2056
2057 if (sync.empty())
2058 return {};
2059
2060 // Access the sync indices.
2061
2062 auto &freqIndex = sync.get<Tag::Freq>();
2063 auto &rankIndex = sync.get<Tag::Rank>();
2064 auto &syncIndex = sync.get<Tag::Sync>();
2065
2066 // Normalize to the 40th percentile using the frequency index,
2067 // which is stable under sync value mutation. One thing to note
2068 // here is that the Fortran version didn't seem to reliably
2069 // calculate the 40th percentile rank; sometimes high, other
2070 // times low, infrequently actually the 40th percentile value.
2071 // This method should be perfectly accurate in all cases.
2072
2073 auto const normalize =
2074 [sync = rankIndex.nth(rankIndex.size() * 4 / 10)->sync](
2075 Sync &entry) { entry.sync /= sync; };
2076
2077 for (auto it = freqIndex.begin(); it != freqIndex.end(); ++it) {
2078 freqIndex.modify(it, normalize);
2079 }
2080
2081 // Extract candidates.
2082
2083 std::vector<Sync> candidates;
2084
2085 for (auto it = syncIndex.begin();
2086 it != syncIndex.end() && candidates.size() < NMAXCAND;
2087 it = syncIndex.begin()) {
2088 // Stop iteration if below threshold or invalid; as the
2089 // index is sorted by sync, any subsequent entries will
2090 // also be below the threshold or invalid.
2091
2092 if (it->sync < ASYNCMIN || std::isnan(it->sync))
2093 break;
2094
2095 // Good value, relatively strong; save the candidate.
2096
2097 candidates.push_back(*it);
2098
2099 // Remove the candidate and any near-duplicates based
2100 // on frequency. This invalidates `it`, so we reset it
2101 // to the index begin in the loop increment condition.
2102
2103 freqIndex.erase(freqIndex.lower_bound(it->freq - Mode::AZ),
2104 freqIndex.upper_bound(it->freq + Mode::AZ));
2105 }
2106
2107 return candidates;
2108 }
2109
2110 // Returns the total synchronization power, which is a measure of how well
2111 // the signal aligns with the Costas sequence after accounting for the
2112 // frequency adjustment. Used to identify the best alignment for further
2113 // decoding.
2114
2115 float syncjs8d(int const i0, float const delf) {
2116 constexpr float BASE_DPHI = TAU * (1.0f / (12000.0f / Mode::NDOWN));
2117
2118 // If delta frequency is non-zero, compute the frequency
2119 // adjustment array, otherwise, use what'll be an identity
2120 // transfrom when multiplied.
2121
2122 std::array<std::complex<float>, Mode::NDOWNSPS> freqAdjust;
2123
2124 if (delf != 0.0f) {
2125 float const dphi = BASE_DPHI * delf;
2126 float phi = 0.0f;
2127
2128 // std::fmod() is almost like Fortran's mod(), but not quite;
2129 // Since delf can be negative, we must ensure that phi stays
2130 // within [0, TAU), which Fortran's mod() handles by itself.
2131
2132 for (int i = 0; i < Mode::NDOWNSPS; ++i) {
2133 freqAdjust[i] = std::polar(1.0f, phi);
2134 if (phi = std::fmod(phi + dphi, TAU); phi < 0.0f) {
2135 phi += TAU;
2136 }
2137 }
2138 } else {
2139 freqAdjust.fill(std::complex<float>{1.0f, 0.0f});
2140 }
2141
2142 // Compute sync power by looping over the Costas indices for
2143 // each of the 3 Costas blocks, accumulating as we go.
2144
2145 float sync = 0.0f;
2146
2147 for (int i = 0; i < 3; ++i) {
2148 for (int j = 0; j < 7; ++j) {
2149 if (auto const offset =
2150 36 * i * Mode::NDOWNSPS + i0 + j * Mode::NDOWNSPS;
2151 offset >= 0 && offset + Mode::NDOWNSPS <= Mode::NP2) {
2152 sync += std::norm(std::transform_reduce(
2153 freqAdjust.begin(), // Range start
2154 freqAdjust.end(), // Range end
2155 cd0.begin() + offset, // Data start
2156 std::complex<float>{}, // Initial reduction value
2157 std::plus<>{}, // Reduction by accumulation
2158 [&](auto const &fa, // Conjugate and multiply
2159 auto const &cd) {
2160 return cd *
2161 std::conj(
2162 fa * csyncs[i][j][&fa - &freqAdjust[0]]);
2163 }));
2164 }
2165 }
2166 }
2167
2168 return sync;
2169 }
2170
2171 // Generate a reference signal, based on the provided tone sequence and
2172 // base frequency. The output is a vector of complex values representing
2173 // the signal in the time domain.
2174
2175 std::vector<std::complex<float>>
2176 genjs8refsig(std::array<int, NN> const &itone, float const f0) {
2177 // Precompute the base frequency contribution; full circle in
2178 // radians, multipled by the base frequency, multiplied by the
2179 // sampling interval, i.e., the time step between samples, which
2180 // results in the base frequency phase increment. Start the
2181 // phase accumulator off at zero.
2182
2183 float const BFPI = TAU * f0 * (1.0f / 12000.0f);
2184 auto phi = 0.0f;
2185
2186 std::vector<std::complex<float>> cref;
2187 cref.reserve(NN * Mode::NSPS);
2188
2189 for (int i = 0; i < NN; ++i) {
2190 // Compute phase increment for the tone; frequency offset is
2191 // determined by the tone value.
2192
2193 float const dphi =
2194 BFPI + TAU * static_cast<float>(itone[i]) / Mode::NSPS;
2195
2196 // Iterate over the samples per symbol to generate the time
2197 // domain signal.
2198
2199 for (std::size_t is = 0; is < Mode::NSPS; ++is) {
2200 cref.push_back(std::polar(1.0f, phi));
2201 phi = std::fmod(phi + dphi, TAU);
2202 }
2203 }
2204
2205 return cref;
2206 }
2207
2208 // Subtract a JS8 signal
2209 //
2210 // Measured signal : dd(t) = a(t)cos(2*pi*f0*t+theta(t))
2211 // Reference signal : cref(t) = exp( j*(2*pi*f0*t+phi(t)) )
2212 // Complex amp : cfilt(t) = LPF[ dd(t)*CONJG(cref(t)) ]
2213 // Subtract : dd(t) = dd(t) - 2*REAL{cref*cfilt}
2214 //
2215 // Important to note that dt can be negative here.
2216
2217 void subtractjs8(std::vector<std::complex<float>> const &cref,
2218 float const dt) {
2219 auto const nstart = static_cast<int>(dt * 12000.0f);
2220 std::size_t const cref_start =
2221 (nstart < 0) ? static_cast<std::size_t>(-nstart) : 0;
2222 std::size_t const dd_start =
2223 (nstart > 0) ? static_cast<std::size_t>(nstart) : 0;
2224 auto const size =
2225 std::min(cref.size() - cref_start, dd.size() - dd_start);
2226
2227 // Populate complex filter with the conjugate of the reference signal.
2228
2229 for (std::size_t i = 0; i < size; ++i) {
2230 cfilt[i] = dd[dd_start + i] * std::conj(cref[cref_start + i]);
2231 }
2232
2233 // Zero-fill the remainder, if any.
2234
2235 std::fill(cfilt.begin() + size, cfilt.end(), ZERO);
2236
2237 // FFT to the frequency domain.
2238
2239 fftwf_execute(plans[Plan::CF]);
2240
2241 // Apply the filter in the frequency domain.
2242
2243 std::transform(cfilt.begin(), cfilt.end(), filter.begin(),
2244 cfilt.begin(), std::multiplies<>());
2245
2246 // Inverse FFT to return to the time domain.
2247
2248 fftwf_execute(plans[Plan::CB]);
2249
2250 // Subtract the reconstructed signal.
2251
2252 for (std::size_t i = 0; i < size; ++i) {
2253 dd[dd_start + i] -=
2254 2.0f * std::real(cfilt[i] * cref[cref_start + i]);
2255 }
2256 }
2257
2258 public:
2259 // Constructor
2260
2261 DecodeMode() {
2262 m_enableFreqTracking =
2263 std::getenv("JS8_DISABLE_FREQ_TRACKING") == nullptr;
2264 m_enableTimingTracking =
2265 std::getenv("JS8_DISABLE_TIMING_TRACKING") == nullptr;
2266
2267 // Intialize the Nuttal window. In theory, we can do this as a
2268 // constexpr function at compile time, but doing so yield results
2269 // slightly different than the Fortran version did, so for sanity
2270 // while testing, we'll opt for consistency. IEEE 754 is always a
2271 // bit brittle.
2272
2273 constexpr float a0 = 0.3635819f;
2274 constexpr float a1 = -0.4891775f;
2275 constexpr float a2 = 0.1365995f;
2276 constexpr float a3 = -0.0106411f;
2277
2278 // Computed Pi constant to match the Fortran version; we could
2279 // probably use std::numbers::pi_v<float> here, but for the
2280 // moment, matching Fortran exactly.
2281
2282 float const pi = 4.0f * std::atan(1.0f);
2283 float sum = 0.0f;
2284
2285 for (std::size_t i = 0; i < nuttal.size(); ++i) {
2286 // Naive summation here will exhibit substantial precision loss
2287 // relative to the Fortran version; we use Kahan summation to
2288 // compensate, which should yield results identical to Fortran.
2289
2290 KahanSum value = a0;
2291
2292 value += a1 * std::cos(2 * pi * i / nuttal.size());
2293 value += a2 * std::cos(4 * pi * i / nuttal.size());
2294 value += a3 * std::cos(6 * pi * i / nuttal.size());
2295
2296 nuttal[i] = value;
2297 sum += value;
2298 }
2299
2300 // Normalize the Nuttal window.
2301
2302 for (auto &value : nuttal)
2303 value = value / sum * nuttal.size() / 300.0f;
2304
2305 // Initialize Costas waveforms.
2306
2307 for (int i = 0; i < 7; ++i) {
2308 float const dphia = TAU * Costas[0][i] / Mode::NDOWNSPS;
2309 float const dphib = TAU * Costas[1][i] / Mode::NDOWNSPS;
2310 float const dphic = TAU * Costas[2][i] / Mode::NDOWNSPS;
2311
2312 float phia = 0.0f;
2313 float phib = 0.0f;
2314 float phic = 0.0f;
2315
2316 for (int j = 0; j < Mode::NDOWNSPS; ++j) {
2317 csyncs[0][i][j] = std::polar(1.0f, phia);
2318 csyncs[1][i][j] = std::polar(1.0f, phib);
2319 csyncs[2][i][j] = std::polar(1.0f, phic);
2320
2321 phia = std::fmod(phia + dphia, TAU);
2322 phib = std::fmod(phib + dphib, TAU);
2323 phic = std::fmod(phic + dphic, TAU);
2324 }
2325 }
2326
2327 // Compute a Hann-like window directly into the real part of the
2328 // first NFILT + 1 elements in the filter, accumulating the sum
2329 // as we go.
2330
2331 sum = 0.0f;
2332
2333 for (int j = -NFILT / 2; j <= NFILT / 2; ++j) {
2334 int const index = j + NFILT / 2;
2335 float const value = std::pow(std::cos(pi * j / NFILT), 2);
2336
2337 filter[index].real(value);
2338 sum += value;
2339 }
2340
2341 // Now that we've got the sum, create actual complex numbers using
2342 // the normalized real values that we just populated and zero the
2343 // rest of the filter.
2344
2345 std::fill(std::transform(filter.begin(), filter.begin() + NFILT + 1,
2346 filter.begin(),
2347 [sum](auto const value) {
2348 return std::complex<float>(
2349 value.real() / sum, 0.0f);
2350 }),
2351 filter.end(), ZERO);
2352
2353 // Shift to position the window.
2354
2355 std::rotate(filter.begin(), filter.begin() + NFILT / 2,
2356 filter.begin() + NFILT + 1);
2357
2358 // Transform the filter into the frequency domain.
2359
2360 fftwf_plan fftw_plan;
2361 {
2362 std::lock_guard<std::mutex> lock(fftw_mutex);
2363
2364 fftw_plan = fftwf_plan_dft_1d(
2365 Mode::NMAX, reinterpret_cast<fftwf_complex *>(filter.data()),
2366 reinterpret_cast<fftwf_complex *>(filter.data()), FFTW_FORWARD,
2367 FFTW_ESTIMATE_PATIENT);
2368
2369 if (!fftw_plan) {
2370 throw std::runtime_error("Failed to create FFT plan");
2371 }
2372 }
2373
2374 fftwf_execute(fftw_plan);
2375
2376 {
2377 std::lock_guard<std::mutex> lock(fftw_mutex);
2378 fftwf_destroy_plan(fftw_plan);
2379 }
2380
2381 // Normalize the frequency domain representation.
2382
2383 std::transform(filter.begin(), filter.end(), filter.begin(),
2384 [factor = 1.0f / Mode::NMAX](auto value) {
2385 return value * factor;
2386 });
2387
2388 // The rest of our FFT plans are always the same size and operate on the
2389 // same data, so we can reuse them as long as we're alive.
2390
2391 std::lock_guard<std::mutex> lock(fftw_mutex);
2392
2393 plans[Plan::DS] = fftwf_plan_dft_1d(
2394 Mode::NDFFT2, reinterpret_cast<fftwf_complex *>(cd0.data()),
2395 reinterpret_cast<fftwf_complex *>(cd0.data()), FFTW_BACKWARD,
2396 FFTW_ESTIMATE_PATIENT);
2397
2398 plans[Plan::BB] = fftwf_plan_dft_r2c_1d(
2399 Mode::NDFFT1, reinterpret_cast<float *>(ds_cx.data()),
2400 reinterpret_cast<fftwf_complex *>(ds_cx.data()),
2401 FFTW_ESTIMATE_PATIENT);
2402
2403 plans[Plan::CF] = fftwf_plan_dft_1d(
2404 Mode::NMAX, reinterpret_cast<fftwf_complex *>(cfilt.data()),
2405 reinterpret_cast<fftwf_complex *>(cfilt.data()), FFTW_FORWARD,
2406 FFTW_ESTIMATE_PATIENT);
2407
2408 plans[Plan::CB] = fftwf_plan_dft_1d(
2409 Mode::NMAX, reinterpret_cast<fftwf_complex *>(cfilt.data()),
2410 reinterpret_cast<fftwf_complex *>(cfilt.data()), FFTW_BACKWARD,
2411 FFTW_ESTIMATE_PATIENT);
2412
2413 plans[Plan::SD] = fftwf_plan_dft_r2c_1d(
2414 Mode::NFFT1, reinterpret_cast<float *>(sd.data()),
2415 reinterpret_cast<fftwf_complex *>(sd.data()),
2416 FFTW_ESTIMATE_PATIENT);
2417
2418 plans[Plan::CS] = fftwf_plan_dft_1d(
2419 Mode::NDOWNSPS, reinterpret_cast<fftwf_complex *>(csymb.data()),
2420 reinterpret_cast<fftwf_complex *>(csymb.data()), FFTW_FORWARD,
2421 FFTW_ESTIMATE_PATIENT);
2422
2423 for (auto plan : plans) {
2424 if (!plan)
2425 throw std::runtime_error("Failed to create FFT plan");
2426 }
2427 }
2428
2429 // Decode entry point.
2430
2431 std::size_t operator()(struct dec_data const &data, int const kpos,
2432 int const ksz, JS8::Event::Emitter emitEvent) {
2433 // Copy the relevant frames for decoding
2434
2435 auto const pos = std::max(0, kpos);
2436 auto const sz = std::max(0, ksz);
2437
2438 assert(sz <= Mode::NMAX);
2439
2440 if (data.params.syncStats)
2441 emitEvent(JS8::Event::SyncStart{pos, sz});
2442
2443 auto const ddCopy = [](auto const begin, auto const end,
2444 auto const to) {
2445 std::transform(begin, end, to, [](auto const value) {
2446 return static_cast<float>(value);
2447 });
2448 };
2449
2450 dd.fill(0.0f);
2451
2452 if ((JS8_RX_SAMPLE_SIZE - pos) < sz) {
2453 // Wrap case; split into two parts.
2454
2455 int const firstsize = JS8_RX_SAMPLE_SIZE - pos;
2456 int const secondsize = sz - firstsize;
2457
2458 ddCopy(std::begin(data.d2) + pos,
2459 std::begin(data.d2) + pos + firstsize, dd.begin());
2460 ddCopy(std::begin(data.d2), std::begin(data.d2) + secondsize,
2461 dd.begin() + firstsize);
2462 } else {
2463 // Non-wrapping case; copy directly.
2464
2465 ddCopy(std::begin(data.d2) + pos, std::begin(data.d2) + pos + sz,
2466 dd.begin());
2467 }
2468
2469 Decode::Map decodes;
2470 auto const ttl = std::chrono::seconds{Mode::NTXDUR * 2};
2471 m_softCombiner.flush(ttl);
2472
2473 for (int ipass = 1; ipass <= 3; ++ipass) {
2474 // Determine if there's anything worth considering in the signal.
2475 // If not, then we can just bail completely; more passes will not
2476 // yield more results. If we do have some candidates, sort them
2477 // by frequency, but put any that are close to nfqso up front.
2478
2479 auto candidates = syncjs8(data.params.nfa, data.params.nfb);
2480
2481 if (candidates.empty())
2482 break;
2483
2484 std::sort(
2485 candidates.begin(), candidates.end(),
2486 [nfqso = data.params.nfqso](auto const &a, auto const &b) {
2487 auto const a_dist = std::abs(a.freq - nfqso);
2488 auto const b_dist = std::abs(b.freq - nfqso);
2489
2490 if (a_dist < 10.0f && b_dist >= 10.0f)
2491 return true;
2492 if (b_dist < 10.0f && a_dist >= 10.0f)
2493 return false;
2494
2495 return std::tie(a_dist, a.freq) < std::tie(b_dist, b.freq);
2496 });
2497
2498 // Recompute the baseband signal; subtraction during the last
2499 // pass might have changed the landscape.
2500
2501 computeBasebandFFT();
2502
2503 bool const subtract = ipass < 3;
2504 bool improved = false;
2505
2506 for (auto [f1, xdt, sync] : candidates) {
2507 float xsnr = 0.0f;
2508 int nharderrors = -1;
2509
2510 if (auto decode = js8dec(data.params.syncStats, subtract, f1,
2511 xdt, nharderrors, xsnr, emitEvent)) {
2512 // We don't need to be emitting duplicate events for
2513 // something that's effectively the same SNR as a previous
2514 // event.
2515
2516 auto const snr = static_cast<int>(std::round(xsnr));
2517
2518 // If this decode is new, or it's a duplicate with a better
2519 // SNR than what we had before, then our situation has
2520 // improved and we must announce that we've had some
2521 // success.
2522
2523 if (auto [it, inserted] =
2524 decodes.try_emplace(std::move(*decode), snr);
2525 inserted || it->second < snr) {
2526 improved = true;
2527
2528 // Update the SNR if this is an improved decode.
2529
2530 if (!inserted)
2531 it->second = snr;
2532
2533 // Emit decoded events on new or improved decodes.
2534
2535 emitEvent(JS8::Event::Decoded{
2536 data.params.nutc, snr, xdt - Mode::ASTART, f1,
2537 it->first.data, it->first.type,
2538 1.0f - nharderrors / 60.0f, Mode::NSUBMODE});
2539 }
2540 }
2541 }
2542
2543 // If nothing from this pass improved our situation, there's no
2544 // point in trying any remaining passes.
2545
2546 if (!improved)
2547 break;
2548 }
2549
2550 // Let the caller know how many unique decodes we discovered, if any.
2551
2552 return decodes.size();
2553 }
2554};
2555
2556// Explicit template class instantiations; avoids compiler complaints
2557// about unused variables.
2558
2559template class DecodeMode<ModeA>;
2560template class DecodeMode<ModeB>;
2561template class DecodeMode<ModeC>;
2562template class DecodeMode<ModeE>;
2563template class DecodeMode<ModeI>;
2564} // namespace
2565} // namespace
2566
2567/******************************************************************************/
2568// Worker
2569/******************************************************************************/
2570
2571namespace JS8 {
2572class Worker : public QObject {
2573 Q_OBJECT
2574
2575 // Initialization of the decoders, in that they're heavy with
2576 // FFT plan creations, is non-trivial, so a handle-body class
2577 // to avoid initializing them on the main thread.
2578
2579 class Impl {
2580 // To avoid data races, decode data is referenced here but is
2581 // actually located in the Worker that instantiates us, as it
2582 // must be possible to copy data for us before we're ready to
2583 // process it.
2584
2585 struct dec_data &m_data;
2586
2587 // Mode-specific decode strategy; we'll instantiate one of
2588 // these for each of the 5 modes; this class is an aggregate
2589 // of the 5 modes.
2590
2591 struct DecodeEntry {
2592 std::variant<DecodeMode<ModeA>, DecodeMode<ModeB>,
2593 DecodeMode<ModeC>, DecodeMode<ModeE>,
2594 DecodeMode<ModeI>>
2595 decode;
2596 int mode;
2597 int &kpos;
2598 int &ksz;
2599
2600 template <typename DecodeModeType>
2601 DecodeEntry(std::in_place_type_t<DecodeModeType>, int mode,
2602 int &kpos, int &ksz)
2603 : decode(std::in_place_type<DecodeModeType>), mode(mode),
2604 kpos(kpos), ksz(ksz) {}
2605 };
2606
2607 // Since a strategy can be neither moved nor copied, we must
2608 // instantiate them in-place. Note that with the advent of the
2609 // multi-decoder, mode identifiers became a bitset instead of
2610 // integral values. The order defined here is the order that
2611 // the decode loop will run in; we're matching the Fortran
2612 // version here in terms of faster modes first.
2613
2614 template <typename ModeType>
2615 DecodeEntry makeDecodeEntry(int shift, int &kpos, int &ksz) {
2616 return DecodeEntry(std::in_place_type<DecodeMode<ModeType>>,
2617 1 << shift, kpos, ksz);
2618 }
2619
2620 std::array<DecodeEntry, 5> m_decodes = {
2621 {makeDecodeEntry<ModeI>(4, m_data.params.kposI, m_data.params.kszI),
2622 makeDecodeEntry<ModeE>(3, m_data.params.kposE, m_data.params.kszE),
2623 makeDecodeEntry<ModeC>(2, m_data.params.kposC, m_data.params.kszC),
2624 makeDecodeEntry<ModeB>(1, m_data.params.kposB, m_data.params.kszB),
2625 makeDecodeEntry<ModeA>(0, m_data.params.kposA,
2626 m_data.params.kszA)}};
2627
2628 public:
2629 // Constructor
2630
2631 explicit Impl(struct dec_data &data) : m_data(data) {}
2632
2633 // Execute a decoding pass, using the supplied event emitter to
2634 // emit events as they occur.
2635
2636 void operator()(::JS8::Event::Emitter emitEvent) {
2637 // The multi-decoder can provide data for multiple modes at
2638 // the same time; specific decodes to be performed for this
2639 // pass are in the `nsubmodes` bitset.
2640
2641 auto const set = m_data.params.nsubmodes;
2642 std::size_t sum = 0;
2643
2644 // Let any interested parties know that we've started a run
2645 // for the set of modes requested.
2646
2647 emitEvent(::JS8::Event::DecodeStarted{set});
2648
2649 // Iterate through all the modes we're aware of, performing
2650 // a mode-specific decode pass if the mode is scheduled for
2651 // decoding during this pass.
2652
2653 for (auto &entry : m_decodes) {
2654 if ((set & entry.mode) == entry.mode) {
2655 std::visit(
2656 [&](auto &&decode) {
2657 sum += decode(m_data, entry.kpos, entry.ksz,
2658 emitEvent);
2659 },
2660 entry.decode);
2661 }
2662 }
2663
2664 // Let any interested parties know the total number of decodes
2665 // performed during this run.
2666
2667 emitEvent(::JS8::Event::DecodeFinished{sum});
2668 }
2669 };
2670
2671 // Data members
2672
2673 QSemaphore *m_semaphore;
2674 std::atomic<bool> m_quit = false;
2675 struct dec_data m_data;
2676
2677 public:
2678 // Constructor
2679
2680 explicit Worker(QSemaphore *semaphore, QObject *parent = nullptr)
2681 : QObject(parent), m_semaphore(semaphore) {}
2682
2683 // Used to inform the worker that it's time to go; the next
2684 // time it wakes up due to the semaphore being released, it
2685 // will exit the runloop.
2686
2687 void stop() { m_quit = true; }
2688
2689 // Called by the owning Decoder to refresh the copy of the
2690 // decode data that the Worker implementation references.
2691
2692 void copy() { m_data = dec_data; };
2693
2694 signals:
2695
2696 // Signal used to indicate that something of interest has
2697 // occurred during a decoding pass.
2698
2699 void decodeEvent(::JS8::Event::Variant const &);
2700
2701 public slots:
2702
2703 // Runloop for the thread that the worker is scheduled on; this
2704 // is started by the Decoder when it's informed that the thread
2705 // has started. Performs decoding runs each time the semaphore
2706 // is released, until it's informed that it should quit.
2707
2708 void run() {
2709 // Our thread has started, and we're now running on it, so
2710 // we're good to now allocate our implementation; we didn't
2711 // want that to happen on the main thread, as the FFT plans
2712 // can take a while. We only need the implementation while
2713 // we're running.
2714
2715 std::unique_ptr<Impl> impl = std::make_unique<Impl>(m_data);
2716
2717 // Wait until there's something that requires our attention,
2718 // which is going to either be needing to quit or needing to
2719 // perform a decoding pass.
2720
2721 while (true) {
2722 m_semaphore->acquire();
2723
2724 if (m_quit)
2725 break;
2726
2727 (*impl)([this](::JS8::Event::Variant const &event) {
2728 emit decodeEvent(event);
2729 });
2730 }
2731 }
2732};
2733} // namespace JS8
2734
2735/******************************************************************************/
2736// Public Interface - Decoding
2737/******************************************************************************/
2738
2739#include "JS8.moc"
2740
2741JS8::Decoder::Decoder(QObject *parent)
2742 : QObject(parent), m_semaphore(0), m_worker(new JS8::Worker(&m_semaphore)) {
2743 m_worker->moveToThread(&m_thread);
2744
2745 connect(&m_thread, &QThread::started, m_worker, &JS8::Worker::run);
2746 connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
2747 connect(m_worker, &JS8::Worker::decodeEvent, this, &Decoder::decodeEvent);
2748}
2749
2750void JS8::Decoder::start(QThread::Priority priority) {
2751 m_thread.start(priority);
2752}
2753
2754void JS8::Decoder::quit() {
2755 m_worker->stop();
2756 m_semaphore.release();
2757 m_thread.quit();
2758 m_thread.wait();
2759}
2760
2761void JS8::Decoder::decode() {
2762 m_worker->copy();
2763 m_semaphore.release();
2764}
2765
2766/******************************************************************************/
2767// Public Interface - Encoding
2768/******************************************************************************/
2769
2770namespace JS8 {
2771// Port of the Fortran `genjs8` subroutine; from the 12 bytes of `message`,
2772// construct an 87-bit JS8 message and encode it into tones. Costas array
2773// to use supplied by the caller, as is the type of message, indicated by
2774// the lower 3 bits of `type`.
2775
2776void encode(int const type, Costas::Array const &costas,
2777 const char *const message, int *const tones) {
2778 // Our initial goal here is an 87-bit message, for which a std::bitset
2779 // would be the obvious choice, but we've got to compute a checksum of
2780 // the first 75 bits; thus, an array instead.
2781 //
2782 // Message structure:
2783 //
2784 // +----------+----------+----------+
2785 // | | | 72 bits | 12 6-bit words
2786 // | | +==========+
2787 // | | 87 bits | 3 bits | Frame type
2788 // | 11 bytes | +==========+
2789 // | | | 12 bits | 12-bit BE checksum
2790 // | |----------+==========+
2791 // | | 1 bit | 1 bit | Leftover bit in array
2792 // +----------+----------+==========+
2793
2794 std::array<std::uint8_t, 11> bytes = {};
2795
2796 // Convert the 12 characters we've been handed to 6-bit words and pack
2797 // them into the byte array, 4 characters, 24 bits at a time, into the
2798 // 9 bytes [0,8], 72 bits total. Throws if handed an invalid character.
2799
2800 for (int i = 0, j = 0; i < 12; i += 4, j += 3) {
2801 std::uint32_t words = (alphabetWord(message[i]) << 18) |
2802 (alphabetWord(message[i + 1]) << 12) |
2803 (alphabetWord(message[i + 2]) << 6) |
2804 alphabetWord(message[i + 3]);
2805
2806 bytes[j] = words >> 16;
2807 bytes[j + 1] = words >> 8;
2808 bytes[j + 2] = words;
2809 }
2810
2811 // The bottom 3 bits of type are the frame type; these go into the
2812 // next 3 bits in the byte array, i.e., the first 3 bits of byte 9,
2813 // after which we'll be at 75 bits in total.
2814
2815 bytes[9] = (type & 0b111) << 5;
2816
2817 // We now need to compute the augmented CRC-12 of the complete
2818 // byte array, including the trailing zero bits that we've not
2819 // set yet.
2820
2821 auto const crc = CRC12(bytes);
2822
2823 // That CRC needs to occupy the next 12 bits of the array, i.e.,
2824 // the final 5 bits of byte 9, and the first 7 bits of byte 10.
2825
2826 bytes[9] |= (crc >> 7) & 0x1F;
2827 bytes[10] = (crc & 0x7F) << 1;
2828
2829 // That's it for our 87-bit message; we're now going to turn it
2830 // into two blocks of 29 3-bit words, which will in turn become
2831 // tones, the first block being parity for the second, bracketed
2832 // by the Costas arrays.
2833 //
2834 // Output structure:
2835 //
2836 // +----------+----------+
2837 // | | 7 bytes | Costas array A
2838 // | +==========+
2839 // | | 29 bytes | Parity data
2840 // | +==========+
2841 // | 79 bytes | 7 bytes | Costas array B
2842 // | +==========+
2843 // | | 29 bytes | Output data
2844 // | +==========+
2845 // | | 7 bytes | Costas array C
2846 // +----------+==========+
2847
2848 auto costasData = tones;
2849 auto parityData = tones + 7;
2850 auto outputData = tones + 43;
2851
2852 // Output the 3 Costas arrays at offsets 0, 36, and 72.
2853
2854 for (auto const &array : costas) {
2855 std::copy(array.begin(), array.end(), costasData);
2856 costasData += 36;
2857 }
2858
2859 // Our 87 bits are going to be morphed into two sets of 29 3-bit
2860 // words, the first one parity for the second; we're going to do
2861 // this in parallel.
2862
2863 std::size_t outputBits = 0;
2864 std::size_t outputByte = 0;
2865 std::uint8_t outputMask = 0x80;
2866 std::uint8_t outputWord = 0;
2867 std::uint8_t parityWord = 0;
2868
2869 for (std::size_t i = 0; i < 87; ++i) {
2870 // Compute parity for the current bit; inputs for parity computation
2871 // are the corresponding parity matrix row and each bit in the message;
2872 // the parity matrix row, referenced by `i`, contains 87 boolean values.
2873 // Each `true` value defines a message bit that must be summed, modulo
2874 // 2, to produce the parity check bit for the bit we're working on now.
2875 //
2876 // In short, if the parity matrix bit `(i, j)` and the message bit `j`
2877 // are both set, then we add 1 to the parity bits accumulator. If, after
2878 // processing all message bits the accumulated result is odd, then the
2879 // parity bit should be set for the current bit.
2880
2881 std::size_t parityBits = 0;
2882 std::size_t parityByte = 0;
2883 std::uint8_t parityMask = 0x80;
2884
2885 for (std::size_t j = 0; j < 87; ++j) {
2886 parityBits += parity(i, j) && (bytes[parityByte] & parityMask);
2887 parityMask =
2888 (parityMask == 1) ? (++parityByte, 0x80) : (parityMask >> 1);
2889 }
2890
2891 // Accumulate the parity and output bits; this is the point at which
2892 // we perform the modulo 2 operation on the summed parity bits.
2893
2894 parityWord = (parityWord << 1) | (parityBits & 1);
2895 outputWord =
2896 (outputWord << 1) | ((bytes[outputByte] & outputMask) != 0);
2897 outputMask =
2898 (outputMask == 1) ? (++outputByte, 0x80) : (outputMask >> 1);
2899
2900 // If we're at a 3-bit boundary, output the words and reset.
2901
2902 if (++outputBits == 3) {
2903 *parityData++ = parityWord;
2904 *outputData++ = outputWord;
2905 parityWord = 0;
2906 outputWord = 0;
2907 outputBits = 0;
2908 }
2909 }
2910}
2911} // namespace JS8
2912
2913/******************************************************************************/
QMultiMap< quint32, QString > candidates(QString word, bool includeTwoEdits)
Generate candidate words that are one or two edit distances away.
Definition JSC_checker.cpp:239
Definition Decoder.h:15
double currentHz() const noexcept
Get the current frequency estimate in Hz.
Definition FrequencyTracker.cpp:56
void reset(double initial_hz, double sample_rate_hz, double alpha=0.15, double max_step_hz=0.3, double max_error_hz=5.0)
Reset the FrequencyTracker with initial parameters.
Definition FrequencyTracker.cpp:24
double averageStepHz() const noexcept
Get the average step size in Hz.
Definition FrequencyTracker.cpp:63
void update(double residual_hz, double weight=1.0)
Update the FrequencyTracker with a new residual frequency measurement.
Definition FrequencyTracker.cpp:93
bool enabled() const noexcept
Check if the FrequencyTracker is enabled.
Definition FrequencyTracker.cpp:49
void disable()
Disable the FrequencyTracker.
Definition FrequencyTracker.cpp:41
void apply(std::complex< float > *data, int count) const
Apply frequency correction to the provided data.
Definition FrequencyTracker.cpp:73
void reset(double initial_samples, double alpha=0.15, double max_step=0.35, double max_total_error=2.0)
Tracks residual timing (sample) offset between the symbol clock and the signal.
Definition FrequencyTracker.cpp:118
double averageStepSamples() const noexcept
Get the average step size in samples.
Definition FrequencyTracker.cpp:155
double currentSamples() const noexcept
Get the current timing estimate in samples.
Definition FrequencyTracker.cpp:148
void disable()
Disable the TimingTracker.
Definition FrequencyTracker.cpp:133
bool enabled() const noexcept
Check if the TimingTracker is enabled.
Definition FrequencyTracker.cpp:141
void update(double residual_samples, double weight=1.0)
Update the TimingTracker with a new residual timing measurement.
Definition FrequencyTracker.cpp:165
Costas::Type costas(int const submode)
Get the Costas array type of the submode.
Definition JS8Submode.cpp:258
Definition JS8.h:74
Definition JS8.h:43
Definition commons.h:50