From 7575c630b8efe5a4acebbe66ec20605f5f1db2e7 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 6 Mar 2025 00:40:05 -0800 Subject: [PATCH] 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 cd025e9a8e08ec9579af10c9dcb187e1ce9568ff. * Revert "Revert 10ms max wait." This reverts commit 0227ce5f0cb546cdd3882bd96c8e3bb698e68a59. * 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. --- .github/workflows/cmake-macos.yml | 20 ++-- CMakeLists.txt | 16 ++- USER_MANUAL.md | 1 + build_macos_sound_drivers.sh | 20 ---- src/audio/CMakeLists.txt | 1 + src/audio/PortAudioDevice.cpp | 59 +++++++--- src/audio/PortAudioDevice.h | 4 +- src/audio/PortAudioEngine.cpp | 34 +++--- src/audio/PortAudioEngine.h | 3 + src/audio/PortAudioInterface.cpp | 175 +++++++++++++++++++++++++++++ src/audio/PortAudioInterface.h | 56 +++++++++ src/config/FreeDVConfiguration.cpp | 3 +- src/freedv_interface.h | 3 +- src/main.cpp | 94 ++++++++-------- src/pipeline/TxRxThread.cpp | 2 +- test/TestFreeDVFullDuplex.ps1 | 4 +- 16 files changed, 377 insertions(+), 118 deletions(-) create mode 100644 src/audio/PortAudioInterface.cpp create mode 100644 src/audio/PortAudioInterface.h diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index d6e8ed70..a96fafa3 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 87d2d5d6..49c4bc2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 28c4cadf..b276e4b0 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -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) diff --git a/build_macos_sound_drivers.sh b/build_macos_sound_drivers.sh index c93b8edf..e9cf7887 100755 --- a/build_macos_sound_drivers.sh +++ b/build_macos_sound_drivers.sh @@ -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 diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index bc90f727..5f0c18cc 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -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) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index efd6628b..c1f91231 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -25,20 +25,24 @@ #include #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 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; } } diff --git a/src/audio/PortAudioDevice.h b/src/audio/PortAudioDevice.h index 4d1c722e..199f81fe 100644 --- a/src/audio/PortAudioDevice.h +++ b/src/audio/PortAudioDevice.h @@ -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 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 portAudioLibrary_; static int OnPortAudioStreamCallback_(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData); }; diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index f5b71038..1783e9af 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -29,7 +29,7 @@ PortAudioEngine::PortAudioEngine() : initialized_(false) { - // empty + portAudioLibrary_ = std::make_shared(); } 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 PortAudioEngine::getAudioDeviceList(AudioDirection direction) { - int numDevices = Pa_GetDeviceCount(); + int numDevices = portAudioLibrary_->GetDeviceCount().get(); std::vector 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 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 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 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 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 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(devObj); } } diff --git a/src/audio/PortAudioEngine.h b/src/audio/PortAudioEngine.h index 56a6c121..b6d12c05 100644 --- a/src/audio/PortAudioEngine.h +++ b/src/audio/PortAudioEngine.h @@ -23,7 +23,9 @@ #ifndef PORT_AUDIO_ENGINE_H #define PORT_AUDIO_ENGINE_H +#include #include "IAudioEngine.h" +#include "PortAudioInterface.h" class PortAudioEngine : public IAudioEngine { @@ -40,6 +42,7 @@ public: private: bool initialized_; + std::shared_ptr portAudioLibrary_; static bool IsDeviceWhitelisted_(const char* devName); }; diff --git a/src/audio/PortAudioInterface.cpp b/src/audio/PortAudioInterface.cpp new file mode 100644 index 00000000..33c8a856 --- /dev/null +++ b/src/audio/PortAudioInterface.cpp @@ -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 . +// +//========================================================================= + +#include +#include "PortAudioInterface.h" + +std::future PortAudioInterface::Initialize() +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_Initialize()); + }); + return fut; +} + +std::future PortAudioInterface::Terminate() +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_Terminate()); + }); + return fut; +} + +std::future PortAudioInterface::GetErrorText(PaError error) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetErrorText(error)); + }); + return fut; +} + +std::future PortAudioInterface::GetLastHostErrorInfo() +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetLastHostErrorInfo()); + }); + return fut; +} + +std::future PortAudioInterface::GetDeviceCount() +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetDeviceCount()); + }); + return fut; +} + +std::future PortAudioInterface::GetDeviceInfo(PaDeviceIndex device) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetDeviceInfo(device)); + }); + return fut; +} + +std::future PortAudioInterface::GetHostApiInfo(PaHostApiIndex hostApi) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetHostApiInfo(hostApi)); + }); + return fut; +} + +std::future PortAudioInterface::IsFormatSupported( + const PaStreamParameters* inputParameters, + const PaStreamParameters* outputParameters, + double sampleRate +) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate)); + }); + return fut; +} + +std::future PortAudioInterface::GetDefaultInputDevice(void) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetDefaultInputDevice()); + }); + return fut; +} + +std::future PortAudioInterface::GetDefaultOutputDevice(void) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_GetDefaultOutputDevice()); + }); + return fut; +} + +std::future PortAudioInterface::OpenStream( + PaStream **stream, const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, double sampleRate, + unsigned long framesPerBuffer, PaStreamFlags streamFlags, + PaStreamCallback *streamCallback, void *userData) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value( + Pa_OpenStream( + stream, inputParameters, outputParameters, sampleRate, + framesPerBuffer, streamFlags, streamCallback, userData)); + }); + return fut; +} + +std::future PortAudioInterface::StartStream(PaStream *stream) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_StartStream(stream)); + }); + return fut; +} + +std::future PortAudioInterface::StopStream(PaStream *stream) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_StopStream(stream)); + }); + return fut; +} + +std::future PortAudioInterface::CloseStream(PaStream *stream) +{ + std::shared_ptr > prom = std::make_shared >(); + auto fut = prom->get_future(); + enqueue_([=]() { + prom->set_value(Pa_CloseStream(stream)); + }); + return fut; +} \ No newline at end of file diff --git a/src/audio/PortAudioInterface.h b/src/audio/PortAudioInterface.h new file mode 100644 index 00000000..4bc3ca55 --- /dev/null +++ b/src/audio/PortAudioInterface.h @@ -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 . +// +//========================================================================= + +#ifndef PORT_AUDIO_INTERFACE_H +#define PORT_AUDIO_INTERFACE_H + +#include +#include "portaudio.h" +#include "../util/ThreadedObject.h" + +class PortAudioInterface : public ThreadedObject +{ +public: + PortAudioInterface() = default; + virtual ~PortAudioInterface() = default; + + std::future Initialize(); + std::future Terminate(); + std::future GetErrorText(PaError error); + std::future GetLastHostErrorInfo(); + std::future GetDeviceCount(); + std::future GetDeviceInfo(PaDeviceIndex device); + std::future GetHostApiInfo(PaHostApiIndex hostApi); + std::future IsFormatSupported( + const PaStreamParameters* inputParameters, + const PaStreamParameters* outputParameters, + double sampleRate + ); + std::future GetDefaultInputDevice(void); + std::future GetDefaultOutputDevice(void); + std::future OpenStream(PaStream **stream, const PaStreamParameters *inputParameters, const PaStreamParameters *outputParameters, double sampleRate, unsigned long framesPerBuffer, PaStreamFlags streamFlags, PaStreamCallback *streamCallback, void *userData); + std::future StartStream(PaStream *stream); + std::future StopStream(PaStream *stream); + std::future CloseStream(PaStream *stream); +}; + +#endif // PORT_AUDIO_INTERFACE_H \ No newline at end of file diff --git a/src/config/FreeDVConfiguration.cpp b/src/config/FreeDVConfiguration.cpp index c7420646..3d50d5e3 100644 --- a/src/config/FreeDVConfiguration.cpp +++ b/src/config/FreeDVConfiguration.cpp @@ -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) diff --git a/src/freedv_interface.h b/src/freedv_interface.h index c41b839c..bb1b26c0 100644 --- a/src/freedv_interface.h +++ b/src/freedv_interface.h @@ -32,6 +32,7 @@ #include #include #include +#include // Codec2 required include files. #include "codec2.h" @@ -195,7 +196,7 @@ private: FARGANState fargan_; LPCNetEncState *lpcnetEncState_; RADETransmitStep *radeTxStep_; - int sync_; + std::atomic sync_; rade_text_t radeTextPtr_; int preProcessRxFn_(ParallelStep* ps); diff --git a/src/main.cpp b/src/main.cpp index ac07a278..144b945c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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); } } } diff --git a/src/pipeline/TxRxThread.cpp b/src/pipeline/TxRxThread.cpp index ce920eae..790e582c 100644 --- a/src/pipeline/TxRxThread.cpp +++ b/src/pipeline/TxRxThread.cpp @@ -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. diff --git a/test/TestFreeDVFullDuplex.ps1 b/test/TestFreeDVFullDuplex.ps1 index a74d7542..1e960804 100644 --- a/test/TestFreeDVFullDuplex.ps1 +++ b/test/TestFreeDVFullDuplex.ps1 @@ -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 }