1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-03-18 05:59:37 -04:00
sdrangel/ft8/ft4worker.cpp

437 lines
12 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2026 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This is the code from ft8mon: https://github.com/rtmrtmrtmrtm/ft8mon //
// 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 "libldpc.h"
#include "osd.h"
#include "ft4worker.h"
namespace FT8 {
namespace {
constexpr int kFT4SymbolSamples = 576;
constexpr int kFT4TotalSymbols = 103;
constexpr int kFT4FrameSamples = kFT4TotalSymbols * kFT4SymbolSamples;
const std::array<std::array<int, 4>, 4> kFT4SyncTones = {{
{{0, 1, 3, 2}},
{{1, 0, 2, 3}},
{{2, 3, 1, 0}},
{{3, 2, 0, 1}}
}};
const std::array<int, 77> kFT4Rvec = {{
0,1,0,0,1,0,1,0,0,1,0,1,1,1,1,0,1,0,0,0,1,0,0,1,1,0,1,1,0,
1,0,0,1,0,1,1,0,0,0,0,1,0,0,0,1,0,1,0,0,1,1,1,1,0,0,1,0,1,
0,1,0,1,0,1,1,0,1,1,1,1,1,0,0,0,1,0,1
}};
struct FT4Candidate
{
int frameIndex;
int start;
int toneBin;
float sync;
float noise;
float score;
};
}
FT4Worker::FT4Worker(
const std::vector<float>& samples,
float minHz,
float maxHz,
int start,
int rate,
CallbackInterface *cb,
const FT4ParamsLight& params
) :
m_samples(samples),
m_minHz(minHz),
m_maxHz(maxHz),
m_start(start),
m_rate(rate),
m_cb(cb),
m_params(params)
{}
void FT4Worker::start_work()
{
decode();
}
float FT4Worker::binPowerInterpolated(const FFTEngine::ffts_t& bins, int symbolIndex, float toneBin)
{
const int binCount = bins[symbolIndex].size();
if (binCount <= 0) {
return 0.0f;
}
if (toneBin <= 0.0f) {
return std::abs(bins[symbolIndex][0]);
}
if (toneBin >= static_cast<float>(binCount - 1)) {
return std::abs(bins[symbolIndex][binCount - 1]);
}
const int lowerBin = static_cast<int>(std::floor(toneBin));
const int upperBin = lowerBin + 1;
const float frac = toneBin - lowerBin;
const float p0 = std::abs(bins[symbolIndex][lowerBin]);
const float p1 = std::abs(bins[symbolIndex][upperBin]);
return p0 + frac * (p1 - p0);
}
void FT4Worker::collectSyncMetrics(const FFTEngine::ffts_t& bins, float toneBin, float& sync, float& noise) const
{
sync = 0.0f;
noise = 0.0f;
for (int block = 0; block < 4; block++)
{
const int symbolOffset = block * 33;
for (int index = 0; index < 4; index++)
{
const int symbolIndex = symbolOffset + index;
const int expectedTone = kFT4SyncTones[block][index];
for (int tone = 0; tone < 4; tone++)
{
const float power = binPowerInterpolated(bins, symbolIndex, toneBin + tone);
if (tone == expectedTone) {
sync += power;
} else {
noise += power;
}
}
}
}
}
float FT4Worker::refineToneBin(const FFTEngine::ffts_t& bins, int toneBin) const
{
auto scoreAt = [&](float candidateToneBin) {
float sync = 0.0f;
float noise = 0.0f;
collectSyncMetrics(bins, candidateToneBin, sync, noise);
return sync - 0.9f * (noise / 3.0f);
};
const float left = scoreAt(toneBin - 1.0f);
const float center = scoreAt(toneBin);
const float right = scoreAt(toneBin + 1.0f);
const float denom = left - 2.0f * center + right;
float frac = 0.0f;
if (std::fabs(denom) > 1.0e-12f) {
frac = 0.5f * (left - right) / denom;
}
if (frac > 0.5f) {
frac = 0.5f;
} else if (frac < -0.5f) {
frac = -0.5f;
}
return toneBin + frac;
}
void FT4Worker::buildBitMetrics(const FFTEngine::ffts_t& bins, float toneBin, std::array<float, 174>& bitMetrics) const
{
int bitIndex = 0;
float llrAbsSum = 0.0f;
for (int symbolIndex = 0; symbolIndex < kFT4TotalSymbols; symbolIndex++)
{
const bool inSync = (symbolIndex < 4)
|| (symbolIndex >= 33 && symbolIndex < 37)
|| (symbolIndex >= 66 && symbolIndex < 70)
|| (symbolIndex >= 99);
if (inSync) {
continue;
}
std::array<float, 4> tonePower;
for (int tone = 0; tone < 4; tone++) {
tonePower[tone] = binPowerInterpolated(bins, symbolIndex, toneBin + tone);
}
const float epsilon = 1.0e-6f;
const float b0Zero = std::max(tonePower[0], tonePower[1]) + epsilon;
const float b0One = std::max(tonePower[2], tonePower[3]) + epsilon;
const float b1Zero = std::max(tonePower[0], tonePower[3]) + epsilon;
const float b1One = std::max(tonePower[1], tonePower[2]) + epsilon;
const float llr0 = std::log(b0Zero / b0One);
const float llr1 = std::log(b1Zero / b1One);
bitMetrics[bitIndex++] = llr0;
bitMetrics[bitIndex++] = llr1;
llrAbsSum += std::fabs(llr0) + std::fabs(llr1);
}
if (bitIndex <= 0) {
return;
}
const float meanAbs = llrAbsSum / bitIndex;
if (meanAbs < 1.0e-5f) {
return;
}
const float targetMean = 3.0f;
const float globalScale = targetMean / meanAbs;
for (int i = 0; i < bitIndex; i++) {
float scaled = bitMetrics[i] * globalScale;
if (scaled > 8.0f) {
scaled = 8.0f;
} else if (scaled < -8.0f) {
scaled = -8.0f;
}
bitMetrics[i] = scaled;
}
}
FFTEngine::ffts_t FT4Worker::makeFrameBins(int start)
{
return m_fftEngine.ffts(m_samples, start, kFT4SymbolSamples);
}
void FT4Worker::decode()
{
if (m_samples.size() < kFT4FrameSamples) {
return;
}
const int maxStart = static_cast<int>(m_samples.size()) - kFT4FrameSamples;
const float binSpacing = static_cast<float>(m_rate) / static_cast<float>(kFT4SymbolSamples);
if (m_maxHz <= m_minHz) {
return;
}
std::vector<int> coarseStarts;
std::vector<int> startCandidates;
auto addCoarseStart = [&](int start)
{
start = std::max(0, std::min(start, maxStart));
if (std::find(coarseStarts.begin(), coarseStarts.end(), start) == coarseStarts.end()) {
coarseStarts.push_back(start);
}
};
for (int start = 0; start <= std::min(maxStart, 14000); start += kFT4SymbolSamples) {
addCoarseStart(start);
}
const int nominalStart = maxStart / 2;
addCoarseStart(nominalStart - 2 * kFT4SymbolSamples);
addCoarseStart(nominalStart - kFT4SymbolSamples);
addCoarseStart(nominalStart);
addCoarseStart(nominalStart + kFT4SymbolSamples);
addCoarseStart(nominalStart + 2 * kFT4SymbolSamples);
addCoarseStart(maxStart);
const int eighthSymbol = kFT4SymbolSamples / 8;
const std::array<int, 9> fineOffsets = {{
-4 * eighthSymbol,
-3 * eighthSymbol,
-2 * eighthSymbol,
-eighthSymbol,
0,
eighthSymbol,
2 * eighthSymbol,
3 * eighthSymbol,
4 * eighthSymbol
}};
auto addStartCandidate = [&](int start)
{
start = std::max(0, std::min(start, maxStart));
if (std::find(startCandidates.begin(), startCandidates.end(), start) == startCandidates.end()) {
startCandidates.push_back(start);
}
};
for (int coarseStart : coarseStarts)
{
for (int offset : fineOffsets) {
addStartCandidate(coarseStart + offset);
}
}
struct FT4Frame
{
int start;
FFTEngine::ffts_t bins;
};
std::vector<FT4Frame> frames;
frames.reserve(startCandidates.size());
for (int start : startCandidates)
{
FFTEngine::ffts_t bins = makeFrameBins(start);
if (bins.size() < kFT4TotalSymbols) {
continue;
}
if (bins[0].size() < 8) {
continue;
}
frames.push_back(FT4Frame{start, std::move(bins)});
}
if (frames.empty()) {
return;
}
const int minToneBin = std::max(0, static_cast<int>(std::ceil(m_minHz / binSpacing)));
const int maxToneBinByHz = static_cast<int>(std::floor(m_maxHz / binSpacing)) - 3;
if (maxToneBinByHz < minToneBin) {
return;
}
std::vector<FT4Candidate> candidates;
for (int frameIndex = 0; frameIndex < static_cast<int>(frames.size()); frameIndex++)
{
const FT4Frame& frame = frames[frameIndex];
const int maxToneBin = std::min(maxToneBinByHz, static_cast<int>(frame.bins[0].size()) - 5);
for (int toneBin = minToneBin; toneBin <= maxToneBin; toneBin++)
{
float sync = 0.0f;
float noise = 0.0f;
collectSyncMetrics(frame.bins, toneBin, sync, noise);
if (sync < 1.8f) {
continue;
}
const float score = sync - 1.0f * (noise / 3.0f);
candidates.push_back(FT4Candidate{frameIndex, frame.start, toneBin, sync, noise, score});
}
}
if (candidates.empty()) {
return;
}
std::sort(candidates.begin(), candidates.end(), [](const FT4Candidate& lhs, const FT4Candidate& rhs) {
return lhs.score > rhs.score;
});
const int maxDecodeCandidates = std::min(200, static_cast<int>(candidates.size()));
const int ldpcThreshold = std::max(25, m_params.osd_ldpc_thresh - 50);
for (int candidateIndex = 0; candidateIndex < maxDecodeCandidates; candidateIndex++)
{
const FT4Candidate& candidate = candidates[candidateIndex];
const FT4Frame& frame = frames[candidate.frameIndex];
const float refinedToneBin = refineToneBin(frame.bins, candidate.toneBin);
std::array<float, 174> llr;
buildBitMetrics(frame.bins, refinedToneBin, llr);
int plain[174];
int ldpcOk = 0;
int correct_bits = 0;
LDPC::ldpc_decode(llr.data(), m_params.ldpc_iters, plain, &ldpcOk);
if (ldpcOk < ldpcThreshold)
{
continue;
}
int a174[174];
for (int i = 0; i < 174; i++)
{
a174[i] = plain[i];
if (llr.data()[i] < 0 && a174[i] == 1) {
correct_bits += 1;
} else if (llr.data()[i] > 0 && a174[i] == 0) {
correct_bits += 1;
}
}
bool crcOk = OSD::check_crc(a174);
if (!crcOk && m_params.use_osd)
{
int oplain[91];
int depth = -1;
std::array<float, 174> llrCopy = llr;
if (OSD::osd_decode(llrCopy.data(), m_params.osd_depth, oplain, &depth))
{
OSD::ldpc_encode(oplain, a174);
crcOk = OSD::check_crc(a174);
}
}
if (!crcOk) {
continue;
}
int a91[91];
for (int i = 0; i < 91; i++) {
a91[i] = a174[i];
}
for (int i = 0; i < 77; i++) {
a91[i] ^= kFT4Rvec[i];
}
const float snrLinear = (candidate.sync + 1.0e-9f) / ((candidate.noise / 3.0f) + 1.0e-9f);
const float snr = 10.0f * std::log10(snrLinear) - 12.0f;
const float tone0 = refinedToneBin * binSpacing;
if (snr < -26.0f) {
continue;
}
const float off = static_cast<float>(m_start) / m_rate + static_cast<float>(candidate.start) / m_rate;
m_cb->hcb(a91, tone0, off, "FT4-EXP", snr, 0, correct_bits);
}
}
} // namespace FT8