Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run_ctest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: drowe67/radae
ref: dr-radev2
ref: dr-tx-bpf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be reset prior to merge?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll set it back to radae/main eventually, but I have a little more work to go in dr-tx-bpf before we merge.

path: ${{github.workspace}}/radae

- name: Build radae
Expand Down
8 changes: 4 additions & 4 deletions src/radae_rx.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void usage(void) {

int main(int argc, char *argv[]) {
int opt;
char *model_name = "model19_check3/checkpoints/checkpoint_epoch_100.pth";
char *model_name = "(unused, built-in weights)";
int flags = 0;
float disable_unsync = 0.0f;
char *snr_est_fn = NULL;
Expand Down Expand Up @@ -160,7 +160,7 @@ int main(int argc, char *argv[]) {
float *snr_log = NULL;

/* Main processing loop */
int frame_count = 0;
int sym_count = 0;
int valid_count = 0;
while (1) {
int nin = rade_nin(r);
Expand Down Expand Up @@ -200,10 +200,10 @@ int main(int argc, char *argv[]) {
snr_log[snr_log_size++] = rade_snrdB_3k_est(r);
}

frame_count++;
sym_count++;
}

fprintf(stderr, "Processed %d modem frames, %d valid outputs\n", frame_count, valid_count);
fprintf(stderr, "Processed %d input OFDM symbols, %d valid outputs\n", sym_count, valid_count);

if (snr_est_fn && snr_log) {
FILE *f = fopen(snr_est_fn, "wb");
Expand Down
4 changes: 2 additions & 2 deletions src/radae_tx.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void usage(void) {

int main(int argc, char *argv[]) {
int opt;
char *model_name = "model19_check3/checkpoints/checkpoint_epoch_100.pth";
char *model_name = "(unused, built-in weights)";
int flags = 0;

static struct option long_options[] = {
Expand Down Expand Up @@ -137,7 +137,7 @@ int main(int argc, char *argv[]) {
fwrite(eoo_out, sizeof(RADE_COMP), n_out, stdout);

// extra silence buf to let Rx finish processing EOO
memset(eoo_out,0,sizeof(eoo_out));
memset(eoo_out, 0, sizeof(RADE_COMP) * n_out);
fwrite(eoo_out, sizeof(RADE_COMP), n_out, stdout);

fprintf(stderr, "Transmitted %d modem frames + EOO\n", frame_count);
Expand Down
19 changes: 17 additions & 2 deletions src/rade_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ struct rade *rade_open(char model_file[], int flags) {
fprintf(stderr, "rade_open: model_file=%s (ignored, using built-in weights)\n", model_file);

if (flags & RADE_MODE_V2) {
/* Initialize V2 transmitter */
if (rade_tx_v2_init(&r->tx_v2) != 0) {
/* Initialize V2 transmitter (SSB BPF enabled by default) */
int tx_bpf_en = (flags & RADE_NO_TX_BPF) ? 0 : 1;
if (rade_tx_v2_init(&r->tx_v2, tx_bpf_en) != 0) {
fprintf(stderr, "rade_open: failed to initialize V2 transmitter\n");
free(r);
return NULL;
Expand Down Expand Up @@ -246,6 +247,20 @@ float rade_snrdB_3k_est(struct rade *r) {
return (float)rade_rx_snrdB_3k_est(&r->rx);
}

void rade_get_stats(struct rade *r, struct rade_stats *stats) {
assert(r != NULL);
assert(stats != NULL);
memset(stats, 0, sizeof(*stats));
if (r->flags & RADE_MODE_V2) {
stats->sync = (r->rx_v2.state == RADE_RX_V2_SYNC);
stats->delta_hat = r->rx_v2.delta_hat;
stats->delta_hat_g = r->rx_v2.delta_hat_g;
stats->freq_offset = r->rx_v2.freq_offset;
stats->gain = r->rx_v2.gain;
stats->snr_est = r->rx_v2.snr_est_dB;
}
}

void rade_set_disable_unsync(struct rade *r, float seconds) {
assert(r != NULL);
if (r->flags & RADE_MODE_V2) return; /* not supported in V2 */
Expand Down
15 changes: 15 additions & 0 deletions src/rade_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ extern "C" {
#define RADE_MODE_V2 0x10 // select RADE V2 (default is V1)
#define RADE_VERBOSE_TERSE 0x20 // terse per-frame status (state, sig, f_off, snr, eoo)
#define RADE_VERBOSE_FULL 0x40 // full per-frame status (all fields)
#define RADE_NO_TX_BPF 0x80 // V2 only: disable Tx SSB BPF (default enabled)

// Must be called BEFORE any other RADE functions as this
// initializes internal library state.
Expand Down Expand Up @@ -170,6 +171,20 @@ RADE_EXPORT float rade_freq_offset(struct rade *r);
// returns the current SNR estimate (in dB) of the Rx signal ( when rade_sync()!=0 )
RADE_EXPORT float rade_snrdB_3k_est(struct rade *r);

// V2 only: per-symbol receiver diagnostics, valid after each rade_rx() call.
// Fields mirror rx2.py's diagnostic log (delta_hat/delta_hat_g/freq_offset/
// gain/snr_est) -- useful for plotting sync/timing/AGC behaviour over a file,
// e.g. to diagnose a false-sync-then-reacquire event. All fields are 0 for V1.
struct rade_stats {
int sync; // 0 = idle, 1 = sync
float delta_hat; // IIR-smoothed timing offset (samples)
float delta_hat_g; // instantaneous timing offset (samples)
float freq_offset; // IIR-smoothed frequency offset (Hz)
float gain; // AGC gain applied to the current symbol
float snr_est; // SNR estimate (dB)
};
RADE_EXPORT void rade_get_stats(struct rade *r, struct rade_stats *stats);

// test mode: disable unsync after this many seconds (0 = disabled)
RADE_EXPORT void rade_set_disable_unsync(struct rade *r, float seconds);

Expand Down
1 change: 1 addition & 0 deletions src/rade_rx_v2.c
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ int rade_rx_v2_process(rade_rx_v2_state *rx, float *features_out,
/* --- AGC --- */
RADE_COMP rx_scaled[RADE_V2_SYM_LEN + TIMING_SHIFT];
float gain = compute_gain(rx, rx_samples, nin);
rx->gain = gain;
if (gain != 1.0f) {
for (int i = 0; i < nin; i++) {
rx_scaled[i].real = rx_samples[i].real * gain;
Expand Down
1 change: 1 addition & 0 deletions src/rade_rx_v2.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ typedef struct {
int agc_en;
float agc_target;
float agc_power;
float gain; /* AGC gain applied to the current symbol */

/* Timing / frequency tracking */
float delta_hat; /* IIR-smoothed timing offset */
Expand Down
70 changes: 58 additions & 12 deletions src/rade_rx_wav.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,13 @@ static void usage(void) {
" -h, --help Show this help\n"
" -v LEVEL Verbosity: 0=quiet 1=normal (default) 2=verbose\n"
" -f FEATURES Write RX features to disk"
" --v2 Use RADE V2 (default: V1)\n",
" --v2 Use RADE V2 (default: V1)\n"
" --write_state FILE V2 only: per-symbol sync state (0=idle,1=sync), .int16\n"
" --write_delta_hat FILE V2 only: per-symbol timing offset, .f32\n"
" --write_delta_hat_g FILE V2 only: per-symbol instantaneous timing offset, .f32\n"
" --write_freq_offset FILE V2 only: per-symbol freq offset (Hz), .f32\n"
" --write_gain FILE V2 only: per-symbol AGC gain, .f32\n"
" --write_snr_est FILE V2 only: per-symbol SNR estimate (dB), .f32\n",
RADE_FS, RADE_FS_SPEECH);
}

Expand All @@ -173,14 +179,23 @@ int main(int argc, char *argv[]) {
int use_v2 = 0;
int opt;
FILE* feature_fp = NULL;
FILE* state_fp = NULL, *delta_hat_fp = NULL, *delta_hat_g_fp = NULL;
FILE* freq_offset_fp = NULL, *gain_fp = NULL, *snr_est_fp = NULL;
static struct option long_options[] = {
{"help", no_argument, NULL, 'h'},
{"v2", no_argument, NULL, 1 },
{"f", required_argument, NULL, 'f'},
{NULL, 0, NULL, 0 }
{"help", no_argument, NULL, 'h'},
{"v2", no_argument, NULL, 1 },
{"f", required_argument, NULL, 'f'},
{"write_state", required_argument, NULL, 2 },
{"write_delta_hat", required_argument, NULL, 3 },
{"write_delta_hat_g", required_argument, NULL, 4 },
{"write_freq_offset", required_argument, NULL, 5 },
{"write_gain", required_argument, NULL, 6 },
{"write_snr_est", required_argument, NULL, 7 },
{NULL, 0, NULL, 0 }
};

while ((opt = getopt_long(argc, argv, "hv:f:", long_options, NULL)) != -1) {
FILE **diag_fp = NULL;
switch (opt) {
case 'h': usage(); return 0;
case 'v': verbose = atoi(optarg); break;
Expand All @@ -193,8 +208,22 @@ int main(int argc, char *argv[]) {
}
break;
case 1: use_v2 = 1; break;
case 2: diag_fp = &state_fp; break;
case 3: diag_fp = &delta_hat_fp; break;
case 4: diag_fp = &delta_hat_g_fp; break;
case 5: diag_fp = &freq_offset_fp; break;
case 6: diag_fp = &gain_fp; break;
case 7: diag_fp = &snr_est_fp; break;
default: usage(); return 1;
}
if (diag_fp) {
*diag_fp = fopen(optarg, "wb");
if (!*diag_fp) {
perror("Could not open diagnostic output file");
usage();
return 1;
}
}
}
if (argc - optind != 2) { usage(); return 1; }

Expand Down Expand Up @@ -276,7 +305,7 @@ int main(int argc, char *argv[]) {
else if (verbose >= 3) flags |= RADE_VERBOSE_FULL;
if (use_v2) flags |= RADE_MODE_V2;
/* model_name is ignored; built-in weights are used */
char *model_name = "model19_check3/checkpoints/checkpoint_epoch_100.pth";
char *model_name = "(unused, built-in weights)";
struct rade *r = rade_open(model_name, flags);
if (!r) {
fprintf(stderr, "rade_demod: rade_open failed\n");
Expand Down Expand Up @@ -323,7 +352,7 @@ int main(int argc, char *argv[]) {

/* ---------------------------------------------------- demodulation loop */
long iq_pos = 0;
int mf_count = 0; /* modem frames fed to RX */
int sym_count = 0; /* input OFDM symbols fed to RX */
int vld_count = 0; /* valid feature outputs */
float snr_sum = 0.0f; /* accumulate SNR while in sync */

Expand All @@ -332,7 +361,7 @@ int main(int argc, char *argv[]) {
long remaining = n_8k - iq_pos;

/* Copy samples into rx_buf; zero-pad the final short block so the
last modem frame has a chance to flush. */
last symbol has a chance to flush. */
if (remaining < nin) {
memset(rx_buf, 0, (size_t)nin * sizeof(RADE_COMP));
memcpy(rx_buf, &iq[iq_pos], (size_t)remaining * sizeof(RADE_COMP));
Expand All @@ -345,8 +374,19 @@ int main(int argc, char *argv[]) {
int has_eoo = 0;
int n_out = rade_rx(r, feat_buf, &has_eoo, eoo_buf, rx_buf);

if (state_fp || delta_hat_fp || delta_hat_g_fp || freq_offset_fp || gain_fp || snr_est_fp) {
struct rade_stats stats;
rade_get_stats(r, &stats);
if (state_fp) { int16_t v = (int16_t)stats.sync; fwrite(&v, sizeof(v), 1, state_fp); }
if (delta_hat_fp) fwrite(&stats.delta_hat, sizeof(float), 1, delta_hat_fp);
if (delta_hat_g_fp) fwrite(&stats.delta_hat_g, sizeof(float), 1, delta_hat_g_fp);
if (freq_offset_fp) fwrite(&stats.freq_offset, sizeof(float), 1, freq_offset_fp);
if (gain_fp) fwrite(&stats.gain, sizeof(float), 1, gain_fp);
if (snr_est_fp) fwrite(&stats.snr_est, sizeof(float), 1, snr_est_fp);
}

if (has_eoo && verbose >= 1)
fprintf(stderr, "End-of-over at modem frame %d\n", mf_count);
fprintf(stderr, "End-of-over at input OFDM symbol %d\n", sym_count);

if (n_out > 0) {
vld_count++;
Expand Down Expand Up @@ -400,7 +440,7 @@ int main(int argc, char *argv[]) {
total_bytes += (uint32_t)(LPCNET_FRAME_SIZE * (int)sizeof(int16_t));
}
}
mf_count++;
sym_count++;
}

/* -------------------------------------------------------- finalise WAV */
Expand All @@ -411,8 +451,8 @@ int main(int argc, char *argv[]) {
/* ------------------------------------------------------------ summary */
if (verbose >= 1) {
float snr_mean = vld_count ? snr_sum / vld_count : 0.0f;
fprintf(stderr, "Modem frames: %d valid: %d SNR: %.1f dB\n",
mf_count, vld_count, snr_mean);
fprintf(stderr, "Input OFDM symbols: %d valid: %d SNR: %.1f dB\n",
sym_count, vld_count, snr_mean);
fprintf(stderr, "Output: %s %.1f s (%u bytes)\n",
output_file, (double)total_bytes / (2.0 * RADE_FS_SPEECH), total_bytes);
}
Expand All @@ -421,6 +461,12 @@ int main(int argc, char *argv[]) {
if (feature_fp) {
fclose(feature_fp);
}
if (state_fp) fclose(state_fp);
if (delta_hat_fp) fclose(delta_hat_fp);
if (delta_hat_g_fp) fclose(delta_hat_g_fp);
if (freq_offset_fp) fclose(freq_offset_fp);
if (gain_fp) fclose(gain_fp);
if (snr_est_fp) fclose(snr_est_fp);

free(iq);
free(rx_buf);
Expand Down
23 changes: 21 additions & 2 deletions src/rade_tx_v2.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
#include "rade_enc_v2_data.h"
#include <string.h>

int rade_tx_v2_init(rade_tx_v2_state *tx) {
int rade_tx_v2_init(rade_tx_v2_state *tx, int bpf_en) {
memset(tx, 0, sizeof(*tx));

if (init_radeencv2(&tx->enc_model, radeencv2_arrays) != 0)
Expand All @@ -47,6 +47,14 @@ int rade_tx_v2_init(rade_tx_v2_state *tx) {
rade_v2_ofdm_init(&tx->ofdm);
tx->data_symbol = -1.0f;

tx->bpf_en = bpf_en;
if (bpf_en) {
float bandwidth = 2700.0f - 300.0f;
float centre = (2700.0f + 300.0f) / 2.0f;
Comment thread
tmiw marked this conversation as resolved.
int max_len = RADE_V2_NEOO > RADE_V2_NMF ? RADE_V2_NEOO : RADE_V2_NMF;
rade_bpf_init(&tx->bpf, RADE_BPF_NTAP, (float)RADE_FS, bandwidth, centre, max_len);
}

return 0;
}

Expand Down Expand Up @@ -84,7 +92,13 @@ int rade_tx_v2_process(rade_tx_v2_state *tx, RADE_COMP *tx_out, const float *fea
rade_core_encoder_v2(&tx->enc_state, &tx->enc_model, z, enc_features, arch);

/* Modulate: z -> IQ samples */
return rade_v2_ofdm_mod_frame(&tx->ofdm, tx_out, z);
int n_out = rade_v2_ofdm_mod_frame(&tx->ofdm, tx_out, z);

if (tx->bpf_en) {
rade_bpf_process(&tx->bpf, tx_out, tx_out, n_out);
}

return n_out;
}

void rade_tx_v2_set_data_symbol(rade_tx_v2_state *tx, float symbol) {
Expand All @@ -95,5 +109,10 @@ int rade_tx_v2_eoo(rade_tx_v2_state *tx, RADE_COMP *tx_out) {
int n_eoo;
const RADE_COMP *eoo = rade_v2_ofdm_get_eoo(&tx->ofdm, &n_eoo);
memcpy(tx_out, eoo, sizeof(RADE_COMP) * n_eoo);

if (tx->bpf_en) {
rade_bpf_process(&tx->bpf, tx_out, tx_out, n_eoo);
}

return n_eoo;
}
11 changes: 10 additions & 1 deletion src/rade_tx_v2.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include "rade_enc_v2.h"
#include "rade_enc_v2_data.h"
#include "rade_v2_ofdm.h"
#include "rade_bpf.h"

#ifdef __cplusplus
extern "C" {
Expand All @@ -49,11 +50,19 @@ typedef struct {
RADEEncV2State enc_state;
rade_v2_ofdm ofdm;
float data_symbol; /* BPSK data symbol (+1.0 or -1.0), default -1.0 */
int bpf_en;
rade_bpf bpf; /* SSB BPF state, continuous across rade_tx_v2_process()
and rade_tx_v2_eoo() calls -- matches radae_v2.py's
RADEv2Transmitter, which filters data and EOO through
the same persistent filter (a real SSB radio's front
end doesn't distinguish data samples from EOO ones) */
} rade_tx_v2_state;

/* Initialise V2 transmitter (loads built-in weights).
bpf_en: 1 to enable Tx SSB bandpass filter (300-2700Hz, matches radae_v2.py's
ssb_bpf -- the model was trained under this filter).
Returns 0 on success, -1 on failure. */
int rade_tx_v2_init(rade_tx_v2_state *tx);
int rade_tx_v2_init(rade_tx_v2_state *tx, int bpf_en);

/* Number of input feature values per modem frame (4 frames x 36 floats = 144) */
int rade_tx_v2_n_features_in(void);
Expand Down
Loading
Loading