Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Initial support for piping audio through external vocoder programs. #719

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d23e508
Initial support for piping audio through external vocoder programs.
tmiw May 1, 2024
9557302
Fix segfaults while attempting to start external vocoder.
tmiw May 6, 2024
d591de2
WIP: pipe pre-generated decoded audio to freedv-gui.
tmiw May 6, 2024
12bc994
Pipe redirections were in the wrong order, causing Python errors.
tmiw May 6, 2024
a57aed8
Fix additional segfault found on macOS.
tmiw May 6, 2024
078e75e
Refactor 'vocoder' script to allow easier parameter adjustment.
tmiw May 6, 2024
5d1bd96
Fix accidental buffer overflow.
tmiw May 6, 2024
45ddfef
Fix crash when toggling between analog and digital modes.
tmiw May 6, 2024
ef8a289
Need to return sane values for sample rates to prevent crashes.
tmiw May 6, 2024
09c3bbf
Force modem stats to index 0 when using external vocoder.
tmiw May 7, 2024
3888560
Add debug dumping for the audio pipeline.
tmiw May 7, 2024
80b080b
Return 8 kHz for external vocoder even though it's 16 kHz to fix wate…
tmiw May 7, 2024
6e9775b
Disable pipeline debug dumping as it's no longer needed.
tmiw May 7, 2024
f6eee16
Fix broken waterfall WAV file.
tmiw May 11, 2024
ba1f5d7
Merge branch 'master' into ms-external-vocoder
tmiw Aug 17, 2024
1dc03af
Use streaming RADAE flow instead of pre-canned files.
tmiw Aug 17, 2024
bdf48bc
Delete missed file.
tmiw Aug 17, 2024
c9e33a8
Create new TX script.
tmiw Aug 17, 2024
fd5928e
Fix segfaults when transmitting through external vocoder.
tmiw Aug 17, 2024
4d22ce9
RADAE scripts need to execute from RADAE directory.
tmiw Aug 17, 2024
46fd9d2
Output sample rate is different from input.
tmiw Aug 17, 2024
dafa7af
Figured out how to get valid audio into radae_tx.py.
tmiw Aug 17, 2024
169a4f3
We can just use - instead of /dev/stdin and /dev/stdout.
tmiw Aug 18, 2024
5a325b1
Cleanup TX/RX scripts and disable buffering.
tmiw Aug 18, 2024
7fe5c94
Kill processes on stop so that the GUI doesn't hang.
tmiw Aug 19, 2024
ed31b5c
Temporarily disable clearing TX pipeline due to RADAE startup latency.
tmiw Aug 19, 2024
1f7fb23
Move process I/O to a separate thread to avoid audio dropouts.
tmiw Aug 21, 2024
fae6119
Fix Linux compiler error.
tmiw Aug 21, 2024
43346da
Fix issue preventing voice keyer from firing more than once.
tmiw Aug 21, 2024
37bc843
First pass at Windows implementation.
tmiw Aug 23, 2024
0df5552
Fix Linux/macOS compiler error.
tmiw Aug 23, 2024
1e473b2
Warning cleanup.
tmiw Aug 23, 2024
65d60b3
Fix Windows compiler errors.
tmiw Aug 23, 2024
fc797b2
Use I/O completion ports for stdout/stderr piping.
tmiw Aug 31, 2024
7f5b51c
Add required Windows batch scripts to make RADAE work.
tmiw Aug 31, 2024
56ddbf5
Code cleanup.
tmiw Aug 31, 2024
9cd9e58
Use larger buffer for stderr on Windows.
tmiw Aug 31, 2024
e75bcb4
Add carriage returns to stderr output to make it look better on Windows.
tmiw Sep 1, 2024
8f66f0f
Use async pipe for stdin as well.
tmiw Sep 2, 2024
f79cccb
Update batch scripts to reflect use of conda instead of pip.
tmiw Sep 2, 2024
c9d272b
Adjust sample rates and number of samples to match what vocoder expects.
tmiw Sep 3, 2024
563526a
Fix crash bug on TX start.
tmiw Sep 3, 2024
d6f0fd3
Write as much as we can get away with to the input pipe.
tmiw Sep 3, 2024
6a2f9ce
Send larger blocks of samples to RADAE to avoid premature timeout of …
tmiw Sep 4, 2024
a23f62e
Scale number of output samples based on input/output sample rates.
tmiw Sep 4, 2024
6f0c6eb
Add delays to make sure we don't process audio too quickly.
tmiw Sep 4, 2024
c034aba
Revert "Add delays to make sure we don't process audio too quickly."
tmiw Sep 4, 2024
69a1bc3
Move delay to TX thread handler.
tmiw Sep 4, 2024
88e6993
Set speech/modem sample counts to match what console scripts actually…
tmiw Sep 6, 2024
520059b
Update TX to wait until a minimum number of samples have been receive…
tmiw Sep 6, 2024
3c6080f
Enforce minimum 16 kHz sample rate for voice keyer files.
tmiw Sep 6, 2024
dd575ff
Disallow sample rates lower than 16000 in audio config.
tmiw Sep 6, 2024
4af9eee
Merge branch 'master' into ms-external-vocoder
tmiw Sep 8, 2024
71642e5
Warning cleanup.
tmiw Sep 9, 2024
6e787b7
Add Linux/macOS support for transmitting RADAE EOT.
tmiw Sep 9, 2024
bc91145
Merge branch 'ms-external-vocoder' of github.com:drowe67/freedv-gui i…
tmiw Sep 9, 2024
bcf2673
Add debugging output to figure out what's going on.
tmiw Sep 10, 2024
f7c76b8
No-op restart since it's not working and I don't know why.
tmiw Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions radae_demo_rx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/sh

