[fix](trx-rs): move FT2 decoding onto raw complex path

This commit is contained in:
2026-03-14 20:40:11 +01:00
parent 6d430a4300
commit 2ab05f5001
+488 -59
View File
@@ -3,6 +3,8 @@
// SPDX-License-Identifier: BSD-2-Clause
#include <ft8/decode.h>
#include <ft8/ldpc.h>
#include <ft8/crc.h>
#include <ft8/message.h>
#include <ft8/text.h>
#include <common/monitor.h>
@@ -154,8 +156,12 @@ static float decoder_candidate_dt_s(const ft8_decoder_t* dec, const ftx_candidat
#define FT2_NH1 (FT2_NFFT1 / 2)
#define FT2_NSTEP 288
#define FT2_MAX_RAW_CANDIDATES 96
#define FT2_MAX_SCAN_HITS 128
#define FT2_SYNC_TWEAK_MIN (-16)
#define FT2_SYNC_TWEAK_MAX (16)
#define FT2_NSS (FT2_NSTEP / FT2_NDOWN)
#define FT2_FRAME_SYMBOLS (FT2_NN - FT2_NR)
#define FT2_FRAME_SAMPLES (FT2_FRAME_SYMBOLS * FT2_NSS)
typedef struct
{
@@ -163,6 +169,15 @@ typedef struct
float score;
} ft2_raw_candidate_t;
typedef struct
{
float freq_hz;
float snr0;
float sync_score;
int start;
int idf;
} ft2_scan_hit_t;
static void ft2_nuttall_window(float* window, int n)
{
const float a0 = 0.355768f;
@@ -187,6 +202,17 @@ static int ft2_cmp_candidates_desc(const void* lhs, const void* rhs)
return 0;
}
static int ft2_cmp_scan_hits_desc(const void* lhs, const void* rhs)
{
const ft2_scan_hit_t* a = (const ft2_scan_hit_t*)lhs;
const ft2_scan_hit_t* b = (const ft2_scan_hit_t*)rhs;
if (a->sync_score < b->sync_score)
return 1;
if (a->sync_score > b->sync_score)
return -1;
return 0;
}
static int ft2_find_frequency_peaks(
const ft8_decoder_t* dec,
ft2_raw_candidate_t* candidates,
@@ -445,7 +471,7 @@ static float ft2_sync2d_score(
const float complex sync_wave[4][64],
const float complex tweak_wave[33][64])
{
const int nss = FT2_NSTEP / FT2_NDOWN;
const int nss = FT2_NSS;
const int positions[4] = {
start,
start + 33 * nss,
@@ -453,30 +479,47 @@ static float ft2_sync2d_score(
start + 99 * nss,
};
float score = 0.0f;
int groups = 0;
const float complex* tweak = tweak_wave[idf - FT2_SYNC_TWEAK_MIN];
for (int group = 0; group < 4; ++group)
{
int pos = positions[group];
if (pos < 0 || (pos + 4 * nss) > n_samples)
continue;
float complex sum = 0.0f;
int usable = 0;
for (int i = 0; i < 64; ++i)
{
int sample_idx = pos + 2 * i;
if (sample_idx < 0 || sample_idx >= n_samples)
continue;
sum += samples[sample_idx] * conjf(sync_wave[group][i] * tweak[i]);
++usable;
}
score += cabsf(sum) / 64.0f;
++groups;
if (usable > 16)
score += cabsf(sum) / (2.0f * nss);
}
return (groups > 0) ? (score / groups) : 0.0f;
return score;
}
static int ft2_find_candidates_raw(
static void ft2_normalize_downsampled(float complex* samples, int n_samples)
{
float power = 0.0f;
for (int i = 0; i < n_samples; ++i)
{
power += crealf(samples[i] * conjf(samples[i]));
}
if (power <= 0.0f)
return;
float scale = sqrtf((float)n_samples / power);
for (int i = 0; i < n_samples; ++i)
{
samples[i] *= scale;
}
}
static int ft2_find_scan_hits(
const ft8_decoder_t* dec,
ftx_candidate_t* out,
int max_candidates)
ft2_scan_hit_t* out,
int max_hits)
{
ft2_raw_candidate_t peaks[FT2_MAX_RAW_CANDIDATES];
int n_peaks = ft2_find_frequency_peaks(dec, peaks, FT2_MAX_RAW_CANDIDATES);
@@ -491,11 +534,12 @@ static int ft2_find_candidates_raw(
ft2_prepare_sync_waveforms(sync_wave, tweak_wave);
int count = 0;
for (int peak = 0; peak < n_peaks && count < max_candidates; ++peak)
for (int peak = 0; peak < n_peaks && count < max_hits; ++peak)
{
int produced = ft2_downsample_candidate(dec, peaks[peak].freq_hz, down, nfft2);
if (produced <= 0)
continue;
ft2_normalize_downsampled(down, produced);
float best_score = -1.0f;
int best_start = 0;
@@ -513,7 +557,7 @@ static int ft2_find_candidates_raw(
}
}
}
if (best_score < 0.18f)
if (best_score < 0.60f)
continue;
for (int idf = best_idf - 4; idf <= best_idf + 4; ++idf)
@@ -531,50 +575,381 @@ static int ft2_find_candidates_raw(
}
}
}
if (best_score < 0.24f)
if (best_score < 0.60f)
continue;
float corrected_freq_hz = peaks[peak].freq_hz + best_idf;
float base_freq_hz = corrected_freq_hz - ft2_frequency_offset_hz();
float bin_pos = base_freq_hz * FT2_SYMBOL_PERIOD;
int absolute_bin = (int)floorf(bin_pos);
int freq_sub = (int)lroundf((bin_pos - absolute_bin) * dec->cfg.freq_osr);
if (freq_sub >= dec->cfg.freq_osr)
{
absolute_bin += 1;
freq_sub = 0;
}
int freq_offset = absolute_bin - dec->mon.min_bin;
if (freq_offset < 0 || (freq_offset + 3) >= dec->mon.wf.num_bins)
continue;
int start_with_ramp = best_start - 32;
int time_offset = start_with_ramp / 32;
int rem = start_with_ramp - time_offset * 32;
if (rem < 0)
{
rem += 32;
time_offset -= 1;
}
int time_sub = (int)lroundf((float)rem / 4.0f);
if (time_sub >= dec->cfg.time_osr)
{
time_offset += 1;
time_sub = 0;
}
out[count].score = (int16_t)lroundf(best_score * 100.0f);
out[count].time_offset = (int16_t)time_offset;
out[count].time_sub = (uint8_t)time_sub;
out[count].freq_offset = (int16_t)freq_offset;
out[count].freq_sub = (uint8_t)freq_sub;
out[count].freq_hz = peaks[peak].freq_hz;
out[count].snr0 = peaks[peak].score - 1.0f;
out[count].sync_score = best_score;
out[count].start = best_start;
out[count].idf = best_idf;
++count;
}
qsort(out, count, sizeof(out[0]), ft2_cmp_scan_hits_desc);
free(down);
return count;
}
static void ft2_extract_signal_region(const float complex* input, int input_len, int start, float complex* out_signal)
{
for (int i = 0; i < FT2_FRAME_SAMPLES; ++i)
{
int src = start + i;
out_signal[i] = (src >= 0 && src < input_len) ? input[src] : 0.0f;
}
}
static void ft2_extract_logl_sequence_raw(const float complex symbols[4][FT2_FRAME_SYMBOLS], 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;
sum += symbols[kFT4_Gray_map[dibit]][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;
}
}
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 s4[4][FT2_FRAME_SYMBOLS];
memset(bitmetrics, 0, sizeof(float) * 2 * FT2_FRAME_SYMBOLS * 3);
size_t fft_mem_len = 0;
kiss_fft_alloc(FT2_NSS, 0, NULL, &fft_mem_len);
void* fft_mem = malloc(fft_mem_len);
kiss_fft_cfg fft_cfg = kiss_fft_alloc(FT2_NSS, 0, fft_mem, &fft_mem_len);
if (!fft_cfg)
{
free(fft_mem);
return false;
}
for (int sym = 0; sym < FT2_FRAME_SYMBOLS; ++sym)
{
kiss_fft_cpx csymb[FT2_NSS];
for (int i = 0; i < FT2_NSS; ++i)
{
float complex sample = signal[sym * FT2_NSS + i];
csymb[i].r = crealf(sample);
csymb[i].i = cimagf(sample);
}
kiss_fft(fft_cfg, csymb, csymb);
for (int tone = 0; tone < 4; ++tone)
{
float complex bin = csymb[tone].r + I * csymb[tone].i;
symbols[tone][sym] = bin;
s4[tone][sym] = cabsf(bin);
}
}
free(fft_mem);
int sync_ok = 0;
for (int group = 0; group < 4; ++group)
{
int base = group * 33;
for (int i = 0; i < 4; ++i)
{
int best = 0;
for (int tone = 1; tone < 4; ++tone)
{
if (s4[tone][base + i] > s4[best][base + i])
best = tone;
}
if (best == kFT4_Costas_pattern[group][i])
++sync_ok;
}
}
if (sync_ok < 4)
return false;
float metric1[2 * FT2_FRAME_SYMBOLS] = { 0 };
float metric2[2 * FT2_FRAME_SYMBOLS] = { 0 };
float metric4[2 * FT2_FRAME_SYMBOLS] = { 0 };
for (int start = 0; start <= FT2_FRAME_SYMBOLS - 1; start += 1)
{
ft2_extract_logl_sequence_raw(symbols, start, 1, metric1 + (2 * start));
}
for (int start = 0; start <= FT2_FRAME_SYMBOLS - 2; start += 2)
{
ft2_extract_logl_sequence_raw(symbols, start, 2, metric2 + (2 * start));
}
for (int start = 0; start <= FT2_FRAME_SYMBOLS - 4; start += 4)
{
ft2_extract_logl_sequence_raw(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 i = 0; i < 2 * FT2_FRAME_SYMBOLS; ++i)
{
bitmetrics[i][0] = metric1[i];
bitmetrics[i][1] = metric2[i];
bitmetrics[i][2] = metric4[i];
}
return true;
}
static void ft2_normalize_logl(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-6f)
return;
float norm_factor = sqrtf(24.0f / variance);
for (int i = 0; i < FTX_LDPC_N; ++i)
{
log174[i] *= norm_factor;
}
}
static void ft2_pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[])
{
int num_bytes = (num_bits + 7) / 8;
for (int i = 0; i < num_bytes; ++i)
packed[i] = 0;
uint8_t mask = 0x80;
int byte_idx = 0;
for (int i = 0; i < num_bits; ++i)
{
if (bit_array[i])
packed[byte_idx] |= mask;
mask >>= 1;
if (!mask)
{
mask = 0x80;
++byte_idx;
}
}
}
static bool ft2_unpack_message(const uint8_t plain174[], ftx_message_t* message)
{
uint8_t a91[FTX_LDPC_K_BYTES];
ft2_pack_bits(plain174, FTX_LDPC_K, a91);
uint16_t crc_extracted = ftx_extract_crc(a91);
a91[9] &= 0xF8;
a91[10] &= 0x00;
uint16_t crc_calculated = ftx_compute_crc(a91, 96 - 14);
if (crc_extracted != crc_calculated)
return false;
message->hash = crc_calculated;
for (int i = 0; i < 10; ++i)
{
message->payload[i] = a91[i] ^ kFT4_XOR_sequence[i];
}
return true;
}
static bool ft2_decode_hit(
const ft8_decoder_t* dec,
const ft2_scan_hit_t* hit,
ftx_message_t* message,
float* dt_s,
float* freq_hz,
float* snr_db)
{
const int nraw = dec->ft2_raw_len;
const int nfft2 = nraw / FT2_NDOWN;
float complex* cd2 = (float complex*)malloc(sizeof(float complex) * nfft2);
float complex* cb = (float complex*)malloc(sizeof(float complex) * nfft2);
float complex signal[FT2_FRAME_SAMPLES];
float bitmetrics[2 * FT2_FRAME_SYMBOLS][3];
float complex sync_wave[4][64];
float complex tweak_wave[33][64];
if (!cd2 || !cb)
{
free(cd2);
free(cb);
return false;
}
ft2_prepare_sync_waveforms(sync_wave, tweak_wave);
int produced = ft2_downsample_candidate(dec, hit->freq_hz, cd2, nfft2);
if (produced <= 0)
{
free(cd2);
free(cb);
return false;
}
ft2_normalize_downsampled(cd2, produced);
float best_score = -1.0f;
int best_start = hit->start;
int best_idf = hit->idf;
for (int idf = hit->idf - 4; idf <= hit->idf + 4; ++idf)
{
if (idf < FT2_SYNC_TWEAK_MIN || idf > FT2_SYNC_TWEAK_MAX)
continue;
for (int start = hit->start - 5; start <= hit->start + 5; ++start)
{
float score = ft2_sync2d_score(cd2, produced, start, idf, sync_wave, tweak_wave);
if (score > best_score)
{
best_score = score;
best_start = start;
best_idf = idf;
}
}
}
if (best_score < 0.80f)
{
free(cd2);
free(cb);
return false;
}
float corrected_freq_hz = hit->freq_hz + best_idf;
if (corrected_freq_hz <= 10.0f || corrected_freq_hz >= 4990.0f)
{
free(cd2);
free(cb);
return false;
}
produced = ft2_downsample_candidate(dec, corrected_freq_hz, cb, nfft2);
if (produced <= 0)
{
free(cd2);
free(cb);
return false;
}
ft2_normalize_downsampled(cb, produced);
ft2_extract_signal_region(cb, produced, best_start, signal);
if (!ft2_extract_bitmetrics_raw(signal, bitmetrics))
{
free(cd2);
free(cb);
return false;
}
static const uint8_t sync_bits_a[8] = { 0, 0, 0, 1, 1, 0, 1, 1 };
static const uint8_t sync_bits_b[8] = { 0, 1, 0, 0, 1, 1, 1, 0 };
static const uint8_t sync_bits_c[8] = { 1, 1, 1, 0, 0, 1, 0, 0 };
static const uint8_t sync_bits_d[8] = { 1, 0, 1, 1, 0, 0, 0, 1 };
int sync_qual = 0;
for (int i = 0; i < 8; ++i)
{
sync_qual += ((bitmetrics[i][0] >= 0.0f) ? 1 : 0) == sync_bits_a[i];
sync_qual += ((bitmetrics[66 + i][0] >= 0.0f) ? 1 : 0) == sync_bits_b[i];
sync_qual += ((bitmetrics[132 + i][0] >= 0.0f) ? 1 : 0) == sync_bits_c[i];
sync_qual += ((bitmetrics[198 + i][0] >= 0.0f) ? 1 : 0) == sync_bits_d[i];
}
if (sync_qual < 13)
{
free(cd2);
free(cb);
return false;
}
float llr_passes[5][FTX_LDPC_N];
for (int i = 0; i < 58; ++i)
{
llr_passes[0][i] = bitmetrics[8 + i][0];
llr_passes[0][58 + i] = bitmetrics[74 + i][0];
llr_passes[0][116 + i] = bitmetrics[140 + i][0];
llr_passes[1][i] = bitmetrics[8 + i][1];
llr_passes[1][58 + i] = bitmetrics[74 + i][1];
llr_passes[1][116 + i] = bitmetrics[140 + i][1];
llr_passes[2][i] = bitmetrics[8 + i][2];
llr_passes[2][58 + i] = bitmetrics[74 + i][2];
llr_passes[2][116 + i] = bitmetrics[140 + i][2];
}
for (int i = 0; i < FTX_LDPC_N; ++i)
{
llr_passes[0][i] *= 2.83f;
llr_passes[1][i] *= 2.83f;
llr_passes[2][i] *= 2.83f;
float a = llr_passes[0][i];
float b = llr_passes[1][i];
float c = llr_passes[2][i];
llr_passes[3][i] = (fabsf(a) >= fabsf(b) && fabsf(a) >= fabsf(c)) ? a : ((fabsf(b) >= fabsf(c)) ? b : c);
llr_passes[4][i] = (a + b + c) / 3.0f;
}
uint8_t plain174[FTX_LDPC_N];
bool ok = false;
for (int pass = 0; pass < 5 && !ok; ++pass)
{
float log174[FTX_LDPC_N];
memcpy(log174, llr_passes[pass], sizeof(log174));
ft2_normalize_logl(log174);
int ldpc_errors = 0;
bp_decode(log174, 50, plain174, &ldpc_errors);
if (ldpc_errors > 0)
continue;
ok = ft2_unpack_message(plain174, message);
}
if (ok)
{
float sm1 = ft2_sync2d_score(cd2, produced, best_start - 1, best_idf, sync_wave, tweak_wave);
float sp1 = ft2_sync2d_score(cd2, produced, best_start + 1, best_idf, sync_wave, tweak_wave);
float xstart = (float)best_start;
float den = sm1 - 2.0f * best_score + sp1;
if (fabsf(den) > 1.0e-6f)
xstart += 0.5f * (sm1 - sp1) / den;
*dt_s = xstart / (12000.0f / FT2_NDOWN) - 0.5f;
*freq_hz = corrected_freq_hz;
if (hit->snr0 > 0.0f)
*snr_db = fmaxf(-21.0f, 10.0f * log10f(hit->snr0) - 13.0f);
else
*snr_db = -21.0f;
}
free(cd2);
free(cb);
return ok;
}
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));
@@ -684,14 +1059,6 @@ 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 ? 0 : 10;
const int kLdpcIters = is_ft2 ? 50 : 30;
ftx_candidate_t candidate_list[kMaxCandidates];
int num_candidates = is_ft2
? ft2_find_candidates_raw(dec, candidate_list, kMaxCandidates)
: ftx_find_candidates(wf, kMaxCandidates, candidate_list, kMinScore);
int num_decoded = 0;
ftx_message_t decoded[200];
@@ -701,6 +1068,71 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
decoded_hashtable[i] = NULL;
}
if (is_ft2)
{
ft2_scan_hit_t hit_list[FT2_MAX_SCAN_HITS];
int num_hits = ft2_find_scan_hits(dec, hit_list, FT2_MAX_SCAN_HITS);
for (int idx = 0; idx < num_hits && num_decoded < max_results; ++idx)
{
const ft2_scan_hit_t* hit = &hit_list[idx];
ftx_message_t message;
float time_sec = 0.0f;
float freq_hz = 0.0f;
float snr_db = -21.0f;
if (!ft2_decode_hit(dec, hit, &message, &time_sec, &freq_hz, &snr_db))
{
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 = snr_db;
num_decoded++;
}
}
else
{
const int kMaxCandidates = 200;
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];
@@ -751,14 +1183,11 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
dst->text[sizeof(dst->text) - 1] = '\0';
dst->dt_s = time_sec;
dst->freq_hz = freq_hz;
/* Convert sync score to SNR in dB (WSJT-X 2500 Hz reference bandwidth).
* score is an average uint8 difference between Costas tones and their
* neighbours; each unit = 0.5 dB. Subtract 10*log10(2500/3.125) ≈ 29 dB
* to normalise from a 3.125 Hz bin (6.25 Hz / freq_osr=2) to 2500 Hz. */
dst->snr_db = cand->score * 0.5f - 29.0f;
num_decoded++;
}
}
hashtable_cleanup(10);
return num_decoded;