Fix dropouts related to virtual audio cables. (#840)

* Extend RADEv1 unit test time to try to force failures.

* txrx test wasn't actually respecting -txtime.

* Increase test length to 10min.

* Increase timeout.

* Temporarily disable macOS and Linux workflows.

* Run RADE test 10 times.

* Tweak to hopefully cause Windows test to actually fail.

* Ensure that all PortAudio calls are executed from the same thread.

* Use exclusive mode for radio devices for lower latency (at least on Windows).

* Reduce TX/RX thread delay to 10ms.

* Increase GH action timeout.

* Oops, need to tell PortAudio to actually set thread priority.

* Suggest low latency to PortAudio.

* Revert 10ms max wait.

* Try forcing shared mode again.

* Revert "Try forcing shared mode again."

This reverts commit cd025e9a8e.

* Revert "Revert 10ms max wait."

This reverts commit 0227ce5f0c.

* Reenable macOS/Linux builds.

* Run RADE tests 10x on Linux and macOS.

* Revert back to 60s tests on macOS/Linux.

* Use sh -c to run test.

* Try defining FPB as 0.

* Split out repeated RADE test into a separate ctest.

* Disable GH action timeout for Windows tests.

* Fix CMakeLists.txt error.

* Try increasing the timeout back to 20ms again.

* Back to 10ms.

* macOS: minimize CPU usage inside PortAudio.

* Use higher quality macOS settings.

* Use Intel macOS runner as it has more cores and RAM.

* Add debug output so we can adapt GH action for Intel.

* Fix gfortran path based on debug output.

* Fix permissions database issue.

* Disable exclusive mode due to invalid device errors.

* Disable stress tests in GitHub environment by default.

* Restructure TX out code to help the compiler optimize for the common case.

* Forgot to disable the repeated test.

* Use VB-Cable for radio device on macOS due to improved reliability.

* Fix side issue reported in original issue surrouding default mode selection.

* Need to restrict number of runs to 1 for RADE on Windows.

* Remove unnecessary API for setting exclusive mode.

* Wrap sync flag in std::atomic just in case the GH failures are actually threading related.

* Add PR #840 to changelog.
ms-embed-sioclient
Mooneer Salem 2025-03-06 00:40:05 -08:00 committed by GitHub
parent 130aeec504
commit 7575c630b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 377 additions and 118 deletions

View File

@ -15,7 +15,7 @@ jobs:
# well on Windows or Mac. You can convert this to a matrix build if you need
# cross-platform coverage.
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
runs-on: macos-latest
runs-on: macos-13
steps:
- uses: actions/checkout@v4
@ -30,15 +30,21 @@ jobs:
working-directory: ${{github.workspace}}
run: |
# make sure gfortran is available
sudo ln -s /opt/homebrew/bin/gfortran-14 /opt/homebrew/bin/gfortran
ls /opt/homebrew/bin/gfortran*
sudo ln -s /usr/local/bin/gfortran-14 /usr/local/bin/gfortran
#ls /opt/homebrew/bin/gfortran*
#sudo mkdir /usr/local/gfortran
#ls /usr/local/Cellar
#sudo ln -s /usr/local/Cellar/gcc@14/*/lib/gcc/14 /usr/local/gfortran/lib
gfortran --version
octave-cli --eval "pkg install -forge control; pkg install -forge signal"
- name: Install virtual audio devices
- name: Install VB-Cable
shell: bash
working-directory: ${{github.workspace}}
run: |
brew install vb-cable
- name: Install other virtual audio devices
shell: bash
working-directory: ${{github.workspace}}
run: ./build_macos_sound_drivers.sh
@ -50,14 +56,14 @@ jobs:
- name: Workaround macOS permission issues
run: |
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159,NULL,NULL,'UNUSED',1687786159);"
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/opt/off/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159,NULL,NULL,'UNUSED',1687786159);"
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/usr/local/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159);"
sqlite3 $HOME/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR IGNORE INTO access VALUES ('kTCCServiceMicrophone','/opt/off/opt/runner/provisioner/provisioner',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,1687786159);"
- name: Execute unit tests
shell: bash
working-directory: ${{github.workspace}}/build_osx
run: |
FREEDV_COMPUTER_TO_RADIO_DEVICE="BlackHoleRadio 2ch" FREEDV_RADIO_TO_COMPUTER_DEVICE="BlackHoleRadio 2ch 2" FREEDV_COMPUTER_TO_SPEAKER_DEVICE="BlackHole1 2ch" FREEDV_MICROPHONE_TO_COMPUTER_DEVICE="BlackHole2 2ch" ctest -V
FREEDV_COMPUTER_TO_RADIO_DEVICE="VB-Cable" FREEDV_RADIO_TO_COMPUTER_DEVICE="VB-Cable" FREEDV_COMPUTER_TO_SPEAKER_DEVICE="BlackHole1 2ch" FREEDV_MICROPHONE_TO_COMPUTER_DEVICE="BlackHole2 2ch" ctest -V
- name: Package executable
working-directory: ${{github.workspace}}/build_osx

