From 902e58b46b831593a209854050d3a5673c26c4c9 Mon Sep 17 00:00:00 2001
From: f4exb <f4exb06@gmail.com>
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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "fft.h"
+#include <mutex>
+#include <unistd.h>
+#include <assert.h>
+#include <sys/file.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#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<std::complex<float>> one_fft(
+    const std::vector<float> &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<std::complex<float>> 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<float>(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<float> &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<float> 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<std::complex<float>> one_fft_c(
+    const std::vector<float> &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<std::complex<float>> 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<float> 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<std::complex<float>> one_fft_cc(
+    const std::vector<std::complex<float>> &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<std::complex<float>> 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<float> 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<std::complex<float>> one_ifft_cc(
+    const std::vector<std::complex<float>> &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<std::complex<float>> 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<float> 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<float> one_ifft(const std::vector<std::complex<float>> &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<float> 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<std::complex<float>> analytic(const std::vector<float> &x, const char *why)
+{
+    ulong n = x.size();
+
+    std::vector<std::complex<float>> 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<std::complex<float>> 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<float> hilbert_shift(const std::vector<float> &x, float hz0, float hz1, int rate)
+{
+    // y = scipy.signal.hilbert(x)
+    std::vector<std::complex<float>> y = analytic(x, "hilbert_shift");
+    assert(y.size() == x.size());
+
+    float dt = 1.0 / rate;
+    int n = x.size();
+
+    std::vector<float> ret(n);
+
+    for (int i = 0; i < n; i++)
+    {
+        // complex "local oscillator" at hz.
+        float hz = hz0 + (i / (float)n) * (hz1 - hz0);
+        std::complex<float> lo = std::exp(std::complex<float>(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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef FFT_H
+#define FFT_H
+
+#include <vector>
+#include <complex>
+#include <fftw3.h>
+
+namespace FT8
+{
+    class Plan;
+    Plan *get_plan(int n, const char *why);
+
+    std::vector<std::complex<float>> one_fft(const std::vector<float> &samples, int i0, int block, const char *why, Plan *p);
+    std::vector<float> one_ifft(const std::vector<std::complex<float>> &bins, const char *why);
+    typedef std::vector<std::vector<std::complex<float>>> ffts_t;
+    ffts_t ffts(const std::vector<float> &samples, int i0, int block, const char *why);
+    std::vector<std::complex<float>> one_fft_c(const std::vector<float> &samples, int i0, int block, const char *why);
+    std::vector<std::complex<float>> one_fft_cc(const std::vector<std::complex<float>> &samples, int i0, int block, const char *why);
+    std::vector<std::complex<float>> one_ifft_cc(const std::vector<std::complex<float>> &bins, const char *why);
+    std::vector<std::complex<float>> analytic(const std::vector<float> &x, const char *why);
+    std::vector<float> hilbert_shift(const std::vector<float> &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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+//
+// An FT8 receiver in C++.
+//
+// Many ideas and protocol details borrowed from Franke
+// and Taylor's WSJT-X code.
+//
+// Robert Morris, AB1HL
+//
+
+#include <stdio.h>
+#include <assert.h>
+#include <math.h>
+#include <complex>
+#include <fftw3.h>
+#include <vector>
+#include <algorithm>
+#include <complex>
+#include <sys/time.h>
+#include <string.h>
+#include <unistd.h>
+#include <thread>
+#include <mutex>
+#include <atomic>
+#include <random>
+#include <functional>
+#include <map>
+#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<float> hamming(int n)
+{
+    std::vector<float> 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<float> blackman(int n)
+{
+    std::vector<float> 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<float> sym_blackman(int n)
+{
+    std::vector<float> 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<float> blackmanharris(int n)
+{
+    float a0 = 0.35875;
+    float a1 = 0.48829;
+    float a2 = 0.14128;
+    float a3 = 0.01168;
+    std::vector<float> 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<float> 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<float> samples_;  // input to each pass
+    std::vector<float> 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<int> hints1_;
+    std::vector<int> 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<std::complex<float>> hack_bins_;
+    std::vector<cdecode> prevdecs_;
+
+    Plan *plan32_;
+
+    FT8(const std::vector<float> &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<cdecode> 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<Strength> 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<Strength> strengths;
+
+        for (int bi = min_bin; bi < max_bin && bi + 8 <= nbins; bi++)
+        {
+            std::vector<Strength> 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<float> reduce_rate(const std::vector<float> &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<std::complex<float>> 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<std::complex<float>> 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<float> 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<int> distribution(0, samples_.size() - 1);
+            auto rnd = std::bind(distribution, generator);
+
+            std::vector<float> 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<int> 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<Strength> 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<std::complex<float>> 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<float> 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<Strength> 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<float> &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<float> &samples,
+        int rate,
+        const std::vector<int> &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<float> 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<float> 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<float> &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<float> 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<std::complex<float>> &bins,
+        int rate,
+        const std::vector<int> &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<float> 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<Strength> search_both(
+        const std::vector<float> &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<Strength> 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<float> &samples,
+        int rate,
+        const std::vector<int> &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<std::complex<float>> 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<float> fft_shift(
+        const std::vector<float> &samples,
+        int off,
+        int len,
+        int rate,
+        float hz
+    )
+    {
+        std::vector<std::complex<float>> 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<float> fft_shift_f(
+        const std::vector<std::complex<float>> &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<std::complex<float>> 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<float> 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<float> shift200(
+        const std::vector<float> &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<float> &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<std::vector<float>>
+    un_gray_code_r(const std::vector<std::vector<float>> &m79)
+    {
+        std::vector<std::vector<float>> 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<std::vector<float>> convert_to_snr(const std::vector<std::vector<float>> &m79)
+    {
+        if (snr_how < 0 || snr_win < 0)
+            return m79;
+
+        //
+        // for each symbol time, what's its "noise" level?
+        //
+        std::vector<float> mm(79);
+        for (int si = 0; si < 79; si++)
+        {
+            std::vector<float> 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<float> winwin;
+        if (snr_win > 0)
+        {
+            winwin = blackman(2 * snr_win + 1);
+        }
+        else
+        {
+            winwin.push_back(1.0);
+        }
+
+        std::vector<std::vector<float>> 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<std::vector<std::complex<float>>> c_convert_to_snr(
+        const std::vector<std::vector<std::complex<float>>> &m79
+    )
+    {
+        if (snr_how < 0 || snr_win < 0)
+            return m79;
+
+        //
+        // for each symbol time, what's its "noise" level?
+        //
+        std::vector<float> mm(79);
+        for (int si = 0; si < 79; si++)
+        {
+            std::vector<float> 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<float> winwin;
+        if (snr_win > 0)
+        {
+            winwin = blackman(2 * snr_win + 1);
+        }
+        else
+        {
+            winwin.push_back(1.0);
+        }
+
+        std::vector<std::vector<std::complex<float>>> 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<std::vector<float>> &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<std::vector<float>> soft_c2m(const ffts_t &c79)
+    {
+        std::vector<std::vector<float>> m79(79);
+        std::vector<float> 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<float> phases(79);
+
+        // for each symbol time, median of nearby phases
+        for (int si = 0; si < 79; si++)
+        {
+            std::vector<float> 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<std::vector<float>> 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<float> maxes[79];
+        for (int i = 0; i < 79; i++)
+        {
+            std::complex<float> 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<std::vector<float>> m79(79);
+        for (int i = 0; i < 79; i++)
+        {
+            m79[i].resize(8);
+            for (int j = 0; j < 8; j++)
+            {
+                std::complex<float> 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<float> c1 = maxes[k];
+                        std::complex<float> 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<float> extract_bits(const std::vector<int> &syms, const std::vector<float> str)
+    {
+        assert(syms.size() == 79);
+        assert(str.size() == 79);
+
+        std::vector<float> 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> 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<float> 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> 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<float> 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<std::complex<float>> fbandpass(
+        const std::vector<std::complex<float>> &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<std::complex<float>> 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<float> down_v7(const std::vector<float> &samples, float hz)
+    {
+        int len = samples.size();
+        std::vector<std::complex<float>> bins = one_fft(samples, 0, len, "down_v7a", 0);
+
+        return down_v7_f(bins, len, hz);
+    }
+
+    std::vector<float> down_v7_f(const std::vector<std::complex<float>> &bins, int len, float hz)
+    {
+        int nbins = bins.size();
+
+        float bin_hz = rate_ / (float)len;
+        int down = round((hz - 25) / bin_hz);
+        std::vector<std::complex<float>> 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<std::complex<float>> bbins(blen / 2 + 1);
+        for (int i = 0; i < (int)bbins.size(); i++)
+            bbins[i] = bins1[i];
+        std::vector<float> 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<std::complex<float>> &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<float> 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<float> &samples200, int best_off, float hz_for_cb)
+    {
+        if (do_second)
+        {
+            std::vector<Strength> 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<float> 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<float> &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<float> 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<int> 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<float> 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<float> phases(79);
+        std::vector<float> amps(79);
+        for (int i = 0; i < 79; i++)
+        {
+            int sym = bin0 + re79[i];
+            std::complex<float> 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<float> &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<int> 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<int> recode(int a174[])
+    {
+        int i174 = 0;
+        int costas[] = {3, 1, 4, 0, 6, 5, 2};
+        std::vector<int> 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<cdecode> prevdecs;
+    for (int i = 0; i < nprevdecs; i++)
+    {
+        prevdecs.push_back(xprevdecs[i]);
+    }
+
+    std::vector<float> 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<FT8 *> 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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+//
+// 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 <stdio.h>
+#include <math.h>
+#include <stdlib.h>
+#include <assert.h>
+#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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+//
+// ordered statistics decoder for LDPC and new FT8.
+// idea from wsjt-x.
+//
+
+#include <vector>
+#include <algorithm>
+#include <stdio.h>
+#include <string.h>
+#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<int> 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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#include <string>
+#include <mutex>
+#include <map>
+#include <string.h>
+#include <assert.h>
+#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<int, std::string> hashes12;
+std::map<int, std::string> 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 "<TOKEN>";
+    }
+
+    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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#include <sndfile.h>
+#include <sys/time.h>
+#include <assert.h>
+#include <math.h>
+#include <string.h>
+#include <complex>
+#include <string>
+#include <algorithm>
+#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<float> &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<float> 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<float> 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<float> 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<float> 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<float> goertzel(std::vector<float> 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<float>(re, im);
+}
+
+float vmax(const std::vector<float> &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<float> vreal(const std::vector<std::complex<float>> &a)
+{
+    std::vector<float> b(a.size());
+    for (int i = 0; i < (int)a.size(); i++)
+    {
+        b[i] = a[i].real();
+    }
+    return b;
+}
+
+std::vector<float> vimag(const std::vector<std::complex<float>> &a)
+{
+    std::vector<float> 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<std::complex<float>> fsk_c(const std::vector<int> &syms)
+{
+    int n = syms.size();
+    std::vector<std::complex<float>> 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<float>(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<float> gfsk_window(int samples_per_symbol, float b)
+{
+    std::vector<float> 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<std::complex<float>> gfsk_c(
+    const std::vector<int> &symbols,
+    float hz0, float hz1,
+    float spacing, int rate, int symsamples,
+    float phase0,
+    const std::vector<float> &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<float> 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<float> 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<float> 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<std::complex<float>> v(symsamples * symbols.size());
+    float theta = phase0;
+    for (int i = 0; i < (int)v.size(); i++)
+    {
+        v[i] = std::complex<float>(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<float> gfsk_r(
+    const std::vector<int> &symbols,
+    float hz0, float hz1,
+    float spacing, int rate, int symsamples,
+    float phase0,
+    const std::vector<float> &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<float> 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<float> 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<float> 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<float> 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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <vector>
+#include <complex>
+
+namespace FT8
+{
+float now();
+void writewav(const std::vector<float> &samples, const char *filename, int rate);
+std::vector<float> readwav(const char *filename, int &rate_out);
+void writetxt(std::vector<float> v, const char *filename);
+std::complex<float> goertzel(std::vector<float> v, int rate, int i0, int n, float hz);
+float vmax(const std::vector<float> &v);
+std::vector<float> vreal(const std::vector<std::complex<float>> &a);
+std::vector<float> vimag(const std::vector<std::complex<float>> &a);
+std::vector<std::complex<float>> gfsk_c(
+    const std::vector<int> &symbols,
+    float hz0, float hz1,
+    float spacing, int rate, int symsamples,
+    float phase0,
+    const std::vector<float> &gwin
+);
+std::vector<float> gfsk_r(
+    const std::vector<int> &symbols,
+    float hz0, float hz1,
+    float spacing, int rate, int symsamples,
+    float phase0,
+    const std::vector<float> &gwin
+);
+std::vector<float> 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 <http://www.gnu.org/licenses/>.          //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include "mainbench.h"
+#ifdef LINUX
+#include "ft8/ft8.h"
+#include "ft8/util.h"
+#include "ft8/unpack.h"
+
+#include <QMutex>
+#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<std::string, bool> 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<float> 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