RADAE_PATH=$1
RADAE_VENV=$2

# Current RADAE scripts seem to require being executed from
# the RADAE folder.
cd $RADAE_PATH

# The below does the following:
# * For each block of OTA audio from freedv-gui:
# * Convert the 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.
$RADAE_VENV/bin/python3 $RADAE_PATH/int16tof32.py --zeropad | $RADAE_VENV/bin/python3 $RADAE_PATH/radae_rx.py $RADAE_PATH/model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata | $RADAE_PATH/build/src/lpcnet_demo -fargan-synthesis - -
Copy link
Owner

@drowe67 drowe67 Aug 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

cd $RADAE_PATH
python3 int16tof32.py --zeropad | python3 radae_rx.py model19_check3/checkpoints/checkpoint_epoch_100.pth -v 2 --auxdata | ./build/src/lpcnet_demo -fargan-synthesis - -

Maybe try Ubuntu 22 and see if the VENV issues go away? It's pretty hard to follow the code with all the explicit paths. Apart from that it looks OK. You could maybe test the script stand alone to see if it streams OK. I'm not sure about streaming to and from scripts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I went ahead and cleaned up the scripts so they should be a bit neater now. Thanks for taking a look!

16 changes: 16 additions & 0 deletions radae_demo_tx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh

RADAE_PATH=$1
RADAE_VENV=$2

# Current RADAE scripts seem to require being executed from
# the RADAE folder.
cd $RADAE_PATH

# The below does the following:
# * For each block of OTA audio from freedv-gui:
# * Convert the audio into IQ data via zero-padding.
# * Pass the IQ data into the RADAE encoder
# * Send the resulting 8 kHz audio back to freedv-gui.
export PATH=$RADAE_VENV/bin:$PATH
$RADAE_PATH/build/src/lpcnet_demo -features - - | $RADAE_VENV/bin/python3 $RADAE_PATH/radae_tx.py $RADAE_PATH/model19_check3/checkpoints/checkpoint_epoch_100.pth --auxdata | python3 $RADAE_PATH/f32toint16.py --real --scale 16383
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks 👍 But as above - see if the streaming works when you run the script form the cmd line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they work on the command line. It looks like there's significant latency before they even start encoding or decoding, mainly while PyTorch is setting itself up (hence why I was having the TX problem). This might be able to be worked around by e.g. not constantly killing and restarting the TX process.

9 changes: 9 additions & 0 deletions src/config/FreeDVConfiguration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ FreeDVConfiguration::FreeDVConfiguration()

, txRxDelayMilliseconds("/Audio/TxRxDelayMilliseconds", 0)

, externalVocoderRxCommand("/ExternalVocoder/RxCommand", _(""))
, externalVocoderTxCommand("/ExternalVocoder/TxCommand", _(""))

, reportingUserMsgColWidth("/Windows/FreeDVReporter/reportingUserMsgColWidth", 130)
{
// empty
Expand Down Expand Up @@ -233,6 +236,9 @@ void FreeDVConfiguration::load(wxConfigBase* config)

load_(config, txRxDelayMilliseconds);

load_(config, externalVocoderRxCommand);
load_(config, externalVocoderTxCommand);

load_(config, reportingUserMsgColWidth);
}

Expand Down Expand Up @@ -321,6 +327,9 @@ void FreeDVConfiguration::save(wxConfigBase* config)

save_(config, txRxDelayMilliseconds);

save_(config, externalVocoderRxCommand);
save_(config, externalVocoderTxCommand);

save_(config, reportingUserMsgColWidth);

config->Flush();
Expand Down
3 changes: 3 additions & 0 deletions src/config/FreeDVConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ class FreeDVConfiguration : public WxWidgetsConfigStore

ConfigurationDataElement<int> reportingUserMsgColWidth;

ConfigurationDataElement<wxString> externalVocoderRxCommand;
ConfigurationDataElement<wxString> externalVocoderTxCommand;

