diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 2e1533ecd..1f9133e26 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -25,7 +25,7 @@ jobs: run: | sudo apt-get update sudo apt-get upgrade -y - sudo apt-get install codespell libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.2-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev xvfb pipewire pulseaudio-utils pipewire-pulse wireplumber metacity dbus-x11 at-spi2-core rtkit + sudo apt-get install codespell libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.2-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev xvfb pipewire pulseaudio-utils pipewire-pulse wireplumber metacity dbus-x11 at-spi2-core rtkit octave octave-signal - name: Spellcheck codebase shell: bash diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index fd1d41374..7fb914abd 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -23,7 +23,20 @@ jobs: - name: Install packages shell: bash working-directory: ${{github.workspace}} - run: brew install automake libtool numpy sox + run: brew install automake libtool numpy sox octave + + - name: Install octave-signal + shell: bash + working-directory: ${{github.workspace}} + run: | + # make sure gfortran is available + sudo ln -s /opt/homebrew/bin/gfortran-14 /opt/homebrew/bin/gfortran + ls /opt/homebrew/bin/gfortran* + #sudo mkdir /usr/local/gfortran + #ls /usr/local/Cellar + #sudo ln -s /usr/local/Cellar/gcc@14/*/lib/gcc/14 /usr/local/gfortran/lib + gfortran --version + octave-cli --eval "pkg install -forge control; pkg install -forge signal" - name: Install virtual audio devices shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 98e189a34..d76138ea3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -731,4 +731,14 @@ DefineAudioTest(RADEV1) DefineAudioTest(700D) DefineAudioTest(700E) DefineAudioTest(1600) + +add_test(NAME rade_reporting_clean COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/test_rade_reporting.sh) +set_tests_properties(rade_reporting_clean PROPERTIES PASS_REGULAR_EXPRESSION "Reporting callsign ZZ0ZZZ @ SNR") + +add_test(NAME rade_reporting_awgn COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/test_rade_reporting.sh ${CODEC2_BUILD_DIR} awgn) +set_tests_properties(rade_reporting_awgn PROPERTIES PASS_REGULAR_EXPRESSION "Reporting callsign ZZ0ZZZ @ SNR") + +add_test(NAME rade_reporting_mpp COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/test_rade_reporting.sh ${CODEC2_BUILD_DIR} mpp) +set_tests_properties(rade_reporting_mpp PROPERTIES PASS_REGULAR_EXPRESSION "Reporting callsign ZZ0ZZZ @ SNR") + endif(UNITTEST) diff --git a/cmake/BuildCodec2.cmake b/cmake/BuildCodec2.cmake index b7afa480e..3bb001529 100644 --- a/cmake/BuildCodec2.cmake +++ b/cmake/BuildCodec2.cmake @@ -38,6 +38,8 @@ set_target_properties(codec2 PROPERTIES IMPORTED_IMPLIB "${BINARY_DIR}/src/libcodec2${CMAKE_IMPORT_LIBRARY_SUFFIX}" ) +set(CODEC2_BUILD_DIR ${BINARY_DIR}) + if(BOOTSTRAP_LPCNET) add_dependencies(build_codec2 build_lpcnetfreedv) endif(BOOTSTRAP_LPCNET) diff --git a/src/freedv_interface.cpp b/src/freedv_interface.cpp index 8bc067533..24287b4e1 100644 --- a/src/freedv_interface.cpp +++ b/src/freedv_interface.cpp @@ -34,6 +34,9 @@ using namespace std::placeholders; +#include +extern wxString utFreeDVMode; + static const char* GetCurrentModeStrImpl_(int mode) { switch(mode) @@ -75,7 +78,8 @@ FreeDVInterface::FreeDVInterface() : rade_(nullptr), lpcnetEncState_(nullptr), radeTxStep_(nullptr), - sync_(0) + sync_(0), + radeTextPtr_(nullptr) { // empty } @@ -105,6 +109,19 @@ void FreeDVInterface::OnReliableTextRx_(reliable_text_t rt, const char* txt_ptr, reliable_text_reset(rt); } +void FreeDVInterface::OnRadeTextRx_(rade_text_t rt, const char* txt_ptr, int length, void* state) +{ + log_info("FreeDVInterface::OnRadeTextRx_: received %s", txt_ptr); + + FreeDVInterface* obj = (FreeDVInterface*)state; + assert(obj != nullptr); + + { + std::unique_lock lock(obj->reliableTextMutex_); + obj->receivedReliableText_ = txt_ptr; + } +} + float FreeDVInterface::GetMinimumSNR_(int mode) { switch(mode) @@ -155,6 +172,20 @@ void FreeDVInterface::start(int txMode, int fifoSizeMs, bool singleRxThread, boo rade_ = rade_open(modelFile, RADE_USE_C_ENCODER | RADE_USE_C_DECODER | (wxGetApp().appConfiguration.debugVerbose ? 0 : RADE_VERBOSE_0)); assert(rade_ != nullptr); + if (usingReliableText) + { + log_info("creating RADE text object"); + radeTextPtr_ = rade_text_create(); + assert(radeTextPtr_ != nullptr); + + rade_text_set_rx_callback(radeTextPtr_, &FreeDVInterface::OnRadeTextRx_, this); + + if (utFreeDVMode != "") + { + rade_text_enable_stats_output(radeTextPtr_, true); + } + } + float zeros[320] = {0}; float in_features[5*NB_TOTAL_FEATURES] = {0}; fargan_init(&fargan_); @@ -242,6 +273,12 @@ void FreeDVInterface::stop() reliable_text_destroy(reliableTextObj); } reliableText_.clear(); + + if (radeTextPtr_ != nullptr) + { + rade_text_destroy(radeTextPtr_); + radeTextPtr_ = nullptr; + } for (auto& dv : dvObjects_) { @@ -649,6 +686,16 @@ const char* FreeDVInterface::getReliableText() void FreeDVInterface::setReliableText(const char* callsign) { + // Special case for RADE. + if (rade_ != nullptr && radeTextPtr_ != nullptr) + { + log_info("generating RADE text string"); + int nsyms = rade_n_eoo_bits(rade_); + float eooSyms[nsyms]; + rade_text_generate_tx_string(radeTextPtr_, callsign, strlen(callsign), eooSyms, nsyms); + rade_tx_set_eoo_bits(rade_, eooSyms); + } + for (auto& rt : reliableText_) { reliable_text_set_string(rt, callsign, strlen(callsign)); @@ -721,7 +768,7 @@ IPipelineStep* FreeDVInterface::createReceivePipeline( if (txMode_ >= FREEDV_MODE_RADE) { // special handling for RADE - parallelSteps.push_back(new RADEReceiveStep(rade_, &fargan_)); + parallelSteps.push_back(new RADEReceiveStep(rade_, &fargan_, radeTextPtr_)); state->preProcessFn = [&](ParallelStep*) { return 0; }; state->postProcessFn = std::bind(&FreeDVInterface::postProcessRxFn_, this, _1); //[&](ParallelStep*) { return 0; }; diff --git a/src/freedv_interface.h b/src/freedv_interface.h index 1d7f2d57e..276c72595 100644 --- a/src/freedv_interface.h +++ b/src/freedv_interface.h @@ -41,6 +41,7 @@ // RADE required include files #include "rade_api.h" +#include "pipeline/rade_text.h" // TBD - need to wrap in "extern C" to avoid linker errors extern "C" @@ -154,6 +155,7 @@ class FreeDVInterface static void FreeDVTextRxFn_(void *callback_state, char c); static void OnReliableTextRx_(reliable_text_t rt, const char* txt_ptr, int length, void* state); + static void OnRadeTextRx_(rade_text_t rt, const char* txt_ptr, int length, void* state); static float GetMinimumSNR_(int mode); @@ -192,6 +194,7 @@ class FreeDVInterface LPCNetEncState *lpcnetEncState_; RADETransmitStep *radeTxStep_; int sync_; + rade_text_t radeTextPtr_; int preProcessRxFn_(ParallelStep* ps); int postProcessRxFn_(ParallelStep* ps); diff --git a/src/gui/dialogs/freedv_reporter.cpp b/src/gui/dialogs/freedv_reporter.cpp index 007b3615b..7beb8a3bb 100644 --- a/src/gui/dialogs/freedv_reporter.cpp +++ b/src/gui/dialogs/freedv_reporter.cpp @@ -51,7 +51,8 @@ extern FreeDVInterface freedvInterface; #define NUM_COLS (LAST_UPDATE_DATE_COL + 1) #endif // defined(WIN32) #define RX_ONLY_STATUS "RX Only" -#define RX_COLORING_TIMEOUT_SEC (20) +#define RX_COLORING_LONG_TIMEOUT_SEC (20) +#define RX_COLORING_SHORT_TIMEOUT_SEC (2) #define MSG_COLORING_TIMEOUT_SEC (5) #define STATUS_MESSAGE_MRU_MAX_SIZE (10) #define MESSAGE_COLUMN_ID (6) @@ -756,9 +757,14 @@ void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) auto reportData = allReporterData_[*sidPtr]; bool isTransmitting = reportData->transmitting; - bool isReceiving = + bool isReceivingValidCallsign = reportData->lastRxDate.IsValid() && - reportData->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_TIMEOUT_SEC)); + reportData->lastRxCallsign != "" && + reportData->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_LONG_TIMEOUT_SEC)); + bool isReceivingNotValidCallsign = + reportData->lastRxDate.IsValid() && + reportData->lastRxCallsign == "" && + reportData->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_SHORT_TIMEOUT_SEC)); bool isMessaging = reportData->lastUpdateUserMessage.IsValid() && reportData->lastUpdateUserMessage.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, MSG_COLORING_TIMEOUT_SEC)); @@ -777,7 +783,7 @@ void FreeDVReporterDialog::OnTimer(wxTimerEvent& event) backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowBackgroundColor); foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowForegroundColor); } - else if (isReceiving) + else if (isReceivingValidCallsign || isReceivingNotValidCallsign) { backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowBackgroundColor); foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowForegroundColor); @@ -2027,7 +2033,9 @@ void FreeDVReporterDialog::addOrUpdateListIfNotFiltered_(ReporterData* data, std backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowBackgroundColor); foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterTxRowForegroundColor); } - else if (data->lastRxDate.IsValid() && data->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_TIMEOUT_SEC))) + else if (data->lastRxDate.IsValid() && + ((data->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_SHORT_TIMEOUT_SEC)) && data->lastRxCallsign == "") || + (data->lastRxDate.ToUTC().IsEqualUpTo(curDate, wxTimeSpan(0, 0, RX_COLORING_LONG_TIMEOUT_SEC)) && data->lastRxCallsign != ""))) { backgroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowBackgroundColor); foregroundColor = wxColour(wxGetApp().appConfiguration.reportingConfiguration.freedvReporterRxRowForegroundColor); diff --git a/src/main.cpp b/src/main.cpp index c6759a049..7dab5c714 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -195,6 +195,8 @@ wxString utTxFile; wxString utRxFile; wxString utTxFeatureFile; wxString utRxFeatureFile; +int utTxTimeSeconds; +int utTxAttempts; // WxWidgets - initialize the application @@ -292,60 +294,76 @@ void MainApp::UnitTest_() /*sim.MouseMove(frame->m_togBtnOnOff->GetScreenPosition()); sim.MouseClick();*/ - // Wait 5 seconds for FreeDV to start - std::this_thread::sleep_for(5s); + // Wait for FreeDV to start + std::this_thread::sleep_for(1s); + while (true) + { + bool isRunning = false; + frame->executeOnUiThreadAndWait_([&]() { + isRunning = frame->m_togBtnOnOff->IsEnabled(); + }); + if (isRunning) break; + std::this_thread::sleep_for(20ms); + } if (testName == "tx") - { - // Fire event to begin TX - //sim.MouseMove(frame->m_btnTogPTT->GetScreenPosition()); - //sim.MouseClick(); - log_info("Firing PTT"); - CallAfter([&]() { - frame->m_btnTogPTT->SetValue(true); - wxCommandEvent* txEvent = new wxCommandEvent(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, frame->m_btnTogPTT->GetId()); - txEvent->SetEventObject(frame->m_btnTogPTT); - //QueueEvent(txEvent); - frame->OnTogBtnPTT(*txEvent); - delete txEvent; - }); - - if (utTxFile != "") + { + log_info("Transmitting %d times", utTxAttempts); + for (int numTimes = 0; numTimes < utTxAttempts; numTimes++) { - // Transmit until file has finished playing - SF_INFO sfInfo; - sfInfo.format = 0; - g_sfPlayFile = sf_open((const char*)utTxFile.ToUTF8(), SFM_READ, &sfInfo); - g_sfTxFs = sfInfo.samplerate; - g_loopPlayFileToMicIn = false; - g_playFileToMicIn = true; - - while (g_playFileToMicIn) + // Fire event to begin TX + //sim.MouseMove(frame->m_btnTogPTT->GetScreenPosition()); + //sim.MouseClick(); + log_info("Firing PTT"); + CallAfter([&]() { + frame->m_btnTogPTT->SetValue(true); + wxCommandEvent* txEvent = new wxCommandEvent(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, frame->m_btnTogPTT->GetId()); + txEvent->SetEventObject(frame->m_btnTogPTT); + //QueueEvent(txEvent); + frame->OnTogBtnPTT(*txEvent); + delete txEvent; + }); + + if (utTxFile != "") { - std::this_thread::sleep_for(20ms); - } - } - else - { - // Transmit for 60 seconds - std::this_thread::sleep_for(60s); + // Transmit until file has finished playing + SF_INFO sfInfo; + sfInfo.format = 0; + g_sfPlayFile = sf_open((const char*)utTxFile.ToUTF8(), SFM_READ, &sfInfo); + g_sfTxFs = sfInfo.samplerate; + g_loopPlayFileToMicIn = false; + g_playFileToMicIn = true; + + while (g_playFileToMicIn) + { + std::this_thread::sleep_for(20ms); + } + } + else + { + // Transmit for user given time period (default 60 seconds) + std::this_thread::sleep_for(std::chrono::seconds(utTxTimeSeconds)); + } + + // Stop transmitting + log_info("Firing PTT"); + endingTx = true; + std::this_thread::sleep_for(1s); + CallAfter([&]() { + frame->m_btnTogPTT->SetValue(false); + endingTx = true; + wxCommandEvent* rxEvent = new wxCommandEvent(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, frame->m_btnTogPTT->GetId()); + rxEvent->SetEventObject(frame->m_btnTogPTT); + frame->OnTogBtnPTT(*rxEvent); + delete rxEvent; + //QueueEvent(rxEvent); + }); + /*sim.MouseMove(frame->m_btnTogPTT->GetScreenPosition()); + sim.MouseClick();*/ + + // Wait 5 seconds for FreeDV to stop + std::this_thread::sleep_for(5s); } - - // Stop transmitting - log_info("Firing PTT"); - CallAfter([&]() { - frame->m_btnTogPTT->SetValue(false); - wxCommandEvent* rxEvent = new wxCommandEvent(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, frame->m_btnTogPTT->GetId()); - rxEvent->SetEventObject(frame->m_btnTogPTT); - frame->OnTogBtnPTT(*rxEvent); - delete rxEvent; - //QueueEvent(rxEvent); - }); - /*sim.MouseMove(frame->m_btnTogPTT->GetScreenPosition()); - sim.MouseClick();*/ - - // Wait 5 seconds for FreeDV to stop - //std::this_thread::sleep_for(5s); } else { @@ -413,6 +431,8 @@ void MainApp::OnInitCmdLine(wxCmdLineParser& parser) parser.AddOption("txfile", wxEmptyString, "In UT mode, pipes given WAV file through transmit pipeline."); parser.AddOption("rxfeaturefile", wxEmptyString, "Capture RX features from RADE decoder into the provided file."); parser.AddOption("txfeaturefile", wxEmptyString, "Capture TX features from FARGAN encoder into the provided file."); + parser.AddOption("txtime", "60", "In UT mode, the amount of time to transmit (default 60 seconds)", wxCMD_LINE_VAL_NUMBER); + parser.AddOption("txattempts", "1", "In UT mode, the number of times to transmit (default 1)", wxCMD_LINE_VAL_NUMBER); } bool MainApp::OnCmdLineParsed(wxCmdLineParser& parser) @@ -457,6 +477,24 @@ bool MainApp::OnCmdLineParsed(wxCmdLineParser& parser) { log_info("Piping %s through TX pipeline", (const char*)utTxFile.ToUTF8()); } + + if (parser.Found("txtime", (long*)&utTxTimeSeconds)) + { + log_info("Will transmit for %d seconds", utTxTimeSeconds); + } + else + { + utTxTimeSeconds = 60; + } + + if (parser.Found("txattempts", (long*)&utTxAttempts)) + { + log_info("Will transmit %d time(s)", utTxAttempts); + } + else + { + utTxAttempts = 1; + } } if (parser.Found("rxfeaturefile", &utRxFeatureFile)) @@ -1879,7 +1917,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) { long long freqLongLong = freq; log_info( - "Adding callsign %s @ SNR %d, freq %lld to PSK Reporter.\n", + "Reporting callsign %s @ SNR %d, freq %lld to reporting services.\n", pendingCallsign.c_str(), pendingSnr, freqLongLong); @@ -1903,7 +1941,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) freedvInterface.getCurrentMode() == FREEDV_MODE_RADE && freedvInterface.getSync()) { - // Special case for RADE--report 'unk' for callsign so we can + // Special case for RADE--report '--' for callsign so we can // at least report that we're receiving *something*. int64_t freq = wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency; @@ -1916,7 +1954,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) if (!g_playFileFromRadio) { wxGetApp().m_sharedReporterObject->addReceiveRecord( - "unk", + "", freedvInterface.getCurrentModeStr(), freq, 0 diff --git a/src/pipeline/CMakeLists.txt b/src/pipeline/CMakeLists.txt index 448bf364f..67d57835c 100644 --- a/src/pipeline/CMakeLists.txt +++ b/src/pipeline/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(fdv_audio_pipeline STATIC TapStep.cpp ToneInterfererStep.cpp TxRxThread.cpp + rade_text.c ) target_include_directories(fdv_audio_pipeline PRIVATE ${CODEC2_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/..) @@ -50,4 +51,11 @@ DefineUnitTest(LevelAdjustTest) DefineUnitTest(ResampleTest) target_link_libraries(ResampleTest PRIVATE ${FREEDV_LINK_LIBS}) DefineUnitTest(TapTest) + +if(LINUX) +# TBD - won't execute on macOS just yet. +DefineUnitTest(RadeTextTest) +target_link_libraries(RadeTextTest PRIVATE ${FREEDV_LINK_LIBS} codec2 rade opus) +endif(LINUX) + endif(UNITTEST) diff --git a/src/pipeline/RADEReceiveStep.cpp b/src/pipeline/RADEReceiveStep.cpp index 0ce4ed1d0..9ca515bde 100644 --- a/src/pipeline/RADEReceiveStep.cpp +++ b/src/pipeline/RADEReceiveStep.cpp @@ -27,12 +27,13 @@ extern wxString utRxFeatureFile; -RADEReceiveStep::RADEReceiveStep(struct rade* dv, FARGANState* fargan) +RADEReceiveStep::RADEReceiveStep(struct rade* dv, FARGANState* fargan, rade_text_t textPtr) : dv_(dv) , fargan_(fargan) , inputSampleFifo_(nullptr) , outputSampleFifo_(nullptr) , featuresFile_(nullptr) + , textPtr_(textPtr) { // Set FIFO to be 2x the number of samples per run so we don't lose anything. inputSampleFifo_ = codec2_fifo_create(rade_nin_max(dv_) * 2); @@ -106,43 +107,53 @@ std::shared_ptr RADEReceiveStep::execute(std::shared_ptr inputSamp } // RADE processing (input signal->features). - nout = rade_rx(dv_, features_out, input_buf_cplx); - if (featuresFile_) + int hasEooOut = 0; + float eooOut[rade_n_eoo_bits(dv_)]; + nout = rade_rx(dv_, features_out, &hasEooOut, eooOut, input_buf_cplx); + if (hasEooOut) { - fwrite(features_out, sizeof(float), nout, featuresFile_); + // Handle RX of bits from EOO. + rade_text_rx(textPtr_, eooOut, rade_n_eoo_bits(dv_) / 2); } - - for (int i = 0; i < nout; i++) - { - pendingFeatures_.push_back(features_out[i]); - } - - // FARGAN processing (features->analog audio) - while (pendingFeatures_.size() >= NB_TOTAL_FEATURES) + else { - // XXX - lpcnet_demo reads NB_TOTAL_FEATURES from RADE - // but only processes NB_FEATURES of those for some reason. - float featuresIn[NB_FEATURES]; - for (int i = 0; i < NB_FEATURES; i++) + if (featuresFile_) { - featuresIn[i] = pendingFeatures_[0]; - pendingFeatures_.erase(pendingFeatures_.begin()); + fwrite(features_out, sizeof(float), nout, featuresFile_); } - for (int i = 0; i < (NB_TOTAL_FEATURES - NB_FEATURES); i++) + + for (int i = 0; i < nout; i++) { - pendingFeatures_.erase(pendingFeatures_.begin()); + pendingFeatures_.push_back(features_out[i]); } - float fpcm[LPCNET_FRAME_SIZE]; - short pcm[LPCNET_FRAME_SIZE]; - fargan_synthesize(fargan_, fpcm, featuresIn); - for (int i = 0; i < LPCNET_FRAME_SIZE; i++) + // FARGAN processing (features->analog audio) + while (pendingFeatures_.size() >= NB_TOTAL_FEATURES) { - pcm[i] = (int)floor(.5 + MIN32(32767, MAX32(-32767, 32768.f*fpcm[i]))); + // XXX - lpcnet_demo reads NB_TOTAL_FEATURES from RADE + // but only processes NB_FEATURES of those for some reason. + float featuresIn[NB_FEATURES]; + for (int i = 0; i < NB_FEATURES; i++) + { + featuresIn[i] = pendingFeatures_[0]; + pendingFeatures_.erase(pendingFeatures_.begin()); + } + for (int i = 0; i < (NB_TOTAL_FEATURES - NB_FEATURES); i++) + { + pendingFeatures_.erase(pendingFeatures_.begin()); + } + + float fpcm[LPCNET_FRAME_SIZE]; + short pcm[LPCNET_FRAME_SIZE]; + fargan_synthesize(fargan_, fpcm, featuresIn); + for (int i = 0; i < LPCNET_FRAME_SIZE; i++) + { + pcm[i] = (int)floor(.5 + MIN32(32767, MAX32(-32767, 32768.f*fpcm[i]))); + } + + *numOutputSamples += LPCNET_FRAME_SIZE; + codec2_fifo_write(outputSampleFifo_, pcm, LPCNET_FRAME_SIZE); } - - *numOutputSamples += LPCNET_FRAME_SIZE; - codec2_fifo_write(outputSampleFifo_, pcm, LPCNET_FRAME_SIZE); } nin = rade_nin(dv_); diff --git a/src/pipeline/RADEReceiveStep.h b/src/pipeline/RADEReceiveStep.h index f63f06f24..c6730642f 100644 --- a/src/pipeline/RADEReceiveStep.h +++ b/src/pipeline/RADEReceiveStep.h @@ -29,6 +29,7 @@ #include "../freedv_interface.h" #include "rade_api.h" #include "codec2_fifo.h" +#include "rade_text.h" // TBD - need to wrap in "extern C" to avoid linker errors extern "C" @@ -39,7 +40,7 @@ extern "C" class RADEReceiveStep : public IPipelineStep { public: - RADEReceiveStep(struct rade* dv, FARGANState* fargan); + RADEReceiveStep(struct rade* dv, FARGANState* fargan, rade_text_t textPtr); virtual ~RADEReceiveStep(); virtual int getInputSampleRate() const; @@ -54,8 +55,8 @@ class RADEReceiveStep : public IPipelineStep struct FIFO* inputSampleFifo_; struct FIFO* outputSampleFifo_; std::vector pendingFeatures_; - FILE* featuresFile_; + rade_text_t textPtr_; }; #endif // AUDIO_PIPELINE__RADE_RECEIVE_STEP_H diff --git a/src/pipeline/rade_text.c b/src/pipeline/rade_text.c new file mode 100644 index 000000000..f1d78c013 --- /dev/null +++ b/src/pipeline/rade_text.c @@ -0,0 +1,491 @@ +//========================================================================== +// Name: rade_text.c +// +// Purpose: Handles reliable text (e.g. text with FEC). +// Created: August 15, 2021 +// Authors: Mooneer Salem +// +// License: +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program; if not, see . +// +//========================================================================== + +#include "rade_text.h" + +#include +#include +#include +#include +#include +#include + +#include "gp_interleaver.h" +#include "ldpc_codes.h" +#include "ofdm_internal.h" +#include "util/logging/ulog.h" + +#define LDPC_TOTAL_SIZE_BITS (112) + +#define RADE_TEXT_MAX_LENGTH (8) +#define RADE_TEXT_CRC_LENGTH (1) +#define RADE_TEXT_MAX_RAW_LENGTH (RADE_TEXT_MAX_LENGTH + RADE_TEXT_CRC_LENGTH) + +/* Two bytes of text/CRC equal four bytes of LDPC(112,56). */ +#define RADE_TEXT_BYTES_PER_ENCODED_SEGMENT (8) + +static float LastEncodedLDPC[LDPC_TOTAL_SIZE_BITS]; +static char LastLDPCAsBits[LDPC_TOTAL_SIZE_BITS]; + +/* Internal definition of rade_text_t. */ +typedef struct +{ + on_text_rx_t text_rx_callback; + void *callback_state; + + char tx_text[LDPC_TOTAL_SIZE_BITS]; + int tx_text_index; + int tx_text_length; + + COMP inbound_pending_syms[LDPC_TOTAL_SIZE_BITS / 2]; + float inbound_pending_amps[LDPC_TOTAL_SIZE_BITS / 2]; + float incomingData[LDPC_TOTAL_SIZE_BITS]; + + struct LDPC ldpc; + int enableStats; + + int unusedEooBitCount; + int unusedEooErrCount; +} rade_text_impl_t; + +// 6 bit character set for text field use: +// 0: ASCII null +// 1-9: ASCII 38-47 +// 10-19: ASCII '0'-'9' +// 20-46: ASCII 'A'-'Z' +// 47: ASCII ' ' +static void convert_callsign_to_ota_string_(const char *input, char *output, int maxLength) +{ + assert(input != NULL); + assert(output != NULL); + assert(maxLength >= 0); + + int outidx = 0; + for (size_t index = 0; index < maxLength; index++) + { + if (input[index] == 0) + break; + + if (input[index] >= 38 && input[index] <= 47) + { + output[outidx++] = input[index] - 37; + } + else if (input[index] >= '0' && input[index] <= '9') + { + output[outidx++] = input[index] - '0' + 10; + } + else if (input[index] >= 'A' && input[index] <= 'Z') + { + output[outidx++] = input[index] - 'A' + 20; + } + else if (input[index] >= 'a' && input[index] <= 'z') + { + output[outidx++] = toupper(input[index]) - 'A' + 20; + } + } + output[outidx] = 0; +} + +static void convert_ota_string_to_callsign_(const char *input, char *output, int maxLength) +{ + assert(input != NULL); + assert(output != NULL); + assert(maxLength >= 0); + + int outidx = 0; + for (size_t index = 0; index < maxLength; index++) + { + if (input[index] == 0) + break; + + if (input[index] >= 1 && input[index] <= 9) + { + output[outidx++] = input[index] + 37; + } + else if (input[index] >= 10 && input[index] <= 19) + { + output[outidx++] = input[index] - 10 + '0'; + } + else if (input[index] >= 20 && input[index] <= 46) + { + output[outidx++] = input[index] - 20 + 'A'; + } + } + output[outidx] = 0; +} + +static char calculateCRC8_(char *input, int length) +{ + assert(input != NULL); + assert(length >= 0); + + unsigned char generator = 0x1D; + unsigned char crc = 0x00; /* start with 0 so first byte can be 'xored' in */ + + while (length > 0) + { + unsigned char ch = *input++; + length--; + + // Break out if we see a null. + if (ch == 0) + break; + + crc ^= ch; /* XOR-in the next input byte */ + + for (int i = 0; i < 8; i++) + { + if ((crc & 0x80) != 0) + { + crc = (unsigned char)((crc << 1) ^ generator); + } + else + { + crc <<= 1; + } + } + } + + return crc; +} + +static int rade_text_ldpc_decode(rade_text_impl_t *obj, char *dest, float meanAmplitude) +{ + assert(obj != NULL); + assert(dest != NULL); + + float llr[LDPC_TOTAL_SIZE_BITS]; + unsigned char output[LDPC_TOTAL_SIZE_BITS]; + int parityCheckCount = 0; + + // Calculate raw BER. + int bitsRaw = 0; + int errorsRaw = 0; + if (obj->enableStats) + { + /*FILE* fp = fopen("tx_bits.f32", "wb"); + assert(fp != NULL); + fwrite(LastEncodedLDPC, sizeof(float), LDPC_TOTAL_SIZE_BITS, fp); + fclose(fp); + fp = fopen("rx_bits.f32", "wb"); + assert(fp != NULL); + fwrite(obj->inbound_pending_syms, sizeof(float), LDPC_TOTAL_SIZE_BITS, fp); + fclose(fp);*/ + + for (int index = 0; index < LDPC_TOTAL_SIZE_BITS; index++) + { + bitsRaw++; + float* pendingFloats = (float*)obj->inbound_pending_syms; + //log_info("LastEncodedLDPC[%d] = %f, pendingFloats[%d] = %f", index, LastEncodedLDPC[index], index, pendingFloats[index]); + int err = (LastEncodedLDPC[index] * pendingFloats[index]) < 0; + if (err) + { + errorsRaw++; + } + } + } + + // Use soft decision for the LDPC decoder. + int Npayloadsymsperpacket = LDPC_TOTAL_SIZE_BITS / 2; + float EsNo = 3.0; // note: constant from freedv_700.c + log_info("mean amplitude: %f", meanAmplitude); + + symbols_to_llrs(llr, (COMP *)obj->inbound_pending_syms, obj->inbound_pending_amps, EsNo, meanAmplitude, + Npayloadsymsperpacket); + run_ldpc_decoder(&obj->ldpc, output, llr, &parityCheckCount); + + // Data is valid if BER < 0.2 + float ber_est = (float)(obj->ldpc.NumberParityBits - parityCheckCount) / obj->ldpc.NumberParityBits; + int result = (ber_est < 0.2); + + log_info("Estimated BER: %f", ber_est); + + if (obj->enableStats) + { + // Calculate coded BER. + int bitsCoded = 0; + int errorsCoded = 0; + for (int index = 0; index < LDPC_TOTAL_SIZE_BITS / 2; index++) + { + bitsCoded++; + int err = LastLDPCAsBits[index] != output[index]; + if (err) + { + errorsCoded++; + } + } + + log_info("EOO Tbits: %6d Terr: %6d BER: %4.3f", bitsRaw + obj->unusedEooBitCount, errorsRaw + obj->unusedEooErrCount, + (float)(errorsRaw + obj->unusedEooErrCount) / (bitsRaw + obj->unusedEooBitCount + 1E-12)); + log_info("Raw Tbits: %6d Terr: %6d BER: %4.3f", bitsRaw, errorsRaw, (float)errorsRaw / (bitsRaw + 1E-12)); + float coded_ber = (float)errorsCoded / (bitsCoded + 1E-12); + log_info("Coded Tbits: %6d Terr: %6d BER: %4.3f", bitsCoded, errorsCoded, coded_ber); + } + + if (result) + { + memset(dest, 0, RADE_TEXT_BYTES_PER_ENCODED_SEGMENT); + + for (int bitIndex = 0; bitIndex < 8; bitIndex++) + { + if (output[bitIndex]) + dest[0] |= 1 << bitIndex; + } + for (int bitIndex = 8; bitIndex < (LDPC_TOTAL_SIZE_BITS / 2); bitIndex++) + { + int bitsSinceCrc = bitIndex - 8; + if (output[bitIndex]) + dest[1 + (bitsSinceCrc / 6)] |= (1 << (bitsSinceCrc % 6)); + } + } + + return result; +} + +/* Decode received symbols from RADE decoder. */ +void rade_text_rx(rade_text_t ptr, float *syms, int symSize) +{ + rade_text_impl_t *obj = (rade_text_impl_t *)ptr; + assert(obj != NULL); + + // Deinterleave received bits. + gp_deinterleave_comp((COMP *)obj->inbound_pending_syms, (COMP *)syms, LDPC_TOTAL_SIZE_BITS / 2); + + // Calculate RMS of all symbols + float rms = 0; + obj->unusedEooBitCount = 0; + obj->unusedEooErrCount = 0; + for (int index = 0; index < symSize; index++) + { + if (index < (LDPC_TOTAL_SIZE_BITS / 2)) + { + COMP *sym = (COMP *)&obj->inbound_pending_syms[index]; + //sym->real = syms[2 * index]; + //sym->imag = syms[2 * index + 1]; + rms += sym->real * sym->real + sym->imag * sym->imag; + } + else + { + // This is the unused part of the EOO that was filled with a known sequence. + float* sym = &syms[2 * index]; + //rms += sym[0] * sym[0] + sym[1] * sym[1]; + + if (obj->enableStats) + { + obj->unusedEooBitCount += 2; + + // Note: the expected sym[0] should always be 0, so + // the EOO formula (expected * real < 0) won't take it + // into consideration. + int err = sym[1] < 0; + if (err) obj->unusedEooErrCount++; + } + } + } + rms = sqrt(rms / symSize); + + // Copy over symbols prior to decode. + for (int index = 0; index < LDPC_TOTAL_SIZE_BITS / 2; index++) + { + COMP *sym = (COMP *)&obj->inbound_pending_syms[index]; + log_debug("RX symbol: %f, %f", sym->real, sym->imag); + + obj->inbound_pending_amps[index] = rms; + log_debug("RX symbol rotated: %f, %f, amp: %f", obj->inbound_pending_syms[index].real, + obj->inbound_pending_syms[index].imag, obj->inbound_pending_amps[index]); + } + + // We have all the bits we need, so we're ready to decode. + char decodedStr[RADE_TEXT_MAX_RAW_LENGTH + 1]; + char rawStr[RADE_TEXT_MAX_RAW_LENGTH + 1]; + memset(rawStr, 0, RADE_TEXT_MAX_RAW_LENGTH + 1); + memset(decodedStr, 0, RADE_TEXT_MAX_RAW_LENGTH + 1); + + if (rade_text_ldpc_decode(obj, rawStr, rms) != 0) + { + // BER is under limits. + convert_ota_string_to_callsign_(&rawStr[RADE_TEXT_CRC_LENGTH], &decodedStr[RADE_TEXT_CRC_LENGTH], + RADE_TEXT_MAX_LENGTH); + decodedStr[0] = rawStr[0]; // CRC + + // Get expected and actual CRC. + unsigned char receivedCRC = decodedStr[0]; + unsigned char calcCRC = calculateCRC8_(&rawStr[RADE_TEXT_CRC_LENGTH], RADE_TEXT_MAX_LENGTH); + + log_info("rxCRC: %d, calcCRC: %d, decodedStr: %s", receivedCRC, calcCRC, &decodedStr[RADE_TEXT_CRC_LENGTH]); + + if (receivedCRC == calcCRC && obj->text_rx_callback) + { + // We got a valid string. Call assigned callback. + obj->text_rx_callback(obj, &decodedStr[RADE_TEXT_CRC_LENGTH], strlen(&decodedStr[RADE_TEXT_CRC_LENGTH]), + obj->callback_state); + } + } +} + +rade_text_t rade_text_create() +{ + rade_text_impl_t *ret = calloc(1, sizeof(rade_text_impl_t)); + assert(ret != NULL); + + // Load LDPC code into memory. + int code_index = ldpc_codes_find("HRA_56_56"); + memcpy(&ret->ldpc, &ldpc_codes[code_index], sizeof(struct LDPC)); + + return (rade_text_t)ret; +} + +void rade_text_destroy(rade_text_t ptr) +{ + assert(ptr != NULL); + free(ptr); +} + +void rade_text_generate_tx_string(rade_text_t ptr, const char *str, int strlength, float *syms, int symSize) +{ + rade_text_impl_t *impl = (rade_text_impl_t *)ptr; + assert(impl != NULL); + + char tmp[RADE_TEXT_MAX_RAW_LENGTH + 1]; + memset(tmp, 0, RADE_TEXT_MAX_RAW_LENGTH + 1); + + convert_callsign_to_ota_string_(str, &tmp[RADE_TEXT_CRC_LENGTH], + strlength < RADE_TEXT_MAX_LENGTH ? strlength : RADE_TEXT_MAX_LENGTH); + + int txt_length = strlen(&tmp[RADE_TEXT_CRC_LENGTH]); + if (txt_length >= RADE_TEXT_MAX_LENGTH) + { + txt_length = RADE_TEXT_MAX_LENGTH; + } + impl->tx_text_length = LDPC_TOTAL_SIZE_BITS; + impl->tx_text_index = 0; + unsigned char crc = calculateCRC8_(&tmp[RADE_TEXT_CRC_LENGTH], txt_length); + tmp[0] = crc; + + // Encode block of text using LDPC(112,56). + unsigned char ibits[LDPC_TOTAL_SIZE_BITS / 2]; + unsigned char pbits[LDPC_TOTAL_SIZE_BITS / 2]; + memset(ibits, 0, LDPC_TOTAL_SIZE_BITS / 2); + memset(pbits, 0, LDPC_TOTAL_SIZE_BITS / 2); + for (int index = 0; index < 8; index++) + { + if (tmp[0] & (1 << index)) + ibits[index] = 1; + } + + // Pack 6 bit characters into single LDPC block. + for (int ibitsBitIndex = 8; ibitsBitIndex < (LDPC_TOTAL_SIZE_BITS / 2); ibitsBitIndex++) + { + int bitsFromCrc = ibitsBitIndex - 8; + unsigned int byte = tmp[RADE_TEXT_CRC_LENGTH + bitsFromCrc / 6]; + unsigned int bitToCheck = bitsFromCrc % 6; + // fprintf(stderr, "bit index: %d, byte: %x, bit to check: %d, result: + // %d\n", ibitsBitIndex, byte, bitToCheck, (byte & (1 << bitToCheck)) != 0); + + if (byte & (1 << bitToCheck)) + { + ibits[ibitsBitIndex] = 1; + } + } + + encode(&impl->ldpc, ibits, pbits); + + // Split LDPC encoded bits into individual bits, with the first + // RADE_TEXT_UW_LENGTH_BITS being UW. + char tmpbits[LDPC_TOTAL_SIZE_BITS]; + + memset(impl->tx_text, 0, LDPC_TOTAL_SIZE_BITS); + memcpy(&tmpbits[0], &ibits[0], LDPC_TOTAL_SIZE_BITS / 2); + memcpy(&tmpbits[LDPC_TOTAL_SIZE_BITS / 2], &pbits[0], LDPC_TOTAL_SIZE_BITS / 2); + memcpy(LastLDPCAsBits, tmpbits, LDPC_TOTAL_SIZE_BITS); + memcpy(impl->tx_text, tmpbits, LDPC_TOTAL_SIZE_BITS); + + // Interleave the bits together to enhance fading performance. + gp_interleave_bits(&impl->tx_text[0], tmpbits, LDPC_TOTAL_SIZE_BITS / 2); + //memcpy(impl->tx_text, tmpbits, LDPC_TOTAL_SIZE_BITS); + + // Generate floats based on the bits. + char debugString[256]; + for (int index = 0; index < LDPC_TOTAL_SIZE_BITS / 2; index++) + { + char *ptr = &impl->tx_text[2 * index]; + if (*ptr == 0 && *(ptr + 1) == 0) + { + syms[2 * index] = 1; + syms[2 * index + 1] = 0; + } + else if (*ptr == 0 && *(ptr + 1) == 1) + { + syms[2 * index] = 0; + syms[2 * index + 1] = 1; + } + else if (*ptr == 1 && *(ptr + 1) == 0) + { + syms[2 * index] = 0; + syms[2 * index + 1] = -1; + } + else if (*ptr == 1 && *(ptr + 1) == 1) + { + syms[2 * index] = -1; + syms[2 * index + 1] = 0; + } + debugString[2 * index] = impl->tx_text[2 * index] ? '1' : '0'; + debugString[2 * index + 1] = impl->tx_text[2 * index + 1] ? '1' : '0'; + } + + if (impl->enableStats) + { + // Copy floats into memory so we can compare them later (for BER calc). + memcpy(LastEncodedLDPC, syms, LDPC_TOTAL_SIZE_BITS * sizeof(float)); + } + + debugString[LDPC_TOTAL_SIZE_BITS] = 0; + log_debug("generated bits: %s", debugString); + + if (symSize > LDPC_TOTAL_SIZE_BITS) + { + // Stuff the remaining space in the EOO with a known sequence + // as anything else (i.e. zeros) will cause problems with decode. + for (int index = LDPC_TOTAL_SIZE_BITS; index < symSize; index++) + { + // Default everything to 0 (represented by 1 + 0j) + syms[index] = index % 2 ? 0 : 1; + } + } +} + +void rade_text_set_rx_callback(rade_text_t ptr, on_text_rx_t text_rx_fn, void *state) +{ + rade_text_impl_t *impl = (rade_text_impl_t *)ptr; + assert(impl != NULL); + + impl->text_rx_callback = text_rx_fn; + impl->callback_state = state; +} + +void rade_text_enable_stats_output(rade_text_t ptr, int enable) +{ + rade_text_impl_t *impl = (rade_text_impl_t *)ptr; + assert(impl != NULL); + + impl->enableStats = enable; +} diff --git a/src/pipeline/rade_text.h b/src/pipeline/rade_text.h new file mode 100644 index 000000000..2678639b7 --- /dev/null +++ b/src/pipeline/rade_text.h @@ -0,0 +1,59 @@ +//========================================================================== +// Name: rade_text.h +// +// Purpose: Handles reliable text (e.g. text with FEC). +// Created: December 7, 2024 +// Authors: Mooneer Salem +// +// License: +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program; if not, see . +// +//========================================================================== + +#ifndef RADE_TEXT_H +#define RADE_TEXT_H + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + + /* Hide internals of rade_text_t. */ + typedef void *rade_text_t; + + /* Function type for callback (when full reliable text has been received). */ + typedef void (*on_text_rx_t)(rade_text_t rt, const char *txt_ptr, int length, void *state); + + /* Allocate rade_text object. */ + rade_text_t rade_text_create(); + + /* Destroy rade_text object. */ + void rade_text_destroy(rade_text_t ptr); + + /* Generates float array for use with RADE EOO functions. */ + void rade_text_generate_tx_string(rade_text_t ptr, const char *str, int strlength, float *syms, int symSize); + + /* Set text RX callback. */ + void rade_text_set_rx_callback(rade_text_t ptr, on_text_rx_t text_rx_fn, void *state); + + /* Decode received symbols from RADE decoder. */ + void rade_text_rx(rade_text_t ptr, float *syms, int symSize); + + /* Whether to enable output of stats (i.e. BER). */ + void rade_text_enable_stats_output(rade_text_t ptr, int enable); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // RADE_TEXT_H \ No newline at end of file diff --git a/src/pipeline/test/RadeTextTest.cpp b/src/pipeline/test/RadeTextTest.cpp new file mode 100644 index 000000000..a5fae4697 --- /dev/null +++ b/src/pipeline/test/RadeTextTest.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include "../RADEReceiveStep.h" +#include "../RADETransmitStep.h" +#include "../rade_text.h" +#include "../../util/logging/ulog.h" + +bool testPassed = false; +wxString utRxFeatureFile; +wxString utTxFeatureFile; + +std::default_random_engine generator; +std::uniform_real_distribution distribution(-0.789, 0.789); + +void OnRadeTextRx(rade_text_t rt, const char* txt_ptr, int length, void* state) +{ + log_info("Callsign received: %s", txt_ptr); + testPassed = !strncmp(txt_ptr, "K6AQ", length); +} + +void addNoise(short* ptr, int size) +{ + for (int i = 0; i < size; i++) + { + double noise = 16384 * distribution(generator); + ptr[i] = (noise + ptr[i]) / 0.707; + } +} + +int main() +{ + struct rade* rade; + LPCNetEncState *encState; + FARGANState fargan; + + // Initialize FARGAN + float zeros[320] = {0}; + float in_features[5*NB_TOTAL_FEATURES] = {0}; + fargan_init(&fargan); + fargan_cont(&fargan, zeros, in_features); + encState = lpcnet_encoder_create(); + assert(encState != nullptr); + + // Initialize RADE + char modelFile[1]; + modelFile[0] = 0; + rade_initialize(); + rade = rade_open(modelFile, RADE_USE_C_ENCODER | RADE_USE_C_DECODER); + assert(rade != nullptr); + + // Initialize RADE text + rade_text_t txt = rade_text_create(); + assert(txt != nullptr); + int nsyms = rade_n_eoo_bits(rade); + float txSyms[nsyms]; + rade_text_generate_tx_string(txt, "K6AQ", 4, txSyms, nsyms); + rade_text_set_rx_callback(txt, OnRadeTextRx, nullptr); + rade_tx_set_eoo_bits(rade, txSyms); + + // Initialize RADE steps + RADEReceiveStep* recvStep = new RADEReceiveStep(rade, &fargan, txt); + assert(recvStep != nullptr); + RADETransmitStep* txStep = new RADETransmitStep(rade, encState); + assert(txStep != nullptr); + + // "Transmit" ~1 second of audio (including EOO) and immediately receive it. + int nout = 0; + int noutRx = 0; + short* inputSamples = new short[16384]; + assert(inputSamples != nullptr); + memset(inputSamples, 0, sizeof(short) * 16384); + auto inputSamplesPtr = std::shared_ptr(inputSamples, std::default_delete()); + auto outputSamples = txStep->execute(inputSamplesPtr, 16384, &nout); + addNoise(outputSamples.get(), nout); + recvStep->execute(outputSamples, nout, &noutRx); + + txStep->restartVocoder(); + while (nout > 0) + { + outputSamples = txStep->execute(inputSamplesPtr, 0, &nout); + addNoise(outputSamples.get(), nout); + recvStep->execute(outputSamples, nout, &noutRx); + } + + // Send silence through to RX step to trigger EOO processing + recvStep->execute(inputSamplesPtr, 16384, &noutRx); + + rade_close(rade); + rade_finalize(); + return testPassed ? 0 : 1; +} diff --git a/test/freedv-ctest-reporting.conf.tmpl b/test/freedv-ctest-reporting.conf.tmpl new file mode 100644 index 000000000..89bcc383a --- /dev/null +++ b/test/freedv-ctest-reporting.conf.tmpl @@ -0,0 +1,189 @@ +FirstTimeUse=0 +ExperimentalFeatures=0 +[Audio] +soundCard1SampleRate=-1 +soundCard2SampleRate=-1 +soundCard1InDeviceName=@FREEDV_RADIO_TO_COMPUTER_DEVICE@ +soundCard1InSampleRate=48000 +soundCard1OutDeviceName=@FREEDV_COMPUTER_TO_RADIO_DEVICE@ +soundCard1OutSampleRate=48000 +soundCard2InDeviceName=@FREEDV_MICROPHONE_TO_COMPUTER_DEVICE@ +soundCard2InSampleRate=48000 +soundCard2OutDeviceName=@FREEDV_COMPUTER_TO_SPEAKER_DEVICE@ +soundCard2OutSampleRate=48000 +SquelchActive=1 +SquelchLevel=-4 +fifoSize_ms=440 +transmitLevel=0 +snrSlow=0 +mode=257 +TxRxDelayMilliseconds=0 +[Filter] +codec2LPCPostFilterGamma=50 +codec2LPCPostFilterBeta=20 +MicInBassFreqHz=100 +MicInBassGaindB=0 +MicInTrebleFreqHz=3000 +MicInTrebleGaindB=0 +MicInMidFreqHz=1500 +MicInMidGaindB=0 +MicInMidQ=100 +MicInVolInDB=0 +SpkOutBassFreqHz=100 +SpkOutBassGaindB=0 +SpkOutTrebleFreqHz=3000 +SpkOutTrebleGaindB=0 +SpkOutMidFreqHz=1500 +SpkOutMidGaindB=0 +SpkOutMidQ=100 +SpkOutVolInDB=0 +codec2LPCPostFilterEnable=1 +codec2LPCPostFilterBassBoost=1 +speexpp_enable=1 +700C_EQ=1 +[Filter/MicIn] +EQEnable=0 +BassFreqHz=100 +BassGaindB=0 +TrebleFreqHz=3000 +TrebleGaindB=0 +MidFreqHz=1500 +MidGaindB=0 +MidQ=1 +VolInDB=0 +[Filter/SpkOut] +EQEnable=0 +BassFreqHz=100 +BassGaindB=0 +TrebleFreqHz=3000 +TrebleGaindB=0 +MidFreqHz=1500 +MidGaindB=0 +MidQ=1 +VolInDB=0 +[Filter/codec2LPCPostFilter] +Gamma=50 +Beta=20 +[Hamlib] +UseForPTT=0 +EnableFreqModeChanges=1 +UseAnalogModes=0 +IcomCIVHex=0 +RigNameStr=ADAT www.adat.ch ADT-200A +PttType=0 +SerialRate=0 +SerialPort= +PttSerialPort= +RigName=0 +[Rig] +UseSerialPTT=0 +Port= +UseRTS=1 +RTSPolarity=1 +UseDTR=0 +DTRPolarity=0 +UseSerialPTTInput=0 +PttInPort= +CTSPolarity=0 +leftChannelVoxTone=0 +EnableSpacebarForPTT=1 +HalfDuplex=1 +MultipleRx=1 +SingleRxThread=1 +[PSKReporter] +Enable=0 +Callsign= +GridSquare= +FrequencyHzStr=0 +[Data] +CallSign= +[Reporting] +Enable=1 +Callsign=ZZ0ZZZ +GridSquare=ZZ12ZZ +FrequencyAsKHz=0 +FrequencyList=1.9970,3.6250,3.6430,3.6930,3.6970,3.8500,5.4035,5.3665,5.3685,7.1770,7.1970,14.2360,14.2400,18.1180,21.3130,24.9330,28.3300,28.7200,10489.6400 +ManualFrequencyReporting=1 +DirectionAsCardinal=0 +Frequency=14236000 +[Reporting/PSKReporter] +Enable=1 +[Reporting/FreeDV] +Enable=1 +Hostname=qso.freedv.org +CurrentBandFilter=0 +UseMetricDistances=1 +BandFilterTracksFrequency=0 +ForceReceiveOnly=0 +StatusText=FreeDV Automated Test System - https://github.com/drowe67/freedv-gui +RecentStatusTexts= +TxRowBackgroundColor=#fc4500 +TxRowForegroundColor=#000000 +RxRowBackgroundColor=#379baf +RxRowForegroundColor=#000000 +MsgRowBackgroundColor=#E58BE5 +MsgRowForegroundColor=#000000 +[Reporting/FreeDV/BandFilterTracking] +TracksFreqBand=1 +TracksExactFreq=0 +[CallsignList] +UseUTCTime=0 +[FreeDV2020] +Allowed=0 +[MainFrame] +left=26 +top=23 +width=800 +height=780 +rxNbookCtrl=0 +TabLayout= +[Windows] +[Windows/AudioConfig] +left=26 +top=23 +width=918 +height=739 +[Windows/FreeDVReporter] +left=20 +top=20 +width=-1 +height=-1 +visible=0 +currentSort=-1 +currentSortDirection=1 +reportingUserMsgColWidth=130 +[File] +playFileToMicInPath= +recFileFromRadioPath= +recFileFromRadioSecs=60 +recFileFromModulatorPath= +recFileFromModulatorSecs=60 +playFileFromRadioPath= +[VoiceKeyer] +WaveFilePath=/home/mooneer/Documents +WaveFile=voicekeyer.wav +RxPause=10 +Repeats=5 +[FreeDV700] +txClip=1 +txBPF=1 +[Noise] +noise_snr=2 +[Debug] +console=0 +verbose=0 +APIverbose=0 +[Waterfall] +Color=0 +[Stats] +ResetTime=10 +[Plot] +[Plot/Spectrum] +CurrentAveraging=0 +[Monitor] +VoiceKeyerAudio=0 +TransmitAudio=0 +VoiceKeyerAudioVol=0 +TransmitAudioVol=0 +[QuickRecord] +SavePath=/home/mooneer/Documents diff --git a/test/test_rade_reporting.sh b/test/test_rade_reporting.sh new file mode 100755 index 000000000..7f53723dc --- /dev/null +++ b/test/test_rade_reporting.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# Determine sox driver to use for recording/playback +OPERATING_SYSTEM=`uname` +SOX_DRIVER=alsa +FREEDV_BINARY=src/freedv +if [ "$OPERATING_SYSTEM" == "Darwin" ]; then + SOX_DRIVER=coreaudio + FREEDV_BINARY=src/FreeDV.app/Contents/MacOS/freedv +fi + +createVirtualAudioCable () { + CABLE_NAME=$1 + pactl load-module module-null-sink sink_name=$CABLE_NAME sink_properties=device.description=$CABLE_NAME +} + +FREEDV_RADIO_TO_COMPUTER_DEVICE="${FREEDV_RADIO_TO_COMPUTER_DEVICE:-FreeDV_Radio_To_Computer}" +FREEDV_COMPUTER_TO_SPEAKER_DEVICE="${FREEDV_COMPUTER_TO_SPEAKER_DEVICE:-FreeDV_Computer_To_Speaker}" +FREEDV_MICROPHONE_TO_COMPUTER_DEVICE="${FREEDV_MICROPHONE_TO_COMPUTER_DEVICE:-FreeDV_Microphone_To_Computer}" +FREEDV_COMPUTER_TO_RADIO_DEVICE="${FREEDV_COMPUTER_TO_RADIO_DEVICE:-FreeDV_Computer_To_Radio}" + +# Automated script to help find audio dropouts. +# NOTE: this must be run from "build_linux". Also assumes PulseAudio/pipewire. +if [ "$OPERATING_SYSTEM" == "Linux" ]; then + DRIVER_INDEX_FREEDV_RADIO_TO_COMPUTER=$(createVirtualAudioCable FreeDV_Radio_To_Computer) + DRIVER_INDEX_FREEDV_COMPUTER_TO_SPEAKER=$(createVirtualAudioCable FreeDV_Computer_To_Speaker) + DRIVER_INDEX_FREEDV_MICROPHONE_TO_COMPUTER=$(createVirtualAudioCable FreeDV_Microphone_To_Computer) + DRIVER_INDEX_FREEDV_COMPUTER_TO_RADIO=$(createVirtualAudioCable FreeDV_Computer_To_Radio) + DRIVER_INDEX_LOOPBACK=`pactl load-module module-loopback source="FreeDV_Computer_To_Radio.monitor" sink="FreeDV_Radio_To_Computer"` +fi + +# Determine correct record device to retrieve TX data +FREEDV_CONF_FILE=freedv-ctest-reporting.conf +if [ "$OPERATING_SYSTEM" == "Linux" ]; then + REC_DEVICE="$FREEDV_COMPUTER_TO_RADIO_DEVICE.monitor" +else + REC_DEVICE="$FREEDV_COMPUTER_TO_RADIO_DEVICE" +fi + +# Generate config file +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +if [ "$FREEDV_RADIO_TO_COMPUTER_DEVICE" == "FreeDV_Radio_To_Computer" ] && [ "$OPERATING_SYSTEM" == "Linux" ]; then + sed "s/@FREEDV_RADIO_TO_COMPUTER_DEVICE@/$FREEDV_RADIO_TO_COMPUTER_DEVICE.monitor/g" $SCRIPTPATH/$FREEDV_CONF_FILE.tmpl > $(pwd)/$FREEDV_CONF_FILE +else + sed "s/@FREEDV_RADIO_TO_COMPUTER_DEVICE@/$FREEDV_RADIO_TO_COMPUTER_DEVICE/g" $SCRIPTPATH/$FREEDV_CONF_FILE.tmpl > $(pwd)/$FREEDV_CONF_FILE +fi + +sed "s/@FREEDV_COMPUTER_TO_RADIO_DEVICE@/$FREEDV_COMPUTER_TO_RADIO_DEVICE/g" $(pwd)/$FREEDV_CONF_FILE > $(pwd)/$FREEDV_CONF_FILE.tmp +mv $(pwd)/$FREEDV_CONF_FILE.tmp $(pwd)/$FREEDV_CONF_FILE +sed "s/@FREEDV_COMPUTER_TO_SPEAKER_DEVICE@/$FREEDV_COMPUTER_TO_SPEAKER_DEVICE/g" $(pwd)/$FREEDV_CONF_FILE > $(pwd)/$FREEDV_CONF_FILE.tmp +mv $(pwd)/$FREEDV_CONF_FILE.tmp $(pwd)/$FREEDV_CONF_FILE + +if [ "$FREEDV_MICROPHONE_TO_COMPUTER_DEVICE" == "FreeDV_Microphone_To_Computer" ] && [ "$OPERATING_SYSTEM" == "Linux" ]; then + sed "s/@FREEDV_MICROPHONE_TO_COMPUTER_DEVICE@/$FREEDV_MICROPHONE_TO_COMPUTER_DEVICE.monitor/g" $(pwd)/$FREEDV_CONF_FILE > $(pwd)/$FREEDV_CONF_FILE.tmp +else + sed "s/@FREEDV_MICROPHONE_TO_COMPUTER_DEVICE@/$FREEDV_MICROPHONE_TO_COMPUTER_DEVICE/g" $(pwd)/$FREEDV_CONF_FILE > $(pwd)/$FREEDV_CONF_FILE.tmp +fi +mv $(pwd)/$FREEDV_CONF_FILE.tmp $(pwd)/$FREEDV_CONF_FILE + +# Start recording +if [ "$OPERATING_SYSTEM" == "Linux" ]; then + parecord --channels=1 --rate 8000 --file-format=wav --device "$REC_DEVICE" --latency 1 test.wav & +else + sox -t $SOX_DRIVER "$REC_DEVICE" -c 1 -r 8000 -t wav test.wav & +fi +RECORD_PID=$! + +# Start FreeDV in test mode to record TX +if [ "$2" == "mpp" ]; then + TX_ARGS="-txtime 1 -txattempts 6 " +else + TX_ARGS="-txtime 5 " +fi +$FREEDV_BINARY -f $(pwd)/$FREEDV_CONF_FILE -ut tx -utmode RADE $TX_ARGS 2>&1 | tee tmp.log + +FDV_PID=$! +#sleep 30 +#screencapture ../screenshot.png +#wpctl status +#pw-top -b -n 5 +#wait $FDV_PID + +# Stop recording, play back in RX mode +kill $RECORD_PID + +if [ "$1" != "" ]; then + FADING_DIR="$(pwd)/fading" + if [ ! -d "$FADING_DIR" ]; then + mkdir $FADING_DIR + (cd $1/../unittest && ./fading_files.sh $FADING_DIR) + fi + # Add noise to recording to test performance + if [ "$2" == "mpp" ]; then + sox $(pwd)/test.wav -t raw -r 8000 -c 1 -e signed-integer -b 16 - | $1/src/ch - - --No -24 --mpp --fading_dir $FADING_DIR | sox -t raw -r 8000 -c 1 -e signed-integer -b 16 - -t wav $(pwd)/testwithnoise.wav + elif [ "$2" == "awgn" ]; then + sox $(pwd)/test.wav -t raw -r 8000 -c 1 -e signed-integer -b 16 - | $1/src/ch - - --No -18 | sox -t raw -r 8000 -c 1 -e signed-integer -b 16 - -t wav $(pwd)/testwithnoise.wav + fi + mv $(pwd)/testwithnoise.wav $(pwd)/test.wav +fi + +$FREEDV_BINARY -f $(pwd)/$FREEDV_CONF_FILE -ut rx -utmode RADE -rxfile $(pwd)/test.wav + +# Clean up PulseAudio virtual devices +if [ "$OPERATING_SYSTEM" == "Linux" ]; then + pactl unload-module $DRIVER_INDEX_LOOPBACK + pactl unload-module $DRIVER_INDEX_FREEDV_RADIO_TO_COMPUTER + pactl unload-module $DRIVER_INDEX_FREEDV_COMPUTER_TO_SPEAKER + pactl unload-module $DRIVER_INDEX_FREEDV_COMPUTER_TO_RADIO + pactl unload-module $DRIVER_INDEX_FREEDV_MICROPHONE_TO_COMPUTER +fi