C Port of RADE V2 - #14
Conversation
- rade_v2_core.h: shared typedefs and function declarations
- rade_v2_constants.h: generated constants (latent_dim=56, fps=4, features=21)
- rade_enc_v2.{h,c} + data: CoreEncoderStatefull inference (DenseNet, 5x GRU+dilated conv)
- rade_dec_v2.{h,c} + data: CoreDecoderStatefull inference (DenseNet, 5x GRU+GLU+conv)
- rade_sync.{h,c} + data: FrameSyncNet inference (3-layer feedforward)
- rade_enc_v2_test.c, rade_dec_v2_test.c: stdin/stdout test tools
- opus-nnet.c.diff: raise MAX_CONV_INPUTS_ALL to 2048 for V2 decoder conv5 (896 ch * k=2)
- BuildOpus.cmake: apply opus-nnet.c.diff patch during build
- CMakeLists.txt: add RADE_V2_ML_SOURCES to librade, add test executables
Encoder and decoder validated against Python reference (n() disabled):
max diff ~0.014 (float32 arithmetic + int8 quantisation noise).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model 250725 trained with bottleneck=0 (no tanh on z_dense output). ACTIVATION_TANH was producing z≈±0.76 instead of natural range, causing decoder loss ~7.5. Linear activation gives loss 0.096. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C port of tx2.py/RADEv2Transmitter. V2 frame structure is simpler than V1: Ns=2 pure data symbols per frame (no pilots), Nc=14, M=128, Ncp=32. EOO is 6 x pend_cp symbols scaled by pilot_gain_eoo_v2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C port of rx2.py / RADEv2Receiver: - rade_rx_v2.h/.c: full receiver state machine — CP autocorrelation, signal/sine detection, IIR timing+freq tracking, symbol extraction, OFDM demod via rade_v2_ofdm_demod_frame, FrameSyncNet even/odd frame sync, EOO detection via channel sparsity metric, stateful decoder via rade_core_decoder_v2 - radae_v2_rx_nopy.c: CLI binary (nin-based read loop, features to stdout) - CMakeLists.txt: adds rade_rx_v2.c to RADE_V2_DSP_SOURCES, adds radae_v2_rx executable target Verified: C tx -> C rx pipeline, loss 0.083 (threshold 0.2), n_acq=1, EOO detected correctly at noiseless SNR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add rade_v2_ofdm_demod_frame() and rade_v2_ofdm_eoo_metric() needed by the V2 receiver. Also adds DFT matrix (Wfwd), per-carrier phase correction, and pend_td fields to the rade_v2_ofdm struct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@peterbmarks @tmiw - thinking about naming conventions for V1 & V2 code in this repo. So Claude has come up with In drowe67/radae#70 I've renamed the ctests |
Adds --write_snr_est <filename> flag to write per-symbol snr_est_dB values to a binary float32 file, matching rx2.py behaviour. Used by ctests radae_nopy_v2_rx_snr_high and radae_nopy_v2_rx_snr_low. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I think rade_vX_tx etc is good. (Leaves the door open to v3) |
You mean the API? Lets take a look at that next. |
|
Using a version in the function names would be good. Also, what about adding a function to the existing API to indicate which version to use (and then eventually default to using the RADEV2 implementations or something)? For example: struct rade* rade = rade_open(...); // uses RADEV1, will eventually be the same as rade_open_with_version(RADE_VERSION_2, ...)
struct rade* rade2 = rade_open_with_version(RADE_VERSION_2, ...);Then internally (for example): struct rade* rade_open_with_version(int version, ...) {
struct rade* result = malloc(sizeof(struct rade));
if (version == RADE_VERSION_1) result->impl = rade_v1_get_impl();
else if (version == RADE_VERSION_2) result->impl = rade_v2_get_impl();
...
// additional initialization here
return result;
}
int rade_tx(struct rade* rade, ...) {
return rade->impl.tx(rade, ...);
}Anyway, just thinking from the perspective of RADEV2 being a thing for potentially quite a while before there's a V3. If we want something quick we can just have e.g. EDIT: whoops, I think @drowe67 and I commented at the same time. We can look at rade_api after for sure. |
That's fine - what you are brainstorming is the topic I was suggesting we discuss. TBH I'm not as worried about breaking the API at this stage - it's pretty early days and the fix (like adding a version argument to But I'm not mandating anything atm - lets ponder it and form a consensus. |
|
I'm not sure that sharing rade_open returning a pointer to the rade structure can work as that struct might change with future versions. It contains rade_tx_state and rade_rx_state and they could also be different. Maybe we should just go with rade and rade2? Also, it's good to build a few clients apps that use the API and collect ideas of what the API should expose. |
There could also be a private state inside the struct that could be initialized/used in a version dependent manner. But as mentioned, if we're already going to be breaking compatibility anyway... |
|
@tmiw @peterbmarks - I'm working on the V2 API changes.
|
…float; remove non-API V2 apps - rade_api.h: add RADE_MODE_V2 flag, V2 state fields in struct rade, declare rade_tx_set_data_symbol(), rade_rx_get_data_symbol(), rade_snr_est_float() - rade_api_nopy.c: dispatch all API functions to V1 or V2 based on RADE_MODE_V2 flag - rade_tx_v2/rade_rx_v2: add data_symbol field and getter/setter - radae_tx_nopy.c / radae_rx_nopy.c: add --v2 flag; radae_rx adds --write_snr_est - CMakeLists.txt: remove radae_v2_tx/radae_v2_rx build targets; V1 and V2 now both accessed exclusively through the API programs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
My apps use this method: |
|
@peterbmarks - I can't seem to find |
|
Hmm, I see what you mean. A wrapper that simply wraps rade_snrdB_3k_est(); The type conversion might generate a warning but will be fine. |
We can try it. freedv-gui builds with |
…loat rade_snr_est_float() was redundant once rade_snrdB_3k_est() returns float. radae_rx_nopy.c updated to use rade_snrdB_3k_est() for SNR logging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V2 is now accessed exclusively through the API programs (radae_tx --v2 / radae_rx --v2), giving symmetry with V1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop _nopy suffix: rade_api_nopy.c→rade_api.c, radae_tx/rx_nopy.c→radae_tx/rx.c - README: update title/intro for V1+V2, restructure demo section with IQ pipeline first, WAV tools as convenience wrappers, add V2 examples, update file tables and directory tree Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@peterbmarks - I've changed a few file names, removing the While the move away from Python was a very big deal at the time (and a major innovation - thanks again) it's backstory we probably don't need to emphasize with new users of the C library implementation of RADE. Pls see b9f2433 and tell me if this is OK with you. Please also see the README, which has been revamped for V2. @tmiw - pls feel free to comment on the README too. |
|
Yes, that's great @drowe67. I agree we have moved on from the python days and there's no need to draw attention to it except for history purposes perhaps. Quite an interesting technical story of prototyping in python and using an LLM to port to C with good success. |
Round-trip text tests exercising the BPSK data symbol API (V2) and EOO callsign channel (V1) entirely through the public rade_api.h interface - no Python, no external files required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames rade_modulate_wav → rade_tx_wav and rade_demod_wav → rade_rx_wav for consistency with the radae_tx/radae_rx naming convention. Adds mean SNR (while in sync) to the rade_rx_wav summary line: Modem frames: 84 valid: 79 SNR: 31.5 dB Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updates stale filenames in header comments, usage strings, and inline comments to reflect the reframe as a C port rather than a Python-free variant. Removes the unused RADE_PYTHON_FREE compile definition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@tmiw @peterbmarks - should the For example (rx side): |
|
@peterbmarks - are your results "over the cable" i.e. two HF radios, attenuators, linked via coax etc? |
|
My results are via WAV recordings. I'm just working on integrating the new c repo but have a two radio setup ready to go. |
|
I used the |
|
I've just done a loss test really over the air. From my radio shack about 20m away running 1W to a radio in my computer room with a whip antenna. RadeV1 at this stage. (.venv) marksp@Mac FreeDVNeo % python loss.py overtheair/features.f32 overtheair/features_decodedv1_air.f32 --clip_start 10 --plot
This is the macOS client now built with the new race_c repo. https://github.com/freedv/rade_c |
@tmiw - so this was a software only (no hardware in the loop) test? How do you reconcile this high loss result with the successful freedv-backend results above with loss ~ 0.08? |
|
@tmiw - with your OTC tests - are the Tx and Rx radios physically separate (like different rooms) or on the test bench? RF tends to jump across if you are not careful. One test is to click down a few attenuating steps on the Rx side - and check the Rx signal level (e.g. s-meter/SDR or spec-an used to monitor test) drops by the same amount. If not - the signal is getting in via a parasitic path. Liberal ferrites on the USB cables is a good idea too. @Tyrbiter - pls advise/help @tmiw with suggestions. I'm setting up my OTC test today. |
Correct, no HW. Not sure yet, I'm currently investigating. Right now I don't think RNNoise/AGC are having much impact on this particular test, so I'm looking into other possibilities. |
|
I'm puzzled about why disabling RNNoise and AGC, which both affect microphone in audio, would affect the features -> signal -> features loss measurement. |
|
Hi all I think that @tmiw probably needs to fully debug his software only tests first, but I would definitely agree that RF testing via cables is likely to cause problems unless care is taken. It's best to use the lowest possible transmit power and probably to put a fixed attenuator immediately on the output of the Tx radio. Be aware that bayonet connectors can be pretty leaky, although at HF this should be better than at UHF. If possible try to use threaded connectors if you have them, SMA will do but maybe N is better if available. You can also determine if there is a leakage path by moving things about and running your fingers along the cables, if any energy has got onto the coax screen then fingers will perturb it and you should see changes in signal levels on the Rx. Be careful with cable screen coverage, if they have <100% screen coverage then they will leak (this is exactly what a leaky feeder antenna system does). Of course if there is leakage then a typical symptom will be that the RSSI will tend to not decrease past a certain point, but even then it might be that if a leakage signal is even 10-15dB below the wanted signal that it's enough to cause a partial cancellation and give a rise in BER (although of course we can't actually measure that directly). I have been forced to use a screened chamber as one end of a test system a number of times in the past to get the isolation to adequate levels, you also need to think about the potential for off-air signals to get in to the Rx, a careful look at the noise level on the display with a dummy load connected is a good way of seeing what happens without the antenna/cable and then you can find a quiet spot to remove the radios from the immediate equation. It might also be sensible to turn off Rx preamps as that means a lower sensitivity which can be compensated for by reducing the attenuator settings which gives a further check that all things are equal. HTH. |
|
Thanks @Tyrbiter I've only got about 20dB of in-line attenuation so haven't plugged radios together directly yet. I'm guessing about 100dB would be wise but, as you say, there may be leakage. So far I've tested pure audio (encode to wav saving features, decode from wave saving features) loss.py on the two feature file. I'm getting "loss: 0.084 start: 134 acq_time: 0.34 s" which is similar to David's pure command line. |
|
@tmiw it would be useful if we had a description of the signal processing steps (e.g. a block diagram) in the test and freedv-gui(-backend). There is a lot going on and I gather this code is a common source or bugs and rework for you. Perhaps Claude can draw an ascii art or markdown block diagram that you could share? It would be great to show the signal processing blocks plus where features/audio signal I/O. |
Unfortunately it's not letting me share directly on OpenAI's website for some reason but I was able to get Claude to generate this: I also spent quite a while working with it last night to chase down other possibilities and didn't really get anywhere. I'll see if I can modify freedv-ka9q to generate RX features and compare that with what freedv-gui generated; hopefully it should produce results similar to |
|
Nice write up @tmiw & Claude ... take your time exploring the problem. It sounds like a subtle issue and it's really important we have good verification results before proceeding to the Phase D V2 tests. |
|
@tmiw - just checking that you're disabling all those signal processing steps between the source audio (file) and the RADE encoder? RNNNoise/AGC/EQ etc? Each one of them is source of bugs and could "color" the input audio compared to what we trained for. We want freedv-gui/backend to have a transfer function of 1 - it should look like a piece of wire. You should be able to do a MSE test on the buffers of audio from the audio file and at the input of the RADE encoder and find 0.0 difference. Claude and I have added some features to loss.py, e.g. PNG generation, histogram, and
|
|
So I tried a couple of OTC tests here, first with Results for
Results for
My experimental config (bear is a laptop, deep my desktop): On bear (Tx):
On deep (Rx):
Some notes:
|
|
I spent way too much time investigating this today and discovered the following:
Anyway, I forced a +6 dB gain in freedv-gui to match freedv-backend (diff below): diff --git a/src/pipeline/TxRxThread.cpp b/src/pipeline/TxRxThread.cpp
index 8fcae874..a101300b 100644
--- a/src/pipeline/TxRxThread.cpp
+++ b/src/pipeline/TxRxThread.cpp
@@ -297,7 +297,7 @@ void TxRxThread::initializePipeline_()
// TX attenuation step
auto txAttenuationStep = new LevelAdjustStep(outputSampleRate_, +[]() FREEDV_NONBLOCKING {
- return g_txLevelScale.load(std::memory_order_acquire);
+ return 2.0f; //g_txLevelScale.load(std::memory_order_acquire);
});
pipeline_->appendPipelineStep(txAttenuationStep);
}and am now getting the same loss figures for the |
The gain mis-match work sounds solid. However you haven't presented the software only test results. The loss values on the screens shots are illegible. Please present your results in a markdown table in the format above, including % error.
That result is puzzling and likely a bug, or experimental error, or the processing may be shaping the features in a way the reference tests don't. We need these switched off and freedv--gui/backed configured as a straight piece of wire, transfer function of 1 for the Phase D tests. Please present these results in your table as well.
Any issues with RADE need to be demonstrated with the RADE command line tools. |
|
@tmiw - I pulled on the 6dB level thread. While it doesn't explain your results (+6dB should make the loss worse), I did find the AGC was never ported from the Python reference. Working on a fix in PR #4 - thanks 🙂 Separately, unrelated to this - the comments above around disabling RNN/AGC and tabulating results still stand - thanks. |
|
I updated the I also reran the
Note all loss.py executions were done with |
|
Reverted the 6 dB gain (while keeping AGC and RNNoise disabled):
|
|
Thanks @tmiw - I think the first test freedv-gui (Tx) to freedv-gui (Rx) might be key. I feel we're zeroing in, and it might be time to come up with some concrete agreed steps that are a subset of what we have been doing so far. Suggest we leave it there for the moment, I'll get back to you in a day or so with a draft test plan for review. |
|
I think we should move this discussion to a PLT group thread. |
|
Here is the revised test procedure in drowe67/radae#76 |







Claude doing his magic, backed by ctests from drowe67/radae#70.
See https://github.com/drowe67/radae_nopy/tree/dr-radev2 for V2 C Port test results.
WIP - do not merge.
The on-air waveform is not frozen and is likely to change at anytime