forked from JS8Call-improved/JS8Call-improved
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJS8.cpp
More file actions
2800 lines (2264 loc) · 111 KB
/
JS8.cpp
File metadata and controls
2800 lines (2264 loc) · 111 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* (C) 2025 Allan Bazinet <w6baz@arrl.net> - All Rights Reserved
**/
#include "JS8.hpp"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <complex>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <memory>
#include <mutex>
#include <numbers>
#include <numeric>
#include <stdexcept>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <boost/crc.hpp>
#include <boost/math/ccmath/round.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/key.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/ranked_index.hpp>
#include <fftw3.h>
#include <vendor/Eigen/Dense>
#include <QDebug>
#include "commons.h"
// A C++ conversion of the Fortran JS8 encoding and decoder function.
// Some notes on the conversion:
//
// 1. Names of variables and functions as much as possible match those
// of the Fortran routines, for ease in cross-referencing during the
// debug comparison phase of testing. You don't have to like them; I
// don't like them either, frankly, but it's the reasonable approach
// to the problem as of this writing; we can make 'em pretty later.
//
// 2. The BP decoder should be a faithful reproduction of the Fortran
// version, albeit modified for the column-major vs. row-major
// differences between the two languages.
//
// 3. The OSD decoder is no longer used, and the depth is now fixed at
// 2, instead of being variable 1 to 4.
//
// 4. The Fortran version didn't compute the 40% rank consistently in
// syncjs8(); this version does. It wasn't typically off by much, but
// it was reliably not going to be at 40%. Hopefully, this change will
// result in more predictable first-pass candidate selection.
//
// 5. The Fortran version was very subject to Runge's phenomenon when
// computing the baseline in baselinejs8(), and was using a ton of
// data points below the 10% threshold for the polynomial determination.
// Neither of these seemed to be helpful, so in contrast we're using
// a number of Chebyshev nodes proportional to the desired polynomial
// terms.
//
// 6. The Fortran version normalized `s1` by dividing by the median in
// js8dec(), but did so in a naive manner, not checking for a median
// of zero. Testing indicates that the normalization does not appear
// to contribute to decoder yield, so it's been removed.
//
// 7. Translating array indices from the world of Fortran to that of C++
// is no one's fun task. If you see things that aren't behaving as
// expected, look at the Fortran code and compare the array indexing;
// would not be surprised in the least to have off-by-one errors here.
/******************************************************************************/
// Compilation Utilities
/******************************************************************************/
namespace
{
// Full-range cosine function using symmetries of cos(x). std::cos
// isn't constexpr until C++20, and we're targeting C++17 at the
// moment. We only use this function during compilation; std::cos
// is the better choice at runtime. Once we move to requiring a
// C++20 compiler, we can just use std::cos.
constexpr auto
cos(double x)
{
constexpr auto RAD_360 = std::numbers::pi * 2;
constexpr auto RAD_180 = std::numbers::pi;
constexpr auto RAD_90 = std::numbers::pi / 2;
// Polynomial approximation of cos(x) for x in [0, RAD_90],
// Accuracy here in theory is 1e-18, but double precision
// itself is only 1-e16, so within the domain of doubles,
// this should be extremely accurate.
constexpr auto cos = [](double const x)
{
constexpr std::array coefficients =
{
1.0, // Coefficient for x^0
-0.49999999999999994, // Coefficient for x^2
0.041666666666666664, // Coefficient for x^4
-0.001388888888888889, // Coefficient for x^6
0.000024801587301587, // Coefficient for x^8
-0.00000027557319223986, // Coefficient for x^10
0.00000000208767569878681, // Coefficient for x^12
-0.00000000001147074513875176, // Coefficient for x^14
0.0000000000000477947733238733 // Coefficient for x^16
};
auto const x2 = x * x;
auto const x4 = x2 * x2;
auto const x6 = x4 * x2;
auto const x8 = x4 * x4;
auto const x10 = x8 * x2;
auto const x12 = x8 * x4;
auto const x14 = x12 * x2;
auto const x16 = x8 * x8;
return coefficients[0]
+ coefficients[1] * x2
+ coefficients[2] * x4
+ coefficients[3] * x6
+ coefficients[4] * x8
+ coefficients[5] * x10
+ coefficients[6] * x12
+ coefficients[7] * x14
+ coefficients[8] * x16;
};
// Reduce x to [0, RAD_360)
x -= static_cast<long long>(x / RAD_360) * RAD_360;
// Map x to [0, RAD_180]
if (x > RAD_180) x = RAD_360 - x;
// Map x to [0, RAD_90] and evaluate the polynomial;
// flip the sign for angles in the second quadrant.
return x > RAD_90 ? -cos(RAD_180 - x) : cos(x);
};
}
/******************************************************************************/
// Constants
/******************************************************************************/
namespace
{
/* COMMON PARAMETERS */
// !Common
//
// parameter (KK=87) !Information bits (75 + CRC12)
// parameter (ND=58) !Data symbols
// parameter (NS=21) !Sync symbols (3 @ Costas 7x7)
// parameter (NN=NS+ND) !Total channel symbols (79)
// parameter (ASYNCMIN=1.5) !Minimum Sync
// parameter (NFSRCH=5) !Search frequency range in Hz (i.e., +/- 2.5 Hz)
// parameter (NMAXCAND=300) !Maximum number of candidate signals
// Parameter Value Description
// KK 87 Number of information bits (75 message bits + 12 CRC bits).
// ND 58 Number of data symbols in the JS8 transmission.
// NS 21 Number of synchronization symbols (3 Costas arrays of size 7).
// NN 79 Total number of channel symbols (NN = NS + ND).
// ASYNCMIN 1.5 Minimum sync value for successful decoding.
// NFSRCH 5 Search frequency range in Hz (±2.5 Hz).
// NMAXCAND 300 Maximum number of candidate signals.
constexpr int N = 174; // Total bits
constexpr int K = 87; // Message bits
constexpr int M = N - K; // Check bits
constexpr int KK = 87; // Information bits (75 + CRC12)
constexpr int ND = 58; // Data symbols
constexpr int NS = 21; // Sync symbols (3 @ Costas 7x7)
constexpr int NN = NS + ND; // Total channel symbols (79)
constexpr float ASYNCMIN = 1.5f; // Minimum sync
constexpr int NFSRCH = 5; // Search frequency range in Hz (i.e., +/- 2.5 Hz)
constexpr std::size_t NMAXCAND = 300; // Maxiumum number of candidate signals
constexpr int NFILT = 1400; // Filter length
constexpr int NROWS = 8;
constexpr int NFOS = 2;
constexpr int NSSY = 4;
constexpr int NP = 3200;
constexpr int NP2 = 2812;
constexpr float TAU = 2.0f * std::numbers::pi_v<float>;
constexpr auto ZERO = std::complex<float>{0.0f, 0.0f};
// Key for the constants that follow:
//
// NSUBMODE - ID of the submode
// NCOSTAS - Which JS8 Costas Arrays to use
// NSPS - Number of samples per second
// NTXDUR - Duration of the transmission in seconds.
// NDOWNSPS - Number of samples per symbol after downsampling.
// NDD - Parameter used in waveform tapering and related calculations. XXX
// JZ - Range of symbol offsets considered during decoding.
// ASTART - Start delay in seconds for decoding.
// BASESUB - XXX
// NMAX - Samples in input wave
// NSTEP - Rough time-sync step size
// NHSYM - Number of symbol spectra (1/4-sym steps)
// NDOW - Downsample factor to 32 samples per symbol
// NQSYMBOL - Downsample factor of a quarter symbol
/* A MODE DECODER */
struct ModeA
{
// Static constants
inline static constexpr int NSUBMODE = 0;
inline static constexpr auto NCOSTAS = JS8::Costas::Type::ORIGINAL;
inline static constexpr int NSPS = JS8A_SYMBOL_SAMPLES;
inline static constexpr int NTXDUR = JS8A_TX_SECONDS;
inline static constexpr int NDOWNSPS = 32;
inline static constexpr int NDD = 100;
inline static constexpr int JZ = 62;
inline static constexpr float ASTART = 0.5f;
inline static constexpr float BASESUB = 40.0f;
// Derived parameters
inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
inline static constexpr int NFFT1 = NSPS * NFOS;
inline static constexpr int NSTEP = NSPS / NSSY;
inline static constexpr int NHSYM = NMAX / NSTEP - 3;
inline static constexpr int NDOWN = NSPS / NDOWNSPS;
inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
inline static constexpr int NDFFT1 = NSPS * NDD;
inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
inline static constexpr int NP2 = NN * NDOWNSPS;
inline static constexpr float TSTEP = NSTEP / 12000.0f;
inline static constexpr int JSTRT = ASTART / TSTEP;
inline static constexpr float DF = 12000.0f / NFFT1;
};
/* B MODE DECODER */
struct ModeB
{
// Static constants
inline static constexpr int NSUBMODE = 1;
inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
inline static constexpr int NSPS = JS8B_SYMBOL_SAMPLES;
inline static constexpr int NTXDUR = JS8B_TX_SECONDS;
inline static constexpr int NDOWNSPS = 20;
inline static constexpr int NDD = 100;
inline static constexpr int JZ = 144;
inline static constexpr float ASTART = 0.2f;
inline static constexpr float BASESUB = 39.0f;
// Derived parameters
inline static constexpr float AZ = (12000.0f / NSPS) * 0.8f;
inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
inline static constexpr int NFFT1 = NSPS * NFOS;
inline static constexpr int NSTEP = NSPS / NSSY;
inline static constexpr int NHSYM = NMAX / NSTEP - 3;
inline static constexpr int NDOWN = NSPS / NDOWNSPS;
inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
inline static constexpr int NDFFT1 = NSPS * NDD;
inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
inline static constexpr int NP2 = NN * NDOWNSPS;
inline static constexpr float TSTEP = NSTEP / 12000.0f;
inline static constexpr int JSTRT = ASTART / TSTEP;
inline static constexpr float DF = 12000.0f / NFFT1;
};
/* C MODE DECODER */
struct ModeC
{
// Static constants
inline static constexpr int NSUBMODE = 2;
inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
inline static constexpr int NSPS = JS8C_SYMBOL_SAMPLES;
inline static constexpr int NTXDUR = JS8C_TX_SECONDS;
inline static constexpr int NDOWNSPS = 12;
inline static constexpr int NDD = 120;
inline static constexpr int JZ = 172;
inline static constexpr float ASTART = 0.1f;
inline static constexpr float BASESUB = 38.0f;
// Derived parameters
inline static constexpr float AZ = (12000.0f / NSPS) * 0.6f;
inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
inline static constexpr int NFFT1 = NSPS * NFOS;
inline static constexpr int NSTEP = NSPS / NSSY;
inline static constexpr int NHSYM = NMAX / NSTEP - 3;
inline static constexpr int NDOWN = NSPS / NDOWNSPS;
inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
inline static constexpr int NDFFT1 = NSPS * NDD;
inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
inline static constexpr int NP2 = NN * NDOWNSPS;
inline static constexpr float TSTEP = NSTEP / 12000.0f;
inline static constexpr int JSTRT = ASTART / TSTEP;
inline static constexpr float DF = 12000.0f / NFFT1;
};
/* E MODE DECODER */
// Note that the original used 28 for NTXDUR and 90 for NDD, but the
// corresponding C++ mainline side used 30 for NTXDUR, so for the
// moment, we're matching that here, which seems logical at present.
struct ModeE
{
// Static constants
inline static constexpr int NSUBMODE = 4;
inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
inline static constexpr int NSPS = JS8E_SYMBOL_SAMPLES;
inline static constexpr int NTXDUR = JS8E_TX_SECONDS; // XXX was 28 in Fortran
inline static constexpr int NDOWNSPS = 32;
inline static constexpr int NDD = 94; // XXX was 90 in Fortran
inline static constexpr int JZ = 32;
inline static constexpr float ASTART = 0.5f;
inline static constexpr float BASESUB = 42.0f;
// Derived parameters
inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
inline static constexpr int NFFT1 = NSPS * NFOS;
inline static constexpr int NSTEP = NSPS / NSSY;
inline static constexpr int NHSYM = NMAX / NSTEP - 3;
inline static constexpr int NDOWN = NSPS / NDOWNSPS;
inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
inline static constexpr int NDFFT1 = NSPS * NDD;
inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
inline static constexpr int NP2 = NN * NDOWNSPS;
inline static constexpr float TSTEP = NSTEP / 12000.0f;
inline static constexpr int JSTRT = ASTART / TSTEP;
inline static constexpr float DF = 12000.0f / NFFT1;
};
/* I MODE DECODER */
struct ModeI
{
// Static constants
inline static constexpr int NSUBMODE = 8;
inline static constexpr auto NCOSTAS = JS8::Costas::Type::MODIFIED;
inline static constexpr int NSPS = JS8I_SYMBOL_SAMPLES;
inline static constexpr int NTXDUR = JS8I_TX_SECONDS;
inline static constexpr int NDOWNSPS = 12;
inline static constexpr int NDD = 125;
inline static constexpr int JZ = 250;
inline static constexpr float ASTART = 0.1f;
inline static constexpr float BASESUB = 36.0f;
// Derived parameters
inline static constexpr float AZ = (12000.0f / NSPS) * 0.64f;
inline static constexpr int NMAX = NTXDUR * JS8_RX_SAMPLE_RATE;
inline static constexpr int NFFT1 = NSPS * NFOS;
inline static constexpr int NSTEP = NSPS / NSSY;
inline static constexpr int NHSYM = NMAX / NSTEP - 3;
inline static constexpr int NDOWN = NSPS / NDOWNSPS;
inline static constexpr int NQSYMBOL = NDOWNSPS / 4;
inline static constexpr int NDFFT1 = NSPS * NDD;
inline static constexpr int NDFFT2 = NDFFT1 / NDOWN;
inline static constexpr int NP2 = NN * NDOWNSPS;
inline static constexpr float TSTEP = NSTEP / 12000.0f;
inline static constexpr int JSTRT = ASTART / TSTEP;
inline static constexpr float DF = 12000.0f / NFFT1;
};
// Tunable settings; degree of the polynomial used for the baseline
// curve fit, and the percentile of the span at which to sample. In
// general, a 5th degree polynomial and the 10th percentile should
// be optimal.
constexpr auto BASELINE_DEGREE = 5;
constexpr auto BASELINE_SAMPLE = 10;
// Define the closed range in Hz that we'll consider to be the window
// for baseline determination.
constexpr auto BASELINE_MIN = 500;
constexpr auto BASELINE_MAX = 2500;
// We're going to do a pairwise Estrin's evaluation of the polynomial
// coefficients, so it's critical that the degree of the polynomial is
// odd, resulting in an even number of coefficients.
static_assert(BASELINE_DEGREE & 1, "Degree must be odd");
static_assert(BASELINE_SAMPLE >= 0 &&
BASELINE_SAMPLE <= 100, "Sample must be a percentage");
// Since we know the degree of the polynomial, and thus the number of
// nodes that we're going to use, we can do all the trigonometry work
// required to calculate the Chebyshev nodes in advance, by computing
// them over the range [0, 1]; we can then scale these at runtime to
// a span of any size by simple multiplication.
//
// Downside to this with C++17 is that std::cos() is not yet constexpr,
// as it is in C++20, so we must provide our own implementation until
// then.
constexpr auto BASELINE_NODES = []()
{
// Down to the actual business of generating Chebyshev nodes
// suitable for scaling; once we move to C++20 as the minimum
// compiler, we can remove the cos() function above and instead
// call std::cos() here, as it's required to be constexpr in
// C++20 and above, and presumably it'll be of high quality.
auto nodes = std::array<double, BASELINE_DEGREE + 1>{};
constexpr auto slice = std::numbers::pi / (2.0 * nodes.size());
for (std::size_t i = 0; i < nodes.size(); ++i)
{
nodes[i] = 0.5 * (1.0 - cos(slice * (2.0 * i + 1)));
}
return nodes;
}();
}
/******************************************************************************/
// Local Types
/******************************************************************************/
namespace
{
// Accumulation of rounding errors in IEEE 754 values can be a problem
// when summing large numbers of small values; a Kahan summation class
// by which to compensate for them.
//
// Fortran, or at least, gfortran, will use this technique under the
// covers in various scenarios. While it'd be reasonable to expect it
// to be used in sum(), that's typically not the case.
//
// However, for example, it'll use it here for the value that goes into
// win(i), and naive summation in C++ will as a result not produce the
// same values without using compensation.
//
// subroutine nuttal_window(win,n)
// real win(n)
// pi=4.0*atan(1.0)
// a0=0.3635819
// a1=-0.4891775;
// a2=0.1365995;
// a3=-0.0106411;
// do i=1,n
// win(i)=a0+a1*cos(2*pi*(i-1)/(n))+ &
// a2*cos(4*pi*(i-1)/(n))+ &
// a3*cos(6*pi*(i-1)/(n))
// enddo
// return
// end subroutine nuttal_window
template <std::floating_point T>
class KahanSum
{
T m_sum; // Accumulated sum
T m_compensation; // Compensation for lost low-order bits
public:
KahanSum(T sum = T(0))
: m_sum(sum)
, m_compensation(T(0))
{}
KahanSum &
operator=(T const sum)
{
m_sum = sum;
m_compensation = T(0);
return *this;
}
KahanSum &
operator+=(T const value)
{
T const y = value - m_compensation; // Correct the value
T const t = m_sum + y; // Perform the sum
m_compensation = (t - m_sum) - y; // Update compensation
m_sum = t; // Update the sum
return *this;
}
operator T() const { return m_sum; }
};
// Management of dynamic FFTW plan storage.
class FFTWPlanManager
{
public:
enum class Type
{
DS,
BB,
CF,
CB,
SD,
CS,
count
};
// Disallow copying and moving
FFTWPlanManager (FFTWPlanManager const &) = delete;
FFTWPlanManager & operator=(FFTWPlanManager const &) = delete;
FFTWPlanManager (FFTWPlanManager &&) = delete;
FFTWPlanManager & operator=(FFTWPlanManager &&) = delete;
// Constructor
FFTWPlanManager()
{
m_plans.fill(nullptr);
}
// Destructor
~FFTWPlanManager()
{
std::lock_guard<std::mutex> lock(fftw_mutex);
for (auto & plan : m_plans)
{
if (plan) fftwf_destroy_plan(plan);
}
}
// Accessor
fftwf_plan const &
operator[](Type const type) const noexcept
{
return m_plans[static_cast<std::size_t>(type)];
}
// Manipulator
fftwf_plan &
operator[](Type const type) noexcept
{
return m_plans[static_cast<std::size_t>(type)];
}
// Iteration support
auto begin() noexcept { return m_plans.begin(); }
auto end() noexcept { return m_plans.end(); }
auto begin() const noexcept { return m_plans.begin(); }
auto end() const noexcept { return m_plans.end(); }
private:
// Data members
std::array<fftwf_plan, static_cast<std::size_t>(Type::count)> m_plans;
};
// Encapsulates the first-order search results provided by syncjs8().
struct Sync
{
float freq;
float step;
float sync;
// Constructor for convenience.
Sync(float const freq,
float const step,
float const sync)
: freq(freq)
, step(step)
, sync(sync)
{}
};
// Tag structs so that we can refer to multi index container indices
// by a descriptive tag instead of by the index of the index. These
// don't need to be anything but a name.
namespace Tag
{
struct Freq {};
struct Rank {};
struct Sync {};
}
// Container indexing Sync objects in useful ways, used by syncjs8().
namespace MI = boost::multi_index;
using SyncIndex = MI::multi_index_container
<
Sync,
MI::indexed_by
<
MI::ordered_non_unique<
MI::tag<Tag::Freq>,
MI::key<&Sync::freq>
>,
MI::ranked_non_unique<
MI::tag<Tag::Rank>,
MI::key<&Sync::sync>
>,
MI::ordered_non_unique<
MI::tag<Tag::Sync>,
MI::key<&Sync::sync>,
std::greater<>
>
>
>;
// Represents a decoded message, i.e., the 3-bit message type
// and the 12 bytes that result from decoding a message.
class Decode
{
public:
int type;
std::string data;
Decode(int type,
std::string data)
: type(type)
, data(std::move(data))
{}
bool operator==(Decode const &) const noexcept = default;
struct Hash
{
std::size_t
operator()(Decode const & decode) const noexcept
{
std::size_t const h1 = std::hash<int>{}(decode.type);
std::size_t const h2 = std::hash<std::string>{}(decode.data);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
using Map = std::unordered_map<Decode, int, Hash>;
};
}
/******************************************************************************/
// Belief Propagation Decoder
/******************************************************************************/
namespace
{
constexpr int BP_MAX_ROWS = 7; // Max rows per column in Nm
constexpr int BP_MAX_CHECKS = 3; // Max checks per bit in Mn
constexpr int BP_MAX_ITERATIONS = 30; // Max iterations in BP decoder
constexpr std::array<std::array<int, BP_MAX_CHECKS>, N> Mn =
{{
{ 0, 24, 68}, { 1, 4, 72}, { 2, 31, 67}, { 3, 50, 60}, { 5, 62, 69}, { 6, 32, 78},
{ 7, 49, 85}, { 8, 36, 42}, { 9, 40, 64}, {10, 13, 63}, {11, 74, 76}, {12, 22, 80},
{14, 15, 81}, {16, 55, 65}, {17, 52, 59}, {18, 30, 51}, {19, 66, 83}, {20, 28, 71},
{21, 23, 43}, {25, 34, 75}, {26, 35, 37}, {27, 39, 41}, {29, 53, 54}, {33, 48, 86},
{38, 56, 57}, {44, 73, 82}, {45, 61, 79}, {46, 47, 84}, {58, 70, 77}, { 0, 49, 52},
{ 1, 46, 83}, { 2, 24, 78}, { 3, 5, 13}, { 4, 6, 79}, { 7, 33, 54}, { 8, 35, 68},
{ 9, 42, 82}, {10, 22, 73}, {11, 16, 43}, {12, 56, 75}, {14, 26, 55}, {15, 27, 28},
{17, 18, 58}, {19, 39, 62}, {20, 34, 51}, {21, 53, 63}, {23, 61, 77}, {25, 31, 76},
{29, 71, 84}, {30, 64, 86}, {32, 38, 50}, {36, 47, 74}, {37, 69, 70}, {40, 41, 67},
{44, 66, 85}, {45, 80, 81}, {48, 65, 72}, {57, 59, 65}, {60, 64, 84}, { 0, 13, 20},
{ 1, 12, 58}, { 2, 66, 81}, { 3, 31, 72}, { 4, 35, 53}, { 5, 42, 45}, { 6, 27, 74},
{ 7, 32, 70}, { 8, 48, 75}, { 9, 57, 63}, {10, 47, 67}, {11, 18, 44}, {14, 49, 60},
{15, 21, 25}, {16, 71, 79}, {17, 39, 54}, {19, 34, 50}, {22, 24, 33}, {23, 62, 86},
{26, 38, 73}, {28, 77, 82}, {29, 69, 76}, {30, 68, 83}, {21, 36, 85}, {37, 40, 80},
{41, 43, 56}, {46, 52, 61}, {51, 55, 78}, {59, 74, 80}, { 0, 38, 76}, { 1, 15, 40},
{ 2, 30, 53}, { 3, 35, 77}, { 4, 44, 64}, { 5, 56, 84}, { 6, 13, 48}, { 7, 20, 45},
{ 8, 14, 71}, { 9, 19, 61}, {10, 16, 70}, {11, 33, 46}, {12, 67, 85}, {17, 22, 42},
{18, 63, 72}, {23, 47, 78}, {24, 69, 82}, {25, 79, 86}, {26, 31, 39}, {27, 55, 68},
{28, 62, 65}, {29, 41, 49}, {32, 36, 81}, {34, 59, 73}, {37, 54, 83}, {43, 51, 60},
{50, 52, 71}, {57, 58, 66}, {46, 55, 75}, { 0, 18, 36}, { 1, 60, 74}, { 2, 7, 65},
{ 3, 59, 83}, { 4, 33, 38}, { 5, 25, 52}, { 6, 31, 56}, { 8, 51, 66}, { 9, 11, 14},
{10, 50, 68}, {12, 13, 64}, {15, 30, 42}, {16, 19, 35}, {17, 79, 85}, {20, 47, 58},
{21, 39, 45}, {22, 32, 61}, {23, 29, 73}, {24, 41, 63}, {26, 48, 84}, {27, 37, 72},
{28, 43, 80}, {34, 67, 69}, {40, 62, 75}, {44, 48, 70}, {49, 57, 86}, {47, 53, 82},
{12, 54, 78}, {76, 77, 81}, { 0, 1, 23}, { 2, 5, 74}, { 3, 55, 86}, { 4, 43, 52},
{ 6, 49, 82}, { 7, 9, 27}, { 8, 54, 61}, {10, 28, 66}, {11, 32, 39}, {13, 15, 19},
{14, 34, 72}, {16, 30, 38}, {17, 35, 56}, {18, 45, 75}, {20, 41, 83}, {21, 33, 58},
{22, 25, 60}, {24, 59, 64}, {26, 63, 79}, {29, 36, 65}, {31, 44, 71}, {37, 50, 85},
{40, 76, 78}, {42, 55, 67}, {46, 73, 81}, {39, 51, 77}, {53, 60, 70}, {45, 57, 68}
}};
struct CheckNode
{
int valid_neighbors;
std::array<int, BP_MAX_ROWS> neighbors;
};
constexpr std::array<CheckNode, M> Nm =
{{
{6, { 0, 29, 59, 88, 117, 146, 0}}, {6, { 1, 30, 60, 89, 118, 146, 0}}, {6, { 2, 31, 61, 90, 119, 147, 0}},
{6, { 3, 32, 62, 91, 120, 148, 0}}, {6, { 1, 33, 63, 92, 121, 149, 0}}, {6, { 4, 32, 64, 93, 122, 147, 0}},
{6, { 5, 33, 65, 94, 123, 150, 0}}, {6, { 6, 34, 66, 95, 119, 151, 0}}, {6, { 7, 35, 67, 96, 124, 152, 0}},
{6, { 8, 36, 68, 97, 125, 151, 0}}, {6, { 9, 37, 69, 98, 126, 153, 0}}, {6, {10, 38, 70, 99, 125, 154, 0}},
{6, {11, 39, 60, 100, 127, 144, 0}}, {6, { 9, 32, 59, 94, 127, 155, 0}}, {6, {12, 40, 71, 96, 125, 156, 0}},
{6, {12, 41, 72, 89, 128, 155, 0}}, {6, {13, 38, 73, 98, 129, 157, 0}}, {6, {14, 42, 74, 101, 130, 158, 0}},
{6, {15, 42, 70, 102, 117, 159, 0}}, {6, {16, 43, 75, 97, 129, 155, 0}}, {6, {17, 44, 59, 95, 131, 160, 0}},
{6, {18, 45, 72, 82, 132, 161, 0}}, {6, {11, 37, 76, 101, 133, 162, 0}}, {6, {18, 46, 77, 103, 134, 146, 0}},
{6, { 0, 31, 76, 104, 135, 163, 0}}, {6, {19, 47, 72, 105, 122, 162, 0}}, {6, {20, 40, 78, 106, 136, 164, 0}},
{6, {21, 41, 65, 107, 137, 151, 0}}, {6, {17, 41, 79, 108, 138, 153, 0}}, {6, {22, 48, 80, 109, 134, 165, 0}},
{6, {15, 49, 81, 90, 128, 157, 0}}, {6, {2, 47, 62, 106, 123, 166, 0}}, {6, { 5, 50, 66, 110, 133, 154, 0}},
{6, {23, 34, 76, 99, 121, 161, 0}}, {6, {19, 44, 75, 111, 139, 156, 0}}, {6, {20, 35, 63, 91, 129, 158, 0}},
{6, { 7, 51, 82, 110, 117, 165, 0}}, {6, {20, 52, 83, 112, 137, 167, 0}}, {6, {24, 50, 78, 88, 121, 157, 0}},
{7, {21, 43, 74, 106, 132, 154, 171}}, {6, { 8, 53, 83, 89, 140, 168, 0}}, {6, {21, 53, 84, 109, 135, 160, 0}},
{6, { 7, 36, 64, 101, 128, 169, 0}}, {6, {18, 38, 84, 113, 138, 149, 0}}, {6, {25, 54, 70, 92, 141, 166, 0}},
{7, {26, 55, 64, 95, 132, 159, 173}}, {6, {27, 30, 85, 99, 116, 170, 0}}, {6, {27, 51, 69, 103, 131, 143, 0}},
{6, {23, 56, 67, 94, 136, 141, 0}}, {6, {6, 29, 71, 109, 142, 150, 0}}, {6, { 3, 50, 75, 114, 126, 167, 0}},
{6, {15, 44, 86, 113, 124, 171, 0}}, {6, {14, 29, 85, 114, 122, 149, 0}}, {6, {22, 45, 63, 90, 143, 172, 0}},
{6, {22, 34, 74, 112, 144, 152, 0}}, {7, {13, 40, 86, 107, 116, 148, 169}}, {6, {24, 39, 84, 93, 123, 158, 0}},
{6, {24, 57, 68, 115, 142, 173, 0}}, {6, {28, 42, 60, 115, 131, 161, 0}}, {6, {14, 57, 87, 111, 120, 163, 0}},
{7, { 3, 58, 71, 113, 118, 162, 172}}, {6, {26, 46, 85, 97, 133, 152, 0}}, {5, { 4, 43, 77, 108, 140, 0, 0}},
{6, { 9, 45, 68, 102, 135, 164, 0}}, {6, { 8, 49, 58, 92, 127, 163, 0}}, {6, {13, 56, 57, 108, 119, 165, 0}},
{6, {16, 54, 61, 115, 124, 153, 0}}, {6, { 2, 53, 69, 100, 139, 169, 0}}, {6, { 0, 35, 81, 107, 126, 173, 0}},
{5, { 4, 52, 80, 104, 139, 0, 0}}, {6, {28, 52, 66, 98, 141, 172, 0}}, {6, {17, 48, 73, 96, 114, 166, 0}},
{6, { 1, 56, 62, 102, 137, 156, 0}}, {6, {25, 37, 78, 111, 134, 170, 0}}, {6, {10, 51, 65, 87, 118, 147, 0}},
{6, {19, 39, 67, 116, 140, 159, 0}}, {6, {10, 47, 80, 88, 145, 168, 0}}, {6, {28, 46, 79, 91, 145, 171, 0}},
{6, { 5, 31, 86, 103, 144, 168, 0}}, {6, {26, 33, 73, 105, 130, 164, 0}}, {5, {11, 55, 83, 87, 138, 0, 0}},
{6, {12, 55, 61, 110, 145, 170, 0}}, {6, {25, 36, 79, 104, 143, 150, 0}}, {6, {16, 30, 81, 112, 120, 160, 0}},
{5, {27, 48, 58, 93, 136, 0, 0}}, {6, { 6, 54, 82, 100, 130, 167, 0}}, {6, {23, 49, 77, 105, 142, 148, 0}}
}};
// Belief Propagation Decoder
int
bpdecode174(std::array<float, N> const & llr,
std::array<int8_t, K> & decoded,
std::array<int8_t, N> & cw)
{
// Initialize messages and variables
std::array<std::array<float, BP_MAX_CHECKS>, N> tov = {}; // Messages to variable nodes
std::array<std::array<float, BP_MAX_ROWS>, M> toc = {}; // Messages to check nodes
std::array<std::array<float, BP_MAX_ROWS> , M> tanhtoc = {}; // Tanh of messages
std::array<float, N> zn = {}; // Bit log likelihood ratios
std::array<int, M> synd = {}; // Syndrome for checks
int ncnt = 0;
int nclast = 0;
// Initialize toc (messages from bits to checks)
for (int i = 0; i < M; ++i) {
for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
toc[i][j] = llr[Nm[i].neighbors[j]];
}
}
// Iterative decoding
for (int iter = 0; iter <= BP_MAX_ITERATIONS; ++iter) {
// Update bit log likelihood ratios
for (int i = 0; i < N; ++i) {
zn[i] = llr[i] + std::accumulate(tov[i].begin(), tov[i].begin() + BP_MAX_CHECKS, 0.0f);
}
// Check if we have a valid codeword
for (int i = 0; i < N; ++i) cw[i] = zn[i] > 0 ? 1 : 0;
int ncheck = 0;
for (int i = 0; i < M; ++i) {
synd[i] = 0;
for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
synd[i] += cw[Nm[i].neighbors[j]];
}
if (synd[i] % 2 != 0) ++ncheck;
}
if (ncheck == 0)
{
// Extract decoded bits (last N-M bits of codeword)
std::copy(cw.begin() + M, cw.end(), decoded.begin());
// Count errors
int nerr = 0;
for (int i = 0; i < N; ++i) {
if ((2 * cw[i] - 1) * llr[i] < 0.0f) {
++nerr;
}
}
return nerr;
}
// Early stopping criterion
if (iter > 0) {
int nd = ncheck - nclast;
ncnt = (nd < 0) ? 0 : ncnt + 1;
if (ncnt >= 5 && iter >= 10 && ncheck > 15) {
return -1;
}
}
nclast = ncheck;
// Send messages from bits to check nodes
for (int i = 0; i < M; ++i) {
for (int j = 0; j < Nm[i].valid_neighbors; ++j) {
int ibj = Nm[i].neighbors[j];
toc[i][j] = zn[ibj];
for (int k = 0; k < BP_MAX_CHECKS; ++k) {
if (Mn[ibj][k] == i) {
toc[i][j] -= tov[ibj][k];
}
}
}
}
// Send messages from check nodes to variable nodes
for (int i = 0; i < M; ++i) {
for (int j = 0; j < 7; ++j) { // Fixed range [0, 7) to match Fortran's 1:7, could be nrw[j], or 7 logically
tanhtoc[i][j] = std::tanh(-toc[i][j] / 2.0f);
}
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < BP_MAX_CHECKS; ++j) {
int ichk = Mn[i][j];
if (ichk >= 0) {
float Tmn = 1.0f;
for (int k = 0; k < Nm[ichk].valid_neighbors; ++k) {
if (Nm[ichk].neighbors[k] != i) {
Tmn *= tanhtoc[ichk][k];
}
}
tov[i][j] = 2.0f * std::atanh(-Tmn);
}
}
}
}
return -1; // Decoding failed
}
}
/******************************************************************************/
// Local Routines
/******************************************************************************/
namespace
{
constexpr std::string_view alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
static_assert(alphabet.size() == 64);
// Function that either translates valid JS8 message characters to their
// corresponding 6-bit word value, or throws. This will end up doing a
// direct index operation into a 256-byte table, the creation of which
// must be constexpr under C++17.
constexpr auto alphabetWord = []()
{
constexpr std::uint8_t invalid = 0xff;
constexpr auto words = []()
{
std::array<std::uint8_t, 256> words{};
for (auto & word : words) word = invalid;
for (std::size_t i = 0; i < alphabet.size(); ++i)
{
words[static_cast<std::uint8_t>(alphabet[i])] = static_cast<std::uint8_t>(i);
}
return words;
}();
return [words](char const value)
{
if (auto const word = words[value];
word != invalid)
{
return word;
}
throw std::runtime_error("Invalid character in message");
};
}();
// Sanity check key bounds of the 6-bit encoding table.
static_assert(alphabetWord('0') == 0);
static_assert(alphabetWord('A') == 10);
static_assert(alphabetWord('a') == 36);
static_assert(alphabetWord('-') == 62);
static_assert(alphabetWord('+') == 63);
template <typename T>
std::uint16_t
CRC12(T const & range)
{
return boost::augmented_crc<12, 0xc06>(range.data(),
range.size()) ^ 42;
}
bool
checkCRC12(std::array<std::int8_t, KK> const & decoded)
{
std::array<uint8_t, 11> bits = {};
for (std::size_t i = 0; i < decoded.size(); ++i)
{
if (decoded[i]) bits[i / 8] |= (1 << (7 - (i % 8)));
}
// Extract the received CRC-12.
uint16_t crc = (static_cast<uint16_t>(bits[9] & 0x1F) << 7) |
(static_cast<uint16_t>(bits[10]) >> 1);
// Clear bits that correspond to the CRC in the last bytes.
bits[9] &= 0xE0;
bits[10] = 0x00;
// Compute CRC and indicate if we have a match.
return crc == CRC12(bits);
}
std::string
extractmessage174(std::array<int8_t, KK> const & decoded)
{
std::string message;
// Ensure received CRC matches computed CRC.
if (checkCRC12(decoded))
{
message.reserve(12);
// Decode the message from the 72 data bits
std::array<uint8_t, 12> words;
for (std::size_t i = 0; i < 12; ++i)
{
words[i] = (decoded[i * 6 + 0] << 5) |
(decoded[i * 6 + 1] << 4) |
(decoded[i * 6 + 2] << 3) |
(decoded[i * 6 + 3] << 2) |
(decoded[i * 6 + 4] << 1) |
(decoded[i * 6 + 5] << 0);
}
// Map 6-bit words to the alphabet
for (auto const word : words) message += alphabet[word];
}
return message;
}
// Parity matrix for JS8 message generation.
//
// This should be 952 bytes in size; to store an 87x87 matrix of bits,
// you need 7569 bits, which requires 119 64-bit values, or 952 bytes.
//
// Background here is that this is a low-density parity check code (LDPC),
// generated using the PEG algorithm. In short, true values in a row i of
// the matrix define which of the 87 message bits must be summed, modulo
// 2, to produce the ith parity check bit. Decent references on this are:
//
// 1. https://wsjt.sourceforge.io/FT4_FT8_QEX.pdf
// 2. https://inference.org.uk/mackay/PEG_ECC.html
// 3. https://github.com/Lcrypto/classic-PEG-
//
// The data used was harvested from the original 'ldpc_174_87_params.f90',
// but you'll note that the rows have been reordered here, because this
// isn't Fortran; C++ is row-major, not column-major.
constexpr auto parity = []()
{
constexpr std::size_t Rows = 87;
constexpr std::size_t Cols = 87;
using ElementType = std::uint64_t;
constexpr std::size_t ElementSize = std::numeric_limits<ElementType>::digits;
constexpr auto matrix = []()
{
constexpr std::array<std::string_view, Rows> Data =
{
"23bba830e23b6b6f50982e", "1f8e55da218c5df3309052", "ca7b3217cd92bd59a5ae20",
"56f78313537d0f4382964e", "6be396b5e2e819e373340c", "293548a138858328af4210",
"cb6c6afcdc28bb3f7c6e86", "3f2a86f5c5bd225c961150", "849dd2d63673481860f62c",
"56cdaec6e7ae14b43feeee", "04ef5cfa3766ba778f45a4", "c525ae4bd4f627320a3974",
"41fd9520b2e4abeb2f989c", "7fb36c24085a34d8c1dbc4", "40fc3e44bb7d2bb2756e44",
"d38ab0a1d2e52a8ec3bc76", "3d0f929ef3949bd84d4734", "45d3814f504064f80549ae",
"f14dbf263825d0bd04b05e", "db714f8f64e8ac7af1a76e", "8d0274de71e7c1a8055eb0",
"51f81573dd4049b082de14", "d8f937f31822e57c562370", "b6537f417e61d1a7085336",