From 27de75cf79b76a84681a20bb8641865f0b4c247f Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Sat, 14 Mar 2026 20:25:59 +0100 Subject: [PATCH] [fix](trx-rs): improve FT2 coherent decoding Add coherent FT2 sync and likelihood extraction. Align FT2 frequency and DT reporting with the reference decoder. Fix vendored monitor phase-mode type mismatches. Co-authored-by: OpenAI Codex Signed-off-by: Stanislaw Grams --- external/ft8_lib/common/monitor.c | 2 +- external/ft8_lib/common/monitor.h | 4 +- external/ft8_lib/ft8/decode.c | 160 ++++++++++++++++++++++++- external/ft8_lib/ft8/decode.h | 2 +- src/decoders/trx-ft8/src/ft8_wrapper.c | 33 ++++- 5 files changed, 192 insertions(+), 9 deletions(-) diff --git a/external/ft8_lib/common/monitor.c b/external/ft8_lib/common/monitor.c index 19290c7..b83be61 100644 --- a/external/ft8_lib/common/monitor.c +++ b/external/ft8_lib/common/monitor.c @@ -192,7 +192,7 @@ void monitor_process(monitor_t* me, const float* frame) } #ifdef WATERFALL_USE_PHASE -void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal) +void monitor_resynth(const monitor_t* me, const ftx_candidate_t* candidate, float* signal) { const int num_ifft = me->nifft; const int num_shift = num_ifft / 2; diff --git a/external/ft8_lib/common/monitor.h b/external/ft8_lib/common/monitor.h index dd70d8d..c0f65b1 100644 --- a/external/ft8_lib/common/monitor.h +++ b/external/ft8_lib/common/monitor.h @@ -52,11 +52,11 @@ void monitor_process(monitor_t* me, const float* frame); void monitor_free(monitor_t* me); #ifdef WATERFALL_USE_PHASE -void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal); +void monitor_resynth(const monitor_t* me, const ftx_candidate_t* candidate, float* signal); #endif #ifdef __cplusplus } #endif -#endif // _INCLUDE_MONITOR_H_ \ No newline at end of file +#endif // _INCLUDE_MONITOR_H_ diff --git a/external/ft8_lib/ft8/decode.c b/external/ft8_lib/ft8/decode.c index cc427a7..3536024 100644 --- a/external/ft8_lib/ft8/decode.c +++ b/external/ft8_lib/ft8/decode.c @@ -5,6 +5,7 @@ #include #include +#include // #define LOG_LEVEL LOG_DEBUG // #include "debug.h" @@ -30,6 +31,7 @@ static const float db_power_sum[40] = { /// @param[in] code_map Symbol encoding map /// @param[out] log174 Output of decoded log likelihoods for each of the 174 message bits static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174); +static void ft2_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174); static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174); /// Packs a string of bits each represented as a zero/non-zero byte in bit_array[], @@ -46,9 +48,17 @@ static void heapify_up(ftx_candidate_t heap[], int heap_size); static void ftx_normalize_logl(float* log174); static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl); +static void ft2_extract_logl_sequence(const float complex symbols[4][FT2_NN - FT2_NR], int start_sym, int n_syms, float* metrics); static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl); static void ft8_decode_multi_symbols(const WF_ELEM_T* wf, int num_bins, int n_syms, int bit_idx, float* log174); +static inline float complex wf_elem_to_complex(const WF_ELEM_T elem) +{ + float mag = WF_ELEM_MAG(elem); + float amplitude = powf(10.0f, mag / 20.0f); + return amplitude * cexpf(I * elem.phase); +} + static const WF_ELEM_T* get_cand_mag(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate) { int offset = candidate->time_offset; @@ -124,6 +134,44 @@ static int ft8_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* cand return score; } +static int ft2_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate) +{ + const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate); + float score = 0.0f; + int groups = 0; + + for (int m = 0; m < FT2_NUM_SYNC; ++m) + { + float complex sum = 0.0f; + bool complete = true; + for (int k = 0; k < FT2_LENGTH_SYNC; ++k) + { + int block = 1 + (FT2_SYNC_OFFSET * m) + k; + int block_abs = candidate->time_offset + block; + if ((block_abs < 0) || (block_abs >= wf->num_blocks)) + { + complete = false; + break; + } + + const WF_ELEM_T* sym = mag_cand + (block * wf->block_stride); + int tone = kFT4_Costas_pattern[m][k]; + sum += wf_elem_to_complex(sym[tone]); + } + + if (!complete) + continue; + + score += cabsf(sum); + ++groups; + } + + if (groups == 0) + return 0; + + return (int)lroundf((score / groups) * 8.0f); +} + static int ft4_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate) { int score = 0; @@ -191,7 +239,7 @@ int ftx_find_candidates(const ftx_waterfall_t* wf, int num_candidates, ftx_candi { bool is_ft2 = (wf->protocol == FTX_PROTOCOL_FT2); int (*sync_fun)(const ftx_waterfall_t*, const ftx_candidate_t*) = - ftx_protocol_uses_ft4_layout(wf->protocol) ? ft4_sync_score : ft8_sync_score; + is_ft2 ? ft2_sync_score : (ftx_protocol_uses_ft4_layout(wf->protocol) ? ft4_sync_score : ft8_sync_score); int num_tones = ftx_protocol_uses_ft4_layout(wf->protocol) ? 4 : 8; int time_offset_min = -10; int time_offset_max = 20; @@ -290,6 +338,74 @@ static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidat } } +static void ft2_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174) +{ + const WF_ELEM_T* mag = get_cand_mag(wf, cand); + float complex symbols[4][FT2_NN - FT2_NR]; + float metric1[2 * (FT2_NN - FT2_NR)] = { 0 }; + float metric2[2 * (FT2_NN - FT2_NR)] = { 0 }; + float metric4[2 * (FT2_NN - FT2_NR)] = { 0 }; + + for (int frame_sym = 0; frame_sym < (FT2_NN - FT2_NR); ++frame_sym) + { + int sym_idx = frame_sym + 1; // skip ramp-up symbol + int block = cand->time_offset + sym_idx; + if ((block < 0) || (block >= wf->num_blocks)) + { + for (int tone = 0; tone < 4; ++tone) + { + symbols[tone][frame_sym] = 0.0f; + } + continue; + } + + const WF_ELEM_T* sym = mag + (sym_idx * wf->block_stride); + for (int tone = 0; tone < 4; ++tone) + { + symbols[tone][frame_sym] = wf_elem_to_complex(sym[tone]); + } + } + + for (int start = 0; start <= (FT2_NN - FT2_NR) - 1; start += 1) + { + ft2_extract_logl_sequence(symbols, start, 1, metric1 + (2 * start)); + } + for (int start = 0; start <= (FT2_NN - FT2_NR) - 2; start += 2) + { + ft2_extract_logl_sequence(symbols, start, 2, metric2 + (2 * start)); + } + for (int start = 0; start <= (FT2_NN - FT2_NR) - 4; start += 4) + { + ft2_extract_logl_sequence(symbols, start, 4, metric4 + (2 * start)); + } + + metric2[204] = metric1[204]; + metric2[205] = metric1[205]; + metric4[200] = metric2[200]; + metric4[201] = metric2[201]; + metric4[202] = metric2[202]; + metric4[203] = metric2[203]; + metric4[204] = metric1[204]; + metric4[205] = metric1[205]; + + for (int data_sym = 0; data_sym < FT2_ND; ++data_sym) + { + int frame_sym = data_sym + ((data_sym < 29) ? 4 : ((data_sym < 58) ? 8 : 12)); + int src_bit = 2 * frame_sym; + int dst_bit = 2 * data_sym; + + float a0 = metric1[src_bit + 0]; + float b0 = metric2[src_bit + 0]; + float c0 = metric4[src_bit + 0]; + float a1 = metric1[src_bit + 1]; + float b1 = metric2[src_bit + 1]; + float c1 = metric4[src_bit + 1]; + + log174[dst_bit + 0] = (fabsf(a0) >= fabsf(b0) && fabsf(a0) >= fabsf(c0)) ? a0 : ((fabsf(b0) >= fabsf(c0)) ? b0 : c0); + log174[dst_bit + 1] = (fabsf(a1) >= fabsf(b1) && fabsf(a1) >= fabsf(c1)) ? a1 : ((fabsf(b1) >= fabsf(c1)) ? b1 : c1); + } +} + static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174) { const WF_ELEM_T* mag = get_cand_mag(wf, cand); // Pointer to 8 magnitude bins of the first symbol @@ -341,7 +457,11 @@ static void ftx_normalize_logl(float* log174) bool ftx_decode_candidate(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status) { float log174[FTX_LDPC_N]; // message bits encoded as likelihood - if (ftx_protocol_uses_ft4_layout(wf->protocol)) + if (wf->protocol == FTX_PROTOCOL_FT2) + { + ft2_extract_likelihood(wf, cand, log174); + } + else if (ftx_protocol_uses_ft4_layout(wf->protocol)) { ft4_extract_likelihood(wf, cand, log174); } @@ -479,6 +599,42 @@ static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl) logl[1] = max2(s2[1], s2[3]) - max2(s2[0], s2[2]); } +static void ft2_extract_logl_sequence(const float complex symbols[4][FT2_NN - FT2_NR], int start_sym, int n_syms, float* metrics) +{ + const int n_bits = 2 * n_syms; + const int n_sequences = 1 << n_bits; + + for (int bit = 0; bit < n_bits; ++bit) + { + float max_zero = -INFINITY; + float max_one = -INFINITY; + for (int seq = 0; seq < n_sequences; ++seq) + { + float complex sum = 0.0f; + for (int sym = 0; sym < n_syms; ++sym) + { + int shift = 2 * (n_syms - sym - 1); + int dibit = (seq >> shift) & 0x3; + int tone = kFT4_Gray_map[dibit]; + sum += symbols[tone][start_sym + sym]; + } + float strength = cabsf(sum); + int mask_bit = n_bits - bit - 1; + if (((seq >> mask_bit) & 0x1) != 0) + { + if (strength > max_one) + max_one = strength; + } + else + { + if (strength > max_zero) + max_zero = strength; + } + } + metrics[bit] = max_one - max_zero; + } +} + // Compute unnormalized log likelihood log(p(1) / p(0)) of 3 message bits (1 FSK symbol) static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl) { diff --git a/external/ft8_lib/ft8/decode.h b/external/ft8_lib/ft8/decode.h index 43cd376..5db1010 100644 --- a/external/ft8_lib/ft8/decode.h +++ b/external/ft8_lib/ft8/decode.h @@ -18,7 +18,7 @@ typedef struct float phase; } waterfall_cpx_t; -// #define WATERFALL_USE_PHASE +#define WATERFALL_USE_PHASE #ifdef WATERFALL_USE_PHASE #define WF_ELEM_T waterfall_cpx_t diff --git a/src/decoders/trx-ft8/src/ft8_wrapper.c b/src/decoders/trx-ft8/src/ft8_wrapper.c index 46b47fd..c90d187 100644 --- a/src/decoders/trx-ft8/src/ft8_wrapper.c +++ b/src/decoders/trx-ft8/src/ft8_wrapper.c @@ -115,6 +115,33 @@ typedef struct float freq_hz; } ft8_decode_result_t; +static float ft2_frequency_offset_hz(void) +{ + return -1.5f / FT2_SYMBOL_PERIOD; +} + +static float decoder_candidate_freq_hz(const ft8_decoder_t* dec, const ftx_candidate_t* cand) +{ + const ftx_waterfall_t* wf = &dec->mon.wf; + float freq_hz = (dec->mon.min_bin + cand->freq_offset + (float)cand->freq_sub / wf->freq_osr) / dec->mon.symbol_period; + if (dec->cfg.protocol == FTX_PROTOCOL_FT2) + { + freq_hz += ft2_frequency_offset_hz(); + } + return freq_hz; +} + +static float decoder_candidate_dt_s(const ft8_decoder_t* dec, const ftx_candidate_t* cand) +{ + const ftx_waterfall_t* wf = &dec->mon.wf; + float time_sec = (cand->time_offset + (float)cand->time_sub / wf->time_osr) * dec->mon.symbol_period; + if (dec->cfg.protocol == FTX_PROTOCOL_FT2) + { + time_sec -= 0.5f; + } + return time_sec; +} + ft8_decoder_t* ft8_decoder_create(int sample_rate, float f_min, float f_max, int time_osr, int freq_osr, int protocol) { ft8_decoder_t* dec = (ft8_decoder_t*)calloc(1, sizeof(ft8_decoder_t)); @@ -195,7 +222,7 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res const ftx_waterfall_t* wf = &dec->mon.wf; const bool is_ft2 = (dec->cfg.protocol == FTX_PROTOCOL_FT2); const int kMaxCandidates = is_ft2 ? 400 : 200; - const int kMinScore = is_ft2 ? 4 : 10; + const int kMinScore = is_ft2 ? 0 : 10; const int kLdpcIters = is_ft2 ? 50 : 30; ftx_candidate_t candidate_list[kMaxCandidates]; @@ -213,8 +240,8 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res { const ftx_candidate_t* cand = &candidate_list[idx]; - float freq_hz = (dec->mon.min_bin + cand->freq_offset + (float)cand->freq_sub / wf->freq_osr) / dec->mon.symbol_period; - float time_sec = (cand->time_offset + (float)cand->time_sub / wf->time_osr) * dec->mon.symbol_period; + 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;