diff --git a/radae_demo_rx.sh b/radae_demo_rx.sh new file mode 100755 index 000000000..995d00b18 --- /dev/null +++ b/radae_demo_rx.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +RADAE_PATH=$1 +RADAE_VENV=$2 + +# The below does the following: +# * For each block of OTA audio from freedv-gui: +# * Convert the 8 kHz audio into IQ data via zero-padding. +# * Pass the IQ data into the RADAE decoder +# * Send the resulting 16 kHz audio back to freedv-gui for playback. +# +# Note: Current RADAE scripts seem to require being executed from +# the RADAE folder. +cd $RADAE_PATH +export PATH=$RADAE_VENV/bin:$PATH +python3 -u int16tof32.py --zeropad | \ + python3 -u radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata | \ + build/src/lpcnet_demo -fargan-synthesis - - diff --git a/radae_demo_tx.sh b/radae_demo_tx.sh new file mode 100755 index 000000000..4809155a5 --- /dev/null +++ b/radae_demo_tx.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +RADAE_PATH=$1 +RADAE_VENV=$2 + +# The below does the following: +# * For each block of microphone audio from freedv-gui: +# * Extract features from 16 kHz input audio. +# * Pass features into the RADAE encoder. +# * Send the resulting 8 kHz audio back to freedv-gui. +# +# Note: Current RADAE scripts seem to require being executed from +# the RADAE folder. +cd $RADAE_PATH +export PATH=$RADAE_VENV/bin:$PATH +build/src/lpcnet_demo -features - - | \ + python3 -u radae_tx.py model19_check3/checkpoints/checkpoint_epoch_100.pth --auxdata | \ + python3 -u f32toint16.py --real --scale 16383 diff --git a/radae_rx.bat b/radae_rx.bat new file mode 100755 index 000000000..ef8d33ca7 --- /dev/null +++ b/radae_rx.bat @@ -0,0 +1,16 @@ +@echo off + +set RADAE_PATH=%1 +set RADAE_VENV=%2 + +rem The below does the following: +rem * For each block of OTA audio from freedv-gui: +rem * Convert the 8 kHz audio into IQ data via zero-padding. +rem * Pass the IQ data into the RADAE decoder +rem * Send the resulting 16 kHz audio back to freedv-gui for playback. +rem +rem Note: Current RADAE scripts seem to require being executed from +rem the RADAE folder. +cd %RADAE_PATH% +set PATH=%RADAE_VENV%;%PATH% +%RADAE_VENV%\python.exe -u int16tof32.py --zeropad | %RADAE_VENV%\python.exe -u radae_rx.py model19_check3\checkpoints\checkpoint_epoch_100.pth -v 2 --auxdata | build\src\lpcnet_demo -fargan-synthesis - - diff --git a/radae_tx.bat b/radae_tx.bat new file mode 100755 index 000000000..e81c4bdfd --- /dev/null +++ b/radae_tx.bat @@ -0,0 +1,16 @@ +@echo off + +set RADAE_PATH=%1 +set RADAE_VENV=%2 + +rem The below does the following: +rem * For each block of microphone audio from freedv-gui: +rem * Extract features from 16 kHz input audio. +rem * Pass features into the RADAE encoder. +rem * Send the resulting 8 kHz audio back to freedv-gui. +rem +rem Note: Current RADAE scripts seem to require being executed from +rem the RADAE folder. +cd %RADAE_PATH% +set PATH=%RADAE_VENV%;%PATH% +build\src\lpcnet_demo -features - - | %RADAE_VENV%\python.exe -u radae_tx.py model19_check3\checkpoints\checkpoint_epoch_100.pth --auxdata | %RADAE_VENV%\python.exe -u f32toint16.py --real --scale 16383 diff --git a/src/audio/IAudioEngine.cpp b/src/audio/IAudioEngine.cpp index 1723a6f2e..b94661493 100644 --- a/src/audio/IAudioEngine.cpp +++ b/src/audio/IAudioEngine.cpp @@ -24,8 +24,6 @@ int IAudioEngine::StandardSampleRates[] = { - 8000, 9600, - 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, diff --git a/src/config/FreeDVConfiguration.cpp b/src/config/FreeDVConfiguration.cpp index c7420646c..d467951e9 100644 --- a/src/config/FreeDVConfiguration.cpp +++ b/src/config/FreeDVConfiguration.cpp @@ -114,6 +114,9 @@ FreeDVConfiguration::FreeDVConfiguration() , txRxDelayMilliseconds("/Audio/TxRxDelayMilliseconds", 0) , reportingUserMsgColWidth("/Windows/FreeDVReporter/reportingUserMsgColWidth", 130) + + , externalVocoderRxCommand("/ExternalVocoder/RxCommand", _("")) + , externalVocoderTxCommand("/ExternalVocoder/TxCommand", _("")) { // empty } @@ -240,6 +243,9 @@ void FreeDVConfiguration::load(wxConfigBase* config) load_(config, txRxDelayMilliseconds); + load_(config, externalVocoderRxCommand); + load_(config, externalVocoderTxCommand); + load_(config, reportingUserMsgColWidth); } @@ -328,6 +334,9 @@ void FreeDVConfiguration::save(wxConfigBase* config) save_(config, txRxDelayMilliseconds); + save_(config, externalVocoderRxCommand); + save_(config, externalVocoderTxCommand); + save_(config, reportingUserMsgColWidth); config->Flush(); diff --git a/src/config/FreeDVConfiguration.h b/src/config/FreeDVConfiguration.h index e9fb48d84..489e1c3e5 100644 --- a/src/config/FreeDVConfiguration.h +++ b/src/config/FreeDVConfiguration.h @@ -126,6 +126,9 @@ class FreeDVConfiguration : public WxWidgetsConfigStore ConfigurationDataElement reportingUserMsgColWidth; + ConfigurationDataElement externalVocoderRxCommand; + ConfigurationDataElement externalVocoderTxCommand; + virtual void load(wxConfigBase* config) override; virtual void save(wxConfigBase* config) override; }; diff --git a/src/freedv_interface.cpp b/src/freedv_interface.cpp index 716b2dce6..92147ade6 100644 --- a/src/freedv_interface.cpp +++ b/src/freedv_interface.cpp @@ -25,6 +25,7 @@ #include "pipeline/ParallelStep.h" #include "pipeline/FreeDVTransmitStep.h" #include "pipeline/FreeDVReceiveStep.h" +#include "pipeline/ExternVocoderStep.h" using namespace std::placeholders; @@ -63,7 +64,8 @@ FreeDVInterface::FreeDVInterface() : modemStatsIndex_(0), currentTxMode_(nullptr), currentRxMode_(nullptr), - lastSyncRxMode_(nullptr) + lastSyncRxMode_(nullptr), + txVocoderStep_(nullptr) { // empty } @@ -114,6 +116,11 @@ float FreeDVInterface::GetMinimumSNR_(int mode) } } +void FreeDVInterface::restartTxVocoder() +{ + txVocoderStep_->restartVocoder(); +} + void FreeDVInterface::start(int txMode, int fifoSizeMs, bool singleRxThread, bool usingReliableText) { singleRxThread_ = singleRxThread; @@ -127,6 +134,16 @@ void FreeDVInterface::start(int txMode, int fifoSizeMs, bool singleRxThread, boo float minimumSnr = 999.0f; for (auto& mode : enabledModes_) { + if (mode == -1) + { + // Special-case to allow use of external vocoder + // NOTE: multi-RX is NOT supported. + rxMode_ = -1; + txMode_ = -1; + modemStatsIndex_ = 0; + continue; + } + struct freedv* dv = freedv_open(mode); assert(dv != nullptr); @@ -221,6 +238,7 @@ void FreeDVInterface::stop() modemStatsList_ = nullptr; currentTxMode_ = nullptr; currentRxMode_ = nullptr; + txVocoderStep_ = nullptr; modemStatsIndex_ = 0; txMode_ = 0; rxMode_ = 0; @@ -274,16 +292,20 @@ void FreeDVInterface::setTestFrames(bool testFrames, bool combine) int FreeDVInterface::getTotalBits() { + if (currentRxMode_ == nullptr) return 1; return freedv_get_total_bits(currentRxMode_); } int FreeDVInterface::getTotalBitErrors() { + if (currentRxMode_ == nullptr) return 0; return freedv_get_total_bit_errors(currentRxMode_); } float FreeDVInterface::getVariance() const { + if (currentRxMode_ == nullptr) return 0.0; + struct CODEC2 *c2 = freedv_get_codec2(currentRxMode_); if (c2 != NULL) return codec2_get_var(c2); @@ -293,6 +315,8 @@ float FreeDVInterface::getVariance() const int FreeDVInterface::getErrorPattern(short** outputPattern) { + if (currentRxMode_ == nullptr) return 0; + int size = freedv_get_sz_error_pattern(currentRxMode_); if (size > 0) { @@ -371,6 +395,7 @@ void FreeDVInterface::setSync(int val) int FreeDVInterface::getSync() const { + if (currentRxMode_ == nullptr) return 0; return freedv_get_sync(currentRxMode_); } @@ -421,24 +446,32 @@ void FreeDVInterface::setTextCallbackFn(void (*rxFunc)(void *, char), char (*txF int FreeDVInterface::getTxModemSampleRate() const { + if (txMode_ == -1) return 8000; + assert(currentTxMode_ != nullptr); return freedv_get_modem_sample_rate(currentTxMode_); } int FreeDVInterface::getTxSpeechSampleRate() const { + if (txMode_ == -1) return 16000; + assert(currentTxMode_ != nullptr); return freedv_get_speech_sample_rate(currentTxMode_); } int FreeDVInterface::getTxNumSpeechSamples() const { + if (txMode_ == -1) return 1920; + assert(currentTxMode_ != nullptr); return freedv_get_n_speech_samples(currentTxMode_); } int FreeDVInterface::getTxNNomModemSamples() const { + if (txMode_ == -1) return 960; + assert(currentTxMode_ != nullptr); return freedv_get_n_nom_modem_samples(currentTxMode_); } @@ -465,6 +498,8 @@ void FreeDVInterface::setTextVaricodeNum(int num) int FreeDVInterface::getRxModemSampleRate() const { + if (rxMode_ == -1) return 8000; + int result = 0; for (auto& dv : dvObjects_) { @@ -476,6 +511,8 @@ int FreeDVInterface::getRxModemSampleRate() const int FreeDVInterface::getRxNumModemSamples() const { + if (rxMode_ == -1) return 512; + int result = 0; for (auto& dv : dvObjects_) { @@ -487,6 +524,8 @@ int FreeDVInterface::getRxNumModemSamples() const int FreeDVInterface::getRxNumSpeechSamples() const { + if (rxMode_ == -1) return 1024; + int result = 0; for (auto& dv : dvObjects_) { @@ -498,6 +537,8 @@ int FreeDVInterface::getRxNumSpeechSamples() const int FreeDVInterface::getRxSpeechSampleRate() const { + if (rxMode_ == -1) return 8000; + int result = 0; for (auto& dv : dvObjects_) { @@ -559,7 +600,14 @@ void FreeDVInterface::setReliableText(const char* callsign) IPipelineStep* FreeDVInterface::createTransmitPipeline(int inputSampleRate, int outputSampleRate, std::function getFreqOffsetFn) { std::vector parallelSteps; - + + if (txMode_ == -1) + { + // special handling for external vocoder + txVocoderStep_ = new ExternVocoderStep(externVocoderTxCommand_, 16000, 8000, 960); + parallelSteps.push_back(txVocoderStep_); + } + for (auto& dv : dvObjects_) { parallelSteps.push_back(new FreeDVTransmitStep(dv, getFreqOffsetFn)); @@ -567,6 +615,9 @@ IPipelineStep* FreeDVInterface::createTransmitPipeline(int inputSampleRate, int std::function modeFn = [&](ParallelStep*) { + // Special handling for external vocoder. + if (txMode_ == -1) return 0; + int index = 0; for (auto& dv : dvObjects_) { @@ -608,16 +659,26 @@ IPipelineStep* FreeDVInterface::createReceivePipeline( state->getFreqOffsetFn = getFreqOffsetFn; state->getSigPwrAvgFn = getSigPwrAvgFn; - for (auto& dv : dvObjects_) + if (txMode_ == -1) { - auto recvStep = new FreeDVReceiveStep(dv); - assert(recvStep != nullptr); - - parallelSteps.push_back(recvStep); + // special handling for external vocoder + parallelSteps.push_back(new ExternVocoderStep(externVocoderRxCommand_, 8000, 16000)); + + state->preProcessFn = [&](ParallelStep*) { return -1; }; + state->postProcessFn = [&](ParallelStep*) { return 0; }; } - - state->preProcessFn = std::bind(&FreeDVInterface::preProcessRxFn_, this, _1); - state->postProcessFn = std::bind(&FreeDVInterface::postProcessRxFn_, this, _1); + else + { + for (auto& dv : dvObjects_) + { + auto recvStep = new FreeDVReceiveStep(dv); + assert(recvStep != nullptr); + + parallelSteps.push_back(recvStep); + } + state->preProcessFn = std::bind(&FreeDVInterface::preProcessRxFn_, this, _1); + state->postProcessFn = std::bind(&FreeDVInterface::postProcessRxFn_, this, _1); + } auto parallelStep = new ParallelStep( inputSampleRate, @@ -671,6 +732,8 @@ int FreeDVInterface::postProcessRxFn_(ParallelStep* stepObj) int maxSyncFound = -25; struct freedv* dvWithSync = nullptr; + if (dvObjects_.size() == 0) goto skipSyncCheck; + for (auto& dv : dvObjects_) { if (dv == currentRxMode_ && freedv_get_sync(currentRxMode_)) @@ -726,21 +789,29 @@ int FreeDVInterface::postProcessRxFn_(ParallelStep* stepObj) skipSyncCheck: struct MODEM_STATS* stats = getCurrentRxModemStats(); - - // grab extended stats so we can plot spectrum, scatter diagram etc - freedv_get_modem_extended_stats(dvWithSync, stats); - // Update sync as it may have gone stale during decode - *state->getRxStateFn() = stats->sync != 0; - - if (*state->getRxStateFn()) + if (dvWithSync != nullptr) { - rxMode_ = enabledModes_[indexWithSync]; - currentRxMode_ = dvWithSync; - lastSyncRxMode_ = currentRxMode_; - } + // grab extended stats so we can plot spectrum, scatter diagram etc + freedv_get_modem_extended_stats(dvWithSync, stats); - *state->getSigPwrAvgFn() = ((FreeDVReceiveStep*)stepObj->getParallelSteps()[indexWithSync].get())->getSigPwrAvg(); + // Update sync as it may have gone stale during decode + *state->getRxStateFn() = stats->sync != 0; + if (*state->getRxStateFn()) + { + rxMode_ = enabledModes_[indexWithSync]; + currentRxMode_ = dvWithSync; + lastSyncRxMode_ = currentRxMode_; + } + *state->getSigPwrAvgFn() = ((FreeDVReceiveStep*)stepObj->getParallelSteps()[indexWithSync].get())->getSigPwrAvg(); + } + else + { + // assume external mode is in sync + *state->getRxStateFn() = 1; + *state->getSigPwrAvgFn() = 0; + } + return indexWithSync; }; diff --git a/src/freedv_interface.h b/src/freedv_interface.h index 86bf2e860..771a8e36b 100644 --- a/src/freedv_interface.h +++ b/src/freedv_interface.h @@ -43,6 +43,7 @@ class IPipelineStep; class ParallelStep; +class ExternVocoderStep; class FreeDVInterface { @@ -115,7 +116,12 @@ class FreeDVInterface std::function getFreqOffsetFn, std::function getSigPwrAvgFn ); - + + void setExternVocoderRxCommand(std::string command) { externVocoderRxCommand_ = command; } + void setExternVocoderTxCommand(std::string command) { externVocoderTxCommand_ = command; } + + void restartTxVocoder(); + private: struct ReceivePipelineState { @@ -168,7 +174,12 @@ class FreeDVInterface std::deque reliableText_; std::string receivedReliableText_; std::mutex reliableTextMutex_; - + + std::string externVocoderRxCommand_; + std::string externVocoderTxCommand_; + + ExternVocoderStep* txVocoderStep_; + int preProcessRxFn_(ParallelStep* ps); int postProcessRxFn_(ParallelStep* ps); }; diff --git a/src/gui/dialogs/dlg_options.cpp b/src/gui/dialogs/dlg_options.cpp index 563c2c583..7f8755dac 100644 --- a/src/gui/dialogs/dlg_options.cpp +++ b/src/gui/dialogs/dlg_options.cpp @@ -431,6 +431,33 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c sizerModem->Add(sbSizer_duplex,0, wxALL | wxEXPAND, 5); + //------------------------------ + // External vocoder + //------------------------------ + + wxStaticBox *sb_vocoder = new wxStaticBox(m_modemTab, wxID_ANY, _("External Vocoder")); + wxStaticBoxSizer* sbSizer_vocoder = new wxStaticBoxSizer(sb_vocoder, wxVERTICAL); + + wxSizer* vocoderRxSizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText *staticTextVocoderRXCmd = new wxStaticText(m_modemTab, wxID_ANY, _("RX command: "), wxDefaultPosition, wxDefaultSize, 0); + vocoderRxSizer->Add(staticTextVocoderRXCmd, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + m_txtCtrlExternalVocoderRxCommand = + new wxTextCtrl(m_modemTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(300, -1), 0); + vocoderRxSizer->Add(m_txtCtrlExternalVocoderRxCommand, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + sbSizer_vocoder->Add(vocoderRxSizer, 0, wxALL | wxEXPAND, 5); + + wxSizer* vocoderTxSizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText *staticTextVocoderTXCmd = new wxStaticText(m_modemTab, wxID_ANY, _("TX command: "), wxDefaultPosition, wxDefaultSize, 0); + vocoderTxSizer->Add(staticTextVocoderTXCmd, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + m_txtCtrlExternalVocoderTxCommand = + new wxTextCtrl(m_modemTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(300, -1), 0); + vocoderTxSizer->Add(m_txtCtrlExternalVocoderTxCommand, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + sbSizer_vocoder->Add(vocoderTxSizer, 0, wxALL | wxEXPAND, 5); + + sizerModem->Add(sbSizer_vocoder,0, wxALL | wxEXPAND, 5); + //------------------------------ // Multiple RX selection //------------------------------ @@ -842,6 +869,9 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) m_ckHalfDuplex->SetValue(wxGetApp().appConfiguration.halfDuplexMode); + m_txtCtrlExternalVocoderTxCommand->SetValue(wxGetApp().appConfiguration.externalVocoderTxCommand); + m_txtCtrlExternalVocoderRxCommand->SetValue(wxGetApp().appConfiguration.externalVocoderRxCommand); + m_ckboxMultipleRx->SetValue(wxGetApp().appConfiguration.multipleReceiveEnabled); m_ckboxSingleRxThread->SetValue(wxGetApp().appConfiguration.multipleReceiveOnSingleThread); @@ -1041,6 +1071,9 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) wxGetApp().appConfiguration.freedv700TxBPF = m_ckboxFreeDV700txBPF->GetValue(); wxGetApp().m_FreeDV700Combine = m_ckboxFreeDV700Combine->GetValue(); + wxGetApp().appConfiguration.externalVocoderTxCommand = m_txtCtrlExternalVocoderTxCommand->GetValue(); + wxGetApp().appConfiguration.externalVocoderRxCommand = m_txtCtrlExternalVocoderRxCommand->GetValue(); + #ifdef __WXMSW__ wxGetApp().appConfiguration.debugConsoleEnabled = m_ckboxDebugConsole->GetValue(); #endif diff --git a/src/gui/dialogs/dlg_options.h b/src/gui/dialogs/dlg_options.h index deebc5a59..21253371e 100644 --- a/src/gui/dialogs/dlg_options.h +++ b/src/gui/dialogs/dlg_options.h @@ -118,7 +118,11 @@ class OptionsDlg : public wxDialog /* Quick Record */ wxButton *m_buttonChooseQuickRecordPath; wxTextCtrl *m_txtCtrlQuickRecordPath; - + + // external vocoder options + wxTextCtrl *m_txtCtrlExternalVocoderRxCommand; + wxTextCtrl *m_txtCtrlExternalVocoderTxCommand; + /* test frames, other simulated channel impairments */ wxCheckBox *m_ckboxTestFrame; diff --git a/src/main.cpp b/src/main.cpp index 76ce106ab..874775e58 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -516,6 +516,8 @@ void MainFrame::loadConfiguration_() m_rb700e->SetValue(1); if (mode == 6) m_rb800xa->SetValue(1); + if (mode == 11) + m_rbExternalVocoder->SetValue(1); // mode 7 was the former 2400B mode, now removed. if ((mode == 9) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported) m_rb2020->SetValue(1); @@ -1033,6 +1035,8 @@ MainFrame::~MainFrame() mode = 6; if (m_rb2020->GetValue()) mode = 9; + if (m_rbExternalVocoder->GetValue()) + mode = 11; #if defined(FREEDV_MODE_2020B) if (m_rb2020b->GetValue()) mode = 10; @@ -1803,6 +1807,7 @@ void MainFrame::OnChangeTxMode( wxCommandEvent& event ) m_hiddenMode1, m_hiddenMode2, + m_rbExternalVocoder, m_rb700c, m_rb700d, m_rb700e, @@ -1840,6 +1845,10 @@ void MainFrame::OnChangeTxMode( wxCommandEvent& event ) { g_mode = FREEDV_MODE_700C; } + else if (eventObject == m_rbExternalVocoder || (eventObject == nullptr && m_rbExternalVocoder->GetValue())) + { + g_mode = -1; // special number to trigger use of external vocoder instead + } else if (eventObject == m_rb700d || (eventObject == nullptr && m_rb700d->GetValue())) { g_mode = FREEDV_MODE_700D; @@ -1939,9 +1948,10 @@ void MainFrame::performFreeDVOn_() wxCommandEvent tmpEvent; OnChangeTxMode(tmpEvent); - if (!wxGetApp().appConfiguration.multipleReceiveEnabled) + if (!wxGetApp().appConfiguration.multipleReceiveEnabled || m_rbExternalVocoder->GetValue()) { m_rb1600->Disable(); + m_rbExternalVocoder->Disable(); m_rb700c->Disable(); m_rb700d->Disable(); m_rb700e->Disable(); @@ -1976,6 +1986,7 @@ void MainFrame::performFreeDVOn_() } // If we're receive-only, it doesn't make sense to be able to change TX mode. + m_rbExternalVocoder->Disable(); if (g_nSoundCards <= 1) { m_rb1600->Disable(); @@ -1995,6 +2006,10 @@ void MainFrame::performFreeDVOn_() g_sfTxFs = FS; wxGetApp().m_prevMode = g_mode; + + freedvInterface.setExternVocoderRxCommand((const char*)wxGetApp().appConfiguration.externalVocoderRxCommand.get().ToUTF8()); + freedvInterface.setExternVocoderTxCommand((const char*)wxGetApp().appConfiguration.externalVocoderTxCommand.get().ToUTF8()); + freedvInterface.start(g_mode, wxGetApp().appConfiguration.fifoSizeMs, !wxGetApp().appConfiguration.multipleReceiveEnabled || wxGetApp().appConfiguration.multipleReceiveOnSingleThread, wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled); // Codec 2 VQ Equaliser @@ -2272,6 +2287,7 @@ void MainFrame::performFreeDVOff_() m_togBtnVoiceKeyer->Disable(); m_rb1600->Enable(); + m_rbExternalVocoder->Enable(); m_rb700c->Enable(); m_rb700d->Enable(); m_rb700e->Enable(); @@ -3160,6 +3176,44 @@ bool MainFrame::validateSoundCardSetup() "Your %s device cannot be found and may have been removed from your system. Please reattach this device, close this message box and retry. If this fails, go to Tools->Audio Config... to check your settings.", failedDeviceName), wxT("Sound Device Not Found"), wxOK, this); } + else + { + const int MIN_SAMPLE_RATE = 16000; + int failedSampleRate = 0; + + // Validate sample rates + if (wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName != "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate < MIN_SAMPLE_RATE) + { + failedDeviceName = wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName.get(); + failedSampleRate = wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate; + canRun = false; + } + else if (wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName != "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate < MIN_SAMPLE_RATE) + { + failedDeviceName = wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName.get(); + failedSampleRate = wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate; + canRun = false; + } + else if (wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName != "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate < MIN_SAMPLE_RATE) + { + failedDeviceName = wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName.get(); + failedSampleRate = wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate; + canRun = false; + } + else if (wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName != "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate < MIN_SAMPLE_RATE) + { + failedDeviceName = wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName.get(); + failedSampleRate = wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate; + canRun = false; + } + + if (!canRun) + { + wxMessageBox(wxString::Format( + "Your %s device is set to use a sample rate of %d, which is less than the minimum of %d. Please go to Tools->Audio Config... to check your settings.", + failedDeviceName, failedSampleRate, MIN_SAMPLE_RATE), wxT("Sample Rate Too Low"), wxOK, this); + } + } engine->stop(); engine->setOnEngineError(nullptr, nullptr); diff --git a/src/pipeline/AudioPipeline.cpp b/src/pipeline/AudioPipeline.cpp index 5709515b2..842ff9fc6 100644 --- a/src/pipeline/AudioPipeline.cpp +++ b/src/pipeline/AudioPipeline.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include "AudioPipeline.h" AudioPipeline::AudioPipeline(int inputSampleRate, int outputSampleRate) @@ -55,6 +56,12 @@ std::shared_ptr AudioPipeline::execute(std::shared_ptr inputSample { if (resamplers_[index]) { + if (resamplers_[index]->getOutputSampleRate() != pipelineSteps_[index]->getInputSampleRate()) + { + resamplers_[index] = nullptr; + reloadResampler_(index); + } + tempResult = resamplers_[index]->execute(tempInput, tempInputSamples, &tempOutputSamples); tempInput = tempResult; tempInputSamples = tempOutputSamples; @@ -151,3 +158,28 @@ void AudioPipeline::reloadResultResampler_() } } } + +void AudioPipeline::dump(int indentLevel) +{ + IPipelineStep::dump(indentLevel); + + indentLevel += 4; + + for (size_t index = 0; index < pipelineSteps_.size(); index++) + { + std::cout << std::string(indentLevel, ' '); + + if (resamplers_[index]) + { + std::cout << "[resample from " << resamplers_[index]->getInputSampleRate() << " to " << resamplers_[index]->getOutputSampleRate() << "] "; + } + + pipelineSteps_[index]->dump(indentLevel); + } + + if (resultSampler_) + { + std::cout << std::string(indentLevel, ' '); + resultSampler_->dump(indentLevel); + } +} diff --git a/src/pipeline/AudioPipeline.h b/src/pipeline/AudioPipeline.h index e96aedc52..1b0a9d72e 100644 --- a/src/pipeline/AudioPipeline.h +++ b/src/pipeline/AudioPipeline.h @@ -33,11 +33,13 @@ class AudioPipeline : public IPipelineStep AudioPipeline(int inputSampleRate, int outputSampleRate); virtual ~AudioPipeline(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; void appendPipelineStep(std::shared_ptr pipelineStep); + + virtual void dump(int indentLevel = 0) override; private: int inputSampleRate_; @@ -51,4 +53,4 @@ class AudioPipeline : public IPipelineStep void reloadResultResampler_(); }; -#endif // AUDIO_PIPELINE__AUDIO_PIPELINE_H \ No newline at end of file +#endif // AUDIO_PIPELINE__AUDIO_PIPELINE_H diff --git a/src/pipeline/CMakeLists.txt b/src/pipeline/CMakeLists.txt index 6efc7494d..2f514b54e 100644 --- a/src/pipeline/CMakeLists.txt +++ b/src/pipeline/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(fdv_audio_pipeline STATIC EqualizerStep.cpp ExclusiveAccessStep.h ExclusiveAccessStep.cpp + ExternVocoderStep.cpp FreeDVReceiveStep.h FreeDVReceiveStep.cpp FreeDVTransmitStep.h @@ -66,4 +67,4 @@ DefineUnitTest(LevelAdjustTest) DefineUnitTest(ResampleTest) target_link_libraries(ResampleTest PRIVATE ${FREEDV_LINK_LIBS}) DefineUnitTest(TapTest) -endif(UNITTEST) \ No newline at end of file +endif(UNITTEST) diff --git a/src/pipeline/ComputeRfSpectrumStep.h b/src/pipeline/ComputeRfSpectrumStep.h index 153506ba0..3c2a301b5 100644 --- a/src/pipeline/ComputeRfSpectrumStep.h +++ b/src/pipeline/ComputeRfSpectrumStep.h @@ -39,9 +39,9 @@ class ComputeRfSpectrumStep : public IPipelineStep std::function getAvMagFn); virtual ~ComputeRfSpectrumStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: std::function modemStatsFn_; diff --git a/src/pipeline/EitherOrStep.cpp b/src/pipeline/EitherOrStep.cpp index 32309dc05..0d3db0cd6 100644 --- a/src/pipeline/EitherOrStep.cpp +++ b/src/pipeline/EitherOrStep.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include #include "EitherOrStep.h" @@ -59,4 +60,16 @@ std::shared_ptr EitherOrStep::execute(std::shared_ptr inputSamples { return falseStep_->execute(inputSamples, numInputSamples, numOutputSamples); } -} \ No newline at end of file +} + +void EitherOrStep::dump(int indentLevel) +{ + IPipelineStep::dump(indentLevel); + + indentLevel += 4; + + std::cout << std::string(indentLevel, ' ') << "[true] "; + trueStep_->dump(indentLevel); + std::cout << std::string(indentLevel, ' ') << "[false] "; + falseStep_->dump(indentLevel); +} diff --git a/src/pipeline/EitherOrStep.h b/src/pipeline/EitherOrStep.h index 49f6642d9..50fbbba61 100644 --- a/src/pipeline/EitherOrStep.h +++ b/src/pipeline/EitherOrStep.h @@ -33,9 +33,11 @@ class EitherOrStep : public IPipelineStep EitherOrStep(std::function conditionalFn, std::shared_ptr trueStep, std::shared_ptr falseStep); virtual ~EitherOrStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; + + virtual void dump(int indentLevel = 0) override; private: std::function conditionalFn_; @@ -43,4 +45,4 @@ class EitherOrStep : public IPipelineStep std::shared_ptr trueStep_; }; -#endif // AUDIO_PIPELINE__EITHER_OR_STEP_H \ No newline at end of file +#endif // AUDIO_PIPELINE__EITHER_OR_STEP_H diff --git a/src/pipeline/EqualizerStep.h b/src/pipeline/EqualizerStep.h index 0a813914d..f7f5a5548 100644 --- a/src/pipeline/EqualizerStep.h +++ b/src/pipeline/EqualizerStep.h @@ -32,10 +32,10 @@ class EqualizerStep : public IPipelineStep EqualizerStep(int sampleRate, bool* enableFilter, void** bassFilter, void** midFilter, void** trebleFilter, void** volFilter); virtual ~EqualizerStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int sampleRate_; diff --git a/src/pipeline/ExclusiveAccessStep.cpp b/src/pipeline/ExclusiveAccessStep.cpp index 7641d7a3f..86b5680f8 100644 --- a/src/pipeline/ExclusiveAccessStep.cpp +++ b/src/pipeline/ExclusiveAccessStep.cpp @@ -21,6 +21,7 @@ // //========================================================================= +#include #include "ExclusiveAccessStep.h" ExclusiveAccessStep::ExclusiveAccessStep(IPipelineStep* step, std::function lockFn, std::function unlockFn) @@ -53,4 +54,13 @@ std::shared_ptr ExclusiveAccessStep::execute(std::shared_ptr input unlockFn_(); return result; -} \ No newline at end of file +} + +void ExclusiveAccessStep::dump(int indentLevel) +{ + IPipelineStep::dump(indentLevel); + indentLevel += 4; + + std::cout << std::string(indentLevel, ' '); + step_->dump(indentLevel); +} diff --git a/src/pipeline/ExclusiveAccessStep.h b/src/pipeline/ExclusiveAccessStep.h index 459e86746..3f8b674a3 100644 --- a/src/pipeline/ExclusiveAccessStep.h +++ b/src/pipeline/ExclusiveAccessStep.h @@ -34,10 +34,12 @@ class ExclusiveAccessStep : public IPipelineStep ExclusiveAccessStep(IPipelineStep* step, std::function lockFn, std::function unlockFn); virtual ~ExclusiveAccessStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; + + virtual void dump(int indentLevel = 0) override; private: std::shared_ptr step_; @@ -46,4 +48,4 @@ class ExclusiveAccessStep : public IPipelineStep }; -#endif // AUDIO_PIPELINE__EXCLUSIVE_ACCESS_STEP_H \ No newline at end of file +#endif // AUDIO_PIPELINE__EXCLUSIVE_ACCESS_STEP_H diff --git a/src/pipeline/ExternVocoderStep.cpp b/src/pipeline/ExternVocoderStep.cpp new file mode 100644 index 000000000..b42a3e500 --- /dev/null +++ b/src/pipeline/ExternVocoderStep.cpp @@ -0,0 +1,684 @@ +//========================================================================= +// Name: ExternVocoderStep.h +// Purpose: Describes a demodulation step in the audio pipeline. +// +// Authors: Mooneer Salem +// License: +// +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License +// along with this program; if not, see . +// +//========================================================================= + +#include +#include +#include +#include +#include +#include "ExternVocoderStep.h" +#include "codec2_fifo.h" +#include "../defines.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#endif // _WIN32 + +ExternVocoderStep::ExternVocoderStep(std::string scriptPath, int workingSampleRate, int outputSampleRate, int minSamplesToReturn) + : sampleRate_(workingSampleRate) + , outputSampleRate_(outputSampleRate) + , minSamplesToReturn_(minSamplesToReturn) + +#ifdef _WIN32 + , recvProcessHandle_(nullptr) + , receiveStdoutHandle_(nullptr) + , receiveStdinHandle_(nullptr) + , receiveStderrHandle_(nullptr) +#else + , recvProcessId_(0) + , receiveStdoutFd_(-1) + , receiveStdinFd_(-1) +#endif // _WIN32 + + , inputSampleFifo_(nullptr) + , outputSampleFifo_(nullptr) + , isExiting_(false) + , scriptPath_(scriptPath) + , isRestarting_(false) +{ + // Create FIFOs so we don't lose any samples during run + inputSampleFifo_ = codec2_fifo_create(16384); + assert(inputSampleFifo_ != nullptr); + + outputSampleFifo_ = codec2_fifo_create(16384); + assert(outputSampleFifo_ != nullptr); + + // Create process thread + vocoderProcessHandlerThread_ = std::thread(std::bind(&ExternVocoderStep::threadEntry_, this)); +} + +ExternVocoderStep::~ExternVocoderStep() +{ + // Stop processing audio samples + isExiting_ = true; + vocoderProcessHandlerThread_.join(); + + if (inputSampleFifo_ != nullptr) + { + codec2_fifo_free(inputSampleFifo_); + } + + if (outputSampleFifo_ != nullptr) + { + codec2_fifo_free(outputSampleFifo_); + } +} + +int ExternVocoderStep::getInputSampleRate() const +{ + return sampleRate_; +} + +int ExternVocoderStep::getOutputSampleRate() const +{ + return outputSampleRate_; +} + +std::shared_ptr ExternVocoderStep::execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) +{ + *numOutputSamples = 0; + short* outputSamples = nullptr; + + // Write input samples to thread for processing + if (numInputSamples > 0) + { + codec2_fifo_write(inputSampleFifo_, inputSamples.get(), numInputSamples); + + std::chrono::high_resolution_clock highResClock; + auto beginTime = highResClock.now(); + auto audioTimeMs = (minSamplesToReturn_ * 1000) / outputSampleRate_; + + while (codec2_fifo_used(outputSampleFifo_) < minSamplesToReturn_) + { + auto diff = highResClock.now() - beginTime; + if (diff > std::chrono::milliseconds(audioTimeMs)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + // Read and return output samples from thread. + *numOutputSamples = codec2_fifo_used(outputSampleFifo_); + *numOutputSamples = std::min(*numOutputSamples, numInputSamples * getOutputSampleRate() / getInputSampleRate()); + if (*numOutputSamples > 0) + { + outputSamples = new short[*numOutputSamples]; + assert(outputSamples != nullptr); + + codec2_fifo_read(outputSampleFifo_, outputSamples, *numOutputSamples); + } + + //fprintf(stderr, "XXX numInput: %d, numOutput: %d\n", numInputSamples, *numOutputSamples); + + return std::shared_ptr(outputSamples, std::default_delete()); +} + +void ExternVocoderStep::restartVocoder() +{ + isRestarting_ = true; +} + +#ifdef _WIN32 +void ExternVocoderStep::openProcess_() +{ + // Ensure that handles are inherited by the child process. + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + HANDLE tmpStdoutWrHandle = NULL; + HANDLE tmpStderrWrHandle = NULL; + HANDLE tmpStdinRdHandle = NULL; + + // Create pipes for the child process's stdin/stdout/stderr. + CreateAsyncPipe_(&receiveStdoutHandle_, &tmpStdoutWrHandle); + CreateAsyncPipe_(&receiveStderrHandle_, &tmpStderrWrHandle); + CreateAsyncPipe_(&tmpStdinRdHandle, &receiveStdinHandle_, true); + + // Create process + PROCESS_INFORMATION piProcInfo; + STARTUPINFOA siStartInfo; + + // Set up members of the PROCESS_INFORMATION structure. + ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) ); + + // Set up members of the STARTUPINFO structure. + // This structure specifies the STDIN and STDOUT handles for redirection. + ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.hStdError = tmpStderrWrHandle; + siStartInfo.hStdOutput = tmpStdoutWrHandle; + siStartInfo.hStdInput = tmpStdinRdHandle; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + BOOL success = CreateProcessA(NULL, + (char*)scriptPath_.c_str(), // command line + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &piProcInfo); // receives PROCESS_INFORMATION + + CloseHandle(tmpStdinRdHandle); + CloseHandle(tmpStdoutWrHandle); + CloseHandle(tmpStderrWrHandle); + + if (!success) + { + fprintf(stderr, "WARNING: cannot run %s\n", scriptPath_.c_str()); + + CloseHandle(receiveStdinHandle_); + CloseHandle(receiveStdoutHandle_); + CloseHandle(receiveStderrHandle_); + receiveStdinHandle_ = NULL; + receiveStdoutHandle_ = NULL; + receiveStderrHandle_ = NULL; + + return; + } + + // Save process handle so we can terminate it later + recvProcessHandle_ = piProcInfo.hProcess; + recvProcessId_ = piProcInfo.dwProcessId; +} + +void ExternVocoderStep::closeProcess_() +{ + if (recvProcessHandle_ != NULL) + { + //KillProcessTree_(recvProcessId_); + + // Make sure process has actually terminated + TerminateProcess(recvProcessHandle_, 0); + WaitForSingleObject(recvProcessHandle_, 1000); + CloseHandle(recvProcessHandle_); + + // Close pipe handles + CloseHandle(receiveStdinHandle_); + CloseHandle(receiveStdoutHandle_); + CloseHandle(receiveStderrHandle_); + } +} + +void ExternVocoderStep::KillProcessTree_(DWORD myprocID) +{ + PROCESSENTRY32 pe; + + memset(&pe, 0, sizeof(PROCESSENTRY32)); + pe.dwSize = sizeof(PROCESSENTRY32); + + HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (::Process32First(hSnap, &pe)) + { + do // Recursion + { + if (pe.th32ParentProcessID == myprocID) + KillProcessTree_(pe.th32ProcessID); + } while (::Process32Next(hSnap, &pe)); + } + + + // kill the main process + HANDLE hProc = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, myprocID); + + if (hProc) + { + fprintf(stderr, "killing process ID %lu\n", myprocID); + ::TerminateProcess(hProc, 0); + ::CloseHandle(hProc); + } +} + +void ExternVocoderStep::threadEntry_() +{ + const int NUM_SAMPLES_TO_READ_WRITE = 512; + const int BYTES_TO_READ_WRITE = NUM_SAMPLES_TO_READ_WRITE * sizeof(short); + const int STDERR_BYTES_TO_READ = 4096; + + openProcess_(); + + // Kick off reading from stdout/stderr + char* stdoutBuffer = new char[BYTES_TO_READ_WRITE]; + assert(stdoutBuffer != nullptr); + char* stderrBuffer = new char[STDERR_BYTES_TO_READ]; + assert(stderrBuffer != nullptr); + FileReadBuffer* stdoutRead = new FileReadBuffer(); + assert(stdoutRead != nullptr); + FileReadBuffer* stderrRead = new FileReadBuffer(); + assert(stderrRead != nullptr); + + std::function onStdoutRead = [&](char* buf, size_t bytesAvailable) { + codec2_fifo_write(outputSampleFifo_, (short*)&buf[0], bytesAvailable / sizeof(short)); + }; + + std::function onStderrRead = [&](char* buf, size_t bytesAvailable) { + std::string tmp(buf, bytesAvailable); + size_t start_pos = 0; + while((start_pos = tmp.find("\n", start_pos)) != std::string::npos) { + tmp.replace(start_pos, 1, "\r\n"); + start_pos += 2; + } + fprintf(stderr, "%s", tmp.c_str()); + }; + + InitFileReadBuffer_(stdoutRead, receiveStdoutHandle_, stdoutBuffer, BYTES_TO_READ_WRITE, onStdoutRead); + ScheduleFileRead_(stdoutRead); + InitFileReadBuffer_(stderrRead, receiveStderrHandle_, stderrBuffer, STDERR_BYTES_TO_READ, onStderrRead); + ScheduleFileRead_(stderrRead); + + while (!isExiting_ && !stdoutRead->eof && !stderrRead->eof) + { + // Write data to STDIN if available + int used = codec2_fifo_used(inputSampleFifo_); + if (used > 0) + { + used = std::min(used, 2048); // 4K max buffer size + short val[used]; + codec2_fifo_read(inputSampleFifo_, val, used); + WriteFile(receiveStdinHandle_, val, used * sizeof(short), NULL, NULL); + } + else + { + // Wait 10ms if we didn't do anything in this round + Sleep(10); + } + } + + closeProcess_(); + + // Make sure eof is actually set + for (int count = 0; count < 5; count++) + { + if (stdoutRead->eof && stderrRead->eof) break; + Sleep(1000); + } + + delete stdoutRead; + delete[] stdoutBuffer; + delete stderrRead; + delete[] stderrBuffer; +} + +void ExternVocoderStep::CreateAsyncPipe_(HANDLE* outRead, HANDLE* outWrite, bool inv) +{ + const int PIPE_SIZE = (4096 + 24); // https://github.com/brettwooldridge/NuProcess/issues/117 + + // Generate unique name for the pipe. + UUID uuid; + UuidCreate(&uuid); + char* uuidAsString; + UuidToStringA(&uuid, (RPC_CSTR*)&uuidAsString); + char pipeName[1024]; + sprintf(pipeName, "\\\\.\\pipe\\freedv-%s", uuidAsString); + RpcStringFreeA((RPC_CSTR*)&uuidAsString); + + // Create a pipe. The "instances" parameter is set to 2 because we call this function twice below. + constexpr DWORD Instances = 2; + // Create the named pipe. This will return the handle we use for reading from the pipe. + HANDLE read = CreateNamedPipeA( + pipeName, + // Set FILE_FLAG_OVERLAPPED to enable async I/O for reading from the pipe. + // Note that we still need to set PIPE_WAIT. + (inv ? PIPE_ACCESS_OUTBOUND : PIPE_ACCESS_INBOUND) | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + Instances, + // in-bound buffer size + inv ? 0 : PIPE_SIZE, + // out-going buffer size + inv ? PIPE_SIZE : 0, + // default timeout for some functions we're not using + 0, + nullptr + ); + if (read == INVALID_HANDLE_VALUE) + { + fprintf(stderr, "Failed to create named pipe (error %lu)", GetLastError()); + return; + } + + // Now create a handle for the other end of the pipe. We are going to pass that handle to the + // process we are creating, so we need to specify that the handle can be inherited. + // Also note that we are NOT setting FILE_FLAG_OVERLAPPED. We could set it, but that's not relevant + // for our end of the pipe. (We do not expect async writes.) + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + HANDLE write = CreateFileA(pipeName, (inv ? GENERIC_READ : GENERIC_WRITE), 0, &saAttr, OPEN_EXISTING, 0, 0); + if (write == INVALID_HANDLE_VALUE) + { + fprintf(stderr, "Failed to open named pipe (error %lu)", GetLastError()); + CloseHandle(read); + return; + } + + if (inv) + { + *outRead = write; + *outWrite = read; + } + else + { + *outRead = read; + *outWrite = write; + } +} + +void ExternVocoderStep::ScheduleFileRead_(FileReadBuffer* readBuffer) +{ + // Prepare the threadpool for an I/O request on our handle. + StartThreadpoolIo(readBuffer->Io); + BOOL success = ReadFile( + readBuffer->FileHandle, + readBuffer->Buffer, + readBuffer->BufferSize, + nullptr, + &readBuffer->Overlapped + ); + if (!success ) { + DWORD error = GetLastError(); + if (error == ERROR_IO_PENDING) { + // Async operation is in progress. This is NOT a failure state. + return; + } + // Since we have started an I/O request above but nothing happened, we need to cancel it. + CancelThreadpoolIo(readBuffer->Io); + + if (error == ERROR_INVALID_USER_BUFFER || error == ERROR_NOT_ENOUGH_MEMORY) { + // Too many outstanding async I/O requests, try again after 10 ms. + // The timer length is given in 100ns increments, negative values indicate relative + // values. FILETIME is actually an unsigned value. Sigh. + constexpr int ToMicro = 10; + constexpr int ToMilli = 1000; + constexpr int64_t Delay = -(10 * ToMicro * ToMilli); + FILETIME timerLength{}; + timerLength.dwHighDateTime = (Delay >> 32) & 0xFFFFFFFF; + timerLength.dwLowDateTime = Delay & 0xFFFFFFFF; + SetThreadpoolTimer(readBuffer->Timer, &timerLength, 0, 0); + return; + } + CloseThreadpoolTimer(readBuffer->Timer); + CloseThreadpoolIo(readBuffer->Io); + if (error == ERROR_BROKEN_PIPE) + { + readBuffer->eof = true; + return; + } + if(error != ERROR_OPERATION_ABORTED) + { + fprintf(stderr, "ReadFile async failed, error code %lu", error); + } + } +} + +void CALLBACK ExternVocoderStep::FileReadComplete_( + PTP_CALLBACK_INSTANCE instance, + void* context, + void* overlapped, + ULONG ioResult, + ULONG_PTR numBytesRead, + PTP_IO io + ) +{ + FileReadBuffer* readBuffer = (FileReadBuffer*)context; + if (ioResult == ERROR_OPERATION_ABORTED) { + // This can happen when someone manually aborts the I/O request. + CloseThreadpoolTimer(readBuffer->Timer); + CloseThreadpoolIo(readBuffer->Io); + return; + } + const bool isEof = ioResult == ERROR_HANDLE_EOF || ioResult == ERROR_BROKEN_PIPE; + if (!(isEof || ioResult == NO_ERROR)) + { + fprintf(stderr, "Got error result %lu while handling I/O callback", ioResult); + readBuffer->eof = true; + return; + } + + readBuffer->onDataRead((char*)readBuffer->Buffer, numBytesRead); + + if (isEof) { + CloseThreadpoolTimer(readBuffer->Timer); + CloseThreadpoolIo(readBuffer->Io); + + readBuffer->eof = true; + } else { + // continue reading + ScheduleFileRead_(readBuffer); + } +} + +void ExternVocoderStep::InitFileReadBuffer_(FileReadBuffer* readBuffer, HANDLE handle, void* buffer, size_t bufferSize, std::function onDataRead) +{ + ZeroMemory(readBuffer, sizeof(*readBuffer)); + readBuffer->onDataRead = onDataRead; + readBuffer->Buffer = buffer; + readBuffer->BufferSize = bufferSize; + readBuffer->FileHandle = handle; + readBuffer->Io = CreateThreadpoolIo(handle, &FileReadComplete_, readBuffer, nullptr); + if (!(readBuffer->Io)) + { + fprintf(stderr, "CreateThreadpoolIo failed, error code %lu", GetLastError()); + return; + } + + // This local struct just exists so I can declare a function here that we can pass to the timer below. + struct Tmp { + static void CALLBACK RetryScheduleFileRead( + PTP_CALLBACK_INSTANCE instance, + void* context, + PTP_TIMER wait + ) { + ScheduleFileRead_((FileReadBuffer*)context); + } + }; + + readBuffer->Timer = CreateThreadpoolTimer(&Tmp::RetryScheduleFileRead, readBuffer, nullptr); + if (!(readBuffer->Timer)) + { + fprintf(stderr, "CreateThreadpoolTimer failed, error code %lu", GetLastError()); + } +} +#else +void ExternVocoderStep::openProcess_() +{ + // Create pipes for stdin/stdout + int stdinPipes[2]; + int stdoutPipes[2]; + int rv = pipe(stdinPipes); + assert(rv == 0); + rv = pipe(stdoutPipes); + assert(rv == 0); + + receiveStdoutFd_ = stdoutPipes[0]; + receiveStdinFd_ = stdinPipes[1]; + + // Make pipes non-blocking. + /*int flags = fcntl(receiveStdoutFd_, F_GETFL, 0); + fcntl(receiveStdoutFd_, F_SETFL, flags | O_NONBLOCK); + flags = fcntl(receiveStdinFd_, F_GETFL, 0); + fcntl(receiveStdinFd_, F_SETFL, flags | O_NONBLOCK);*/ + + // Start external process + recvProcessId_ = fork(); + if (recvProcessId_ == 0) + { + // Child process, redirect stdin/stdout and execute receiver + rv = dup2(stdinPipes[0], STDIN_FILENO); + assert(rv != -1); + close(stdinPipes[0]); + + rv = dup2(stdoutPipes[1], STDOUT_FILENO); + assert(rv != -1); + close(stdoutPipes[1]); + + close(stdoutPipes[0]); + close(stdinPipes[1]); + + // Tokenize and generate an argv for exec() + std::vector args; + std::stringstream ss(scriptPath_); + std::string tmp; + while(std::getline(ss, tmp, ' ')) + { + args.push_back(tmp); + } + + char** argv = new char*[args.size() + 1]; + assert(argv != nullptr); + int index = 0; + for (auto& arg : args) + { + fprintf(stderr, "arg %d: %s\n", index, arg.c_str()); + argv[index] = new char[arg.size() + 1]; + assert(argv[index] != nullptr); + + memset(argv[index], 0, arg.size() + 1); + strncpy(argv[index], arg.c_str(), arg.size()); + + index++; + } + + argv[index] = nullptr; + + // Should not normally return. + execv(argv[0], argv); + fprintf(stderr, "WARNING: could not run %s (errno %d)\n", scriptPath_.c_str(), errno); + exit(-1); + } + else + { + close(stdoutPipes[1]); + close(stdinPipes[0]); + } +} + +void ExternVocoderStep::closeProcess_() +{ + // Close pipes and kill process. Hopefully the process + // will die on its own but if it doesn't, force kill it. + close(receiveStdoutFd_); + close(receiveStdinFd_); + + kill(recvProcessId_, SIGTERM); + for (int count = 0; count < 5 && waitpid(recvProcessId_, NULL, WNOHANG) <= 0; count++) + { + sleep(1); + } + kill(recvProcessId_, SIGKILL); + waitpid(recvProcessId_, NULL, 0); +} + +void ExternVocoderStep::threadEntry_() +{ + const int NUM_SAMPLES_TO_READ_WRITE = 1; + + while (!isExiting_) + { + openProcess_(); + + while (!isExiting_) + { + fd_set processReadFds; + fd_set processWriteFds; + FD_ZERO(&processReadFds); + FD_ZERO(&processWriteFds); + + if (receiveStdinFd_ != -1) + { + FD_SET(receiveStdinFd_, &processWriteFds); + } + FD_SET(receiveStdoutFd_, &processReadFds); + + // 10ms timeout + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 10000; + + int rv = select(std::max(receiveStdinFd_, receiveStdoutFd_) + 1, &processReadFds, &processWriteFds, nullptr, &tv); + if (rv > 0) + { + if (receiveStdinFd_ != -1 && FD_ISSET(receiveStdinFd_, &processWriteFds) && codec2_fifo_used(inputSampleFifo_) >= NUM_SAMPLES_TO_READ_WRITE) + { + // Can write to process + short val[NUM_SAMPLES_TO_READ_WRITE]; + codec2_fifo_read(inputSampleFifo_, val, NUM_SAMPLES_TO_READ_WRITE); + if (write(receiveStdinFd_, val, NUM_SAMPLES_TO_READ_WRITE * sizeof(short)) == -1) + { + fprintf(stderr, "[ExternVocoderStep] write() failed (errno %d)\n", errno); + break; + } + } + + if (FD_ISSET(receiveStdoutFd_, &processReadFds)) + { + short output_buf[NUM_SAMPLES_TO_READ_WRITE]; + if ((rv = read(receiveStdoutFd_, output_buf, NUM_SAMPLES_TO_READ_WRITE * sizeof(short))) > 0) + { + codec2_fifo_write(outputSampleFifo_, output_buf, rv / sizeof(short)); + } + else if (rv == -1) + { + fprintf(stderr, "[ExternVocoderStep] read() failed (errno %d)\n", errno); + break; + } + else + { + break; + } + } + } + else if (rv == -1) + { + fprintf(stderr, "[ExternVocoderStep] select() failed (errno %d, filenos %d / %d)\n", errno, receiveStdinFd_, receiveStdoutFd_); + break; + } + +#if 0 + if (isRestarting_) + { + // Close stdin and wait for process to die. + fprintf(stderr, "[ExternVocoderStep] Restarting...\n"); + isRestarting_ = false; + close(receiveStdinFd_); + receiveStdinFd_ = -1; + } +#endif // 0 + } + + closeProcess_(); + + if (!isExiting_) + { + // Sleep for 1s before restarting + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } +} +#endif // _WIN32 diff --git a/src/pipeline/ExternVocoderStep.h b/src/pipeline/ExternVocoderStep.h new file mode 100644 index 000000000..e1a0961c3 --- /dev/null +++ b/src/pipeline/ExternVocoderStep.h @@ -0,0 +1,123 @@ +//========================================================================= +// Name: ExternVocoderStep.h +// Purpose: Describes a demodulation step in the audio pipeline. +// +// Authors: Mooneer Salem +// License: +// +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU 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 General Public License +// along with this program; if not, see . +// +//========================================================================= + +#ifndef AUDIO_PIPELINE__EXTERN_VOCODER_STEP_H +#define AUDIO_PIPELINE__EXTERN_VOCODER_STEP_H + +#ifdef _WIN32 +#include +#else +#include +#endif // _WIN32 + +#include +#include +#include "IPipelineStep.h" + +// Forward definition of structs from Codec2. +extern "C" +{ + struct FIFO; +} + +class ExternVocoderStep : public IPipelineStep +{ +public: + ExternVocoderStep(std::string scriptPath, int workingSampleRate, int outputSampleRate, int minSamplesToReturn = 0); + virtual ~ExternVocoderStep(); + + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; + + void restartVocoder(); + +private: + int sampleRate_; + int outputSampleRate_; + int minSamplesToReturn_; + +#ifdef _WIN32 + struct FileReadBuffer + { + OVERLAPPED Overlapped; + + // Some buffer to read into + void* Buffer; + size_t BufferSize; + + // The handle to the file or pipe. + HANDLE FileHandle; + PTP_IO Io; + PTP_TIMER Timer; + + // Callback when data is read + std::function onDataRead; + + // Indicator that there's nothing left to read + bool eof; + }; + + DWORD recvProcessId_; + HANDLE recvProcessHandle_; + HANDLE receiveStdoutHandle_; + HANDLE receiveStdinHandle_; + HANDLE receiveStderrHandle_; +#else + pid_t recvProcessId_; + int receiveStdoutFd_; + int receiveStdinFd_; +#endif // _WIN32 + + struct FIFO* inputSampleFifo_; + struct FIFO* outputSampleFifo_; + + std::thread vocoderProcessHandlerThread_; + bool isExiting_; + + std::string scriptPath_; + bool isRestarting_; + + void threadEntry_(); + void openProcess_(); + void closeProcess_(); + +#ifdef _WIN32 + static void KillProcessTree_(DWORD myprocID); + + // Adapted from https://blog.s-schoener.com/2024-06-16-stream-redirection-win32/. + static void CreateAsyncPipe_(HANDLE* outRead, HANDLE* outWrite, bool inv = false); + static void ScheduleFileRead_(FileReadBuffer* readBuffer); + static void CALLBACK FileReadComplete_( + PTP_CALLBACK_INSTANCE instance, + void* context, + void* overlapped, + ULONG ioResult, + ULONG_PTR numBytesRead, + PTP_IO io + ); + static void InitFileReadBuffer_(FileReadBuffer* readBuffer, HANDLE handle, void* buffer, size_t bufferSize, std::function onDataRead); +#endif // _WIN32 + +}; + +#endif // AUDIO_PIPELINE__EXTERN_VOCODER_STEP_H diff --git a/src/pipeline/FreeDVReceiveStep.h b/src/pipeline/FreeDVReceiveStep.h index 3e400dc06..ca94bf154 100644 --- a/src/pipeline/FreeDVReceiveStep.h +++ b/src/pipeline/FreeDVReceiveStep.h @@ -41,9 +41,9 @@ class FreeDVReceiveStep : public IPipelineStep FreeDVReceiveStep(struct freedv* dv); virtual ~FreeDVReceiveStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; void setSigPwrAvg(float newVal) { sigPwrAvg_ = newVal; } float getSigPwrAvg() const { return sigPwrAvg_; } diff --git a/src/pipeline/FreeDVTransmitStep.h b/src/pipeline/FreeDVTransmitStep.h index 37b70068e..a415dc4f6 100644 --- a/src/pipeline/FreeDVTransmitStep.h +++ b/src/pipeline/FreeDVTransmitStep.h @@ -41,9 +41,9 @@ class FreeDVTransmitStep : public IPipelineStep FreeDVTransmitStep(struct freedv* dv, std::function getFreqOffsetFn); virtual ~FreeDVTransmitStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: struct freedv* dv_; diff --git a/src/pipeline/IPipelineStep.cpp b/src/pipeline/IPipelineStep.cpp index 93a3eec0f..9e7d28f73 100644 --- a/src/pipeline/IPipelineStep.cpp +++ b/src/pipeline/IPipelineStep.cpp @@ -20,9 +20,15 @@ // //========================================================================= +#include #include "IPipelineStep.h" IPipelineStep::~IPipelineStep() { // empty -} \ No newline at end of file +} + +void IPipelineStep::dump(int indentLevel) +{ + std::cout << typeid(*this).name() << "[inputSR = " << getInputSampleRate() << ", outputSR = " << getOutputSampleRate() << "]" << std::endl; +} diff --git a/src/pipeline/IPipelineStep.h b/src/pipeline/IPipelineStep.h index a6e7c4f79..4645c06e9 100644 --- a/src/pipeline/IPipelineStep.h +++ b/src/pipeline/IPipelineStep.h @@ -43,7 +43,9 @@ class IPipelineStep // numOutputSamples: Location to store number of output samples. // Returns: Array of int16 values corresponding to result audio. virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) = 0; + + virtual void dump(int indentLevel = 0); }; -#endif // AUDIO_PIPELINE__I_PIPELINE_STEP_H \ No newline at end of file +#endif // AUDIO_PIPELINE__I_PIPELINE_STEP_H diff --git a/src/pipeline/LevelAdjustStep.h b/src/pipeline/LevelAdjustStep.h index 2fb011982..5b49d0dc9 100644 --- a/src/pipeline/LevelAdjustStep.h +++ b/src/pipeline/LevelAdjustStep.h @@ -33,9 +33,9 @@ class LevelAdjustStep : public IPipelineStep LevelAdjustStep(int sampleRate, std::function scaleFactorFn); virtual ~LevelAdjustStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: std::function scaleFactorFn_; diff --git a/src/pipeline/LinkStep.cpp b/src/pipeline/LinkStep.cpp index 28360c65c..f15269c03 100644 --- a/src/pipeline/LinkStep.cpp +++ b/src/pipeline/LinkStep.cpp @@ -53,8 +53,11 @@ void LinkStep::clearFifo() std::shared_ptr LinkStep::InputStep::execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) { auto fifo = parent_->getFifo(); - codec2_fifo_write(fifo, inputSamples.get(), numInputSamples); - + if (numInputSamples > 0) + { + codec2_fifo_write(fifo, inputSamples.get(), numInputSamples); + } + // Since we short circuited to the output step, don't return any samples here. *numOutputSamples = 0; return nullptr; diff --git a/src/pipeline/ParallelStep.cpp b/src/pipeline/ParallelStep.cpp index 6bbb635ab..f24d51a41 100644 --- a/src/pipeline/ParallelStep.cpp +++ b/src/pipeline/ParallelStep.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include #include #include "ParallelStep.h" @@ -234,3 +235,23 @@ std::future ParallelStep::enqueueTask_(ThreadInfo* tas return theFuture; } + +void ParallelStep::dump(int indentLevel) +{ + IPipelineStep::dump(indentLevel); + indentLevel += 4; + + for (int index = 0; index < parallelSteps_.size(); index++) + { + std::cout << std::string(indentLevel, ' ') << "[" << index << "] "; + if (getInputSampleRate() != parallelSteps_[index]->getInputSampleRate()) + { + std::cout << "[resample from " << getInputSampleRate() << " to " << parallelSteps_[index]->getInputSampleRate() << "] "; + } + parallelSteps_[index]->dump(indentLevel); + if (parallelSteps_[index]->getOutputSampleRate() != getOutputSampleRate()) + { + std::cout << std::string(indentLevel + 4, ' ') << "[resample from " << parallelSteps_[index]->getOutputSampleRate() << " to " << getOutputSampleRate() << "] " << std::endl; + } + } +} diff --git a/src/pipeline/ParallelStep.h b/src/pipeline/ParallelStep.h index a3c121d32..acb4844ea 100644 --- a/src/pipeline/ParallelStep.h +++ b/src/pipeline/ParallelStep.h @@ -44,13 +44,15 @@ class ParallelStep : public IPipelineStep std::shared_ptr state); virtual ~ParallelStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; const std::vector> getParallelSteps() const { return parallelSteps_; } std::shared_ptr getState() { return state_; } + + virtual void dump(int indentLevel = 0) override; private: typedef std::pair, int> TaskResult; diff --git a/src/pipeline/PlaybackStep.h b/src/pipeline/PlaybackStep.h index 38af159bb..623771a53 100644 --- a/src/pipeline/PlaybackStep.h +++ b/src/pipeline/PlaybackStep.h @@ -35,9 +35,9 @@ class PlaybackStep : public IPipelineStep std::function getSndFileFn, std::function fileCompleteFn); virtual ~PlaybackStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int inputSampleRate_; diff --git a/src/pipeline/RecordStep.h b/src/pipeline/RecordStep.h index 3832e5c47..a739b473b 100644 --- a/src/pipeline/RecordStep.h +++ b/src/pipeline/RecordStep.h @@ -34,9 +34,9 @@ class RecordStep : public IPipelineStep int inputSampleRate, std::function getSndFileFn, std::function isFileCompleteFn); virtual ~RecordStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int inputSampleRate_; diff --git a/src/pipeline/ResamplePlotStep.h b/src/pipeline/ResamplePlotStep.h index 2e32234e7..f629b64a5 100644 --- a/src/pipeline/ResamplePlotStep.h +++ b/src/pipeline/ResamplePlotStep.h @@ -35,9 +35,9 @@ class ResampleForPlotStep : public IPipelineStep ResampleForPlotStep(struct FIFO* fifo); virtual ~ResampleForPlotStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: struct FIFO* fifo_; diff --git a/src/pipeline/ResampleStep.h b/src/pipeline/ResampleStep.h index c8ebe93b7..62146d093 100644 --- a/src/pipeline/ResampleStep.h +++ b/src/pipeline/ResampleStep.h @@ -33,9 +33,9 @@ class ResampleStep : public IPipelineStep ResampleStep(int inputSampleRate, int outputSampleRate); virtual ~ResampleStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int inputSampleRate_; diff --git a/src/pipeline/SpeexStep.h b/src/pipeline/SpeexStep.h index f2621aee0..a082b1fcd 100644 --- a/src/pipeline/SpeexStep.h +++ b/src/pipeline/SpeexStep.h @@ -40,10 +40,10 @@ class SpeexStep : public IPipelineStep SpeexStep(int sampleRate); virtual ~SpeexStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int sampleRate_; diff --git a/src/pipeline/TapStep.cpp b/src/pipeline/TapStep.cpp index 1981d3873..e6cddd19e 100644 --- a/src/pipeline/TapStep.cpp +++ b/src/pipeline/TapStep.cpp @@ -22,6 +22,7 @@ #include "TapStep.h" +#include #include TapStep::TapStep(int sampleRate, IPipelineStep* tapStep) @@ -55,3 +56,11 @@ std::shared_ptr TapStep::execute(std::shared_ptr inputSamples, int *numOutputSamples = numInputSamples; return inputSamples; } + +void TapStep::dump(int indentLevel) +{ + IPipelineStep::dump(indentLevel); + indentLevel += 4; + std::cout << std::string(indentLevel, ' '); + tapStep_->dump(indentLevel); +} diff --git a/src/pipeline/TapStep.h b/src/pipeline/TapStep.h index 931aa362c..17f192c12 100644 --- a/src/pipeline/TapStep.h +++ b/src/pipeline/TapStep.h @@ -33,13 +33,15 @@ class TapStep : public IPipelineStep TapStep(int inputSampleRate, IPipelineStep* tapStep); virtual ~TapStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; + + virtual void dump(int indentLevel = 0) override; private: std::shared_ptr tapStep_; int sampleRate_; }; -#endif // AUDIO_PIPELINE__TAP_STEP_H \ No newline at end of file +#endif // AUDIO_PIPELINE__TAP_STEP_H diff --git a/src/pipeline/ToneInterfererStep.h b/src/pipeline/ToneInterfererStep.h index 6112ca8ec..d3c9597ac 100644 --- a/src/pipeline/ToneInterfererStep.h +++ b/src/pipeline/ToneInterfererStep.h @@ -36,9 +36,9 @@ class ToneInterfererStep : public IPipelineStep std::function toneAmplitudeFn, std::function tonePhaseFn); virtual ~ToneInterfererStep(); - virtual int getInputSampleRate() const; - virtual int getOutputSampleRate() const; - virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples); + virtual int getInputSampleRate() const override; + virtual int getOutputSampleRate() const override; + virtual std::shared_ptr execute(std::shared_ptr inputSamples, int numInputSamples, int* numOutputSamples) override; private: int sampleRate_; diff --git a/src/pipeline/TxRxThread.cpp b/src/pipeline/TxRxThread.cpp index 524ca4d91..9aef3586a 100644 --- a/src/pipeline/TxRxThread.cpp +++ b/src/pipeline/TxRxThread.cpp @@ -20,6 +20,8 @@ // //========================================================================= +#include + // This forces us to use freedv-gui's version rather than another one. // TBD -- may not be needed once we fully switch over to the audio pipeline. #include "../defines.h" @@ -627,8 +629,28 @@ void TxRxThread::txProcessing_() // There may be recorded audio left to encode while ending TX. To handle this, // we keep reading from the FIFO until we have less than nsam_in_48 samples available. int nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); - if (nread != 0 && endingTx) break; - + if (nread != 0 && endingTx) + { + if (freedvInterface.getCurrentMode() == -1) + { + // Special case for handling RADAE EOT + freedvInterface.restartTxVocoder(); + + short* inputSamples = new short[1]; + auto inputSamplesPtr = std::shared_ptr(inputSamples, std::default_delete()); + do + { + auto outputSamples = pipeline_->execute(inputSamplesPtr, 0, &nout); + if (nout > 0) + { + fprintf(stderr, "YYY outputting %d remaining samples\n", nout); + codec2_fifo_write(cbData->outfifo1, outputSamples.get(), nout); + } + } while (nout > 0); + } + break; + } + short* inputSamples = new short[nsam_in_48]; memcpy(inputSamples, insound_card, nsam_in_48 * sizeof(short)); @@ -648,7 +670,8 @@ void TxRxThread::txProcessing_() { // Deallocates TX pipeline when not in use. This is needed to reset the state of // certain TX pipeline steps (such as Speex). - pipeline_ = nullptr; + // XXX: temporarily disabling due to latency of RADAE scripts while restarting. + //pipeline_ = nullptr; // Wipe anything added in the FIFO to prevent pops on next TX. clearFifos_(); @@ -704,6 +727,8 @@ void TxRxThread::rxProcessing_() short* inputSamples = new short[nsam]; memcpy(inputSamples, insound_card, nsam * sizeof(short)); + //pipeline_->dump(); + auto inputSamplesPtr = std::shared_ptr(inputSamples, std::default_delete()); auto outputSamples = pipeline_->execute(inputSamplesPtr, nsam, &nout); auto outFifo = (g_nSoundCards == 1) ? cbData->outfifo1 : cbData->outfifo2; diff --git a/src/pipeline/TxRxThread.h b/src/pipeline/TxRxThread.h index 87529b165..a56b9f54d 100644 --- a/src/pipeline/TxRxThread.h +++ b/src/pipeline/TxRxThread.h @@ -72,7 +72,7 @@ class TxRxThread : public wxThread int inputSampleRate_; int outputSampleRate_; LinkStep* equalizedMicAudioLink_; - + void initializePipeline_(); void txProcessing_(); void rxProcessing_(); diff --git a/src/topFrame.cpp b/src/topFrame.cpp index 5867a1f37..f6146c501 100644 --- a/src/topFrame.cpp +++ b/src/topFrame.cpp @@ -687,6 +687,8 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxWindow *otherModeWin = m_collpane->GetPane(); wxSizer *otherModeSizer = new wxBoxSizer(wxVERTICAL); + m_rbExternalVocoder = new wxRadioButton( otherModeWin, wxID_ANY, wxT("External"), wxDefaultPosition, wxDefaultSize, 0); + otherModeSizer->Add(m_rbExternalVocoder, 1, wxALIGN_LEFT|wxALL|wxEXPAND, 1); m_rb700c = new wxRadioButton( otherModeWin, wxID_ANY, wxT("700C"), wxDefaultPosition, wxDefaultSize, 0); otherModeSizer->Add(m_rb700c, 1, wxALIGN_LEFT|wxALL|wxEXPAND, 1); m_rb800xa = new wxRadioButton( otherModeWin, wxID_ANY, wxT("800XA"), wxDefaultPosition, wxDefaultSize, 0); @@ -850,6 +852,7 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const m_BtnReSync->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnReSync), NULL, this); m_btnCenterRx->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnCenterRx), NULL, this); + m_rbExternalVocoder->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700c->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700d->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700e->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); @@ -933,6 +936,7 @@ TopFrame::~TopFrame() m_audioRecord->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnTogBtnRecord), NULL, this); + m_rbExternalVocoder->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700c->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700d->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); m_rb700e->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(TopFrame::OnChangeTxMode), NULL, this); diff --git a/src/topFrame.h b/src/topFrame.h index 9a00958f0..ae82f6cee 100644 --- a/src/topFrame.h +++ b/src/topFrame.h @@ -131,6 +131,7 @@ class TopFrame : public wxFrame wxToggleButton *m_audioRecord; + wxRadioButton *m_rbExternalVocoder; wxRadioButton *m_rb700c; wxRadioButton *m_rb700d; wxRadioButton *m_rb700e; diff --git a/src/voicekeyer.cpp b/src/voicekeyer.cpp index 728af3812..72416729f 100644 --- a/src/voicekeyer.cpp +++ b/src/voicekeyer.cpp @@ -224,6 +224,16 @@ int MainFrame::VoiceKeyerStartTx(void) } else { g_sfTxFs = sfInfo.samplerate; + + if (g_sfTxFs < 16000) + { + wxMessageBox(wxT("The selected voice keyer file does not have a high enough sample rate to guarantee acceptable audio quality. Please ensure that your file's sample rate is 16 kHz or greater."), wxT("Sample Rate Too Low"), wxOK); + sf_close(tmpPlayFile); + m_togBtnVoiceKeyer->SetBackgroundColour(wxNullColour); + m_togBtnVoiceKeyer->SetValue(false); + return VK_IDLE; + } + g_sfPlayFile = tmpPlayFile; SetStatusText(wxT("Voice Keyer: Playing file ") + vkFileName_ + wxT(" to mic input") , 0);