[fix](trx-rs): align FT2 downsample path with reference

Reuse a shared FT2 downsample FFT context per decode window and\nfix filter shift/bin handling to match the reference implementation.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-14 21:45:35 +01:00
parent dc683792de
commit cededd9c3f
+149 -90
View File
@@ -195,6 +195,18 @@ typedef struct
float dmin[5]; float dmin[5];
} ft2_pass_diag_t; } ft2_pass_diag_t;
typedef struct
{
int nraw;
int nfft2;
float df;
float* window;
kiss_fft_cpx* spectrum;
kiss_fft_cpx* band;
kiss_fft_cfg ifft_cfg;
void* ifft_mem;
} ft2_downsample_ctx_t;
typedef enum typedef enum
{ {
FT2_FAIL_NONE = 0, FT2_FAIL_NONE = 0,
@@ -411,106 +423,139 @@ static void ft2_prepare_sync_waveforms(float complex sync_wave[4][64], float com
} }
static int ft2_downsample_candidate( static int ft2_downsample_candidate(
const ft8_decoder_t* dec, const ft2_downsample_ctx_t* ctx,
float freq_hz, float freq_hz,
float complex* out_samples, float complex* out_samples,
int out_len) int out_len)
{ {
const int nraw = dec->ft2_raw_len; if (!ctx || !ctx->spectrum || !ctx->window || !ctx->band || !ctx->ifft_cfg)
const int nfft2 = nraw / FT2_NDOWN;
if (!dec->ft2_raw || nraw <= 0 || out_len < nfft2)
return 0; return 0;
kiss_fft_scalar* timedata = (kiss_fft_scalar*)malloc(sizeof(kiss_fft_scalar) * nraw); const int nraw = ctx->nraw;
kiss_fft_cpx* spectrum = (kiss_fft_cpx*)malloc(sizeof(kiss_fft_cpx) * (nraw / 2 + 1)); const int nfft2 = ctx->nfft2;
kiss_fft_cpx* band = (kiss_fft_cpx*)calloc(nfft2, sizeof(kiss_fft_cpx)); if (nraw <= 0 || out_len < nfft2)
float* filt = (float*)calloc(nfft2, sizeof(float));
if (!timedata || !spectrum || !band || !filt)
{
free(timedata);
free(spectrum);
free(band);
free(filt);
return 0; return 0;
memset(ctx->band, 0, sizeof(kiss_fft_cpx) * nfft2);
const int i0 = (int)lroundf(freq_hz / ctx->df);
if (i0 >= 0 && i0 <= nraw / 2)
ctx->band[0] = ctx->spectrum[i0];
for (int i = 1; i <= nfft2 / 2; ++i)
{
if ((i0 + i) >= 0 && (i0 + i) <= nraw / 2)
ctx->band[i] = ctx->spectrum[i0 + i];
if ((i0 - i) >= 0 && (i0 - i) <= nraw / 2)
ctx->band[nfft2 - i] = ctx->spectrum[i0 - i];
} }
size_t rfft_mem_len = 0;
kiss_fftr_alloc(nraw, 0, NULL, &rfft_mem_len);
void* rfft_mem = malloc(rfft_mem_len);
kiss_fftr_cfg rfft_cfg = kiss_fftr_alloc(nraw, 0, rfft_mem, &rfft_mem_len);
size_t ifft_mem_len = 0;
kiss_fft_alloc(nfft2, 1, NULL, &ifft_mem_len);
void* ifft_mem = malloc(ifft_mem_len);
kiss_fft_cfg ifft_cfg = kiss_fft_alloc(nfft2, 1, ifft_mem, &ifft_mem_len);
if (!rfft_cfg || !ifft_cfg)
{
free(timedata);
free(spectrum);
free(band);
free(filt);
free(rfft_mem);
free(ifft_mem);
return 0;
}
for (int i = 0; i < nraw; ++i)
timedata[i] = dec->ft2_raw[i];
kiss_fftr(rfft_cfg, timedata, spectrum);
const float df = (float)dec->cfg.sample_rate / nraw;
const float baud = 1.0f / FT2_SYMBOL_PERIOD;
const int i0 = (int)lroundf(freq_hz / df);
const int iwt = (int)lroundf((0.5f * baud) / df);
const int iwf = (int)lroundf((4.0f * baud) / df);
const int iws = (int)lroundf(baud / df);
for (int i = 0; i < nfft2; ++i) for (int i = 0; i < nfft2; ++i)
filt[i] = 0.0f; {
for (int i = 0; i < iwt && i < nfft2; ++i) ctx->band[i].r = ctx->band[i].r * ctx->window[i] / nfft2;
filt[i] = 0.5f * (1.0f + cosf((float)M_PI * (float)(iwt - 1 - i) / fmaxf((float)iwt, 1.0f))); ctx->band[i].i = ctx->band[i].i * ctx->window[i] / nfft2;
for (int i = iwt; i < iwt + iwf && i < nfft2; ++i) }
filt[i] = 1.0f; kiss_fft(ctx->ifft_cfg, ctx->band, ctx->band);
for (int i = iwt + iwf; i < 2 * iwt + iwf && i < nfft2; ++i) for (int i = 0; i < nfft2; ++i)
filt[i] = 0.5f * (1.0f + cosf((float)M_PI * (float)(i - (iwt + iwf)) / fmaxf((float)iwt, 1.0f))); out_samples[i] = ctx->band[i].r + I * ctx->band[i].i;
return nfft2;
}
static void ft2_downsample_ctx_free(ft2_downsample_ctx_t* ctx)
{
if (!ctx)
return;
free(ctx->window);
free(ctx->spectrum);
free(ctx->band);
free(ctx->ifft_mem);
memset(ctx, 0, sizeof(*ctx));
}
static bool ft2_downsample_ctx_init(const ft8_decoder_t* dec, ft2_downsample_ctx_t* ctx)
{
if (!dec || !ctx || !dec->ft2_raw)
return false;
memset(ctx, 0, sizeof(*ctx));
ctx->nraw = dec->ft2_raw_len;
ctx->nfft2 = ctx->nraw / FT2_NDOWN;
if (ctx->nraw <= 0 || ctx->nfft2 <= 0)
return false;
ctx->df = (float)dec->cfg.sample_rate / ctx->nraw;
ctx->window = (float*)calloc(ctx->nfft2, sizeof(float));
ctx->spectrum = (kiss_fft_cpx*)malloc(sizeof(kiss_fft_cpx) * (ctx->nraw / 2 + 1));
ctx->band = (kiss_fft_cpx*)malloc(sizeof(kiss_fft_cpx) * ctx->nfft2);
if (!ctx->window || !ctx->spectrum || !ctx->band)
{
ft2_downsample_ctx_free(ctx);
return false;
}
const float baud = 1.0f / FT2_SYMBOL_PERIOD;
const int iwt = (int)((0.5f * baud) / ctx->df);
const int iwf = (int)((4.0f * baud) / ctx->df);
const int iws = (int)(baud / ctx->df);
if (iwt <= 0)
{
ft2_downsample_ctx_free(ctx);
return false;
}
for (int i = 0; i < iwt && i < ctx->nfft2; ++i)
ctx->window[i] = 0.5f * (1.0f + cosf((float)M_PI * (float)(iwt - 1 - i) / (float)iwt));
for (int i = iwt; i < iwt + iwf && i < ctx->nfft2; ++i)
ctx->window[i] = 1.0f;
for (int i = iwt + iwf; i < 2 * iwt + iwf && i < ctx->nfft2; ++i)
ctx->window[i] = 0.5f * (1.0f + cosf((float)M_PI * (float)(i - (iwt + iwf)) / (float)iwt));
if (iws > 0) if (iws > 0)
{ {
float* shifted = (float*)calloc(nfft2, sizeof(float)); float* shifted = (float*)calloc(ctx->nfft2, sizeof(float));
for (int i = 0; i < nfft2; ++i) if (!shifted)
{ {
shifted[(i + iws) % nfft2] = filt[i]; ft2_downsample_ctx_free(ctx);
return false;
} }
memcpy(filt, shifted, sizeof(float) * nfft2); for (int i = 0; i < ctx->nfft2; ++i)
shifted[i] = ctx->window[(i + iws) % ctx->nfft2];
memcpy(ctx->window, shifted, sizeof(float) * ctx->nfft2);
free(shifted); free(shifted);
} }
if (i0 >= 0 && i0 <= nraw / 2) kiss_fft_scalar* timedata = (kiss_fft_scalar*)malloc(sizeof(kiss_fft_scalar) * ctx->nraw);
band[0] = spectrum[i0]; if (!timedata)
for (int i = 1; i < nfft2 / 2; ++i)
{ {
if ((i0 + i) >= 0 && (i0 + i) <= nraw / 2) ft2_downsample_ctx_free(ctx);
band[i] = spectrum[i0 + i]; return false;
if ((i0 - i) >= 0 && (i0 - i) <= nraw / 2)
band[nfft2 - i] = spectrum[i0 - i];
} }
for (int i = 0; i < nfft2; ++i) size_t rfft_mem_len = 0;
kiss_fftr_alloc(ctx->nraw, 0, NULL, &rfft_mem_len);
void* rfft_mem = malloc(rfft_mem_len);
kiss_fftr_cfg rfft_cfg = kiss_fftr_alloc(ctx->nraw, 0, rfft_mem, &rfft_mem_len);
if (!rfft_cfg)
{ {
band[i].r = band[i].r * filt[i] / nfft2; free(timedata);
band[i].i = band[i].i * filt[i] / nfft2; free(rfft_mem);
ft2_downsample_ctx_free(ctx);
return false;
} }
kiss_fft(ifft_cfg, band, band); for (int i = 0; i < ctx->nraw; ++i)
for (int i = 0; i < nfft2; ++i) timedata[i] = dec->ft2_raw[i];
out_samples[i] = band[i].r + I * band[i].i; kiss_fftr(rfft_cfg, timedata, ctx->spectrum);
free(timedata); free(timedata);
free(spectrum);
free(band);
free(filt);
free(rfft_mem); free(rfft_mem);
free(ifft_mem);
return nfft2; size_t ifft_mem_len = 0;
kiss_fft_alloc(ctx->nfft2, 1, NULL, &ifft_mem_len);
ctx->ifft_mem = malloc(ifft_mem_len);
ctx->ifft_cfg = kiss_fft_alloc(ctx->nfft2, 1, ctx->ifft_mem, &ifft_mem_len);
if (!ctx->ifft_cfg)
{
ft2_downsample_ctx_free(ctx);
return false;
}
return true;
} }
static float ft2_sync2d_score( static float ft2_sync2d_score(
@@ -550,7 +595,7 @@ static float ft2_sync2d_score(
return score; return score;
} }
static void ft2_normalize_downsampled(float complex* samples, int n_samples) static void ft2_normalize_downsampled(float complex* samples, int n_samples, int ref_count)
{ {
float power = 0.0f; float power = 0.0f;
for (int i = 0; i < n_samples; ++i) for (int i = 0; i < n_samples; ++i)
@@ -559,7 +604,9 @@ static void ft2_normalize_downsampled(float complex* samples, int n_samples)
} }
if (power <= 0.0f) if (power <= 0.0f)
return; return;
float scale = sqrtf((float)n_samples / power); if (ref_count <= 0)
ref_count = n_samples;
float scale = sqrtf((float)ref_count / power);
for (int i = 0; i < n_samples; ++i) for (int i = 0; i < n_samples; ++i)
{ {
samples[i] *= scale; samples[i] *= scale;
@@ -568,10 +615,14 @@ static void ft2_normalize_downsampled(float complex* samples, int n_samples)
static int ft2_find_scan_hits( static int ft2_find_scan_hits(
const ft8_decoder_t* dec, const ft8_decoder_t* dec,
const ft2_downsample_ctx_t* downsample_ctx,
ft2_scan_hit_t* out, ft2_scan_hit_t* out,
int max_hits, int max_hits,
ft2_scan_stats_t* stats) ft2_scan_stats_t* stats)
{ {
if (!downsample_ctx)
return 0;
ft2_raw_candidate_t peaks[FT2_MAX_RAW_CANDIDATES]; ft2_raw_candidate_t peaks[FT2_MAX_RAW_CANDIDATES];
int n_peaks = ft2_find_frequency_peaks(dec, peaks, FT2_MAX_RAW_CANDIDATES); int n_peaks = ft2_find_frequency_peaks(dec, peaks, FT2_MAX_RAW_CANDIDATES);
if (stats) if (stats)
@@ -589,8 +640,7 @@ static int ft2_find_scan_hits(
if (n_peaks <= 0) if (n_peaks <= 0)
return 0; return 0;
const int nraw = dec->ft2_raw_len; const int nfft2 = downsample_ctx ? downsample_ctx->nfft2 : 0;
const int nfft2 = nraw / FT2_NDOWN;
float complex* down = (float complex*)malloc(sizeof(float complex) * nfft2); float complex* down = (float complex*)malloc(sizeof(float complex) * nfft2);
float complex sync_wave[4][64]; float complex sync_wave[4][64];
float complex tweak_wave[33][64]; float complex tweak_wave[33][64];
@@ -599,10 +649,10 @@ static int ft2_find_scan_hits(
int count = 0; int count = 0;
for (int peak = 0; peak < n_peaks && count < max_hits; ++peak) for (int peak = 0; peak < n_peaks && count < max_hits; ++peak)
{ {
int produced = ft2_downsample_candidate(dec, peaks[peak].freq_hz, down, nfft2); int produced = ft2_downsample_candidate(downsample_ctx, peaks[peak].freq_hz, down, nfft2);
if (produced <= 0) if (produced <= 0)
continue; continue;
ft2_normalize_downsampled(down, produced); ft2_normalize_downsampled(down, produced, produced);
float best_score = -1.0f; float best_score = -1.0f;
int best_start = 0; int best_start = 0;
@@ -1010,7 +1060,7 @@ static bool ft2_unpack_message(const uint8_t plain174[], ftx_message_t* message)
} }
static bool ft2_decode_hit( static bool ft2_decode_hit(
const ft8_decoder_t* dec, const ft2_downsample_ctx_t* downsample_ctx,
const ft2_scan_hit_t* hit, const ft2_scan_hit_t* hit,
ftx_message_t* message, ftx_message_t* message,
float* dt_s, float* dt_s,
@@ -1019,6 +1069,9 @@ static bool ft2_decode_hit(
ft2_fail_stage_t* fail_stage, ft2_fail_stage_t* fail_stage,
ft2_pass_diag_t* pass_diag) ft2_pass_diag_t* pass_diag)
{ {
if (!downsample_ctx)
return false;
if (fail_stage) if (fail_stage)
*fail_stage = FT2_FAIL_NONE; *fail_stage = FT2_FAIL_NONE;
if (pass_diag) if (pass_diag)
@@ -1030,8 +1083,7 @@ static bool ft2_decode_hit(
pass_diag->dmin[i] = INFINITY; pass_diag->dmin[i] = INFINITY;
} }
} }
const int nraw = dec->ft2_raw_len; const int nfft2 = downsample_ctx ? downsample_ctx->nfft2 : 0;
const int nfft2 = nraw / FT2_NDOWN;
float complex* cd2 = (float complex*)malloc(sizeof(float complex) * nfft2); float complex* cd2 = (float complex*)malloc(sizeof(float complex) * nfft2);
float complex* cb = (float complex*)malloc(sizeof(float complex) * nfft2); float complex* cb = (float complex*)malloc(sizeof(float complex) * nfft2);
float complex signal[FT2_FRAME_SAMPLES]; float complex signal[FT2_FRAME_SAMPLES];
@@ -1047,7 +1099,7 @@ static bool ft2_decode_hit(
ft2_prepare_sync_waveforms(sync_wave, tweak_wave); ft2_prepare_sync_waveforms(sync_wave, tweak_wave);
int produced = ft2_downsample_candidate(dec, hit->freq_hz, cd2, nfft2); int produced = ft2_downsample_candidate(downsample_ctx, hit->freq_hz, cd2, nfft2);
if (produced <= 0) if (produced <= 0)
{ {
if (fail_stage) if (fail_stage)
@@ -1056,7 +1108,7 @@ static bool ft2_decode_hit(
free(cb); free(cb);
return false; return false;
} }
ft2_normalize_downsampled(cd2, produced); ft2_normalize_downsampled(cd2, produced, produced);
float best_score = -1.0f; float best_score = -1.0f;
int best_start = hit->start; int best_start = hit->start;
@@ -1095,7 +1147,7 @@ static bool ft2_decode_hit(
return false; return false;
} }
produced = ft2_downsample_candidate(dec, corrected_freq_hz, cb, nfft2); produced = ft2_downsample_candidate(downsample_ctx, corrected_freq_hz, cb, nfft2);
if (produced <= 0) if (produced <= 0)
{ {
if (fail_stage) if (fail_stage)
@@ -1104,7 +1156,7 @@ static bool ft2_decode_hit(
free(cb); free(cb);
return false; return false;
} }
ft2_normalize_downsampled(cb, produced); ft2_normalize_downsampled(cb, produced, FT2_FRAME_SAMPLES);
ft2_extract_signal_region(cb, produced, best_start, signal); ft2_extract_signal_region(cb, produced, best_start, signal);
if (!ft2_extract_bitmetrics_raw(signal, bitmetrics)) if (!ft2_extract_bitmetrics_raw(signal, bitmetrics))
@@ -1331,9 +1383,15 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
if (is_ft2) if (is_ft2)
{ {
ft2_downsample_ctx_t downsample_ctx;
if (!ft2_downsample_ctx_init(dec, &downsample_ctx))
{
LOG(LOG_WARN, "FT2 decode: downsample context init failed\n");
return 0;
}
ft2_scan_hit_t hit_list[FT2_MAX_SCAN_HITS]; ft2_scan_hit_t hit_list[FT2_MAX_SCAN_HITS];
ft2_scan_stats_t scan_stats; ft2_scan_stats_t scan_stats;
int num_hits = ft2_find_scan_hits(dec, hit_list, FT2_MAX_SCAN_HITS, &scan_stats); int num_hits = ft2_find_scan_hits(dec, &downsample_ctx, hit_list, FT2_MAX_SCAN_HITS, &scan_stats);
int fail_refined_sync = 0; int fail_refined_sync = 0;
int fail_freq_range = 0; int fail_freq_range = 0;
int fail_final_downsample = 0; int fail_final_downsample = 0;
@@ -1354,7 +1412,7 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
float snr_db = -21.0f; float snr_db = -21.0f;
ft2_fail_stage_t fail_stage = FT2_FAIL_NONE; ft2_fail_stage_t fail_stage = FT2_FAIL_NONE;
ft2_pass_diag_t pass_diag; ft2_pass_diag_t pass_diag;
if (!ft2_decode_hit(dec, hit, &message, &time_sec, &freq_hz, &snr_db, &fail_stage, &pass_diag)) if (!ft2_decode_hit(&downsample_ctx, hit, &message, &time_sec, &freq_hz, &snr_db, &fail_stage, &pass_diag))
{ {
for (int pass = 0; pass < 5; ++pass) for (int pass = 0; pass < 5; ++pass)
{ {
@@ -1472,6 +1530,7 @@ int ft8_decoder_decode(ft8_decoder_t* dec, ft8_decode_result_t* out, int max_res
isfinite(pass_best_dmin[2]) ? pass_best_dmin[2] : -1.0f, isfinite(pass_best_dmin[2]) ? pass_best_dmin[2] : -1.0f,
isfinite(pass_best_dmin[3]) ? pass_best_dmin[3] : -1.0f, isfinite(pass_best_dmin[3]) ? pass_best_dmin[3] : -1.0f,
isfinite(pass_best_dmin[4]) ? pass_best_dmin[4] : -1.0f); isfinite(pass_best_dmin[4]) ? pass_best_dmin[4] : -1.0f);
ft2_downsample_ctx_free(&downsample_ctx);
} }
else else
{ {