View File

@ -720,14 +720,22 @@ elseif(UNIX AND NOT APPLE)
endif(WIN32)
if(UNITTEST)
# The below tests are currently Linux-only due to a dependency on
# PulseAudio/pipewire.
# The below tests are currently Linux/macOS-only. A PowerShell version of fullduplex_*
# for Windows is implemented in tests/TestFreeDVFullDuplex.ps1.
macro(DefineAudioTest utName)
add_test(NAME fullduplex_${utName} COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx ${utName})
set_tests_properties(fullduplex_${utName} PROPERTIES PASS_REGULAR_EXPRESSION "Got 1 sync changes")
add_test(NAME fullduplex_${utName}
COMMAND sh -c "${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx ${utName}")
set_tests_properties(fullduplex_${utName} PROPERTIES FAIL_REGULAR_EXPRESSION "Sync changed from 1 to 0")
endmacro()
DefineAudioTest(RADEV1)
add_test(NAME fullduplex_RADEV1_repeated
COMMAND sh -c " ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1 && ${CMAKE_CURRENT_SOURCE_DIR}/test/test_zeros.sh txrx RADEV1")
set_tests_properties(fullduplex_RADEV1_repeated PROPERTIES FAIL_REGULAR_EXPRESSION "Sync changed from 1 to 0")
set_tests_properties(fullduplex_RADEV1_repeated PROPERTIES DISABLED TRUE)
set_tests_properties(fullduplex_RADEV1_repeated PROPERTIES LABELS stress_test)
DefineAudioTest(700D)
DefineAudioTest(700E)
DefineAudioTest(1600)

View File

