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 commitms-embed-sioclientcd025e9a8e
. * Revert "Revert 10ms max wait." This reverts commit0227ce5f0c
. * 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.
parent
130aeec504
commit
7575c630b8
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
94
src/main.cpp
94
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue