[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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user