@ -900,6 +900,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes
* Don't adjust Msg column width when user disconnects. (PR #828)
* Fix issue preventing suppression of the Msg tooltip for non-truncated messages. (PR #829)
* Preserve Hamlib rig names on startup to guard against changes by Hamlib during execution. (PR #834)
* Fix dropouts related to virtual audio cables. (PR #840)
2. Enhancements:
* Show green line indicating RX frequency. (PR #725)
* Update configuration of the Voice Keyer feature based on user feedback. (PR #730, #746, #793)

View File

@ -3,26 +3,6 @@
git clone https://github.com/tmiw/BlackHole.git
cd BlackHole
bundleID=audio.existential.BlackHoleRadio
driverName=BlackHoleRadio
xcodebuild \
-project BlackHole.xcodeproj \
-configuration Release \
-target BlackHole \
CONFIGURATION_BUILD_DIR=build \
PRODUCT_BUNDLE_IDENTIFIER=$bundleID \
GCC_PREPROCESSOR_DEFINITIONS="$GCC_PREPROCESSOR_DEFINITIONS \
kNumber_Of_Channels='2' \
kPlugIn_BundleID='\"$bundleID\"' \
kDriver_Name='\"$driverName\"' \
kDevice2_IsHidden=false \
kDevice2_HasInput=true \
kDevice2_HasOutput=true" \
MACOSX_DEPLOYMENT_TARGET=10.13
sudo mv build/BlackHole.driver /Library/Audio/Plug-Ins/HAL/$driverName.driver
for i in {1..2}; do
git reset --hard
rm -rf build

View File

@ -8,6 +8,7 @@ else(USE_PULSEAUDIO AND LINUX)
set(AUDIO_ENGINE_LIBRARY_SPECIFIC_FILES
PortAudioDevice.cpp
PortAudioEngine.cpp
PortAudioInterface.cpp
)
endif(USE_PULSEAUDIO AND LINUX)

View File

@ -25,20 +25,24 @@
#include <cstring>
#include "PortAudioDevice.h"
#include "portaudio.h"
#if defined(WIN32)
#include "pa_win_wasapi.h"
#elif defined(__APPLE__)
#include "pa_mac_core.h"
#endif // defined(WIN32)
// Brought over from previous implementation. "Optimal" value of 0 (per PA
// documentation) causes occasional audio pops/cracks on start for macOS.
#define PA_FPB 256
#define PA_FPB 0
PortAudioDevice::PortAudioDevice(int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels)
PortAudioDevice::PortAudioDevice(std::shared_ptr<PortAudioInterface> library, int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels)
: deviceId_(deviceId)
, direction_(direction)
, sampleRate_(sampleRate)
, numChannels_(numChannels)
, deviceStream_(nullptr)
, portAudioLibrary_(library)
{
auto deviceInfo = Pa_GetDeviceInfo(deviceId_);
std::string hostApiName = Pa_GetHostApiInfo(deviceInfo->hostApi)->name;
auto deviceInfo = portAudioLibrary_->GetDeviceInfo(deviceId_).get();
std::string hostApiName = portAudioLibrary_->GetHostApiInfo(deviceInfo->hostApi).get()->name;
// Windows only: we are switching from MME to WASAPI. A side effect
// of this is that we really only support one sample rate. Instead
@ -73,15 +77,34 @@ bool PortAudioDevice::isRunning()
void PortAudioDevice::start()
{
PaStreamParameters streamParameters;
auto deviceInfo = Pa_GetDeviceInfo(deviceId_);
auto deviceInfo = portAudioLibrary_->GetDeviceInfo(deviceId_).get();
streamParameters.device = deviceId_;
streamParameters.channelCount = numChannels_;
streamParameters.sampleFormat = paInt16;
streamParameters.suggestedLatency = deviceInfo->defaultHighInputLatency;
streamParameters.suggestedLatency =
IAudioEngine::AUDIO_ENGINE_IN ? deviceInfo->defaultLowInputLatency : deviceInfo->defaultLowOutputLatency;
#if defined(WIN32)
PaWasapiStreamInfo wasapiInfo;
wasapiInfo.size = sizeof(PaWasapiStreamInfo);
wasapiInfo.hostApiType = paWASAPI;
wasapiInfo.version = 1;
wasapiInfo.flags = paWinWasapiThreadPriority;
wasapiInfo.channelMask = NULL;
wasapiInfo.hostProcessorOutput = NULL;
wasapiInfo.hostProcessorInput = NULL;
wasapiInfo.threadPriority = eThreadPriorityProAudio;
streamParameters.hostApiSpecificStreamInfo = &wasapiInfo;
#elif defined(__APPLE__)
PaMacCoreStreamInfo macInfo;
PaMacCore_SetupStreamInfo(&macInfo, paMacCorePro);
streamParameters.hostApiSpecificStreamInfo = &macInfo;
#else
streamParameters.hostApiSpecificStreamInfo = NULL;
#endif // defined(WIN32)
auto error = Pa_OpenStream(
auto error = portAudioLibrary_->OpenStream(
&deviceStream_,
direction_ == IAudioEngine::AUDIO_ENGINE_IN ? &streamParameters : nullptr,
direction_ == IAudioEngine::AUDIO_ENGINE_OUT ? &streamParameters : nullptr,
@ -90,18 +113,18 @@ void PortAudioDevice::start()
paClipOff,
&OnPortAudioStreamCallback_,
this
);
).get();
if (error == paNoError)
{
error = Pa_StartStream(deviceStream_);
error = portAudioLibrary_->StartStream(deviceStream_).get();
if (error != paNoError)
{
std::string errText = Pa_GetErrorText(error);
std::string errText = portAudioLibrary_->GetErrorText(error).get();
if (error == paUnanticipatedHostError)
{
std::stringstream ss;
auto errInfo = Pa_GetLastHostErrorInfo();
auto errInfo = portAudioLibrary_->GetLastHostErrorInfo().get();
ss << " (error code " << std::hex << errInfo->errorCode << " - " << std::string(errInfo->errorText) << ")";
errText += ss.str();
}
@ -111,17 +134,17 @@ void PortAudioDevice::start()
onAudioErrorFunction(*this, errText, onAudioErrorState);
}
Pa_CloseStream(deviceStream_);
portAudioLibrary_->CloseStream(deviceStream_).wait();
deviceStream_ = nullptr;
}
}
else
{
std::string errText = Pa_GetErrorText(error);
std::string errText = portAudioLibrary_->GetErrorText(error).get();
if (error == paUnanticipatedHostError)
{
std::stringstream ss;
auto errInfo = Pa_GetLastHostErrorInfo();
auto errInfo = portAudioLibrary_->GetLastHostErrorInfo().get();
ss << " (error code " << std::hex << errInfo->errorCode << " - " << std::string(errInfo->errorText) << ")";
errText += ss.str();
}
@ -138,8 +161,8 @@ void PortAudioDevice::stop()
{
if (deviceStream_ != nullptr)
{
Pa_StopStream(deviceStream_);
Pa_CloseStream(deviceStream_);
portAudioLibrary_->StopStream(deviceStream_).wait();
portAudioLibrary_->CloseStream(deviceStream_).wait();
deviceStream_ = nullptr;
}
}

View File

@ -26,6 +26,7 @@
#include "portaudio.h"
#include "IAudioEngine.h"
#include "IAudioDevice.h"
#include "PortAudioInterface.h"
class PortAudioDevice : public IAudioDevice
{
@ -44,7 +45,7 @@ protected:
// PortAudioDevice cannot be created directly, only via PortAudioEngine.
friend class PortAudioEngine;
PortAudioDevice(int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels);
PortAudioDevice(std::shared_ptr<PortAudioInterface> library, int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels);
private:
int deviceId_;
@ -52,6 +53,7 @@ private:
int sampleRate_;
int numChannels_;
PaStream* deviceStream_;
std::shared_ptr<PortAudioInterface> portAudioLibrary_;
static int OnPortAudioStreamCallback_(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData);
};

View File

@ -29,7 +29,7 @@
PortAudioEngine::PortAudioEngine()
: initialized_(false)
{
// empty
portAudioLibrary_ = std::make_shared<PortAudioInterface>();
}
PortAudioEngine::~PortAudioEngine()
@ -42,14 +42,14 @@ PortAudioEngine::~PortAudioEngine()
void PortAudioEngine::start()
{
auto error = Pa_Initialize();
auto error = portAudioLibrary_->Initialize().get();
if (error != paNoError)
{
std::string errText = Pa_GetErrorText(error);
std::string errText = portAudioLibrary_->GetErrorText(error).get();
if (error == paUnanticipatedHostError)
{
std::stringstream ss;
auto errInfo = Pa_GetLastHostErrorInfo();
auto errInfo = portAudioLibrary_->GetLastHostErrorInfo().get();
ss << " (error code " << std::hex << errInfo->errorCode << " - " << std::string(errInfo->errorText) << ")";
errText += ss.str();
}
@ -67,20 +67,20 @@ void PortAudioEngine::start()
void PortAudioEngine::stop()
{
Pa_Terminate();
portAudioLibrary_->Terminate().wait();
initialized_ = false;
}
std::vector<AudioDeviceSpecification> PortAudioEngine::getAudioDeviceList(AudioDirection direction)
{
int numDevices = Pa_GetDeviceCount();
int numDevices = portAudioLibrary_->GetDeviceCount().get();
std::vector<AudioDeviceSpecification> result;
for (int index = 0; index < numDevices; index++)
{
const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(index);
const PaDeviceInfo *deviceInfo = portAudioLibrary_->GetDeviceInfo(index).get();
std::string hostApiName = Pa_GetHostApiInfo(deviceInfo->hostApi)->name;
std::string hostApiName = portAudioLibrary_->GetHostApiInfo(deviceInfo->hostApi).get()->name;
if (hostApiName.find("DirectSound") != std::string::npos ||
hostApiName.find("surround") != std::string::npos ||
//hostApiName.find("Windows WASAPI") != std::string::npos ||
@ -104,7 +104,7 @@ std::vector<AudioDeviceSpecification> PortAudioEngine::getAudioDeviceList(AudioD
streamParameters.device = index;
streamParameters.channelCount = 1;
streamParameters.sampleFormat = paInt16;
streamParameters.suggestedLatency = Pa_GetDeviceInfo(index)->defaultHighInputLatency;
streamParameters.suggestedLatency = portAudioLibrary_->GetDeviceInfo(index).get()->defaultLowInputLatency;
streamParameters.hostApiSpecificStreamInfo = NULL;
// On Linux, the below logic causes the device lookup process to take MUCH
@ -117,10 +117,10 @@ std::vector<AudioDeviceSpecification> PortAudioEngine::getAudioDeviceList(AudioD
{
while (streamParameters.channelCount < maxChannels)
{
PaError err = Pa_IsFormatSupported(
PaError err = portAudioLibrary_->IsFormatSupported(
direction == AUDIO_ENGINE_IN ? &streamParameters : NULL,
direction == AUDIO_ENGINE_OUT ? &streamParameters : NULL,
deviceInfo->defaultSampleRate);
deviceInfo->defaultSampleRate).get();
if (err == paFormatIsSupported)
{
@ -171,7 +171,7 @@ std::vector<int> PortAudioEngine::getSupportedSampleRates(wxString deviceName, A
streamParameters.device = device.deviceId;
streamParameters.channelCount = device.minChannels;
streamParameters.sampleFormat = paInt16;
streamParameters.suggestedLatency = Pa_GetDeviceInfo(device.deviceId)->defaultHighInputLatency;
streamParameters.suggestedLatency = portAudioLibrary_->GetDeviceInfo(device.deviceId).get()->defaultLowInputLatency;
streamParameters.hostApiSpecificStreamInfo = NULL;
int rateIndex = 0;
@ -182,10 +182,10 @@ std::vector<int> PortAudioEngine::getSupportedSampleRates(wxString deviceName, A
bool isDeviceWithKnownMinimum = IsDeviceWhitelisted_(deviceName);
if (!isDeviceWithKnownMinimum)
{
err = Pa_IsFormatSupported(
err = portAudioLibrary_->IsFormatSupported(
direction == AUDIO_ENGINE_IN ? &streamParameters : NULL,
direction == AUDIO_ENGINE_OUT ? &streamParameters : NULL,
IAudioEngine::StandardSampleRates[rateIndex]);
IAudioEngine::StandardSampleRates[rateIndex]).get();
}
if (err == paFormatIsSupported)
@ -213,7 +213,9 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d
{
auto devices = getAudioDeviceList(direction);
PaDeviceIndex defaultDeviceIndex =
direction == AUDIO_ENGINE_IN ? Pa_GetDefaultInputDevice() : Pa_GetDefaultOutputDevice();
(direction == AUDIO_ENGINE_IN ?
portAudioLibrary_->GetDefaultInputDevice() :
portAudioLibrary_->GetDefaultOutputDevice()).get();
if (defaultDeviceIndex != paNoDevice)
{
@ -260,7 +262,7 @@ std::shared_ptr<IAudioDevice> PortAudioEngine::getAudioDevice(wxString deviceNam
numChannels = std::min(numChannels, dev.maxChannels);
// Create device object.
auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, numChannels);
auto devObj = new PortAudioDevice(portAudioLibrary_, dev.deviceId, direction, sampleRate, numChannels);
return std::shared_ptr<IAudioDevice>(devObj);
}
}

View File

@ -23,7 +23,9 @@
#ifndef PORT_AUDIO_ENGINE_H
#define PORT_AUDIO_ENGINE_H
#include <memory>
#include "IAudioEngine.h"
#include "PortAudioInterface.h"
class PortAudioEngine : public IAudioEngine
{
@ -40,6 +42,7 @@ public:
private:
bool initialized_;
std::shared_ptr<PortAudioInterface> portAudioLibrary_;
static bool IsDeviceWhitelisted_(const char* devName);
};

View File

@ -0,0 +1,175 @@
//=========================================================================
// Name: PortAudioInterface.cpp
// Purpose: Wrapper to enforce thread safety around PortAudio.
//
// 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 <http://www.gnu.org/licenses/>.
//
//=========================================================================
#include <memory>
#include "PortAudioInterface.h"
std::future<PaError> PortAudioInterface::Initialize()
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_Initialize());
});
return fut;
}
std::future<PaError> PortAudioInterface::Terminate()
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_Terminate());
});
return fut;
}
std::future<const char*> PortAudioInterface::GetErrorText(PaError error)
{
std::shared_ptr<std::promise<const char*> > prom = std::make_shared<std::promise<const char*> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetErrorText(error));
});
return fut;
}
std::future<const PaHostErrorInfo*> PortAudioInterface::GetLastHostErrorInfo()
{
std::shared_ptr<std::promise<const PaHostErrorInfo*> > prom = std::make_shared<std::promise<const PaHostErrorInfo*> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetLastHostErrorInfo());
});
return fut;
}
std::future<PaDeviceIndex> PortAudioInterface::GetDeviceCount()
{
std::shared_ptr<std::promise<PaDeviceIndex> > prom = std::make_shared<std::promise<PaDeviceIndex> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetDeviceCount());
});
return fut;
}
std::future<const PaDeviceInfo*> PortAudioInterface::GetDeviceInfo(PaDeviceIndex device)
{
std::shared_ptr<std::promise<const PaDeviceInfo*> > prom = std::make_shared<std::promise<const PaDeviceInfo*> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetDeviceInfo(device));
});
return fut;
}
std::future<const PaHostApiInfo*> PortAudioInterface::GetHostApiInfo(PaHostApiIndex hostApi)
{
std::shared_ptr<std::promise<const PaHostApiInfo*> > prom = std::make_shared<std::promise<const PaHostApiInfo*> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetHostApiInfo(hostApi));
});
return fut;
}
std::future<PaError> PortAudioInterface::IsFormatSupported(
const PaStreamParameters* inputParameters,
const PaStreamParameters* outputParameters,
double sampleRate
)
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate));
});
return fut;
}
std::future<PaDeviceIndex> PortAudioInterface::GetDefaultInputDevice(void)
{
std::shared_ptr<std::promise<PaDeviceIndex> > prom = std::make_shared<std::promise<PaDeviceIndex> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetDefaultInputDevice());
});
return fut;
}
std::future<PaDeviceIndex> PortAudioInterface::GetDefaultOutputDevice(void)
{
std::shared_ptr<std::promise<PaDeviceIndex> > prom = std::make_shared<std::promise<PaDeviceIndex> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_GetDefaultOutputDevice());
});
return fut;
}
std::future<PaError> PortAudioInterface::OpenStream(
PaStream **stream, const PaStreamParameters *inputParameters,
const PaStreamParameters *outputParameters, double sampleRate,
unsigned long framesPerBuffer, PaStreamFlags streamFlags,
PaStreamCallback *streamCallback, void *userData)
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(
Pa_OpenStream(
stream, inputParameters, outputParameters, sampleRate,
framesPerBuffer, streamFlags, streamCallback, userData));
});
return fut;
}
std::future<PaError> PortAudioInterface::StartStream(PaStream *stream)
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_StartStream(stream));
});
return fut;
}
std::future<PaError> PortAudioInterface::StopStream(PaStream *stream)
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_StopStream(stream));
});
return fut;
}
std::future<PaError> PortAudioInterface::CloseStream(PaStream *stream)
{
std::shared_ptr<std::promise<PaError> > prom = std::make_shared<std::promise<PaError> >();
auto fut = prom->get_future();
enqueue_([=]() {
prom->set_value(Pa_CloseStream(stream));
});
return fut;
}

