From 902e58b46b831593a209854050d3a5673c26c4c9 Mon Sep 17 00:00:00 2001 From: f4exb Date: Sun, 8 Jan 2023 19:03:29 +0100 Subject: [PATCH] FT8 demod: initial commit of FT8 library with minimal changes and benchmark test --- CMakeLists.txt | 4 + ft8/CMakeLists.txt | 29 + ft8/arrays.h | 300 +++ ft8/fft.cpp | 644 +++++++ ft8/fft.h | 46 + ft8/ft8.cpp | 3914 ++++++++++++++++++++++++++++++++++++++ ft8/ft8.h | 65 + ft8/libldpc.cpp | 747 ++++++++ ft8/libldpc.h | 36 + ft8/osd.cpp | 489 +++++ ft8/osd.h | 37 + ft8/unpack.cpp | 551 ++++++ ft8/unpack.h | 30 + ft8/util.cpp | 408 ++++ ft8/util.h | 58 + sdrbench/CMakeLists.txt | 5 + sdrbench/mainbench.cpp | 2 + sdrbench/mainbench.h | 2 + sdrbench/parserbench.cpp | 4 +- sdrbench/parserbench.h | 3 +- sdrbench/test_ft8.cpp | 111 ++ 21 files changed, 7483 insertions(+), 2 deletions(-) create mode 100644 ft8/CMakeLists.txt create mode 100644 ft8/arrays.h create mode 100644 ft8/fft.cpp create mode 100644 ft8/fft.h create mode 100644 ft8/ft8.cpp create mode 100644 ft8/ft8.h create mode 100644 ft8/libldpc.cpp create mode 100644 ft8/libldpc.h create mode 100644 ft8/osd.cpp create mode 100644 ft8/osd.h create mode 100644 ft8/unpack.cpp create mode 100644 ft8/unpack.h create mode 100644 ft8/util.cpp create mode 100644 ft8/util.h create mode 100644 sdrbench/test_ft8.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dcb1781dd..541fc7c8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -781,6 +781,10 @@ add_subdirectory(sdrbench) add_subdirectory(modemm17) +if (LINUX) + add_subdirectory(ft8) +endif() + if (BUILD_GUI) add_subdirectory(sdrgui) add_subdirectory(plugins plugins) diff --git a/ft8/CMakeLists.txt b/ft8/CMakeLists.txt new file mode 100644 index 000000000..e58523db8 --- /dev/null +++ b/ft8/CMakeLists.txt @@ -0,0 +1,29 @@ +project(ft8) + +set(ft8_SOURCES + fft.cpp + ft8.cpp + libldpc.cpp + osd.cpp + unpack.cpp + util.cpp +) + +set(ft8_HEADERS + fft.h + ft8.h + libldpc.h + osd.h + unpack.h + util.h +) + +add_library(ft8 SHARED + ${ft8_SOURCES} +) + +target_link_libraries(ft8 + ${FFTW3F_LIBRARIES} +) + +install(TARGETS ft8 DESTINATION ${INSTALL_LIB_DIR}) diff --git a/ft8/arrays.h b/ft8/arrays.h new file mode 100644 index 000000000..37052c0e8 --- /dev/null +++ b/ft8/arrays.h @@ -0,0 +1,300 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +namespace FT8 { + +// +// this is the LDPC(174,91) parity check matrix. +// each row describes one parity check. +// 83 rows. +// each number is an index into the codeword (1-origin). +// the codeword bits mentioned in each row must xor to zero. +// From WSJT-X's ldpc_174_91_c_reordered_parity.f90 +// +int Nm[][7] = { + { 4, 31, 59, 91, 92, 96, 153 }, + { 5, 32, 60, 93, 115, 146, 0 }, + { 6, 24, 61, 94, 122, 151, 0 }, + { 7, 33, 62, 95, 96, 143, 0 }, + { 8, 25, 63, 83, 93, 96, 148 }, + { 6, 32, 64, 97, 126, 138, 0 }, + { 5, 34, 65, 78, 98, 107, 154 }, + { 9, 35, 66, 99, 139, 146, 0 }, + { 10, 36, 67, 100, 107, 126, 0 }, + { 11, 37, 67, 87, 101, 139, 158 }, + { 12, 38, 68, 102, 105, 155, 0 }, + { 13, 39, 69, 103, 149, 162, 0 }, + { 8, 40, 70, 82, 104, 114, 145 }, + { 14, 41, 71, 88, 102, 123, 156 }, + { 15, 42, 59, 106, 123, 159, 0 }, + { 1, 33, 72, 106, 107, 157, 0 }, + { 16, 43, 73, 108, 141, 160, 0 }, + { 17, 37, 74, 81, 109, 131, 154 }, + { 11, 44, 75, 110, 121, 166, 0 }, + { 45, 55, 64, 111, 130, 161, 173 }, + { 8, 46, 71, 112, 119, 166, 0 }, + { 18, 36, 76, 89, 113, 114, 143 }, + { 19, 38, 77, 104, 116, 163, 0 }, + { 20, 47, 70, 92, 138, 165, 0 }, + { 2, 48, 74, 113, 128, 160, 0 }, + { 21, 45, 78, 83, 117, 121, 151 }, + { 22, 47, 58, 118, 127, 164, 0 }, + { 16, 39, 62, 112, 134, 158, 0 }, + { 23, 43, 79, 120, 131, 145, 0 }, + { 19, 35, 59, 73, 110, 125, 161 }, + { 20, 36, 63, 94, 136, 161, 0 }, + { 14, 31, 79, 98, 132, 164, 0 }, + { 3, 44, 80, 124, 127, 169, 0 }, + { 19, 46, 81, 117, 135, 167, 0 }, + { 7, 49, 58, 90, 100, 105, 168 }, + { 12, 50, 61, 118, 119, 144, 0 }, + { 13, 51, 64, 114, 118, 157, 0 }, + { 24, 52, 76, 129, 148, 149, 0 }, + { 25, 53, 69, 90, 101, 130, 156 }, + { 20, 46, 65, 80, 120, 140, 170 }, + { 21, 54, 77, 100, 140, 171, 0 }, + { 35, 82, 133, 142, 171, 174, 0 }, + { 14, 30, 83, 113, 125, 170, 0 }, + { 4, 29, 68, 120, 134, 173, 0 }, + { 1, 4, 52, 57, 86, 136, 152 }, + { 26, 51, 56, 91, 122, 137, 168 }, + { 52, 84, 110, 115, 145, 168, 0 }, + { 7, 50, 81, 99, 132, 173, 0 }, + { 23, 55, 67, 95, 172, 174, 0 }, + { 26, 41, 77, 109, 141, 148, 0 }, + { 2, 27, 41, 61, 62, 115, 133 }, + { 27, 40, 56, 124, 125, 126, 0 }, + { 18, 49, 55, 124, 141, 167, 0 }, + { 6, 33, 85, 108, 116, 156, 0 }, + { 28, 48, 70, 85, 105, 129, 158 }, + { 9, 54, 63, 131, 147, 155, 0 }, + { 22, 53, 68, 109, 121, 174, 0 }, + { 3, 13, 48, 78, 95, 123, 0 }, + { 31, 69, 133, 150, 155, 169, 0 }, + { 12, 43, 66, 89, 97, 135, 159 }, + { 5, 39, 75, 102, 136, 167, 0 }, + { 2, 54, 86, 101, 135, 164, 0 }, + { 15, 56, 87, 108, 119, 171, 0 }, + { 10, 44, 82, 91, 111, 144, 149 }, + { 23, 34, 71, 94, 127, 153, 0 }, + { 11, 49, 88, 92, 142, 157, 0 }, + { 29, 34, 87, 97, 147, 162, 0 }, + { 30, 50, 60, 86, 137, 142, 162 }, + { 10, 53, 66, 84, 112, 128, 165 }, + { 22, 57, 85, 93, 140, 159, 0 }, + { 28, 32, 72, 103, 132, 166, 0 }, + { 28, 29, 84, 88, 117, 143, 150 }, + { 1, 26, 45, 80, 128, 147, 0 }, + { 17, 27, 89, 103, 116, 153, 0 }, + { 51, 57, 98, 163, 165, 172, 0 }, + { 21, 37, 73, 138, 152, 169, 0 }, + { 16, 47, 76, 130, 137, 154, 0 }, + { 3, 24, 30, 72, 104, 139, 0 }, + { 9, 40, 90, 106, 134, 151, 0 }, + { 15, 58, 60, 74, 111, 150, 163 }, + { 18, 42, 79, 144, 146, 152, 0 }, + { 25, 38, 65, 99, 122, 160, 0 }, + { 17, 42, 75, 129, 170, 172, 0 }, +}; + +// Mn from WSJT-X's ldpc_174_91_c_reordered_parity.f90 +// each of the 174 rows corresponds to a codeword bit. +// the numbers indicate which three parity +// checks (rows in Nm) refer to the codeword bit. +// 1-origin. +int Mn[][3] = { + { 16, 45, 73 }, + { 25, 51, 62 }, + { 33, 58, 78 }, + { 1, 44, 45 }, + { 2, 7, 61 }, + { 3, 6, 54 }, + { 4, 35, 48 }, + { 5, 13, 21 }, + { 8, 56, 79 }, + { 9, 64, 69 }, + { 10, 19, 66 }, + { 11, 36, 60 }, + { 12, 37, 58 }, + { 14, 32, 43 }, + { 15, 63, 80 }, + { 17, 28, 77 }, + { 18, 74, 83 }, + { 22, 53, 81 }, + { 23, 30, 34 }, + { 24, 31, 40 }, + { 26, 41, 76 }, + { 27, 57, 70 }, + { 29, 49, 65 }, + { 3, 38, 78 }, + { 5, 39, 82 }, + { 46, 50, 73 }, + { 51, 52, 74 }, + { 55, 71, 72 }, + { 44, 67, 72 }, + { 43, 68, 78 }, + { 1, 32, 59 }, + { 2, 6, 71 }, + { 4, 16, 54 }, + { 7, 65, 67 }, + { 8, 30, 42 }, + { 9, 22, 31 }, + { 10, 18, 76 }, + { 11, 23, 82 }, + { 12, 28, 61 }, + { 13, 52, 79 }, + { 14, 50, 51 }, + { 15, 81, 83 }, + { 17, 29, 60 }, + { 19, 33, 64 }, + { 20, 26, 73 }, + { 21, 34, 40 }, + { 24, 27, 77 }, + { 25, 55, 58 }, + { 35, 53, 66 }, + { 36, 48, 68 }, + { 37, 46, 75 }, + { 38, 45, 47 }, + { 39, 57, 69 }, + { 41, 56, 62 }, + { 20, 49, 53 }, + { 46, 52, 63 }, + { 45, 70, 75 }, + { 27, 35, 80 }, + { 1, 15, 30 }, + { 2, 68, 80 }, + { 3, 36, 51 }, + { 4, 28, 51 }, + { 5, 31, 56 }, + { 6, 20, 37 }, + { 7, 40, 82 }, + { 8, 60, 69 }, + { 9, 10, 49 }, + { 11, 44, 57 }, + { 12, 39, 59 }, + { 13, 24, 55 }, + { 14, 21, 65 }, + { 16, 71, 78 }, + { 17, 30, 76 }, + { 18, 25, 80 }, + { 19, 61, 83 }, + { 22, 38, 77 }, + { 23, 41, 50 }, + { 7, 26, 58 }, + { 29, 32, 81 }, + { 33, 40, 73 }, + { 18, 34, 48 }, + { 13, 42, 64 }, + { 5, 26, 43 }, + { 47, 69, 72 }, + { 54, 55, 70 }, + { 45, 62, 68 }, + { 10, 63, 67 }, + { 14, 66, 72 }, + { 22, 60, 74 }, + { 35, 39, 79 }, + { 1, 46, 64 }, + { 1, 24, 66 }, + { 2, 5, 70 }, + { 3, 31, 65 }, + { 4, 49, 58 }, + { 1, 4, 5 }, + { 6, 60, 67 }, + { 7, 32, 75 }, + { 8, 48, 82 }, + { 9, 35, 41 }, + { 10, 39, 62 }, + { 11, 14, 61 }, + { 12, 71, 74 }, + { 13, 23, 78 }, + { 11, 35, 55 }, + { 15, 16, 79 }, + { 7, 9, 16 }, + { 17, 54, 63 }, + { 18, 50, 57 }, + { 19, 30, 47 }, + { 20, 64, 80 }, + { 21, 28, 69 }, + { 22, 25, 43 }, + { 13, 22, 37 }, + { 2, 47, 51 }, + { 23, 54, 74 }, + { 26, 34, 72 }, + { 27, 36, 37 }, + { 21, 36, 63 }, + { 29, 40, 44 }, + { 19, 26, 57 }, + { 3, 46, 82 }, + { 14, 15, 58 }, + { 33, 52, 53 }, + { 30, 43, 52 }, + { 6, 9, 52 }, + { 27, 33, 65 }, + { 25, 69, 73 }, + { 38, 55, 83 }, + { 20, 39, 77 }, + { 18, 29, 56 }, + { 32, 48, 71 }, + { 42, 51, 59 }, + { 28, 44, 79 }, + { 34, 60, 62 }, + { 31, 45, 61 }, + { 46, 68, 77 }, + { 6, 24, 76 }, + { 8, 10, 78 }, + { 40, 41, 70 }, + { 17, 50, 53 }, + { 42, 66, 68 }, + { 4, 22, 72 }, + { 36, 64, 81 }, + { 13, 29, 47 }, + { 2, 8, 81 }, + { 56, 67, 73 }, + { 5, 38, 50 }, + { 12, 38, 64 }, + { 59, 72, 80 }, + { 3, 26, 79 }, + { 45, 76, 81 }, + { 1, 65, 74 }, + { 7, 18, 77 }, + { 11, 56, 59 }, + { 14, 39, 54 }, + { 16, 37, 66 }, + { 10, 28, 55 }, + { 15, 60, 70 }, + { 17, 25, 82 }, + { 20, 30, 31 }, + { 12, 67, 68 }, + { 23, 75, 80 }, + { 27, 32, 62 }, + { 24, 69, 75 }, + { 19, 21, 71 }, + { 34, 53, 61 }, + { 35, 46, 47 }, + { 33, 59, 76 }, + { 40, 43, 83 }, + { 41, 42, 63 }, + { 49, 75, 83 }, + { 20, 44, 48 }, + { 42, 49, 57 }, +}; + +} // namespace FT8 diff --git a/ft8/fft.cpp b/ft8/fft.cpp new file mode 100644 index 000000000..5e3bbdd89 --- /dev/null +++ b/ft8/fft.cpp @@ -0,0 +1,644 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "fft.h" +#include +#include +#include +#include +#include +#include +#include "util.h" + +#define TIMING 0 + +namespace FT8 { + +// MEASURE=0, ESTIMATE=64, PATIENT=32 +int fftw_type = FFTW_ESTIMATE; + +// a cached fftw plan, for both of: +// fftwf_plan_dft_r2c_1d(n, m_in, m_out, FFTW_ESTIMATE); +// fftwf_plan_dft_c2r_1d(n, m_in, m_out, FFTW_ESTIMATE); +class Plan +{ +public: + int n_; + int type_; + + // + // real -> complex + // + fftwf_complex *c_; // (n_ / 2) + 1 of these + float *r_; // n_ of these + fftwf_plan fwd_; // forward plan + fftwf_plan rev_; // reverse plan + + // + // complex -> complex + // + fftwf_complex *cc1_; // n + fftwf_complex *cc2_; // n + fftwf_plan cfwd_; // forward plan + fftwf_plan crev_; // reverse plan + + // how much CPU time spent in FFTs that use this plan. +#if TIMING + float time_; +#endif + const char *why_; + int uses_; +}; + +static std::mutex plansmu; +static Plan *plans[1000]; +static int nplans; +static int plan_master_pid = 0; + +Plan *get_plan(int n, const char *why) +{ + // cache fftw plans in the parent process, + // so they will already be there for fork()ed children. + + plansmu.lock(); + + if (plan_master_pid == 0) + { + plan_master_pid = getpid(); + } + + for (int i = 0; i < nplans; i++) + { + if (plans[i]->n_ == n && plans[i]->type_ == fftw_type +#if TIMING + && strcmp(plans[i]->why_, why) == 0 +#endif + ) + { + Plan *p = plans[i]; + p->uses_ += 1; + plansmu.unlock(); + return p; + } + } + + float t0 = now(); + + // fftw_make_planner_thread_safe(); + + // the fftw planner is not thread-safe. + // can't rely on plansmu because both ft8.so + // and snd.so may be using separate copies of fft.cc. + // the lock file really should be per process. + // FIXME: Qt-fy this + int lockfd = creat("/tmp/fft-plan-lock", 0666); + assert(lockfd >= 0); + fchmod(lockfd, 0666); + int lockret = flock(lockfd, LOCK_EX); + assert(lockret == 0); + + fftwf_set_timelimit(5); + + // + // real -> complex + // + + Plan *p = new Plan; + + p->n_ = n; +#if TIMING + p->time_ = 0; +#endif + p->uses_ = 1; + p->why_ = why; + p->r_ = (float *)fftwf_malloc(n * sizeof(float)); + assert(p->r_); + p->c_ = (fftwf_complex *)fftwf_malloc(((n / 2) + 1) * sizeof(fftwf_complex)); + assert(p->c_); + + // FFTW_ESTIMATE + // FFTW_MEASURE + // FFTW_PATIENT + // FFTW_EXHAUSTIVE + int type = fftw_type; + if (getpid() != plan_master_pid) + { + type = FFTW_ESTIMATE; + } + p->type_ = type; + p->fwd_ = fftwf_plan_dft_r2c_1d(n, p->r_, p->c_, type); + assert(p->fwd_); + p->rev_ = fftwf_plan_dft_c2r_1d(n, p->c_, p->r_, type); + assert(p->rev_); + + // + // complex -> complex + // + p->cc1_ = (fftwf_complex *)fftwf_malloc(n * sizeof(fftwf_complex)); + assert(p->cc1_); + p->cc2_ = (fftwf_complex *)fftwf_malloc(n * sizeof(fftwf_complex)); + assert(p->cc2_); + p->cfwd_ = fftwf_plan_dft_1d(n, p->cc1_, p->cc2_, FFTW_FORWARD, type); + assert(p->cfwd_); + p->crev_ = fftwf_plan_dft_1d(n, p->cc2_, p->cc1_, FFTW_BACKWARD, type); + assert(p->crev_); + + flock(lockfd, LOCK_UN); + close(lockfd); + + assert(nplans + 1 < 1000); + + plans[nplans] = p; + __sync_synchronize(); + nplans += 1; + + if (0 && getpid() == plan_master_pid) + { + float t1 = now(); + fprintf(stderr, "miss pid=%d master=%d n=%d t=%.3f total=%d type=%d, %s\n", + getpid(), plan_master_pid, n, t1 - t0, nplans, type, why); + } + + plansmu.unlock(); + + return p; +} + +// +// do just one FFT on samples[i0..i0+block] +// real inputs, complex outputs. +// output has (block / 2) + 1 points. +// +std::vector> one_fft( + const std::vector &samples, + int i0, + int block, + const char *why, + Plan *p +) +{ + assert(i0 >= 0); + assert(block > 1); + + int nsamples = samples.size(); + int nbins = (block / 2) + 1; + + if (p) + { + assert(p->n_ == block); + p->uses_ += 1; + } + else + { + p = get_plan(block, why); + } + fftwf_plan m_plan = p->fwd_; + +#if TIMING + float t0 = now(); +#endif + + assert((int)samples.size() - i0 >= block); + + int m_in_allocated = 0; + float *m_in = (float *)samples.data() + i0; + + if ((((unsigned long long)m_in) % 16) != 0) + { + // m_in must be on a 16-byte boundary for FFTW. + m_in = (float *)fftwf_malloc(sizeof(float) * p->n_); + assert(m_in); + m_in_allocated = 1; + for (int i = 0; i < block; i++) + { + if (i0 + i < nsamples) + { + m_in[i] = samples[i0 + i]; + } + else + { + m_in[i] = 0; + } + } + } + + fftwf_complex *m_out = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * ((p->n_ / 2) + 1)); + assert(m_out); + + fftwf_execute_dft_r2c(m_plan, m_in, m_out); + + std::vector> out(nbins); + + for (int bi = 0; bi < nbins; bi++) + { + float re = m_out[bi][0]; + float im = m_out[bi][1]; + out[bi] = std::complex(re, im); + } + + if (m_in_allocated) + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return out; +} + +// +// do a full set of FFTs, one per symbol-time. +// bins[time][frequency] +// +ffts_t ffts(const std::vector &samples, int i0, int block, const char *why) +{ + assert(i0 >= 0); + assert(block > 1 && (block % 2) == 0); + + int nsamples = samples.size(); + int nbins = (block / 2) + 1; + int nblocks = (nsamples - i0) / block; + ffts_t bins(nblocks); + for (int si = 0; si < nblocks; si++) + { + bins[si].resize(nbins); + } + + Plan *p = get_plan(block, why); + fftwf_plan m_plan = p->fwd_; + +#if TIMING + float t0 = now(); +#endif + + // allocate our own b/c using p->m_in and p->m_out isn't thread-safe. + float *m_in = (float *)fftwf_malloc(sizeof(float) * p->n_); + fftwf_complex *m_out = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * ((p->n_ / 2) + 1)); + assert(m_in && m_out); + + // float *m_in = p->r_; + // fftw_complex *m_out = p->c_; + + for (int si = 0; si < nblocks; si++) + { + int off = i0 + si * block; + for (int i = 0; i < block; i++) + { + if (off + i < nsamples) + { + float x = samples[off + i]; + m_in[i] = x; + } + else + { + m_in[i] = 0; + } + } + + fftwf_execute_dft_r2c(m_plan, m_in, m_out); + + for (int bi = 0; bi < nbins; bi++) + { + float re = m_out[bi][0]; + float im = m_out[bi][1]; + std::complex c(re, im); + bins[si][bi] = c; + } + } + + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return bins; +} + +// +// do just one FFT on samples[i0..i0+block] +// real inputs, complex outputs. +// output has block points. +// +std::vector> one_fft_c( + const std::vector &samples, + int i0, + int block, + const char *why +) +{ + assert(i0 >= 0); + assert(block > 1); + + int nsamples = samples.size(); + + Plan *p = get_plan(block, why); + fftwf_plan m_plan = p->cfwd_; + +#if TIMING + float t0 = now(); +#endif + + fftwf_complex *m_in = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + fftwf_complex *m_out = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + assert(m_in && m_out); + + for (int i = 0; i < block; i++) + { + if (i0 + i < nsamples) + { + m_in[i][0] = samples[i0 + i]; // real + } + else + { + m_in[i][0] = 0; + } + m_in[i][1] = 0; // imaginary + } + + fftwf_execute_dft(m_plan, m_in, m_out); + + std::vector> out(block); + + float norm = 1.0 / sqrt(block); + for (int bi = 0; bi < block; bi++) + { + float re = m_out[bi][0]; + float im = m_out[bi][1]; + std::complex c(re, im); + c *= norm; + out[bi] = c; + } + + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return out; +} + +std::vector> one_fft_cc( + const std::vector> &samples, + int i0, + int block, + const char *why +) +{ + assert(i0 >= 0); + assert(block > 1); + + int nsamples = samples.size(); + + Plan *p = get_plan(block, why); + fftwf_plan m_plan = p->cfwd_; + +#if TIMING + float t0 = now(); +#endif + + fftwf_complex *m_in = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + fftwf_complex *m_out = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + assert(m_in && m_out); + + for (int i = 0; i < block; i++) + { + if (i0 + i < nsamples) + { + m_in[i][0] = samples[i0 + i].real(); + m_in[i][1] = samples[i0 + i].imag(); + } + else + { + m_in[i][0] = 0; + m_in[i][1] = 0; + } + } + + fftwf_execute_dft(m_plan, m_in, m_out); + + std::vector> out(block); + + // float norm = 1.0 / sqrt(block); + for (int bi = 0; bi < block; bi++) + { + float re = m_out[bi][0]; + float im = m_out[bi][1]; + std::complex c(re, im); + // c *= norm; + out[bi] = c; + } + + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return out; +} + +std::vector> one_ifft_cc( + const std::vector> &bins, + const char *why +) +{ + int block = bins.size(); + + Plan *p = get_plan(block, why); + fftwf_plan m_plan = p->crev_; + +#if TIMING + float t0 = now(); +#endif + + fftwf_complex *m_in = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + fftwf_complex *m_out = (fftwf_complex *)fftwf_malloc(block * sizeof(fftwf_complex)); + assert(m_in && m_out); + + for (int bi = 0; bi < block; bi++) + { + float re = bins[bi].real(); + float im = bins[bi].imag(); + m_in[bi][0] = re; + m_in[bi][1] = im; + } + + fftwf_execute_dft(m_plan, m_in, m_out); + + std::vector> out(block); + float norm = 1.0 / sqrt(block); + for (int i = 0; i < block; i++) + { + float re = m_out[i][0]; + float im = m_out[i][1]; + std::complex c(re, im); + c *= norm; + out[i] = c; + } + + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return out; +} + +std::vector one_ifft(const std::vector> &bins, const char *why) +{ + int nbins = bins.size(); + int block = (nbins - 1) * 2; + + Plan *p = get_plan(block, why); + fftwf_plan m_plan = p->rev_; + +#if TIMING + float t0 = now(); +#endif + + fftwf_complex *m_in = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * ((p->n_ / 2) + 1)); + float *m_out = (float *)fftwf_malloc(sizeof(float) * p->n_); + + for (int bi = 0; bi < nbins; bi++) + { + float re = bins[bi].real(); + float im = bins[bi].imag(); + m_in[bi][0] = re; + m_in[bi][1] = im; + } + + fftwf_execute_dft_c2r(m_plan, m_in, m_out); + + std::vector out(block); + for (int i = 0; i < block; i++) + { + out[i] = m_out[i]; + } + + fftwf_free(m_in); + fftwf_free(m_out); + +#if TIMING + p->time_ += now() - t0; +#endif + + return out; +} + +// +// return the analytic signal for signal x, +// just like scipy.signal.hilbert(), from which +// this code is copied. +// +// the return value is x + iy, where y is the hilbert transform of x. +// +std::vector> analytic(const std::vector &x, const char *why) +{ + ulong n = x.size(); + + std::vector> y = one_fft_c(x, 0, n, why); + assert(y.size() == n); + + // leave y[0] alone. + // float the first (positive) half of the spectrum. + // zero out the second (negative) half of the spectrum. + // y[n/2] is the nyquist bucket if n is even; leave it alone. + if ((n % 2) == 0) + { + for (ulong i = 1; i < n / 2; i++) + y[i] *= 2; + for (ulong i = n / 2 + 1; i < n; i++) + y[i] = 0; + } + else + { + for (ulong i = 1; i < (n + 1) / 2; i++) + y[i] *= 2; + for (ulong i = (n + 1) / 2; i < n; i++) + y[i] = 0; + } + + std::vector> z = one_ifft_cc(y, why); + + return z; +} + +// +// general-purpose shift x in frequency by hz. +// uses hilbert transform to avoid sidebands. +// but it does wrap around at 0 hz and the nyquist frequency. +// +// note analytic() does an FFT over the whole signal, which +// is expensive, and often re-used, but it turns out it +// isn't a big factor in overall run-time. +// +// like weakutil.py's freq_shift(). +// +std::vector hilbert_shift(const std::vector &x, float hz0, float hz1, int rate) +{ + // y = scipy.signal.hilbert(x) + std::vector> y = analytic(x, "hilbert_shift"); + assert(y.size() == x.size()); + + float dt = 1.0 / rate; + int n = x.size(); + + std::vector ret(n); + + for (int i = 0; i < n; i++) + { + // complex "local oscillator" at hz. + float hz = hz0 + (i / (float)n) * (hz1 - hz0); + std::complex lo = std::exp(std::complex(0.0, 2 * M_PI * hz * dt * i)); + ret[i] = (lo * y[i]).real(); + } + + return ret; +} + +void +fft_stats() +{ + for (int i = 0; i < nplans; i++) + { + Plan *p = plans[i]; + printf("%-13s %6d %9d %6.3f\n", + p->why_, + p->n_, + p->uses_, +#if TIMING + p->time_ +#else + 0.0 +#endif + ); + } +} + +} // namespace FT8 diff --git a/ft8/fft.h b/ft8/fft.h new file mode 100644 index 000000000..0d5b7d149 --- /dev/null +++ b/ft8/fft.h @@ -0,0 +1,46 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef FFT_H +#define FFT_H + +#include +#include +#include + +namespace FT8 +{ + class Plan; + Plan *get_plan(int n, const char *why); + + std::vector> one_fft(const std::vector &samples, int i0, int block, const char *why, Plan *p); + std::vector one_ifft(const std::vector> &bins, const char *why); + typedef std::vector>> ffts_t; + ffts_t ffts(const std::vector &samples, int i0, int block, const char *why); + std::vector> one_fft_c(const std::vector &samples, int i0, int block, const char *why); + std::vector> one_fft_cc(const std::vector> &samples, int i0, int block, const char *why); + std::vector> one_ifft_cc(const std::vector> &bins, const char *why); + std::vector> analytic(const std::vector &x, const char *why); + std::vector hilbert_shift(const std::vector &x, float hz0, float hz1, int rate); + +} // namespace FT8 + +#endif diff --git a/ft8/ft8.cpp b/ft8/ft8.cpp new file mode 100644 index 000000000..aeff70b45 --- /dev/null +++ b/ft8/ft8.cpp @@ -0,0 +1,3914 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +// +// An FT8 receiver in C++. +// +// Many ideas and protocol details borrowed from Franke +// and Taylor's WSJT-X code. +// +// Robert Morris, AB1HL +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "util.h" +#include "fft.h" +#include "ft8.h" +#include "libldpc.h" +#include "osd.h" + +namespace FT8 { + +// 1920-point FFT at 12000 samples/second +// 6.25 Hz spacing, 0.16 seconds/symbol +// encode chain: +// 77 bits +// append 14 bits CRC (for 91 bits) +// LDPC(174,91) yields 174 bits +// that's 58 3-bit FSK-8 symbols +// gray code each 3 bits +// insert three 7-symbol Costas sync arrays +// at symbol #s 0, 36, 72 of final signal +// thus: 79 FSK-8 symbols +// total transmission time is 12.64 seconds + +// tunable parameters +int nthreads = 8; // number of parallel threads, for multi-core +int npasses_one = 3; // number of spectral subtraction passes +int npasses_two = 3; // number of spectral subtraction passes +int ldpc_iters = 25; // how hard LDPC decoding should work +int snr_win = 7; // averaging window, in symbols, for SNR conversion +int snr_how = 3; // technique to measure "N" for SNR. 0 means median of the 8 tones. +float shoulder200 = 10; // for 200 sps bandpass filter +float shoulder200_extra = 0.0; // for bandpass filter +float second_hz_win = 3.5; // +/- hz +int second_hz_n = 8; // divide total window into this many pieces +float second_off_win = 0.5; // +/- search window in symbol-times +int second_off_n = 10; +int third_hz_n = 3; +float third_hz_win = 0.25; +int third_off_n = 4; +float third_off_win = 0.075; +float log_tail = 0.1; +float log_rate = 8.0; +int problt_how_noise = 0; +int problt_how_sig = 0; +int use_apriori = 1; +int use_hints = 2; // 1 means use all hints, 2 means just CQ hints +int win_type = 1; +int osd_depth = 0; // 6; // don't increase beyond 6, produces too much garbage +int osd_ldpc_thresh = 70; // demand this many correct LDPC parity bits before OSD +int ncoarse = 1; // number of offsets per hz produced by coarse() +int ncoarse_blocks = 1; +float tminus = 2.2; // start looking at 0.5 - tminus seconds +float tplus = 2.4; +int coarse_off_n = 4; +int coarse_hz_n = 4; +float already_hz = 27; +float overlap = 20; +int overlap_edges = 0; +float nyquist = 0.925; +int oddrate = 1; +float pass0_frac = 1.0; +int reduce_how = 2; +float go_extra = 3.5; +int do_reduce = 1; +int pass_threshold = 1; +int strength_how = 4; +int known_strength_how = 7; +int coarse_strength_how = 6; +float reduce_shoulder = -1; +float reduce_factor = 0.25; +float reduce_extra = 0; +float coarse_all = -1; +int second_count = 3; +int soft_phase_win = 2; +float subtract_ramp = 0.11; +extern int fftw_type; // fft.cc. MEASURE=0, ESTIMATE=64, PATIENT=32 +int soft_ones = 2; +int soft_pairs = 1; +int soft_triples = 1; +int do_second = 1; +int do_fine_hz = 1; +int do_fine_off = 1; +int do_third = 2; +float fine_thresh = 0.19; +int fine_max_off = 2; +int fine_max_tone = 4; +int known_sparse = 1; +float c_soft_weight = 7; +int c_soft_win = 2; +int bayes_how = 1; + +// +// return a Hamming window of length n. +// +std::vector hamming(int n) +{ + std::vector h(n); + for (int k = 0; k < n; k++) + { + h[k] = 0.54 - 0.46 * cos(2 * M_PI * k / (n - 1.0)); + } + return h; +} + +// +// blackman window +// +std::vector blackman(int n) +{ + std::vector h(n); + for (int k = 0; k < n; k++) + { + h[k] = 0.42 - 0.5 * cos(2 * M_PI * k / n) + 0.08 * cos(4 * M_PI * k / n); + } + return h; +} + +// +// symmetric blackman window +// +std::vector sym_blackman(int n) +{ + std::vector h(n); + for (int k = 0; k < (n / 2) + 1; k++) + { + h[k] = 0.42 - 0.5 * cos(2 * M_PI * k / n) + 0.08 * cos(4 * M_PI * k / n); + } + for (int k = n - 1; k >= (n / 2) + 1; --k) + { + h[k] = h[(n - 1) - k]; + } + return h; +} + +// +// blackman-harris window +// +std::vector blackmanharris(int n) +{ + float a0 = 0.35875; + float a1 = 0.48829; + float a2 = 0.14128; + float a3 = 0.01168; + std::vector h(n); + for (int k = 0; k < n; k++) + { + // symmetric + h[k] = + a0 - a1 * cos(2 * M_PI * k / (n - 1)) + a2 * cos(4 * M_PI * k / (n - 1)) - a3 * cos(6 * M_PI * k / (n - 1)); + // periodic + // h[k] = + // a0 + // - a1 * cos(2 * M_PI * k / n) + // + a2 * cos(4 * M_PI * k / n) + // - a3 * cos(6 * M_PI * k / n); + } + return h; +} + + + +// +// manage statistics for soft decoding, to help +// decide how likely each symbol is to be correct, +// to drive LDPC decoding. +// +// meaning of the how (problt_how) parameter: +// 0: gaussian +// 1: index into the actual distribution +// 2: do something complex for the tails. +// 3: index into the actual distribution plus gaussian for tails. +// 4: similar to 3. +// 5: laplace +// +class Stats +{ +public: + std::vector a_; + float sum_; + bool finalized_; + float mean_; // cached + float stddev_; // cached + float b_; // cached + int how_; + +public: + Stats(int how) : sum_(0), finalized_(false), how_(how) {} + + void add(float x) + { + a_.push_back(x); + sum_ += x; + finalized_ = false; + } + + void finalize() + { + finalized_ = true; + + int n = a_.size(); + mean_ = sum_ / n; + + float var = 0; + float bsum = 0; + for (int i = 0; i < n; i++) + { + float y = a_[i] - mean_; + var += y * y; + bsum += fabs(y); + } + var /= n; + stddev_ = sqrt(var); + b_ = bsum / n; + + // prepare for binary search to find where values lie + // in the distribution. + if (how_ != 0 && how_ != 5) + std::sort(a_.begin(), a_.end()); + } + + float mean() + { + if (!finalized_) + finalize(); + return mean_; + } + + float stddev() + { + if (!finalized_) + finalize(); + return stddev_; + } + + // fraction of distribution that's less than x. + // assumes normal distribution. + // this is PHI(x), or the CDF at x, + // or the integral from -infinity + // to x of the PDF. + float gaussian_problt(float x) + { + float SDs = (x - mean()) / stddev(); + float frac = 0.5 * (1.0 + erf(SDs / sqrt(2.0))); + return frac; + } + + // https://en.wikipedia.org/wiki/Laplace_distribution + // m and b from page 116 of Mark Owen's Practical Signal Processing. + float laplace_problt(float x) + { + float m = mean(); + + float cdf; + if (x < m) + { + cdf = 0.5 * exp((x - m) / b_); + } + else + { + cdf = 1.0 - 0.5 * exp(-(x - m) / b_); + } + + return cdf; + } + + // look into the actual distribution. + float problt(float x) + { + if (!finalized_) + finalize(); + + if (how_ == 0) + { + return gaussian_problt(x); + } + + if (how_ == 5) + { + return laplace_problt(x); + } + + // binary search. + auto it = std::lower_bound(a_.begin(), a_.end(), x); + int i = it - a_.begin(); + int n = a_.size(); + + if (how_ == 1) + { + // index into the distribution. + // works poorly for values that are off the ends + // of the distribution, since those are all + // mapped to 0.0 or 1.0, regardless of magnitude. + return i / (float)n; + } + + if (how_ == 2) + { + // use a kind of logistic regression for + // values near the edges of the distribution. + if (i < log_tail * n) + { + float x0 = a_[(int)(log_tail * n)]; + float y = 1.0 / (1.0 + exp(-log_rate * (x - x0))); + // y is 0..0.5 + y /= 5; + return y; + } + else if (i > (1 - log_tail) * n) + { + float x0 = a_[(int)((1 - log_tail) * n)]; + float y = 1.0 / (1.0 + exp(-log_rate * (x - x0))); + // y is 0.5..1 + // we want (1-log_tail)..1 + y -= 0.5; + y *= 2; + y *= log_tail; + y += (1 - log_tail); + return y; + } + else + { + return i / (float)n; + } + } + + if (how_ == 3) + { + // gaussian for values near the edge of the distribution. + if (i < log_tail * n) + { + return gaussian_problt(x); + } + else if (i > (1 - log_tail) * n) + { + return gaussian_problt(x); + } + else + { + return i / (float)n; + } + } + + if (how_ == 4) + { + // gaussian for values outside the distribution. + if (x < a_[0] || x > a_.back()) + { + return gaussian_problt(x); + } + else + { + return i / (float)n; + } + } + + return 0; + } +}; + +// a-priori probability of each of the 174 LDPC codeword +// bits being one. measured from reconstructed correct +// codewords, into ft8bits, then python bprob.py. +// from ft8-n4 +float apriori174[] = { + 0.47, + 0.32, + 0.29, + 0.37, + 0.52, + 0.36, + 0.40, + 0.42, + 0.42, + 0.53, + 0.44, + 0.44, + 0.39, + 0.46, + 0.39, + 0.38, + 0.42, + 0.43, + 0.45, + 0.51, + 0.42, + 0.48, + 0.31, + 0.45, + 0.47, + 0.53, + 0.59, + 0.41, + 0.03, + 0.50, + 0.30, + 0.26, + 0.40, + 0.65, + 0.34, + 0.49, + 0.46, + 0.49, + 0.69, + 0.40, + 0.45, + 0.45, + 0.60, + 0.46, + 0.43, + 0.49, + 0.56, + 0.45, + 0.55, + 0.51, + 0.46, + 0.37, + 0.55, + 0.52, + 0.56, + 0.55, + 0.50, + 0.01, + 0.19, + 0.70, + 0.88, + 0.75, + 0.75, + 0.74, + 0.73, + 0.18, + 0.71, + 0.35, + 0.60, + 0.58, + 0.36, + 0.60, + 0.38, + 0.50, + 0.02, + 0.01, + 0.98, + 0.48, + 0.49, + 0.54, + 0.50, + 0.49, + 0.53, + 0.50, + 0.49, + 0.49, + 0.51, + 0.51, + 0.51, + 0.47, + 0.50, + 0.53, + 0.51, + 0.46, + 0.51, + 0.51, + 0.48, + 0.51, + 0.52, + 0.50, + 0.52, + 0.51, + 0.50, + 0.49, + 0.53, + 0.52, + 0.50, + 0.46, + 0.47, + 0.48, + 0.52, + 0.50, + 0.49, + 0.51, + 0.49, + 0.49, + 0.50, + 0.50, + 0.50, + 0.50, + 0.51, + 0.50, + 0.49, + 0.49, + 0.55, + 0.49, + 0.51, + 0.48, + 0.55, + 0.49, + 0.48, + 0.50, + 0.51, + 0.50, + 0.51, + 0.50, + 0.51, + 0.53, + 0.49, + 0.54, + 0.50, + 0.48, + 0.49, + 0.46, + 0.51, + 0.51, + 0.52, + 0.49, + 0.51, + 0.49, + 0.51, + 0.50, + 0.49, + 0.50, + 0.50, + 0.47, + 0.49, + 0.52, + 0.49, + 0.51, + 0.49, + 0.48, + 0.52, + 0.48, + 0.49, + 0.47, + 0.50, + 0.48, + 0.50, + 0.49, + 0.51, + 0.51, + 0.51, + 0.49, +}; + +class FT8 +{ +public: + std::thread *th_; + + float min_hz_; + float max_hz_; + std::vector samples_; // input to each pass + std::vector nsamples_; // subtract from here + + int start_; // sample number of 0.5 seconds into samples[] + int rate_; // samples/second + float deadline_; // start time + budget + float final_deadline_; // keep going this long if no decodes + std::vector hints1_; + std::vector hints2_; + int pass_; + float down_hz_; + + static std::mutex cb_mu_; + cb_t cb_; // call-back into Python + + std::mutex hack_mu_; + int hack_size_; + int hack_off_; + int hack_len_; + float hack_0_; + float hack_1_; + const float *hack_data_; + std::vector> hack_bins_; + std::vector prevdecs_; + + Plan *plan32_; + + FT8(const std::vector &samples, + float min_hz, + float max_hz, + int start, int rate, + int hints1[], int hints2[], float deadline, + float final_deadline, cb_t cb, + std::vector prevdecs) + { + samples_ = samples; + min_hz_ = min_hz; + max_hz_ = max_hz; + prevdecs_ = prevdecs; + start_ = start; + rate_ = rate; + deadline_ = deadline; + final_deadline_ = final_deadline; + cb_ = cb; + down_hz_ = 0; + + for (int i = 0; hints1[i]; i++) + { + hints1_.push_back(hints1[i]); + } + for (int i = 0; hints2[i]; i++) + { + hints2_.push_back(hints2[i]); + } + + hack_size_ = -1; + hack_data_ = 0; + hack_off_ = -1; + hack_len_ = -1; + + plan32_ = 0; + } + + ~FT8() + { + } + + // strength of costas block of signal with tone 0 at bi0, + // and symbol zero at si0. + float one_coarse_strength(const ffts_t &bins, int bi0, int si0) + { + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + + assert(si0 >= 0 && si0 + 72 + 8 <= (int)bins.size()); + assert(bi0 >= 0 && bi0 + 8 <= (int)bins[0].size()); + + float sig = 0.0; + float noise = 0.0; + + if (coarse_all >= 0) + { + for (int si = 0; si < 79; si++) + { + float mx; + int mxi = -1; + float sum = 0; + for (int i = 0; i < 8; i++) + { + float x = std::abs(bins[si0 + si][bi0 + i]); + sum += x; + if (mxi < 0 || x > mx) + { + mxi = i; + mx = x; + } + } + if (si >= 0 && si < 7) + { + float x = std::abs(bins[si0 + si][bi0 + costas[si - 0]]); + sig += x; + noise += sum - x; + } + else if (si >= 36 && si < 36 + 7) + { + float x = std::abs(bins[si0 + si][bi0 + costas[si - 36]]); + sig += x; + noise += sum - x; + } + else if (si >= 72 && si < 72 + 7) + { + float x = std::abs(bins[si0 + si][bi0 + costas[si - 72]]); + sig += x; + noise += sum - x; + } + else + { + sig += coarse_all * mx; + noise += coarse_all * (sum - mx); + } + } + } + else + { + // coarse_all = -1 + // just costas symbols + for (int si = 0; si < 7; si++) + { + for (int bi = 0; bi < 8; bi++) + { + float x = 0; + x += std::abs(bins[si0 + si][bi0 + bi]); + x += std::abs(bins[si0 + 36 + si][bi0 + bi]); + x += std::abs(bins[si0 + 72 + si][bi0 + bi]); + if (bi == costas[si]) + { + sig += x; + } + else + { + noise += x; + } + } + } + } + + if (coarse_strength_how == 0) + { + return sig - noise; + } + else if (coarse_strength_how == 1) + { + return sig - noise / 7; + } + else if (coarse_strength_how == 2) + { + return sig / (noise / 7); + } + else if (coarse_strength_how == 3) + { + return sig / (sig + (noise / 7)); + } + else if (coarse_strength_how == 4) + { + return sig; + } + else if (coarse_strength_how == 5) + { + return sig / (sig + noise); + } + else if (coarse_strength_how == 6) + { + // this is it. + return sig / noise; + } + else + { + return 0; + } + } + + // return symbol length in samples at the given rate. + // insist on integer symbol lengths so that we can + // use whole FFT bins. + int blocksize(int rate) + { + // FT8 symbol length is 1920 at 12000 samples/second. + int xblock = 1920 / (12000.0 / rate); + assert(xblock == (int)xblock); + int block = xblock; + return block; + } + + class Strength + { + public: + float hz_; + int off_; + float strength_; // higher is better + }; + + // + // look for potential signals by searching FFT bins for Costas symbol + // blocks. returns a vector of candidate positions. + // + std::vector coarse(const ffts_t &bins, int si0, int si1) + { + int block = blocksize(rate_); + int nbins = bins[0].size(); + float bin_hz = rate_ / (float)block; + int min_bin = min_hz_ / bin_hz; + int max_bin = max_hz_ / bin_hz; + + std::vector strengths; + + for (int bi = min_bin; bi < max_bin && bi + 8 <= nbins; bi++) + { + std::vector sv; + for (int si = si0; si < si1 && si + 79 < (int)bins.size(); si++) + { + float s = one_coarse_strength(bins, bi, si); + Strength st; + st.strength_ = s; + st.hz_ = bi * 6.25; + st.off_ = si * block; + sv.push_back(st); + } + if (sv.size() < 1) + break; + + // save best ncoarse offsets, but require that they be separated + // by at least one symbol time. + + std::sort(sv.begin(), sv.end(), + [](const Strength &a, const Strength &b) -> bool + { return a.strength_ > b.strength_; }); + + strengths.push_back(sv[0]); + + int nn = 1; + for (int i = 1; nn < ncoarse && i < (int)sv.size(); i++) + { + if (std::abs(sv[i].off_ - sv[0].off_) > ncoarse_blocks * block) + { + strengths.push_back(sv[i]); + nn++; + } + } + } + + return strengths; + } + + // + // reduce the sample rate from arate to brate. + // center hz0..hz1 in the new nyquist range. + // but first filter to that range. + // sets delta_hz to hz moved down. + // + std::vector reduce_rate(const std::vector &a, float hz0, float hz1, + int arate, int brate, + float &delta_hz) + { + assert(brate < arate); + assert(hz1 - hz0 <= brate / 2); + + // the pass band is hz0..hz1 + // stop bands are 0..hz00 and hz11..nyquist. + float hz00, hz11; + + hz0 = std::max(0.0f, hz0 - reduce_extra); + hz1 = std::min(arate / 2.0f, hz1 + reduce_extra); + + if (reduce_shoulder > 0) + { + hz00 = hz0 - reduce_shoulder; + hz11 = hz1 + reduce_shoulder; + } + else + { + float mid = (hz0 + hz1) / 2; + hz00 = mid - (brate * reduce_factor); + hz00 = std::min(hz00, hz0); + hz11 = mid + (brate * reduce_factor); + hz11 = std::max(hz11, hz1); + } + + int alen = a.size(); + std::vector> bins1 = one_fft(a, 0, alen, + "reduce_rate1", 0); + int nbins1 = bins1.size(); + float bin_hz = arate / (float)alen; + + if (reduce_how == 2) + { + // band-pass filter the FFT output. + bins1 = fbandpass(bins1, bin_hz, + hz00, + hz0, + hz1, + hz11); + } + + if (reduce_how == 3) + { + for (int i = 0; i < nbins1; i++) + { + if (i < (hz0 / bin_hz)) + { + bins1[i] = 0; + } + else if (i > (hz1 / bin_hz)) + { + bins1[i] = 0; + } + } + } + + // shift down. + int omid = ((hz0 + hz1) / 2) / bin_hz; + int nmid = (brate / 4.0) / bin_hz; + + int delta = omid - nmid; // amount to move down + assert(delta < nbins1); + int blen = round(alen * (brate / (float)arate)); + std::vector> bbins(blen / 2 + 1); + for (int i = 0; i < (int)bbins.size(); i++) + { + if (delta > 0) + { + bbins[i] = bins1[i + delta]; + } + else + { + bbins[i] = bins1[i]; + } + } + + // use ifft to reduce the rate. + std::vector vvv = one_ifft(bbins, "reduce_rate2"); + + delta_hz = delta * bin_hz; + + return vvv; + } + + void go(int npasses) + { + // cache to avoid cost of fftw planner mutex. + plan32_ = get_plan(32, "cache32"); + + if (0) + { + fprintf(stderr, "go: %.0f .. %.0f, %.0f, rate=%d\n", + min_hz_, max_hz_, max_hz_ - min_hz_, rate_); + } + + // trim to make samples_ a good size for FFTW. + int nice_sizes[] = {18000, 18225, 36000, 36450, + 54000, 54675, 72000, 72900, + 144000, 145800, 216000, 218700, + 0}; + int nice = -1; + for (int i = 0; nice_sizes[i]; i++) + { + int sz = nice_sizes[i]; + if (fabs(samples_.size() - sz) < 0.05 * samples_.size()) + { + nice = sz; + break; + } + } + if (nice != -1) + { + samples_.resize(nice); + } + + assert(min_hz_ >= 0 && max_hz_ + 50 <= rate_ / 2); + + // can we reduce the sample rate? + int nrate = -1; + for (int xrate = 100; xrate < rate_; xrate += 100) + { + if (xrate < rate_ && (oddrate || (rate_ % xrate) == 0)) + { + if (((max_hz_ - min_hz_) + 50 + 2 * go_extra) < nyquist * (xrate / 2)) + { + nrate = xrate; + break; + } + } + } + + if (do_reduce && nrate > 0 && nrate < rate_ * 0.75) + { + // filter and reduce the sample rate from rate_ to nrate. + + float t0 = now(); + int osize = samples_.size(); + + float delta_hz; // how much it moved down + samples_ = reduce_rate(samples_, + min_hz_ - 3.1 - go_extra, + max_hz_ + 50 - 3.1 + go_extra, + rate_, nrate, delta_hz); + + float t1 = now(); + if (t1 - t0 > 0.1) + { + fprintf(stderr, "reduce oops, size %d -> %d, rate %d -> %d, took %.2f\n", + osize, + (int)samples_.size(), + rate_, + nrate, + t1 - t0); + } + if (0) + { + fprintf(stderr, "%.0f..%.0f, range %.0f, rate %d -> %d, delta hz %.0f, %.6f sec\n", + min_hz_, max_hz_, + max_hz_ - min_hz_, + rate_, nrate, delta_hz, t1 - t0); + } + + if (delta_hz > 0) + { + down_hz_ = delta_hz; // to adjust hz for Python. + min_hz_ -= down_hz_; + max_hz_ -= down_hz_; + for (int i = 0; i < (int)prevdecs_.size(); i++) + { + prevdecs_[i].hz0 -= delta_hz; + prevdecs_[i].hz1 -= delta_hz; + } + } + assert(max_hz_ + 50 < nrate / 2); + assert(min_hz_ >= 0); + + float ratio = nrate / (float)rate_; + rate_ = nrate; + start_ = round(start_ * ratio); + } + + int block = blocksize(rate_); + + // start_ is the sample number of 0.5 seconds, the nominal start time. + + // make sure there's at least tplus*rate_ samples after the end. + if (start_ + tplus * rate_ + 79 * block + block > samples_.size()) + { + int need = start_ + tplus * rate_ + 79 * block - samples_.size(); + + // round up to a whole second, to ease fft plan caching. + if ((need % rate_) != 0) + need += rate_ - (need % rate_); + + std::default_random_engine generator; + std::uniform_int_distribution distribution(0, samples_.size() - 1); + auto rnd = std::bind(distribution, generator); + + std::vector v(need); + for (int i = 0; i < need; i++) + { + // v[i] = 0; + v[i] = samples_[rnd()]; + } + samples_.insert(samples_.end(), v.begin(), v.end()); + } + + int si0 = (start_ - tminus * rate_) / block; + if (si0 < 0) + si0 = 0; + int si1 = (start_ + tplus * rate_) / block; + + // a copy from which to subtract. + nsamples_ = samples_; + + int any = 0; + for (int i = 0; i < (int)prevdecs_.size(); i++) + { + auto d = prevdecs_[i]; + if (d.hz0 >= min_hz_ && d.hz0 <= max_hz_) + { + // reconstruct correct 79 symbols from LDPC output. + std::vector re79 = recode(d.bits); + + // fine up hz/off again now that we have more samples + float best_hz = (d.hz0 + d.hz1) / 2.0; + float best_off = d.off; // seconds + search_both_known(samples_, rate_, re79, + best_hz, + best_off, + best_hz, best_off); + + // subtract from nsamples_. + subtract(re79, best_hz, best_hz, best_off); + any += 1; + } + } + if (any) + { + samples_ = nsamples_; + } + + for (pass_ = 0; pass_ < npasses; pass_++) + { + float total_remaining = deadline_ - now(); + float remaining = total_remaining / (npasses - pass_); + if (pass_ == 0) + { + remaining *= pass0_frac; + } + float deadline = now() + remaining; + + int new_decodes = 0; + samples_ = nsamples_; + + std::vector order; + + // + // search coarsely for Costas blocks. + // in fractions of bins in off and hz. + // + + // just do this once, re-use for every fractional fft_shift + // and down_v7_f() to 200 sps. + std::vector> bins = one_fft(samples_, 0, samples_.size(), + "go1", 0); + + for (int hz_frac_i = 0; hz_frac_i < coarse_hz_n; hz_frac_i++) + { + // shift down by hz_frac + float hz_frac = hz_frac_i * (6.25 / coarse_hz_n); + std::vector samples1; + if (hz_frac_i == 0) + { + samples1 = samples_; + } + else + { + samples1 = fft_shift_f(bins, rate_, hz_frac); + } + + for (int off_frac_i = 0; off_frac_i < coarse_off_n; off_frac_i++) + { + int off_frac = off_frac_i * (block / coarse_off_n); + ffts_t bins = ffts(samples1, off_frac, block, "go2"); + std::vector oo = coarse(bins, si0, si1); + for (int i = 0; i < (int)oo.size(); i++) + { + oo[i].hz_ += hz_frac; + oo[i].off_ += off_frac; + } + order.insert(order.end(), oo.begin(), oo.end()); + } + } + + // + // sort strongest-first. + // + std::sort(order.begin(), order.end(), + [](const Strength &a, const Strength &b) -> bool + { return a.strength_ > b.strength_; }); + + char already[2000]; // XXX + for (int i = 0; i < (int)(sizeof(already) / sizeof(already[0])); i++) + already[i] = 0; + + for (int ii = 0; ii < (int)order.size(); ii++) + { + float tt = now(); + if (ii > 0 && + tt > deadline && + (tt > deadline_ || new_decodes >= pass_threshold) && + (pass_ < npasses - 1 || tt > final_deadline_)) + { + break; + } + + float hz = order[ii].hz_; + if (already[(int)round(hz / already_hz)]) + continue; + int off = order[ii].off_; + int ret = one(bins, samples_.size(), hz, off); + if (ret) + { + if (ret == 2) + { + new_decodes++; + } + already[(int)round(hz / already_hz)] = 1; + } + } + } + } + + // + // what's the strength of the Costas sync blocks of + // the signal starting at hz and off? + // + float one_strength(const std::vector &samples200, float hz, int off) + { + int bin0 = round(hz / 6.25); + + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + int starts[] = {0, 36, 72}; + + float sig = 0; + float noise = 0; + + for (int which = 0; which < 3; which++) + { + int start = starts[which]; + for (int si = 0; si < 7; si++) + { + auto fft = one_fft(samples200, off + (si + start) * 32, 32, "one_strength", plan32_); + for (int bi = 0; bi < 8; bi++) + { + float x = std::abs(fft[bin0 + bi]); + if (bi == costas[si]) + { + sig += x; + } + else + { + noise += x; + } + } + } + } + + if (strength_how == 0) + { + return sig - noise; + } + else if (strength_how == 1) + { + return sig - noise / 7; + } + else if (strength_how == 2) + { + return sig / (noise / 7); + } + else if (strength_how == 3) + { + return sig / (sig + (noise / 7)); + } + else if (strength_how == 4) + { + return sig; + } + else if (strength_how == 5) + { + return sig / (sig + noise); + } + else if (strength_how == 6) + { + return sig / noise; + } + else + { + return 0; + } + } + + // + // given a complete known signal's symbols in syms, + // how strong is it? used to look for the best + // offset and frequency at which to subtract a + // decoded signal. + // + float one_strength_known( + const std::vector &samples, + int rate, + const std::vector &syms, + float hz, int off + ) + { + int block = blocksize(rate); + assert(syms.size() == 79); + + int bin0 = round(hz / 6.25); + + float sig = 0; + float noise = 0; + + float sum7 = 0; + std::complex prev = 0; + + for (int si = 0; si < 79; si += known_sparse) + { + auto fft = one_fft(samples, off + si * block, block, "one_strength_known", 0); + if (known_strength_how == 7) + { + std::complex c = fft[bin0 + syms[si]]; + if (si > 0) + { + sum7 += std::abs(c - prev); + } + prev = c; + } + else + { + for (int bi = 0; bi < 8; bi++) + { + float x = std::abs(fft[bin0 + bi]); + if (bi == syms[si]) + { + sig += x; + } + else + { + noise += x; + } + } + } + } + + if (known_strength_how == 0) + { + return sig - noise; + } + else if (known_strength_how == 1) + { + return sig - noise / 7; + } + else if (known_strength_how == 2) + { + return sig / (noise / 7); + } + else if (known_strength_how == 3) + { + return sig / (sig + (noise / 7)); + } + else if (known_strength_how == 4) + { + return sig; + } + else if (known_strength_how == 5) + { + return sig / (sig + noise); + } + else if (known_strength_how == 6) + { + return sig / noise; + } + else if (known_strength_how == 7) + { + return -sum7; + } + else + { + return 0; + } + } + + int search_time_fine( + const std::vector &samples200, + int off0, int offN, + float hz, + int gran, + float &str + ) + { + if (off0 < 0) + off0 = 0; + + // + // shift in frequency to put hz at 25. + // only shift the samples we need, both for speed, + // and try to always shift down the same number of samples + // to make it easier to cache fftw plans. + // + int len = (offN - off0) + 79 * 32 + 32; + if (off0 + len > (int)samples200.size()) + { + // len = samples200.size() - off0; + // don't provoke random-length FFTs. + return -1; + } + std::vector downsamples200 = shift200(samples200, off0, len, hz); + + int best_off = -1; + float best_sum = 0.0; + + for (int g = 0; g <= (offN - off0) && g + 79 * 32 <= len; g += gran) + { + float sum = one_strength(downsamples200, 25, g); + if (sum > best_sum || best_off == -1) + { + best_off = g; + best_sum = sum; + } + } + + str = best_sum; + assert(best_off >= 0); + return off0 + best_off; + } + + int search_time_fine_known( + const std::vector> &bins, + int rate, + const std::vector &syms, + int off0, int offN, + float hz, + int gran, + float &str + ) + { + if (off0 < 0) + off0 = 0; + + // nearest FFT bin center. + float hz0 = round(hz / 6.25) * 6.25; + + // move hz to hz0, so it is centered in a symbol-sized bin. + std::vector downsamples = fft_shift_f(bins, rate, hz - hz0); + + int best_off = -1; + int block = blocksize(rate); + float best_sum = 0.0; + + for (int g = off0; g <= offN; g += gran) + { + if (g >= 0 && g + 79 * block <= (int)downsamples.size()) + { + float sum = one_strength_known(downsamples, rate, syms, hz0, g); + if (sum > best_sum || best_off == -1) + { + best_off = g; + best_sum = sum; + } + } + } + + if (best_off < 0) + return -1; + + str = best_sum; + return best_off; + } + + // + // search for costas blocks in an MxN time/frequency grid. + // hz0 +/- hz_win in hz_inc increments. hz0 should be near 25. + // off0 +/- off_win in off_inc incremenents. + // + std::vector search_both( + const std::vector &samples200, + float hz0, + int hz_n, + float hz_win, + int off0, + int off_n, + int off_win + ) + { + assert(hz0 >= 25 - 6.25 / 2 && hz0 <= 25 + 6.25 / 2); + + std::vector strengths; + + float hz_inc = 2 * hz_win / hz_n; + int off_inc = round(2 * off_win / (float)off_n); + if (off_inc < 1) + off_inc = 1; + + for (float hz = hz0 - hz_win; hz <= hz0 + hz_win + 0.01; hz += hz_inc) + { + float str = 0; + int off = search_time_fine(samples200, off0 - off_win, off0 + off_win, hz, + off_inc, str); + if (off >= 0) + { + Strength st; + st.hz_ = hz; + st.off_ = off; + st.strength_ = str; + strengths.push_back(st); + } + } + + return strengths; + } + + void search_both_known( + const std::vector &samples, + int rate, + const std::vector &syms, + float hz0, + float off_secs0, // seconds + float &hz_out, float &off_out + ) + { + assert(hz0 >= 0 && hz0 + 50 < rate / 2); + + int off0 = round(off_secs0 * (float)rate); + + int off_win = third_off_win * blocksize(rate_); + if (off_win < 1) + off_win = 1; + int off_inc = trunc((2.0 * off_win) / (third_off_n - 1.0)); + if (off_inc < 1) + off_inc = 1; + + int got_best = 0; + float best_hz = 0; + int best_off = 0; + float best_strength = 0; + + std::vector> bins = one_fft(samples, 0, samples.size(), "stfk", 0); + + float hz_start, hz_inc, hz_end; + if (third_hz_n > 1) + { + hz_inc = (2.0 * third_hz_win) / (third_hz_n - 1.0); + hz_start = hz0 - third_hz_win; + hz_end = hz0 + third_hz_win; + } + else + { + hz_inc = 1; + hz_start = hz0; + hz_end = hz0; + } + + for (float hz = hz_start; hz <= hz_end + 0.0001; hz += hz_inc) + { + float strength = 0; + int off = search_time_fine_known(bins, rate, syms, + off0 - off_win, off0 + off_win, hz, + off_inc, strength); + if (off >= 0 && (got_best == 0 || strength > best_strength)) + { + got_best = 1; + best_hz = hz; + best_off = off; + best_strength = strength; + } + } + + if (got_best) + { + hz_out = best_hz; + off_out = best_off / (float)rate; + } + } + + // + // shift frequency by shifting the bins of one giant FFT. + // so no problem with phase mismatch &c at block boundaries. + // surprisingly fast at 200 samples/second. + // shifts *down* by hz. + // + std::vector fft_shift( + const std::vector &samples, + int off, + int len, + int rate, + float hz + ) + { + std::vector> bins; + + // horrible hack to avoid repeated FFTs on the same input. + hack_mu_.lock(); + if ((int)samples.size() == hack_size_ && samples.data() == hack_data_ && + off == hack_off_ && len == hack_len_ && + samples[0] == hack_0_ && samples[1] == hack_1_) + { + bins = hack_bins_; + } + else + { + bins = one_fft(samples, off, len, "fft_shift", 0); + hack_bins_ = bins; + hack_size_ = samples.size(); + hack_off_ = off; + hack_len_ = len; + hack_0_ = samples[0]; + hack_1_ = samples[1]; + hack_data_ = samples.data(); + } + hack_mu_.unlock(); + + return fft_shift_f(bins, rate, hz); + } + + // + // shift down by hz. + // + std::vector fft_shift_f( + const std::vector> &bins, + int rate, + float hz + ) + { + int nbins = bins.size(); + int len = (nbins - 1) * 2; + + float bin_hz = rate / (float)len; + int down = round(hz / bin_hz); + std::vector> bins1(nbins); + for (int i = 0; i < nbins; i++) + { + int j = i + down; + if (j >= 0 && j < nbins) + { + bins1[i] = bins[j]; + } + else + { + bins1[i] = 0; + } + } + std::vector out = one_ifft(bins1, "fft_shift"); + return out; + } + + // shift the frequency by a fraction of 6.25, + // to center hz on bin 4 (25 hz). + std::vector shift200( + const std::vector &samples200, + int off, + int len, + float hz + ) + { + if (std::abs(hz - 25) < 0.001 && off == 0 && len == (int)samples200.size()) + { + return samples200; + } + else + { + return fft_shift(samples200, off, len, 200, hz - 25.0); + } + // return hilbert_shift(samples200, hz - 25.0, hz - 25.0, 200); + } + + // returns a mini-FFT of 79 8-tone symbols. + ffts_t extract(const std::vector &samples200, float, int off) + { + + ffts_t bins3 = ffts(samples200, off, 32, "extract"); + + ffts_t m79(79); + for (int si = 0; si < 79; si++) + { + m79[si].resize(8); + if (si < (int)bins3.size()) + { + for (int bi = 0; bi < 8; bi++) + { + auto x = bins3[si][4 + bi]; + m79[si][bi] = x; + } + } + else + { + for (int bi = 0; bi < 8; bi++) + { + m79[si][bi] = 0; + } + } + } + + return m79; + } + + // + // m79 is a 79x8 array of complex. + // + ffts_t un_gray_code_c(const ffts_t &m79) + { + ffts_t m79a(79); + + int map[] = {0, 1, 3, 2, 6, 4, 5, 7}; + for (int si = 0; si < 79; si++) + { + m79a[si].resize(8); + for (int bi = 0; bi < 8; bi++) + { + m79a[si][map[bi]] = m79[si][bi]; + } + } + + return m79a; + } + + // + // m79 is a 79x8 array of float. + // + std::vector> + un_gray_code_r(const std::vector> &m79) + { + std::vector> m79a(79); + + int map[] = {0, 1, 3, 2, 6, 4, 5, 7}; + for (int si = 0; si < 79; si++) + { + m79a[si].resize(8); + for (int bi = 0; bi < 8; bi++) + { + m79a[si][map[bi]] = m79[si][bi]; + } + } + + return m79a; + } + + // + // normalize levels by windowed median. + // this helps, but why? + // + std::vector> convert_to_snr(const std::vector> &m79) + { + if (snr_how < 0 || snr_win < 0) + return m79; + + // + // for each symbol time, what's its "noise" level? + // + std::vector mm(79); + for (int si = 0; si < 79; si++) + { + std::vector v(8); + float sum = 0.0; + for (int bi = 0; bi < 8; bi++) + { + float x = m79[si][bi]; + v[bi] = x; + sum += x; + } + if (snr_how != 1) + std::sort(v.begin(), v.end()); + if (snr_how == 0) + { + // median + mm[si] = (v[3] + v[4]) / 2; + } + else if (snr_how == 1) + { + mm[si] = sum / 8; + } + else if (snr_how == 2) + { + // all but strongest tone. + mm[si] = (v[0] + v[1] + v[2] + v[3] + v[4] + v[5] + v[6]) / 7; + } + else if (snr_how == 3) + { + mm[si] = v[0]; // weakest tone + } + else if (snr_how == 4) + { + mm[si] = v[7]; // strongest tone + } + else if (snr_how == 5) + { + mm[si] = v[6]; // second-strongest tone + } + else + { + mm[si] = 1.0; + } + } + + // we're going to take a windowed average. + std::vector winwin; + if (snr_win > 0) + { + winwin = blackman(2 * snr_win + 1); + } + else + { + winwin.push_back(1.0); + } + + std::vector> n79(79); + + for (int si = 0; si < 79; si++) + { + float sum = 0; + for (int dd = si - snr_win; dd <= si + snr_win; dd++) + { + int wi = dd - (si - snr_win); + if (dd >= 0 && dd < 79) + { + sum += mm[dd] * winwin[wi]; + } + else if (dd < 0) + { + sum += mm[0] * winwin[wi]; + } + else + { + sum += mm[78] * winwin[wi]; + } + } + n79[si].resize(8); + for (int bi = 0; bi < 8; bi++) + { + n79[si][bi] = m79[si][bi] / sum; + } + } + + return n79; + } + + // + // normalize levels by windowed median. + // this helps, but why? + // + std::vector>> c_convert_to_snr( + const std::vector>> &m79 + ) + { + if (snr_how < 0 || snr_win < 0) + return m79; + + // + // for each symbol time, what's its "noise" level? + // + std::vector mm(79); + for (int si = 0; si < 79; si++) + { + std::vector v(8); + float sum = 0.0; + for (int bi = 0; bi < 8; bi++) + { + float x = std::abs(m79[si][bi]); + v[bi] = x; + sum += x; + } + if (snr_how != 1) + std::sort(v.begin(), v.end()); + if (snr_how == 0) + { + // median + mm[si] = (v[3] + v[4]) / 2; + } + else if (snr_how == 1) + { + mm[si] = sum / 8; + } + else if (snr_how == 2) + { + // all but strongest tone. + mm[si] = (v[0] + v[1] + v[2] + v[3] + v[4] + v[5] + v[6]) / 7; + } + else if (snr_how == 3) + { + mm[si] = v[0]; // weakest tone + } + else if (snr_how == 4) + { + mm[si] = v[7]; // strongest tone + } + else if (snr_how == 5) + { + mm[si] = v[6]; // second-strongest tone + } + else + { + mm[si] = 1.0; + } + } + + // we're going to take a windowed average. + std::vector winwin; + if (snr_win > 0) + { + winwin = blackman(2 * snr_win + 1); + } + else + { + winwin.push_back(1.0); + } + + std::vector>> n79(79); + + for (int si = 0; si < 79; si++) + { + float sum = 0; + for (int dd = si - snr_win; dd <= si + snr_win; dd++) + { + int wi = dd - (si - snr_win); + if (dd >= 0 && dd < 79) + { + sum += mm[dd] * winwin[wi]; + } + else if (dd < 0) + { + sum += mm[0] * winwin[wi]; + } + else + { + sum += mm[78] * winwin[wi]; + } + } + n79[si].resize(8); + for (int bi = 0; bi < 8; bi++) + { + n79[si][bi] = m79[si][bi] / sum; + } + } + + return n79; + } + + // + // statistics to decide soft probabilities, + // to drive LDPC decoder. + // distribution of strongest tones, and + // distribution of noise. + // + void make_stats( + const std::vector> &m79, + Stats &bests, + Stats &all + ) + { + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + + for (int si = 0; si < 79; si++) + { + if (si < 7 || (si >= 36 && si < 36 + 7) || si >= 72) + { + // Costas. + int ci; + if (si >= 72) + ci = si - 72; + else if (si >= 36) + ci = si - 36; + else + ci = si; + for (int bi = 0; bi < 8; bi++) + { + float x = m79[si][bi]; + all.add(x); + if (bi == costas[ci]) + { + bests.add(x); + } + } + } + else + { + float mx = 0; + for (int bi = 0; bi < 8; bi++) + { + float x = m79[si][bi]; + if (x > mx) + mx = x; + all.add(x); + } + bests.add(mx); + } + } + } + + // + // convert 79x8 complex FFT bins to magnitudes. + // + // exploits local phase coherence by decreasing magnitudes of bins + // whose phase is far from the phases of nearby strongest tones. + // + // relies on each tone being reasonably well centered in its FFT bin + // (in time and frequency) so that each tone completes an integer + // number of cycles and thus preserves phase from one symbol to the + // next. + // + std::vector> soft_c2m(const ffts_t &c79) + { + std::vector> m79(79); + std::vector raw_phases(79); // of strongest tone in each symbol time + for (int si = 0; si < 79; si++) + { + m79[si].resize(8); + int mxi = -1; + float mx; + float mx_phase; + for (int bi = 0; bi < 8; bi++) + { + float x = std::abs(c79[si][bi]); + m79[si][bi] = x; + if (mxi < 0 || x > mx) + { + mxi = bi; + mx = x; + mx_phase = std::arg(c79[si][bi]); // -pi .. pi + } + } + raw_phases[si] = mx_phase; + } + + if (soft_phase_win <= 0) + return m79; + + // phase around each symbol. + std::vector phases(79); + + // for each symbol time, median of nearby phases + for (int si = 0; si < 79; si++) + { + std::vector v; + for (int si1 = si - soft_phase_win; si1 <= si + soft_phase_win; si1++) + { + if (si1 >= 0 && si1 < 79) + { + float x = raw_phases[si1]; + v.push_back(x); + } + } + + // choose the phase that has the lowest total distance to other + // phases. like median but avoids -pi..pi wrap-around. + int n = v.size(); + int best = -1; + float best_score = 0; + for (int i = 0; i < n; i++) + { + float score = 0; + for (int j = 0; j < n; j++) + { + if (i == j) + continue; + float d = fabs(v[i] - v[j]); + if (d > M_PI) + d = 2 * M_PI - d; + score += d; + } + if (best == -1 || score < best_score) + { + best = i; + best_score = score; + } + } + phases[si] = v[best]; + } + + // project each tone against the median phase around that symbol time. + for (int si = 0; si < 79; si++) + { + for (int bi = 0; bi < 8; bi++) + { + float mag = std::abs(c79[si][bi]); + float angle = std::arg(c79[si][bi]); + float d = angle - phases[si]; + float factor = 0.1; + if (d < M_PI / 2 && d > -M_PI / 2) + { + factor = cos(d); + } + m79[si][bi] = factor * mag; + } + } + + return m79; + } + + // + // guess the probability that a bit is zero vs one, + // based on strengths of strongest tones that would + // give it those values. for soft LDPC decoding. + // + // returns log-likelihood, zero is positive, one is negative. + // + float bayes( + float best_zero, + float best_one, + int lli, + Stats &bests, + Stats &all + ) + { + float maxlog = 4.97; + float ll = 0; + + float pzero = 0.5; + float pone = 0.5; + if (use_apriori) + { + pzero = 1.0 - apriori174[lli]; + pone = apriori174[lli]; + } + + // + // Bayes combining rule normalization from: + // http://cs.wellesley.edu/~anderson/writing/naive-bayes.pdf + // + // a = P(zero)P(e0|zero)P(e1|zero) + // b = P(one)P(e0|one)P(e1|one) + // p = a / (a + b) + // + // also see Mark Owen's book Practical Signal Processing, + // Chapter 6. + // + + // zero + float a = pzero * + bests.problt(best_zero) * + (1.0 - all.problt(best_one)); + if (bayes_how == 1) + a *= all.problt(all.mean() + (best_zero - best_one)); + + // one + float b = pone * + bests.problt(best_one) * + (1.0 - all.problt(best_zero)); + if (bayes_how == 1) + b *= all.problt(all.mean() + (best_one - best_zero)); + + float p; + if (a + b == 0) + { + p = 0.5; + } + else + { + p = a / (a + b); + } + + if (1 - p == 0.0) + { + ll = maxlog; + } + else + { + ll = log(p / (1 - p)); + } + + if (ll > maxlog) + ll = maxlog; + if (ll < -maxlog) + ll = -maxlog; + + return ll; + } + + // + // c79 is 79x8 complex tones, before un-gray-coding. + // + void soft_decode(const ffts_t &c79, float ll174[]) + { + std::vector> m79(79); + + // m79 = absolute values of c79. + // still pre-un-gray-coding so we know which + // are the correct Costas tones. + m79 = soft_c2m(c79); + + m79 = convert_to_snr(m79); + + // statistics to decide soft probabilities. + // distribution of strongest tones, and + // distribution of noise. + Stats bests(problt_how_sig); + Stats all(problt_how_noise); + make_stats(m79, bests, all); + + m79 = un_gray_code_r(m79); + + int lli = 0; + for (int i79 = 0; i79 < 79; i79++) + { + if (i79 < 7 || (i79 >= 36 && i79 < 36 + 7) || i79 >= 72) + { + // Costas, skip + continue; + } + + // for each of the three bits, look at the strongest tone + // that would make it a zero, and the strongest tone that + // would make it a one. use Bayes to decide which is more + // likely, comparing each against the distribution of noise + // and the distribution of strongest tones. + // most-significant-bit first. + + for (int biti = 0; biti < 3; biti++) + { + // tone numbers that make this bit zero or one. + int zeroi[4]; + int onei[4]; + if (biti == 0) + { + // high bit + zeroi[0] = 0; + zeroi[1] = 1; + zeroi[2] = 2; + zeroi[3] = 3; + onei[0] = 4; + onei[1] = 5; + onei[2] = 6; + onei[3] = 7; + } + if (biti == 1) + { + // middle bit + zeroi[0] = 0; + zeroi[1] = 1; + zeroi[2] = 4; + zeroi[3] = 5; + onei[0] = 2; + onei[1] = 3; + onei[2] = 6; + onei[3] = 7; + } + if (biti == 2) + { + // low bit + zeroi[0] = 0; + zeroi[1] = 2; + zeroi[2] = 4; + zeroi[3] = 6; + onei[0] = 1; + onei[1] = 3; + onei[2] = 5; + onei[3] = 7; + } + + // strongest tone that would make this bit be zero. + int got_best_zero = 0; + float best_zero = 0; + for (int i = 0; i < 4; i++) + { + float x = m79[i79][zeroi[i]]; + if (got_best_zero == 0 || x > best_zero) + { + got_best_zero = 1; + best_zero = x; + } + } + + // strongest tone that would make this bit be one. + int got_best_one = 0; + float best_one = 0; + for (int i = 0; i < 4; i++) + { + float x = m79[i79][onei[i]]; + if (got_best_one == 0 || x > best_one) + { + got_best_one = 1; + best_one = x; + } + } + + float ll = bayes(best_zero, best_one, lli, bests, all); + + ll174[lli++] = ll; + } + } + assert(lli == 174); + } + + // + // c79 is 79x8 complex tones, before un-gray-coding. + // + void c_soft_decode(const ffts_t &c79x, float ll174[]) + { + ffts_t c79 = c_convert_to_snr(c79x); + + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + std::complex maxes[79]; + for (int i = 0; i < 79; i++) + { + std::complex m; + if (i < 7) + { + // Costas. + m = c79[i][costas[i]]; + } + else if (i >= 36 && i < 36 + 7) + { + // Costas. + m = c79[i][costas[i - 36]]; + } + else if (i >= 72) + { + // Costas. + m = c79[i][costas[i - 72]]; + } + else + { + int got = 0; + for (int j = 0; j < 8; j++) + { + if (got == 0 || std::abs(c79[i][j]) > std::abs(m)) + { + got = 1; + m = c79[i][j]; + } + } + } + maxes[i] = m; + } + + std::vector> m79(79); + for (int i = 0; i < 79; i++) + { + m79[i].resize(8); + for (int j = 0; j < 8; j++) + { + std::complex c = c79[i][j]; + int n = 0; + float sum = 0; + for (int k = i - c_soft_win; k <= i + c_soft_win; k++) + { + if (k < 0 || k >= 79) + continue; + if (k == i) + { + sum -= c_soft_weight * std::abs(c); + } + else + { + // we're expecting all genuine tones to have + // about the same phase and magnitude. + // so set m79[i][j] to the distance from the + // phase/magnitude predicted by surrounding + // genuine-looking tones. + std::complex c1 = maxes[k]; + std::complex d = c1 - c; + sum += std::abs(d); + } + n += 1; + } + m79[i][j] = 0 - (sum / n); + } + } + + // statistics to decide soft probabilities. + // distribution of strongest tones, and + // distribution of noise. + Stats bests(problt_how_sig); + Stats all(problt_how_noise); + make_stats(m79, bests, all); + + m79 = un_gray_code_r(m79); + + int lli = 0; + for (int i79 = 0; i79 < 79; i79++) + { + if (i79 < 7 || (i79 >= 36 && i79 < 36 + 7) || i79 >= 72) + { + // Costas, skip + continue; + } + + // for each of the three bits, look at the strongest tone + // that would make it a zero, and the strongest tone that + // would make it a one. use Bayes to decide which is more + // likely, comparing each against the distribution of noise + // and the distribution of strongest tones. + // most-significant-bit first. + + for (int biti = 0; biti < 3; biti++) + { + // tone numbers that make this bit zero or one. + int zeroi[4]; + int onei[4]; + if (biti == 0) + { + // high bit + zeroi[0] = 0; + zeroi[1] = 1; + zeroi[2] = 2; + zeroi[3] = 3; + onei[0] = 4; + onei[1] = 5; + onei[2] = 6; + onei[3] = 7; + } + if (biti == 1) + { + // middle bit + zeroi[0] = 0; + zeroi[1] = 1; + zeroi[2] = 4; + zeroi[3] = 5; + onei[0] = 2; + onei[1] = 3; + onei[2] = 6; + onei[3] = 7; + } + if (biti == 2) + { + // low bit + zeroi[0] = 0; + zeroi[1] = 2; + zeroi[2] = 4; + zeroi[3] = 6; + onei[0] = 1; + onei[1] = 3; + onei[2] = 5; + onei[3] = 7; + } + + // strongest tone that would make this bit be zero. + int got_best_zero = 0; + float best_zero = 0; + for (int i = 0; i < 4; i++) + { + float x = m79[i79][zeroi[i]]; + if (got_best_zero == 0 || x > best_zero) + { + got_best_zero = 1; + best_zero = x; + } + } + + // strongest tone that would make this bit be one. + int got_best_one = 0; + float best_one = 0; + for (int i = 0; i < 4; i++) + { + float x = m79[i79][onei[i]]; + if (got_best_one == 0 || x > best_one) + { + got_best_one = 1; + best_one = x; + } + } + + float ll = bayes(best_zero, best_one, lli, bests, all); + + ll174[lli++] = ll; + } + } + assert(lli == 174); + } + + // + // turn 79 symbol numbers into 174 bits. + // strip out the three Costas sync blocks, + // leaving 58 symbol numbers. + // each represents three bits. + // (all post-un-gray-code). + // str is per-symbol strength; must be positive. + // each returned element is < 0 for 1, > 0 for zero, + // scaled by str. + // + std::vector extract_bits(const std::vector &syms, const std::vector str) + { + assert(syms.size() == 79); + assert(str.size() == 79); + + std::vector bits; + for (int si = 0; si < 79; si++) + { + if (si < 7 || (si >= 36 && si < 36 + 7) || si >= 72) + { + // costas -- skip + } + else + { + bits.push_back((syms[si] & 4) == 0 ? str[si] : -str[si]); + bits.push_back((syms[si] & 2) == 0 ? str[si] : -str[si]); + bits.push_back((syms[si] & 1) == 0 ? str[si] : -str[si]); + } + } + + return bits; + } + + // decode successive pairs of symbols. exploits the likelyhood + // that they have the same phase, by summing the complex + // correlations for each possible pair and using the max. + void soft_decode_pairs( + const ffts_t &m79x, + float ll174[] + ) + { + ffts_t m79 = c_convert_to_snr(m79x); + + struct BitInfo + { + float zero; // strongest correlation that makes it zero + float one; // and one + }; + std::vector bitinfo(79 * 3); + for (int i = 0; i < (int)bitinfo.size(); i++) + { + bitinfo[i].zero = 0; + bitinfo[i].one = 0; + } + + Stats all(problt_how_noise); + Stats bests(problt_how_sig); + + int map[] = {0, 1, 3, 2, 6, 4, 5, 7}; // un-gray-code + + for (int si = 0; si < 79; si += 2) + { + float mx = 0; + float corrs[8 * 8]; + for (int s1 = 0; s1 < 8; s1++) + { + for (int s2 = 0; s2 < 8; s2++) + { + // sum up the correlations. + std::complex csum = m79[si][s1]; + if (si + 1 < 79) + csum += m79[si + 1][s2]; + float x = std::abs(csum); + + corrs[s1 * 8 + s2] = x; + if (x > mx) + mx = x; + + all.add(x); + + // first symbol + int i = map[s1]; + for (int bit = 0; bit < 3; bit++) + { + int bitind = (si + 0) * 3 + (2 - bit); + if ((i & (1 << bit))) + { + // symbol i would make this bit a one. + if (x > bitinfo[bitind].one) + { + bitinfo[bitind].one = x; + } + } + else + { + // symbol i would make this bit a zero. + if (x > bitinfo[bitind].zero) + { + bitinfo[bitind].zero = x; + } + } + } + + // second symbol + if (si + 1 < 79) + { + i = map[s2]; + for (int bit = 0; bit < 3; bit++) + { + int bitind = (si + 1) * 3 + (2 - bit); + if ((i & (1 << bit))) + { + // symbol i would make this bit a one. + if (x > bitinfo[bitind].one) + { + bitinfo[bitind].one = x; + } + } + else + { + // symbol i would make this bit a zero. + if (x > bitinfo[bitind].zero) + { + bitinfo[bitind].zero = x; + } + } + } + } + } + } + if (si == 0 || si == 36 || si == 72) + { + bests.add(corrs[3 * 8 + 1]); + } + else if (si == 2 || si == 38 || si == 74) + { + bests.add(corrs[4 * 8 + 0]); + } + else if (si == 4 || si == 40 || si == 76) + { + bests.add(corrs[6 * 8 + 5]); + } + else + { + bests.add(mx); + } + } + + int lli = 0; + for (int si = 0; si < 79; si++) + { + if (si < 7 || (si >= 36 && si < 36 + 7) || si >= 72) + { + // costas + continue; + } + for (int i = 0; i < 3; i++) + { + float best_zero = bitinfo[si * 3 + i].zero; + float best_one = bitinfo[si * 3 + i].one; + // ll174[lli++] = best_zero > best_one ? 4.99 : -4.99; + + float ll = bayes(best_zero, best_one, lli, bests, all); + + ll174[lli++] = ll; + } + } + assert(lli == 174); + } + + void soft_decode_triples( + const ffts_t &m79x, + float ll174[] + ) + { + ffts_t m79 = c_convert_to_snr(m79x); + + struct BitInfo + { + float zero; // strongest correlation that makes it zero + float one; // and one + }; + std::vector bitinfo(79 * 3); + for (int i = 0; i < (int)bitinfo.size(); i++) + { + bitinfo[i].zero = 0; + bitinfo[i].one = 0; + } + + Stats all(problt_how_noise); + Stats bests(problt_how_sig); + + int map[] = {0, 1, 3, 2, 6, 4, 5, 7}; // un-gray-code + + for (int si = 0; si < 79; si += 3) + { + float mx = 0; + float corrs[8 * 8 * 8]; + for (int s1 = 0; s1 < 8; s1++) + { + for (int s2 = 0; s2 < 8; s2++) + { + for (int s3 = 0; s3 < 8; s3++) + { + std::complex csum = m79[si][s1]; + if (si + 1 < 79) + csum += m79[si + 1][s2]; + if (si + 2 < 79) + csum += m79[si + 2][s3]; + float x = std::abs(csum); + + corrs[s1 * 64 + s2 * 8 + s3] = x; + if (x > mx) + mx = x; + + all.add(x); + + // first symbol + int i = map[s1]; + for (int bit = 0; bit < 3; bit++) + { + int bitind = (si + 0) * 3 + (2 - bit); + if ((i & (1 << bit))) + { + // symbol i would make this bit a one. + if (x > bitinfo[bitind].one) + { + bitinfo[bitind].one = x; + } + } + else + { + // symbol i would make this bit a zero. + if (x > bitinfo[bitind].zero) + { + bitinfo[bitind].zero = x; + } + } + } + + // second symbol + if (si + 1 < 79) + { + i = map[s2]; + for (int bit = 0; bit < 3; bit++) + { + int bitind = (si + 1) * 3 + (2 - bit); + if ((i & (1 << bit))) + { + // symbol i would make this bit a one. + if (x > bitinfo[bitind].one) + { + bitinfo[bitind].one = x; + } + } + else + { + // symbol i would make this bit a zero. + if (x > bitinfo[bitind].zero) + { + bitinfo[bitind].zero = x; + } + } + } + } + + // third symbol + if (si + 2 < 79) + { + i = map[s3]; + for (int bit = 0; bit < 3; bit++) + { + int bitind = (si + 2) * 3 + (2 - bit); + if ((i & (1 << bit))) + { + // symbol i would make this bit a one. + if (x > bitinfo[bitind].one) + { + bitinfo[bitind].one = x; + } + } + else + { + // symbol i would make this bit a zero. + if (x > bitinfo[bitind].zero) + { + bitinfo[bitind].zero = x; + } + } + } + } + } + } + } + + // costas: 3, 1, 4, 0, 6, 5, 2 + if (si == 0 || si == 36 || si == 72) + { + bests.add(corrs[3 * 64 + 1 * 8 + 4]); + } + else if (si == 3 || si == 39 || si == 75) + { + bests.add(corrs[0 * 64 + 6 * 8 + 5]); + } + else + { + bests.add(mx); + } + } + + int lli = 0; + for (int si = 0; si < 79; si++) + { + if (si < 7 || (si >= 36 && si < 36 + 7) || si >= 72) + { + // costas + continue; + } + for (int i = 0; i < 3; i++) + { + float best_zero = bitinfo[si * 3 + i].zero; + float best_one = bitinfo[si * 3 + i].one; + // ll174[lli++] = best_zero > best_one ? 4.99 : -4.99; + + float ll = bayes(best_zero, best_one, lli, bests, all); + + ll174[lli++] = ll; + } + } + assert(lli == 174); + } + + // + // given log likelyhood for each bit, try LDPC and OSD decoders. + // on success, puts corrected 174 bits into a174[]. + // + int decode(const float ll174[], int a174[], int use_osd, std::string &comment) + { + void ldpc_decode(float llcodeword[], int iters, int plain[], int *ok); + void ldpc_decode_log(float codeword[], int iters, int plain[], int *ok); + + int plain[174]; // will be 0/1 bits. + int ldpc_ok = 0; // 83 will mean success. + + ldpc_decode((float *)ll174, ldpc_iters, plain, &ldpc_ok); + + int ok_thresh = 83; // 83 is perfect + if (ldpc_ok >= ok_thresh) + { + // plain[] is 91 systematic data bits, 83 parity bits. + for (int i = 0; i < 174; i++) + { + a174[i] = plain[i]; + } + if (check_crc(a174)) + { + // success! + return 1; + } + } + + if (use_osd && osd_depth >= 0 && ldpc_ok >= osd_ldpc_thresh) + { + extern int osd_decode(float codeword[174], int depth, int out[91], int *); + extern void ldpc_encode(int plain[91], int codeword[174]); + + int oplain[91]; + int got_depth = -1; + int osd_ok = osd_decode((float *)ll174, osd_depth, oplain, &got_depth); + if (osd_ok) + { + // reconstruct all 174. + comment += "OSD-" + std::to_string(got_depth) + "-" + std::to_string(ldpc_ok); + ldpc_encode(oplain, a174); + return 1; + } + } + + return 0; + } + + // + // bandpass filter some FFT bins. + // smooth transition from stop-band to pass-band, + // so that it's not a brick-wall filter, so that it + // doesn't ring. + // + std::vector> fbandpass( + const std::vector> &bins0, + float bin_hz, + float low_outer, // start of transition + float low_inner, // start of flat area + float high_inner, // end of flat area + float high_outer // end of transition + ) + { + // assert(low_outer >= 0); + assert(low_outer <= low_inner); + assert(low_inner <= high_inner); + assert(high_inner <= high_outer); + // assert(high_outer <= bin_hz * bins0.size()); + + int nbins = bins0.size(); + std::vector> bins1(nbins); + + for (int i = 0; i < nbins; i++) + { + float ihz = i * bin_hz; + // cos(x)+flat+cos(x) taper + float factor; + if (ihz <= low_outer || ihz >= high_outer) + { + factor = 0; + } + else if (ihz >= low_outer && ihz < low_inner) + { + // rising shoulder +#if 1 + factor = (ihz - low_outer) / (low_inner - low_outer); // 0 .. 1 +#else + float theta = (ihz - low_outer) / (low_inner - low_outer); // 0 .. 1 + theta -= 1; // -1 .. 0 + theta *= 3.14159; // -pi .. 0 + factor = cos(theta); // -1 .. 1 + factor = (factor + 1) / 2; // 0 .. 1 +#endif + } + else if (ihz > high_inner && ihz <= high_outer) + { + // falling shoulder +#if 1 + factor = (high_outer - ihz) / (high_outer - high_inner); // 1 .. 0 +#else + float theta = (high_outer - ihz) / (high_outer - high_inner); // 1 .. 0 + theta = 1.0 - theta; // 0 .. 1 + theta *= 3.14159; // 0 .. pi + factor = cos(theta); // 1 .. -1 + factor = (factor + 1) / 2; // 1 .. 0 +#endif + } + else + { + factor = 1.0; + } + bins1[i] = bins0[i] * factor; + } + + return bins1; + } + + // + // move hz down to 25, filter+convert to 200 samples/second. + // + // like fft_shift(). one big FFT, move bins down and + // zero out those outside the band, then IFFT, + // then re-sample. + // + // XXX maybe merge w/ fft_shift() / shift200(). + // + std::vector down_v7(const std::vector &samples, float hz) + { + int len = samples.size(); + std::vector> bins = one_fft(samples, 0, len, "down_v7a", 0); + + return down_v7_f(bins, len, hz); + } + + std::vector down_v7_f(const std::vector> &bins, int len, float hz) + { + int nbins = bins.size(); + + float bin_hz = rate_ / (float)len; + int down = round((hz - 25) / bin_hz); + std::vector> bins1(nbins); + for (int i = 0; i < nbins; i++) + { + int j = i + down; + if (j >= 0 && j < nbins) + { + bins1[i] = bins[j]; + } + else + { + bins1[i] = 0; + } + } + + // now filter to fit in 200 samples/second. + + float low_inner = 25.0 - shoulder200_extra; + float low_outer = low_inner - shoulder200; + if (low_outer < 0) + low_outer = 0; + float high_inner = 75 - 6.25 + shoulder200_extra; + float high_outer = high_inner + shoulder200; + if (high_outer > 100) + high_outer = 100; + + bins1 = fbandpass(bins1, bin_hz, + low_outer, low_inner, + high_inner, high_outer); + + // convert back to time domain and down-sample to 200 samples/second. + int blen = round(len * (200.0 / rate_)); + std::vector> bbins(blen / 2 + 1); + for (int i = 0; i < (int)bbins.size(); i++) + bbins[i] = bins1[i]; + std::vector out = one_ifft(bbins, "down_v7b"); + + return out; + } + + // + // putative start of signal is at hz and symbol si0. + // + // return 2 if it decodes to a brand-new message. + // return 1 if it decodes but we've already seen it, + // perhaps in a different pass. + // return 0 if we could not decode. + // + // XXX merge with one_iter(). + // + int one(const std::vector> &bins, int len, float hz, int off) + { + // + // set up to search for best frequency and time offset. + // + + // + // move down to 25 hz and re-sample to 200 samples/second, + // i.e. 32 samples/symbol. + // + std::vector samples200 = down_v7_f(bins, len, hz); + + int off200 = round((off / (float)rate_) * 200.0); + + int ret = one_iter(samples200, off200, hz); + return ret; + } + + // return 2 if it decodes to a brand-new message. + // return 1 if it decodes but we've already seen it, + // perhaps in a different pass. + // return 0 if we could not decode. + int one_iter(const std::vector &samples200, int best_off, float hz_for_cb) + { + if (do_second) + { + std::vector strengths = + search_both(samples200, + 25, second_hz_n, second_hz_win, + best_off, second_off_n, second_off_win * 32); + // + // sort strongest-first. + // + std::sort(strengths.begin(), strengths.end(), + [](const Strength &a, const Strength &b) -> bool + { return a.strength_ > b.strength_; }); + + for (int i = 0; i < (int)strengths.size() && i < second_count; i++) + { + float hz = strengths[i].hz_; + int off = strengths[i].off_; + int ret = one_iter1(samples200, off, hz, hz_for_cb, hz_for_cb); + if (ret > 0) + { + return ret; + } + } + } + else + { + int ret = one_iter1(samples200, best_off, 25, hz_for_cb, hz_for_cb); + return ret; + } + + return 0; + } + + // + // estimate SNR, yielding numbers vaguely similar to WSJT-X. + // m79 is a 79x8 complex FFT output. + // + float guess_snr(const ffts_t &m79) + { + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + float noises = 0; + float signals = 0; + + for (int i = 0; i < 7; i++) + { + signals += std::abs(m79[i][costas[i]]); + signals += std::abs(m79[36 + i][costas[i]]); + signals += std::abs(m79[72 + i][costas[i]]); + noises += std::abs(m79[i][(costas[i] + 4) % 8]); + noises += std::abs(m79[36 + i][(costas[i] + 4) % 8]); + noises += std::abs(m79[72 + i][(costas[i] + 4) % 8]); + } + + for (int i = 0; i < 79; i++) + { + if (i < 7 || (i >= 36 && i < 36 + 7) || (i >= 72 && i < 72 + 7)) + continue; + std::vector v(8); + for (int j = 0; j < 8; j++) + { + v[j] = std::abs(m79[i][j]); + } + std::sort(v.begin(), v.end()); + signals += v[7]; // strongest tone, probably the signal + noises += (v[2] + v[3] + v[4]) / 3; + } + + noises /= 79; + signals /= 79; + + noises *= noises; // square yields power + signals *= signals; + + float raw = signals / noises; + raw -= 1; // turn (s+n)/n into s/n + if (raw < 0.1) + raw = 0.1; + raw /= (2500.0 / 2.7); // 2.7 hz noise b/w -> 2500 hz b/w + float snr = 10 * log10(raw); + snr += 5; + snr *= 1.4; + return snr; + } + + // + // compare phases of successive symbols to guess whether + // the starting offset is a little too high or low. + // we expect each symbol to have the same phase. + // an error in causes the phase to advance at a steady rate. + // so if hz is wrong, we expect the phase to advance + // or retard at a steady pace. + // an error in offset causes each symbol to start at + // a phase that depends on the symbol's frequency; + // a particular offset error causes a phase error + // that depends on frequency. + // hz0 is actual FFT bin number of m79[...][0] (always 4). + // + // the output adj_hz is relative to the FFT bin center; + // a positive number means the real signal seems to be + // a bit higher in frequency that the bin center. + // + // adj_off is the amount to change the offset, in samples. + // should be subtracted from offset. + // + void fine(const ffts_t &m79, int, float &adj_hz, float &adj_off) + { + adj_hz = 0.0; + adj_off = 0.0; + + // tone number for each of the 79 symbols. + int sym[79]; + float symval[79]; + float symphase[79]; + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + for (int i = 0; i < 79; i++) + { + if (i < 7) + { + sym[i] = costas[i]; + } + else if (i >= 36 && i < 36 + 7) + { + sym[i] = costas[i - 36]; + } + else if (i >= 72) + { + sym[i] = costas[i - 72]; + } + else + { + int mxj = -1; + float mx = 0; + for (int j = 0; j < 8; j++) + { + float x = std::abs(m79[i][j]); + if (mxj < 0 || x > mx) + { + mx = x; + mxj = j; + } + } + sym[i] = mxj; + } + symphase[i] = std::arg(m79[i][sym[i]]); + symval[i] = std::abs(m79[i][sym[i]]); + } + + float sum = 0; + float weight_sum = 0; + for (int i = 0; i < 79 - 1; i++) + { + float d = symphase[i + 1] - symphase[i]; + while (d > M_PI) + d -= 2 * M_PI; + while (d < -M_PI) + d += 2 * M_PI; + float w = symval[i]; + sum += d * w; + weight_sum += w; + } + float mean = sum / weight_sum; + + float err_rad = mean; // radians per symbol time + + float err_hz = (err_rad / (2 * M_PI)) / 0.16; // cycles per symbol time + + // if each symbol's phase is a bit more than we expect, + // that means the real frequency is a bit higher + // than we thought, so increase our estimate. + adj_hz = err_hz; + + // + // now think about offset error. + // + // the higher tones have many cycles per + // symbol -- e.g. tone 7 has 11 cycles + // in each symbol. a one- or two-sample + // offset error at such a high tone will + // change the phase by pi or more, + // which makes the phase-to-samples + // conversion ambiguous. so only try + // to distinguish early-ontime-late, + // not the amount. + // + int nearly = 0; + int nlate = 0; + float early = 0.0; + float late = 0.0; + for (int i = 1; i < 79; i++) + { + float ph0 = std::arg(m79[i - 1][sym[i - 1]]); + float ph = std::arg(m79[i][sym[i]]); + float d = ph - ph0; + d -= err_rad; // correct for hz error. + while (d > M_PI) + d -= 2 * M_PI; + while (d < -M_PI) + d += 2 * M_PI; + + // if off is correct, each symbol will have the same phase (modulo + // the above hz correction), since each FFT bin holds an integer + // number of cycles. + + // if off is too small, the phase is altered by the trailing part + // of the previous symbol. if the previous tone was lower, + // the phase won't have advanced as much as expected, and + // this symbol's phase will be lower than the previous phase. + // if the previous tone was higher, the phase will be more + // advanced than expected. thus off too small leads to + // a phase difference that's the reverse of the tone difference. + + // if off is too high, then the FFT started a little way into + // this symbol, which causes the phase to be advanced a bit. + // of course the previous symbol's phase was also advanced + // too much. if this tone is higher than the previous symbol, + // its phase will be more advanced than the previous. if + // less, less. + + // the point: if successive phases and tone differences + // are positively correlated, off is too high. if negatively, + // too low. + + // fine_max_tone: + // if late, ignore if a high tone, since ambiguous. + // if early, ignore if prev is a high tone. + + if (sym[i] > sym[i - 1]) + { + if (d > 0 && sym[i] <= fine_max_tone) + { + nlate++; + late += d / std::abs(sym[i] - sym[i - 1]); + } + if (d < 0 && sym[i - 1] <= fine_max_tone) + { + nearly++; + early += fabs(d) / std::abs(sym[i] - sym[i - 1]); + } + } + else if (sym[i] < sym[i - 1]) + { + if (d > 0 && sym[i - 1] <= fine_max_tone) + { + nearly++; + early += d / std::abs(sym[i] - sym[i - 1]); + } + if (d < 0 && sym[i] <= fine_max_tone) + { + nlate++; + late += fabs(d) / std::abs(sym[i] - sym[i - 1]); + } + } + } + + if (nearly > 0) + early /= nearly; + if (nlate > 0) + late /= nlate; + + // printf("early %d %.1f, late %d %.1f\n", nearly, early, nlate, late); + + // assumes 32 samples/symbol. + if (nearly > 2 * nlate) + { + adj_off = round(32 * early / fine_thresh); + if (adj_off > fine_max_off) + adj_off = fine_max_off; + } + else if (nlate > 2 * nearly) + { + adj_off = 0 - round(32 * late / fine_thresh); + if (fabs(adj_off) > fine_max_off) + adj_off = -fine_max_off; + } + } + + // + // the signal is at roughly 25 hz in samples200. + // + // return 2 if it decodes to a brand-new message. + // return 1 if it decodes but we've already seen it, + // perhaps in a different pass. + // return 0 if we could not decode. + // + int one_iter1( + const std::vector &samples200x, + int best_off, + float best_hz, + float hz0_for_cb, + float hz1_for_cb + ) + { + // put best_hz in the middle of bin 4, at 25.0. + std::vector samples200 = shift200(samples200x, 0, samples200x.size(), + best_hz); + + // mini 79x8 FFT. + ffts_t m79 = extract(samples200, 25, best_off); + + // look at symbol-to-symbol phase change to try + // to improve best_hz and best_off. + if (do_fine_hz || do_fine_off) + { + float adj_hz = 0; + float adj_off = 0; + fine(m79, 4, adj_hz, adj_off); + if (do_fine_hz == 0) + adj_hz = 0; + if (do_fine_off == 0) + adj_off = 0; + if (fabs(adj_hz) < 6.25 / 4 && fabs(adj_off) < 4) + { + best_hz += adj_hz; + best_off += round(adj_off); + if (best_off < 0) + best_off = 0; + samples200 = shift200(samples200x, 0, samples200x.size(), best_hz); + m79 = extract(samples200, 25, best_off); + } + } + + float ll174[174]; + + if (soft_ones) + { + if (soft_ones == 1) + { + soft_decode(m79, ll174); + } + else + { + c_soft_decode(m79, ll174); + } + int ret = try_decode(samples200, ll174, best_hz, best_off, + hz0_for_cb, hz1_for_cb, 1, "", m79); + if (ret) + return ret; + } + + if (soft_pairs) + { + float p174[174]; + soft_decode_pairs(m79, p174); + int ret = try_decode(samples200, p174, best_hz, best_off, + hz0_for_cb, hz1_for_cb, 1, "", m79); + if (ret) + return ret; + if (soft_ones == 0) + memcpy(ll174, p174, sizeof(ll174)); + } + + if (soft_triples) + { + float p174[174]; + soft_decode_triples(m79, p174); + int ret = try_decode(samples200, p174, best_hz, best_off, + hz0_for_cb, hz1_for_cb, 1, "", m79); + if (ret) + return ret; + } + + if (use_hints) + { + for (int hi = 0; hi < (int)hints1_.size(); hi++) + { + int h = hints1_[hi]; // 28-bit number, goes in ll174 0..28 + if (use_hints == 2 && h != 2) + { + // just CQ + continue; + } + float n174[174]; + for (int i = 0; i < 174; i++) + { + if (i < 28) + { + int bit = h & (1 << 27); + if (bit) + { + n174[i] = -4.97; + } + else + { + n174[i] = 4.97; + } + h <<= 1; + } + else + { + n174[i] = ll174[i]; + } + } + int ret = try_decode(samples200, n174, best_hz, best_off, + hz0_for_cb, hz1_for_cb, 0, "hint1", m79); + if (ret) + { + return ret; + } + } + } + + if (use_hints == 1) + { + for (int hi = 0; hi < (int)hints2_.size(); hi++) + { + int h = hints2_[hi]; // 28-bit number, goes in ll174 29:29+28 + float n174[174]; + for (int i = 0; i < 174; i++) + { + if (i >= 29 && i < 29 + 28) + { + int bit = h & (1 << 27); + if (bit) + { + n174[i] = -4.97; + } + else + { + n174[i] = 4.97; + } + h <<= 1; + } + else + { + n174[i] = ll174[i]; + } + } + int ret = try_decode(samples200, n174, best_hz, best_off, + hz0_for_cb, hz1_for_cb, 0, "hint2", m79); + if (ret) + { + return ret; + } + } + } + + return 0; + } + + // + // subtract a corrected decoded signal from nsamples_, + // perhaps revealing a weaker signal underneath, + // to be decoded in a subsequent pass. + // + // re79[] holds the error-corrected symbol numbers. + // + void subtract( + const std::vector re79, + float hz0, + float hz1, + float off_sec + ) + { + int block = blocksize(rate_); + float bin_hz = rate_ / (float)block; + int off0 = off_sec * rate_; + + float mhz = (hz0 + hz1) / 2.0; + int bin0 = round(mhz / bin_hz); + + // move nsamples so that signal is centered in bin0. + float diff0 = (bin0 * bin_hz) - hz0; + float diff1 = (bin0 * bin_hz) - hz1; + std::vector moved = hilbert_shift(nsamples_, diff0, diff1, rate_); + + ffts_t bins = ffts(moved, off0, block, "subtract"); + + if (bin0 + 8 > (int)bins[0].size()) + return; + if ((int)bins.size() < 79) + return; + + std::vector phases(79); + std::vector amps(79); + for (int i = 0; i < 79; i++) + { + int sym = bin0 + re79[i]; + std::complex c = bins[i][sym]; + phases[i] = std::arg(c); + + // FFT multiplies magnitudes by number of bins, + // or half the number of samples. + amps[i] = std::abs(c) / (block / 2.0); + } + + int ramp = round(block * subtract_ramp); + if (ramp < 1) + ramp = 1; + + // initial ramp part of first symbol. + { + int sym = bin0 + re79[0]; + float phase = phases[0]; + float amp = amps[0]; + float hz = 6.25 * sym; + float dtheta = 2 * M_PI / (rate_ / hz); // advance per sample + for (int jj = 0; jj < ramp; jj++) + { + float theta = phase + jj * dtheta; + float x = amp * cos(theta); + x *= jj / (float)ramp; + int iii = off0 + block * 0 + jj; + moved[iii] -= x; + } + } + + for (int si = 0; si < 79; si++) + { + int sym = bin0 + re79[si]; + + float phase = phases[si]; + float amp = amps[si]; + + float hz = 6.25 * sym; + float dtheta = 2 * M_PI / (rate_ / hz); // advance per sample + + // we've already done the first ramp for this symbol. + // now for the steady part between ramps. + for (int jj = ramp; jj < block - ramp; jj++) + { + float theta = phase + jj * dtheta; + float x = amp * cos(theta); + int iii = off0 + block * si + jj; + moved[iii] -= x; + } + + // now the two ramps, from us to the next symbol. + // we need to smoothly change the frequency, + // approximating wsjt-x's gaussian frequency shift, + // and also end up matching the next symbol's phase, + // which is often different from this symbol due + // to inaccuracies in hz or offset. + + // at start of this symbol's off-ramp. + float theta = phase + (block - ramp) * dtheta; + + float hz1; + float phase1; + if (si + 1 >= 79) + { + hz1 = hz; + phase1 = phase; + } + else + { + int sym1 = bin0 + re79[si + 1]; + hz1 = 6.25 * sym1; + phase1 = phases[si + 1]; + } + float dtheta1 = 2 * M_PI / (rate_ / hz1); + + // add this to dtheta for each sample, to gradually + // change the frequency. + float inc = (dtheta1 - dtheta) / (2.0 * ramp); + + // after we've applied all those inc's, what will the + // phase be at the end of the next symbol's initial ramp, + // if we don't do anything to correct it? + float actual = theta + dtheta * 2.0 * ramp + inc * 4.0 * ramp * ramp / 2.0; + + // what phase does the next symbol want to be at when + // its on-ramp finishes? + float target = phase1 + dtheta1 * ramp; + + // ??? + while (fabs(target - actual) > M_PI) + { + if (target < actual) + target += 2 * M_PI; + else + target -= 2 * M_PI; + } + + // adj is to be spread evenly over the off-ramp and on-ramp samples. + float adj = target - actual; + + int end = block + ramp; + if (si == 79 - 1) + end = block; + + for (int jj = block - ramp; jj < end; jj++) + { + int iii = off0 + block * si + jj; + float x = amp * cos(theta); + + // trail off to zero at the very end. + if (si == 79 - 1) + x *= 1.0 - ((jj - (block - ramp)) / (float)ramp); + + moved[iii] -= x; + + theta += dtheta; + dtheta += inc; + theta += adj / (2.0 * ramp); + } + } + + nsamples_ = hilbert_shift(moved, -diff0, -diff1, rate_); + } + + // + // decode, give to callback, and subtract. + // + // return 2 if it decodes to a brand-new message. + // return 1 if it decodes but we've already seen it, + // perhaps in a different pass. + // return 0 if we could not decode. + // + int try_decode( + const std::vector &samples200, + float ll174[174], + float best_hz, + int best_off_samples, + float hz0_for_cb, + float, + int use_osd, + const char *comment1, + const ffts_t &m79 + ) + { + int a174[174]; + std::string comment(comment1); + + if (decode(ll174, a174, use_osd, comment)) + { + // a174 is corrected 91 bits of plain message plus 83 bits of LDPC parity. + + // how many of the corrected 174 bits match the received signal in ll174? + int correct_bits = 0; + for (int i = 0; i < 174; i++) + { + if (ll174[i] < 0 && a174[i] == 1) + { + correct_bits += 1; + } + else if (ll174[i] > 0 && a174[i] == 0) + { + correct_bits += 1; + } + } + + // reconstruct correct 79 symbols from LDPC output. + std::vector re79 = recode(a174); + + if (do_third == 1) + { + // fine-tune offset and hz for better subtraction. + float best_off = best_off_samples / 200.0; + search_both_known(samples200, 200, re79, + best_hz, best_off, + best_hz, best_off); + best_off_samples = round(best_off * 200.0); + } + + // convert starting sample # from 200 samples/second back to rate_. + // also hz. + float best_off = best_off_samples / 200.0; // convert to seconds + best_hz = hz0_for_cb + (best_hz - 25.0); + + if (do_third == 2) + { + // fine-tune offset and hz for better subtraction. + search_both_known(samples_, rate_, re79, + best_hz, best_off, + best_hz, best_off); + } + + float snr = guess_snr(m79); + + if (cb_ != 0) + { + cb_mu_.lock(); + int ret = cb_(a174, best_hz + down_hz_, best_hz + down_hz_, + best_off, comment.c_str(), snr, pass_, correct_bits); + cb_mu_.unlock(); + if (ret == 2) + { + // a new decode. subtract it from nsamples_. + subtract(re79, best_hz, best_hz, best_off); + } + + return ret; + } + return 1; + } + else + { + return 0; + } + } + + // + // given 174 bits corrected by LDPC, work + // backwards to the symbols that must have + // been sent. + // used to help ensure that subtraction subtracts + // at the right place. + // + std::vector recode(int a174[]) + { + int i174 = 0; + int costas[] = {3, 1, 4, 0, 6, 5, 2}; + std::vector out79; + for (int i79 = 0; i79 < 79; i79++) + { + if (i79 < 7) + { + out79.push_back(costas[i79]); + } + else if (i79 >= 36 && i79 < 36 + 7) + { + out79.push_back(costas[i79 - 36]); + } + else if (i79 >= 72) + { + out79.push_back(costas[i79 - 72]); + } + else + { + int sym = (a174[i174 + 0] << 2) | (a174[i174 + 1] << 1) | (a174[i174 + 2] << 0); + i174 += 3; + // gray code + int map[] = {0, 1, 3, 2, 5, 6, 4, 7}; + sym = map[sym]; + out79.push_back(sym); + } + } + assert(out79.size() == 79); + assert(i174 == 174); + return out79; + } +}; + +std::mutex FT8::cb_mu_; + +// +// Python calls these. +// +void entry( + float xsamples[], + int nsamples, + int start, + int rate, + float min_hz, + float max_hz, + int hints1[], + int hints2[], + float time_left, + float total_time_left, + cb_t cb, + int nprevdecs, + struct cdecode *xprevdecs +) +{ + float t0 = now(); + float deadline = t0 + time_left; + float final_deadline = t0 + total_time_left; + + // decodes from previous runs, for subtraction. + std::vector prevdecs; + for (int i = 0; i < nprevdecs; i++) + { + prevdecs.push_back(xprevdecs[i]); + } + + std::vector samples(nsamples); + for (int i = 0; i < nsamples; i++) + { + samples[i] = xsamples[i]; + } + + if (min_hz < 0) + { + min_hz = 0; + } + if (max_hz > rate / 2) + { + max_hz = rate / 2; + } + float per = (max_hz - min_hz) / nthreads; + + std::vector thv; + + for (int i = 0; i < nthreads; i++) + { + float hz0 = min_hz + i * per; + if (i > 0 || overlap_edges) + hz0 -= overlap; + + float hz1 = min_hz + (i + 1) * per; + if (i != nthreads - 1 || overlap_edges) + hz1 += overlap; + + hz0 = std::max(hz0, 0.0f); + hz1 = std::min(hz1, (rate / 2.0f) - 50); + + FT8 *ft8 = new FT8( + samples, + hz0, + hz1, + start, + rate, + hints1, + hints2, + deadline, + final_deadline, + cb, + prevdecs + ); + + int npasses = nprevdecs > 0 ? npasses_two : npasses_one; + printf("FT8::entry: npasses: %d\n", npasses); + + ft8->th_ = new std::thread([ft8, npasses] () { ft8->go(npasses); }); + + thv.push_back(ft8); + } + + for (int i = 0; i < (int)thv.size(); i++) + { + thv[i]->th_->join(); + delete thv[i]->th_; + delete thv[i]; + } +} + +float set(char *param, char *val) +{ + struct sss + { + const char *name; + void *addr; + int type; // 0 int, 1 float + }; + struct sss params[] = + { + {"snr_win", &snr_win, 0}, + {"snr_how", &snr_how, 0}, + {"ldpc_iters", &ldpc_iters, 0}, + {"shoulder200", &shoulder200, 1}, + {"shoulder200_extra", &shoulder200_extra, 1}, + {"second_hz_n", &second_hz_n, 0}, + {"second_hz_win", &second_hz_win, 1}, + {"second_off_n", &second_off_n, 0}, + {"second_off_win", &second_off_win, 1}, + {"third_hz_n", &third_hz_n, 0}, + {"third_hz_win", &third_hz_win, 1}, + {"third_off_n", &third_off_n, 0}, + {"third_off_win", &third_off_win, 1}, + {"log_tail", &log_tail, 1}, + {"log_rate", &log_rate, 1}, + {"problt_how_noise", &problt_how_noise, 0}, + {"problt_how_sig", &problt_how_sig, 0}, + {"use_apriori", &use_apriori, 0}, + {"use_hints", &use_hints, 0}, + {"win_type", &win_type, 0}, + {"osd_depth", &osd_depth, 0}, + {"ncoarse", &ncoarse, 0}, + {"ncoarse_blocks", &ncoarse_blocks, 0}, + {"tminus", &tminus, 1}, + {"tplus", &tplus, 1}, + {"coarse_off_n", &coarse_off_n, 0}, + {"coarse_hz_n", &coarse_hz_n, 0}, + {"already_hz", &already_hz, 1}, + {"nthreads", &nthreads, 0}, + {"npasses_one", &npasses_one, 0}, + {"npasses_two", &npasses_two, 0}, + {"overlap", &overlap, 1}, + {"nyquist", &nyquist, 1}, + {"oddrate", &oddrate, 0}, + {"osd_ldpc_thresh", &osd_ldpc_thresh, 0}, + {"pass0_frac", &pass0_frac, 1}, + {"go_extra", &go_extra, 1}, + {"reduce_how", &reduce_how, 0}, + {"do_reduce", &do_reduce, 0}, + {"pass_threshold", &pass_threshold, 0}, + {"strength_how", &strength_how, 0}, + {"known_strength_how", &known_strength_how, 0}, + {"reduce_shoulder", &reduce_shoulder, 1}, + {"reduce_factor", &reduce_factor, 1}, + {"reduce_extra", &reduce_extra, 1}, + {"overlap_edges", &overlap_edges, 0}, + {"coarse_strength_how", &coarse_strength_how, 0}, + {"coarse_all", &coarse_all, 1}, + {"second_count", &second_count, 0}, + {"fftw_type", &fftw_type, 0}, + {"soft_phase_win", &soft_phase_win, 0}, + {"subtract_ramp", &subtract_ramp, 1}, + {"soft_pairs", &soft_pairs, 0}, + {"soft_triples", &soft_triples, 0}, + {"do_second", &do_second, 0}, + {"do_fine_hz", &do_fine_hz, 0}, + {"do_fine_off", &do_fine_off, 0}, + {"do_third", &do_third, 0}, + {"fine_thresh", &fine_thresh, 1}, + {"fine_max_off", &fine_max_off, 0}, + {"fine_max_tone", &fine_max_tone, 0}, + {"known_sparse", &known_sparse, 0}, + {"soft_ones", &soft_ones, 0}, + {"c_soft_weight", &c_soft_weight, 1}, + {"c_soft_win", &c_soft_win, 0}, + {"bayes_how", &bayes_how, 0}, + }; + int nparams = sizeof(params) / sizeof(params[0]); + + for (int i = 0; i < nparams; i++) + { + if (strcmp(param, params[i].name) == 0) + { + if (val[0]) + { + if (params[i].type == 0) + { + *(int *)params[i].addr = round(atof(val)); + } + else if (params[i].type == 1) + { + *(float *)params[i].addr = atof(val); + } + else + { + return 0; + } + } + if (params[i].type == 0) + { + return *(int *)params[i].addr; + } + else if (params[i].type == 1) + { + return *(float *)params[i].addr; + } + else + { + fprintf(stderr, "weird type %d\n", params[i].type); + return 0; + } + } + } + fprintf(stderr, "ft8.cc set(%s, %s) unknown parameter\n", param, val); + exit(1); + return 0; +} + +} // namespace FT8 diff --git a/ft8/ft8.h b/ft8/ft8.h new file mode 100644 index 000000000..5960a250a --- /dev/null +++ b/ft8/ft8.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#ifndef ft8_h +#define ft8_h + +namespace FT8 { +// Callback function to get the results +typedef int (*cb_t)( + int *a91, + float hz0, + float hz1, + float off, + const char *, + float snr, + int pass, + int correct_bits +); +// same as Python class CDECODE +// +struct cdecode +{ + float hz0; + float hz1; + float off; + int *bits; // 174 +}; + +void entry( + float xsamples[], + int nsamples, + int start, + int rate, + float min_hz, + float max_hz, + int hints1[], + int hints2[], + float time_left, + float total_time_left, + cb_t cb, + int, + struct cdecode * +); + +float set(char *param, char *val); +} // namespace FT8 + +#endif diff --git a/ft8/libldpc.cpp b/ft8/libldpc.cpp new file mode 100644 index 000000000..e0f1c1611 --- /dev/null +++ b/ft8/libldpc.cpp @@ -0,0 +1,747 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +// +// Low Density Parity Check (LDPC) decoder for new FT8. +// +// given a 174-bit codeword as an array of log-likelihood of zero, +// return a 174-bit corrected codeword, or zero-length array. +// first 91 bits are the (systematic) plain-text. +// codeword[i] = log ( P(x=0) / P(x=1) ) +// +// this is an implementation of the sum-product algorithm +// from Sarah Johnson's Iterative Error Correction book, and +// Bernhard Leiner's http://www.bernh.net/media/download/papers/ldpc.pdf +// +// cc -O3 libldpc.c -shared -fPIC -o libldpc.so +// + +#include +#include +#include +#include +#include "arrays.h" + +// float, long float, __float128 +#define REAL float + +namespace FT8 +{ +// +// does a 174-bit codeword pass the FT8's LDPC parity checks? +// returns the number of parity checks that passed. +// 83 means total success. +// +int ldpc_check(int codeword[]) +{ + int score = 0; + + // Nm[83][7] + for (int j = 0; j < 83; j++) + { + int x = 0; + for (int ii1 = 0; ii1 < 7; ii1++) + { + int i1 = Nm[j][ii1] - 1; + if (i1 >= 0) + { + x ^= codeword[i1]; + } + } + if (x == 0) + score++; + } + return score; +} + +// llcodeword is 174 log-likelihoods. +// plain is a return value, 174 ints, to be 0 or 1. +// iters is how hard to try. +// ok is the number of parity checks that worked out, +// ok == 83 means success. +void ldpc_decode(float llcodeword[], int iters, int plain[], int *ok) +{ + REAL m[83][174]; + REAL e[83][174]; + REAL codeword[174]; + int best_score = -1; + int best_cw[174]; + + // to translate from log-likelihood x to probability p, + // p = e**x / (1 + e**x) + // it's P(zero), not P(one). + for (int i = 0; i < 174; i++) + { + REAL ex = expl(llcodeword[i]); + REAL p = ex / (1.0 + ex); + codeword[i] = p; + } + + // m[j][i] tells the j'th check bit the P(zero) of + // each of its codeword inputs, based on check + // bits other than j. + for (int i = 0; i < 174; i++) + for (int j = 0; j < 83; j++) + m[j][i] = codeword[i]; + + // e[j][i]: each check j tells each codeword bit i the + // probability of the bit being zero based + // on the *other* bits contributing to that check. + for (int i = 0; i < 174; i++) + for (int j = 0; j < 83; j++) + e[j][i] = 0.0; + + for (int iter = 0; iter < iters; iter++) + { + + for (int j = 0; j < 83; j++) + { + for (int ii1 = 0; ii1 < 7; ii1++) + { + int i1 = Nm[j][ii1] - 1; + if (i1 < 0) + continue; + REAL a = 1.0; + for (int ii2 = 0; ii2 < 7; ii2++) + { + int i2 = Nm[j][ii2] - 1; + if (i2 >= 0 && i2 != i1) + { + // tmp ranges from 1.0 to -1.0, for + // definitely zero to definitely one. + float tmp = 1.0 - 2.0 * (1.0 - m[j][i2]); + a *= tmp; + } + } + // a ranges from 1.0 to -1.0, meaning + // bit i1 should be zero .. one. + // so e[j][i1] will be 0.0 .. 1.0 meaning + // bit i1 is one .. zero. + REAL tmp = 0.5 + 0.5 * a; + e[j][i1] = tmp; + } + } + + int cw[174]; + for (int i = 0; i < 174; i++) + { + REAL q0 = codeword[i]; + REAL q1 = 1.0 - q0; + for (int j = 0; j < 3; j++) + { + int j2 = Mn[i][j] - 1; + q0 *= e[j2][i]; + q1 *= 1.0 - e[j2][i]; + } + // REAL p = q0 / (q0 + q1); + REAL p; + if (q0 == 0.0) + { + p = 1.0; + } + else + { + p = 1.0 / (1.0 + (q1 / q0)); + } + cw[i] = (p <= 0.5); + } + int score = ldpc_check(cw); + if (score == 83) + { + for (int i = 0; i < 174; i++) + plain[i] = cw[i]; + *ok = 83; + return; + } + + if (score > best_score) + { + for (int i = 0; i < 174; i++) + best_cw[i] = cw[i]; + best_score = score; + } + + for (int i = 0; i < 174; i++) + { + for (int ji1 = 0; ji1 < 3; ji1++) + { + int j1 = Mn[i][ji1] - 1; + REAL q0 = codeword[i]; + REAL q1 = 1.0 - q0; + for (int ji2 = 0; ji2 < 3; ji2++) + { + int j2 = Mn[i][ji2] - 1; + if (j1 != j2) + { + q0 *= e[j2][i]; + q1 *= 1.0 - e[j2][i]; + } + } + // REAL p = q0 / (q0 + q1); + REAL p; + if (q0 == 0.0) + { + p = 1.0; + } + else + { + p = 1.0 / (1.0 + (q1 / q0)); + } + m[j1][i] = p; + } + } + } + + // decode didn't work, return best guess. + for (int i = 0; i < 174; i++) + plain[i] = best_cw[i]; + + *ok = best_score; +} + +// thank you Douglas Bagnall +// https://math.stackexchange.com/a/446411 +float fast_tanh(float x) +{ + if (x < -7.6) + { + return -0.999; + } + if (x > 7.6) + { + return 0.999; + } + float x2 = x * x; + float a = x * (135135.0f + x2 * (17325.0f + x2 * (378.0f + x2))); + float b = 135135.0f + x2 * (62370.0f + x2 * (3150.0f + x2 * 28.0f)); + return a / b; +} + +#if 0 +#define TANGRAN 0.01 +static float tanhtable[]; + +float +table_tanh(float x) +{ +int ind = (x - (-5.0)) / TANGRAN; +if(ind < 0){ +return -1.0; +} +if(ind >= 1000){ +return 1.0; +} +return tanhtable[ind]; +} +#endif + +// codeword is 174 log-likelihoods. +// plain is a return value, 174 ints, to be 0 or 1. +// iters is how hard to try. +// ok is the number of parity checks that worked out, +// ok == 83 means success. +void ldpc_decode_log(float codeword[], int iters, int plain[], int *ok) +{ + REAL m[83][174]; + REAL e[83][174]; + int best_score = -1; + int best_cw[174]; + + for (int i = 0; i < 174; i++) + for (int j = 0; j < 83; j++) + m[j][i] = codeword[i]; + + for (int i = 0; i < 174; i++) + for (int j = 0; j < 83; j++) + e[j][i] = 0.0; + + for (int iter = 0; iter < iters; iter++) + { + for (int j = 0; j < 83; j++) + { + for (int ii1 = 0; ii1 < 7; ii1++) + { + int i1 = Nm[j][ii1] - 1; + if (i1 < 0) + continue; + REAL a = 1.0; + for (int ii2 = 0; ii2 < 7; ii2++) + { + int i2 = Nm[j][ii2] - 1; + if (i2 >= 0 && i2 != i1) + { + // a *= table_tanh(m[j][i2] / 2.0); + a *= fast_tanh(m[j][i2] / 2.0); + } + } + REAL tmp; + if (a >= 0.999) + { + tmp = 7.6; + } + else if (a <= -0.999) + { + tmp = -7.6; + } + else + { + tmp = log((1 + a) / (1 - a)); + } + e[j][i1] = tmp; + } + } + + int cw[174]; + for (int i = 0; i < 174; i++) + { + REAL l = codeword[i]; + for (int j = 0; j < 3; j++) + l += e[Mn[i][j] - 1][i]; + cw[i] = (l <= 0.0); + } + int score = ldpc_check(cw); + if (score == 83) + { + for (int i = 0; i < 174; i++) + plain[i] = cw[i]; + *ok = 83; + return; + } + + if (score > best_score) + { + for (int i = 0; i < 174; i++) + best_cw[i] = cw[i]; + best_score = score; + } + + for (int i = 0; i < 174; i++) + { + for (int ji1 = 0; ji1 < 3; ji1++) + { + int j1 = Mn[i][ji1] - 1; + REAL l = codeword[i]; + for (int ji2 = 0; ji2 < 3; ji2++) + { + int j2 = Mn[i][ji2] - 1; + if (j1 != j2) + { + l += e[j2][i]; + } + } + m[j1][i] = l; + } + } + } + + // decode didn't work, return best guess. + for (int i = 0; i < 174; i++) + plain[i] = best_cw[i]; + + *ok = best_score; +} + +// +// check the FT8 CRC-14 +// + +void ft8_crc(int msg1[], int msglen, int out[14]) +{ + // the old FT8 polynomial for 12-bit CRC, 0xc06. + // int div[] = { 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0 }; + + // the new FT8 polynomial for 14-bit CRC, 0x2757, + // with leading 1 bit. + int div[] = {1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1}; + + // append 14 zeros. + int *msg = (int *)malloc(sizeof(int) * (msglen + 14)); + for (int i = 0; i < msglen + 14; i++) + { + if (i < msglen) + { + msg[i] = msg1[i]; + } + else + { + msg[i] = 0; + } + } + + for (int i = 0; i < msglen; i++) + { + if (msg[i]) + { + for (int j = 0; j < 15; j++) + { + msg[i + j] = (msg[i + j] + div[j]) % 2; + } + } + } + + for (int i = 0; i < 14; i++) + { + out[i] = msg[msglen + i]; + } + + free(msg); +} + +// rows is 91, cols is 174. +// m[174][2*91]. +// m's right half should start out as zeros. +// m's upper-right quarter will be the desired inverse. +void gauss_jordan(int rows, int cols, int m[174][2 * 91], int which[91], int *ok) +// gauss_jordan(int rows, int cols, int m[cols][2*rows], int which[rows], int *ok) +{ + *ok = 0; + + assert(rows == 91); + assert(cols == 174); + + for (int row = 0; row < rows; row++) + { + if (m[row][row] != 1) + { + for (int row1 = row + 1; row1 < cols; row1++) + { + if (m[row1][row] == 1) + { + // swap m[row] and m[row1] + for (int col = 0; col < 2 * rows; col++) + { + int tmp = m[row][col]; + m[row][col] = m[row1][col]; + m[row1][col] = tmp; + } + int tmp = which[row]; + which[row] = which[row1]; + which[row1] = tmp; + break; + } + } + } + if (m[row][row] != 1) + { + // could not invert + *ok = 0; + return; + } + // lazy creation of identity matrix in the upper-right quarter + m[row][rows + row] = (m[row][rows + row] + 1) % 2; + // now eliminate + for (int row1 = 0; row1 < cols; row1++) + { + if (row1 == row) + continue; + if (m[row1][row] != 0) + { + for (int col = 0; col < 2 * rows; col++) + { + m[row1][col] = (m[row1][col] + m[row][col]) % 2; + } + } + } + } + + *ok = 1; +} + +// # given a 174-bit codeword as an array of log-likelihood of zero, +// # return a 87-bit plain text, or zero-length array. +// # this is an implementation of the sum-product algorithm +// # from Sarah Johnson's Iterative Error Correction book. +// # codeword[i] = log ( P(x=0) / P(x=1) ) +// def ldpc_decode(self, codeword): +// # 174 codeword bits +// # 87 parity checks +// +// # Mji +// # each codeword bit i tells each parity check j +// # what the bit's log-likelihood of being 0 is +// # based on information *other* than from that +// # parity check. +// m = numpy.zeros((87, 174)) +// +// # Eji +// # each check j tells each codeword bit i the +// # log likelihood of the bit being zero based +// # on the *other* bits in that check. +// e = numpy.zeros((87, 174)) +// +// for i in range(0, 174): +// for j in range(0, 87): +// m[j][i] = codeword[i] +// +// for iter in range(0, 50): +// # messages from checks to bits. +// # for each parity check +// for j in range(0, 87): +// # for each bit mentioned in this parity check +// for i in Nm[j]: +// if i <= 0: +// continue +// a = 1 +// # for each other bit mentioned in this parity check +// for ii in Nm[j]: +// if ii != i: +// a *= math.tanh(m[j][ii-1] / 2.0) +// e[j][i-1] = math.log((1 + a) / (1 - a)) +// +// # decide if we are done -- compute the corrected codeword, +// # see if the parity check succeeds. +// cw = numpy.zeros(174, dtype=numpy.int32) +// for i in range(0, 174): +// # sum the log likelihoods for codeword bit i being 0. +// l = codeword[i] +// for j in Mn[i]: +// l += e[j-1][i] +// if l > 0: +// cw[i] = 0 +// else: +// cw[i] = 1 +// if self.ldpc_check(cw): +// # success! +// # it's a systematic code, though the plain-text bits are scattered. +// # collect them. +// decoded = cw[colorder] +// decoded = decoded[-87:] +// return decoded +// +// # messages from bits to checks. +// for i in range(0, 174): +// for j in Mn[i]: +// l = codeword[i] +// for jj in Mn[i]: +// if jj != j: +// l += e[jj-1][i] +// m[j-1][i] = l +// +// # could not decode. +// return numpy.array([]) + +#if 0 +static float tanhtable[] = { +-0.99990920, -0.99990737, -0.99990550, -0.99990359, -0.99990164, + -0.99989966, -0.99989763, -0.99989556, -0.99989345, -0.99989130, + -0.99988910, -0.99988686, -0.99988458, -0.99988225, -0.99987987, + -0.99987744, -0.99987496, -0.99987244, -0.99986986, -0.99986723, + -0.99986455, -0.99986182, -0.99985902, -0.99985618, -0.99985327, + -0.99985031, -0.99984728, -0.99984420, -0.99984105, -0.99983784, + -0.99983457, -0.99983122, -0.99982781, -0.99982434, -0.99982079, + -0.99981717, -0.99981348, -0.99980971, -0.99980586, -0.99980194, + -0.99979794, -0.99979386, -0.99978970, -0.99978545, -0.99978111, + -0.99977669, -0.99977218, -0.99976758, -0.99976289, -0.99975810, + -0.99975321, -0.99974823, -0.99974314, -0.99973795, -0.99973266, + -0.99972726, -0.99972175, -0.99971613, -0.99971040, -0.99970455, + -0.99969858, -0.99969249, -0.99968628, -0.99967994, -0.99967348, + -0.99966688, -0.99966016, -0.99965329, -0.99964629, -0.99963914, + -0.99963186, -0.99962442, -0.99961683, -0.99960910, -0.99960120, + -0.99959315, -0.99958493, -0.99957655, -0.99956799, -0.99955927, + -0.99955037, -0.99954129, -0.99953202, -0.99952257, -0.99951293, + -0.99950309, -0.99949305, -0.99948282, -0.99947237, -0.99946171, + -0.99945084, -0.99943975, -0.99942844, -0.99941690, -0.99940512, + -0.99939311, -0.99938085, -0.99936835, -0.99935559, -0.99934258, + -0.99932930, -0.99931576, -0.99930194, -0.99928784, -0.99927346, + -0.99925879, -0.99924382, -0.99922855, -0.99921297, -0.99919708, + -0.99918087, -0.99916432, -0.99914745, -0.99913024, -0.99911267, + -0.99909476, -0.99907648, -0.99905783, -0.99903881, -0.99901940, + -0.99899960, -0.99897940, -0.99895879, -0.99893777, -0.99891632, + -0.99889444, -0.99887212, -0.99884935, -0.99882612, -0.99880242, + -0.99877824, -0.99875358, -0.99872841, -0.99870274, -0.99867655, + -0.99864983, -0.99862258, -0.99859477, -0.99856640, -0.99853747, + -0.99850794, -0.99847782, -0.99844710, -0.99841575, -0.99838377, + -0.99835115, -0.99831787, -0.99828392, -0.99824928, -0.99821395, + -0.99817790, -0.99814112, -0.99810361, -0.99806533, -0.99802629, + -0.99798646, -0.99794582, -0.99790437, -0.99786208, -0.99781894, + -0.99777493, -0.99773003, -0.99768423, -0.99763750, -0.99758983, + -0.99754120, -0.99749159, -0.99744099, -0.99738936, -0.99733669, + -0.99728296, -0.99722815, -0.99717223, -0.99711519, -0.99705700, + -0.99699764, -0.99693708, -0.99687530, -0.99681228, -0.99674798, + -0.99668240, -0.99661549, -0.99654724, -0.99647761, -0.99640658, + -0.99633412, -0.99626020, -0.99618480, -0.99610788, -0.99602941, + -0.99594936, -0.99586770, -0.99578440, -0.99569942, -0.99561273, + -0.99552430, -0.99543409, -0.99534207, -0.99524820, -0.99515244, + -0.99505475, -0.99495511, -0.99485345, -0.99474976, -0.99464398, + -0.99453608, -0.99442601, -0.99431373, -0.99419919, -0.99408235, + -0.99396317, -0.99384159, -0.99371757, -0.99359107, -0.99346202, + -0.99333039, -0.99319611, -0.99305914, -0.99291942, -0.99277690, + -0.99263152, -0.99248323, -0.99233196, -0.99217766, -0.99202027, + -0.99185972, -0.99169596, -0.99152892, -0.99135853, -0.99118473, + -0.99100745, -0.99082663, -0.99064218, -0.99045404, -0.99026214, + -0.99006640, -0.98986674, -0.98966309, -0.98945538, -0.98924351, + -0.98902740, -0.98880698, -0.98858216, -0.98835285, -0.98811896, + -0.98788040, -0.98763708, -0.98738891, -0.98713578, -0.98687761, + -0.98661430, -0.98634574, -0.98607182, -0.98579245, -0.98550752, + -0.98521692, -0.98492053, -0.98461825, -0.98430995, -0.98399553, + -0.98367486, -0.98334781, -0.98301427, -0.98267411, -0.98232720, + -0.98197340, -0.98161259, -0.98124462, -0.98086936, -0.98048667, + -0.98009640, -0.97969840, -0.97929252, -0.97887862, -0.97845654, + -0.97802611, -0.97758719, -0.97713959, -0.97668317, -0.97621774, + -0.97574313, -0.97525917, -0.97476568, -0.97426247, -0.97374936, + -0.97322616, -0.97269268, -0.97214872, -0.97159408, -0.97102855, + -0.97045194, -0.96986402, -0.96926459, -0.96865342, -0.96803030, + -0.96739500, -0.96674729, -0.96608693, -0.96541369, -0.96472732, + -0.96402758, -0.96331422, -0.96258698, -0.96184561, -0.96108983, + -0.96031939, -0.95953401, -0.95873341, -0.95791731, -0.95708542, + -0.95623746, -0.95537312, -0.95449211, -0.95359412, -0.95267884, + -0.95174596, -0.95079514, -0.94982608, -0.94883842, -0.94783185, + -0.94680601, -0.94576057, -0.94469516, -0.94360942, -0.94250301, + -0.94137554, -0.94022664, -0.93905593, -0.93786303, -0.93664754, + -0.93540907, -0.93414721, -0.93286155, -0.93155168, -0.93021718, + -0.92885762, -0.92747257, -0.92606158, -0.92462422, -0.92316003, + -0.92166855, -0.92014933, -0.91860189, -0.91702576, -0.91542046, + -0.91378549, -0.91212037, -0.91042459, -0.90869766, -0.90693905, + -0.90514825, -0.90332474, -0.90146799, -0.89957745, -0.89765260, + -0.89569287, -0.89369773, -0.89166660, -0.88959892, -0.88749413, + -0.88535165, -0.88317089, -0.88095127, -0.87869219, -0.87639307, + -0.87405329, -0.87167225, -0.86924933, -0.86678393, -0.86427541, + -0.86172316, -0.85912654, -0.85648492, -0.85379765, -0.85106411, + -0.84828364, -0.84545560, -0.84257933, -0.83965418, -0.83667949, + -0.83365461, -0.83057887, -0.82745161, -0.82427217, -0.82103988, + -0.81775408, -0.81441409, -0.81101926, -0.80756892, -0.80406239, + -0.80049902, -0.79687814, -0.79319910, -0.78946122, -0.78566386, + -0.78180636, -0.77788807, -0.77390834, -0.76986654, -0.76576202, + -0.76159416, -0.75736232, -0.75306590, -0.74870429, -0.74427687, + -0.73978305, -0.73522225, -0.73059390, -0.72589741, -0.72113225, + -0.71629787, -0.71139373, -0.70641932, -0.70137413, -0.69625767, + -0.69106947, -0.68580906, -0.68047601, -0.67506987, -0.66959026, + -0.66403677, -0.65840904, -0.65270671, -0.64692945, -0.64107696, + -0.63514895, -0.62914516, -0.62306535, -0.61690930, -0.61067683, + -0.60436778, -0.59798200, -0.59151940, -0.58497988, -0.57836341, + -0.57166997, -0.56489955, -0.55805222, -0.55112803, -0.54412710, + -0.53704957, -0.52989561, -0.52266543, -0.51535928, -0.50797743, + -0.50052021, -0.49298797, -0.48538109, -0.47770001, -0.46994520, + -0.46211716, -0.45421643, -0.44624361, -0.43819931, -0.43008421, + -0.42189901, -0.41364444, -0.40532131, -0.39693043, -0.38847268, + -0.37994896, -0.37136023, -0.36270747, -0.35399171, -0.34521403, + -0.33637554, -0.32747739, -0.31852078, -0.30950692, -0.30043710, + -0.29131261, -0.28213481, -0.27290508, -0.26362484, -0.25429553, + -0.24491866, -0.23549575, -0.22602835, -0.21651806, -0.20696650, + -0.19737532, -0.18774621, -0.17808087, -0.16838105, -0.15864850, + -0.14888503, -0.13909245, -0.12927258, -0.11942730, -0.10955847, + -0.09966799, -0.08975778, -0.07982977, -0.06988589, -0.05992810, + -0.04995837, -0.03997868, -0.02999100, -0.01999733, -0.00999967, + -0.00000000, 0.00999967, 0.01999733, 0.02999100, 0.03997868, + 0.04995837, 0.05992810, 0.06988589, 0.07982977, 0.08975778, + 0.09966799, 0.10955847, 0.11942730, 0.12927258, 0.13909245, + 0.14888503, 0.15864850, 0.16838105, 0.17808087, 0.18774621, + 0.19737532, 0.20696650, 0.21651806, 0.22602835, 0.23549575, + 0.24491866, 0.25429553, 0.26362484, 0.27290508, 0.28213481, + 0.29131261, 0.30043710, 0.30950692, 0.31852078, 0.32747739, + 0.33637554, 0.34521403, 0.35399171, 0.36270747, 0.37136023, + 0.37994896, 0.38847268, 0.39693043, 0.40532131, 0.41364444, + 0.42189901, 0.43008421, 0.43819931, 0.44624361, 0.45421643, + 0.46211716, 0.46994520, 0.47770001, 0.48538109, 0.49298797, + 0.50052021, 0.50797743, 0.51535928, 0.52266543, 0.52989561, + 0.53704957, 0.54412710, 0.55112803, 0.55805222, 0.56489955, + 0.57166997, 0.57836341, 0.58497988, 0.59151940, 0.59798200, + 0.60436778, 0.61067683, 0.61690930, 0.62306535, 0.62914516, + 0.63514895, 0.64107696, 0.64692945, 0.65270671, 0.65840904, + 0.66403677, 0.66959026, 0.67506987, 0.68047601, 0.68580906, + 0.69106947, 0.69625767, 0.70137413, 0.70641932, 0.71139373, + 0.71629787, 0.72113225, 0.72589741, 0.73059390, 0.73522225, + 0.73978305, 0.74427687, 0.74870429, 0.75306590, 0.75736232, + 0.76159416, 0.76576202, 0.76986654, 0.77390834, 0.77788807, + 0.78180636, 0.78566386, 0.78946122, 0.79319910, 0.79687814, + 0.80049902, 0.80406239, 0.80756892, 0.81101926, 0.81441409, + 0.81775408, 0.82103988, 0.82427217, 0.82745161, 0.83057887, + 0.83365461, 0.83667949, 0.83965418, 0.84257933, 0.84545560, + 0.84828364, 0.85106411, 0.85379765, 0.85648492, 0.85912654, + 0.86172316, 0.86427541, 0.86678393, 0.86924933, 0.87167225, + 0.87405329, 0.87639307, 0.87869219, 0.88095127, 0.88317089, + 0.88535165, 0.88749413, 0.88959892, 0.89166660, 0.89369773, + 0.89569287, 0.89765260, 0.89957745, 0.90146799, 0.90332474, + 0.90514825, 0.90693905, 0.90869766, 0.91042459, 0.91212037, + 0.91378549, 0.91542046, 0.91702576, 0.91860189, 0.92014933, + 0.92166855, 0.92316003, 0.92462422, 0.92606158, 0.92747257, + 0.92885762, 0.93021718, 0.93155168, 0.93286155, 0.93414721, + 0.93540907, 0.93664754, 0.93786303, 0.93905593, 0.94022664, + 0.94137554, 0.94250301, 0.94360942, 0.94469516, 0.94576057, + 0.94680601, 0.94783185, 0.94883842, 0.94982608, 0.95079514, + 0.95174596, 0.95267884, 0.95359412, 0.95449211, 0.95537312, + 0.95623746, 0.95708542, 0.95791731, 0.95873341, 0.95953401, + 0.96031939, 0.96108983, 0.96184561, 0.96258698, 0.96331422, + 0.96402758, 0.96472732, 0.96541369, 0.96608693, 0.96674729, + 0.96739500, 0.96803030, 0.96865342, 0.96926459, 0.96986402, + 0.97045194, 0.97102855, 0.97159408, 0.97214872, 0.97269268, + 0.97322616, 0.97374936, 0.97426247, 0.97476568, 0.97525917, + 0.97574313, 0.97621774, 0.97668317, 0.97713959, 0.97758719, + 0.97802611, 0.97845654, 0.97887862, 0.97929252, 0.97969840, + 0.98009640, 0.98048667, 0.98086936, 0.98124462, 0.98161259, + 0.98197340, 0.98232720, 0.98267411, 0.98301427, 0.98334781, + 0.98367486, 0.98399553, 0.98430995, 0.98461825, 0.98492053, + 0.98521692, 0.98550752, 0.98579245, 0.98607182, 0.98634574, + 0.98661430, 0.98687761, 0.98713578, 0.98738891, 0.98763708, + 0.98788040, 0.98811896, 0.98835285, 0.98858216, 0.98880698, + 0.98902740, 0.98924351, 0.98945538, 0.98966309, 0.98986674, + 0.99006640, 0.99026214, 0.99045404, 0.99064218, 0.99082663, + 0.99100745, 0.99118473, 0.99135853, 0.99152892, 0.99169596, + 0.99185972, 0.99202027, 0.99217766, 0.99233196, 0.99248323, + 0.99263152, 0.99277690, 0.99291942, 0.99305914, 0.99319611, + 0.99333039, 0.99346202, 0.99359107, 0.99371757, 0.99384159, + 0.99396317, 0.99408235, 0.99419919, 0.99431373, 0.99442601, + 0.99453608, 0.99464398, 0.99474976, 0.99485345, 0.99495511, + 0.99505475, 0.99515244, 0.99524820, 0.99534207, 0.99543409, + 0.99552430, 0.99561273, 0.99569942, 0.99578440, 0.99586770, + 0.99594936, 0.99602941, 0.99610788, 0.99618480, 0.99626020, + 0.99633412, 0.99640658, 0.99647761, 0.99654724, 0.99661549, + 0.99668240, 0.99674798, 0.99681228, 0.99687530, 0.99693708, + 0.99699764, 0.99705700, 0.99711519, 0.99717223, 0.99722815, + 0.99728296, 0.99733669, 0.99738936, 0.99744099, 0.99749159, + 0.99754120, 0.99758983, 0.99763750, 0.99768423, 0.99773003, + 0.99777493, 0.99781894, 0.99786208, 0.99790437, 0.99794582, + 0.99798646, 0.99802629, 0.99806533, 0.99810361, 0.99814112, + 0.99817790, 0.99821395, 0.99824928, 0.99828392, 0.99831787, + 0.99835115, 0.99838377, 0.99841575, 0.99844710, 0.99847782, + 0.99850794, 0.99853747, 0.99856640, 0.99859477, 0.99862258, + 0.99864983, 0.99867655, 0.99870274, 0.99872841, 0.99875358, + 0.99877824, 0.99880242, 0.99882612, 0.99884935, 0.99887212, + 0.99889444, 0.99891632, 0.99893777, 0.99895879, 0.99897940, + 0.99899960, 0.99901940, 0.99903881, 0.99905783, 0.99907648, + 0.99909476, 0.99911267, 0.99913024, 0.99914745, 0.99916432, + 0.99918087, 0.99919708, 0.99921297, 0.99922855, 0.99924382, + 0.99925879, 0.99927346, 0.99928784, 0.99930194, 0.99931576, + 0.99932930, 0.99934258, 0.99935559, 0.99936835, 0.99938085, + 0.99939311, 0.99940512, 0.99941690, 0.99942844, 0.99943975, + 0.99945084, 0.99946171, 0.99947237, 0.99948282, 0.99949305, + 0.99950309, 0.99951293, 0.99952257, 0.99953202, 0.99954129, + 0.99955037, 0.99955927, 0.99956799, 0.99957655, 0.99958493, + 0.99959315, 0.99960120, 0.99960910, 0.99961683, 0.99962442, + 0.99963186, 0.99963914, 0.99964629, 0.99965329, 0.99966016, + 0.99966688, 0.99967348, 0.99967994, 0.99968628, 0.99969249, + 0.99969858, 0.99970455, 0.99971040, 0.99971613, 0.99972175, + 0.99972726, 0.99973266, 0.99973795, 0.99974314, 0.99974823, + 0.99975321, 0.99975810, 0.99976289, 0.99976758, 0.99977218, + 0.99977669, 0.99978111, 0.99978545, 0.99978970, 0.99979386, + 0.99979794, 0.99980194, 0.99980586, 0.99980971, 0.99981348, + 0.99981717, 0.99982079, 0.99982434, 0.99982781, 0.99983122, + 0.99983457, 0.99983784, 0.99984105, 0.99984420, 0.99984728, + 0.99985031, 0.99985327, 0.99985618, 0.99985902, 0.99986182, + 0.99986455, 0.99986723, 0.99986986, 0.99987244, 0.99987496, + 0.99987744, 0.99987987, 0.99988225, 0.99988458, 0.99988686, + 0.99988910, 0.99989130, 0.99989345, 0.99989556, 0.99989763, + 0.99989966, 0.99990164, 0.99990359, 0.99990550, 0.99990737, + 0.99990920, +}; +#endif + +} // namespace FT8 diff --git a/ft8/libldpc.h b/ft8/libldpc.h new file mode 100644 index 000000000..b2f9ce019 --- /dev/null +++ b/ft8/libldpc.h @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#ifndef libldpc_h +#define libldpc_h + +namespace FT8 { + +int ldpc_check(int codeword[]); +void ldpc_decode(float llcodeword[], int iters, int plain[], int *ok); +float fast_tanh(float x); +void ldpc_decode_log(float codeword[], int iters, int plain[], int *ok); +void ft8_crc(int msg1[], int msglen, int out[14]); +void gauss_jordan(int rows, int cols, int m[174][2 * 91], int which[91], int *ok); + + +} // namespace FT8 + +#endif // libldpc_h diff --git a/ft8/osd.cpp b/ft8/osd.cpp new file mode 100644 index 000000000..917ef90ea --- /dev/null +++ b/ft8/osd.cpp @@ -0,0 +1,489 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +// +// ordered statistics decoder for LDPC and new FT8. +// idea from wsjt-x. +// + +#include +#include +#include +#include +#include "libldpc.h" +#include "osd.h" + +namespace FT8 { +// +// check the FT8 CRC-14 +// +int check_crc(const int a91[91]) +{ + int aa[91]; + int non_zero = 0; + for (int i = 0; i < 91; i++) + { + if (i < 77) + { + aa[i] = a91[i]; + } + else + { + aa[i] = 0; + } + if (aa[i]) + non_zero++; + } + int out1[14]; + + // don't bother with all-zero messages. + if (non_zero == 0) + return 0; + + // why 82? why not 77? + ft8_crc(aa, 82, out1); + + for (int i = 0; i < 14; i++) + { + if (out1[i] != a91[91 - 14 + i]) + { + return 0; + } + } + return 1; +} + +// plain is 91 bits of plain-text. +// returns a 174-bit codeword. +// mimics wsjt-x's encode174_91.f90. +void ldpc_encode(int plain[91], int codeword[174]) +{ + // the systematic 91 bits. + for (int i = 0; i < 91; i++) + { + codeword[i] = plain[i]; + } + + // the 174-91 bits of redundancy. + for (int i = 0; i + 91 < 174; i++) + { + int sum = 0; + for (int j = 0; j < 91; j++) + { + sum += gen_sys[i + 91][j] * plain[j]; + codeword[i + 91] = sum % 2; + } + } +} + +// xplain is a possible original codeword. +// ll174 is what was received. +// ldpc-encode xplain; how close is the +// result to what we received? +float osd_score(int xplain[91], float ll174[174]) +{ + int xcode[174]; + ldpc_encode(xplain, xcode); + + float score = 0; + for (int i = 0; i < 174; i++) + { + if (xcode[i]) + { + // one-bit, expect ll to be negative. + score -= ll174[i] * 4.6; + } + else + { + // zero-bit, expect ll to be positive. + score += ll174[i] * 4.6; + } + } + + return -score; +} + +// does a decode look plausible? +int osd_check(const int plain[91]) +{ + int allzero = 1; + for (int i = 0; i < 91; i++) + { + if (plain[i] != 0) + { + allzero = 0; + } + } + if (allzero) + { + return 0; + } + + if (check_crc(plain) == 0) + { + return 0; + } + + return 1; +} + +void matmul(int a[91][91], int b[91], int c[91]) +{ + for (int i = 0; i < 91; i++) + { + int sum = 0; + for (int j = 0; j < 91; j++) + { + sum += a[i][j] * b[j]; + } + c[i] = sum % 2; + } +} + +// ordered statistics decoder for LDPC and new FT8. +// idea from wsjt-x. +// codeword[i] = log ( P(x=0) / P(x=1) ) +// codeword has 174 bits. +// first 91 bits are plaintext, remaining 83 are parity. +// returns 0 or 1, with decoded plain bits in out91[]. +// and actual depth used in *out_depth. +int osd_decode(float codeword[174], int depth, int out[91], int *out_depth) +{ + // strength = abs(codeword) + float strength[174]; + for (int i = 0; i < 174; i++) + { + float x = codeword[i]; + strength[i] = (x < 0 ? -x : x); + } + + // sort, strongest first; we'll use strongest 91. + std::vector which(174); + for (int i = 0; i < 174; i++) + which[i] = i; + std::sort(which.begin(), + which.end(), + [=](int a, int b) + { + return strength[a] > strength[b]; + }); + + // gen_sys[174 rows][91 cols] has a row per each of the 174 codeword bits, + // indicating how to generate it by xor with each of the 91 plain bits. + + // generator matrix, reordered strongest codeword bit first. + int b[174][91 * 2]; + for (int i = 0; i < 174; i++) + { + int ii = which[i]; + for (int j = 0; j < 91 * 2; j++) + { + if (j < 91) + { + b[i][j] = gen_sys[ii][j]; + } + else + { + b[i][j] = 0; + } + } + } + + int xwhich[174]; + for (int i = 0; i < 174; i++) + xwhich[i] = which[i]; + + int ok = 0; + gauss_jordan(91, 174, b, xwhich, &ok); + if (ok == 0) + { + fprintf(stderr, "gauss_jordan failed\n"); + } + + int gen1_inv[91][91]; + for (int i = 0; i < 91; i++) + { + for (int j = 0; j < 91; j++) + { + gen1_inv[i][j] = b[i][91 + j]; + } + } + + for (int i = 0; i < 174; i++) + { + which[i] = xwhich[i]; + } + + // y1 is the received bits, same order as gen1_inv, + // more or less strongest-first, converted from + // log-likihood to 0/1. + int y1[91]; + for (int i = 0; i < 91; i++) + { + int j = which[i]; + y1[i] = (codeword[j] < 0 ? 1 : 0); + } + + int best_plain[91]; + float best_score = 0; + int got_a_best = 0; + int best_depth = -1; + + // can we decode without flipping any bits? + int xplain[91]; + matmul(gen1_inv, y1, xplain); // also does mod 2 + + int osd_thresh = -500; + + float xscore = osd_score(xplain, codeword); + int ch = osd_check(xplain); + if (xscore < osd_thresh && ch) + { + if (got_a_best == 0 || xscore < best_score) + { + if (1) + { + // just accept this, since no bits had to be flipped. + memcpy(out, xplain, sizeof(xplain)); + *out_depth = 0; + return 1; + } + else + { + got_a_best = 1; + memcpy(best_plain, xplain, sizeof(best_plain)); + best_score = xscore; + best_depth = 0; + } + } + } + + // flip a few bits, see if decode works. + for (int ii = 0; ii < depth; ii++) + { + int i = 91 - 1 - ii; + y1[i] ^= 1; + matmul(gen1_inv, y1, xplain); + y1[i] ^= 1; + float xscore = osd_score(xplain, codeword); + int ch = osd_check(xplain); + if (xscore < osd_thresh && ch) + { + if (got_a_best == 0 || xscore < best_score) + { + got_a_best = 1; + memcpy(best_plain, xplain, sizeof(best_plain)); + best_score = xscore; + best_depth = ii; + } + } + } + + if (got_a_best) + { + memcpy(out, best_plain, sizeof(best_plain)); + *out_depth = best_depth; + return 1; + } + else + { + return 0; + } +} + +int gen_sys[174][91] = { + { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, }, + { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, }, + { 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, }, + { 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, }, + { 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, }, + { 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, }, + { 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, }, + { 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, }, + { 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, }, + { 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, }, + { 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, }, + { 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, }, + { 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, }, + { 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, }, + { 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, }, + { 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, }, + { 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, }, + { 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, }, + { 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, }, + { 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, }, + { 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, }, + { 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, }, + { 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, }, + { 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, }, + { 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, }, + { 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, }, + { 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, }, + { 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, }, + { 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, }, + { 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, }, + { 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, }, + { 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, }, + { 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, }, + { 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, }, + { 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, }, + { 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, }, + { 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, }, + { 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, }, + { 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, }, + { 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, }, + { 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, }, + { 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, }, + { 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, }, + { 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, }, + { 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, }, + { 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, }, + { 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, }, + { 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, }, + { 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, }, + { 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, }, + { 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, }, + { 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, }, + { 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, }, + { 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, }, + { 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, }, + { 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, }, + { 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, }, + { 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, }, + { 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, }, + { 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, }, + { 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, }, + { 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, }, + { 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, }, + { 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, }, + { 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, }, + { 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, }, + { 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, }, + { 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, }, + { 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, }, + { 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, }, + { 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, }, + { 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, }, + { 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, }, + { 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, }, + { 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, }, + { 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, }, + { 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, }, + { 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, }, + { 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, }, + { 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, }, + { 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, }, + { 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, }, + { 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, }, +}; + +} // namespace FT8 diff --git a/ft8/osd.h b/ft8/osd.h new file mode 100644 index 000000000..73a98c309 --- /dev/null +++ b/ft8/osd.h @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#ifndef osd_h +#define osd_h + +namespace FT8 { + + extern int gen_sys[174][91]; + int check_crc(const int a91[91]); + void ldpc_encode(int plain[91], int codeword[174]); + float osd_score(int xplain[91], float ll174[174]); + int osd_check(const int plain[91]); + void matmul(int a[91][91], int b[91], int c[91]); + int osd_decode(float codeword[174], int depth, int out[91], int *out_depth); + +} // namepsace FT8 + +#endif // osd_h + diff --git a/ft8/unpack.cpp b/ft8/unpack.cpp new file mode 100644 index 000000000..f30f85cf1 --- /dev/null +++ b/ft8/unpack.cpp @@ -0,0 +1,551 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include "unpack.h" +#include "util.h" + +namespace FT8 { + +// +// turn bits into a 128-bit integer. +// most significant bit first. +// +__int128 un(int a77[], int start, int len) +{ + __int128 x = 0; + + assert(len < (int)sizeof(x) * 8 && start >= 0 && start + len <= 77); + for (int i = 0; i < len; i++) + { + x <<= 1; + x |= a77[start + i]; + } + + return x; +} + +std::mutex hashes_mu; +std::map hashes12; +std::map hashes22; + +int ihashcall(std::string call, int m) +{ + while (call.size() > 0 && call[0] == ' ') + call.erase(0, 1); + while (call.size() > 0 && call[call.size() - 1] == ' ') + call.erase(call.end() - 1); + + const char *chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"; + + while (call.size() < 11) + call += " "; + + unsigned long long x = 0; + for (int i = 0; i < 11; i++) + { + int c = call[i]; + const char *p = strchr(chars, c); + assert(p); + int j = p - chars; + x = 38 * x + j; + } + x = x * 47055833459LL; + x = x >> (64 - m); + + return x; +} + +#define NGBASE (180 * 180) +#define NTOKENS 2063592 +#define MAX22 4194304 + +// +// turn 28 bits of packed call into the call +// +std::string unpackcall(int x) +{ + char tmp[64]; + + const char *c1 = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const char *c2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const char *c3 = "0123456789"; + const char *c4 = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + if (x == 0) + return "DE"; + if (x == 1) + return "QRZ"; + if (x == 2) + return "CQ"; + if (x <= 1002) + { + sprintf(tmp, "CQ %d", x - 3); + return tmp; + } + if (x <= 532443) + { + x -= 1003; + int ci1 = x / (27 * 27 * 27); + x %= 27 * 27 * 27; + int ci2 = x / (27 * 27); + x %= 27 * 27; + int ci3 = x / 27; + x %= 27; + int ci4 = x; + sprintf(tmp, "CQ %c%c%c%c", c4[ci1], c4[ci2], c4[ci3], c4[ci4]); + return tmp; + } + + if (x < NTOKENS) + { + return ""; + } + + x -= NTOKENS; + + if (x < MAX22) + { + // 22-bit hash... + std::string s; + hashes_mu.lock(); + if (hashes22.count(x) > 0) + { + s = hashes22[x]; + } + else + { + s = "<...22>"; + } + hashes_mu.unlock(); + return s; + } + + x -= MAX22; + + char a[7]; + + a[5] = c4[x % 27]; + x = x / 27; + a[4] = c4[x % 27]; + x = x / 27; + a[3] = c4[x % 27]; + x = x / 27; + a[2] = c3[x % 10]; + x = x / 10; + a[1] = c2[x % 36]; + x = x / 36; + a[0] = c1[x]; + + a[6] = '\0'; + + return a; +} + +// unpack a 15-bit grid square &c. +// 77-bit version, from inspection of packjt77.f90. +// ir is the bit after the two 28+1-bit callee/caller. +// i3 is the message type, usually 1. +std::string unpackgrid(int ng, int ir, int i3) +{ + (void) i3; + + if (ng < NGBASE) + { + // maidenhead grid system: + // latitude from south pole to north pole. + // longitude eastward from anti-meridian. + // first: 20 degrees longitude. + // second: 10 degrees latitude. + // third: 2 degrees longitude. + // fourth: 1 degree latitude. + // so there are 18*18*10*10 possibilities. + int x1 = ng / (18 * 10 * 10); + ng %= 18 * 10 * 10; + int x2 = ng / (10 * 10); + ng %= 10 * 10; + int x3 = ng / 10; + ng %= 10; + int x4 = ng; + char tmp[5]; + tmp[0] = 'A' + x1; + tmp[1] = 'A' + x2; + tmp[2] = '0' + x3; + tmp[3] = '0' + x4; + tmp[4] = '\0'; + return tmp; + } + + ng -= NGBASE; + + if (ng == 1) + { + return " "; // ??? + } + if (ng == 2) + { + return "RRR "; + } + if (ng == 3) + { + return "RR73"; + } + if (ng == 4) + { + return "73 "; + } + + int db = ng - 35; + char tmp[16]; + if (db >= 0) + { + sprintf(tmp, "%s+%02d", ir ? "R" : "", db); + } + else + { + sprintf(tmp, "%s-%02d", ir ? "R" : "", 0 - db); + } + return tmp; +} + +void remember_call(std::string call) +{ + hashes_mu.lock(); + if (call.size() >= 3 && call[0] != '<') + { + hashes22[ihashcall(call, 22)] = call; + hashes12[ihashcall(call, 12)] = call; + } + hashes_mu.unlock(); +} + +// +// i3 == 4 +// a call that doesn't fit in 28 bits. +// 12 bits: hash of a previous call +// 58 bits: 11 characters +// 1 bit: swap +// 2 bits: 1 RRR, 2 RR73, 3 73 +// 1 bit: 1 means CQ +std::string unpack_4(int a77[]) +{ + // 38 possible characters: + const char *chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"; + + long long n58 = un(a77, 12, 58); + char call[16]; + for (int i = 0; i < 11; i++) + { + call[10 - i] = chars[n58 % 38]; + n58 = n58 / 38; + } + call[11] = '\0'; + + remember_call(call); + + if (un(a77, 73, 1) == 1) + { + return std::string("CQ ") + call; + } + + int x12 = un(a77, 0, 12); + // 12-bit hash + hashes_mu.lock(); + std::string ocall; + if (hashes12.count(x12) > 0) + { + ocall = hashes12[x12]; + } + else + { + ocall = "<...12>"; + } + hashes_mu.unlock(); + + int swap = un(a77, 70, 1); + std::string msg; + if (swap) + { + msg = std::string(call) + " " + ocall; + } + else + { + msg = std::string(ocall) + " " + call; + } + + int suffix = un(a77, 71, 2); + if (suffix == 1) + { + msg += " RRR"; + } + else if (suffix == 2) + { + msg += " RR73"; + } + else if (suffix == 3) + { + msg += " 73"; + } + + return msg; +} + +// +// i3=1 +// +std::string unpack_1(int a77[]) +{ + // type 1: + // 28 call1 + // 1 P/R + // 28 call2 + // 1 P/R + // 1 ??? + // 15 grid + // 3 type + + int i = 0; + int call1 = un(a77, i, 28); + i += 28; + int rover1 = a77[i]; + i += 1; + int call2 = un(a77, i, 28); + i += 28; + int rover2 = a77[i]; + i += 1; + int ir = a77[i]; + i += 1; + int grid = un(a77, i, 15); + i += 15; + int i3 = un(a77, i, 3); + i += 3; + assert((i3 == 1 || i3 == 2) && i == 77); + + std::string call1text = trim(unpackcall(call1)); + std::string call2text = trim(unpackcall(call2)); + std::string gridtext = unpackgrid(grid, ir, i3); + + remember_call(call1text); + remember_call(call2text); + + const char *pr = (i3 == 1 ? "/R" : "/P"); + + return call1text + (rover1 ? pr : "") + " " + call2text + (rover2 ? pr : "") + " " + gridtext; +} + +// free text +// 71 bits, 13 characters, each one of 42 choices. +// reversed. +// details from wsjt-x's packjt77.f90 +std::string unpack_0_0(int a77[]) +{ + // the 42 possible characters. + const char *cc = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"; + __int128 x = un(a77, 0, 71); + std::string msg = "0123456789123"; + for (int i = 0; i < 13; i++) + { + msg[13 - 1 - i] = cc[x % 42]; + x = x / 42; + } + return msg; +} + +// ARRL RTTY Round-Up states/provinces +const char *ru_states[] = { + "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY", + "NB", "NS", "QC", "ON", "MB", "SK", "AB", "BC", "NWT", "NF", + "LB", "NU", "YT", "PEI", "DC"}; + +// i3=3 +// 3 TU; W9XYZ K1ABC R 579 MA 1 28 28 1 3 13 74 ARRL RTTY Roundup +// 1 TU +// 28 call1 +// 28 call2 +// 1 R +// 3 RST 529 to 599 +// 13 state/province/serialnumber +std::string unpack_3(int a77[]) +{ + int i = 0; + int tu = a77[i]; + i += 1; + int call1 = un(a77, i, 28); + i += 28; + int call2 = un(a77, i, 28); + i += 28; + int r = a77[i]; + i += 1; + int rst = un(a77, i, 3); + i += 3; + int serial = un(a77, i, 13); + i += 13; + + std::string call1text = unpackcall(call1); + std::string call2text = unpackcall(call2); + + rst = 529 + 10 * rst; + + int statei = serial - 8001; + std::string serialstr; + int nstates = sizeof(ru_states) / sizeof(ru_states[0]); + if (serial > 8000 && statei < nstates) + { + serialstr = ru_states[statei]; + } + else + { + char tmp[32]; + sprintf(tmp, "%04d", serial); + serialstr = tmp; + } + + std::string msg; + + if (tu) + { + msg += "TU; "; + } + msg += call1text + " " + call2text + " "; + if (r) + { + msg += "R "; + } + { + char tmp[16]; + sprintf(tmp, "%d ", rst); + msg += tmp; + } + msg += serialstr; + + remember_call(call1text); + remember_call(call2text); + + return msg; +} + +// ARRL Field Day sections +const char *sections[] = { + "AB ", "AK ", "AL ", "AR ", "AZ ", "BC ", "CO ", "CT ", "DE ", "EB ", + "EMA", "ENY", "EPA", "EWA", "GA ", "GTA", "IA ", "ID ", "IL ", "IN ", + "KS ", "KY ", "LA ", "LAX", "MAR", "MB ", "MDC", "ME ", "MI ", "MN ", + "MO ", "MS ", "MT ", "NC ", "ND ", "NE ", "NFL", "NH ", "NL ", "NLI", + "NM ", "NNJ", "NNY", "NT ", "NTX", "NV ", "OH ", "OK ", "ONE", "ONN", + "ONS", "OR ", "ORG", "PAC", "PR ", "QC ", "RI ", "SB ", "SC ", "SCV", + "SD ", "SDG", "SF ", "SFL", "SJV", "SK ", "SNJ", "STX", "SV ", "TN ", + "UT ", "VA ", "VI ", "VT ", "WCF", "WI ", "WMA", "WNY", "WPA", "WTX", + "WV ", "WWA", "WY ", "DX "}; + +// i3 = 0, n3 = 3 or 4: ARRL Field Day +// 0.3 WA9XYZ KA1ABC R 16A EMA 28 28 1 4 3 7 71 ARRL Field Day +// 0.4 WA9XYZ KA1ABC R 32A EMA 28 28 1 4 3 7 71 ARRL Field Day +std::string unpack_0_3(int a77[], int n3) +{ + int i = 0; + int call1 = un(a77, i, 28); + i += 28; + int call2 = un(a77, i, 28); + i += 28; + int R = un(a77, i, 1); + i += 1; + int n_transmitters = un(a77, i, 4); + if (n3 == 4) + n_transmitters += 16; + i += 4; + int clss = un(a77, i, 3); // class + i += 3; + int section = un(a77, i, 7); // ARRL section + i += 7; + + std::string msg; + msg += unpackcall(call1); + msg += " "; + msg += unpackcall(call2); + msg += " "; + if (R) + msg += "R "; + { + char tmp[16]; + sprintf(tmp, "%d%c ", n_transmitters + 1, clss + 'A'); + msg += tmp; + } + if (section - 1 >= 0 && section - 1 < (int)(sizeof(sections) / sizeof(sections[0]))) + { + msg += sections[section - 1]; + } + + return msg; +} + +// +// unpack an FT8 message. +// a77 is 91 bits -- 77 plus the 14-bit CRC. +// CRC and LDPC have already been checked. +// details from wsjt-x's packjt77.f90 and 77bit.txt. +// +std::string unpack(int a77[]) +{ + int i3 = un(a77, 74, 3); + int n3 = un(a77, 71, 3); + + if (i3 == 0 && n3 == 0) + { + // free text + return unpack_0_0(a77); + } + + if (i3 == 0 && (n3 == 3 || n3 == 4)) + { + // ARRL Field Day + return unpack_0_3(a77, n3); + } + + if (i3 == 1 || i3 == 2) + { + // ordinary message + return unpack_1(a77); + } + + if (i3 == 3) + { + // RTTY Round-Up + return unpack_3(a77); + } + + if (i3 == 4) + { + // call that doesn't fit in 28 bits + return unpack_4(a77); + } + + char tmp[64]; + sprintf(tmp, "UNK i3=%d n3=%d", i3, n3); + return tmp; +} + +} // namespace FT8 diff --git a/ft8/unpack.h b/ft8/unpack.h new file mode 100644 index 000000000..ea266cd20 --- /dev/null +++ b/ft8/unpack.h @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#ifndef unpack_h +#define unpack_h + +namespace FT8 { + +std::string unpack(int a91[]); + +} // namespace FT8 + +#endif diff --git a/ft8/util.cpp b/ft8/util.cpp new file mode 100644 index 000000000..2bd00fdca --- /dev/null +++ b/ft8/util.cpp @@ -0,0 +1,408 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include +#include +#include +#include "util.h" + +namespace FT8 { + +float now() +{ + struct timeval tv; + gettimeofday(&tv, 0); + return tv.tv_sec + tv.tv_usec / 1000000.0; +} + +void writewav(const std::vector &samples, const char *filename, int rate) +{ + float mx = 0; + for (ulong i = 0; i < samples.size(); i++) + { + mx = std::max(mx, std::abs(samples[i])); + } + std::vector v(samples.size()); + for (ulong i = 0; i < samples.size(); i++) + { + v[i] = (samples[i] / mx) * 0.95; + } + + SF_INFO sf; + sf.channels = 1; + sf.samplerate = rate; + sf.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16; + SNDFILE *f = sf_open(filename, SFM_WRITE, &sf); + assert(f); + sf_write_float(f, v.data(), v.size()); + sf_write_sync(f); + sf_close(f); +} + +std::vector readwav(const char *filename, int &rate_out) +{ + SF_INFO info; + memset(&info, 0, sizeof(info)); + SNDFILE *sf = sf_open(filename, SFM_READ, &info); + if (sf == 0) + { + fprintf(stderr, "cannot open %s\n", filename); + exit(1); // XXX + } + rate_out = info.samplerate; + + std::vector out; + + while (1) + { + float buf[512]; + int n = sf_read_float(sf, buf, 512); + if (n <= 0) + break; + for (int i = 0; i < n; i++) + { + out.push_back(buf[i]); + } + } + + sf_close(sf); + + return out; +} + +void writetxt(std::vector v, const char *filename) +{ + FILE *fp = fopen(filename, "w"); + if (fp == 0) + { + fprintf(stderr, "could not write %s\n", filename); + exit(1); + } + for (ulong i = 0; i < v.size(); i++) + { + fprintf(fp, "%f\n", v[i]); + } + fclose(fp); +} + +// +// Goertzel Algorithm for a Non-integer Frequency Index, Rick Lyons +// https://www.dsprelated.com/showarticle/495.php +// +std::complex goertzel(std::vector v, int rate, int i0, int n, float hz) +{ + // float radians_per_sample = (hz * 2 * M_PI) / rate; + // float k = radians_per_sample * n; + float bin_hz = rate / (float)n; + float k = hz / bin_hz; + + float alpha = 2 * M_PI * k / n; + float beta = 2 * M_PI * k * (n - 1.0) / n; + + float two_cos_alpha = 2 * cos(alpha); + float a = cos(beta); + float b = -sin(beta); + float c = sin(alpha) * sin(beta) - cos(alpha) * cos(beta); + float d = sin(2 * M_PI * k); + + float w1 = 0; + float w2 = 0; + + for (int i = 0; i < n; i++) + { + float w0 = v[i0 + i] + two_cos_alpha * w1 - w2; + w2 = w1; + w1 = w0; + } + + float re = w1 * a + w2 * c; + float im = w1 * b + w2 * d; + + return std::complex(re, im); +} + +float vmax(const std::vector &v) +{ + float mx = 0; + int got = 0; + for (int i = 0; i < (int)v.size(); i++) + { + if (got == 0 || v[i] > mx) + { + got = 1; + mx = v[i]; + } + } + return mx; +} + +std::vector vreal(const std::vector> &a) +{ + std::vector b(a.size()); + for (int i = 0; i < (int)a.size(); i++) + { + b[i] = a[i].real(); + } + return b; +} + +std::vector vimag(const std::vector> &a) +{ + std::vector b(a.size()); + for (int i = 0; i < (int)a.size(); i++) + { + b[i] = a[i].imag(); + } + return b; +} + +// generate 8-FSK, at 25 hz, bin size 6.25 hz, +// 200 samples/second, 32 samples/symbol. +// used as reference to detect pairs of symbols. +// superseded by gfsk(). +std::vector> fsk_c(const std::vector &syms) +{ + int n = syms.size(); + std::vector> v(n * 32); + float theta = 0; + for (int si = 0; si < n; si++) + { + float hz = 25 + syms[si] * 6.25; + for (int i = 0; i < 32; i++) + { + v[si * 32 + i] = std::complex(cos(theta), sin(theta)); + theta += 2 * M_PI / (200 / hz); + } + } + return v; +} + +// copied from wsjt-x ft2/gfsk_pulse.f90. +// b is 1.0 for FT4; 2.0 for FT8. +float gfsk_point(float b, float t) +{ + float c = M_PI * sqrt(2.0 / log(2.0)); + float x = 0.5 * (erf(c * b * (t + 0.5)) - erf(c * b * (t - 0.5))); + return x; +} + +// the smoothing window for gfsk. +// run the window over impulses of symbol frequencies, +// each impulse at the center of its symbol time. +// three symbols wide. +// most of the pulse is in the center symbol. +// b is 1.0 for FT4; 2.0 for FT8. +std::vector gfsk_window(int samples_per_symbol, float b) +{ + std::vector v(3 * samples_per_symbol); + float sum = 0; + for (int i = 0; i < (int)v.size(); i++) + { + float x = i / (float)samples_per_symbol; + x -= 1.5; + float y = gfsk_point(b, x); + v[i] = y; + sum += y; + } + + for (int i = 0; i < (int)v.size(); i++) + { + v[i] /= sum; + } + + return v; +} + +// gaussian-smoothed fsk. +// the gaussian smooths the instantaneous frequencies, +// so that the transitions between symbols don't +// cause clicks. +// gwin is gfsk_window(32, 2.0) +std::vector> gfsk_c( + const std::vector &symbols, + float hz0, float hz1, + float spacing, int rate, int symsamples, + float phase0, + const std::vector &gwin +) +{ + assert((gwin.size() % 2) == 0); + + // compute frequency for each symbol. + // generate a spike in the middle of each symbol time; + // the gaussian filter will turn it into a waveform. + std::vector hzv(symsamples * (symbols.size() + 2), 0.0); + for (int bi = 0; bi < (int)symbols.size(); bi++) + { + float base_hz = hz0 + (hz1 - hz0) * (bi / (float)symbols.size()); + float fr = base_hz + (symbols[bi] * spacing); + int mid = symsamples * (bi + 1) + symsamples / 2; + // the window has even size, so split the impulse over + // the two middle samples to be symmetric. + hzv[mid] = fr * symsamples / 2.0; + hzv[mid - 1] = fr * symsamples / 2.0; + } + + // repeat first and last symbols + for (int i = 0; i < symsamples; i++) + { + hzv[i] = hzv[i + symsamples]; + hzv[symsamples * (symbols.size() + 1) + i] = hzv[symsamples * symbols.size() + i]; + } + + // run the per-sample frequency vector through + // the gaussian filter. + int half = gwin.size() / 2; + std::vector o(hzv.size()); + for (int i = 0; i < (int)o.size(); i++) + { + float sum = 0; + for (int j = 0; j < (int)gwin.size(); j++) + { + int k = i - half + j; + if (k >= 0 && k < (int)hzv.size()) + { + sum += hzv[k] * gwin[j]; + } + } + o[i] = sum; + } + + // drop repeated first and last symbols + std::vector oo(symsamples * symbols.size()); + for (int i = 0; i < (int)oo.size(); i++) + { + oo[i] = o[i + symsamples]; + } + + // now oo[i] contains the frequency for the i'th sample. + + std::vector> v(symsamples * symbols.size()); + float theta = phase0; + for (int i = 0; i < (int)v.size(); i++) + { + v[i] = std::complex(cos(theta), sin(theta)); + float hz = oo[i]; + theta += 2 * M_PI / (rate / hz); + } + + return v; +} + +// gaussian-smoothed fsk. +// the gaussian smooths the instantaneous frequencies, +// so that the transitions between symbols don't +// cause clicks. +// gwin is gfsk_window(32, 2.0) +std::vector gfsk_r( + const std::vector &symbols, + float hz0, float hz1, + float spacing, int rate, int symsamples, + float phase0, + const std::vector &gwin +) +{ + assert((gwin.size() % 2) == 0); + + // compute frequency for each symbol. + // generate a spike in the middle of each symbol time; + // the gaussian filter will turn it into a waveform. + std::vector hzv(symsamples * (symbols.size() + 2), 0.0); + for (int bi = 0; bi < (int)symbols.size(); bi++) + { + float base_hz = hz0 + (hz1 - hz0) * (bi / (float)symbols.size()); + float fr = base_hz + (symbols[bi] * spacing); + int mid = symsamples * (bi + 1) + symsamples / 2; + // the window has even size, so split the impulse over + // the two middle samples to be symmetric. + hzv[mid] = fr * symsamples / 2.0; + hzv[mid - 1] = fr * symsamples / 2.0; + } + + // repeat first and last symbols + for (int i = 0; i < symsamples; i++) + { + hzv[i] = hzv[i + symsamples]; + hzv[symsamples * (symbols.size() + 1) + i] = hzv[symsamples * symbols.size() + i]; + } + + // run the per-sample frequency vector through + // the gaussian filter. + int half = gwin.size() / 2; + std::vector o(hzv.size()); + for (int i = 0; i < (int)o.size(); i++) + { + float sum = 0; + for (int j = 0; j < (int)gwin.size(); j++) + { + int k = i - half + j; + if (k >= 0 && k < (int)hzv.size()) + { + sum += hzv[k] * gwin[j]; + } + } + o[i] = sum; + } + + // drop repeated first and last symbols + std::vector oo(symsamples * symbols.size()); + for (int i = 0; i < (int)oo.size(); i++) + { + oo[i] = o[i + symsamples]; + } + + // now oo[i] contains the frequency for the i'th sample. + + std::vector v(symsamples * symbols.size()); + float theta = phase0; + for (int i = 0; i < (int)v.size(); i++) + { + v[i] = cos(theta); + float hz = oo[i]; + theta += 2 * M_PI / (rate / hz); + } + + return v; +} + +const std::string WHITESPACE = " \n\r\t\f\v"; + +std::string ltrim(const std::string &s) +{ + size_t start = s.find_first_not_of(WHITESPACE); + return (start == std::string::npos) ? "" : s.substr(start); +} + +std::string rtrim(const std::string &s) +{ + size_t end = s.find_last_not_of(WHITESPACE); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +std::string trim(const std::string &s) { + return rtrim(ltrim(s)); +} + +} // namespace FT8 diff --git a/ft8/util.h b/ft8/util.h new file mode 100644 index 000000000..f1807325f --- /dev/null +++ b/ft8/util.h @@ -0,0 +1,58 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon // +// written by Robert Morris, AB1HL // +// reformatted and adapted to Qt and SDRangel context // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// +#ifndef UTIL_H +#define UTIL_H + +#include +#include + +namespace FT8 +{ +float now(); +void writewav(const std::vector &samples, const char *filename, int rate); +std::vector readwav(const char *filename, int &rate_out); +void writetxt(std::vector v, const char *filename); +std::complex goertzel(std::vector v, int rate, int i0, int n, float hz); +float vmax(const std::vector &v); +std::vector vreal(const std::vector> &a); +std::vector vimag(const std::vector> &a); +std::vector> gfsk_c( + const std::vector &symbols, + float hz0, float hz1, + float spacing, int rate, int symsamples, + float phase0, + const std::vector &gwin +); +std::vector gfsk_r( + const std::vector &symbols, + float hz0, float hz1, + float spacing, int rate, int symsamples, + float phase0, + const std::vector &gwin +); +std::vector gfsk_window(int samples_per_symbol, float b); +std::string trim(const std::string &s); + +typedef unsigned long ulong; +typedef unsigned int uint; +} // namespace FT8 + +#endif diff --git a/sdrbench/CMakeLists.txt b/sdrbench/CMakeLists.txt index eded1a2c2..0783c3d65 100644 --- a/sdrbench/CMakeLists.txt +++ b/sdrbench/CMakeLists.txt @@ -4,6 +4,7 @@ set(sdrbench_SOURCES mainbench.cpp parserbench.cpp test_golay2312.cpp + test_ft8.cpp ) set(sdrbench_HEADERS @@ -16,16 +17,20 @@ add_library(sdrbench SHARED ) include_directories( + ${FFTW3F_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/exports ${CMAKE_SOURCE_DIR}/sdrbase ${CMAKE_SOURCE_DIR}/logging + ${CMAKE_SOURCE_DIR} ) target_link_libraries(sdrbench + ${FFTW3F_LIBRARIES} Qt::Core Qt::Gui sdrbase logging + ft8 ) install(TARGETS sdrbench DESTINATION ${INSTALL_LIB_DIR}) diff --git a/sdrbench/mainbench.cpp b/sdrbench/mainbench.cpp index 624d3fcba..0a97f0c24 100644 --- a/sdrbench/mainbench.cpp +++ b/sdrbench/mainbench.cpp @@ -62,6 +62,8 @@ void MainBench::run() testDecimateFF(); } else if (m_parser.getTestType() == ParserBench::TestGolay2312) { testGolay2312(); + } else if (m_parser.getTestType() == ParserBench::TestFT8) { + testFT8(); } else { qDebug() << "MainBench::run: unknown test type: " << m_parser.getTestType(); } diff --git a/sdrbench/mainbench.h b/sdrbench/mainbench.h index 8992ecc2c..559914218 100644 --- a/sdrbench/mainbench.h +++ b/sdrbench/mainbench.h @@ -54,6 +54,7 @@ private: void testDecimateFI(); void testDecimateFF(); void testGolay2312(); + void testFT8(); void decimateII(const qint16 *buf, int len); void decimateInfII(const qint16 *buf, int len); void decimateSupII(const qint16 *buf, int len); @@ -62,6 +63,7 @@ private: void decimateFF(const float *buf, int len); void printResults(const QString& prefix, qint64 nsecs); + static MainBench *m_instance; qtwebapp::LoggerWithFile *m_logger; const ParserBench& m_parser; diff --git a/sdrbench/parserbench.cpp b/sdrbench/parserbench.cpp index 122c235f9..e3a9ee2fd 100644 --- a/sdrbench/parserbench.cpp +++ b/sdrbench/parserbench.cpp @@ -24,7 +24,7 @@ ParserBench::ParserBench() : m_testOption(QStringList() << "t" << "test", - "Test type: decimateii, decimatefi, decimateff, decimateif, decimateinfii, decimatesupii, ambe, golay2312", + "Test type: decimateii, decimatefi, decimateff, decimateif, decimateinfii, decimatesupii, ambe, golay2312, ft8" "test", "decimateii"), m_nbSamplesOption(QStringList() << "n" << "nb-samples", @@ -127,6 +127,8 @@ ParserBench::TestType ParserBench::getTestType() const return TestDecimatorsSupII; } else if (m_testStr == "golay2312") { return TestGolay2312; + } else if (m_testStr == "ft8") { + return TestFT8; } else { return TestDecimatorsII; } diff --git a/sdrbench/parserbench.h b/sdrbench/parserbench.h index 50b455c27..b3c32789d 100644 --- a/sdrbench/parserbench.h +++ b/sdrbench/parserbench.h @@ -35,7 +35,8 @@ public: TestDecimatorsFF, TestDecimatorsInfII, TestDecimatorsSupII, - TestGolay2312 + TestGolay2312, + TestFT8 } TestType; ParserBench(); diff --git a/sdrbench/test_ft8.cpp b/sdrbench/test_ft8.cpp new file mode 100644 index 000000000..0c2324d4e --- /dev/null +++ b/sdrbench/test_ft8.cpp @@ -0,0 +1,111 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "mainbench.h" +#ifdef LINUX +#include "ft8/ft8.h" +#include "ft8/util.h" +#include "ft8/unpack.h" + +#include +#endif + +#ifndef LINUX +void MainBench::testFT8() +{ + qDebug("Implemented in Linux only"); +} +#else + +QMutex cycle_mu; +volatile int cycle_count; +time_t saved_cycle_start; +std::map cycle_already; + +int hcb( + int *a91, + float hz0, + float hz1, + float off, + const char *comment, + float snr, + int pass, + int correct_bits) +{ + (void) hz1; + (void) comment; + (void) pass; + + std::string msg = FT8::unpack(a91); + + cycle_mu.lock(); + + if (cycle_already.count(msg) > 0) + { + // already decoded this message on this cycle + cycle_mu.unlock(); + return 1; // 1 => already seen, don't subtract. + } + + cycle_already[msg] = true; + cycle_count += 1; + + cycle_mu.unlock(); + + struct tm result; + gmtime_r(&saved_cycle_start, &result); + + printf("%02d%02d%02d %3d %3d %5.2f %6.1f %s\n", + result.tm_hour, + result.tm_min, + result.tm_sec, + (int)snr, + correct_bits, + off - 0.5, + hz0, + msg.c_str()); + fflush(stdout); + + return 2; // 2 => new decode, do subtract. +} + +void MainBench::testFT8() +{ + qDebug("MainBench::testFT8: start"); + int hints[2] = { 2, 0 }; // CQ + double budget = 5; // compute for this many seconds per cycle + + int rate; + std::vector s = FT8::readwav("/home/f4exb/.local/share/WSJT-X/save/230105_091630.wav", rate); // FIXME: download file + FT8::entry( + s.data(), + s.size(), + 0.5 * rate, + rate, + 150, + 3600, // 2900, + hints, + hints, + budget, + budget, + hcb, + 0, + (struct FT8::cdecode *) 0 + ); + qDebug("MainBench::testFT8: end"); +} +#endif