virtual void load(wxConfigBase* config) override;
virtual void save(wxConfigBase* config) override;
};
Expand Down
105 changes: 84 additions & 21 deletions src/freedv_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "pipeline/ParallelStep.h"
#include "pipeline/FreeDVTransmitStep.h"
#include "pipeline/FreeDVReceiveStep.h"
#include "pipeline/ExternVocoderStep.h"

using namespace std::placeholders;

Expand Down Expand Up @@ -127,6 +128,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);

Expand Down Expand Up @@ -274,16 +285,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);
Expand All @@ -293,6 +308,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)
{
Expand Down Expand Up @@ -371,6 +388,7 @@ void FreeDVInterface::setSync(int val)

int FreeDVInterface::getSync() const
{
if (currentRxMode_ == nullptr) return 1;
return freedv_get_sync(currentRxMode_);
}

Expand Down Expand Up @@ -421,24 +439,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 8000;

assert(currentTxMode_ != nullptr);
return freedv_get_speech_sample_rate(currentTxMode_);
}

int FreeDVInterface::getTxNumSpeechSamples() const
{
if (txMode_ == -1) return 1024;

assert(currentTxMode_ != nullptr);
return freedv_get_n_speech_samples(currentTxMode_);
}

int FreeDVInterface::getTxNNomModemSamples() const
{
if (txMode_ == -1) return 1024;

assert(currentTxMode_ != nullptr);
return freedv_get_n_nom_modem_samples(currentTxMode_);
}
Expand All @@ -465,6 +491,8 @@ void FreeDVInterface::setTextVaricodeNum(int num)

int FreeDVInterface::getRxModemSampleRate() const
{
if (rxMode_ == -1) return 8000;

int result = 0;
for (auto& dv : dvObjects_)
{
Expand All @@ -476,6 +504,8 @@ int FreeDVInterface::getRxModemSampleRate() const

int FreeDVInterface::getRxNumModemSamples() const
{
if (rxMode_ == -1) return 1024;

int result = 0;
for (auto& dv : dvObjects_)
{
Expand All @@ -487,6 +517,8 @@ int FreeDVInterface::getRxNumModemSamples() const

int FreeDVInterface::getRxNumSpeechSamples() const
{
if (rxMode_ == -1) return 1024;

int result = 0;
for (auto& dv : dvObjects_)
{
Expand All @@ -498,6 +530,8 @@ int FreeDVInterface::getRxNumSpeechSamples() const

int FreeDVInterface::getRxSpeechSampleRate() const
{
if (rxMode_ == -1) return 8000;

int result = 0;
for (auto& dv : dvObjects_)
{
Expand Down Expand Up @@ -559,14 +593,23 @@ void FreeDVInterface::setReliableText(const char* callsign)
IPipelineStep* FreeDVInterface::createTransmitPipeline(int inputSampleRate, int outputSampleRate, std::function<float()> getFreqOffsetFn)
{
std::vector<IPipelineStep*> parallelSteps;


if (txMode_ == -1)
{
// special handling for external vocoder
parallelSteps.push_back(new ExternVocoderStep(externVocoderTxCommand_, 16000, 8000));
}

for (auto& dv : dvObjects_)
{
parallelSteps.push_back(new FreeDVTransmitStep(dv, getFreqOffsetFn));
}

std::function<int(ParallelStep*)> modeFn =
[&](ParallelStep*) {
// Special handling for external vocoder.
if (txMode_ == -1) return 0;

int index = 0;
for (auto& dv : dvObjects_)
{
Expand Down Expand Up @@ -608,16 +651,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,
Expand Down Expand Up @@ -671,6 +724,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_))
Expand Down Expand Up @@ -726,21 +781,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;
};
10 changes: 8 additions & 2 deletions src/freedv_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ class FreeDVInterface
std::function<float()> getFreqOffsetFn,
std::function<float*()> getSigPwrAvgFn
);


void setExternVocoderRxCommand(std::string command) { externVocoderRxCommand_ = command; }
void setExternVocoderTxCommand(std::string command) { externVocoderTxCommand_ = command; }

private:
struct ReceivePipelineState
{
Expand Down Expand Up @@ -168,7 +171,10 @@ class FreeDVInterface
std::deque<reliable_text_t> reliableText_;
std::string receivedReliableText_;
std::mutex reliableTextMutex_;


std::string externVocoderRxCommand_;
std::string externVocoderTxCommand_;

int preProcessRxFn_(ParallelStep* ps);
int postProcessRxFn_(ParallelStep* ps);
};
Expand Down
33 changes: 33 additions & 0 deletions src/gui/dialogs/dlg_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,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
//------------------------------
Expand Down Expand Up @@ -837,6 +864,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);

Expand Down Expand Up @@ -1036,6 +1066,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
Expand Down
Loading
Loading