View File

@ -0,0 +1,56 @@
//=========================================================================
// Name: PortAudioInterface.h
// Purpose: Wrapper to enforce thread safety around PortAudio.
//
// 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 <http://www.gnu.org/licenses/>.
//
//=========================================================================
#ifndef PORT_AUDIO_INTERFACE_H
#define PORT_AUDIO_INTERFACE_H
#include <future>
#include "portaudio.h"
#include "../util/ThreadedObject.h"
class PortAudioInterface : public ThreadedObject
{
public:
PortAudioInterface() = default;
virtual ~PortAudioInterface() = default;
std::future<PaError> Initialize();
std::future<PaError> Terminate();
std::future<const char*> GetErrorText(PaError error);
std::future<const PaHostErrorInfo*> GetLastHostErrorInfo();
std::future<PaDeviceIndex> GetDeviceCount();
std::future<const PaDeviceInfo*> GetDeviceInfo(PaDeviceIndex device);
std::future<const PaHostApiInfo*> GetHostApiInfo(PaHostApiIndex hostApi);
std::future<PaError> IsFormatSupported(
const PaStreamParameters* inputParameters,
const PaStreamParameters* outputParameters,
double sampleRate
);
std::future<PaDeviceIndex> GetDefaultInputDevice(void);
std::future<PaDeviceIndex> GetDefaultOutputDevice(void);
std::future<PaError> OpenStream(PaStream **stream, const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData);
std::future<PaError> StartStream(PaStream *stream);
std::future<PaError> StopStream(PaStream *stream);
std::future<PaError> CloseStream(PaStream *stream);
};
#endif // PORT_AUDIO_INTERFACE_H

