Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9ce86d9
export_rade_v2_weights.py: export V2 encoder/decoder/sync weights to C
drowe67 May 2, 2026
e593c36
add V2 ctests and stateful encoder/decoder reference script
drowe67 May 2, 2026
220540c
add radae_nopy_v2_tx ctest
drowe67 May 2, 2026
3cc83fe
add radae_nopy_v2_rx ctest for V2 C receiver
drowe67 May 2, 2026
cb009b8
add ctests for V2 C receiver: AWGN and MPP at high and low SNR
drowe67 May 5, 2026
a10e0c2
ctests: add V2 C receiver EOO and SNR estimator tests
drowe67 May 5, 2026
f23de0d
hard code bottleneck=0 in rx2.py
drowe67 May 5, 2026
a843577
remove section of README as this is handled in radae_nopy now
drowe67 May 5, 2026
0b12799
README: add Testing RADE and Verifying RADE Integration sections
drowe67 May 7, 2026
9d8a947
updated V2 test report for Phase C crowd sourcing tests
drowe67 May 12, 2026
32762dd
Merge branch 'main' into dr-radev2
drowe67 May 12, 2026
ad892b9
Merge origin/main into dr-radev2, resolve README conflict
drowe67 May 12, 2026
5c56e01
inference.py: add 99.9% CCDF PAPR measurement alongside 100% peak PAPR
drowe67 Jun 9, 2026
fc353aa
updated README and V2 test report with PAPR 100% CCDF note
drowe67 Jun 10, 2026
7a9dfa2
Add RADE V2 WER test infrastructure
drowe67 Jul 2, 2026
188a249
asr_test_top.sh: add mode and -n argument parsing
drowe67 Jul 2, 2026
3684dfa
WER plot improvements and sweep infrastructure updates
drowe67 Jul 2, 2026
1441ede
asr: add fading_adv to ch, CPAPR column, n comment, EPS plot
drowe67 Jul 3, 2026
a029184
radae_plots: tune plot_wer_v2 EPS output for paper
drowe67 Jul 3, 2026
5683262
ASR test improvements: CPAPR column, V1 fix, fading file, plot tuning
drowe67 Jul 5, 2026
b7b3256
update V2 ctests to use API programs (radae_tx --v2 / radae_rx --v2)
drowe67 Jul 7, 2026
e812068
rade_snrdB_3k_est: change return type to float
drowe67 Jul 7, 2026
9aea255
add radae_nopy_v1_text and radae_nopy_v2_text ctests
drowe67 Jul 8, 2026
58f89ae
add radae_nopy_v1_wav ctest for WAV pipeline sanity check
drowe67 Jul 8, 2026
d5d7179
add radae_nopy_v2_wav ctest
drowe67 Jul 8, 2026
4593f81
add radev2_rx_wav.sh: decode V2 OTA WAV recordings with diagnostic plots
drowe67 Jul 9, 2026
563642e
Merge branch 'dr-radev2' of github.com:drowe67/radae into dr-radev2
drowe67 Jul 9, 2026
4f2b094
README: add radev2_rx_wav.sh section and Files entry
drowe67 Jul 9, 2026
80faa25
draft write up of Phase C stored file results, draft Phase D
drowe67 Jul 10, 2026
d794a40
edits to Phase D PTT
drowe67 Jul 11, 2026
f839863
test: add snr_loss_curve.sh - RADE V2 loss vs SNR3k calibration tool
drowe67 Jul 14, 2026
de38097
README: update C port links from peterbmarks/radae_nopy to freedv/rade_c
drowe67 Jul 14, 2026
4231a31
Merge origin/main into dr-radev2: rm C API files, update rade_c URLs
drowe67 Jul 14, 2026
ee95dd2
Update doc/v2_test_report.tex
drowe67 Jul 14, 2026
327c8e0
README: rename title to Radio Autoencoder, add RADE V2 Status notice …
drowe67 Jul 15, 2026
6bd44c6
rename radae_nopy ctests and cmake var to rade_c
drowe67 Jul 15, 2026
28f30a9
rename BER test scripts to match rade_c naming
drowe67 Jul 15, 2026
3ab3479
loss.py: add --stats/--hist/--png options, fix time axis on plots
drowe67 Jul 16, 2026
c421265
loss.py: fix nframes so --stats mean matches min_loss
drowe67 Jul 16, 2026
aef7dc8
add ctests for V2 C receiver AGC (rade_c/dr-v2-rx-gain)
drowe67 Jul 18, 2026
85179f9
add +/-6dB gain verification ctests; fix standalone-run dependency
drowe67 Jul 18, 2026
b549586
rade_c_v1/v2_wav: replace SNR check with loss-based ctest
drowe67 Jul 20, 2026
799d858
rade_c_v2_wav_real: ctest for real-valued WAV rx noise folding
drowe67 Jul 20, 2026
5ac284b
Add README smoke tests for IQ and WAV pipeline examples
drowe67 Jul 20, 2026
9b398a8
Update smoke tests: input_sample.wav and distinct output filenames
drowe67 Jul 21, 2026
b4b6a08
Add verification procedure and template; tighten README verification …
drowe67 Jul 22, 2026
473e366
verification: add Scope section to Test Levels, clarify L2/L3 as logi…
drowe67 Jul 23, 2026
07941e0
verification: add worked example with plots; loss.py FAIL exit code f…
drowe67 Jul 23, 2026
cae3fb3
verification: remove duplicate worked example section; regenerate cle…
drowe67 Jul 23, 2026
1e47cbd
verification: add RX-only worked example using tx2.py + f32toint16.py
drowe67 Jul 23, 2026
25d58fe
Merge pull request #76 from drowe67/dr-verification
drowe67 Jul 24, 2026
3226f37
correct time axis when plotting features_hat2
drowe67 Aug 8, 2026
cec4dfe
increased precision for delta_hat, when working on Aug 26 tracking de…
drowe67 Aug 8, 2026
908bdbe
increased precision for delta_hat, when working on Aug 26 tracking de…
drowe67 Aug 8, 2026
50112bc
revise timing of V2 signal extraction
drowe67 Aug 30, 2026
5a6d07a
V2 AGC: replace memoryless per-block RMS with IIR-smoothed power esti…
drowe67 Aug 30, 2026
6e0ceaf
Add --no_eoo option; loss-vs-delay report tooling for Issue #8
drowe67 Aug 31, 2026
73f7183
Add v2_c_loss_vs_delay regression ctest for Issue #8
drowe67 Aug 31, 2026
c311396
V2 test report: add Issue #8 loss-vs-delay evidence to AGC section
drowe67 Sep 1, 2026
b89a329
V2 test report: match AGC state for C-vs-Python delay comparison
drowe67 Sep 1, 2026
2c61beb
Add ota_test.sh --v2_c option to test rade_c's production wav tools f…
drowe67 Sep 3, 2026
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
463 changes: 399 additions & 64 deletions CMakeLists.txt

