[fix](trx-ft8): improve FT2 decode fallback

Normalize FT2 log-likelihoods before LDPC and fall back to\nthe standard waterfall candidate decoder when the raw FT2\npath produces no decodes.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 22:39:07 +01:00
parent 71bfc5fca1
commit 188fa38f1b
+117 -68
View File
@@ -128,6 +128,11 @@ typedef struct
float freq_hz; float freq_hz;
} ft8_decode_result_t; } ft8_decode_result_t;
static int decode_from_waterfall_candidates(
const ft8_decoder_t* dec,
ft8_decode_result_t* out,
int max_results);
static float ft2_frequency_offset_hz(void) static float ft2_frequency_offset_hz(void)
{ {
return -1.5f / FT2_SYMBOL_PERIOD; return -1.5f / FT2_SYMBOL_PERIOD;
@@ -735,6 +740,25 @@ static void ft2_normalize_metric(float* metric, int count)
metric[i] /= sigma; metric[i] /= sigma;
} }
static void ft2_normalize_log174(float* log174)
{
float sum = 0.0f;
float sum2 = 0.0f;
for (int i = 0; i < FTX_LDPC_N; ++i)
{
sum += log174[i];
sum2 += log174[i] * log174[i];
}
float inv_n = 1.0f / FTX_LDPC_N;
float variance = (sum2 - (sum * sum * inv_n)) * inv_n;
if (variance <= 1.0e-12f)
return;
float norm_factor = sqrtf(24.0f / variance);
for (int i = 0; i < FTX_LDPC_N; ++i)
log174[i] *= norm_factor;
}
static bool ft2_extract_bitmetrics_raw(const float complex* signal, float bitmetrics[2 * FT2_FRAME_SYMBOLS][3]) static bool ft2_extract_bitmetrics_raw(const float complex* signal, float bitmetrics[2 * FT2_FRAME_SYMBOLS][3])
{ {
float complex symbols[4][FT2_FRAME_SYMBOLS]; float complex symbols[4][FT2_FRAME_SYMBOLS];
@@ -1059,6 +1083,84 @@ static bool ft2_unpack_message(const uint8_t plain174[], ftx_message_t* message)
return true; return true;
} }
static int decode_from_waterfall_candidates(
const ft8_decoder_t* dec,
ft8_decode_result_t* out,
int max_results)
{
if (!dec || !out || max_results <= 0)
return 0;
const ftx_waterfall_t* wf = &dec->mon.wf;
const int kMaxCandidates = 200;
const int kMinScore = 10;
const int kLdpcIters = 30;
int num_decoded = 0;
ftx_message_t decoded[200];
ftx_message_t* decoded_hashtable[200];
for (int i = 0; i < 200; ++i)
decoded_hashtable[i] = NULL;
ftx_candidate_t candidate_list[kMaxCandidates];
int num_candidates = ftx_find_candidates(wf, kMaxCandidates, candidate_list, kMinScore);
for (int idx = 0; idx < num_candidates && num_decoded < max_results; ++idx)
{
const ftx_candidate_t* cand = &candidate_list[idx];
float freq_hz = decoder_candidate_freq_hz(dec, cand);
float time_sec = decoder_candidate_dt_s(dec, cand);
ftx_message_t message;
ftx_decode_status_t status;
if (!ftx_decode_candidate(wf, cand, kLdpcIters, &message, &status))
continue;
int idx_hash = message.hash % 200;
bool found_empty_slot = false;
bool found_duplicate = false;
do
{
if (decoded_hashtable[idx_hash] == NULL)
{
found_empty_slot = true;
}
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == memcmp(decoded_hashtable[idx_hash]->payload, message.payload, sizeof(message.payload))))
{
found_duplicate = true;
}
else
{
idx_hash = (idx_hash + 1) % 200;
}
} while (!found_empty_slot && !found_duplicate);
if (!found_empty_slot)
continue;
memcpy(&decoded[idx_hash], &message, sizeof(message));
decoded_hashtable[idx_hash] = &decoded[idx_hash];
char text[FTX_MAX_MESSAGE_LENGTH];
ftx_message_offsets_t offsets;
ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text, &offsets);
if (unpack_status != FTX_MESSAGE_RC_OK)
continue;
ft8_decode_result_t* dst = &out[num_decoded];
strncpy(dst->text, text, sizeof(dst->text) - 1);
dst->text[sizeof(dst->text) - 1] = '\0';
dst->dt_s = time_sec;
dst->freq_hz = freq_hz;
dst->snr_db = cand->score * 0.5f - 29.0f;
num_decoded++;
}
return num_decoded;
}
static bool ft2_decode_hit( static bool ft2_decode_hit(
const ft2_downsample_ctx_t* downsample_ctx, const ft2_downsample_ctx_t* downsample_ctx,
const ft2_scan_hit_t* hit, const ft2_scan_hit_t* hit,
@@ -1220,6 +1322,7 @@ static bool ft2_decode_hit(
{ {
float log174[FTX_LDPC_N]; float log174[FTX_LDPC_N];
memcpy(log174, llr_passes[pass], sizeof(log174)); memcpy(log174, llr_passes[pass], sizeof(log174));
ft2_normalize_log174(log174);
int ntype = 0; // 1: bp_decode, 2: ldpc_decode int ntype = 0; // 1: bp_decode, 2: ldpc_decode
int nharderror = FTX_LDPC_M; int nharderror = FTX_LDPC_M;
float dmin = 0.0f; float dmin = 0.0f;
@@ -1403,16 +1506,9 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
if (!dec || !out || max_results <= 0) if (!dec || !out || max_results <= 0)
return 0; return 0;
const ftx_waterfall_t* wf = &dec->mon.wf;
const bool is_ft2 = (dec->cfg.protocol == FTX_PROTOCOL_FT2); const bool is_ft2 = (dec->cfg.protocol == FTX_PROTOCOL_FT2);
int num_decoded = 0; int num_decoded = 0;
ftx_message_t decoded[200];
ftx_message_t* decoded_hashtable[200];
for (int i = 0; i < 200; ++i)
{
decoded_hashtable[i] = NULL;
}
if (is_ft2) if (is_ft2)
{ {
@@ -1433,6 +1529,11 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
int fail_ldpc = 0; int fail_ldpc = 0;
int fail_crc = 0; int fail_crc = 0;
int fail_unpack = 0; int fail_unpack = 0;
int fallback_decoded = 0;
ftx_message_t decoded[200];
ftx_message_t* decoded_hashtable[200];
for (int i = 0; i < 200; ++i)
decoded_hashtable[i] = NULL;
int pass_bp[5] = { 0 }; int pass_bp[5] = { 0 };
int pass_sp[5] = { 0 }; int pass_sp[5] = { 0 };
float pass_best_dmin[5] = { INFINITY, INFINITY, INFINITY, INFINITY, INFINITY }; float pass_best_dmin[5] = { INFINITY, INFINITY, INFINITY, INFINITY, INFINITY };
@@ -1540,14 +1641,21 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
num_decoded++; num_decoded++;
} }
if (num_decoded == 0)
{
fallback_decoded = decode_from_waterfall_candidates(dec, out, max_results);
if (fallback_decoded > 0)
num_decoded = fallback_decoded;
}
LOG(LOG_INFO, LOG(LOG_INFO,
"FT2 window: raw=%d peaks=%d hits=%d best_peak=%.3f best_sync=%.3f decoded=%d fail(sync=%d freq=%d down=%d bits=%d qual=%d ldpc=%d crc=%d unpack=%d) pass(bp=%d/%d/%d/%d/%d sp=%d/%d/%d/%d/%d dmin=%.2f/%.2f/%.2f/%.2f/%.2f)\n", "FT2 window: raw=%d peaks=%d hits=%d best_peak=%.3f best_sync=%.3f decoded=%d fallback=%d fail(sync=%d freq=%d down=%d bits=%d qual=%d ldpc=%d crc=%d unpack=%d) pass(bp=%d/%d/%d/%d/%d sp=%d/%d/%d/%d/%d dmin=%.2f/%.2f/%.2f/%.2f/%.2f)\n",
dec->ft2_raw_len, dec->ft2_raw_len,
scan_stats.peaks_found, scan_stats.peaks_found,
scan_stats.hits_found, scan_stats.hits_found,
scan_stats.best_peak_score, scan_stats.best_peak_score,
scan_stats.best_sync_score, scan_stats.best_sync_score,
num_decoded, num_decoded,
fallback_decoded,
fail_refined_sync, fail_refined_sync,
fail_freq_range, fail_freq_range,
fail_final_downsample, fail_final_downsample,
@@ -1567,66 +1675,7 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
} }
else else
{ {
const int kMaxCandidates = 200; num_decoded = decode_from_waterfall_candidates(dec, out, max_results);
const int kMinScore = 10;
const int kLdpcIters = 30;
ftx_candidate_t candidate_list[kMaxCandidates];
int num_candidates = ftx_find_candidates(wf, kMaxCandidates, candidate_list, kMinScore);
for (int idx = 0; idx < num_candidates && num_decoded < max_results; ++idx)
{
const ftx_candidate_t* cand = &candidate_list[idx];
float freq_hz = decoder_candidate_freq_hz(dec, cand);
float time_sec = decoder_candidate_dt_s(dec, cand);
ftx_message_t message;
ftx_decode_status_t status;
if (!ftx_decode_candidate(wf, cand, kLdpcIters, &message, &status))
{
continue;
}
int idx_hash = message.hash % 200;
bool found_empty_slot = false;
bool found_duplicate = false;
do
{
if (decoded_hashtable[idx_hash] == NULL)
{
found_empty_slot = true;
}
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == memcmp(decoded_hashtable[idx_hash]->payload, message.payload, sizeof(message.payload))))
{
found_duplicate = true;
}
else
{
idx_hash = (idx_hash + 1) % 200;
}
} while (!found_empty_slot && !found_duplicate);
if (!found_empty_slot)
continue;
memcpy(&decoded[idx_hash], &message, sizeof(message));
decoded_hashtable[idx_hash] = &decoded[idx_hash];
char text[FTX_MAX_MESSAGE_LENGTH];
ftx_message_offsets_t offsets;
ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text, &offsets);
if (unpack_status != FTX_MESSAGE_RC_OK)
continue;
ft8_decode_result_t* dst = &out[num_decoded];
strncpy(dst->text, text, sizeof(dst->text) - 1);
dst->text[sizeof(dst->text) - 1] = '\0';
dst->dt_s = time_sec;
dst->freq_hz = freq_hz;
dst->snr_db = cand->score * 0.5f - 29.0f;
num_decoded++;
}
} }
hashtable_cleanup(10); hashtable_cleanup(10);