View File

@ -24,6 +24,7 @@
#include "../defines.h"
#include "FreeDVConfiguration.h"
#include "../freedv_interface.h"
FreeDVConfiguration::FreeDVConfiguration()
/* First time configuration options */
@ -99,7 +100,7 @@ FreeDVConfiguration::FreeDVConfiguration()
, waterfallColor("/Waterfall/Color", 0)
, statsResetTimeSecs("/Stats/ResetTime", 10)
, currentFreeDVMode("/Audio/mode", 4)
, currentFreeDVMode("/Audio/mode", FREEDV_MODE_RADE)
, currentSpectrumAveraging("/Plot/Spectrum/CurrentAveraging", 0)

View File

@ -32,6 +32,7 @@
#include <chrono>
#include <queue>
#include <future>
#include <atomic>
// Codec2 required include files.
#include "codec2.h"
@ -195,7 +196,7 @@ private:
FARGANState fargan_;
LPCNetEncState *lpcnetEncState_;
RADETransmitStep *radeTxStep_;
int sync_;
std::atomic<int> sync_;
rade_text_t radeTextPtr_;
int preProcessRxFn_(ParallelStep* ps);

View File

@ -383,9 +383,9 @@ void MainApp::UnitTest_()
}
else
{
// Receive for 60 seconds
// Receive for txtime seconds
auto sync = 0;
for (int i = 0; i < 60*10; i++)
for (int i = 0; i < utTxTimeSeconds*10; i++)
{
std::this_thread::sleep_for(100ms);
auto newSync = freedvInterface.getSync();
@ -863,38 +863,46 @@ void MainFrame::loadConfiguration_()
int mode = wxGetApp().appConfiguration.currentFreeDVMode;
setDefaultMode:
if (mode == 0)
m_rb1600->SetValue(1);
if (mode == 3)
m_rb700c->SetValue(1);
if (mode == 4)
m_rb700d->SetValue(1);
if (mode == 5)
m_rb700e->SetValue(1);
if (mode == 6)
m_rb800xa->SetValue(1);
// mode 7 was the former 2400B mode, now removed.
if ((mode == 9) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported)
m_rb2020->SetValue(1);
else if (mode == 9)
{
// Default to 700D otherwise
mode = defaultMode;
goto setDefaultMode;
m_rb1600->SetValue(1);
}
if (mode == FREEDV_MODE_RADE)
else if (mode == 3)
{
m_rb700c->SetValue(1);
}
else if (mode == 4)
{
m_rb700d->SetValue(1);
}
else if (mode == 5)
{
m_rb700e->SetValue(1);
}
else if (mode == 6)
{
m_rb800xa->SetValue(1);
}
// mode 7 was the former 2400B mode, now removed.
else if ((mode == 9) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported)
{
m_rb2020->SetValue(1);
}
else if (mode == FREEDV_MODE_RADE)
{
m_rbRADE->SetValue(1);
}
#if defined(FREEDV_MODE_2020B)
if ((mode == 10) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported)
m_rb2020b->SetValue(1);
else if (mode == 10)
else if ((mode == 10) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported)
{
// Default to 700D otherwise
m_rb2020b->SetValue(1);
}
#endif // defined(FREEDV_MODE_2020B)
else
{
// Default to RADE otherwise
mode = defaultMode;
goto setDefaultMode;
}
#endif // defined(FREEDV_MODE_2020B)
pConfig->SetPath(wxT("/"));
// Set initial state of additional modes.
@ -3306,35 +3314,27 @@ void MainFrame::startRxStream()
short outdata[size];
int result = codec2_fifo_read(cbData->outfifo1, outdata, size);
if (result == 0) {
// write signal to all channels if the device can support 2+ channels.
// Otherwise, we assume we're only dealing with one channel and write
// only to that channel.
if (dev.getNumChannels() >= 2)
if (result == 0)
{
// write signal to all channels to start. This is so that
// the compiler can optimize for the most common case.
for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels())
{
for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels())
for (auto j = 0; j < dev.getNumChannels(); j++)
{
if (cbData->leftChannelVoxTone)
{
cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate;
cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI));
audioData[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase);
}
else
audioData[0] = outdata[i];
for (auto j = 1; j < dev.getNumChannels(); j++)
{
audioData[j] = outdata[i];
}
audioData[j] = outdata[i];
}
}
else
// If VOX tone is enabled, go back through and add the VOX tone
// on the left channel.
if (cbData->leftChannelVoxTone)
{
for(size_t i = 0; i < size; i++, audioData++)
for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels())
{
audioData[0] = outdata[i];
cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate;
cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI));
audioData[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase);
}
}
}

View File

@ -492,7 +492,7 @@ void* TxRxThread::Entry()
if (m_tx) txProcessing_();
else rxProcessing_();
std::this_thread::sleep_until(currentTime + 20ms);
std::this_thread::sleep_until(currentTime + 10ms);
}
// Force pipeline to delete itself when we're done with the thread.

View File

@ -88,7 +88,7 @@ function Test-FreeDV {
$psi.FileName = "$current_loc\freedv.exe"
$psi.WorkingDirectory = $current_loc
$quoted_tmp_filename = "`"" + $tmp_file.FullName + "`""
$psi.Arguments = @("/f $quoted_tmp_filename /ut txrx /utmode $ModeToTest")
$psi.Arguments = @("/f $quoted_tmp_filename /ut txrx /utmode $ModeToTest /txtime 60")
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
@ -101,7 +101,7 @@ function Test-FreeDV {
Write-Host "$err_output"
$syncs = $err_output.Split([Environment]::NewLine) | Where { $_.Contains("Sync changed") }
$syncs = ($err_output -split "`r?`n") | Where { $_.Contains("Sync changed") }
if ($syncs.Count -eq 1) {
return $true
}