Large diffs are not rendered by default.

90 changes: 63 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Radio Autoencoder V2
# Radio Autoencoder

RADE (Radio AutoEncoder) is a neural codec for transmitting speech over HF radio channels. A neural encoder compresses speech into a latent vector which is modulated onto an OFDM waveform and transmitted. At the receiver a neural decoder reconstructs the speech features, which are synthesised into audio by the [FARGAN](https://arxiv.org/abs/2405.21069) vocoder. The system is trained end-to-end, jointly optimising the encoder, channel layer, and decoder for minimum speech distortion across a range of channel conditions.

Expand All @@ -10,7 +10,7 @@ RADE V2 builds on V1 with several algorithmic improvements:
| Equalisation | Classical DSP, pilot-aided | ML-based, no pilots required |
| 99% Occupied Bandwidth | ~2100 Hz (SSB filter limited) | ~860 Hz |
| Frame duration | ~180 ms | ~40 ms |
| PAPR | 4.2 dB | 3.5 dB |
| PAPR (100% CCDF) | 4.2 dB | 3.5 dB |
| Frame sync | DSP | Neural network |
| End-of-over detection | Pilot pend sequence | Channel sparsity metric |
| Threshold SNR (AWGN) | -2 dB | ~-4.5 dB |
Expand All @@ -26,7 +26,15 @@ This repo is the reference Python implementation for RADE V1 and V2. The current

This repo is intended to support experimental work, with just enough information for the advanced experimenter to reproduce aspects of the work. The focus is on waveform development, not software configuration. It is not intended to be packaged for general use or to work across multiple Linux distros and operating systems. Unless otherwise stated, the code in this repo is intended to run only on Ubuntu Linux 22-24 on a non-virtual machine.

For deployment and distribution of RADE V1 please use the [C port](https://github.com/peterbmarks/radae_nopy). RADE V2 is still under development but we hope to make an initial release soon.
For deployment and distribution of RADE please use the [C port](https://github.com/freedv/rade_c).

## ⚠️ RADE V2 Status

RADE V2 is under active development. The waveform, model weights, and API are subject to change without notice, and future versions will not be backwards compatible with the current implementation.

Known issues are under investigation. On-air use is not recommended at this stage, and the FreeDV team is not able to provide support for pre-release V2 deployments. Any on-air V2 signals should be considered premature use of the development waveform and are not part of official FreeDV development activity.

The official V2 status will be announced on the [FreeDV blog](https://freedv.org/blog/).

# Quickstart

Expand Down Expand Up @@ -67,6 +75,7 @@ The RADE source code is released under the two-clause BSD license.
| `loss.py` | Measures ML loss (speech distortion) between encoder and decoder feature vectors |
| `compare_models_inf.sh` | Generates loss versus SNR curves across models and channel types |
| `ota_test.sh` | Over-the-air/over-the-cable test: generates tx signal, decodes rx, measures loss |
| `radev2_rx_wav.sh` | Decode an off-air RADE V2 WAV recording; outputs decoded speech and diagnostic plots |
| `est_CNo.py` | C/No estimation from a received chirp signal |
| `chirp.py` | Generates a chirp reference signal used for timing and level calibration in OTA tests |
| `int16tof32.py` / `f32toint16.py` | Sample format converters between int16 and float32 |
Expand Down Expand Up @@ -116,16 +125,30 @@ cat rx.f32 | python3 f32toint16.py --real --scale 8192 | play -t .s16 -r 8000 -c
```
The scaling `--scale` is required as the low SNRs mean the noise peak amplitude can clip 16 bit samples if not carefully scaled.

## Optional: RADE V1 C Port Tests (radae_nopy)
## Decoding an off air RADE V2 wave file

The [radae_nopy](https://github.com/peterbmarks/radae_nopy) repo contains a C port of the RADE V1 receiver. Its ctests are optional and only enabled when `RADAE_NOPY_BUILD_DIR` is passed to cmake:
To decode a WAV file received off air (e.g. from a KiwiSDR or similar SDR receiver):
```
./radev2_rx_wav.sh ~/Downloads/kiwi_sdr_rx.wav
```
All output artefacts are stored in a subdirectory named after the input file:
```
~/Downloads/kiwi_sdr_rx/kiwi_sdr_rx_rade2.wav # decoded speech
~/Downloads/kiwi_sdr_rx/kiwi_sdr_rx_plots.png # sync state, SNR, freq offset, gain plots
~/Downloads/kiwi_sdr_rx/report.txt # terse per-frame decoder log
```
The input WAV can be any sample rate (resampled to 8kHz internally). Pass `--verbose` for the full decoder log including bash trace.

## Optional: RADE C Port Tests (rade_c)

The [rade_c](https://github.com/freedv/rade_c) repo contains the C port of RADE V1 and V2. Its ctests are optional and only enabled when `RADE_C_BUILD_DIR` is passed to cmake:
```
cd ~
git clone https://github.com/peterbmarks/radae_nopy.git
cd radae_nopy && mkdir build && cd build && cmake .. && make
git clone https://github.com/freedv/rade_c.git
cd rade_c && mkdir build && cd build && cmake .. && make
cd ~/radae/build
cmake -DRADAE_NOPY_BUILD_DIR=~/radae_nopy/build ..
ctest -R radae_nopy
cmake -DRADE_C_BUILD_DIR=~/rade_c/build ..
ctest -R rade_c
```


Expand Down Expand Up @@ -175,7 +198,7 @@ Automatic Speech Recognition (ASR) is used as an objective speech quality metric

1. Install dependencies:
```
pip3 install jiwer openai-whisper
pip3 install jiwer openai-whisper soundfile
```

1. The LibriSpeech `test-clean` dataset (~400 MB) is downloaded automatically to `~/.cache/LibriSpeech/` on first run via `torchaudio`.
Expand All @@ -189,30 +212,37 @@ Automatic Speech Recognition (ASR) is used as an objective speech quality metric
```
./asr_test_top.sh ssb -n 100
./asr_test_top.sh rade -n 100
./asr_test_top.sh radev2 -n 100
./asr_test_top.sh 700D -n 100
```

1. For MPP channel, first generate fading samples (if not already present), then re-run with `--g_file`:
1. For MPP channel, first generate the 4000s fading file (if not already present), then run MPP sweeps:
```
./test/make_g.sh
./asr_test_top.sh rade -n 100 --g_file g_mpp.f32
if [ ! -f g_mpp_4000s.f32 ]; then
DISPLAY="" echo "Fs=8000; Rs=50; Nc=20; multipath_samples('mpp', Fs, Rs, Nc, 4000, '','g_mpp_4000s.f32'); quit" | octave-cli -qf
fi
./asr_test_top.sh ssb -n 100
./asr_test_top.sh rade -n 100
./asr_test_top.sh radev2 -n 100
```

1. Plot WER curves in Octave:
```
octave:1> radae_plots; plot_wer("241221","241221_asr_test.png")
octave:1> radae_plots; plot_wer("260702","260702_asr_test.png")
octave:1> radae_plots; plot_wer_v2("260702","260702_wer_v2.png")
```

# Exporting Weights for the C Port (radae_nopy)
# Exporting Weights for the C Port (rade_c)

The [radae_nopy](https://github.com/peterbmarks/radae_nopy) repo contains the full standalone C port of RADE. When a new model is trained, the weights need to be exported from Python and compiled into radae_nopy:
The [rade_c](https://github.com/freedv/rade_c) repo contains the full standalone C port of RADE. When a new model is trained, the weights need to be exported from Python and compiled into rade_c:

1. Export weights to C source files:
```
cd radae
python3 export_rade_weights.py model19_check3/checkpoints/checkpoint_epoch_100.pth src
```
1. Copy the generated `rade_enc_data.c`, `rade_enc_data.h`, `rade_dec_data.c`, `rade_dec_data.h` into `radae_nopy/src/` and rebuild.
1. Copy the generated `rade_enc_data.c`, `rade_enc_data.h`, `rade_dec_data.c`, `rade_dec_data.h` into `rade_c/src/` and rebuild.


# Testing RADE

Expand All @@ -228,24 +258,30 @@ Any test results must be reproducible using the RADE command line tools (our ver

## Verifying RADE Integration

Application (and radio) developers - to confirm that RADE is successfully integrated into your application, please perform a loss test based on the feature vectors at the input of the RADE encoder at the Tx, and output of the RADE decoder at the Rx. The Python tool `loss.py` can be used for this test. You may need to modify your application (or radio) to dump these vectors to a disk file.
Before contributing OTA test results or deploying RADE in an application,
integration must be verified using a loss-based test procedure. This confirms
the signal path is clean — no dropped buffers, no unintended DSP, no scaling
errors — so that any on-air results reflect RADE performance, not integration
issues.

Radio developers should perform a complete end-to-end over the cable test to demonstrate successful integration. Over the air tests are not meaningful as the channel will impact the loss in unpredictable and unrepeatable fashion.
The full procedure, including a checklist template for submitting results, is
in [doc/verification/verification_procedure.md](doc/verification/verification_procedure.md).

The loss test will tease out gross errors like dropped buffers of samples, and more subtle issues such as distortion in signal processing steps. There are many examples of loss tests in the RADE ctests, and `ota_test.sh` can use real radio and SDRs to perform loss tests over the cable.

The [V2 test report](doc/v2_test_report.pdf) Table 10 has some examples of over the cable (OTC) loss test results (v216 line). A pass is defined as +\- 10% of the software only loss result with the 56 second file `all.wav`.

To establish the software-only loss baseline, run the V2 transmitter and receiver on `all.wav` with no channel noise (actually a very high SNR set by the default EbNodB=100). In this example `lpcnet_demo` is used to produce the input feature file `features_in.f32`. The file `tx.f32` is the Fs=8 kHz IQ float samples sent over the "channel". We are using the reference Python implementation:
A software-only loss baseline must be established using the current version
of the code under test — loss values shift slightly between model versions.
We use the reference Python implementation and `wav/all.wav` to establish
the baseline; re-run with the latest version to obtain the current baseline:
```
lpcnet_demo -features wav/all.wav features_in.f32
python3 tx2.py 250725/checkpoints/checkpoint_epoch_200.pth features_in.f32 tx.f32
python3 rx2.py 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync tx.f32 features_rx.f32 --quiet
python3 loss.py features_in.f32 features_rx.f32 --clip_start 100 --clip_end 300
<snip>
loss: 0.081 start: 224 acq_time: 1.24 s
```
Record the loss value printed by `loss.py` (in this example 0.081) — this is your software-only reference. When testing RADE integrated into your application (or radio), a loss within ±10% of this figure is considered a pass.
Example output (Python reference, `wav/all.wav`, model `250725`, commit `b549586`):
```
loss: 0.081 start: 224 acq_time: 1.24 s
```
Record the current baseline loss value. A pass is within ±10% of the baseline.

## Stored File Tests

Expand Down
64 changes: 59 additions & 5 deletions asr_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# a dataset with channel simulations (RADE, SSB etc) applied.

CODEC2_DEV=${CODEC2_DEV:-${HOME}/codec2-dev}
PATH=${PATH}:${CODEC2_DEV}/build_linux/src:${CODEC2_DEV}/build_linux/misc:${PWD}/build/src
PATH=${PATH}:${PWD}/build/src:${CODEC2_DEV}/build_linux/src:${CODEC2_DEV}/build_linux/misc

which ch >/dev/null || { printf "\n**** Can't find ch - check CODEC2_PATH **** \n\n"; exit 1; }

Expand Down Expand Up @@ -143,6 +143,8 @@ function process {
rm -f ${snr_log}
CNo_log=CNo_log.txt
rm -f ${CNo_log}
cpapr_log=cpapr_log.txt
rm -f ${cpapr_log}
sox -n -r 16000 -c 1 /tmp/silence.wav trim 0.0 ${sil}

if [ $mode == "ssb" ] || [ $mode == "4kHz" ]; then
Expand All @@ -166,8 +168,10 @@ function process {
fi
snr=$(cat $ch_log | grep "SNR3k" | tr -s ' ' | cut -d' ' -f3)
CNo=$(cat $ch_log | grep "SNR3k" | tr -s ' ' | cut -d' ' -f5)
cpapr=$(cat $ch_log | grep "CPAPR" | tr -s ' ' | cut -d' ' -f7)
echo $snr >> ${snr_log}
echo $CNo >> ${CNo_log}
echo $cpapr >> ${cpapr_log}

# advance through fading simulation file
dur=$(sox --info -D ${source}/${f})
Expand All @@ -180,9 +184,10 @@ function process {
if [ $mode == "ssb" ]; then
SNR_mean=$(print_mean_text_file ${snr_log})
CNo_mean=$(print_mean_text_file ${CNo_log})
CPAPR_mean=$(print_mean_text_file ${cpapr_log})
fi
fi

if [ $mode == "700D" ]; then

fading_adv=0
Expand All @@ -207,8 +212,10 @@ function process {
fi
snr=$(cat $ch_log | grep "SNR3k" | tr -s ' ' | cut -d' ' -f3)
CNo=$(cat $ch_log | grep "SNR3k" | tr -s ' ' | cut -d' ' -f5)
cpapr=$(cat $ch_log | grep "CPAPR" | tr -s ' ' | cut -d' ' -f7)
echo $snr >> ${snr_log}
echo $CNo >> ${CNo_log}
echo $cpapr >> ${cpapr_log}

# advance through fading simulation file
dur=$(sox --info -D ${source}/${f})
Expand All @@ -217,6 +224,51 @@ function process {
done
SNR_mean=$(print_mean_text_file ${snr_log})
CNo_mean=$(print_mean_text_file ${CNo_log})
CPAPR_mean=$(print_mean_text_file ${cpapr_log})
fi

if [ $mode == "radev2" ]; then
duration_log=""
flac_full=""
pushd $source > /dev/null;
for f in $flac
do
duration_log+=$(sox --info -D ${f})
duration_log+=" "
flac_full+="${source}/${f} /tmp/silence.wav "
done
popd > /dev/null;

sox $flac_full -t .s16 ${in}

./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth ${in} out.wav \
--rate_Fs --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 \
--auxdata --w1_dec 128 --ssb_bpf \
--EbNodB $EbNodB ${inference_args} | tee ${rade_log}
grep "Multipath Doppler spread file too short" $rade_log
if [ $? -eq 0 ]; then
echo "Error - fading file too short"
exit 1
fi

SNR_mean=$(cat $rade_log | grep "Measured" | tr -s ' ' | cut -d' ' -f4)
CNo_mean=$(cat $rade_log | grep "Measured" | tr -s ' ' | cut -d' ' -f3)

duration_array=( ${duration_log} )
i=0
st=0
for f in $flac
do
dur=${duration_array[i]}
dur=$(python3 -c "print($dur + ${sil})")
((i++))
if [ $i -eq ${#duration_array[@]} ]; then
sox out.wav ${dest}/${f} trim $st
else
sox out.wav ${dest}/${f} trim $st $dur
fi
st=$(python3 -c "print($st + $dur)")
done
fi

if [ $mode == "rade" ] || [ $mode == "fargan" ]; then
Expand All @@ -239,8 +291,8 @@ function process {

if [ $mode == "rade" ]; then
./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth ${in} out.wav \
--rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --time_offset -16 \
--EbNodB $EbNodB ${inference_args} | tee ${rade_log}
--rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --tanh_clipper --ssb_bpf \
--time_offset -16 --EbNodB $EbNodB ${inference_args} | tee ${rade_log}
grep "Multipath Doppler spread file too short" $rade_log
if [ $? -eq 0 ]; then
echo "Error - fading file too short"
Expand Down Expand Up @@ -285,7 +337,9 @@ function process {

python3 asr_wer.py test-other -n $n_samples --model turbo | tee > $asr_log
wer=$(tail -n1 $asr_log | tr -s ' ' | cut -d' ' -f2)
if [ $mode == "ssb" ] || [ $mode == "rade" ] || [ $mode == "700D" ]; then
if [ $mode == "ssb" ] || [ $mode == "700D" ]; then
printf "%-6s %5.2f %5.2f %5.2f %5.2f\n" $mode $SNR_mean $CNo_mean $wer $CPAPR_mean | tee -a $results
elif [ $mode == "rade" ] || [ $mode == "radev2" ]; then
printf "%-6s %5.2f %5.2f %5.2f\n" $mode $SNR_mean $CNo_mean $wer | tee -a $results
else
printf "%-6s %5.2f\n" $mode $wer | tee -a $results
Expand Down
Loading
Loading