diff --git a/CMakeLists.txt b/CMakeLists.txt index e9cc020..5d090e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,14 +97,14 @@ add_test(NAME inference_loss_model5 add_test(NAME inference_loss_model17 COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ - --EbNodB 0.5 --freq_offset 1 --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 \ + --EbNodB 1 --freq_offset 1 --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 \ --loss_test 0.3") set_tests_properties(inference_loss_model17 PROPERTIES PASS_REGULAR_EXPRESSION "PASS") add_test(NAME inference_loss_model18 COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./inference.sh model18/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null --latent-dim 40 \ - --EbNodB 3.5 --freq_offset 1 --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 \ + --EbNodB 4 --freq_offset 1 --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 \ --loss_test 0.3") set_tests_properties(inference_loss_model18 PROPERTIES PASS_REGULAR_EXPRESSION "PASS") @@ -125,12 +125,11 @@ add_test(NAME rx_loss_acq_time add_test(NAME chirp_mpp COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./test/chirp_mpp.sh ${CODEC2_DEV_BUILD_DIR} -16") - set_tests_properties(chirp_mpp PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # Low SNR ota_test.sh, with chirp measurement, AWGN add_test(NAME ota_test_awgn COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./test/ota_test_cal.sh ~/codec2-dev/build_linux/ -21 0.3") + ./test/ota_test_cal.sh ~/codec2-dev/build_linux/ -21 0.4") # Low SNR ota_test.sh, with chirp measurement, MPP, high loss threshold as we only care about gross errors, # like stuck in false sync @@ -167,7 +166,7 @@ add_test(NAME acq_mpp test/make_g.sh; \ ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 \ - --EbNodB 3 --freq_offset $F_OFF --g_file g_mpp.f32 --write_rx rx.f32; \ + --EbNodB 4 --freq_offset $F_OFF --g_file g_mpp.f32 --write_rx rx.f32; \ ./rx.sh model17/checkpoints/checkpoint_epoch_100.pth rx.f32 /dev/null --pilots --pilot_eq \ --bottleneck 3 --cp 0.004 --acq_test --fmax_target $F_OFF --acq_time_target 1.5") set_tests_properties(acq_mpp PROPERTIES PASS_REGULAR_EXPRESSION "PASS") @@ -210,10 +209,10 @@ add_test(NAME acq_sine_mpp # basic test of streaming Tx/Rx, compare to vanilla Tx in inference.py add_test(NAME radae_tx_basic COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ - --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset; \ - cat features_in.f32 | python3 radae_tx.py model17/checkpoints/checkpoint_epoch_100.pth > rx.f32 - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 1 > features_txs_out.f32; \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ + --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --write_rx rx.f32 --correct_freq_offset; \ + cat features_in.f32 | python3 radae_tx.py model19_check3/checkpoints/checkpoint_epoch_100.pth --auxdata > rx.f32 + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 1 --auxdata > features_txs_out.f32; \ python3 loss.py features_in.f32 features_txs_out.f32 --loss_test 0.15 --acq_time_test 0.5") set_tests_properties(radae_tx_basic PROPERTIES PASS_REGULAR_EXPRESSION "PASS") @@ -226,7 +225,7 @@ add_test(NAME complex_bpf add_test(NAME rx_stateful COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ - --EbNodB 0 --freq_offset 11 \ + --EbNodB 6 --freq_offset 11 \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset; \ ./rx.sh model17/checkpoints/checkpoint_epoch_100.pth rx.f32 /dev/null \ --pilots --pilot_eq --bottleneck 3 --cp 0.004 --coarse_mag --time_offset -16 --stateful; @@ -240,7 +239,7 @@ add_test(NAME rx_stateful add_test(NAME rx_streaming COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ - --EbNodB 0 --freq_offset 11 \ + --EbNodB 6 --freq_offset 11 \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset; \ ./rx.sh model17/checkpoints/checkpoint_epoch_100.pth rx.f32 /dev/null \ --pilots --pilot_eq --bottleneck 3 --cp 0.004 --time_offset -16 --coarse_mag --rx_one; @@ -250,7 +249,7 @@ add_test(NAME rx_streaming python3 loss.py features_in.f32 features_rx_out.f32 --features_hat2 features_rx_one_out.f32 --compare;") set_tests_properties(rx_streaming PROPERTIES PASS_REGULAR_EXPRESSION "PASS") -# basic test of streaming rx, run rx in vanilla and streaming , compare +# basic test of streaming rx, run rx in vanilla and streaming, compare add_test(NAME radae_rx_basic COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ @@ -266,12 +265,12 @@ add_test(NAME radae_rx_basic # SNR=-2dB AWGN add_test(NAME radae_rx_awgn COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --EbNodB 1 --freq_offset 13 \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 --write_rx rx.f32 \ - --prepend_noise 1 --append_noise 3 --end_of_over; \ - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 1 > features_rx_out.f32; \ - python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --acq_time_test 0.5") + --prepend_noise 1 --append_noise 3 --end_of_over --auxdata; \ + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rx_out.f32; \ + python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --acq_time_test 1.0 --clip_end 100") set_tests_properties(radae_rx_awgn PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # SNR=0dB MPP @@ -280,23 +279,24 @@ add_test(NAME radae_rx_awgn add_test(NAME radae_rx_mpp COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ test/make_g.sh; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 \ --EbNodB 3 --freq_offset -11 --g_file g_mpp.f32 --write_rx rx.f32 \ - --prepend_noise 1 --append_noise 3 --end_of_over --correct_freq_offset; \ - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rx_out.f32; \ - python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --clip_end 300") + --prepend_noise 1 --append_noise 3 --end_of_over --auxdata --correct_freq_offset; \ + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata --disable_unsync 5.0 > features_rx_out.f32; \ + python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.35 --clip_end 300") set_tests_properties(radae_rx_mpp PROPERTIES PASS_REGULAR_EXPRESSION "PASS") -# SNR=0dB MPG +# SNR=0dB MPG. We disable_unsync as we don't really care if we re-sync on long fades - user won't care much. Main aim is to make sure loss is OK +# in this channel add_test(NAME radae_rx_mpg COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ test/make_g.sh; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 \ --EbNodB 3 --freq_offset -11 --g_file g_mpg.f32 --write_rx rx.f32 \ - --prepend_noise 1 --append_noise 3 --end_of_over --correct_freq_offset; \ - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rx_out.f32; \ + --prepend_noise 1 --append_noise 3 --end_of_over --auxdata --correct_freq_offset; \ + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata --disable_unsync 5.0 > features_rx_out.f32; \ python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --clip_end 300") set_tests_properties(radae_rx_mpg PROPERTIES PASS_REGULAR_EXPRESSION "PASS") @@ -304,57 +304,59 @@ add_test(NAME radae_rx_mpg add_test(NAME radae_rx_mpd COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ test/make_g.sh; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 \ --EbNodB 6 --freq_offset -28 --g_file g_mpd.f32 --write_rx rx.f32 \ - --prepend_noise 1 --append_noise 3 --end_of_over --correct_freq_offset; \ - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rx_out.f32; \ + --prepend_noise 1 --append_noise 3 --end_of_over --auxdata --correct_freq_offset; \ + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rx_out.f32; \ python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --clip_end 300") set_tests_properties(radae_rx_mpd PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # SNR=-2dB AWGN ~5 Hz/min = 5/60 Hz/s freq drift add_test(NAME radae_rx_dfdt COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --EbNodB 1 --freq_offset 13 --df_dt 0.1 \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 --write_rx rx.f32 \ - --prepend_noise 1 --append_noise 3 --end_of_over; \ - cat rx.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rx_out.f32; \ - python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --acq_time_test 0.8") + --prepend_noise 1 --append_noise 3 --end_of_over --auxdata; \ + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rx_out.f32; \ + python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --acq_time_test 0.8 --clip_end 100") set_tests_properties(radae_rx_dfdt PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # SNR=7dB ability to handle small differences in sample rate between tx and rx (delta Fs) add_test(NAME radae_rx_dfs COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ --EbNodB 10 --freq_offset 11 \ - --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset; \ + --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --write_rx rx.f32 --correct_freq_offset; \ cat rx.f32 | python3 f32toint16.py --scale 8192 | sox -t .s16 -r 8000 -c 2 - -t .s16 -r 8001 -c 2 - | python3 int16tof32.py > rx_.f32; \ - cat rx_.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rxs_out.f32; \ + cat rx_.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rxs_out.f32; \ python3 loss.py features_in.f32 features_rxs_out.f32 --loss_test 0.15 --acq_time_test 0.5") set_tests_properties(radae_rx_dfs PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # Test ability to handle buffer slips due to sample clock offsets, rx ADC clock > tx ADC clock. We make sample clock error larger than 200ppm spec -# in order to exercise code. Nice thing about "nin" design is it allows us to get meaningful "loss.py" results, ie no frames are lost. +# in order to exercise code. Nice thing about "nin" design is it allows us to get meaningful "loss.py" results, ie no frames are lost. We're +# really just trying to exercise the slip code here, not too cocnerned about loss. There should be a noticable transition in tmax in each test as +# it hits the upper or lower limit add_test(NAME radae_rx_slip_plus COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ --EbNodB 10 --freq_offset 11 \ - --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset --prepend_noise 0.08; \ + --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --write_rx rx.f32 --correct_freq_offset --prepend_noise 0.06; \ cat rx.f32 | python3 f32toint16.py --scale 8192 | sox -t .s16 -r 8000 -c 2 - -t .s16 -r 8005 -c 2 - | python3 int16tof32.py > rx_.f32; \ - cat rx_.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rxs_out.f32; \ - python3 loss.py features_in.f32 features_rxs_out.f32 --loss_test 0.15 --acq_time_test 1.0") + cat rx_.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rxs_out.f32; \ + python3 loss.py features_in.f32 features_rxs_out.f32 --loss_test 0.2 --acq_time_test 1.0") set_tests_properties(radae_rx_slip_plus PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # Test ability to handle buffer slips due to sample clock offsets, rx ADC clock < tx ADC clock add_test(NAME radae_rx_slip_minus COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh model17/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ + ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/brian_g8sez.wav /dev/null \ --EbNodB 10 --freq_offset 31 \ - --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --write_rx rx.f32 --correct_freq_offset --prepend_noise 0.11; \ + --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --auxdata --write_rx rx.f32 --correct_freq_offset --prepend_noise 0.11; \ cat rx.f32 | python3 f32toint16.py --scale 8192 | sox -t .s16 -r 8000 -c 2 - -t .s16 -r 7995 -c 2 - | python3 int16tof32.py > rx_.f32; \ - cat rx_.f32 | python3 radae_rx.py model17/checkpoints/checkpoint_epoch_100.pth -v 2 > features_rxs_out.f32; \ - python3 loss.py features_in.f32 features_rxs_out.f32 --loss_test 0.15 --acq_time_test 1.0") + cat rx_.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rxs_out.f32; \ + python3 loss.py features_in.f32 features_rxs_out.f32 --loss_test 0.2 --acq_time_test 1.0") set_tests_properties(radae_rx_slip_minus PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # profiles a run with a 50 second file (no pass/fail, run with -V to get a rough idea of execution time) @@ -380,16 +382,16 @@ add_test(NAME radae_rx_fargan # Embedded data (--auxdata) use for false sync detection, we --clip_start as false sync messes up alignment of feat vecs, Eb/No adjusted # to pass (but low SNR not really the aim of this test), --foff_err forces a false sync state after first sync -add_test(NAME radae_rx_mpp_aux +add_test(NAME radae_rx_aux_mpp COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ test/make_g.sh; \ ./inference.sh model19_check3/checkpoints/checkpoint_epoch_100.pth wav/all.wav /dev/null \ --rate_Fs --pilots --pilot_eq --eq_ls --cp 0.004 --bottleneck 3 --time_offset -16 --auxdata \ --EbNodB 4 --freq_offset -11 --g_file g_mpp.f32 --write_rx rx.f32 \ --prepend_noise 1 --append_noise 3 --end_of_over --correct_freq_offset; \ - cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --foff_err -8 --auxdata > features_rx_out.f32; \ - python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --clip_start 100") - set_tests_properties(radae_rx_mpp_aux PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + cat rx.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata --foff_err 8 > features_rx_out.f32; \ + python3 loss.py features_in.f32 features_rx_out.f32 --loss 0.3 --clip_start 300") + set_tests_properties(radae_rx_aux_mpp PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # evaluate.sh ----------------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index 5ceca8b..c9e7913 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A hybrid Machine Learning/DSP system for sending speech over HF radio channels. ## Scope -This repo is intended to support the authors experimental work, with just enough information for the advanced experimenter to reproduce aspects of the work. It is not intended to be a polished distribution for general use. Unless otherwise stated, the code is this repo is intended to run only on Ubuntu Linux. +This repo is intended to support the authors experimental work, with just enough information for the advanced experimenter to reproduce aspects of the work. It is not intended to be a polished distribution for general use. Unless otherwise stated, the code is this repo is intended to run only on Ubuntu Linux 22. # Quickstart @@ -39,7 +39,7 @@ The RDOVAE derived Python source code is released under the two-clause BSD licen | multipath_samples.m | Octave script to generate multipath magnitude sample over a time/freq grid | | plot_specgram.m | Plots sepctrogram of radae modem signals | | radae_plots.m | Helper Octave script to generate various plots | -| radio_ae.[tex|pdf] | Latex documenation | +| radio_ae.[tex,pdf] | Latex documenation | | ota_test.sh | Script to automate Over The Air (OTA) testing | | Radio Autoencoder Waveform Design.ods | Working for OFDM waveform, including pilot and cyclic prefix overheads | | compare_models.sh | Builds loss versus Eq/No curves for models to objectively compare | @@ -47,10 +47,10 @@ The RDOVAE derived Python source code is released under the two-clause BSD licen | test folder | Helper scripts for ctests | | loss.py | Tool to calculate mean loss between two feature files, a useful objective measure | | ml_pilot.py | Training low PAPR pilot sequence | -| stateful_decoder.[py|sh] | Inference test that compares stateful to vanilla decoder | -| stateful_encoder.[py|sh] | Inference test that compares stateful to vanilla encoder | -| radae_tx.[py|sh] | streaming RADAE encoder and helper script | -| radae_rx.[py|sh] | streaming RADAE decoder and helper script | +| stateful_decoder.[py,sh] | Inference test that compares stateful to vanilla decoder | +| stateful_encoder.[py,sh] | Inference test that compares stateful to vanilla encoder | +| radae_tx.[py,sh] | streaming RADAE encoder and helper script | +| radae_rx.[py,sh] | streaming RADAE decoder and helper script | | resource_est.py | WIP estimate CPU/memory resources | # Installation @@ -65,28 +65,34 @@ Supplies some utilities used for `ota_test.sh` and `evaluate.sh` ``` cd ~ git clone git@github.com:drowe67/codec2-dev.git -cd codec2 +cd codec2-dev mkdir build_linux cd build_linux cmake -DUNITTEST=1 .. make ch mksine tlininterp ``` -(optional if using HackRF) manually compile codec2-dev/misc/tsrc.c -# Building and Automated Tests +## RADAE -The `cmake/ctest` framework is being used as a build and test framework. The command lines in `CmakeLists.txt` are a good source of examples, if you are interested in running the code in this repo. - -To configure and run the cests: +Builds the FARGAN vocoder and ctest framework, most of RADAE is in Python. ``` cd radae mkdir build cd build cmake .. make +``` + +# Automated Tests + +The `cmake/ctest` framework is being used as a build and test framework. The command lines in `CmakeLists.txt` are a good source of examples, if you are interested in running the code in this repo. The ctests are a work in progress and may not pass on all systems (see Scope above). + +To run the cests: +``` +cd radae/build ctest ``` -To list tests `ctest -N`, to run just one test `ctest -R inference_model5`, to run in verbose mode `ctest -V -R inference_model5`. You can change the paths to `codec2-dev` and `opus` on the `cmake` command line: +To list tests `ctest -N`, to run just one test `ctest -R inference_model5`, to run in verbose mode `ctest -V -R inference_model5`. You can change the paths to `codec2-dev` on the `cmake` command line: ``` cmake -DCODEC2_DEV=~/tmp/codec2-dev .. ``` @@ -245,9 +251,9 @@ BER tests are useful to calibrate the system, and measure loss from classical DS 1. Testing OTA over HF channels. Using my IC7200 as the Tx station: ``` - ./ota_test.sh wav/david.wav -g 9 -t -d -f 14236 + ./ota_test.sh wav/david_vk5dgr.wav -g 6 -t -d -f 14236 ``` - The `-g 9` sample gives the `david.wav` sample a little more compression, this was ajusted by experiment, listening to the `tx.wav` file, and looking for signs of a compressed waveform on Audacity. To receive the signal I tune into a convenient KiwiSDR, and manually start recording when my radio starts transmitting. I stop recording when I hear the transmission end. This will result in a wave file being downloaded. It's a good idea to trim any excess off the start and end of the rx wave file. It can be decoded with: + The `-g 6` is the SSB compressor gain (default 6 so in this case optional); this can be adjusted by experiment, e.g. listening to the `tx.wav` file, and looking for signs of a compressed waveform on Audacity. To receive the signal I tune into a convenient KiwiSDR, and manually start recording when my radio starts transmitting. I stop recording when I hear the transmission end. This will result in a wave file being downloaded. It's a good idea to trim any excess off the start and end of the rx wave file. It can be decoded with: ``` ./ota_test.sh -d -r ~/Downloads/kiwisdr_usb.wav ``` @@ -407,3 +413,86 @@ Using model 17 waveform: | Auxilary text channel | No | | | SNR measurement | No | | | Tx and Rx sample clock offset | 200ppm | e.g. Tx sample clock 8000 Hz, Rx sample clock 8001 Hz | + +# Web based Stored File Processing + +This section contains some notes on setting up a web server to run `ota_test.sh`. The idea is to make it easier for non-Linux users to contribute to the stored file test program. The general idea is a CGI script interfaces to `ota_test.sh` to perform the Tx and Rx processing. We configure the web server so that the HTML forms and CGI scripts run in `~/public_html`. The notes below are for Apache on Ubuntu 22. + +1. The Python packages need to be available system wide , so `www-data` can use them: + ``` + sudo pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 + sudo -u www-data python3 -c "import torch" + ``` + + ``` + sudo pip3 install matplotlib + sudo -u www-data python3 -c "import matplotlib" + ``` + The presence of the packages can be checked by mimicing the www-data user (the last line in each step above should return nothing if all is well). + +1. Configure Apache for CGI and serving pages from our `~/public_html` dir. + ``` + sudo a2enmod cgid + sudo a2enmod userdir + sudo systemctl restart apache2 + ``` + We want html and cgi to run out of ~/public_html, so permissions have to be `755` and `www-data` has to be added to the users group. + ``` + mkdir ~/public_html + chmod 755 public_html + sudo usermod -a -G www-data + ``` + To let CGI scripts run from ~/public_html I placed this in my `/etc/apache2.conf`: + ``` + + Options +ExecCGI + AddHandler cgi-script .cgi + + ``` + Then restart apache as above. + +1. Create sym links to HTML/CGI scripts in `radae` repo, this allows the script to be part of the RADAE repo: + ``` + cd ~/public_html + ln -s ~/radae/public_html/tx_form.html tx_form.html + ln -s ~/radae/public_html/tx_process.cgi tx_process.cgi + ``` + +1. Note that files created when the CGI process run (e.g. `/tmp/input.wav`) get put in a sandbox rather than directly in `/tmp`. This is a systemd security feature. You can find the files with: + ``` + sudo find /tmp -name input.wav | xargs sudo ls -ld + -rw-r--r-- 1 www-data www-data 3918458 Aug 15 15:28 /tmp/systemd-private-2fcf85ad243b4da08d79d2e27e0375af-apache2.service-vDE2Dg/tmp/input.wav + ``` + +1. Apache error log, good for viewing `ota_test.sh` progress and spotting any issues: + ``` + tail -f /var/log/apache2/error.log + ``` + +# Real Time PTT + +WIP notes + +## Real Time decode using KiwiSDR + +1. Install pulse audio null module + ``` + pactl load-module module-null-sink sink_name=vsink + ``` +1. Start your web browser, and open a tab to a KiwiSDR. +1. Open `pavucontrol`, *Playback* tab, send web browser sound to NULL module. Audio from web browser should go silent. +1. We take the audio from the null device monitor output for the input to the RADAE Rx: + ``` + parec --device=vsink.monitor --rate=8000 --channels=1 | python3 int16tof32.py --zeropad | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata | ./build/src/lpcnet_demo -fargan-synthesis - - | aplay -f S16_LE -r 16000 + ``` +1. Try transmitting a RADAE signal: + ``` + ./ota_test.sh -t radae_test.raw -d -f 7175 + ``` + Where `radae_test.raw` is a RADAE-only sample (i.e. without the chirp and SSB, copied from a temp file generated by `ota_test.sh -x`). If you can't open the SSB radio playback device to radio try closing `pavucontrol`. +1. Other useful pulse audio commands: + ``` + pactl list sinks short + pactl list sources short + pactl list modules + ``` diff --git a/build_libopus.sh b/build_libopus.sh deleted file mode 100755 index 1ba8552..0000000 --- a/build_libopus.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -x -# -# Build libopus.a from previously built objects - -OPUS_DIR=$1 -cd $OPUS_DIR -ar rcs libopus.a dnn/.libs/burg.o dnn/.libs/freq.o dnn/.libs/fargan.o dnn/.libs/fargan_data.o dnn/.libs/lpcnet_enc.o dnn/.libs/lpcnet_plc.o dnn/.libs/lpcnet_tables.o dnn/.libs/nnet.o dnn/.libs/nnet_default.o dnn/.libs/plc_data.o dnn/.libs/parse_lpcnet_weights.o dnn/.libs/pitchdnn.o dnn/.libs/pitchdnn_data.o dnn/.libs/dred_rdovae_enc.o dnn/.libs/dred_rdovae_enc_data.o dnn/.libs/dred_rdovae_dec.o dnn/.libs/dred_rdovae_dec_data.o dnn/.libs/dred_rdovae_stats_data.o silk/.libs/dred_encoder.o silk/.libs/dred_coding.o silk/.libs/dred_decoder.o dnn/x86/.libs/x86_dnn_map.o dnn/x86/.libs/nnet_sse2.o dnn/x86/.libs/nnet_sse4_1.o dnn/x86/.libs/nnet_avx2.o celt/.libs/bands.o celt/.libs/celt.o celt/.libs/celt_encoder.o celt/.libs/celt_decoder.o celt/.libs/cwrs.o celt/.libs/entcode.o celt/.libs/entdec.o celt/.libs/entenc.o celt/.libs/kiss_fft.o celt/.libs/laplace.o celt/.libs/mathops.o celt/.libs/mdct.o celt/.libs/modes.o celt/.libs/pitch.o celt/.libs/celt_lpc.o celt/.libs/quant_bands.o celt/.libs/rate.o celt/.libs/vq.o celt/x86/.libs/x86cpu.o celt/x86/.libs/x86_celt_map.o celt/x86/.libs/pitch_sse.o celt/x86/.libs/pitch_sse2.o celt/x86/.libs/vq_sse2.o celt/x86/.libs/celt_lpc_sse4_1.o celt/x86/.libs/pitch_sse4_1.o celt/x86/.libs/pitch_avx.o diff --git a/doc/radae.tex b/doc/radae.tex index 7747f9c..67c4ec0 100644 --- a/doc/radae.tex +++ b/doc/radae.tex @@ -1165,6 +1165,14 @@ \subsection{Sync State Machine} \label{tab:acq_req} \end{table} +TODO: +\begin{enumerate} +\item discuss EOO system. +\item EOO system, not always reliable, higher threshold to avoid false unsync at low SNRs. Art low SNRs its acceptable to use UW errors to unsync. Add to table above? +\item State diagram with EOO, run on counter, UW errors. +\item Is run on counter required with UW error system? +\end{enumerate} + \section{Auxiliary Data} In existing FreeDV modes, a few bits/frame are allocated for supplementary information such as a unique word (UW) to support reliable sync, and low bit rate (e.g. 25 bits/s) data. In existing FreeDV modes we use digital modulation, so it is easy to allocate some of these bits to low bit rate data. For ML based waveforms, the network controls the PSK symbols being sent across the channel. One approach is to add additional OFDM carriers dedicated to data, however this may upset the PAPR optimisation, and increases the bandwidth of the signal. A single carrier would have poor resilience to multipath. @@ -1192,7 +1200,7 @@ \section{Auxiliary Data} \end{table} \begin{figure}[H] -\caption{Loss versus $E_q/N_0$ for various models. \emph{mode17\_check3} is coincident with \emph{mode17}, indicating very little penalty for the injection of the auxiliary data over a range of SNRs.} +\caption{Loss versus $E_q/N_0$ for various models. \emph{mode17\_check3} is coincident with \emph{model17}, indicating very little penalty for the injection of the auxiliary data over a range of SNRs.} \label{fig:loss_eqno_models} \begin{center} \input loss_EqNo_models.tex diff --git a/doc/stored_file_level.png b/doc/stored_file_level.png new file mode 100644 index 0000000..7e38010 Binary files /dev/null and b/doc/stored_file_level.png differ diff --git a/doc/stored_file_rx.png b/doc/stored_file_rx.png new file mode 100644 index 0000000..baf99fe Binary files /dev/null and b/doc/stored_file_rx.png differ diff --git a/doc/stored_file_test.md b/doc/stored_file_test.md new file mode 100644 index 0000000..48be9f9 --- /dev/null +++ b/doc/stored_file_test.md @@ -0,0 +1,94 @@ +# Stored File Test Procedure + +This document is a test procedure for the August 2024 RADAE stored file test campaign. The general idea is to take a 10 second sample of input speech, then send it over a HF radio channel as compressed SSB and RADAE. This allows a side by side comparison using the same speech, over approximately the same channel conditions. Using a stored file makes it possible to repeat the experiment in a controlled fashion over several trials, for example with varying power levels of different receiver locations. As background a similar campaign was conducted in [April 2024](https://freedv.org/?p=595). + +If you are comfortable with Ubuntu Linux you can install the RADAE tools required on your machine and perform the processing yourself. If you are uncomfortable with Linux the RADAE processing can be performed using a web site. See Web Based Processing section below as an alternative to using `ota_test.sh`. + +## Preparing a file for Tx + +1. If you wish to perform the RADAE processing on your Ubuntu Linux machine, [Install](../README.md#installation) the RADAE software. +1. Record a wave file of your own voice, for example, "Hello, this is VK5XYZ testing the radio Autoencoder 1 2 3 4". +1. The wave file format required is 1 channel, 16 kHz, 16 bit signed integer. +1. We suggest about 10 sec long but feel free to experiment. The length is not critical. +1. Try to make the peak level between about half way and the clipping level. + ![Peak level example](stored_file_level.png) +1. Use a headset microphone and try to avoid room echo and background noise. Don't use a laptop microphone. +1. As examples, there are samples from other hams in `radae/wav` +1. Use `ota_test.sh` to create a `tx.wav`, this consist of chirp-compressed SSB-radae: + ``` + ota_test.sh -x vk5xyz.wav + ``` +1. You can listen to and plot `tx.wav` with your favorite waveform editor, you can see the signals are adjusted to have the same peak level. The SSB compression gain can be adjusted using the `-g` option; `-g 6` is the default. Trying going up or down 3dB. A quieter sample may benefit from more compression. + ![Peak level example](stored_file_tx.png) + +## Transmitting your sample + +1. Configure your SSB radio with voice compressor off. The Tx audio path must be "clean" with no additional processing. +1. The sound card levels should be adjusted to "just move the ALC". +1. Tune the remote receiver (I use a KiwiSDR) to your SSB radio frequency. You need to be within +/- 50 Hz for the RADAE receiver to acquire. +1. Start transmitting. You can do this manually by playing tx.wav through your transmitter, or use `ota_test.sh` + ``` + ./ota_test.sh wav/david_vk5dgr.wav -d -f 14236 + ``` + You can adjust the hamlib rig and serial port with command line options, to get help: `ota_test.sh -h` +1. After you start transmitting quickly start the KiwiSDR recording. +1. When you hear transmission stop on the KiwiSDR, stop recording. +1. The KiwiSDR file will be downloaded. +1. Place a serial number in front of the downloaded file to easily identify it e.g. `vk5xyz_14_`. Make your own notes of the conditions for that sample (e.g. rx station location, distance, power level, anything else you think is relevant) +1. It's useful to load the file into your waveform viewer. I find spectrogram mode useful. + ![Peak level example](stored_file_rx.png) +1. The RADAE receiver will search for the location of the chirp in the first 10 seconds of the sample. Make sure there is no more than 6 seconds of noise before the chirp starts. If necessary, edit the file by removing any excess before the chirp starts. +1. If there is more than a few seconds after the RADAE signal stops, clip that off the sample too. +1. Process the received sample: + ``` + ./ota_test.sh -r ~/Downloads/14_sdr.ironstonerange.com_2024-08-08T05_10_14Z_7175.00_lsb.wav + ``` +1. This will locate the chirp, print the C/No and SNR in dB, and generate several other files in the same directory, `_ssb.wav`, `_radae.wav` and a spectrogram `_spec.jpg`. The mesured C/No and SNR will be in `_report.txt`. +1. Note the C/No, SNR, and listen to the results, comparing SSB to RADAE. +1. Try to collect some interesting results, for example: + * Different channels (ground wave, NVIS, DX) and power levels + * Any cases where RADAE fails to acquire (no decode) + * Intercontinental DX, fast and slow fading + * co-channel interference (SSB on top of or near RADAE) + * interference from carriers + * try listening through small laptop speaker, large external speakers, headphones + * try different microphones + * try different langauges + * different radios and rig interfaces + * Old VFO radio with some drift + +## Web Based Processing + +A web site has been developed to perform the `ota_test.sh` processing. The URL will be made available to the test team. + +1. In the Tx Processing section Browse to the file you want to encode for Tx, then press Process. In a few seconds hopefully `tx.wav`` will be downloaded. Use your own sample (prepared as described above), or for testing choose a sample form the RADAE Git repo [wav](https://github.com/drowe67/radae/tree/main/wav) folder. +2. To test the Rx Processing Browse to the `tx.wav`` you just generated. Press Process, in about 10 seconds a zip file will be returned to you with the SSB and RADAE, a spectogram, and a report file. +3. If you get this far, Transmit `tx.wav`` over your SSB radio to a remote Rx (as described above), and try processing the received file. + +## Receiver log fields + +For example: + +``` +43 state: candidate valid: 1 0 2 Dthresh: 9.55 Dtmax12: 15.67 4.01 tmax: 596 tmax_candidate: 596 fmax: -12.50 +44 state: candidate valid: 1 0 3 Dthresh: 9.34 Dtmax12: 15.08 4.01 tmax: 596 tmax_candidate: 596 fmax: -12.50 +45 state: sync valid: 1 0 25 Dthresh: 8.45 Dtmax12: 12.70 2.56 tmax: 596 tmax_candidate: 596 fmax: -10.98 auxbits: [0 0 0] uw_errors: 0 +46 state: sync valid: 1 0 25 Dthresh: 8.35 Dtmax12: 8.92 1.48 tmax: 596 tmax_candidate: 596 fmax: -10.98 auxbits: [0 1 0] uw_errors: 1 +47 state: sync valid: 0 0 25 Dthresh: 8.17 Dtmax12: 5.17 1.45 tmax: 596 tmax_candidate: 596 fmax: -10.89 auxbits: [0 0 0] uw_errors: 1 +48 state: sync valid: 0 0 24 Dthresh: 7.97 Dtmax12: 3.52 1.47 tmax: 595 tmax_candidate: 596 fmax: -10.83 auxbits: [0 0 1] uw_errors: 2 +49 state: sync valid: 0 0 23 Dthresh: 7.80 Dtmax12: 4.95 1.60 tmax: 596 tmax_candidate: 596 fmax: -10.74 auxbits: [1 0 0] uw_errors: 3 +50 state: sync valid: 0 0 22 Dthresh: 7.64 Dtmax12: 7.06 2.37 tmax: 595 tmax_candidate: 596 fmax: -10.72 auxbits: [0 0 0] uw_errors: 3 +51 state: sync valid: 1 0 21 Dthresh: 7.52 Dtmax12: 8.37 1.06 tmax: 596 tmax_candidate: 596 fmax: -10.81 auxbits: [1 0 0] uw_errors: 4 +``` + +| Field | Description | +| ---- | ---- | +| state | Sync state machine state | +| valid | Input to state machine: valid pilot seq, valid end of over sequence, run on counter | +| Dthresh | current threshold for detection of pilot sequence | +| Dtmax12 | current maxima of pilot sequence, end of over sequence | +| tmax | current timing estimate | +| fmax | current frequency offset estimate | +| auxbits | Auxillary bits received, will be all 0s if no bit errors | +| uw_errors | current count of unique word (auxillary bit) errors, reset every second | + diff --git a/doc/stored_file_tx.png b/doc/stored_file_tx.png new file mode 100644 index 0000000..6d190e3 Binary files /dev/null and b/doc/stored_file_tx.png differ diff --git a/est_CNo.py b/est_CNo.py index f7579a3..af776b6 100644 --- a/est_CNo.py +++ b/est_CNo.py @@ -14,6 +14,7 @@ parser = argparse.ArgumentParser() parser.add_argument('rx', type=str, help='path to signal + noise input file of rate Fs rx samples in ..IQIQ...f32 format') +parser.add_argument('--window_time', type=float, default=4.0, help='Size of time domain window (seconds) used for SNR measurement. If sample is longer than this we search for peak in C/No (default 4.0s)') parser.add_argument('--plots', action='store_true', help='display various plots') parser.add_argument('--flow', type=float, default=400.0, help='lower freq limit for C+N band (default 400 Hz)') parser.add_argument('--fhigh', type=float, default=2000.0, help='upper limit for C+N band (default 2000 Hz)') @@ -21,42 +22,52 @@ Fs = 8000 B3k = 3000 - +N = int(Fs*args.window_time) rx = np.fromfile(args.rx, dtype=np.csingle) -Rx = np.abs(np.fft.fft(rx))**2 -RxdB = 10*np.log10(Rx) -bins_per_Hz = len(rx) / Fs - -flow_bin = int(bins_per_Hz * args.flow) -fhigh_bin = int(bins_per_Hz * args.fhigh) -C_plus_N = np.sum(Rx[flow_bin:fhigh_bin]) -C_plus_N_dB = 10*np.log10(C_plus_N) - -noise_st = fhigh_bin + int(0.1*fhigh_bin) -noise_en = noise_st + int(0.1*fhigh_bin) - -Nbw = (noise_en-noise_st)/bins_per_Hz -No = np.sum(Rx[noise_st:noise_en])/Nbw - -C = C_plus_N - No*(args.fhigh-args.flow) -CdB = 10*np.log10(C) -NodB = 10*np.log10(No) - -CNodB = CdB-NodB -SNRdB = CNodB - 10*np.log10(B3k) - -print(f" C/No SNR3k") -print(f"Measured: {CNodB:6.2f} {SNRdB:6.2f}") - -if args.plots: - fig, ax = plt.subplots(1, 1) - bin_3k = int(bins_per_Hz*B3k) - x = np.arange(bin_3k)/bins_per_Hz - mx = np.max(RxdB) - ax.plot(x, RxdB[:bin_3k]) - ax.plot([args.flow,args.fhigh],[mx,mx],'r') - ax.plot([int(noise_st/bins_per_Hz),int(noise_en/bins_per_Hz)],[mx,mx],'k') - plt.show(block=False) - plt.pause(0.001) - input("hit[enter] to end.") +assert len(rx) >= N +max_CNodB = 0 +max_time = 0 + +for st in np.arange(0,len(rx)-N,Fs//4): + Rx = np.abs(np.fft.fft(rx[st:st+N]))**2 + RxdB = 10*np.log10(Rx) + bins_per_Hz = N / Fs + + flow_bin = int(bins_per_Hz * args.flow) + fhigh_bin = int(bins_per_Hz * args.fhigh) + C_plus_N = np.sum(Rx[flow_bin:fhigh_bin]) + C_plus_N_dB = 10*np.log10(C_plus_N) + + noise_st = fhigh_bin + int(0.1*fhigh_bin) + noise_en = noise_st + int(0.1*fhigh_bin) + + Nbw = (noise_en-noise_st)/bins_per_Hz + No = np.sum(Rx[noise_st:noise_en])/Nbw + + C = C_plus_N - No*(args.fhigh-args.flow) + if C > 0: + CdB = 10*np.log10(C) + NodB = 10*np.log10(No) + + CNodB = CdB-NodB + if CNodB > max_CNodB: + max_CNodB = CNodB + max_time = st/Fs + + print(f"time: {st:8d} {st/Fs:5.2f} CNodB: {CNodB:5.2f}") + + if args.plots: + fig, ax = plt.subplots(1, 1) + bin_3k = int(bins_per_Hz*B3k) + x = np.arange(bin_3k)/bins_per_Hz + mx = np.max(RxdB) + ax.plot(x, RxdB[:bin_3k]) + ax.plot([args.flow,args.fhigh],[mx,mx],'r') + ax.plot([int(noise_st/bins_per_Hz),int(noise_en/bins_per_Hz)],[mx,mx],'k') + plt.show(block=False) + plt.pause(0.001) + input("hit[enter] to end.") +max_SNRdB = max_CNodB - 10*np.log10(B3k) +print(f" Time C/No SNR3k") +print(f"Measured: {max_time:5.2f} {max_CNodB:6.2f} {max_SNRdB:6.2f}") diff --git a/evaluate.sh b/evaluate.sh index 28a1689..e46b3c0 100755 --- a/evaluate.sh +++ b/evaluate.sh @@ -2,7 +2,7 @@ # # Evaluate a model and sample wave file using AWGN and MPP multipath models, with SSB at same C/No -OPUS=${HOME}/opus +OPUS=build/src CODEC2=${HOME}/codec2-dev/build_linux/src PATH=${PATH}:${OPUS}:${CODEC2} diff --git a/inference.py b/inference.py index a4d2f26..3d2c513 100644 --- a/inference.py +++ b/inference.py @@ -114,13 +114,13 @@ features = features[:, :, :num_used_features] if args.auxdata: #aux_symb = 1.0 - 2.0*(np.random.rand(1,features.shape[1],1) > 0.5) - aux_symb = -np.ones((1,features.shape[1],1)) + aux_symb = -np.ones((1,features.shape[1],1),dtype=np.float32) symb_repeat = 4 for i in range(1,symb_repeat): aux_symb[0,i::symb_repeat,:] = aux_symb[0,::symb_repeat,:] - #print(features.shape, aux_symb.shape) + #print(features.dtype, aux_symb.dtype) #quit() - features = np.concatenate([features, aux_symb],axis=2,dtype=np.float32) + features = np.concatenate([features, aux_symb],axis=2) features = torch.tensor(features) print(f"Processing: {nb_features_rounded} feature vectors") diff --git a/inference.sh b/inference.sh index 00a01e8..e85a53a 100755 --- a/inference.sh +++ b/inference.sh @@ -2,7 +2,7 @@ # # Some automation around inference.py to help with testing -OPUS=${HOME}/opus +OPUS=build/src PATH=${PATH}:${OPUS} if [ $# -lt 3 ]; then @@ -33,6 +33,9 @@ shift; shift; shift lpcnet_demo -features ${input_speech} ${features_in} python3 ./inference.py ${model} ${features_in} ${features_out} "$@" +if [ $? -ne 0 ]; then + exit 1 +fi if [ $output_speech == "-" ]; then tmp=$(mktemp) lpcnet_demo -fargan-synthesis ${features_out} ${tmp} diff --git a/ota_test.sh b/ota_test.sh index d20ab5f..b030680 100755 --- a/ota_test.sh +++ b/ota_test.sh @@ -24,7 +24,7 @@ # echo "m" | rigctl -m 3061 -r /dev/ttyUSB0 # 5. Using Settings to make sure default sound device is not the radio # 6. Adjust HF radio Tx drive so ALC is just being tickled, set desired RF power: -# ./ota_test.sh wav/david.wav -x +# ./ota_test.sh wav/david_vk5dgr.wav -x # aplay -f S16_LE --device="plughw:CARD=CODEC,DEV=0" tx.wav # # Usage @@ -37,7 +37,7 @@ # aplay rx_ssb.wav rx_radae.wav # # 2. Use IC-7200 SSB radio to Tx -# ./ota_test.sh wav/david.wav -g 9 -d -f 14236 +# ./ota_test.sh wav/david_vk5dgr.wav -d -f 14236 # # 3. Process file rx.wav received off. First use a wav file editor to trim any silence from start, then: # ./ota_test.sh -r rx.wav @@ -50,10 +50,10 @@ # # TODO: way to adjust /build_linux/src for OSX -CODEC2_DEV=${HOME}/codec2-dev +CODEC2_DEV=${CODEC2_DEV:-${HOME}/codec2-dev} PATH=${PATH}:${CODEC2_DEV}/build_linux/src:${CODEC2_DEV}/build_linux/misc:${PWD}/build/src -which ch || { printf "\n**** Can't find ch - check CODEC2_PATH **** \n\n"; exit 1; } +which ch >/dev/null || { printf "\n**** Can't find ch - check CODEC2_PATH **** \n\n"; exit 1; } kiwi_url="" port=8074 @@ -72,6 +72,8 @@ setpoint_peak=16384 freq_offset=0 peak=1 hackrf=0 +tx_path="." +just_tx=0 source utils.sh @@ -94,6 +96,8 @@ function print_help { echo " default /dev/ttyUSB0" echo " -x InputSpeechWaveFile Generate tx.wav and exit (no SSB radio Tx)" echo " --rms Equalise RMS power of RADAE and SSB (default is equal peak power)" + echo " --tx_path optional path to tx.raw/tx.wav" + echo " -t SSBRadioFile.raw Tx SSBRadioFile.raw over SSB radio (e.g. tx.wav or RADAE encoded file), no pre-processing" echo exit } @@ -111,15 +115,14 @@ function run_rigctl { } function clean_up { - echo "killing KiwiSDR process" - kill ${kiwi_pid} - wait ${kiwi_pid} 2>/dev/null - exit 1 + run_rigctl "\\set_ptt 0" $model } function process_rx { + echo "-----------------------------------------------" echo "Process receiver sample" - # Place results in same path, same file name as inpout file + echo "-----------------------------------------------" + # Place results in same path, same file name as input file filename="${1%.*}" rx=$(mktemp).wav @@ -130,11 +133,20 @@ function process_rx { plot_specgram(s, 8000, 200, 3000); print('${filename}_spec.jpg', '-djpg'); \ quit" | octave-cli -p ${CODEC2_PATH}/octave -qf > /dev/null - # extract chirp at start and estimate C/No + # extract chirp at start and estimate C/No, and chirp start time. We allow a 10 second window rx_chirp=$(mktemp) - sox $rx -t .s16 ${rx_chirp}.raw trim 0 4 + sox $rx -t .s16 ${rx_chirp}.raw trim 0 10 cat ${rx_chirp}.raw | python3 int16tof32.py --zeropad > ${rx_chirp}.f32 - python3 est_CNo.py ${rx_chirp}.f32 + est_log=$(mktemp) + python3 est_CNo.py ${rx_chirp}.f32 | tee $est_log + chirp_start=$(cat ${est_log} | grep "Measured:" | tr -s ' ' | cut -d' ' -f2) + cat ${est_log} | tail -n 2 > ${filename}_report.txt + echo >> ${filename}_report.txt + + # remove silence before chirp + rx_trim=$(mktemp).wav + sox $rx $rx_trim trim $chirp_start + cp $rx_trim $rx # 4 sec chirp - 1 sec silence - x sec SSB - 1 sec silence - x sec RADAE # start_radae = 4+1+x @@ -144,13 +156,47 @@ function process_rx { rx_radae=$(mktemp) sox $rx ${filename}_ssb.wav trim 5 $x sox $rx -t .s16 ${rx_radae}.raw trim $start_radae + sox -t .s16 -r 8000 -c 1 ${rx_radae}.raw radae_in.wav # wave version for debugging # Use streaming RADAE Rx cat ${rx_radae}.raw | python3 int16tof32.py --zeropad > ${rx_radae}.f32 - cat ${rx_radae}.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata > features_rx_out.f32 + cat ${rx_radae}.f32 | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata 2>>${filename}_report.txt > features_rx_out.f32 lpcnet_demo -fargan-synthesis features_rx_out.f32 - | sox -t .s16 -r 16000 -c 1 - ${filename}_radae.wav } +function tx_ssb_radio { + echo "--------------------------------------------------------" + echo "Transmitting $1 using SSB radio" + echo "--------------------------------------------------------" + + tx_file=$1 + + echo "Tx data signal" + freq_Hz=$((freq_kHz*1000)) + # TODO - IC7200 switches from USB sound card to mic when I try to set PKTLSB/PKTUSB + # - How to select LSB/USB while keeping internal sound card? + #usb_lsb=$(python3 -c "print('usb') if ${freq_kHz} >= 10000 else print('lsb')") + #usb_lsb_upper=$(echo ${usb_lsb} | awk '{print toupper($0)}') + #run_rigctl "\\set_mode PKT${usb_lsb_upper} 0" $model + run_rigctl "\\set_freq ${freq_Hz}" $model + run_rigctl "\\set_ptt 1" $model + if [ `uname` == "Darwin" ]; then + AUDIODEV="${soundDevice}" play -t raw -b 16 -c 1 -r 8000 -e signed-integer --endian little ${tx_file} + else + aplay --device="${soundDevice}" -f S16_LE ${tx_file} 2>/dev/null + fi + if [ $? -ne 0 ]; then + run_rigctl "\\set_ptt 0" $model + clean_up + echo "Problem running aplay!" + echo " 1. Is ${soundDevice} configured as the default sound device in Settings-Sound?" + echo " 2. Is pavucontrol running?" + + exit 1 + fi + run_rigctl "\\set_ptt 0" $model +} + POSITIONAL=() while [[ $# -gt 0 ]] @@ -217,6 +263,15 @@ case $key in shift shift ;; + --tx_path) + tx_path="$2" + shift + shift + ;; + -t) + just_tx=1 + shift + ;; -h) print_help ;; @@ -233,21 +288,45 @@ if [ $# -eq 0 ]; then fi if [ $rxwavefile -eq 1 ]; then + if [ ! -f $1 ]; then + echo "Can't find input wave file: ${1}!" + exit 1 + fi process_rx $1 $freq_offset exit 0 fi speechfile="$1" if [ ! -f $speechfile ]; then - echo "Can't find input speech wave file: ${speechfile}!" + echo "Can't find input file: ${speechfile}!" exit 1 fi +if [ $just_tx -eq 1 ]; then + tx_ssb_radio $1 + exit 0 +fi + +# check format of input speech file +soxi_log=$(mktemp) +soxi $speechfile > $soxi_log +channels=$(cat ${soxi_log} | grep "Channels" | tr -s ' ' | cut -d' ' -f3) +sample_rate=$(cat ${soxi_log} | grep "Sample Rate" | tr -s ' ' | cut -d' ' -f4) +if [ $channels -ne 1 ] || [ $sample_rate -ne 16000 ]; then + echo "Input speech wave file must be single channel 16000 Hz sample rate" + exit 1 +fi + # create Tx file ------------------------ # create 400-2000 Hz chirp header used for C/No est. We generate 4.5s of chirp, to allow for trimming of # rx wave file - we need >=4 seconds of received chirp for C/No est at Rx -tx_chirp=$(mktemp) + +echo "--------------------------------------------------------" +echo "Creating chirp - compressed SSB - RADAE wave file tx.wav" +echo "--------------------------------------------------------" + +chirp=$(mktemp) if [ $peak -eq 1 ]; then amp=$(python3 -c "import numpy as np; amp=0.25*${setpoint_peak}/8192.0; print(\"%f\" % amp)") else @@ -297,36 +376,19 @@ sox -t .s16 -r 8k -c 1 $tx_ssb -t .s16 -r 8k -c 1 ${tx_ssb}_pad.raw pad 1@0 sox -t .s16 -r 8k -c 1 ${tx_radae}.raw -t .s16 -r 8k -c 1 ${tx_radae}_pad.raw pad 1@0 # cat signals together so we can send them over a radio at the same time -cat ${chirp}.raw ${tx_ssb}_pad.raw ${tx_radae}_pad.raw > tx.raw -sox -t .s16 -r 8000 -c 1 tx.raw tx.wav +cat ${chirp}.raw ${tx_ssb}_pad.raw ${tx_radae}_pad.raw > ${tx_path}/tx.raw +sox -t .s16 -r 8000 -c 1 ${tx_path}/tx.raw ${tx_path}/tx.wav # generate a 4MSP .iq8 file suitable for replaying by HackRF (can disable if not using HackRF) if [ $hackrf -eq 1 ]; then - ch tx.raw - --complexout | tsrc - - 5 -c | tlininterp - tx.iq8 100 -d -f + ch ${tx_path}/tx.raw - --complexout | tsrc - - 5 -c | tlininterp - tx.iq8 100 -d -f fi if [ $tx_file -eq 1 ]; then + echo "Finished OK!" exit 0 fi -# transmit using local SSB radio - -echo "Tx data signal" -freq_Hz=$((freq_kHz*1000)) -usb_lsb_upper=$(echo ${usb_lsb} | awk '{print toupper($0)}') -run_rigctl "\\set_freq ${freq_Hz}" $model -run_rigctl "\\set_ptt 1" $model -if [ `uname` == "Darwin" ]; then - AUDIODEV="${soundDevice}" play -t raw -b 16 -c 1 -r 8000 -e signed-integer --endian little tx.raw -else - aplay --device="${soundDevice}" -f S16_LE tx.raw 2>/dev/null -fi -if [ $? -ne 0 ]; then - run_rigctl "\\set_ptt 0" $model - clean_up - echo "Problem running aplay!" - echo "Is ${soundDevice} configured as the default sound device in Settings-Sound?" - exit 1 -fi -run_rigctl "\\set_ptt 0" $model +tx_ssb_radio ${tx_path}/tx.raw +echo "Finished OK!" diff --git a/public_html/tx_form.html b/public_html/tx_form.html new file mode 100755 index 0000000..79d7b3b --- /dev/null +++ b/public_html/tx_form.html @@ -0,0 +1,16 @@ + + +

RADAE Tx Processing

+
+ + + +
+

RADAE Rx Processing

+
+ + + +
+ + \ No newline at end of file diff --git a/public_html/tx_process.cgi b/public_html/tx_process.cgi new file mode 100755 index 0000000..e82f371 --- /dev/null +++ b/public_html/tx_process.cgi @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import cgi +import cgitb +import sys +import os +import subprocess +from pathlib import Path + +def choke_on_error(error_str): + print("Content-type: text/html\n\n") + print(error_str) + quit() + +def return_file(filename): + print(f"returning {filename}", file=sys.stderr) + print(f"Content-Type: application/octet-stream") + print(f"Content-Disposition: attachment; filename={os.path.basename(filename)}") + print() + sys.stdout.flush() + bstdout = open(sys.stdout.fileno(), 'wb', closefd=False) + file = open(filename,'rb') + bstdout.write(file.read()) + +cgitb.enable() + +# extract the form fields and pointer to POSTed file +form = cgi.FieldStorage() +form_file = form['file'] +form_filename = os.path.basename(form_file.filename) +form_processing = form.getvalue('processing') + +if len(form_filename) == 0: + choke_on_error("No file supplied") + +# anything on stderr will appear on apache error log which is very handy for tracing execution +# tail -f /var/log/apache2/error.log +print("\n----------------------------------------------------", file=sys.stderr) +print(f"{form_processing} {form_filename}", file=sys.stderr) +print("----------------------------------------------------\n", file=sys.stderr) + +# upload file from POST +data = sys.stdin.buffer.read() +with open(f"/tmp/{form_filename}", 'wb') as f: + while True: + chunk = form_file.file.read(100000) + if not chunk: + break + f.write(chunk) + +my_env = os.environ.copy() +my_env["CODEC2_DEV"] = "/home/david/codec2-dev" +my_env["MPLCONFIGDIR"] = "/tmp" # stop matplotlib complaining abpout cache directory +os.chdir('../radae') +if form_processing == "tx": + ota_test = subprocess.check_output(["./ota_test.sh","-x",f"/tmp/{form_filename}","--tx_path","/tmp/","-d",],env=my_env, encoding='utf-8').replace('\n','
') + #print(ota_test, file=sys.stderr) + return_file('/tmp/tx.wav') +else: + ota_test = subprocess.check_output(["./ota_test.sh","-r",f"/tmp/{form_filename}","-d",],env=my_env, encoding='utf-8').replace('\n','
') + # zip up files for return + filename = Path(form_filename).stem + #print("Content-type: text/html\n\n") + zip_test = subprocess.check_output(["zip","-j",f"/tmp/{filename}.zip",f"/tmp/{filename}_ssb.wav", f"/tmp/{filename}_radae.wav", + f"/tmp/{filename}_spec.jpg", f"/tmp/{filename}_report.txt"], encoding='utf-8').replace('\n','
') + print(zip_test, file=sys.stderr) + print("finished!", file=sys.stderr) + return_file(f"/tmp/{filename}.zip") diff --git a/radae/dsp.py b/radae/dsp.py index e7b05bf..e4ea108 100644 --- a/radae/dsp.py +++ b/radae/dsp.py @@ -148,6 +148,7 @@ def detect_pilots(self, rx): Dt1 = np.zeros((self.Nmf,len(self.fcoarse_range)), dtype=np.csingle) Dt2 = np.zeros((self.Nmf,len(self.fcoarse_range)), dtype=np.csingle) Dtmax12 = 0 + f_ind_max = 0 tmax = 0 fmax = 0 @@ -256,6 +257,7 @@ def check_pilots(self, rx, tmax, fmax): sigma_r2 = np.mean(np.abs(self.Dt2))/((np.pi/2)**0.5) sigma_r = (sigma_r1 + sigma_r2)/2.0 Dthresh = 2*sigma_r*np.sqrt(-np.log(self.Pacq_error2/5.0)) + Dthresh_eoo = 2*sigma_r*np.sqrt(-np.log(self.Pacq_error1/5.0)) # low thresh of false EOO # compare to maxima at current timing and freq offset w = 2*np.pi*fmax/Fs @@ -267,7 +269,7 @@ def check_pilots(self, rx, tmax, fmax): # compare with end of over sequence Dtmax12_eoo = np.abs(np.dot(np.conj(w_vec*rx[tmax+M+Ncp:tmax+2*M+Ncp]),pend)) Dtmax12_eoo += np.abs(np.dot(np.conj(w_vec*rx[tmax+Nmf:tmax+Nmf+M]),pend)) - endofover = Dtmax12_eoo > Dthresh + endofover = Dtmax12_eoo > Dthresh_eoo self.Dthresh = Dthresh self.Dtmax12 = Dtmax12 diff --git a/radae/radae.py b/radae/radae.py index 0a449f8..4082311 100644 --- a/radae/radae.py +++ b/radae/radae.py @@ -540,8 +540,9 @@ def __init__(self, # DFT matrices for Nc freq samples, M time samples (could be a FFT but matrix convenient for small, non power of 2 DFTs) self.M = round(self.Fs / Rs_dash) # oversampling rate - lower = round(400/Rs_dash) # start carrier freqs at about 400Hz to be above analog filtering in radios - self.w = 2*m.pi*(lower+torch.arange(Nc))/self.M # note: must be integer DFT freq indexes or DFT falls over + carrier_1_freq = 1500-Rs_dash*Nc/2 # centre signal on 1500 Hz offset from carrier (centre of SSB radio passband) + carrier_1_index = round(carrier_1_freq/Rs_dash) # DFT index of first carrier, must be an integer for OFDM to work + self.w = 2*m.pi*(carrier_1_index+torch.arange(Nc))/self.M # note: must be integer DFT freq indexes or DFT falls over self.Winv = torch.zeros((Nc,self.M), dtype=torch.complex64) # inverse DFT matrix, Nc freq domain to M time domain (OFDM Tx) self.Wfwd = torch.zeros((self.M,Nc), dtype=torch.complex64) # forward DFT matrix, M time domain to Nc freq domain (OFDM Rx) for c in range(0,Nc): @@ -742,7 +743,7 @@ def do_pilot_eq(self, num_modem_frames, rx_sym_pilots): mag = torch.mean(torch.abs(rx_pilots)**2)**0.5 if self.bottleneck == 3: mag = mag*torch.abs(self.P[0])/self.pilot_gain - print(f"coarse mag: {mag:f}") + print(f"coarse mag: {mag:f}", file=sys.stderr) rx_sym_pilots = rx_sym_pilots/mag return rx_sym_pilots @@ -781,13 +782,13 @@ def receiver(self, rx): z_hat[:,:,1::2] = rx_sym.imag if self.stateful_decoder: - print("stateful!") + print("stateful!", file=sys.stderr) features_hat = torch.empty(1,0,self.feature_dim) for i in range(z_hat.shape[1]): features_hat = torch.cat([features_hat, self.core_decoder_statefull(z_hat[:,i:i+1,:])],dim=1) else: features_hat = self.core_decoder(z_hat) - print(features_hat.shape,z_hat.shape) + print(features_hat.shape,z_hat.shape, file=sys.stderr) return features_hat,z_hat diff --git a/radae_rx.py b/radae_rx.py index 66a2816..fa224f2 100644 --- a/radae_rx.py +++ b/radae_rx.py @@ -59,6 +59,8 @@ parser.add_argument('-v', type=int, default=2, help='Verbose level (default 2)') parser.add_argument('--no_stdout', action='store_false', dest='use_stdout', help='disable the use of stdout (e.g. with python3 -m cProfile)') parser.add_argument('--auxdata', action='store_true', help='inject auxillary data symbol') +parser.add_argument('--disable_unsync', type=float, default=0.0, help='test mode: disable auxdata based unsyncs after this many seconds (default disabled)') + parser.set_defaults(bpf=True) parser.set_defaults(use_stdout=True) args = parser.parse_args() @@ -78,7 +80,7 @@ model = RADAE(num_features, latent_dim, EbNodB=100, ber_test=args.ber_test, rate_Fs=True, pilots=True, pilot_eq=True, eq_mean6 = False, cyclic_prefix=0.004, coarse_mag=True,time_offset=-16, bottleneck=args.bottleneck) -checkpoint = torch.load(args.model_name, map_location='cpu') +checkpoint = torch.load(args.model_name, map_location='cpu',weights_only=True) model.load_state_dict(checkpoint['state_dict'], strict=False) # Stateful decoder wasn't present during training, so we need to load weights from existing decoder model.core_decoder_statefull_load_state_dict() @@ -113,13 +115,6 @@ acq = acquisition(Fs,Rs,M,Ncp,Nmf,p,model.pend) -""" -# optional acq_test variables -tmax_candidate_target = Ncp + Ntap/2 -acq_pass = 0 -acq_fail = 0 -""" - tmax_candidate = 0 acquired = False state = "search" @@ -130,7 +125,8 @@ Nmf_unsync = int(Tunsync*Fs/Nmf) endofover = False uw_errors = 0 -uw_error_thresh = 12 +uw_error_thresh = 7 # P(reject|correct) = 1 - binocdf(8,24,0.1) = 4.5E-4 + # P(accept|false) = binocdf(8,24,0.5) = 3.2E-3 synced_count = 0 synced_count_one_sec = Fs//Nmf @@ -163,9 +159,23 @@ fmax = 0.9*fmax + 0.1*fmax_hat candidate,endofover = acq.check_pilots(rx_buf,tmax,fmax) + # handle timing slip when rx sample clock > tx sample clock + nin = Nmf + if tmax >= Nmf-M: + nin = Nmf + M + tmax -= M + #print("slip+", file=sys.stderr) + # handle timing slip when rx sample clock < tx sample clock + if tmax < M: + nin = Nmf - M + tmax += M + #print("slip-", file=sys.stderr) + synced_count += 1 - if synced_count == synced_count_one_sec: - synced_count = 0 + if synced_count % synced_count_one_sec == 0: + if uw_errors > uw_error_thresh: + uw_fail = True + uw_errors = 0 if not endofover: # correct frequency offset, note we preserve state of phase @@ -174,7 +184,9 @@ for n in range(Nmf+M+Ncp): rx_phase = rx_phase*np.exp(-1j*w) rx_phase_vec[n] = rx_phase - rx = torch.tensor(rx_buf[tmax-Ncp:tmax-Ncp+Nmf+M+Ncp]*rx_phase_vec, dtype=torch.complex64) + rx1 = rx_buf[tmax-Ncp:tmax-Ncp+Nmf+M+Ncp] + #print(tmax-Ncp, tmax-Ncp+Nmf+M+Ncp,rx_buf.shape, rx1.shape, rx_phase_vec.shape, file=sys.stderr) + rx = torch.tensor(rx1*rx_phase_vec, dtype=torch.complex64) # run through RADAE receiver DSP z_hat = receiver.receiver_one(rx) # decode z_hat to features @@ -186,8 +198,6 @@ aux_bits = 1*(aux_symb[0,::symb_repeat] > 0) features_hat = features_hat[:,:,0:20] uw_errors += np.sum(aux_bits) - if synced_count == 0: - uw_errors = 0 # add unused features and send to stdout features_hat = torch.cat([features_hat, torch.zeros_like(features_hat)[:,:,:16]], dim=-1) features_hat = features_hat.cpu().detach().numpy().flatten().astype('float32') @@ -197,23 +207,12 @@ if len(args.write_latent): z_hat_log = torch.cat([z_hat_log,z_hat]) - # handle timing slip when rx sample clock > tx sample clock - nin = Nmf - if tmax >= Nmf-M: - nin = Nmf + M - tmax -= M - #print("slip+", file=sys.stderr) - # handle timing slip when rx sample clock < tx sample clock - if tmax < M: - nin = Nmf - M - tmax += M - #print("slip-", file=sys.stderr) if args.v == 2 or (args.v == 1 and (state == "search" or state == "candidate" or prev_state == "candidate")): print(f"{mf:3d} state: {state:10s} valid: {candidate:d} {endofover:d} {valid_count:2d} Dthresh: {acq.Dthresh:8.2f} ", end='', file=sys.stderr) - print(f"Dtmax12: {acq.Dtmax12:8.2f} {acq.Dtmax12_eoo:8.2f} tmax: {tmax:4d} tmax_candidate: {tmax_candidate:4d} fmax: {fmax:6.2f}", end='', file=sys.stderr) + print(f"Dtmax12: {acq.Dtmax12:8.2f} {acq.Dtmax12_eoo:8.2f} tmax: {tmax:4d} fmax: {fmax:6.2f}", end='', file=sys.stderr) if args.auxdata and state == "sync": - print(f" auxbits: {aux_bits:} uw_errors: {uw_errors:d}", file=sys.stderr) + print(f" aux: {aux_bits:} uw_err: {uw_errors:d}", file=sys.stderr) else: print("",file=sys.stderr) @@ -233,6 +232,7 @@ next_state = "sync" acquired = True synced_count = 0 + uw_fail = False if args.auxdata: uw_errors = 0 valid_count = Nmf_unsync @@ -245,14 +245,22 @@ else: next_state = "search" elif state == "sync": + # during some tests it's useful to disable these unsync features + unsync_enable = True + if args.disable_unsync: + if synced_count > int(args.disable_unsync*Fs/Nmf): + unsync_enable = False + if candidate: valid_count = Nmf_unsync else: valid_count -= 1 - if valid_count == 0: + if unsync_enable and valid_count == 0: next_state = "search" - if endofover or uw_errors > uw_error_thresh: + + if unsync_enable and (endofover or uw_fail): next_state = "search" + state = next_state mf += 1 diff --git a/radae_rx.sh b/radae_rx.sh index 2201381..32e1451 100755 --- a/radae_rx.sh +++ b/radae_rx.sh @@ -2,7 +2,7 @@ # # Some automation around radae_rx.py to help with testing -OPUS=${HOME}/opus +OPUS=build/src PATH=${PATH}:${OPUS} features_out=features_rx_out.f32 diff --git a/radae_tx.py b/radae_tx.py index cac428d..a581031 100644 --- a/radae_tx.py +++ b/radae_tx.py @@ -49,6 +49,7 @@ parser.add_argument('--ber_test', type=str, default="", help='symbols are PSK bits, compare to z.f32 file to calculate BER') parser.add_argument('--bottleneck', type=int, default=3, help='1-1D rate Rs, 2-2D rate Rs, 3-2D rate Fs time domain') parser.add_argument('--no_stdout', action='store_false', dest='use_stdout', help='disable the use of stdout (e.g. with python3 -m cProfile)') +parser.add_argument('--auxdata', action='store_true', help='inject auxillary data symbol') parser.set_defaults(use_stdout=True) args = parser.parse_args() @@ -60,6 +61,8 @@ nb_total_features = 36 num_features = 20 num_used_features = 20 +if args.auxdata: + num_features += 1 # load model from a checkpoint file model = RADAE(num_features, latent_dim, EbNodB=100, ber_test=args.ber_test, rate_Fs=True, @@ -84,6 +87,12 @@ buffer_f32 = np.frombuffer(buffer,np.single) features = torch.reshape(torch.tensor(buffer_f32),(1,model.Nzmf*model.enc_stride, nb_total_features)) features = features[:,:,:num_used_features] + if args.auxdata: + aux_symb = -torch.ones((1,features.shape[1],1)) + symb_repeat = 4 + for i in range(1,symb_repeat): + aux_symb[0,i::symb_repeat,:] = aux_symb[0,::symb_repeat,:] + features = torch.concatenate([features, aux_symb],axis=2) #print(features.shape, file=sys.stderr) num_timesteps_at_rate_Rs = model.num_timesteps_at_rate_Rs(model.Nzmf*model.enc_stride) z = model.core_encoder_statefull(features) diff --git a/rx.sh b/rx.sh index 9c16a16..e317ebc 100755 --- a/rx.sh +++ b/rx.sh @@ -2,7 +2,7 @@ # # Some automation around rx.py to help with testing -OPUS=${HOME}/opus +OPUS=build/src PATH=${PATH}:${OPUS} features_out=features_rx_out.f32 diff --git a/stateful_decoder.sh b/stateful_decoder.sh index 8bce3cd..325b7f0 100755 --- a/stateful_decoder.sh +++ b/stateful_decoder.sh @@ -2,7 +2,7 @@ # # Some automation around stateful_decoder.sh to help with testing -OPUS=${HOME}/opus +OPUS=build/src PATH=${PATH}:${OPUS} if [ $# -lt 3 ]; then diff --git a/stateful_encoder.sh b/stateful_encoder.sh index a412cce..d48f032 100755 --- a/stateful_encoder.sh +++ b/stateful_encoder.sh @@ -2,7 +2,7 @@ # # Some automation around stateful_encoder.sh to help with testing -OPUS=${HOME}/opus +OPUS=build/src PATH=${PATH}:${OPUS} if [ $# -lt 3 ]; then diff --git a/test/chirp_mpp.sh b/test/chirp_mpp.sh index dc0ac09..333c046 100755 --- a/test/chirp_mpp.sh +++ b/test/chirp_mpp.sh @@ -13,23 +13,56 @@ fi CODEC2_DEV_BUILD_DIR=$1 No=$2 +chirp_duration=4 +silence_duration=3 source test/make_g.sh cp -f g_mpp.f32 fast_fading_samples.float chirp_f32=$(mktemp) chirp_int16=$(mktemp) +silence_int16=$(mktemp) +chirp_pad_int16=$(mktemp) chirp_noise_int16=$(mktemp) chirp_noise_f32=$(mktemp) -python3 chirp.py ${chirp_f32} 4 -cat ${chirp_f32} | python3 f32toint16.py --real > ${chirp_int16} ch_log=$(mktemp) +python3 chirp.py ${chirp_f32} ${chirp_duration} +cat ${chirp_f32} | python3 f32toint16.py --real > ${chirp_int16} +# pad either side with a few seconds of seconds of silence +dd if=/dev/zero of=/dev/stdout bs=16000 count=${silence_duration} > ${silence_int16} +cat ${silence_int16} ${chirp_int16} ${silence_int16} > ${chirp_pad_int16} # Note --ssbfilt 1 (default) removes -ve freq part of complex noise # which ensures C/No remains unaffected by real() operation at output -${CODEC2_DEV_BUILD_DIR}/src/ch ${chirp_int16} ${chirp_noise_int16} --No ${No} --mpp --fading_dir . --after_fade 2>${ch_log} +${CODEC2_DEV_BUILD_DIR}/src/ch ${chirp_pad_int16} ${chirp_noise_int16} --No ${No} --mpp --fading_dir . --after_fade 2>${ch_log} cat ${chirp_noise_int16} | python3 int16tof32.py --zeropad > ${chirp_noise_f32} est_log=$(mktemp) python3 est_CNo.py ${chirp_noise_f32} >${est_log} +# check two estimates are about the same + CNodB_ch=$(cat ${ch_log} | grep "C/No" | tr -s ' ' | cut -d' ' -f5) -CNodB_est=$(cat ${est_log} | grep "Measured:" | tr -s ' ' | cut -d' ' -f2) -python3 -c "if abs(${CNodB_ch}-${CNodB_est}) < 1.0: print('PASS')" +CNodB_est=$(cat ${est_log} | grep "Measured:" | tr -s ' ' | cut -d' ' -f3) +python3 < 1.0: + sys.exit(1) +EOF +if [ $? -eq 1 ]; then + exit 1 +fi + +# check time estimate is OK + +chirp_start=$(cat ${est_log} | grep "Measured:" | tr -s ' ' | cut -d' ' -f2) +python3 < 0.5: + sys.exit(1) +EOF +if [ $? -eq 1 ]; then + exit 1 +fi + diff --git a/test/ota_test_cal.sh b/test/ota_test_cal.sh index ce3640a..e9fd55d 100755 --- a/test/ota_test_cal.sh +++ b/test/ota_test_cal.sh @@ -17,6 +17,7 @@ No=$2 loss_thresh=$3 shift; shift; GAIN=0.25 # allow some headroom for noise and fading to prevent clipping +silence_duration=1 printf "\nMake fading samples .... \n\n" source test/make_g.sh @@ -24,25 +25,28 @@ cp -f g_mpp.f32 fast_fading_samples.float printf "\nGenerate tx file and add noise ... \n\n" ./ota_test.sh -x wav/brian_g8sez.wav --peak -${CODEC2_DEV_BUILD_DIR}/src/ch tx.wav - --gain ${GAIN} --No ${No} --after_fade --fading_dir . $@ | sox -t .s16 -r 8000 -c 1 - rx.wav +# add 1 second of silence to start to give est_CNo.py a work out +dd if=/dev/zero of=/dev/stdout bs=16000 count=${silence_duration} | sox -t .s16 -r 8000 -c 1 - sil.wav +sox sil.wav tx.wav tx_pad.wav +${CODEC2_DEV_BUILD_DIR}/src/ch tx_pad.wav - --gain ${GAIN} --No ${No} --after_fade --fading_dir . $@ | sox -t .s16 -r 8000 -c 1 - rx.wav printf "\nRun chirp only through 'ch' to get reference estimate of C/No ... \n\n" ch_log=$(mktemp) sox tx.wav -t .s16 - trim 0 4 | \ ~/codec2-dev/build_linux/src/ch - /dev/null --gain ${GAIN} --No ${No} --after_fade --fading_dir . $@ 2>${ch_log} +CNodB_ch=$(cat ${ch_log} | grep "C/No" | tr -s ' ' | cut -d' ' -f5) printf "\nRun Rx and check ML "loss" is OK ... \n\n" # We don't check acq time as start time of RADAE is uncertain due to silence etc rm -f features_rx_out.f32 rx_log=$(mktemp) -./ota_test.sh -d -r rx.wav >${rx_log} +./ota_test.sh -r rx.wav >${rx_log} python3 loss.py features_in.f32 features_rx_out.f32 --loss_test ${loss_thresh} --clip_start 150 | tee /dev/stderr | grep "PASS" if [ $? -ne 0 ]; then exit 1 fi printf "\nCheck C/No estimates close ...\n\n" -CNodB_ch=$(cat ${ch_log} | grep "C/No" | tr -s ' ' | cut -d' ' -f5) CNodB_est=$(cat ${rx_log} | grep "Measured:" | tr -s ' ' | cut -d' ' -f2) if [ $? -ne 0 ]; then exit 1 diff --git a/wav/david.wav b/wav/david.wav deleted file mode 100644 index 0d11ea3..0000000 Binary files a/wav/david.wav and /dev/null differ diff --git a/wav/david_vk5dgr.wav b/wav/david_vk5dgr.wav new file mode 100644 index 0000000..5f008b6 Binary files /dev/null and b/wav/david_vk5dgr.wav differ diff --git a/wav/vk5dgr_test.wav b/wav/vk5dgr_test.wav deleted file mode 100644 index f750bb2..0000000 Binary files a/wav/vk5dgr_test.wav and /dev/null differ