From 80177ff278a128b9e6b421c908285560220e9cfd Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 18 Dec 2021 22:54:55 -0800 Subject: [PATCH 01/89] WIP for refactoring of PortAudio specific code. --- src/CMakeLists.txt | 7 +- src/audio/AudioDeviceSpecification.cpp | 37 +++++++ src/audio/AudioDeviceSpecification.h | 42 ++++++++ src/audio/CMakeLists.txt | 5 + src/audio/IAudioDevice.h | 37 +++++++ src/audio/IAudioEngine.cpp | 34 ++++++ src/audio/IAudioEngine.h | 47 +++++++++ src/audio/PortAudioEngine.cpp | 137 +++++++++++++++++++++++++ src/audio/PortAudioEngine.h | 44 ++++++++ 9 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 src/audio/AudioDeviceSpecification.cpp create mode 100644 src/audio/AudioDeviceSpecification.h create mode 100644 src/audio/CMakeLists.txt create mode 100644 src/audio/IAudioDevice.h create mode 100644 src/audio/IAudioEngine.cpp create mode 100644 src/audio/IAudioEngine.h create mode 100644 src/audio/PortAudioEngine.cpp create mode 100644 src/audio/PortAudioEngine.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f8168a2e..9709b0ba2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -67,6 +67,9 @@ set(FREEDV_LINK_LIBS_OSX "-framework AVFoundation" ) +# Compile FreeDV components +add_subdirectory(audio) + # WIN32 is needed for Windows GUI apps and is ignored for UNIX like systems. # In addition, there are some required OSX-specific code files for platform specific handling. if(APPLE) @@ -90,9 +93,9 @@ endif(APPLE) # Link imported or build tree targets. if(APPLE) -target_link_libraries(FreeDV codec2 lpcnetfreedv) +target_link_libraries(FreeDV codec2 lpcnetfreedv fdv_audio) else(APPLE) -target_link_libraries(freedv codec2 lpcnetfreedv) +target_link_libraries(freedv codec2 lpcnetfreedv fdv_audio) endif(APPLE) # Add build dependencies for interally built external libraries. diff --git a/src/audio/AudioDeviceSpecification.cpp b/src/audio/AudioDeviceSpecification.cpp new file mode 100644 index 000000000..d615e2337 --- /dev/null +++ b/src/audio/AudioDeviceSpecification.cpp @@ -0,0 +1,37 @@ +//========================================================================= +// Name: AudioDeviceSpecification.cpp +// Purpose: Describes an audio device on the user's system. +// +// 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 "AudioDeviceSpecification.h" + +bool AudioDeviceSpecification::isValid() const +{ + return deviceId != -1; +} + +AudioDeviceSpecification AudioDeviceSpecification::GetInvalidDevice() +{ + AudioDeviceSpecification result = { + .deviceId = -1 + }; + + return result; +} \ No newline at end of file diff --git a/src/audio/AudioDeviceSpecification.h b/src/audio/AudioDeviceSpecification.h new file mode 100644 index 000000000..d0299a5c7 --- /dev/null +++ b/src/audio/AudioDeviceSpecification.h @@ -0,0 +1,42 @@ +//========================================================================= +// Name: AudioDeviceSpecification.h +// Purpose: Describes an audio device on the user's system. +// +// Authors: Mooneer Salem +// License: +// +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see . +// +//========================================================================= + +#ifndef AUDIO_DEVICE_SPECIFICATION_H +#define AUDIO_DEVICE_SPECIFICATION_H + +#include +#include + +struct AudioDeviceSpecification +{ + int deviceId; + std::string name; + std::string apiName; + int defaultSampleRate; + int maxChannels; + std::vector supportedSampleRates; + + bool isValid() const; + static AudioDeviceSpecification GetInvalidDevice(); +}; + +#endif // AUDIO_DEVICE_SPECIFICATION_H \ No newline at end of file diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt new file mode 100644 index 000000000..9835053ee --- /dev/null +++ b/src/audio/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(fdv_audio STATIC + AudioDeviceSpecification.cpp + IAudioEngine.cpp + PortAudioEngine.cpp +) \ No newline at end of file diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h new file mode 100644 index 000000000..febd19492 --- /dev/null +++ b/src/audio/IAudioDevice.h @@ -0,0 +1,37 @@ +//========================================================================= +// Name: IAudioDevice.h +// Purpose: Defines the interface to an audio device. +// +// 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 I_AUDIO_DEVICE_H +#define I_AUDIO_DEVICE_H + +#include +#include +#include "AudioDeviceSpecification.h" + +class IAudioDevice +{ +public: + virtual void start() = 0; + virtual void stop() = 0; +}; + +#endif // AUDIO_ENGINE_H \ No newline at end of file diff --git a/src/audio/IAudioEngine.cpp b/src/audio/IAudioEngine.cpp new file mode 100644 index 000000000..adb65e3cf --- /dev/null +++ b/src/audio/IAudioEngine.cpp @@ -0,0 +1,34 @@ +//========================================================================= +// Name: IAudioEngine.cpp +// Purpose: Defines the main interface to the selected audio engine. +// +// 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 "IAudioEngine.h" + +int IAudioEngine::StandardSampleRates[] = +{ + 8000, 9600, + 11025, 12000, + 16000, 22050, + 24000, 32000, + 44100, 48000, + 88200, 96000, + 192000, -1 // negative terminated list +}; \ No newline at end of file diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h new file mode 100644 index 000000000..0588eb302 --- /dev/null +++ b/src/audio/IAudioEngine.h @@ -0,0 +1,47 @@ +//========================================================================= +// Name: IAudioEngine.h +// Purpose: Defines the main interface to the selected audio engine. +// +// 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 I_AUDIO_ENGINE_H +#define I_AUDIO_ENGINE_H + +#include +#include +#include "AudioDeviceSpecification.h" + +class IAudioDevice; + +class IAudioEngine +{ +public: + enum AudioDirection { IN, OUT }; + + virtual void start() = 0; + virtual void stop() = 0; + virtual std::vector getAudioDeviceList(AudioDirection direction) = 0; + virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction) = 0; + virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) = 0; + +protected: + static int StandardSampleRates[]; +}; + +#endif // AUDIO_ENGINE_H \ No newline at end of file diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp new file mode 100644 index 000000000..da9229114 --- /dev/null +++ b/src/audio/PortAudioEngine.cpp @@ -0,0 +1,137 @@ +//========================================================================= +// Name: PortAudioEngine.cpp +// Purpose: Defines the interface to the PortAudio audio engine. +// +// 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 "portaudio.h" +#include "PortAudioEngine.h" + +PortAudioEngine::PortAudioEngine() + : initialized_(false) +{ + // empty +} + +PortAudioEngine::~PortAudioEngine() +{ + if (initialized_) + { + stop(); + } +} + +void PortAudioEngine::start() +{ + Pa_Initialize(); + initialized_ = true; +} + +void PortAudioEngine::stop() +{ + Pa_Terminate(); + initialized_ = false; +} + +std::vector PortAudioEngine::getAudioDeviceList(AudioDirection direction) +{ + int numDevices = Pa_GetDeviceCount(); + std::vector result; + + for (int index = 0; index < numDevices; index++) + { + const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(index); + + std::string hostApiName = Pa_GetHostApiInfo(deviceInfo->hostApi)->name; + if (hostApiName.find("DirectSound") != std::string::npos) + { + // Skip DirectSound devices due to poor real-time performance. + continue; + } + + if ((direction == IN && deviceInfo->maxInputChannels > 0) || + (direction == OUT && deviceInfo->maxOutputChannels > 0)) + { + PaStreamParameters streamParameters; + + streamParameters.device = index; + streamParameters.channelCount = 1; + streamParameters.sampleFormat = paInt16; + streamParameters.suggestedLatency = 0; + streamParameters.hostApiSpecificStreamInfo = NULL; + + AudioDeviceSpecification device; + device.deviceId = index; + device.name = deviceInfo->name; + device.apiName = hostApiName; + device.maxChannels = + direction == IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + device.defaultSampleRate = deviceInfo->defaultSampleRate; + + int rateIndex = 0; + while (IAudioEngine::StandardSampleRates[rateIndex] != -1) + { + PaError err = Pa_IsFormatSupported( + direction == IN ? &streamParameters : NULL, + direction == OUT ? &streamParameters : NULL, + IAudioEngine::StandardSampleRates[rateIndex]); + + if (err == paFormatIsSupported) + { + device.supportedSampleRates.push_back(IAudioEngine::StandardSampleRates[rateIndex]); + } + + rateIndex++; + } + + if (device.supportedSampleRates.size() > 0) + { + result.push_back(device); + } + } + } + + return result; +} + +AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection direction) +{ + auto devices = getAudioDeviceList(direction); + PaDeviceIndex defaultDeviceIndex = + direction == IN ? Pa_GetDefaultInputDevice() : Pa_GetDefaultOutputDevice(); + + if (defaultDeviceIndex != paNoDevice) + { + for (auto& device : devices) + { + if (device.deviceId == defaultDeviceIndex) + { + return device; + } + } + } + + return AudioDeviceSpecification::GetInvalidDevice(); +} + +std::shared_ptr PortAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) +{ + // TBD + return nullptr; +} \ No newline at end of file diff --git a/src/audio/PortAudioEngine.h b/src/audio/PortAudioEngine.h new file mode 100644 index 000000000..b2965e1c1 --- /dev/null +++ b/src/audio/PortAudioEngine.h @@ -0,0 +1,44 @@ +//========================================================================= +// Name: PortAudioEngine.h +// Purpose: Defines the interface to the PortAudio audio engine. +// +// 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_ENGINE_H +#define PORT_AUDIO_ENGINE_H + +#include "IAudioEngine.h" + +class PortAudioEngine : public IAudioEngine +{ +public: + PortAudioEngine(); + virtual ~PortAudioEngine(); + + virtual void start(); + virtual void stop(); + virtual std::vector getAudioDeviceList(AudioDirection direction); + virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction); + virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels); + +private: + bool initialized_; +}; + +#endif // PORT_AUDIO_ENGINE_H \ No newline at end of file From 45d3193a2a3791a15bc2f78574588c4c0ca03348 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 19 Dec 2021 01:16:14 -0800 Subject: [PATCH 02/89] Add callback handlers for events the main code will need to handle. --- src/audio/CMakeLists.txt | 1 + src/audio/IAudioDevice.cpp | 41 ++++++++++++++++++++++++++++++++++++++ src/audio/IAudioDevice.h | 36 ++++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/audio/IAudioDevice.cpp diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 9835053ee..c14c1965b 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(fdv_audio STATIC AudioDeviceSpecification.cpp + IAudioDevice.cpp IAudioEngine.cpp PortAudioEngine.cpp ) \ No newline at end of file diff --git a/src/audio/IAudioDevice.cpp b/src/audio/IAudioDevice.cpp new file mode 100644 index 000000000..c665b170e --- /dev/null +++ b/src/audio/IAudioDevice.cpp @@ -0,0 +1,41 @@ +//========================================================================= +// Name: IAudioDevice.cpp +// Purpose: Defines the interface to an audio device. +// +// 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 "IAudioDevice.h" + +void IAudioDevice::setOnAudioData(AudioDataCallbackFn fn, void* state) +{ + onAudioDataFunction = fn; + onAudioDataState = state; +} + +void IAudioDevice::setOnAudioOverflow(AudioOverflowCallbackFn fn, void* state) +{ + onAudioOverflowFunction = fn; + onAudioOverflowState = state; +} + +void IAudioDevice::setOnAudioUnderflow(AudioOverflowCallbackFn fn, void* state) +{ + onAudioUnderflowFunction = fn; + onAudioUnderflowState = state; +} \ No newline at end of file diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index febd19492..4fd553024 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -30,8 +30,42 @@ class IAudioDevice { public: + typedef std::function AudioDataCallbackFn; + typedef std::function AudioUnderflowCallbackFn; + typedef std::function AudioOverflowCallbackFn; + virtual void start() = 0; virtual void stop() = 0; + + // Set RX/TX ready callback. + // Callback must take the following parameters: + // 1. Audio device. + // 2. Pointer to buffer containing RX/TX data. + // 3. Size of buffer. + // 4. Pointer to user-provided state object (typically onAudioDataState, defined below). + void setOnAudioData(AudioDataCallbackFn fn, void* state); + + // Set overflow callback. + // Callback must take the following parameters: + // 1. Audio device. + // 2. Pointer to user-provided state object (typically onAudioOverflowState, defined below). + void setOnAudioOverflow(AudioOverflowCallbackFn fn, void* state); + + // Set underflow callback. + // Callback must take the following parameters: + // 1. Audio device. + // 2. Pointer to user-provided state object (typically onAudioUnderflowState, defined below). + void setOnAudioUnderflow(AudioOverflowCallbackFn fn, void* state); + +protected: + AudioDataCallbackFn onAudioDataFunction; + void* onAudioDataState; + + AudioOverflowCallbackFn onAudioOverflowFunction; + void* onAudioOverflowState; + + AudioUnderflowCallbackFn onAudioUnderflowFunction; + void* onAudioUnderflowState; }; -#endif // AUDIO_ENGINE_H \ No newline at end of file +#endif // I_AUDIO_DEVICE_H \ No newline at end of file From 645313da601e49f41a70df261c973496ef5b055a Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 19 Dec 2021 11:34:58 -0800 Subject: [PATCH 03/89] Add PortAudio device implementation. --- src/audio/CMakeLists.txt | 1 + src/audio/IAudioDevice.h | 1 + src/audio/PortAudioDevice.cpp | 119 ++++++++++++++++++++++++++++++++++ src/audio/PortAudioDevice.h | 54 +++++++++++++++ src/audio/PortAudioEngine.cpp | 13 +++- 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/audio/PortAudioDevice.cpp create mode 100644 src/audio/PortAudioDevice.h diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index c14c1965b..4f83826ac 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -2,5 +2,6 @@ add_library(fdv_audio STATIC AudioDeviceSpecification.cpp IAudioDevice.cpp IAudioEngine.cpp + PortAudioDevice.cpp PortAudioEngine.cpp ) \ No newline at end of file diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index 4fd553024..4019ce4ea 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -25,6 +25,7 @@ #include #include +#include #include "AudioDeviceSpecification.h" class IAudioDevice diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp new file mode 100644 index 000000000..35be1acba --- /dev/null +++ b/src/audio/PortAudioDevice.cpp @@ -0,0 +1,119 @@ +//========================================================================= +// Name: PortAudioDevice.cpp +// Purpose: Defines the interface to a PortAudio device. +// +// 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 "PortAudioDevice.h" +#include "portaudio.h" + +PortAudioDevice::PortAudioDevice(int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) + : deviceId_(deviceId) + , direction_(direction) + , sampleRate_(sampleRate) + , numChannels_(numChannels) + , deviceStream_(nullptr) +{ + // empty +} + +PortAudioDevice::~PortAudioDevice() +{ + if (deviceStream_ != nullptr) + { + stop(); + } +} + +void PortAudioDevice::start() +{ + PaStreamParameters streamParameters; + + streamParameters.device = deviceId_; + streamParameters.channelCount = numChannels_; + streamParameters.sampleFormat = paInt16; + streamParameters.suggestedLatency = 0; + streamParameters.hostApiSpecificStreamInfo = NULL; + + auto error = Pa_OpenStream( + &deviceStream_, + direction_ == IAudioEngine::IN ? &streamParameters : nullptr, + direction_ == IAudioEngine::OUT ? &streamParameters : nullptr, + sampleRate_, + 0, + paClipOff, + &OnPortAudioStreamCallback_, + this + ); + + if (error == paNoError) + { + error = Pa_StartStream(deviceStream_); + if (error != paNoError) + { + Pa_CloseStream(deviceStream_); + deviceStream_ = nullptr; + } + } + else + { + deviceStream_ = nullptr; + } +} + +void PortAudioDevice::stop() +{ + if (deviceStream_ != nullptr) + { + Pa_StopStream(deviceStream_); + Pa_CloseStream(deviceStream_); + deviceStream_ = nullptr; + } +} + +int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) +{ + PortAudioDevice* thisObj = static_cast(userData); + + if (thisObj->onAudioUnderflowFunction && statusFlags & 0x1) + { + // underflow + thisObj->onAudioUnderflowFunction(*thisObj, thisObj->onAudioUnderflowState); + } + + if (thisObj->onAudioOverflowFunction && statusFlags & 0x2) + { + // overflow + thisObj->onAudioOverflowFunction(*thisObj, thisObj->onAudioOverflowState); + } + + if (thisObj->onAudioDataFunction) + { + if (thisObj->direction_ == IAudioEngine::IN) + { + thisObj->onAudioDataFunction(*thisObj, const_cast(input), frameCount, thisObj->onAudioDataState); + } + else + { + thisObj->onAudioDataFunction(*thisObj, const_cast(output), frameCount, thisObj->onAudioDataState); + } + } + + return paContinue; +} \ No newline at end of file diff --git a/src/audio/PortAudioDevice.h b/src/audio/PortAudioDevice.h new file mode 100644 index 000000000..e1c265fff --- /dev/null +++ b/src/audio/PortAudioDevice.h @@ -0,0 +1,54 @@ +//========================================================================= +// Name: PortAudioDevice.h +// Purpose: Defines the interface to a PortAudio device. +// +// 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_DEVICE_H +#define PORT_AUDIO_DEVICE_H + +#include "portaudio.h" +#include "IAudioEngine.h" +#include "IAudioDevice.h" + +class PortAudioDevice : public IAudioDevice +{ +public: + virtual ~PortAudioDevice(); + + virtual void start(); + virtual void stop(); + +protected: + // PortAudioDevice cannot be created directly, only via PortAudioEngine. + friend class PortAudioEngine; + + PortAudioDevice(int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); + +private: + int deviceId_; + IAudioEngine::AudioDirection direction_; + int sampleRate_; + int numChannels_; + PaStream* deviceStream_; + + static int OnPortAudioStreamCallback_(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData); +}; + +#endif // PORT_AUDIO_DEVICE_H \ No newline at end of file diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index da9229114..0d9b241fd 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -21,6 +21,7 @@ //========================================================================= #include "portaudio.h" +#include "PortAudioDevice.h" #include "PortAudioEngine.h" PortAudioEngine::PortAudioEngine() @@ -132,6 +133,16 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d std::shared_ptr PortAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) { - // TBD + auto deviceList = getAudioDeviceList(direction); + + for (auto& dev : deviceList) + { + if (dev.name == deviceName) + { + auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, numChannels); + return std::shared_ptr(devObj); + } + } + return nullptr; } \ No newline at end of file From b77b126b6f93e737a9232511d4ab2e1c7fa2ba69 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 19 Dec 2021 11:44:19 -0800 Subject: [PATCH 04/89] Add support for getting errors back from PortAudio. --- src/audio/IAudioDevice.cpp | 6 +++++ src/audio/IAudioDevice.h | 11 ++++++++++ src/audio/PortAudioDevice.cpp | 41 ++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/audio/IAudioDevice.cpp b/src/audio/IAudioDevice.cpp index c665b170e..fd71ac13e 100644 --- a/src/audio/IAudioDevice.cpp +++ b/src/audio/IAudioDevice.cpp @@ -38,4 +38,10 @@ void IAudioDevice::setOnAudioUnderflow(AudioOverflowCallbackFn fn, void* state) { onAudioUnderflowFunction = fn; onAudioUnderflowState = state; +} + +void IAudioDevice::setOnAudioError(AudioErrorCallbackFn fn, void* state) +{ + onAudioErrorFunction = fn; + onAudioErrorState = state; } \ No newline at end of file diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index 4019ce4ea..7b1e5d267 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -34,6 +34,7 @@ class IAudioDevice typedef std::function AudioDataCallbackFn; typedef std::function AudioUnderflowCallbackFn; typedef std::function AudioOverflowCallbackFn; + typedef std::function AudioErrorCallbackFn; virtual void start() = 0; virtual void stop() = 0; @@ -58,6 +59,13 @@ class IAudioDevice // 2. Pointer to user-provided state object (typically onAudioUnderflowState, defined below). void setOnAudioUnderflow(AudioOverflowCallbackFn fn, void* state); + // Set error callback. + // Callback must take the following parameters: + // 1. Audio device. + // 2. String representing the error encountered. + // 3. Pointer to user-provided state object (typically onAudioUnderflowState, defined below). + void setOnAudioError(AudioErrorCallbackFn fn, void* state); + protected: AudioDataCallbackFn onAudioDataFunction; void* onAudioDataState; @@ -67,6 +75,9 @@ class IAudioDevice AudioUnderflowCallbackFn onAudioUnderflowFunction; void* onAudioUnderflowState; + + AudioErrorCallbackFn onAudioErrorFunction; + void* onAudioErrorState; }; #endif // I_AUDIO_DEVICE_H \ No newline at end of file diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 35be1acba..60002b9fc 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -67,12 +67,21 @@ void PortAudioDevice::start() error = Pa_StartStream(deviceStream_); if (error != paNoError) { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, Pa_GetErrorText(error), onAudioErrorState); + } + Pa_CloseStream(deviceStream_); deviceStream_ = nullptr; } } else { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, Pa_GetErrorText(error), onAudioErrorState); + } deviceStream_ = nullptr; } } @@ -91,28 +100,40 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, { PortAudioDevice* thisObj = static_cast(userData); - if (thisObj->onAudioUnderflowFunction && statusFlags & 0x1) + unsigned int overflowFlag = 0; + unsigned int underflowFlag = 0; + + if (thisObj->direction_ == IAudioEngine::IN) + { + underflowFlag = 0x1; + overflowFlag = 0x2; + } + else + { + underflowFlag = 0x4; + overflowFlag = 0x8; + } + + if (thisObj->onAudioUnderflowFunction && statusFlags & underflowFlag) { // underflow thisObj->onAudioUnderflowFunction(*thisObj, thisObj->onAudioUnderflowState); } - if (thisObj->onAudioOverflowFunction && statusFlags & 0x2) + if (thisObj->onAudioOverflowFunction && statusFlags & overflowFlag) { // overflow thisObj->onAudioOverflowFunction(*thisObj, thisObj->onAudioOverflowState); } + void* dataPtr = + thisObj->direction_ == IAudioEngine::IN ? + const_cast(input) : + const_cast(output); + if (thisObj->onAudioDataFunction) { - if (thisObj->direction_ == IAudioEngine::IN) - { - thisObj->onAudioDataFunction(*thisObj, const_cast(input), frameCount, thisObj->onAudioDataState); - } - else - { - thisObj->onAudioDataFunction(*thisObj, const_cast(output), frameCount, thisObj->onAudioDataState); - } + thisObj->onAudioDataFunction(*thisObj, dataPtr, frameCount, thisObj->onAudioDataState); } return paContinue; From ab169d999db0bbfed80c870189e5411e5f2078a9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 19 Dec 2021 23:38:38 -0800 Subject: [PATCH 05/89] Add error handling and engine factory. --- src/audio/AudioEngineFactory.cpp | 37 ++++++++++++++++++++++++++++ src/audio/AudioEngineFactory.h | 41 ++++++++++++++++++++++++++++++++ src/audio/CMakeLists.txt | 1 + src/audio/IAudioEngine.cpp | 8 ++++++- src/audio/IAudioEngine.h | 13 ++++++++++ src/audio/PortAudioEngine.cpp | 14 +++++++++-- 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/audio/AudioEngineFactory.cpp create mode 100644 src/audio/AudioEngineFactory.h diff --git a/src/audio/AudioEngineFactory.cpp b/src/audio/AudioEngineFactory.cpp new file mode 100644 index 000000000..a09f0f2bf --- /dev/null +++ b/src/audio/AudioEngineFactory.cpp @@ -0,0 +1,37 @@ +//========================================================================= +// Name: AudioEngineFactory.cpp +// Purpose: Creates AudioEngines for the current platform. +// +// 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 "AudioEngineFactory.h" +#include "PortAudioEngine.h" + +std::shared_ptr AudioEngineFactory::SystemEngine_; + +std::shared_ptr AudioEngineFactory::GetAudioEngine() +{ + if (!SystemEngine_) + { + // TBD: support PulseAudio as well. + SystemEngine_ = std::shared_ptr(new PortAudioEngine()); + } + + return SystemEngine_; +} \ No newline at end of file diff --git a/src/audio/AudioEngineFactory.h b/src/audio/AudioEngineFactory.h new file mode 100644 index 000000000..8db34b130 --- /dev/null +++ b/src/audio/AudioEngineFactory.h @@ -0,0 +1,41 @@ +//========================================================================= +// Name: AudioEngineFactory.h +// Purpose: Creates AudioEngines for the current platform. +// +// Authors: Mooneer Salem +// License: +// +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2.1, +// as published by the Free Software Foundation. This program is +// distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +// License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, see . +// +//========================================================================= + +#ifndef AUDIO_ENGINE_FACTORY_H +#define AUDIO_ENGINE_FACTORY_H + +#include "IAudioEngine.h" + +class AudioEngineFactory +{ +public: + static std::shared_ptr GetAudioEngine(); + +private: + AudioEngineFactory() = delete; + AudioEngineFactory(const AudioEngineFactory&) = delete; + ~AudioEngineFactory() = delete; + + static std::shared_ptr SystemEngine_; +}; + +#endif // AUDIO_ENGINE_FACTORY_H \ No newline at end of file diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 4f83826ac..c51cd363a 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(fdv_audio STATIC AudioDeviceSpecification.cpp + AudioEngineFactory.cpp IAudioDevice.cpp IAudioEngine.cpp PortAudioDevice.cpp diff --git a/src/audio/IAudioEngine.cpp b/src/audio/IAudioEngine.cpp index adb65e3cf..bcfea4d8e 100644 --- a/src/audio/IAudioEngine.cpp +++ b/src/audio/IAudioEngine.cpp @@ -31,4 +31,10 @@ int IAudioEngine::StandardSampleRates[] = 44100, 48000, 88200, 96000, 192000, -1 // negative terminated list -}; \ No newline at end of file +}; + +void IAudioEngine::setOnEngineError(AudioErrorCallbackFn fn, void* state) +{ + onAudioErrorFunction = fn; + onAudioErrorState = state; +} \ No newline at end of file diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h index 0588eb302..b4854aaf9 100644 --- a/src/audio/IAudioEngine.h +++ b/src/audio/IAudioEngine.h @@ -23,6 +23,7 @@ #ifndef I_AUDIO_ENGINE_H #define I_AUDIO_ENGINE_H +#include #include #include #include "AudioDeviceSpecification.h" @@ -32,6 +33,8 @@ class IAudioDevice; class IAudioEngine { public: + typedef std::function AudioErrorCallbackFn; + enum AudioDirection { IN, OUT }; virtual void start() = 0; @@ -40,8 +43,18 @@ class IAudioEngine virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction) = 0; virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) = 0; + // Set error callback. + // Callback must take the following parameters: + // 1. Audio engine. + // 2. String representing the error encountered. + // 3. Pointer to user-provided state object (typically onAudioUnderflowState, defined below). + void setOnEngineError(AudioErrorCallbackFn fn, void* state); + protected: static int StandardSampleRates[]; + + AudioErrorCallbackFn onAudioErrorFunction; + void* onAudioErrorState; }; #endif // AUDIO_ENGINE_H \ No newline at end of file diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index 0d9b241fd..0e9630371 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -40,8 +40,18 @@ PortAudioEngine::~PortAudioEngine() void PortAudioEngine::start() { - Pa_Initialize(); - initialized_ = true; + auto error = Pa_Initialize(); + if (error != paNoError) + { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, Pa_GetErrorText(error), onAudioErrorState); + } + } + else + { + initialized_ = true; + } } void PortAudioEngine::stop() From bb543e605a352b72c5c15aa4d3ebabc71a856949 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 20 Dec 2021 12:24:28 -0800 Subject: [PATCH 06/89] Use new audio API for the audio options dialog. --- src/audio/IAudioEngine.h | 1 + src/audio/PortAudioEngine.cpp | 3 +- src/dlg_audiooptions.cpp | 746 ++++++++++++---------------------- src/dlg_audiooptions.h | 21 +- 4 files changed, 259 insertions(+), 512 deletions(-) diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h index b4854aaf9..db0b155a5 100644 --- a/src/audio/IAudioEngine.h +++ b/src/audio/IAudioEngine.h @@ -26,6 +26,7 @@ #include #include #include +#include #include "AudioDeviceSpecification.h" class IAudioDevice; diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index 0e9630371..f3a1fb0e4 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -70,7 +70,8 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(index); std::string hostApiName = Pa_GetHostApiInfo(deviceInfo->hostApi)->name; - if (hostApiName.find("DirectSound") != std::string::npos) + if (hostApiName.find("DirectSound") != std::string::npos || + hostApiName.find("surround") != std::string::npos) { // Skip DirectSound devices due to poor real-time performance. continue; diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 5bcbc9672..dd7475276 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -21,7 +21,8 @@ //========================================================================= #include "main.h" #include "dlg_audiooptions.h" -#include "pa_wrapper.h" +#include "audio/AudioEngineFactory.h" +#include "audio/IAudioDevice.h" // constants for test waveform plots @@ -36,19 +37,21 @@ extern wxConfigBase *pConfig; -void AudioOptsDialog::Pa_Init(void) +void AudioOptsDialog::audioEngineInit(void) { - m_isPaInitialized = false; + m_isPaInitialized = true; - if((pa_err = Pa_Initialize()) == paNoError) + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->setOnEngineError([this](IAudioEngine&, std::string error, void*) { - m_isPaInitialized = true; - } - else - { - wxMessageBox(wxT("Port Audio failed to initialize"), wxT("Pa_Initialize"), wxOK); - return; - } + CallAfter([&]() { + wxMessageBox(wxT("Sound engine failed to initialize"), wxT("Error"), wxOK); + }); + + m_isPaInitialized = false; + }, nullptr); + + engine->start(); } @@ -75,10 +78,8 @@ void AudioOptsDialog::buildTestControls(PlotScalar **plotScalar, wxButton **btnT //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= AudioOptsDialog::AudioOptsDialog(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style) : wxDialog(parent, id, title, pos, size, style) { - m_audioTestThread = nullptr; - if (g_verbose) fprintf(stderr, "pos %d %d\n", pos.x, pos.y); - Pa_Init(); + audioEngineInit(); wxBoxSizer* mainSizer; mainSizer = new wxBoxSizer(wxVERTICAL); @@ -230,54 +231,6 @@ AudioOptsDialog::AudioOptsDialog(wxWindow* parent, wxWindowID id, const wxString bSizer18->Fit(m_panelTx); m_notebook1->AddPage(m_panelTx, _("Transmit"), false); - // API Tab ------------------------------------------------------------------- - - m_panelAPI = new wxPanel(m_notebook1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* bSizer12; - bSizer12 = new wxBoxSizer(wxHORIZONTAL); - wxGridSizer* gSizer31; - gSizer31 = new wxGridSizer(2, 1, 0, 0); - wxStaticBoxSizer* sbSizer1; - sbSizer1 = new wxStaticBoxSizer(new wxStaticBox(m_panelAPI, wxID_ANY, _("PortAudio")), wxVERTICAL); - - wxGridSizer* gSizer3; - gSizer3 = new wxGridSizer(4, 2, 0, 0); - - m_staticText7 = new wxStaticText(m_panelAPI, wxID_ANY, _("PortAudio Version String:"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText7->Wrap(-1); - gSizer3->Add(m_staticText7, 1, wxALIGN_RIGHT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - m_textStringVer = new wxStaticText(m_panelAPI, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); - gSizer3->Add(m_textStringVer, 1, wxALIGN_LEFT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - - m_staticText8 = new wxStaticText(m_panelAPI, wxID_ANY, _("PortAudio Int Version:"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText8->Wrap(-1); - gSizer3->Add(m_staticText8, 1, wxALIGN_RIGHT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - m_textIntVer = new wxStaticText(m_panelAPI, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(45,-1), 0); - gSizer3->Add(m_textIntVer, 1, wxALIGN_LEFT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - - m_staticText5 = new wxStaticText(m_panelAPI, wxID_ANY, _("Device Count:"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText5->Wrap(-1); - gSizer3->Add(m_staticText5, 1, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 10); - m_textCDevCount = new wxStaticText(m_panelAPI, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(45,-1), 0); - gSizer3->Add(m_textCDevCount, 1, wxALIGN_LEFT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - - m_staticText4 = new wxStaticText(m_panelAPI, wxID_ANY, _("API Count:"), wxDefaultPosition, wxDefaultSize, 0); - m_staticText4->Wrap(-1); - gSizer3->Add(m_staticText4, 1, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL, 10); - m_textAPICount = new wxStaticText(m_panelAPI, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(45,-1), 0); - m_textAPICount->SetMaxSize(wxSize(45,-1)); - gSizer3->Add(m_textAPICount, 1, wxALIGN_LEFT|wxALL|wxALIGN_CENTER_VERTICAL, 10); - - sbSizer1->Add(gSizer3, 1, wxEXPAND, 2); - gSizer31->Add(sbSizer1, 1, wxEXPAND, 2); - wxStaticBoxSizer* sbSizer6; - sbSizer6 = new wxStaticBoxSizer(new wxStaticBox(m_panelAPI, wxID_ANY, _("Other")), wxVERTICAL); - gSizer31->Add(sbSizer6, 1, wxEXPAND, 5); - bSizer12->Add(gSizer31, 1, wxEXPAND, 5); - m_panelAPI->SetSizer(bSizer12); - m_panelAPI->Layout(); - bSizer12->Fit(m_panelAPI); - m_notebook1->AddPage(m_panelAPI, _("API Info"), false); bSizer4->Add(m_notebook1, 1, wxEXPAND | wxALL, 0); m_panel1->SetSizer(bSizer4); m_panel1->Layout(); @@ -311,7 +264,6 @@ AudioOptsDialog::AudioOptsDialog(wxWindow* parent, wxWindowID id, const wxString m_notebook1->SetSelection(0); - showAPIInfo(); m_RxInDevices.m_listDevices = m_listCtrlRxInDevices; m_RxInDevices.direction = AUDIO_IN; m_RxInDevices.m_textDevice = m_textCtrlRxIn; @@ -369,13 +321,7 @@ AudioOptsDialog::AudioOptsDialog(wxWindow* parent, wxWindowID id, const wxString //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= AudioOptsDialog::~AudioOptsDialog() { - if (m_audioTestThread != nullptr && m_audioTestThread->joinable()) - { - // Wait for the audio thread to stop. No need to delete as thread stop will trigger delete. - m_audioTestThread->join(); - } - - Pa_Terminate(); + AudioEngineFactory::GetAudioEngine()->stop(); // Disconnect Events this->Disconnect(wxEVT_HIBERNATE, wxActivateEventHandler(AudioOptsDialog::OnHibernate)); @@ -693,100 +639,42 @@ int AudioOptsDialog::ExchangeData(int inout) //------------------------------------------------------------------------- int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, int devNum, int in_out) { - // every sound device has a different list of supported sample rates, so - // we work out which ones are supported and populate the list ctrl - const PaDeviceInfo *deviceInfo; - PaStreamParameters inputParameters, outputParameters; - PaError err; - wxString str; - int i, numSampleRates; - - deviceInfo = Pa_GetDeviceInfo(devNum); - if (deviceInfo == NULL) { - if (g_verbose) fprintf(stderr,"Pa_GetDeviceInfo(%d) failed!\n", devNum); - cbSampleRate->Clear(); - return 0; - } - - inputParameters.device = devNum; - inputParameters.channelCount = deviceInfo->maxInputChannels; - inputParameters.sampleFormat = paInt16; - inputParameters.suggestedLatency = 0; - inputParameters.hostApiSpecificStreamInfo = NULL; - - outputParameters.device = devNum; - outputParameters.channelCount = deviceInfo->maxOutputChannels; - outputParameters.sampleFormat = paInt16; - outputParameters.suggestedLatency = 0; - outputParameters.hostApiSpecificStreamInfo = NULL; + auto engine = AudioEngineFactory::GetAudioEngine(); + auto deviceList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + wxString str; + int numSampleRates = 0; cbSampleRate->Clear(); - //printf("devNum %d supports: ", devNum); - numSampleRates = 0; - for(i = 0; PortAudioWrap::standardSampleRates[i] > 0; i++) + for (auto& dev : deviceList) { - if (in_out == AUDIO_IN) - err = Pa_IsFormatSupported(&inputParameters, NULL, PortAudioWrap::standardSampleRates[i]); - else - err = Pa_IsFormatSupported(NULL, &outputParameters, PortAudioWrap::standardSampleRates[i]); - - if( err == paFormatIsSupported ) { - str.Printf("%i", (int)PortAudioWrap::standardSampleRates[i]); - cbSampleRate->AppendString(str); - if (g_verbose) fprintf(stderr,"%i ", (int)PortAudioWrap::standardSampleRates[i]); - numSampleRates++; - } else { - fprintf(stderr, "PA error while testing sample rate for dev ID %d: %s\n", devNum, Pa_GetErrorText(err)); + if (dev.deviceId == devNum) + { + for (auto& rate : dev.supportedSampleRates) + { + str.Printf("%i", rate); + cbSampleRate->AppendString(str); + } + numSampleRates = dev.supportedSampleRates.size(); } } - if (g_verbose) fprintf(stderr,"\n"); return numSampleRates; } -//------------------------------------------------------------------------- -// showAPIInfo() -//------------------------------------------------------------------------- -void AudioOptsDialog::showAPIInfo() -{ - wxString strval; - int apiVersion; - int apiCount = 0; - int numDevices = 0; - - strval = Pa_GetVersionText(); - m_textStringVer->SetLabel(strval); - - apiVersion = Pa_GetVersion(); - strval.Printf(wxT("%d"), apiVersion); - m_textIntVer->SetLabel(strval); - - apiCount = Pa_GetHostApiCount(); - strval.Printf(wxT("%d"), apiCount); - m_textAPICount->SetLabel(strval); - - numDevices = Pa_GetDeviceCount(); - strval.Printf(wxT("%d"), numDevices); - m_textCDevCount->SetLabel(strval); -} - //------------------------------------------------------------------------- // populateParams() //------------------------------------------------------------------------- void AudioOptsDialog::populateParams(AudioInfoDisplay ai) { - const PaDeviceInfo *deviceInfo = NULL; wxListCtrl* ctrl = ai.m_listDevices; int in_out = ai.direction; - long idx; - int numDevices; wxListItem listItem; wxString buf; - int devn; - int col = 0; - - numDevices = Pa_GetDeviceCount(); + int col = 0, idx; + auto engine = AudioEngineFactory::GetAudioEngine(); + auto devList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + if(ctrl->GetColumnCount() > 0) { ctrl->ClearAll(); @@ -822,65 +710,21 @@ void AudioOptsDialog::populateParams(AudioInfoDisplay ai) ctrl->SetColumnWidth(col++, 160); } - #ifdef LATENCY - listItem.SetAlign(wxLIST_FORMAT_CENTRE); - listItem.SetText(wxT("Min Latency")); - ctrl->InsertColumn(col, listItem); - ctrl->SetColumnWidth(col++, 100); + for(auto& dev : devList) + { + col = 0; + buf.Printf(wxT("%s"), dev.name); + idx = ctrl->InsertItem(ctrl->GetItemCount(), buf); + col++; + + buf.Printf(wxT("%d"), dev.deviceId); + ctrl->SetItem(idx, col++, buf); - listItem.SetAlign(wxLIST_FORMAT_CENTRE); - listItem.SetText(wxT("Max Latency")); - ctrl->InsertColumn(col, listItem); - ctrl->SetColumnWidth(col++, 100); - #endif + buf.Printf(wxT("%s"), dev.apiName); + ctrl->SetItem(idx, col++, buf); - for(devn = 0; devn < numDevices; devn++) - { - buf.Printf(wxT("")); - deviceInfo = Pa_GetDeviceInfo(devn); - if( ((in_out == AUDIO_IN) && (deviceInfo->maxInputChannels > 0)) || - ((in_out == AUDIO_OUT) && (deviceInfo->maxOutputChannels > 0))) - { - wxString hostApiName(wxString::FromUTF8(Pa_GetHostApiInfo(deviceInfo->hostApi)->name)); - - // Exclude DirectSound devices from the list, as they are duplicates to MME - // devices and sometimes do not work well for users - if(hostApiName.Find("DirectSound") != wxNOT_FOUND) - continue; - - // Exclude "surround" devices as they clutter the dev list and are not used - wxString devName(wxString::FromUTF8(deviceInfo->name)); - if(devName.Find("surround") != wxNOT_FOUND) - continue; - - col = 0; - buf.Printf(wxT("%s"), devName); - idx = ctrl->InsertItem(ctrl->GetItemCount(), buf); - col++; - - buf.Printf(wxT("%d"), devn); - ctrl->SetItem(idx, col++, buf); - - buf.Printf(wxT("%s"), hostApiName); - ctrl->SetItem(idx, col++, buf); - - buf.Printf(wxT("%i"), (int)deviceInfo->defaultSampleRate); - ctrl->SetItem(idx, col++, buf); - - #ifdef LATENCY - if (in_out == AUDIO_IN) - buf.Printf(wxT("%8.4f"), deviceInfo->defaultLowInputLatency); - else - buf.Printf(wxT("%8.4f"), deviceInfo->defaultLowOutputLatency); - ctrl->SetItem(idx, col++, buf); - - if (in_out == AUDIO_IN) - buf.Printf(wxT("%8.4f"), deviceInfo->defaultHighInputLatency); - else - buf.Printf(wxT("%8.4f"), deviceInfo->defaultHighOutputLatency); - ctrl->SetItem(idx, col++, buf); - #endif - } + buf.Printf(wxT("%i"), dev.defaultSampleRate); + ctrl->SetItem(idx, col++, buf); } // add "none" option at end @@ -994,137 +838,105 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(int dn, PlotScalar *ps) { m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - m_audioTestThread = new std::thread([this](PlotScalar *plotScalar, int devNum) { - PaStreamParameters inputParameters; - const PaDeviceInfo *deviceInfo = NULL; - PaStream *stream = NULL; - PaError err; - short in48k_stereo_short[2*TEST_BUF_SIZE]; - short in48k_short[TEST_BUF_SIZE]; - short in8k_short[TEST_BUF_SIZE]; - int numDevices, nBufs, j, src_error,inputChannels, sampleRate, sampleCount; - SRC_STATE *src; - FIFO *fifo; - - // a basic sanity check - numDevices = Pa_GetDeviceCount(); - if (devNum >= numDevices || devNum < 0) - { - goto plot_in_reenable_ui; - } - if (g_verbose) fprintf(stderr,"devNum %d\n", devNum); - - fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); - src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); - - // work out how many input channels this device supports. - - deviceInfo = Pa_GetDeviceInfo(devNum); - if (deviceInfo == NULL) { - CallAfter([&]() { - wxMessageBox(wxT("Couldn't get device info from Port Audio for Sound Card "), wxT("Error"), wxOK); - }); - goto plot_in_cleanup; - } - if (deviceInfo->maxInputChannels == 1) - inputChannels = 1; - else - inputChannels = 2; - - // open device - - inputParameters.device = devNum; - inputParameters.channelCount = inputChannels; - inputParameters.sampleFormat = paInt16; - inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputParameters.device )->defaultHighInputLatency; - inputParameters.hostApiSpecificStreamInfo = NULL; - - sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); - nBufs = TEST_WAVEFORM_PLOT_TIME*sampleRate/TEST_BUF_SIZE; - if (g_verbose) fprintf(stderr,"inputChannels: %d nBufs %d\n", inputChannels, nBufs); - - err = Pa_OpenStream( - &stream, - &inputParameters, - NULL, - sampleRate, - 0, - paClipOff, - NULL, // no callback, use blocking API - NULL ); - if (err != paNoError) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't initialise sound device."), wxT("Error"), wxOK); - }); - goto plot_in_cleanup; - } - - err = Pa_StartStream(stream); - if (err != paNoError) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't start sound device."), wxT("Error"), wxOK); - }); - goto plot_in_cleanup; - } - - // Sometimes this buffer doesn't get completely filled. Unset values show up as - // junk on the plot. - memset(in8k_short, 0, TEST_BUF_SIZE * sizeof(short)); - - sampleCount = 0; - - while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) + SRC_STATE *src; + FIFO *fifo, *callbackFifo; + int src_error; + + fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); + src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); + + auto engine = AudioEngineFactory::GetAudioEngine(); + auto devList = engine->getAudioDeviceList(IAudioEngine::IN); + for (auto& devInfo : devList) + { + if (devInfo.deviceId == dn) { - Pa_ReadStream(stream, in48k_stereo_short, TEST_BUF_SIZE); - if (inputChannels == 2) { - for(j=0; jGetValue()); + auto device = engine->getAudioDevice( + devInfo.name, + IAudioEngine::IN, + sampleRate, + devInfo.maxChannels >= 2 ? 2 : 1); + + if (device) { - // come back when the fifo is refilled - continue; - } - - plotScalar->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); - sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, plotScalar); - } + std::mutex callbackFifoMutex; + std::condition_variable callbackFifoCV; + + callbackFifo = codec2_fifo_create(sampleRate); + assert(callbackFifo != nullptr); + + device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { + short* in48k_stereo_short = static_cast(data); + short in48k_short[numSamples]; + + if (devInfo.maxChannels >= 2) { + for(int j = 0; j < numSamples; j++) + in48k_short[j] = in48k_stereo_short[2*j]; // left channel only + } + else { + for(int j = 0; j < numSamples; j++) + in48k_short[j] = in48k_stereo_short[j]; + } + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + codec2_fifo_write(callbackFifo, in48k_short, numSamples); + } + callbackFifoCV.notify_one(); + }, nullptr); + + device->start(); + while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) + { + short in8k_short[TEST_BUF_SIZE]; + short in48k_short[TEST_BUF_SIZE]; + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait_for(callbackFifoLock, std::chrono::milliseconds(1)); + if (!codec2_fifo_read(callbackFifo, in48k_short, TEST_BUF_SIZE)) + { + continue; + } + } + + // Process any pending UI events. + wxSafeYield(); + + int n8k = resample(src, in8k_short, in48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); + resample_for_plot(fifo, in8k_short, n8k, FS); + + short plotSamples[TEST_WAVEFORM_PLOT_BUF]; + if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) + { + // come back when the fifo is refilled + continue; + } + + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + sampleCount += TEST_WAVEFORM_PLOT_BUF; + CallAfter(&AudioOptsDialog::UpdatePlot, ps); + } - err = Pa_StopStream(stream); - if (err != paNoError) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't stop sound device."), wxT("Error"), wxOK); - }); + device->stop(); + + codec2_fifo_destroy(callbackFifo); + } + break; } - - Pa_CloseStream(stream); - -plot_in_cleanup: - codec2_fifo_destroy(fifo); - src_delete(src); - -plot_in_reenable_ui: - CallAfter([&]() { - m_btnRxInTest->Enable(true); - m_btnRxOutTest->Enable(true); - m_btnTxInTest->Enable(true); - m_btnTxOutTest->Enable(true); + } - // Clean up after thread. - m_audioTestThread->join(); - delete m_audioTestThread; - m_audioTestThread = nullptr; - }); - }, ps, dn); + codec2_fifo_destroy(fifo); + src_delete(src); + + CallAfter([&]() { + m_btnRxInTest->Enable(true); + m_btnRxOutTest->Enable(true); + m_btnTxInTest->Enable(true); + m_btnTxOutTest->Enable(true); + }); } //------------------------------------------------------------------------- @@ -1140,138 +952,110 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(int dn, PlotScalar *ps) { m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - m_audioTestThread = new std::thread([this](PlotScalar *plotScalar, int devNum) { - PaStreamParameters outputParameters; - const PaDeviceInfo *deviceInfo = NULL; - PaStream *stream = NULL; - PaError err; - short out48k_stereo_short[2*TEST_BUF_SIZE]; - short out48k_short[TEST_BUF_SIZE]; - short out8k_short[TEST_BUF_SIZE]; - int numDevices, j, src_error, n, outputChannels, sampleRate, sampleCount; - SRC_STATE *src; - FIFO *fifo; - - // a basic sanity check - numDevices = Pa_GetDeviceCount(); - if (devNum >= numDevices || devNum < 0) + SRC_STATE *src; + FIFO *fifo, *callbackFifo; + int src_error, n = 0; + + fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); + src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); + + auto engine = AudioEngineFactory::GetAudioEngine(); + auto devList = engine->getAudioDeviceList(IAudioEngine::OUT); + for (auto& devInfo : devList) + { + if (devInfo.deviceId == dn) { - goto plot_out_reenable_ui; - } - - fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); - src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); - - // work out how many output channels this device supports. - - deviceInfo = Pa_GetDeviceInfo(devNum); - if (deviceInfo == NULL) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't get device info from Port Audio for Sound Card "), wxT("Error"), wxOK); - }); - goto plot_out_cleanup; - } - if (deviceInfo->maxOutputChannels == 1) - outputChannels = 1; - else - outputChannels = 2; - - if (g_verbose) fprintf(stderr,"outputChannels: %d\n", outputChannels); - - outputParameters.device = devNum; - outputParameters.channelCount = outputChannels; - outputParameters.sampleFormat = paInt16; - outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputParameters.device )->defaultHighOutputLatency; - outputParameters.hostApiSpecificStreamInfo = NULL; - - sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); - - err = Pa_OpenStream( - &stream, - NULL, - &outputParameters, - sampleRate, - 0, - paClipOff, - NULL, // no callback, use blocking API - NULL ); - if (err != paNoError) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't initialise sound device."), wxT("Error"), wxOK); - }); - goto plot_out_cleanup; - } - - err = Pa_StartStream(stream); - if (err != paNoError) { - CallAfter([&] { - wxMessageBox(wxT("Couldn't start sound device."), wxT("Error"), wxOK); - }); - goto plot_out_cleanup; - } - - // Sometimes this buffer doesn't get completely filled. Unset values show up as - // junk on the plot. - memset(out8k_short, 0, TEST_BUF_SIZE * sizeof(short)); - - sampleCount = 0; - n = 0; - - while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { - for(j=0; jGetValue()); + auto device = engine->getAudioDevice( + devInfo.name, + IAudioEngine::OUT, + sampleRate, + devInfo.maxChannels >= 2 ? 2 : 1); + + if (device) + { + std::mutex callbackFifoMutex; + std::condition_variable callbackFifoCV; + + callbackFifo = codec2_fifo_create(sampleRate); + assert(callbackFifo != nullptr); + + device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { + short out48k_short[numSamples]; + short out48k_stereo_short[sampleRate]; + + for(int j = 0; j < numSamples; j++, n++) + { + out48k_short[j] = 2000.0*cos(6.2832*(n)*400.0/sampleRate); + if (devInfo.maxChannels >= 2) { + out48k_stereo_short[2*j] = out48k_short[j]; // left channel + out48k_stereo_short[2*j+1] = out48k_short[j]; // right channel + } + else { + out48k_stereo_short[j] = out48k_short[j]; // mono + } + } + + memcpy(data, &out48k_stereo_short[0], numSamples); + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + codec2_fifo_write(callbackFifo, out48k_short, numSamples); + } + callbackFifoCV.notify_one(); + }, nullptr); + + device->start(); + while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) + { + short out8k_short[TEST_BUF_SIZE]; + short out48k_short[TEST_BUF_SIZE]; + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait_for(callbackFifoLock, std::chrono::milliseconds(1)); + if (!codec2_fifo_read(callbackFifo, out48k_short, TEST_BUF_SIZE)) + { + continue; + } + } + + // Process any pending UI events. + wxSafeYield(); + + int n8k = resample(src, out8k_short, out48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); + resample_for_plot(fifo, out8k_short, n8k, FS); + + short plotSamples[TEST_WAVEFORM_PLOT_BUF]; + if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) + { + // come back when the fifo is refilled + continue; + } + + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + sampleCount += TEST_WAVEFORM_PLOT_BUF; + CallAfter(&AudioOptsDialog::UpdatePlot, ps); } - } - Pa_WriteStream(stream, out48k_stereo_short, TEST_BUF_SIZE); - - // convert back to 8kHz just for plotting - int n8k = resample(src, out8k_short, out48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); - resample_for_plot(fifo, out8k_short, n8k, FS); - // If enough 8 kHz samples are buffered, go ahead and plot, otherwise wait for more - short plotSamples[TEST_WAVEFORM_PLOT_BUF]; - if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) - { - // come back when the fifo is refilled - continue; + device->stop(); + + codec2_fifo_destroy(callbackFifo); } - - plotScalar->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); - sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, plotScalar); + break; } - - err = Pa_StopStream(stream); - if (err != paNoError) { - CallAfter([&]() { - wxMessageBox(wxT("Couldn't stop sound device."), wxT("Error"), wxOK); - }); - } - Pa_CloseStream(stream); - -plot_out_cleanup: - codec2_fifo_destroy(fifo); - src_delete(src); - -plot_out_reenable_ui: - CallAfter([&]() { - m_btnRxInTest->Enable(true); - m_btnRxOutTest->Enable(true); - m_btnTxInTest->Enable(true); - m_btnTxOutTest->Enable(true); - - // Clean up after thread. - m_audioTestThread->join(); - delete m_audioTestThread; - m_audioTestThread = nullptr; - }); - }, ps, dn); + } + + codec2_fifo_destroy(fifo); + src_delete(src); + + CallAfter([&]() { + m_btnRxInTest->Enable(true); + m_btnRxOutTest->Enable(true); + m_btnTxInTest->Enable(true); + m_btnTxOutTest->Enable(true); + }); } //------------------------------------------------------------------------- @@ -1311,13 +1095,12 @@ void AudioOptsDialog::OnTxOutTest(wxCommandEvent& event) //------------------------------------------------------------------------- void AudioOptsDialog::OnRefreshClick(wxCommandEvent& event) { - // restart portaudio, to re-sample available devices - - Pa_Terminate(); - Pa_Init(); + // restart audio engine, to re-sample available devices + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->start(); m_notebook1->SetSelection(0); - showAPIInfo(); populateParams(m_RxInDevices); populateParams(m_RxOutDevices); populateParams(m_TxInDevices); @@ -1337,14 +1120,10 @@ void AudioOptsDialog::OnApplyAudioParameters(wxCommandEvent& event) ExchangeData(EXCHANGE_DATA_OUT); if(m_isPaInitialized) { - if((pa_err = Pa_Terminate()) == paNoError) - { - m_isPaInitialized = false; - } - else - { - wxMessageBox(wxT("Port Audio failed to Terminate"), wxT("Pa_Terminate"), wxOK); - } + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + m_isPaInitialized = false; } } @@ -1355,20 +1134,10 @@ void AudioOptsDialog::OnCancelAudioParameters(wxCommandEvent& event) { if(m_isPaInitialized) { - if (m_audioTestThread != nullptr && m_audioTestThread->joinable()) - { - // Wait for the audio thread to stop. No need to delete as thread stop will trigger delete. - m_audioTestThread->join(); - } - - if((pa_err = Pa_Terminate()) == paNoError) - { - m_isPaInitialized = false; - } - else - { - wxMessageBox(wxT("Port Audio failed to Terminate"), wxT("Pa_Terminate"), wxOK); - } + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + m_isPaInitialized = false; } EndModal(wxCANCEL); } @@ -1386,15 +1155,10 @@ void AudioOptsDialog::OnOkAudioParameters(wxCommandEvent& event) if (status == 0) { if(m_isPaInitialized) { - if((pa_err = Pa_Terminate()) == paNoError) - { - if (g_verbose) fprintf(stderr, "terminated OK\n"); - m_isPaInitialized = false; - } - else - { - wxMessageBox(wxT("Port Audio failed to Terminate"), wxT("Pa_Terminate"), wxOK); - } + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + m_isPaInitialized = false; } EndModal(wxOK); } diff --git a/src/dlg_audiooptions.h b/src/dlg_audiooptions.h index 5cffc160b..39dbca71d 100644 --- a/src/dlg_audiooptions.h +++ b/src/dlg_audiooptions.h @@ -28,12 +28,6 @@ #define AUDIO_IN 0 #define AUDIO_OUT 1 -#include "portaudio.h" -#ifdef WIN32 -#if PA_USE_ASIO -#include "pa_asio.h" -#endif -#endif #include "codec2_fifo.h" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= @@ -71,9 +65,8 @@ class AudioOptsDialog : public wxDialog int buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, int devNum, int in_out); void populateParams(AudioInfoDisplay); - void showAPIInfo(); int setTextCtrlIfDevNumValid(wxTextCtrl *textCtrl, wxListCtrl *listCtrl, int devNum); - void Pa_Init(void); + void audioEngineInit(void); void OnDeviceSelect(wxComboBox *cbSampleRate, wxTextCtrl *textCtrl, int *devNum, @@ -128,23 +121,11 @@ class AudioOptsDialog : public wxDialog wxButton* m_btnTxOutTest; PlotScalar* m_plotScalarTxOut; - wxPanel* m_panelAPI; - - wxStaticText* m_staticText7; - wxStaticText* m_textStringVer; - wxStaticText* m_staticText8; - wxStaticText* m_textIntVer; - wxStaticText* m_staticText5; - wxStaticText* m_textCDevCount; - wxStaticText* m_staticText4; - wxStaticText* m_textAPICount; wxButton* m_btnRefresh; wxStdDialogButtonSizer* m_sdbSizer1; wxButton* m_sdbSizer1OK; wxButton* m_sdbSizer1Apply; wxButton* m_sdbSizer1Cancel; - - std::thread* m_audioTestThread; // Virtual event handlers, overide them in your derived class //virtual void OnActivateApp( wxActivateEvent& event ) { event.Skip(); } From ed2b88d0a2c7990114e8b113cac036c967caf385 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 20 Dec 2021 12:33:33 -0800 Subject: [PATCH 07/89] Move supported sample rate detection to a new method to avoid performance issues. --- src/audio/AudioDeviceSpecification.h | 1 - src/audio/IAudioEngine.h | 1 + src/audio/PortAudioEngine.cpp | 41 ++++++++++++++++++---------- src/audio/PortAudioEngine.h | 1 + src/dlg_audiooptions.cpp | 9 ++++-- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/audio/AudioDeviceSpecification.h b/src/audio/AudioDeviceSpecification.h index d0299a5c7..da2c5eea2 100644 --- a/src/audio/AudioDeviceSpecification.h +++ b/src/audio/AudioDeviceSpecification.h @@ -33,7 +33,6 @@ struct AudioDeviceSpecification std::string apiName; int defaultSampleRate; int maxChannels; - std::vector supportedSampleRates; bool isValid() const; static AudioDeviceSpecification GetInvalidDevice(); diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h index db0b155a5..72828fc15 100644 --- a/src/audio/IAudioEngine.h +++ b/src/audio/IAudioEngine.h @@ -43,6 +43,7 @@ class IAudioEngine virtual std::vector getAudioDeviceList(AudioDirection direction) = 0; virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction) = 0; virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) = 0; + virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction) = 0; // Set error callback. // Callback must take the following parameters: diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index f3a1fb0e4..d1f8e9859 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -80,14 +80,6 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD if ((direction == IN && deviceInfo->maxInputChannels > 0) || (direction == OUT && deviceInfo->maxOutputChannels > 0)) { - PaStreamParameters streamParameters; - - streamParameters.device = index; - streamParameters.channelCount = 1; - streamParameters.sampleFormat = paInt16; - streamParameters.suggestedLatency = 0; - streamParameters.hostApiSpecificStreamInfo = NULL; - AudioDeviceSpecification device; device.deviceId = index; device.name = deviceInfo->name; @@ -96,6 +88,30 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD direction == IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; device.defaultSampleRate = deviceInfo->defaultSampleRate; + result.push_back(device); + } + } + + return result; +} + +std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName, AudioDirection direction) +{ + std::vector result; + auto devInfo = getAudioDeviceList(direction); + + for (auto& device : devInfo) + { + if (deviceName == device.name) + { + PaStreamParameters streamParameters; + + streamParameters.device = device.deviceId; + streamParameters.channelCount = 1; + streamParameters.sampleFormat = paInt16; + streamParameters.suggestedLatency = 0; + streamParameters.hostApiSpecificStreamInfo = NULL; + int rateIndex = 0; while (IAudioEngine::StandardSampleRates[rateIndex] != -1) { @@ -106,16 +122,11 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD if (err == paFormatIsSupported) { - device.supportedSampleRates.push_back(IAudioEngine::StandardSampleRates[rateIndex]); + result.push_back(IAudioEngine::StandardSampleRates[rateIndex]); } rateIndex++; - } - - if (device.supportedSampleRates.size() > 0) - { - result.push_back(device); - } + } } } diff --git a/src/audio/PortAudioEngine.h b/src/audio/PortAudioEngine.h index b2965e1c1..a63c042ed 100644 --- a/src/audio/PortAudioEngine.h +++ b/src/audio/PortAudioEngine.h @@ -36,6 +36,7 @@ class PortAudioEngine : public IAudioEngine virtual std::vector getAudioDeviceList(AudioDirection direction); virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction); virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels); + virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction); private: bool initialized_; diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index dd7475276..2102ebf37 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -649,12 +649,17 @@ int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, i { if (dev.deviceId == devNum) { - for (auto& rate : dev.supportedSampleRates) + auto supportedSampleRates = + engine->getSupportedSampleRates( + dev.name, + in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + + for (auto& rate : supportedSampleRates) { str.Printf("%i", rate); cbSampleRate->AppendString(str); } - numSampleRates = dev.supportedSampleRates.size(); + numSampleRates = supportedSampleRates.size(); } } From cd654b46fac177a765388c6af213af709422a961 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 00:02:22 -0800 Subject: [PATCH 08/89] Switch main audio pipeline to IAudioEngine. --- src/CMakeLists.txt | 2 - src/audio/IAudioDevice.h | 2 + src/audio/PortAudioDevice.h | 2 + src/audio/PortAudioEngine.cpp | 2 +- src/dlg_audiooptions.cpp | 187 +++--- src/dlg_audiooptions.h | 15 +- src/freedv_interface.cpp | 1 + src/main.cpp | 1099 +++++++++++++-------------------- src/main.h | 58 +- src/ongui.cpp | 5 +- src/pa_wrapper.cpp | 351 ----------- src/pa_wrapper.h | 124 ---- src/util.cpp | 1 + 13 files changed, 516 insertions(+), 1333 deletions(-) delete mode 100644 src/pa_wrapper.cpp delete mode 100644 src/pa_wrapper.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9709b0ba2..3e9fd6876 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,6 @@ set(FREEDV_SOURCES dlg_options.cpp dlg_ptt.cpp main.cpp - pa_wrapper.cpp plot.cpp plot_scalar.cpp plot_scatter.cpp @@ -21,7 +20,6 @@ set(FREEDV_SOURCES dlg_ptt.h defines.h main.h - pa_wrapper.h plot.h plot_scalar.h plot_scatter.h diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index 7b1e5d267..0af7b6a92 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -36,6 +36,8 @@ class IAudioDevice typedef std::function AudioOverflowCallbackFn; typedef std::function AudioErrorCallbackFn; + virtual int getNumChannels() = 0; + virtual void start() = 0; virtual void stop() = 0; diff --git a/src/audio/PortAudioDevice.h b/src/audio/PortAudioDevice.h index e1c265fff..9a5371d23 100644 --- a/src/audio/PortAudioDevice.h +++ b/src/audio/PortAudioDevice.h @@ -32,6 +32,8 @@ class PortAudioDevice : public IAudioDevice public: virtual ~PortAudioDevice(); + virtual int getNumChannels() { return numChannels_; } + virtual void start(); virtual void stop(); diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index d1f8e9859..fda90e4a4 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -161,7 +161,7 @@ std::shared_ptr PortAudioEngine::getAudioDevice(std::string device { if (dev.name == deviceName) { - auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, numChannels); + auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); return std::shared_ptr(devObj); } } diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 2102ebf37..dfddc61a2 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -354,35 +354,26 @@ void AudioOptsDialog::OnInitDialog( wxInitDialogEvent& event ) } //------------------------------------------------------------------------- -// OnInitDialog() +// setTextCtrlIfDevNameValid() // -// helper function to look up name of devNum, and if it exists write +// helper function to look up name of devName, and if it exists write // name to textCtrl. Used to trap dissapearing devices. //------------------------------------------------------------------------- -int AudioOptsDialog::setTextCtrlIfDevNumValid(wxTextCtrl *textCtrl, wxListCtrl *listCtrl, int devNum) +bool AudioOptsDialog::setTextCtrlIfDevNameValid(wxTextCtrl *textCtrl, wxListCtrl *listCtrl, wxString devName) { - int i, aDevNum, found_devNum; - // ignore last list entry as it is the "none" entry - - found_devNum = 0; - for(i=0; iGetItemCount()-1; i++) { - aDevNum = wxAtoi(listCtrl->GetItemText(i, 1)); - //printf("aDevNum: %d devNum: %d\n", aDevNum, devNum); - if (aDevNum == devNum) { - found_devNum = 1; - textCtrl->SetValue(listCtrl->GetItemText(i, 0) + " (" + wxString::Format(wxT("%i"),devNum) + ")"); + for(int i = 0; i < listCtrl->GetItemCount() - 1; i++) + { + if (devName.Trim() == listCtrl->GetItemText(i, 0).Trim()) + { + textCtrl->SetValue(listCtrl->GetItemText(i, 0)); if (g_verbose) fprintf(stderr,"setting focus of %d\n", i); listCtrl->SetItemState(i, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED); + return true; } } - if (found_devNum) - return devNum; - else { - textCtrl->SetValue("none"); - return -1; - } + return false; } //------------------------------------------------------------------------- @@ -397,80 +388,74 @@ int AudioOptsDialog::ExchangeData(int inout) if (g_verbose) fprintf(stderr,"EXCHANGE_DATA_IN:\n"); if (g_verbose) fprintf(stderr," g_nSoundCards: %d\n", g_nSoundCards); - if (g_verbose) fprintf(stderr," g_soundCard1InDeviceNum: %d\n", g_soundCard1InDeviceNum); - if (g_verbose) fprintf(stderr," g_soundCard1OutDeviceNum: %d\n", g_soundCard1OutDeviceNum); if (g_verbose) fprintf(stderr," g_soundCard1SampleRate: %d\n", g_soundCard1SampleRate); - if (g_verbose) fprintf(stderr," g_soundCard2InDeviceNum: %d\n", g_soundCard2InDeviceNum); - if (g_verbose) fprintf(stderr," g_soundCard2OutDeviceNum: %d\n", g_soundCard2OutDeviceNum); if (g_verbose) fprintf(stderr," g_soundCard2SampleRate: %d\n", g_soundCard2SampleRate); if (g_nSoundCards == 0) { - m_textCtrlRxIn ->SetValue("none"); rxInAudioDeviceNum = -1; - m_textCtrlRxOut->SetValue("none"); rxOutAudioDeviceNum = -1; - m_textCtrlTxIn ->SetValue("none"); txInAudioDeviceNum = -1; - m_textCtrlTxOut->SetValue("none"); txOutAudioDeviceNum = -1; + m_textCtrlRxIn ->SetValue("none"); + m_textCtrlRxOut->SetValue("none"); + m_textCtrlTxIn ->SetValue("none"); + m_textCtrlTxOut->SetValue("none"); } if (g_nSoundCards == 1) { - rxInAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlRxIn, - m_listCtrlRxInDevices, - g_soundCard1InDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlRxIn, + m_listCtrlRxInDevices, + wxGetApp().m_soundCard1InDeviceName); - rxOutAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlRxOut, - m_listCtrlRxOutDevices, - g_soundCard1OutDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlRxOut, + m_listCtrlRxOutDevices, + wxGetApp().m_soundCard1OutDeviceName); - if ((rxInAudioDeviceNum != -1) && (rxOutAudioDeviceNum != -1)) { + if ((m_textCtrlRxIn->GetValue() != "none") && (m_textCtrlRxOut->GetValue() != "none")) { // Build sample rate dropdown lists - buildListOfSupportedSampleRates(m_cbSampleRateRxIn, rxInAudioDeviceNum, AUDIO_IN); - buildListOfSupportedSampleRates(m_cbSampleRateRxOut, rxOutAudioDeviceNum, AUDIO_OUT); + buildListOfSupportedSampleRates(m_cbSampleRateRxIn, wxGetApp().m_soundCard1InDeviceName, AUDIO_IN); + buildListOfSupportedSampleRates(m_cbSampleRateRxOut, wxGetApp().m_soundCard1OutDeviceName, AUDIO_OUT); m_cbSampleRateRxIn->SetValue(wxString::Format(wxT("%i"),g_soundCard1SampleRate)); m_cbSampleRateRxOut->SetValue(wxString::Format(wxT("%i"),g_soundCard1SampleRate)); } - m_textCtrlTxIn ->SetValue("none"); txInAudioDeviceNum = -1; - m_textCtrlTxOut->SetValue("none"); txOutAudioDeviceNum = -1; + m_textCtrlTxIn->SetValue("none"); + m_textCtrlTxOut->SetValue("none"); } if (g_nSoundCards == 2) { - rxInAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlRxIn, - m_listCtrlRxInDevices, - g_soundCard1InDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlRxIn, + m_listCtrlRxInDevices, + wxGetApp().m_soundCard1InDeviceName); - rxOutAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlRxOut, - m_listCtrlRxOutDevices, - g_soundCard2OutDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlRxOut, + m_listCtrlRxOutDevices, + wxGetApp().m_soundCard2OutDeviceName); - txInAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlTxIn, - m_listCtrlTxInDevices, - g_soundCard2InDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlTxIn, + m_listCtrlTxInDevices, + wxGetApp().m_soundCard2InDeviceName); - txOutAudioDeviceNum = setTextCtrlIfDevNumValid(m_textCtrlTxOut, - m_listCtrlTxOutDevices, - g_soundCard1OutDeviceNum); + setTextCtrlIfDevNameValid(m_textCtrlTxOut, + m_listCtrlTxOutDevices, + wxGetApp().m_soundCard1OutDeviceName); - if ((rxInAudioDeviceNum != -1) && (txOutAudioDeviceNum != -1)) { + if ((m_textCtrlRxIn->GetValue() != "none") && (m_textCtrlTxOut->GetValue() != "none")) { // Build sample rate dropdown lists - buildListOfSupportedSampleRates(m_cbSampleRateRxIn, rxInAudioDeviceNum, AUDIO_IN); - buildListOfSupportedSampleRates(m_cbSampleRateTxOut, txOutAudioDeviceNum, AUDIO_OUT); + buildListOfSupportedSampleRates(m_cbSampleRateRxIn, wxGetApp().m_soundCard1InDeviceName, AUDIO_IN); + buildListOfSupportedSampleRates(m_cbSampleRateTxOut, wxGetApp().m_soundCard1OutDeviceName, AUDIO_OUT); m_cbSampleRateRxIn->SetValue(wxString::Format(wxT("%i"),g_soundCard1SampleRate)); m_cbSampleRateTxOut->SetValue(wxString::Format(wxT("%i"),g_soundCard1SampleRate)); } - if ((txInAudioDeviceNum != -1) && (rxOutAudioDeviceNum != -1)) { + if ((m_textCtrlTxIn->GetValue() != "none") && (m_textCtrlRxOut->GetValue() != "none")) { // Build sample rate dropdown lists - buildListOfSupportedSampleRates(m_cbSampleRateTxIn, txInAudioDeviceNum, AUDIO_IN); - buildListOfSupportedSampleRates(m_cbSampleRateRxOut, rxOutAudioDeviceNum, AUDIO_OUT); + buildListOfSupportedSampleRates(m_cbSampleRateTxIn, wxGetApp().m_soundCard2InDeviceName, AUDIO_IN); + buildListOfSupportedSampleRates(m_cbSampleRateRxOut, wxGetApp().m_soundCard2OutDeviceName, AUDIO_OUT); m_cbSampleRateTxIn->SetValue(wxString::Format(wxT("%i"),g_soundCard2SampleRate)); m_cbSampleRateRxOut->SetValue(wxString::Format(wxT("%i"),g_soundCard2SampleRate)); } } - if (g_verbose) fprintf(stderr," rxInAudioDeviceNum: %d\n rxOutAudioDeviceNum: %d\n txInAudioDeviceNum: %d\n txOutAudioDeviceNum: %d\n", - rxInAudioDeviceNum, rxOutAudioDeviceNum, txInAudioDeviceNum, txOutAudioDeviceNum); } if(inout == EXCHANGE_DATA_OUT) @@ -479,18 +464,18 @@ int AudioOptsDialog::ExchangeData(int inout) int valid_two_card_config = 0; wxString sampleRate1, sampleRate2; - if (g_verbose) fprintf(stderr,"EXCHANGE_DATA_OUT:\n"); - if (g_verbose) fprintf(stderr," rxInAudioDeviceNum: %d\n rxOutAudioDeviceNum: %d\n txInAudioDeviceNum: %d\n txOutAudioDeviceNum: %d\n", - rxInAudioDeviceNum, rxOutAudioDeviceNum, txInAudioDeviceNum, txOutAudioDeviceNum); - // --------------------------------------------------------------- // check we have a valid 1 or 2 sound card configuration // --------------------------------------------------------------- - // one sound card config, tx device numbers should be set to -1 - - if ((rxInAudioDeviceNum != -1) && (rxOutAudioDeviceNum != -1) && - (txInAudioDeviceNum == -1) && (txOutAudioDeviceNum == -1)) { + // one sound card config, tx device names should be set to "none" + wxString rxInAudioDeviceName = m_textCtrlRxIn->GetValue(); + wxString rxOutAudioDeviceName = m_textCtrlRxOut->GetValue(); + wxString txInAudioDeviceName = m_textCtrlTxIn->GetValue(); + wxString txOutAudioDeviceName = m_textCtrlTxOut->GetValue(); + + if ((rxInAudioDeviceName != "none") && (rxOutAudioDeviceName != "none") && + (txInAudioDeviceName == "none") && (txOutAudioDeviceName == "none")) { valid_one_card_config = 1; @@ -506,19 +491,19 @@ int AudioOptsDialog::ExchangeData(int inout) // two card configuration - if ((rxInAudioDeviceNum != -1) && (rxOutAudioDeviceNum != -1) && - (txInAudioDeviceNum != -1) && (txOutAudioDeviceNum != -1)) { + if ((rxInAudioDeviceName != "none") && (rxOutAudioDeviceName != "none") && + (txInAudioDeviceName != "none") && (txOutAudioDeviceName != "none")) { valid_two_card_config = 1; // Check we haven't doubled up on sound devices - if (rxInAudioDeviceNum == txInAudioDeviceNum) { + if (rxInAudioDeviceName == txInAudioDeviceName) { wxMessageBox(wxT("You must use different devices for From Radio and From Microphone"), wxT(""), wxOK); return -1; } - if (rxOutAudioDeviceNum == txOutAudioDeviceNum) { + if (rxOutAudioDeviceName == txOutAudioDeviceName) { wxMessageBox(wxT("You must use different devices for To Radio and To Speaker/Headphones"), wxT(""), wxOK); return -1; } @@ -548,7 +533,7 @@ int AudioOptsDialog::ExchangeData(int inout) if (g_verbose) fprintf(stderr," valid_one_card_config: %d valid_two_card_config: %d\n", valid_one_card_config, valid_two_card_config); if (!valid_one_card_config && !valid_two_card_config) { - wxMessageBox(wxT("Invalid one or two sound card configuration"), wxT(""), wxOK); + wxMessageBox(wxT("Invalid one or two sound card configuration. For RX only, both devices in 'Receive' tab must be selected. Otherwise, all devices in both 'Receive' and 'Transmit' tabs must be selected."), wxT(""), wxOK); return -1; } @@ -559,57 +544,40 @@ int AudioOptsDialog::ExchangeData(int inout) // Tx/Rx oriented as in this dialog. // --------------------------------------------------------------- g_nSoundCards = 0; - g_soundCard1InDeviceNum = g_soundCard1OutDeviceNum = g_soundCard2InDeviceNum = g_soundCard2OutDeviceNum = -1; if (valid_one_card_config) { // Only callback 1 used g_nSoundCards = 1; - g_soundCard1InDeviceNum = rxInAudioDeviceNum; - g_soundCard1OutDeviceNum = rxOutAudioDeviceNum; g_soundCard1SampleRate = wxAtoi(sampleRate1); } if (valid_two_card_config) { g_nSoundCards = 2; - g_soundCard1InDeviceNum = rxInAudioDeviceNum; - g_soundCard1OutDeviceNum = txOutAudioDeviceNum; g_soundCard1SampleRate = wxAtoi(sampleRate1); - g_soundCard2InDeviceNum = txInAudioDeviceNum; - g_soundCard2OutDeviceNum = rxOutAudioDeviceNum; g_soundCard2SampleRate = wxAtoi(sampleRate2); } if (g_verbose) fprintf(stderr," g_nSoundCards: %d\n", g_nSoundCards); - if (g_verbose) fprintf(stderr," g_soundCard1InDeviceNum: %d\n", g_soundCard1InDeviceNum); - if (g_verbose) fprintf(stderr," g_soundCard1OutDeviceNum: %d\n", g_soundCard1OutDeviceNum); if (g_verbose) fprintf(stderr," g_soundCard1SampleRate: %d\n", g_soundCard1SampleRate); - if (g_verbose) fprintf(stderr," g_soundCard2InDeviceNum: %d\n", g_soundCard2InDeviceNum); - if (g_verbose) fprintf(stderr," g_soundCard2OutDeviceNum: %d\n", g_soundCard2OutDeviceNum); if (g_verbose) fprintf(stderr," g_soundCard2SampleRate: %d\n", g_soundCard2SampleRate); assert (pConfig != NULL); if (valid_one_card_config) { - int lastIndex = m_textCtrlRxIn->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard1InDeviceNum)); - wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Mid(0, lastIndex).Trim(); - lastIndex = m_textCtrlRxOut->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard1OutDeviceNum)); - wxGetApp().m_soundCard1OutDeviceName = m_textCtrlRxOut->GetValue().Mid(0, lastIndex).Trim(); + wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Trim(); + wxGetApp().m_soundCard1OutDeviceName = m_textCtrlRxOut->GetValue().Trim(); wxGetApp().m_soundCard2InDeviceName = "none"; wxGetApp().m_soundCard2OutDeviceName = "none"; } else if (valid_two_card_config) { - int lastIndex = m_textCtrlRxIn->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard1InDeviceNum)); - wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Mid(0, lastIndex).Trim(); - lastIndex = m_textCtrlTxOut->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard1OutDeviceNum)); - wxGetApp().m_soundCard1OutDeviceName = m_textCtrlTxOut->GetValue().Mid(0, lastIndex).Trim(); - lastIndex = m_textCtrlTxIn->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard2InDeviceNum)); - wxGetApp().m_soundCard2InDeviceName = m_textCtrlTxIn->GetValue().Mid(0, lastIndex).Trim(); - lastIndex = m_textCtrlRxOut->GetValue().Find(wxString::Format(wxT("(%i)"), g_soundCard2OutDeviceNum)); - wxGetApp().m_soundCard2OutDeviceName = m_textCtrlRxOut->GetValue().Mid(0, lastIndex).Trim(); + wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Trim(); + wxGetApp().m_soundCard1OutDeviceName = m_textCtrlTxOut->GetValue().Trim(); + wxGetApp().m_soundCard2InDeviceName = m_textCtrlTxIn->GetValue().Trim(); + wxGetApp().m_soundCard2OutDeviceName = m_textCtrlRxOut->GetValue().Trim(); } else { @@ -637,7 +605,7 @@ int AudioOptsDialog::ExchangeData(int inout) //------------------------------------------------------------------------- // buildListOfSupportedSampleRates() //------------------------------------------------------------------------- -int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, int devNum, int in_out) +int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, wxString devName, int in_out) { auto engine = AudioEngineFactory::GetAudioEngine(); auto deviceList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); @@ -647,7 +615,7 @@ int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, i cbSampleRate->Clear(); for (auto& dev : deviceList) { - if (dev.deviceId == devNum) + if (wxString(dev.name).Trim() == devName.Trim()) { auto supportedSampleRates = engine->getSupportedSampleRates( @@ -746,7 +714,6 @@ void AudioOptsDialog::populateParams(AudioInfoDisplay ai) //------------------------------------------------------------------------- void AudioOptsDialog::OnDeviceSelect(wxComboBox *cbSampleRate, wxTextCtrl *textCtrl, - int *devNum, wxListCtrl *listCtrlDevices, int index, int in_out) @@ -754,14 +721,12 @@ void AudioOptsDialog::OnDeviceSelect(wxComboBox *cbSampleRate, wxString devName = listCtrlDevices->GetItemText(index, 0); if (devName.IsSameAs("none")) { - *devNum = -1; textCtrl->SetValue("none"); } else { - *devNum = wxAtoi(listCtrlDevices->GetItemText(index, 1)); - textCtrl->SetValue(devName + " (" + wxString::Format(wxT("%i"),*devNum) + ")"); + textCtrl->SetValue(devName); - int numSampleRates = buildListOfSupportedSampleRates(cbSampleRate, *devNum, in_out); + int numSampleRates = buildListOfSupportedSampleRates(cbSampleRate, devName, in_out); if (numSampleRates) { wxString defSampleRate = listCtrlDevices->GetItemText(index, 3); cbSampleRate->SetValue(defSampleRate); @@ -779,7 +744,6 @@ void AudioOptsDialog::OnRxInDeviceSelect(wxListEvent& evt) { OnDeviceSelect(m_cbSampleRateRxIn, m_textCtrlRxIn, - &rxInAudioDeviceNum, m_listCtrlRxInDevices, evt.GetIndex(), AUDIO_IN); @@ -792,7 +756,6 @@ void AudioOptsDialog::OnRxOutDeviceSelect(wxListEvent& evt) { OnDeviceSelect(m_cbSampleRateRxOut, m_textCtrlRxOut, - &rxOutAudioDeviceNum, m_listCtrlRxOutDevices, evt.GetIndex(), AUDIO_OUT); @@ -805,7 +768,6 @@ void AudioOptsDialog::OnTxInDeviceSelect(wxListEvent& evt) { OnDeviceSelect(m_cbSampleRateTxIn, m_textCtrlTxIn, - &txInAudioDeviceNum, m_listCtrlTxInDevices, evt.GetIndex(), AUDIO_IN); @@ -818,7 +780,6 @@ void AudioOptsDialog::OnTxOutDeviceSelect(wxListEvent& evt) { OnDeviceSelect(m_cbSampleRateTxOut, m_textCtrlTxOut, - &txOutAudioDeviceNum, m_listCtrlTxOutDevices, evt.GetIndex(), AUDIO_OUT); @@ -837,7 +798,7 @@ void AudioOptsDialog::UpdatePlot(PlotScalar *plotScalar) // synchronous portaudio functions, so the GUI will not respond until after test sample has been // taken //------------------------------------------------------------------------- -void AudioOptsDialog::plotDeviceInputForAFewSecs(int dn, PlotScalar *ps) { +void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *ps) { m_btnRxInTest->Enable(false); m_btnRxOutTest->Enable(false); m_btnTxInTest->Enable(false); @@ -854,7 +815,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(int dn, PlotScalar *ps) { auto devList = engine->getAudioDeviceList(IAudioEngine::IN); for (auto& devInfo : devList) { - if (devInfo.deviceId == dn) + if (wxString(devInfo.name).Trim() == devName.Trim()) { int sampleCount = 0; int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); @@ -951,7 +912,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(int dn, PlotScalar *ps) { // synchronous portaudio functions, so the GUI will not respond until after test sample has been // taken. Also plots a pretty picture like the record versions //------------------------------------------------------------------------- -void AudioOptsDialog::plotDeviceOutputForAFewSecs(int dn, PlotScalar *ps) { +void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar *ps) { m_btnRxInTest->Enable(false); m_btnRxOutTest->Enable(false); m_btnTxInTest->Enable(false); @@ -968,7 +929,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(int dn, PlotScalar *ps) { auto devList = engine->getAudioDeviceList(IAudioEngine::OUT); for (auto& devInfo : devList) { - if (devInfo.deviceId == dn) + if (wxString(devInfo.name).Trim() == devName.Trim()) { int sampleCount = 0; int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); @@ -1068,7 +1029,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(int dn, PlotScalar *ps) { //------------------------------------------------------------------------- void AudioOptsDialog::OnRxInTest(wxCommandEvent& event) { - plotDeviceInputForAFewSecs(rxInAudioDeviceNum, m_plotScalarRxIn); + plotDeviceInputForAFewSecs(m_textCtrlRxIn->GetValue(), m_plotScalarRxIn); } //------------------------------------------------------------------------- @@ -1076,7 +1037,7 @@ void AudioOptsDialog::OnRxInTest(wxCommandEvent& event) //------------------------------------------------------------------------- void AudioOptsDialog::OnRxOutTest(wxCommandEvent& event) { - plotDeviceOutputForAFewSecs(rxOutAudioDeviceNum, m_plotScalarRxOut); + plotDeviceOutputForAFewSecs(m_textCtrlRxOut->GetValue(), m_plotScalarRxOut); } //------------------------------------------------------------------------- @@ -1084,7 +1045,7 @@ void AudioOptsDialog::OnRxOutTest(wxCommandEvent& event) //------------------------------------------------------------------------- void AudioOptsDialog::OnTxInTest(wxCommandEvent& event) { - plotDeviceInputForAFewSecs(txInAudioDeviceNum, m_plotScalarTxIn); + plotDeviceInputForAFewSecs(m_textCtrlTxIn->GetValue(), m_plotScalarTxIn); } //------------------------------------------------------------------------- @@ -1092,7 +1053,7 @@ void AudioOptsDialog::OnTxInTest(wxCommandEvent& event) //------------------------------------------------------------------------- void AudioOptsDialog::OnTxOutTest(wxCommandEvent& event) { - plotDeviceOutputForAFewSecs(txOutAudioDeviceNum, m_plotScalarTxOut); + plotDeviceOutputForAFewSecs(m_textCtrlTxOut->GetValue(), m_plotScalarTxOut); } //------------------------------------------------------------------------- diff --git a/src/dlg_audiooptions.h b/src/dlg_audiooptions.h index 39dbca71d..5e581e725 100644 --- a/src/dlg_audiooptions.h +++ b/src/dlg_audiooptions.h @@ -50,26 +50,19 @@ class AudioOptsDialog : public wxDialog private: protected: - PaError pa_err; bool m_isPaInitialized; - int rxInAudioDeviceNum; - int rxOutAudioDeviceNum; - int txInAudioDeviceNum; - int txOutAudioDeviceNum; - void buildTestControls(PlotScalar **plotScalar, wxButton **btnTest, wxPanel *parentPanel, wxBoxSizer *bSizer, wxString buttonLabel); - void plotDeviceInputForAFewSecs(int devNum, PlotScalar *plotScalar); - void plotDeviceOutputForAFewSecs(int devNum, PlotScalar *plotScalar); + void plotDeviceInputForAFewSecs(wxString devName, PlotScalar *plotScalar); + void plotDeviceOutputForAFewSecs(wxString devName, PlotScalar *plotScalar); - int buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, int devNum, int in_out); + int buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, wxString devName, int in_out); void populateParams(AudioInfoDisplay); - int setTextCtrlIfDevNumValid(wxTextCtrl *textCtrl, wxListCtrl *listCtrl, int devNum); + bool setTextCtrlIfDevNameValid(wxTextCtrl *textCtrl, wxListCtrl *listCtrl, wxString devName); void audioEngineInit(void); void OnDeviceSelect(wxComboBox *cbSampleRate, wxTextCtrl *textCtrl, - int *devNum, wxListCtrl *listCtrlDevices, int index, int in_out); diff --git a/src/freedv_interface.cpp b/src/freedv_interface.cpp index c7ceaddab..94776d642 100644 --- a/src/freedv_interface.cpp +++ b/src/freedv_interface.cpp @@ -21,6 +21,7 @@ #include #include "main.h" +#include "codec2_fdmdv.h" FreeDVInterface::FreeDVInterface() : textRxFunc_(nullptr), diff --git a/src/main.cpp b/src/main.cpp index 89ff5017e..b1d9a4ea3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,8 @@ #include "main.h" #include "osx_interface.h" #include "freedv_interface.h" +#include "audio/AudioEngineFactory.h" +#include "codec2_fdmdv.h" #define wxUSE_FILEDLG 1 #define wxUSE_LIBPNG 1 @@ -99,11 +101,7 @@ struct FIFO *g_plotSpeechInFifo; // Soundcard config int g_nSoundCards; -int g_soundCard1InDeviceNum; -int g_soundCard1OutDeviceNum; int g_soundCard1SampleRate; -int g_soundCard2InDeviceNum; -int g_soundCard2OutDeviceNum; int g_soundCard2SampleRate; // PortAudio over/underflow counters @@ -1447,7 +1445,10 @@ void MainFrame::OnExit(wxCommandEvent& event) m_togBtnAnalog->Disable(); //m_btnTogPTT->Disable(); - Pa_Terminate(); + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + Destroy(); } @@ -1880,24 +1881,28 @@ void MainFrame::stopRxStream() //fprintf(stderr, "thread stopped\n"); delete m_txRxThread; - m_rxInPa->stop(); - m_rxInPa->streamClose(); - delete m_rxInPa; - if(m_rxOutPa != m_rxInPa) { - m_rxOutPa->stop(); - m_rxOutPa->streamClose(); - delete m_rxOutPa; - } - - if (g_nSoundCards == 2) { - m_txInPa->stop(); - m_txInPa->streamClose(); - delete m_txInPa; - if(m_txInPa != m_txOutPa) { - m_txOutPa->stop(); - m_txOutPa->streamClose(); - delete m_txOutPa; - } + if (rxInSoundDevice) + { + rxInSoundDevice->stop(); + rxInSoundDevice.reset(); + } + + if (rxOutSoundDevice) + { + rxOutSoundDevice->stop(); + rxOutSoundDevice.reset(); + } + + if (txInSoundDevice) + { + txInSoundDevice->stop(); + txInSoundDevice.reset(); + } + + if (txOutSoundDevice) + { + txOutSoundDevice->stop(); + txOutSoundDevice.reset(); } destroy_fifos(); @@ -1909,7 +1914,9 @@ void MainFrame::stopRxStream() deleteEQFilters(g_rxUserdata); delete g_rxUserdata; - Pa_Terminate(); + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); } } @@ -1933,249 +1940,144 @@ void MainFrame::destroy_src(void) src_delete(g_rxUserdata->insrctxsf); } -void MainFrame::initPortAudioDevice(PortAudioWrap *pa, int inDevice, int outDevice, - int soundCard, int sampleRate, int inputChannels, - int outputChannels) -{ - // Note all of the wrapper functions below just set values in a - // portaudio struct so can't return any errors. So no need to trap - // any errors in this function. - - // init input params - - pa->setInputDevice(inDevice); - if(inDevice != paNoDevice) { - pa->setInputChannelCount(inputChannels); // stereo input - pa->setInputSampleFormat(PA_SAMPLE_TYPE); - pa->setInputLatency(pa->getInputDefaultHighLatency()); - //fprintf(stderr,"PA in; low: %f high: %f\n", pa->getInputDefaultLowLatency(), pa->getInputDefaultHighLatency()); - pa->setInputHostApiStreamInfo(NULL); - } - - pa->setOutputDevice(paNoDevice); - - // init output params - - pa->setOutputDevice(outDevice); - if(outDevice != paNoDevice) { - pa->setOutputChannelCount(outputChannels); // stereo output - pa->setOutputSampleFormat(PA_SAMPLE_TYPE); - pa->setOutputLatency(pa->getOutputDefaultHighLatency()); - //fprintf(stderr,"PA out; low: %f high: %f\n", pa->getOutputDefaultLowLatency(), pa->getOutputDefaultHighLatency()); - pa->setOutputHostApiStreamInfo(NULL); - } - - // init params that affect input and output - - /* - DR 2013: - - On Linux, setting this to wxGetApp().m_framesPerBuffer caused - intermittant break up on the audio from my IC7200 on Ubuntu 14. - After a day of bug hunting I found that 0, as recommended by the - PortAudio documentation, fixed the problem. - - DR 2018: - - During 700D testing some break up in from radio audio, so made - this adjustable again. - */ - - pa->setFramesPerBuffer(wxGetApp().m_framesPerBuffer); - - pa->setSampleRate(sampleRate); - pa->setStreamFlags(paClipOff); -} - //------------------------------------------------------------------------- // startRxStream() //------------------------------------------------------------------------- void MainFrame::startRxStream() { int src_error; - const PaDeviceInfo *deviceInfo1a = NULL, *deviceInfo1b = NULL, *deviceInfo2a = NULL, *deviceInfo2b = NULL; - int inputChannels1, outputChannels1, inputChannels2, outputChannels2; - bool two_rx=false; - bool two_tx=false; if (g_verbose) fprintf(stderr, "startRxStream .....\n"); if(!m_RxRunning) { m_RxRunning = true; - if(Pa_Initialize()) + + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->setOnEngineError([&](IAudioEngine&, std::string error, void* state) { + wxMessageBox(wxString::Format( + "Error encountered while initializing the audio engine: %s.", + error), wxT("Error"), wxOK, this); + }, nullptr); + engine->start(); + + if (g_nSoundCards == 0) { - wxMessageBox(wxT("Port Audio failed to initialize"), wxT("Pa_Initialize"), wxOK); - } - - m_rxInPa = new PortAudioWrap(); - if(g_soundCard1InDeviceNum != g_soundCard1OutDeviceNum) - two_rx=true; - if(g_soundCard2InDeviceNum != g_soundCard2OutDeviceNum) - two_tx=true; - - if (g_verbose) fprintf(stderr, "two_rx: %d two_tx: %d\n", two_rx, two_tx); - if(two_rx) - m_rxOutPa = new PortAudioWrap(); - else - m_rxOutPa = m_rxInPa; - - if (g_nSoundCards == 0) { wxMessageBox(wxT("No Sound Cards configured, use Tools - Audio Config to configure"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - m_RxRunning = false; - Pa_Terminate(); - return; - } - - // Init Sound card 1 ---------------------------------------------- - // sanity check on sound card device numbers - if ((m_rxInPa->getDeviceCount() <= g_soundCard1InDeviceNum) || - (m_rxOutPa->getDeviceCount() <= g_soundCard1OutDeviceNum)) { - wxMessageBox(wxT("Sound Card 1 not present"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - m_RxRunning = false; - Pa_Terminate(); - return; - } - - // work out how many input channels this device supports. - - deviceInfo1a = Pa_GetDeviceInfo(g_soundCard1InDeviceNum); - if (deviceInfo1a == NULL) { - wxMessageBox(wxT("Couldn't get input device info from Port Audio for Sound Card 1"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - m_RxRunning = false; - Pa_Terminate(); - return; - } - if (deviceInfo1a->maxInputChannels == 1) - inputChannels1 = 1; - else - inputChannels1 = 2; - - // Grab the info for the FreeDV->speaker device as well and ensure we're using - // the smallest number of common channels (1 or 2). - deviceInfo1b = Pa_GetDeviceInfo(g_soundCard1OutDeviceNum); - if (deviceInfo1b == NULL) { - wxMessageBox(wxT("Couldn't get output device info from Port Audio for Sound Card 1"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; m_RxRunning = false; - Pa_Terminate(); + + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + return; } - if (deviceInfo1b->maxOutputChannels == 1) - outputChannels1 = 1; - else - outputChannels1 = 2; - - if(two_rx) { - initPortAudioDevice(m_rxInPa, g_soundCard1InDeviceNum, paNoDevice, 1, - g_soundCard1SampleRate, inputChannels1, inputChannels1); - initPortAudioDevice(m_rxOutPa, paNoDevice, g_soundCard1OutDeviceNum, 1, - g_soundCard1SampleRate, outputChannels1, outputChannels1); - } - else - { - initPortAudioDevice(m_rxInPa, g_soundCard1InDeviceNum, g_soundCard1OutDeviceNum, 1, - g_soundCard1SampleRate, inputChannels1, outputChannels1); - } - - // Init Sound Card 2 ------------------------------------------------ - - if (g_nSoundCards == 2) { - - m_txInPa = new PortAudioWrap(); - if(two_tx) - m_txOutPa = new PortAudioWrap(); - else - m_txOutPa = m_txInPa; - - // sanity check on sound card device numbers - - //printf("m_txInPa->getDeviceCount(): %d\n", m_txInPa->getDeviceCount()); - //printf("g_soundCard2InDeviceNum: %d\n", g_soundCard2InDeviceNum); - //printf("g_soundCard2OutDeviceNum: %d\n", g_soundCard2OutDeviceNum); - - if ((m_txInPa->getDeviceCount() <= g_soundCard2InDeviceNum) || - (m_txOutPa->getDeviceCount() <= g_soundCard2OutDeviceNum)) { - wxMessageBox(wxT("Sound Card 2 not present"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - delete m_txInPa; - if(two_tx) - delete m_txOutPa; - m_RxRunning = false; - Pa_Terminate(); - return; + else if (g_nSoundCards == 1) + { + // RX-only setup. + // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. + rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + + bool failed = false; + if (!rxInSoundDevice) + { + wxMessageBox(wxString::Format("Could not find RX input sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard1InDeviceName), wxT("Error"), wxOK); + failed = true; } - - deviceInfo2a = Pa_GetDeviceInfo(g_soundCard2InDeviceNum); - if (deviceInfo2a == NULL) { - wxMessageBox(wxT("Couldn't get device info from Port Audio for Sound Card 2"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - delete m_txInPa; - if(two_tx) - delete m_txOutPa; + + if (!rxOutSoundDevice) + { + wxMessageBox(wxString::Format("Could not find RX output sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard1OutDeviceName), wxT("Error"), wxOK); + failed = true; + } + + if (failed) + { + if (rxInSoundDevice) + { + rxInSoundDevice.reset(); + } + + if (rxOutSoundDevice) + { + rxOutSoundDevice.reset(); + } + m_RxRunning = false; - Pa_Terminate(); + + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + return; } - if (deviceInfo2a->maxInputChannels == 1) - inputChannels2 = 1; - else - inputChannels2 = 2; + } + else + { + // RX + TX setup + // Same note as above re: number of channels. + rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard2SampleRate, 2); + txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard2SampleRate, 2); + txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + + bool failed = false; + if (!rxInSoundDevice) + { + wxMessageBox(wxString::Format("Could not find RX input sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard1InDeviceName), wxT("Error"), wxOK); + failed = true; + } + + if (!rxOutSoundDevice) + { + wxMessageBox(wxString::Format("Could not find RX output sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard2OutDeviceName), wxT("Error"), wxOK); + failed = true; + } + + if (!txInSoundDevice) + { + wxMessageBox(wxString::Format("Could not find TX input sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard2InDeviceName), wxT("Error"), wxOK); + failed = true; + } + + if (!txOutSoundDevice) + { + wxMessageBox(wxString::Format("Could not find TX output sound device '%s'. Please check settings and try again.", wxGetApp().m_soundCard1OutDeviceName), wxT("Error"), wxOK); + failed = true; + } - // Grab info for FreeDV->radio device. - deviceInfo2b = Pa_GetDeviceInfo(g_soundCard2OutDeviceNum); - if (deviceInfo2b == NULL) { - wxMessageBox(wxT("Couldn't get device info from Port Audio for Sound Card 2"), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - delete m_txInPa; - if(two_tx) - delete m_txOutPa; + if (failed) + { + if (rxInSoundDevice) + { + rxInSoundDevice.reset(); + } + + if (rxOutSoundDevice) + { + rxOutSoundDevice.reset(); + } + + if (txInSoundDevice) + { + txInSoundDevice.reset(); + } + + if (txOutSoundDevice) + { + txOutSoundDevice.reset(); + } + m_RxRunning = false; - Pa_Terminate(); + + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + return; } - if (deviceInfo2b->maxOutputChannels == 1) - outputChannels2 = 1; - else - outputChannels2 = 2; - - if(two_tx) { - initPortAudioDevice(m_txInPa, g_soundCard2InDeviceNum, paNoDevice, 2, - g_soundCard2SampleRate, inputChannels2, inputChannels2); - initPortAudioDevice(m_txOutPa, paNoDevice, g_soundCard2OutDeviceNum, 2, - g_soundCard2SampleRate, outputChannels2, outputChannels2); - } - else - initPortAudioDevice(m_txInPa, g_soundCard2InDeviceNum, g_soundCard2OutDeviceNum, 2, - g_soundCard2SampleRate, inputChannels2, outputChannels2); } // Init call back data structure ---------------------------------------------- g_rxUserdata = new paCallBackData; - g_rxUserdata->inputChannels1 = inputChannels1; - g_rxUserdata->outputChannels1 = outputChannels1; - if (deviceInfo2a != NULL) - { - g_rxUserdata->inputChannels2 = inputChannels2; - g_rxUserdata->outputChannels2 = outputChannels2; - } // init sample rate conversion states @@ -2194,7 +2096,7 @@ void MainFrame::startRxStream() g_rxUserdata->insrctxsf = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(g_rxUserdata->insrctxsf != NULL); - // create FIFOs used to interface between Port Audio and txRx + // create FIFOs used to interface between IAudioEngine and txRx // processing loop, which iterates about once every 20ms. // Sample rate conversion, stats for spectral plots, and // transmit processng are all performed in the txRxProcessing @@ -2265,209 +2167,264 @@ void MainFrame::startRxStream() g_rxUserdata->leftChannelVoxTone = wxGetApp().m_leftChannelVoxTone; g_rxUserdata->voxTonePhase = 0; - // Start sound card 1 ---------------------------------------------------------- - - m_rxInPa->setUserData(g_rxUserdata); - m_rxErr = m_rxInPa->setCallback(rxCallback); - - m_rxErr = m_rxInPa->streamOpen(); - - if(m_rxErr != paNoError) { - wxMessageBox(wxT("Sound Card 1 Open/Setup error."), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - delete m_txInPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; - } - - m_rxErr = m_rxInPa->streamStart(); - if(m_rxErr != paNoError) { - wxMessageBox(wxT("Sound Card 1 Stream Start Error."), wxT("Error"), wxOK); - delete m_rxInPa; - if(two_rx) - delete m_rxOutPa; - delete m_txInPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; - } - - // Start separate output stream if needed - - if(two_rx) { - m_rxOutPa->setUserData(g_rxUserdata); - m_rxErr = m_rxOutPa->setCallback(rxCallback); - - m_rxErr = m_rxOutPa->streamOpen(); - - if(m_rxErr != paNoError) { - wxMessageBox(wxT("Sound Card 1 Second Stream Open/Setup error."), wxT("Error"), wxOK); - delete m_rxInPa; - delete m_rxOutPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; + // Set sound card callbacks + auto errorCallback = [&](IAudioDevice&, std::string error, void*) + { + CallAfter([&]() { + wxMessageBox(wxString::Format("Error encountered while processing audio: %s", error), wxT("Error"), wxOK); + }); + }; + + rxInSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + paCallBackData* cbData = static_cast(state); + short* audioData = static_cast(data); + short indata[size]; + for (int i = 0; i < size; i++, audioData += dev.getNumChannels()) + { + indata[i] = audioData[0]; } - - m_rxErr = m_rxOutPa->streamStart(); - if(m_rxErr != paNoError) { - wxMessageBox(wxT("Sound Card 1 Second Stream Start Error."), wxT("Error"), wxOK); - m_rxInPa->stop(); - m_rxInPa->streamClose(); - delete m_rxInPa; - delete m_rxOutPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; + if (codec2_fifo_write(cbData->infifo1, indata, size)) + { + g_infifo1_full++; } - } - - if (g_verbose) fprintf(stderr, "started stream 1\n"); - - // Start sound card 2 ---------------------------------------------------------- - - if (g_nSoundCards == 2) { - - // question: can we use same callback data - // (g_rxUserdata)or both sound card callbacks? Is there a - // chance of them both being called at the same time? We - // could need a mutex ... - - m_txInPa->setUserData(g_rxUserdata); - m_txErr = m_txInPa->setCallback(txCallback); - m_txErr = m_txInPa->streamOpen(); - - if(m_txErr != paNoError) { - if (g_verbose) fprintf(stderr, "Err: %d\n", m_txErr); - wxMessageBox(wxT("Sound Card 2 Open/Setup error."), wxT("Error"), wxOK); - m_rxInPa->stop(); - m_rxInPa->streamClose(); - delete m_rxInPa; - if(two_rx) { - m_rxOutPa->stop(); - m_rxOutPa->streamClose(); - delete m_rxOutPa; + }, g_rxUserdata); + + rxInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[1]++; + }, nullptr); + + rxInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[0]++; + }, nullptr); + + rxInSoundDevice->setOnAudioError(errorCallback, nullptr); + + if (txInSoundDevice && txOutSoundDevice) + { + rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + paCallBackData* cbData = static_cast(state); + short* audioData = static_cast(data); + short outdata[size]; + + if (codec2_fifo_read(cbData->outfifo2, outdata, size) == 0) + { + if (dev.getNumChannels() == 2) + { + // write signal to both channels */ + for(int i = 0; i < size; i++, audioData += 2) + { + audioData[0] = outdata[i]; + audioData[1] = outdata[i]; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = outdata[i]; + } + } } - delete m_txInPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; - } - m_txErr = m_txInPa->streamStart(); - if(m_txErr != paNoError) { - wxMessageBox(wxT("Sound Card 2 Start Error."), wxT("Error"), wxOK); - m_rxInPa->stop(); - m_rxInPa->streamClose(); - delete m_rxInPa; - if(two_rx) { - m_rxOutPa->stop(); - m_rxOutPa->streamClose(); - delete m_rxOutPa; + else + { + g_outfifo2_empty++; + + // zero output if no data available + if (dev.getNumChannels() == 2) + { + for(int i = 0; i < size; i++, audioData += 2) + { + audioData[0] = 0; + audioData[1] = 0; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = 0; + } + } } - delete m_txInPa; - if(two_tx) - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; - } - - // Start separate output stream if needed - - if (two_tx) { - - // question: can we use same callback data - // (g_rxUserdata)or both sound card callbacks? Is there a - // chance of them both being called at the same time? We - // could need a mutex ... - - m_txOutPa->setUserData(g_rxUserdata); - m_txErr = m_txOutPa->setCallback(txCallback); - m_txErr = m_txOutPa->streamOpen(); + }, g_rxUserdata); + + rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) + { + g_PAstatus2[3]++; + }, nullptr); + + rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) + { + g_PAstatus2[2]++; + }, nullptr); + + txInSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + paCallBackData* cbData = static_cast(state); + short* audioData = static_cast(data); + short indata[size]; + + if (!endingTx) + { + for(int i = 0; i < size; i++, audioData += dev.getNumChannels()) + { + indata[i] = audioData[0]; + } + if (codec2_fifo_write(cbData->infifo2, indata, size)) + { + g_infifo2_full++; + } + } + }, g_rxUserdata); + + txInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) + { + g_PAstatus2[1]++; + }, nullptr); + + txInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) + { + g_PAstatus2[0]++; + }, nullptr); + + txOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + paCallBackData* cbData = static_cast(state); + short* audioData = static_cast(data); + short outdata[size]; + + if (codec2_fifo_read(cbData->outfifo1, outdata, size) == 0) { - if(m_txErr != paNoError) { - wxMessageBox(wxT("Sound Card 2 Second Stream Open/Setup error."), wxT("Error"), wxOK); - m_rxInPa->stop(); - m_rxInPa->streamClose(); - delete m_rxInPa; - if(two_rx) { - m_rxOutPa->stop(); - m_rxOutPa->streamClose(); - delete m_rxOutPa; + // write signal to both channels if the device can support two channels. + // Otherwise, we assume we're only dealing with one channel and write + // only to that channel. + if (dev.getNumChannels() == 2) + { + for(int i = 0; i < size; i++, audioData += 2) + { + if (cbData->leftChannelVoxTone) + { + cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/g_soundCard1SampleRate; + 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]; + + audioData[1] = outdata[i]; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = outdata[i]; + } } - m_txInPa->stop(); - m_txInPa->streamClose(); - delete m_txInPa; - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; } - m_txErr = m_txOutPa->streamStart(); - if(m_txErr != paNoError) { - wxMessageBox(wxT("Sound Card 2 Second Stream Start Error."), wxT("Error"), wxOK); - m_rxInPa->stop(); - m_rxInPa->streamClose(); - m_txInPa->stop(); - m_txInPa->streamClose(); - delete m_txInPa; - if(two_rx) { - m_rxOutPa->stop(); - m_rxOutPa->streamClose(); - delete m_rxOutPa; + else + { + g_outfifo1_empty++; + + // zero output if no data available + if (dev.getNumChannels() == 2) + { + for(int i = 0; i < size; i++, audioData += 2) + { + audioData[0] = 0; + audioData[1] = 0; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = 0; + } } - delete m_txInPa; - delete m_txOutPa; - destroy_fifos(); - destroy_src(); - deleteEQFilters(g_rxUserdata); - delete g_rxUserdata; - m_RxRunning = false; - Pa_Terminate(); - return; } - } + }, g_rxUserdata); + + txOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[3]++; + }, nullptr); + + txOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[2]++; + }, nullptr); + + txInSoundDevice->setOnAudioError(errorCallback, nullptr); + txOutSoundDevice->setOnAudioError(errorCallback, nullptr); + } + else + { + rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + paCallBackData* cbData = static_cast(state); + short* audioData = static_cast(data); + short outdata[size]; + + if (codec2_fifo_read(cbData->outfifo1, outdata, size) == 0) + { + if (dev.getNumChannels() == 2) + { + // write signal to both channels */ + for(int i = 0; i < size; i++, audioData += 2) + { + audioData[0] = outdata[i]; + audioData[1] = outdata[i]; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = outdata[i]; + } + } + } + else + { + g_outfifo2_empty++; + + // zero output if no data available + if (dev.getNumChannels() == 2) + { + for(int i = 0; i < size; i++, audioData += 2) + { + audioData[0] = 0; + audioData[1] = 0; + } + } + else + { + for(int i = 0; i < size; i++, audioData++) + { + audioData[0] = 0; + } + } + } + }, g_rxUserdata); + + rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[3]++; + }, nullptr); + + rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) + { + g_PAstatus1[2]++; + }, nullptr); + } + + // Start sound devices + rxInSoundDevice->start(); + rxOutSoundDevice->start(); + if (txInSoundDevice && txOutSoundDevice) + { + txInSoundDevice->start(); + txOutSoundDevice->start(); } if (g_verbose) fprintf(stderr, "starting tx/rx processing thread\n"); @@ -2867,278 +2824,49 @@ void txRxProcessing() } } -//------------------------------------------------------------------------- -// rxCallback() -// -// Sound card 1 callback from PortAudio, that is used for processing rx -// side: -// -// + infifo1 is the "from radio" off air modem signal from the SSB rx that we send to the demod. -// + In single sound card mode outfifo1 is the "to speaker/headphones" decoded speech output. -// + In dual sound card mode outfifo1 is the "to radio" modulator signal to the SSB tx. -// -//------------------------------------------------------------------------- - -int MainFrame::rxCallback( - const void *inputBuffer, - void *outputBuffer, - unsigned long framesPerBuffer, - const PaStreamCallbackTimeInfo* timeInfo, - PaStreamCallbackFlags statusFlags, - void *userData - ) -{ - paCallBackData *cbData = (paCallBackData*)userData; - short *rptr = (short*)inputBuffer; - short *wptr = (short*)outputBuffer; - - short indata[MAX_FPB]; - short outdata[MAX_FPB]; - - unsigned int i; - - (void) timeInfo; - (void) statusFlags; - - if (statusFlags & 0x1) { // input underflow - g_PAstatus1[0]++; - } - if (statusFlags & 0x2) { // input overflow - g_PAstatus1[1]++; - } - if (statusFlags & 0x4) { // output underflow - g_PAstatus1[2]++; - } - if (statusFlags & 0x8) { // output overflow - g_PAstatus1[3]++; - } - - g_PAframesPerBuffer1 = framesPerBuffer; - - // - // RX side processing -------------------------------------------- - // - - // assemble a mono buffer and write to FIFO - - assert(framesPerBuffer < MAX_FPB); - - if (rptr) { - for(i = 0; i < framesPerBuffer; i++, rptr += cbData->inputChannels1) - indata[i] = rptr[0]; - if (codec2_fifo_write(cbData->infifo1, indata, framesPerBuffer)) { - g_infifo1_full++; - } - } - - // OK now set up output samples for this callback - - if (wptr) { - memset(outdata, 0, sizeof(short)*MAX_FPB); - if (codec2_fifo_read(cbData->outfifo1, outdata, framesPerBuffer) == 0) { - - // write signal to both channels if the device can support two channels. - // Otherwise, we assume we're only dealing with one channel and write - // only to that channel. - if (cbData->outputChannels1 == 2) - { - for(i = 0; i < framesPerBuffer; i++, wptr += 2) - { - if (cbData->leftChannelVoxTone) - { - cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/g_soundCard1SampleRate; - cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI)); - wptr[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase); - } - else - wptr[0] = outdata[i]; - - wptr[1] = outdata[i]; - } - } - else - { - for(i = 0; i < framesPerBuffer; i++, wptr++) - { - wptr[0] = outdata[i]; - } - } - } - else - { - g_outfifo1_empty++; - - // zero output if no data available - if (cbData->outputChannels1 == 2) - { - for(i = 0; i < framesPerBuffer; i++, wptr += 2) { - wptr[0] = 0; - wptr[1] = 0; - } - } - else - { - for(i = 0; i < framesPerBuffer; i++, wptr++) - { - wptr[0] = 0; - } - } - } - } - - return paContinue; -} - - -//------------------------------------------------------------------------- -// txCallback() -//------------------------------------------------------------------------- -int MainFrame::txCallback( - const void *inputBuffer, - void *outputBuffer, - unsigned long framesPerBuffer, - const PaStreamCallbackTimeInfo *outTime, - PaStreamCallbackFlags statusFlags, - void *userData - ) -{ - paCallBackData *cbData = (paCallBackData*)userData; - unsigned int i; - short *rptr = (short*)inputBuffer; - short *wptr = (short*)outputBuffer; - short indata[MAX_FPB]; - short outdata[MAX_FPB]; - - if (statusFlags & 0x1) { // input underflow - g_PAstatus2[0]++; - } - if (statusFlags & 0x2) { // input overflow - g_PAstatus2[1]++; - } - if (statusFlags & 0x4) { // output underflow - g_PAstatus2[2]++; - } - if (statusFlags & 0x8) { // output overflow - g_PAstatus2[3]++; - } - - g_PAframesPerBuffer2 = framesPerBuffer; - - // assemble a mono buffer and write to FIFO - - assert(framesPerBuffer < MAX_FPB); - - if (rptr && !endingTx) { - for(i = 0; i < framesPerBuffer; i++, rptr += cbData->inputChannels2) - indata[i] = rptr[0]; - if (codec2_fifo_write(cbData->infifo2, indata, framesPerBuffer)) { - g_infifo2_full++; - } - } - - // OK now set up output samples for this callback - - if (wptr) { - if (codec2_fifo_read(cbData->outfifo2, outdata, framesPerBuffer) == 0) { - if (cbData->outputChannels2 == 2) - { - // write signal to both channels */ - for(i = 0; i < framesPerBuffer; i++, wptr += 2) { - wptr[0] = outdata[i]; - wptr[1] = outdata[i]; - } - } - else - { - for(i = 0; i < framesPerBuffer; i++, wptr++) { - wptr[0] = outdata[i]; - } - } - } - else { - g_outfifo2_empty++; - // zero output if no data available - if (cbData->outputChannels2 == 2) - { - for(i = 0; i < framesPerBuffer; i++, wptr += 2) { - wptr[0] = 0; - wptr[1] = 0; - } - } - else - { - for(i = 0; i < framesPerBuffer; i++, wptr++) { - wptr[0] = 0; - } - } - } - } - - return paContinue; -} - -int MainFrame::getSoundCardIDFromName(wxString& name, bool input) -{ - int result = -1; - - if (name != "none") - { - PaError paResult = Pa_Initialize(); - if (paResult == paNoError) - { - for (PaDeviceIndex index = 0; index < Pa_GetDeviceCount(); index++) - { - const PaDeviceInfo* device = Pa_GetDeviceInfo(index); - wxString deviceName = wxString::FromUTF8(device->name).Trim(); - deviceName = deviceName.Trim(); - if (name == deviceName) - { - result = index; - break; - } - } - } - else - { - fprintf(stderr, "WARNING: could not initialize PortAudio (err=%d, txt=%s)\n", paResult, Pa_GetErrorText(paResult)); - } - Pa_Terminate(); - } - return result; -} - bool MainFrame::validateSoundCardSetup() { bool canRun = true; // Translate device names to IDs - g_soundCard1InDeviceNum = getSoundCardIDFromName(wxGetApp().m_soundCard1InDeviceName, true); - g_soundCard1OutDeviceNum = getSoundCardIDFromName(wxGetApp().m_soundCard1OutDeviceName, false); - g_soundCard2InDeviceNum = getSoundCardIDFromName(wxGetApp().m_soundCard2InDeviceName, true); - g_soundCard2OutDeviceNum = getSoundCardIDFromName(wxGetApp().m_soundCard2OutDeviceName, false); + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->setOnEngineError([&](IAudioEngine&, std::string error, void* state) { + CallAfter([&]() { + wxMessageBox(wxString::Format( + "Error encountered while initializing the audio engine: %s.", + error), wxT("Error"), wxOK, this); + }); + }, nullptr); + engine->start(); + + // For the purposes of validation, number of channels isn't necessary. + auto soundCard1InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 1); + auto soundCard1OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 1); + auto soundCard2InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard2SampleRate, 1); + auto soundCard2OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard2SampleRate, 1); - if (wxGetApp().m_soundCard1InDeviceName != "none" && g_soundCard1InDeviceNum == -1) + if (wxGetApp().m_soundCard1InDeviceName != "none" && !soundCard1InDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().m_soundCard1InDeviceName), wxT("Sound Device Removed"), wxOK, this); canRun = false; } - else if (canRun && wxGetApp().m_soundCard1OutDeviceName != "none" && g_soundCard1OutDeviceNum == -1) + else if (canRun && wxGetApp().m_soundCard1OutDeviceName != "none" && !soundCard1OutDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().m_soundCard1OutDeviceName), wxT("Sound Device Removed"), wxOK, this); canRun = false; } - else if (canRun && wxGetApp().m_soundCard2InDeviceName != "none" && g_soundCard2InDeviceNum == -1) + else if (canRun && wxGetApp().m_soundCard2InDeviceName != "none" && !soundCard2InDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().m_soundCard2InDeviceName), wxT("Sound Device Removed"), wxOK, this); canRun = false; } - else if (canRun && wxGetApp().m_soundCard2OutDeviceName != "none" && g_soundCard2OutDeviceNum == -1) + else if (canRun && wxGetApp().m_soundCard2OutDeviceName != "none" && !soundCard2OutDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", @@ -3147,9 +2875,9 @@ bool MainFrame::validateSoundCardSetup() } g_nSoundCards = 0; - if ((g_soundCard1InDeviceNum > -1) && (g_soundCard1OutDeviceNum > -1)) { + if (soundCard1InDevice && soundCard1OutDevice) { g_nSoundCards = 1; - if ((g_soundCard2InDeviceNum > -1) && (g_soundCard2OutDeviceNum > -1)) + if (soundCard2InDevice && soundCard2OutDevice) g_nSoundCards = 2; } @@ -3159,7 +2887,10 @@ bool MainFrame::validateSoundCardSetup() wxMessageBox(wxString("It looks like this is your first time running FreeDV. Please go to Tools->Audio Config... to choose your sound card(s) before using."), wxT("First Time Setup"), wxOK, this); canRun = false; } - + + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + return canRun; } diff --git a/src/main.h b/src/main.h index 1cffb0bc3..154d6e52b 100644 --- a/src/main.h +++ b/src/main.h @@ -81,9 +81,7 @@ #include "plot_scatter.h" #include "plot_waterfall.h" #include "plot_spectrum.h" -#include "pa_wrapper.h" #include "sndfile.h" -#include "portaudio.h" #include "dlg_audiooptions.h" #include "dlg_filter.h" #include "dlg_options.h" @@ -93,6 +91,8 @@ #include "serialport.h" #include "pskreporter.h" #include "freedv_interface.h" +#include "audio/AudioEngineFactory.h" +#include "audio/IAudioDevice.h" #define _USE_TIMER 1 #define _USE_ONIDLE 1 @@ -115,11 +115,7 @@ enum { extern int g_verbose; extern int g_nSoundCards; -extern int g_soundCard1InDeviceNum; -extern int g_soundCard1OutDeviceNum; extern int g_soundCard1SampleRate; -extern int g_soundCard2InDeviceNum; -extern int g_soundCard2OutDeviceNum; extern int g_soundCard2SampleRate; // Voice Keyer Constants @@ -376,10 +372,6 @@ typedef struct paCallBackData , outfifo2(nullptr) , rxinfifo(nullptr) , rxoutfifo(nullptr) - , inputChannels1(0) - , inputChannels2(0) - , outputChannels1(0) - , outputChannels2(0) , sbqMicInBass(nullptr) , sbqMicInTreble(nullptr) , sbqMicInMid(nullptr) @@ -416,9 +408,6 @@ typedef struct paCallBackData struct FIFO *rxinfifo; struct FIFO *rxoutfifo; - int inputChannels1, inputChannels2; - int outputChannels1, outputChannels2; - // EQ filter states void *sbqMicInBass; void *sbqMicInTreble; @@ -502,14 +491,6 @@ class MainFrame : public TopFrame bool m_RxRunning; - PortAudioWrap *m_rxInPa; - PortAudioWrap *m_rxOutPa; - PortAudioWrap *m_txInPa; - PortAudioWrap *m_txOutPa; - - PaError m_rxErr; - PaError m_txErr; - txRxThread* m_txRxThread; bool OpenHamlibRig(); @@ -529,30 +510,6 @@ class MainFrame : public TopFrame void destroy_fifos(void); void destroy_src(void); - void autoDetectSoundCards(PortAudioWrap *pa); - - static int rxCallback( - const void *inBuffer, - void *outBuffer, - unsigned long framesPerBuffer, - const PaStreamCallbackTimeInfo *outTime, - PaStreamCallbackFlags statusFlags, - void *userData - ); - - static int txCallback( - const void *inBuffer, - void *outBuffer, - unsigned long framesPerBuffer, - const PaStreamCallbackTimeInfo *outTime, - PaStreamCallbackFlags statusFlags, - void *userData - ); - - - void initPortAudioDevice(PortAudioWrap *pa, int inDevice, int outDevice, - int soundCard, int sampleRate, int inputChannels, - int outputChannels); void togglePTT(void); @@ -651,6 +608,11 @@ class MainFrame : public TopFrame void OnChangeReportFrequency( wxCommandEvent& event ); private: + std::shared_ptr rxInSoundDevice; + std::shared_ptr rxOutSoundDevice; + std::shared_ptr txInSoundDevice; + std::shared_ptr txOutSoundDevice; + bool m_useMemory; wxTextCtrl* m_tc; int m_zoom; @@ -708,7 +670,11 @@ class txRxThread : public wxThread txRxProcessing(); wxThread::Sleep(20); } - Pa_Terminate(); + + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + return NULL; } diff --git a/src/ongui.cpp b/src/ongui.cpp index 62f7d9734..3bdc39961 100644 --- a/src/ongui.cpp +++ b/src/ongui.cpp @@ -197,7 +197,10 @@ bool MainFrame::OpenHamlibRig() { void MainFrame::OnCloseFrame(wxCloseEvent& event) { //fprintf(stderr, "MainFrame::OnCloseFrame()\n"); - Pa_Terminate(); + auto engine = AudioEngineFactory::GetAudioEngine(); + engine->stop(); + engine->setOnEngineError(nullptr, nullptr); + Destroy(); } diff --git a/src/pa_wrapper.cpp b/src/pa_wrapper.cpp deleted file mode 100644 index 6ff86b2f3..000000000 --- a/src/pa_wrapper.cpp +++ /dev/null @@ -1,351 +0,0 @@ -//========================================================================== -// Name: pa_wrapper.cpp -// Purpose: Implements a wrapper class around the PortAudio library. -// Created: August 12, 2012 -// Authors: David Rowe, David Witten -// -// License: -// -// 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 "pa_wrapper.h" - -double PortAudioWrap::standardSampleRates[] = -{ - 8000.0, 9600.0, - 11025.0, 12000.0, - 16000.0, 22050.0, - 24000.0, 32000.0, - 44100.0, 48000.0, - 88200.0, 96000.0, - 192000.0, -1 // negative terminated list -}; - -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= -// PortAudioWrap() -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= -PortAudioWrap::PortAudioWrap() -{ - m_pStream = NULL; - m_pUserData = NULL; - m_samplerate = 0; - m_framesPerBuffer = 0; - m_statusFlags = 0; - m_pStreamCallback = NULL; - m_pStreamFinishedCallback = NULL; - m_pTimeInfo = 0; - m_newdata = false; - -// loadData(); -} - -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= -// ~PortAudioWrap() -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= -PortAudioWrap::~PortAudioWrap() -{ -} - -//---------------------------------------------------------------- -// streamOpen() -//---------------------------------------------------------------- -PaError PortAudioWrap::streamOpen() -{ - return Pa_OpenStream( - &m_pStream, - m_inputBuffer.device == paNoDevice ? NULL : &m_inputBuffer, - m_outputBuffer.device == paNoDevice ? NULL : &m_outputBuffer, - m_samplerate, - m_framesPerBuffer, - m_statusFlags, - *m_pStreamCallback, - m_pUserData - ); -} - -//---------------------------------------------------------------- -// streamStart() -//---------------------------------------------------------------- -PaError PortAudioWrap::streamStart() -{ - return Pa_StartStream(m_pStream); -} - -//---------------------------------------------------------------- -// streamClose() -//---------------------------------------------------------------- -PaError PortAudioWrap::streamClose() -{ - if(isOpen()) - { - PaError rv = Pa_CloseStream(m_pStream); - return rv; - } - else - { - return paNoError; - } -} - -//---------------------------------------------------------------- -// terminate() -//---------------------------------------------------------------- -void PortAudioWrap::terminate() -{ - if(Pa_IsStreamStopped(m_pStream) != paNoError) - { - Pa_StopStream(m_pStream); - } - Pa_Terminate(); -} - -//---------------------------------------------------------------- -// stop() -//---------------------------------------------------------------- -void PortAudioWrap::stop() -{ - Pa_StopStream(m_pStream); -} - -//---------------------------------------------------------------- -// abort() -//---------------------------------------------------------------- -void PortAudioWrap::abort() -{ - Pa_AbortStream(m_pStream); -} - -//---------------------------------------------------------------- -// isStopped() -//---------------------------------------------------------------- -bool PortAudioWrap::isStopped() const -{ - PaError ret = Pa_IsStreamStopped(m_pStream); - return ret; -} - -//---------------------------------------------------------------- -// isActive() -//---------------------------------------------------------------- -bool PortAudioWrap::isActive() const -{ - PaError ret = Pa_IsStreamActive(m_pStream); - return ret; -} - -//---------------------------------------------------------------- -// isOpen() -//---------------------------------------------------------------- -bool PortAudioWrap::isOpen() const -{ - return (m_pStream != NULL); -} - -//---------------------------------------------------------------- -// getDefaultInputDevice() -//---------------------------------------------------------------- -PaDeviceIndex PortAudioWrap::getDefaultInputDevice() -{ - return Pa_GetDefaultInputDevice(); -} - -//---------------------------------------------------------------- -// getDefaultOutputDevice() -//---------------------------------------------------------------- -PaDeviceIndex PortAudioWrap::getDefaultOutputDevice() -{ - return Pa_GetDefaultOutputDevice(); -} - -//---------------------------------------------------------------- -// setInputChannelCount() -//---------------------------------------------------------------- -PaError PortAudioWrap::setInputChannelCount(int count) -{ - m_inputBuffer.channelCount = count; - return paNoError; -} - -//---------------------------------------------------------------- -// getInputChannelCount() -//---------------------------------------------------------------- -PaError PortAudioWrap::getInputChannelCount() -{ - return m_inputBuffer.channelCount; -} - -//---------------------------------------------------------------- -// setInputSampleFormat() -//---------------------------------------------------------------- -PaError PortAudioWrap::setInputSampleFormat(PaSampleFormat format) -{ - m_inputBuffer.sampleFormat = format; - return paNoError; -} - -//---------------------------------------------------------------- -// setInputLatency() -//---------------------------------------------------------------- -PaError PortAudioWrap::setInputLatency(PaTime latency) -{ - m_inputBuffer.suggestedLatency = latency; - return paNoError; -} - -//---------------------------------------------------------------- -// setInputHostApiStreamInfo() -//---------------------------------------------------------------- -void PortAudioWrap::setInputHostApiStreamInfo(void *info) -{ - m_inputBuffer.hostApiSpecificStreamInfo = info; -} - -//---------------------------------------------------------------- -// getInputDefaultLowLatency() -//---------------------------------------------------------------- -PaTime PortAudioWrap::getInputDefaultLowLatency() -{ - return Pa_GetDeviceInfo(m_inputBuffer.device)->defaultLowInputLatency; -} - -//---------------------------------------------------------------- -// getInputDefaultHighLatency() -//---------------------------------------------------------------- -PaTime PortAudioWrap::getInputDefaultHighLatency() -{ - return Pa_GetDeviceInfo(m_inputBuffer.device)->defaultHighInputLatency; -} - -//---------------------------------------------------------------- -// setOutputChannelCount() -//---------------------------------------------------------------- -PaError PortAudioWrap::setOutputChannelCount(int count) -{ - m_outputBuffer.channelCount = count; - return paNoError; -} - -//---------------------------------------------------------------- -// getOutputChannelCount() -//---------------------------------------------------------------- -const int PortAudioWrap::getOutputChannelCount() -{ - return m_outputBuffer.channelCount; -} - -//---------------------------------------------------------------- -// getDeviceName() -//---------------------------------------------------------------- -const char *PortAudioWrap::getDeviceName(PaDeviceIndex dev) -{ - const PaDeviceInfo *info; - info = Pa_GetDeviceInfo(dev); - return info->name; -} - -//---------------------------------------------------------------- -// setOutputSampleFormat() -//---------------------------------------------------------------- -PaError PortAudioWrap::setOutputSampleFormat(PaSampleFormat format) -{ - m_outputBuffer.sampleFormat = format; - return paNoError; -} - -//---------------------------------------------------------------- -// setOutputLatency() -//---------------------------------------------------------------- -PaError PortAudioWrap::setOutputLatency(PaTime latency) -{ - m_outputBuffer.suggestedLatency = latency; - return paNoError; -} - -//---------------------------------------------------------------- -// setOutputHostApiStreamInfo() -//---------------------------------------------------------------- -void PortAudioWrap::setOutputHostApiStreamInfo(void *info) -{ - m_outputBuffer.hostApiSpecificStreamInfo = info; -} - -//---------------------------------------------------------------- -// getOutputDefaultLowLatency() -//---------------------------------------------------------------- -PaTime PortAudioWrap::getOutputDefaultLowLatency() -{ - return Pa_GetDeviceInfo(m_outputBuffer.device)->defaultLowOutputLatency; -} - -//---------------------------------------------------------------- -// getOutputDefaultHighLatency() -//---------------------------------------------------------------- -PaTime PortAudioWrap::getOutputDefaultHighLatency() -{ - return Pa_GetDeviceInfo(m_outputBuffer.device)->defaultHighOutputLatency; -} - -//---------------------------------------------------------------- -// setFramesPerBuffer() -//---------------------------------------------------------------- -PaError PortAudioWrap::setFramesPerBuffer(unsigned long size) -{ - m_framesPerBuffer = size; - return paNoError; -} - -//---------------------------------------------------------------- -// setSampleRate() -//---------------------------------------------------------------- -PaError PortAudioWrap::setSampleRate(unsigned long rate) -{ - m_samplerate = rate; - return paNoError; -} - -//---------------------------------------------------------------- -// setStreamFlags() -//---------------------------------------------------------------- -PaError PortAudioWrap::setStreamFlags(PaStreamFlags flags) -{ - m_statusFlags = flags; - return paNoError; -} - -//---------------------------------------------------------------- -// setInputDevice() -//---------------------------------------------------------------- -PaError PortAudioWrap::setInputDevice(PaDeviceIndex index) -{ - m_inputBuffer.device = index; - return paNoError; -} - -//---------------------------------------------------------------- -// setOutputDevice() -//---------------------------------------------------------------- -PaError PortAudioWrap::setOutputDevice(PaDeviceIndex index) -{ - m_outputBuffer.device = index; - return paNoError; -} - -//---------------------------------------------------------------- -// setCallback() -//---------------------------------------------------------------- -PaError PortAudioWrap::setCallback(PaStreamCallback *callback) -{ - m_pStreamCallback = callback; - return paNoError; -} - diff --git a/src/pa_wrapper.h b/src/pa_wrapper.h deleted file mode 100644 index 8e4df44c8..000000000 --- a/src/pa_wrapper.h +++ /dev/null @@ -1,124 +0,0 @@ -//========================================================================== -// Name: pa_wrapper.h -// Purpose: Defines a wrapper class around PortAudio -// Created: August 12, 2012 -// Authors: David Rowe, David Witten -// -// License: -// -// 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 FDMDV2_PA_WRAPPER_H -#define FDMDV2_PA_WRAPPER_H - -#include -#include -#include "defines.h" -#include "codec2_fdmdv.h" -#include "codec2.h" -#include "portaudio.h" - -#define PA_SAMPLE_TYPE paInt16 //paFloat32 -#define FRAMES_PER_BUFFER (64) - -typedef float SAMPLE; - -class PortAudioWrap -{ - public: - PortAudioWrap(); - ~PortAudioWrap(); - -// float m_av_mag[FDMDV_NSPEC]; - - private: - PaStream *m_pStream; - void *m_pUserData; - PaStreamCallback *m_pStreamCallback; - PaStreamFinishedCallback *m_pStreamFinishedCallback; - const PaStreamCallbackTimeInfo *m_pTimeInfo; - struct FDMDV *m_pFDMDV_state; - PaStreamParameters m_inputBuffer; - PaStreamParameters m_outputBuffer; - int m_samplerate; - unsigned long m_framesPerBuffer; - PaStreamCallbackFlags m_statusFlags; - bool m_newdata; - - public: - - void averageData(float mag_dB[]); - - int getDeviceCount() { return Pa_GetDeviceCount(); } - PaDeviceIndex getDefaultInputDevice(); - PaDeviceIndex getDefaultOutputDevice(); - PaStreamParameters *getDeviceInfo(PaDeviceIndex idx); - - PaError setFramesPerBuffer(unsigned long size); - PaError setSampleRate(unsigned long size); - - PaError setStreamFlags(PaStreamFlags flags); - PaError setCallback(PaStreamCallback *m_pStreamCallback); - PaError setStreamCallback(PaStream *stream, PaStreamCallback* callback) { m_pStreamCallback = callback; return 0;} - PaError setStreamFinishedCallback(PaStream *stream, PaStreamFinishedCallback* m_pStreamFinishedCallback); - - void setInputBuffer(const PaStreamParameters& inputBuffer) {this->m_inputBuffer = inputBuffer;} - PaError setInputDevice(PaDeviceIndex dev); - PaError setInputChannelCount(int count); - int getInputChannelCount(); - PaError setInputSampleFormat(PaSampleFormat format); - PaError setInputSampleRate(PaSampleFormat format); - PaError setInputLatency(PaTime latency); - void setInputHostApiStreamInfo(void *info = NULL); - PaTime getInputDefaultLowLatency(); - PaTime getInputDefaultHighLatency(); - const char *getDeviceName(PaDeviceIndex dev); - - PaError setOutputDevice(PaDeviceIndex dev); - PaError setOutputChannelCount(int count); - const int getOutputChannelCount(); - PaError setOutputSampleFormat(PaSampleFormat format); - PaError setOutputLatency(PaTime latency); - void setOutputHostApiStreamInfo(void *info = NULL); - PaTime getOutputDefaultLowLatency(); - PaTime getOutputDefaultHighLatency(); - - void setFdmdvState(FDMDV* fdmdv_state) {this->m_pFDMDV_state = fdmdv_state;} - void setOutputBuffer(const PaStreamParameters& outputBuffer) {this->m_outputBuffer = outputBuffer;} - void setTimeInfo(PaStreamCallbackTimeInfo* timeInfo) {this->m_pTimeInfo = timeInfo;} - void setUserData(void* userData) {this->m_pUserData = userData;} - unsigned long getFramesPerBuffer() const {return m_framesPerBuffer;} - const PaStreamParameters& getInputBuffer() const {return m_inputBuffer;} - const PaStreamParameters& getOutputBuffer() const {return m_outputBuffer;} - const PaStreamCallbackFlags& getStatusFlags() const {return m_statusFlags;} - - FDMDV* getFdmdvState() {return m_pFDMDV_state;} - int getSamplerate() const {return m_samplerate;} - PaStream* getStream() {return m_pStream;} - void *getUserData() {return m_pUserData;} - bool getDataAvail() {return m_newdata;} - PaError streamStart(); - PaError streamClose(); - PaError streamOpen(); - void terminate(); - void stop(); - void abort(); - bool isOpen() const; - bool isStopped() const; - bool isActive() const; -// void loadData(); - - static double standardSampleRates[]; -}; - -#endif // FDMDV2_PA_WRAPPER_H diff --git a/src/util.cpp b/src/util.cpp index b553262bd..a063bd94d 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -5,6 +5,7 @@ */ #include "main.h" +#include "codec2_fdmdv.h" // Callback from plot_spectrum & plot_waterfall. would be nice to // work out a way to do this without globals. From be30f01540b2aa6b5fca835a348c5521bfe11a35 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 00:52:32 -0800 Subject: [PATCH 09/89] Use high latency and mutexes for audio devices and FIFOs. --- src/audio/PortAudioDevice.cpp | 2 +- src/audio/PortAudioEngine.cpp | 2 +- src/main.cpp | 82 ++++++++++++++++++++++++++++------- src/main.h | 5 +++ 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 60002b9fc..ccdced125 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -48,7 +48,7 @@ void PortAudioDevice::start() streamParameters.device = deviceId_; streamParameters.channelCount = numChannels_; streamParameters.sampleFormat = paInt16; - streamParameters.suggestedLatency = 0; + streamParameters.suggestedLatency = Pa_GetDeviceInfo(deviceId_)->defaultHighInputLatency; streamParameters.hostApiSpecificStreamInfo = NULL; auto error = Pa_OpenStream( diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index fda90e4a4..6e3a764d4 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -109,7 +109,7 @@ std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName streamParameters.device = device.deviceId; streamParameters.channelCount = 1; streamParameters.sampleFormat = paInt16; - streamParameters.suggestedLatency = 0; + streamParameters.suggestedLatency = Pa_GetDeviceInfo(device.deviceId)->defaultHighInputLatency; streamParameters.hostApiSpecificStreamInfo = NULL; int rateIndex = 0; diff --git a/src/main.cpp b/src/main.cpp index b1d9a4ea3..cf6773066 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2183,9 +2183,13 @@ void MainFrame::startRxStream() { indata[i] = audioData[0]; } - if (codec2_fifo_write(cbData->infifo1, indata, size)) + { - g_infifo1_full++; + std::unique_lock lk(cbData->infifo1Mutex); + if (codec2_fifo_write(cbData->infifo1, indata, size)) + { + g_infifo1_full++; + } } }, g_rxUserdata); @@ -2208,7 +2212,12 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - if (codec2_fifo_read(cbData->outfifo2, outdata, size) == 0) + int result = 0; + { + std::unique_lock lk(cbData->outfifo2Mutex); + result = codec2_fifo_read(cbData->outfifo2, outdata, size); + } + if (result == 0) { if (dev.getNumChannels() == 2) { @@ -2271,9 +2280,13 @@ void MainFrame::startRxStream() { indata[i] = audioData[0]; } - if (codec2_fifo_write(cbData->infifo2, indata, size)) + { - g_infifo2_full++; + std::unique_lock lk(cbData->infifo2Mutex); + if (codec2_fifo_write(cbData->infifo2, indata, size)) + { + g_infifo2_full++; + } } } }, g_rxUserdata); @@ -2293,7 +2306,12 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - if (codec2_fifo_read(cbData->outfifo1, outdata, size) == 0) { + int result = 0; + { + std::unique_lock lk(cbData->outfifo1Mutex); + result = codec2_fifo_read(cbData->outfifo1, outdata, size); + } + if (result == 0) { // write signal to both channels if the device can support two channels. // Otherwise, we assume we're only dealing with one channel and write @@ -2365,7 +2383,12 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - if (codec2_fifo_read(cbData->outfifo1, outdata, size) == 0) + int result = 0; + { + std::unique_lock lk(cbData->outfifo1Mutex); + result = codec2_fifo_read(cbData->outfifo1, outdata, size); + } + if (result == 0) { if (dev.getNumChannels() == 2) { @@ -2508,8 +2531,14 @@ void txRxProcessing() assert(nsam != 0); // while we have enough input samples available ... - while ((codec2_fifo_read(cbData->infifo1, insound_card, nsam) == 0) && ((g_half_duplex && !g_tx) || !g_half_duplex)) { - + while ((g_half_duplex && !g_tx) || !g_half_duplex) { + int fifoResult = 0; + { + std::unique_lock lk(cbData->infifo1Mutex); + fifoResult = codec2_fifo_read(cbData->infifo1, insound_card, nsam); + } + if (fifoResult != 0) break; + /* convert sound card sample rate FreeDV input sample rate */ nfreedv = resample(cbData->insrc1, infreedv, insound_card, freedv_samplerate, g_soundCard1SampleRate, N48, nsam); assert(nfreedv <= N48); @@ -2651,14 +2680,21 @@ void txRxProcessing() nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard1SampleRate, freedv_samplerate, N48, nfreedv); else nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getRxSpeechSampleRate(), N48, speechOutbufferSize); - codec2_fifo_write(cbData->outfifo1, outsound_card, nout); + + { + std::unique_lock lk(cbData->outfifo1Mutex); + codec2_fifo_write(cbData->outfifo1, outsound_card, nout); + } } else { if (g_analog) /* special case */ nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard2SampleRate, freedv_samplerate, N48, nfreedv); else nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard2SampleRate, freedvInterface.getRxSpeechSampleRate(), N48, speechOutbufferSize); - codec2_fifo_write(cbData->outfifo2, outsound_card, nout); + { + std::unique_lock lk(cbData->outfifo2Mutex); + codec2_fifo_write(cbData->outfifo2, outsound_card, nout); + } } } @@ -2692,8 +2728,15 @@ void txRxProcessing() int nsam_in_48 = g_soundCard2SampleRate * freedvInterface.getTxNumSpeechSamples()/freedvInterface.getTxSpeechSampleRate(); assert(nsam_in_48 < 10*N48); - while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { - + while(true) { + unsigned int fifoResult = 0; + { + std::unique_lock lk(cbData->outfifo1Mutex); + fifoResult = (unsigned)codec2_fifo_free(cbData->outfifo1); + } + + if (fifoResult < nsam_one_modem_frame) break; + // OK to generate a frame of modem output samples we need // an input frame of speech samples from the microphone. @@ -2708,7 +2751,12 @@ void txRxProcessing() // There may be recorded audio left to encode while ending TX. To handle this, // we keep reading from the FIFO until we have less than nsam_in_48 samples available. - int nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); + int nread = 0; + { + std::unique_lock lk(cbData->infifo2Mutex); + nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); + } + if (nread != 0 && endingTx) break; // optionally use file for mic input signal @@ -2813,7 +2861,11 @@ void txRxProcessing() if (g_dump_fifo_state) { fprintf(stderr, " nout: %d\n", nout); } - codec2_fifo_write(cbData->outfifo1, outsound_card, nout); + + { + std::unique_lock lk(cbData->outfifo1Mutex); + codec2_fifo_write(cbData->outfifo1, outsound_card, nout); + } } txModeChangeMutex.Unlock(); diff --git a/src/main.h b/src/main.h index 154d6e52b..9364d10f4 100644 --- a/src/main.h +++ b/src/main.h @@ -423,6 +423,11 @@ typedef struct paCallBackData bool leftChannelVoxTone; float voxTonePhase; + // Mutexes for the fifos + std::mutex infifo1Mutex; + std::mutex infifo2Mutex; + std::mutex outfifo1Mutex; + std::mutex outfifo2Mutex; } paCallBackData; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= From b5a71c29426acd2a2cbd0fc8fdd269b755106527 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 01:09:23 -0800 Subject: [PATCH 10/89] Remove PortAudio specific configuration settings. --- src/defines.h | 2 -- src/dlg_options.cpp | 22 +++++----------------- src/dlg_options.h | 1 - src/main.cpp | 4 ---- src/main.h | 1 - 5 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/defines.h b/src/defines.h index cec307b64..7209cf347 100644 --- a/src/defines.h +++ b/src/defines.h @@ -56,8 +56,6 @@ // sample rate I/O & conversion constants -#define MAX_FPB 8096 // maximum value of portAudio framesPerBuffer -#define PA_FPB 256 // default value of portAudio framesPerBuffer #define SAMPLE_RATE 48000 // 48 kHz sampling rate rec. as we can trust accuracy of sound card #define N8 160 // processing buffer size at 8 kHz #define MEM8 (FDMDV_OS_TAPS/FDMDV_OS) diff --git a/src/dlg_options.cpp b/src/dlg_options.cpp index f2d0b5e6f..4e449f5ea 100644 --- a/src/dlg_options.cpp +++ b/src/dlg_options.cpp @@ -32,8 +32,6 @@ extern int g_infifo2_full; extern int g_outfifo2_empty; extern int g_PAstatus1[4]; extern int g_PAstatus2[4]; -extern int g_PAframesPerBuffer1; -extern int g_PAframesPerBuffer2; extern wxDatagramSocket *g_sock; extern int g_dump_timing; extern int g_dump_fifo_state; @@ -324,10 +322,10 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c #endif // __WXMSW__ //---------------------------------------------------------- - // FIFO and PortAudio under/overflow counters used for debug + // FIFO and under/overflow counters used for debug //---------------------------------------------------------- - wxStaticBox* sb_fifo = new wxStaticBox(m_debugTab, wxID_ANY, _("Debug: FIFO and PortAudio Under/Over Flow Counters")); + wxStaticBox* sb_fifo = new wxStaticBox(m_debugTab, wxID_ANY, _("Debug: FIFO and Under/Over Flow Counters")); wxStaticBoxSizer* sbSizer_fifo = new wxStaticBoxSizer(sb_fifo, wxVERTICAL); wxStaticBox* sb_fifo1 = new wxStaticBox(m_debugTab, wxID_ANY, _("")); @@ -335,11 +333,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c // first line - wxStaticText *m_staticTextPA1 = new wxStaticText(m_debugTab, wxID_ANY, _(" PortAudio framesPerBuffer:"), wxDefaultPosition, wxDefaultSize, 0); - sbSizer_fifo1->Add(m_staticTextPA1, 0, wxALIGN_CENTER_VERTICAL , 5); - m_txtCtrlframesPerBuffer = new wxTextCtrl(m_debugTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(40,-1), 0); - sbSizer_fifo1->Add(m_txtCtrlframesPerBuffer, 0, 0, 5); - wxStaticText *m_staticTextFifo1 = new wxStaticText(m_debugTab, wxID_ANY, _(" Fifo Size (ms):"), wxDefaultPosition, wxDefaultSize, 0); + wxStaticText *m_staticTextFifo1 = new wxStaticText(m_debugTab, wxID_ANY, _("Fifo Size (ms):"), wxDefaultPosition, wxDefaultSize, 0); sbSizer_fifo1->Add(m_staticTextFifo1, 0, wxALIGN_CENTER_VERTICAL , 5); m_txtCtrlFifoSize = new wxTextCtrl(m_debugTab, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(40,-1), 0); sbSizer_fifo1->Add(m_txtCtrlFifoSize, 0, 0, 5); @@ -443,7 +437,6 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c m_ckbox_udp_enable->MoveBeforeInTabOrder(m_txt_udp_port); m_txt_udp_port->MoveBeforeInTabOrder(m_btn_udp_test); - m_txtCtrlframesPerBuffer->MoveBeforeInTabOrder(m_txtCtrlFifoSize); m_txtCtrlFifoSize->MoveBeforeInTabOrder(m_ckboxVerbose); m_ckboxVerbose->MoveBeforeInTabOrder(m_ckboxTxRxThreadPriority); m_ckboxTxRxThreadPriority->MoveBeforeInTabOrder(m_ckboxTxRxDumpTiming); @@ -568,7 +561,6 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) m_ckbox_udp_enable->SetValue(wxGetApp().m_udp_enable); m_txt_udp_port->SetValue(wxString::Format(wxT("%i"),wxGetApp().m_udp_port)); - m_txtCtrlframesPerBuffer->SetValue(wxString::Format(wxT("%i"),wxGetApp().m_framesPerBuffer)); m_txtCtrlFifoSize->SetValue(wxString::Format(wxT("%i"),wxGetApp().m_fifoSize_ms)); m_ckboxTxRxThreadPriority->SetValue(wxGetApp().m_txRxThreadHighPriority); @@ -682,10 +674,6 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent) m_txtAttnCarrier->GetValue().ToLong(&attn_carrier); wxGetApp().m_attn_carrier = (int)attn_carrier; - long framesPerBuffer; - m_txtCtrlframesPerBuffer->GetValue().ToLong(&framesPerBuffer); - wxGetApp().m_framesPerBuffer = (int)framesPerBuffer; - long FifoSize_ms; m_txtCtrlFifoSize->GetValue().ToLong(&FifoSize_ms); wxGetApp().m_fifoSize_ms = (int)FifoSize_ms; @@ -997,13 +985,13 @@ void OptionsDlg::DisplayFifoPACounters() { char pa_counters1[256]; // input: underflow overflow output: underflow overflow - sprintf(pa_counters1, "PortAudio1: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d framesPerBuf: %d", g_PAstatus1[0], g_PAstatus1[1], g_PAstatus1[2], g_PAstatus1[3], g_PAframesPerBuffer1); + sprintf(pa_counters1, "Audio1: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_PAstatus1[0], g_PAstatus1[1], g_PAstatus1[2], g_PAstatus1[3]); wxString pa_counters1_string(pa_counters1); m_textPA1->SetLabel(pa_counters1_string); char pa_counters2[256]; // input: underflow overflow output: underflow overflow - sprintf(pa_counters2, "PortAudio2: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d framesPerBuf: %d", g_PAstatus2[0], g_PAstatus2[1], g_PAstatus2[2], g_PAstatus2[3], g_PAframesPerBuffer2); + sprintf(pa_counters2, "Audio2: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_PAstatus2[0], g_PAstatus2[1], g_PAstatus2[2], g_PAstatus2[3]); wxString pa_counters2_string(pa_counters2); m_textPA2->SetLabel(pa_counters2_string); } diff --git a/src/dlg_options.h b/src/dlg_options.h index 3019de3f4..3f7a51255 100644 --- a/src/dlg_options.h +++ b/src/dlg_options.h @@ -133,7 +133,6 @@ class OptionsDlg : public wxDialog wxStaticText *m_textFifos; wxStaticText *m_textPA1; wxStaticText *m_textPA2; - wxTextCtrl *m_txtCtrlframesPerBuffer; wxTextCtrl *m_txtCtrlFifoSize; wxCheckBox *m_ckboxTxRxThreadPriority; wxCheckBox *m_ckboxTxRxDumpTiming; diff --git a/src/main.cpp b/src/main.cpp index cf6773066..cf67b6b20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -112,8 +112,6 @@ int g_infifo2_full; int g_outfifo2_empty; int g_PAstatus1[4]; int g_PAstatus2[4]; -int g_PAframesPerBuffer1; -int g_PAframesPerBuffer2; // playing and recording from sound files @@ -407,7 +405,6 @@ MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent) SetClientSize(w, h); SetSizeHints(size); - wxGetApp().m_framesPerBuffer = pConfig->Read(wxT("/Audio/framesPerBuffer"), (int)PA_FPB); wxGetApp().m_fifoSize_ms = pConfig->Read(wxT("/Audio/fifoSize_ms"), (int)FIFO_SIZE); wxGetApp().m_soundCard1InDeviceName = pConfig->Read(wxT("/Audio/soundCard1InDeviceName"), _("none")); @@ -719,7 +716,6 @@ MainFrame::~MainFrame() pConfig->Write(wxT("/Audio/SquelchActive"), g_SquelchActive); pConfig->Write(wxT("/Audio/SquelchLevel"), (int)(g_SquelchLevel*2.0)); - pConfig->Write(wxT("/Audio/framesPerBuffer"), wxGetApp().m_framesPerBuffer); pConfig->Write(wxT("/Audio/fifoSize_ms"), wxGetApp().m_fifoSize_ms); pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); diff --git a/src/main.h b/src/main.h index 9364d10f4..4523bc9a0 100644 --- a/src/main.h +++ b/src/main.h @@ -285,7 +285,6 @@ class MainApp : public wxApp wxRect m_rTopWindow; - int m_framesPerBuffer; int m_fifoSize_ms; // PSK Reporter configuration From 1f533fd38dcbcc5572223acc710420bea66d58c9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 11:35:34 -0800 Subject: [PATCH 11/89] Rename g_PAstatus->g_AEstatus. --- src/dlg_options.cpp | 10 +++++----- src/main.cpp | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/dlg_options.cpp b/src/dlg_options.cpp index 4e449f5ea..49c53a452 100644 --- a/src/dlg_options.cpp +++ b/src/dlg_options.cpp @@ -30,8 +30,8 @@ extern int g_infifo1_full; extern int g_outfifo1_empty; extern int g_infifo2_full; extern int g_outfifo2_empty; -extern int g_PAstatus1[4]; -extern int g_PAstatus2[4]; +extern int g_AEstatus1[4]; +extern int g_AEstatus2[4]; extern wxDatagramSocket *g_sock; extern int g_dump_timing; extern int g_dump_fifo_state; @@ -879,7 +879,7 @@ void OptionsDlg::OnFifoReset(wxCommandEvent& event) { g_infifo1_full = g_outfifo1_empty = g_infifo2_full = g_outfifo2_empty = 0; for (int i=0; i<4; i++) { - g_PAstatus1[i] = g_PAstatus2[i] = 0; + g_AEstatus1[i] = g_AEstatus2[i] = 0; } } @@ -985,13 +985,13 @@ void OptionsDlg::DisplayFifoPACounters() { char pa_counters1[256]; // input: underflow overflow output: underflow overflow - sprintf(pa_counters1, "Audio1: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_PAstatus1[0], g_PAstatus1[1], g_PAstatus1[2], g_PAstatus1[3]); + sprintf(pa_counters1, "Audio1: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_AEstatus1[0], g_AEstatus1[1], g_AEstatus1[2], g_AEstatus1[3]); wxString pa_counters1_string(pa_counters1); m_textPA1->SetLabel(pa_counters1_string); char pa_counters2[256]; // input: underflow overflow output: underflow overflow - sprintf(pa_counters2, "Audio2: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_PAstatus2[0], g_PAstatus2[1], g_PAstatus2[2], g_PAstatus2[3]); + sprintf(pa_counters2, "Audio2: inUnderflow: %d inOverflow: %d outUnderflow %d outOverflow %d", g_AEstatus2[0], g_AEstatus2[1], g_AEstatus2[2], g_AEstatus2[3]); wxString pa_counters2_string(pa_counters2); m_textPA2->SetLabel(pa_counters2_string); } diff --git a/src/main.cpp b/src/main.cpp index cf67b6b20..8d88e720f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,8 +110,8 @@ int g_infifo1_full; int g_outfifo1_empty; int g_infifo2_full; int g_outfifo2_empty; -int g_PAstatus1[4]; -int g_PAstatus2[4]; +int g_AEstatus1[4]; +int g_AEstatus2[4]; // playing and recording from sound files @@ -2115,7 +2115,7 @@ void MainFrame::startRxStream() g_infifo1_full = g_outfifo1_empty = g_infifo2_full = g_outfifo2_empty = 0; g_infifo1_full = g_outfifo1_empty = g_infifo2_full = g_outfifo2_empty = 0; for (int i=0; i<4; i++) { - g_PAstatus1[i] = g_PAstatus2[i] = 0; + g_AEstatus1[i] = g_AEstatus2[i] = 0; } // These FIFOs interface between the 20ms txRxProcessing() @@ -2191,12 +2191,12 @@ void MainFrame::startRxStream() rxInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[1]++; + g_AEstatus1[1]++; }, nullptr); rxInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[0]++; + g_AEstatus1[0]++; }, nullptr); rxInSoundDevice->setOnAudioError(errorCallback, nullptr); @@ -2257,12 +2257,12 @@ void MainFrame::startRxStream() rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { - g_PAstatus2[3]++; + g_AEstatus2[3]++; }, nullptr); rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { - g_PAstatus2[2]++; + g_AEstatus2[2]++; }, nullptr); txInSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { @@ -2289,12 +2289,12 @@ void MainFrame::startRxStream() txInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { - g_PAstatus2[1]++; + g_AEstatus2[1]++; }, nullptr); txInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { - g_PAstatus2[0]++; + g_AEstatus2[0]++; }, nullptr); txOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { @@ -2361,12 +2361,12 @@ void MainFrame::startRxStream() txOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[3]++; + g_AEstatus1[3]++; }, nullptr); txOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[2]++; + g_AEstatus1[2]++; }, nullptr); txInSoundDevice->setOnAudioError(errorCallback, nullptr); @@ -2428,12 +2428,12 @@ void MainFrame::startRxStream() rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[3]++; + g_AEstatus1[3]++; }, nullptr); rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { - g_PAstatus1[2]++; + g_AEstatus1[2]++; }, nullptr); } From 624f3da5aa3640794c56f6d5aaaefcca45c6e75e Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 11:42:26 -0800 Subject: [PATCH 12/89] Remove unnecessary FIFO mutexes. --- src/main.cpp | 75 +++++++++++----------------------------------------- src/main.h | 6 ----- 2 files changed, 15 insertions(+), 66 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8d88e720f..23c1f8ea9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2180,12 +2180,9 @@ void MainFrame::startRxStream() indata[i] = audioData[0]; } + if (codec2_fifo_write(cbData->infifo1, indata, size)) { - std::unique_lock lk(cbData->infifo1Mutex); - if (codec2_fifo_write(cbData->infifo1, indata, size)) - { - g_infifo1_full++; - } + g_infifo1_full++; } }, g_rxUserdata); @@ -2208,11 +2205,7 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - int result = 0; - { - std::unique_lock lk(cbData->outfifo2Mutex); - result = codec2_fifo_read(cbData->outfifo2, outdata, size); - } + int result = codec2_fifo_read(cbData->outfifo2, outdata, size); if (result == 0) { if (dev.getNumChannels() == 2) @@ -2277,12 +2270,9 @@ void MainFrame::startRxStream() indata[i] = audioData[0]; } + if (codec2_fifo_write(cbData->infifo2, indata, size)) { - std::unique_lock lk(cbData->infifo2Mutex); - if (codec2_fifo_write(cbData->infifo2, indata, size)) - { - g_infifo2_full++; - } + g_infifo2_full++; } } }, g_rxUserdata); @@ -2302,11 +2292,7 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - int result = 0; - { - std::unique_lock lk(cbData->outfifo1Mutex); - result = codec2_fifo_read(cbData->outfifo1, outdata, size); - } + int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { // write signal to both channels if the device can support two channels. @@ -2379,11 +2365,7 @@ void MainFrame::startRxStream() short* audioData = static_cast(data); short outdata[size]; - int result = 0; - { - std::unique_lock lk(cbData->outfifo1Mutex); - result = codec2_fifo_read(cbData->outfifo1, outdata, size); - } + int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { if (dev.getNumChannels() == 2) @@ -2527,14 +2509,8 @@ void txRxProcessing() assert(nsam != 0); // while we have enough input samples available ... - while ((g_half_duplex && !g_tx) || !g_half_duplex) { - int fifoResult = 0; - { - std::unique_lock lk(cbData->infifo1Mutex); - fifoResult = codec2_fifo_read(cbData->infifo1, insound_card, nsam); - } - if (fifoResult != 0) break; - + while (codec2_fifo_read(cbData->infifo1, insound_card, nsam) != 0 && ((g_half_duplex && !g_tx) || !g_half_duplex)) { + /* convert sound card sample rate FreeDV input sample rate */ nfreedv = resample(cbData->insrc1, infreedv, insound_card, freedv_samplerate, g_soundCard1SampleRate, N48, nsam); assert(nfreedv <= N48); @@ -2677,20 +2653,15 @@ void txRxProcessing() else nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getRxSpeechSampleRate(), N48, speechOutbufferSize); - { - std::unique_lock lk(cbData->outfifo1Mutex); - codec2_fifo_write(cbData->outfifo1, outsound_card, nout); - } + codec2_fifo_write(cbData->outfifo1, outsound_card, nout); } else { if (g_analog) /* special case */ nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard2SampleRate, freedv_samplerate, N48, nfreedv); else nout = resample(cbData->outsrc2, outsound_card, outfreedv, g_soundCard2SampleRate, freedvInterface.getRxSpeechSampleRate(), N48, speechOutbufferSize); - { - std::unique_lock lk(cbData->outfifo2Mutex); - codec2_fifo_write(cbData->outfifo2, outsound_card, nout); - } + + codec2_fifo_write(cbData->outfifo2, outsound_card, nout); } } @@ -2724,15 +2695,7 @@ void txRxProcessing() int nsam_in_48 = g_soundCard2SampleRate * freedvInterface.getTxNumSpeechSamples()/freedvInterface.getTxSpeechSampleRate(); assert(nsam_in_48 < 10*N48); - while(true) { - unsigned int fifoResult = 0; - { - std::unique_lock lk(cbData->outfifo1Mutex); - fifoResult = (unsigned)codec2_fifo_free(cbData->outfifo1); - } - - if (fifoResult < nsam_one_modem_frame) break; - + while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { // OK to generate a frame of modem output samples we need // an input frame of speech samples from the microphone. @@ -2747,12 +2710,7 @@ void txRxProcessing() // There may be recorded audio left to encode while ending TX. To handle this, // we keep reading from the FIFO until we have less than nsam_in_48 samples available. - int nread = 0; - { - std::unique_lock lk(cbData->infifo2Mutex); - nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); - } - + int nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); if (nread != 0 && endingTx) break; // optionally use file for mic input signal @@ -2858,10 +2816,7 @@ void txRxProcessing() fprintf(stderr, " nout: %d\n", nout); } - { - std::unique_lock lk(cbData->outfifo1Mutex); - codec2_fifo_write(cbData->outfifo1, outsound_card, nout); - } + codec2_fifo_write(cbData->outfifo1, outsound_card, nout); } txModeChangeMutex.Unlock(); diff --git a/src/main.h b/src/main.h index 4523bc9a0..e5a59ceac 100644 --- a/src/main.h +++ b/src/main.h @@ -421,12 +421,6 @@ typedef struct paCallBackData // optional loud tone on left channel to reliably trigger vox bool leftChannelVoxTone; float voxTonePhase; - - // Mutexes for the fifos - std::mutex infifo1Mutex; - std::mutex infifo2Mutex; - std::mutex outfifo1Mutex; - std::mutex outfifo2Mutex; } paCallBackData; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= From ab0ec85633054a8020ee32e678faf70f0ca75995 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 12:33:05 -0800 Subject: [PATCH 13/89] Fix typo preventing RX. --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 23c1f8ea9..c64fd4510 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2509,7 +2509,7 @@ void txRxProcessing() assert(nsam != 0); // while we have enough input samples available ... - while (codec2_fifo_read(cbData->infifo1, insound_card, nsam) != 0 && ((g_half_duplex && !g_tx) || !g_half_duplex)) { + while (codec2_fifo_read(cbData->infifo1, insound_card, nsam) == 0 && ((g_half_duplex && !g_tx) || !g_half_duplex)) { /* convert sound card sample rate FreeDV input sample rate */ nfreedv = resample(cbData->insrc1, infreedv, insound_card, freedv_samplerate, g_soundCard1SampleRate, N48, nsam); From 54b4c7c419579c43df98bb1ada294cbf8be6fed4 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 21 Dec 2021 16:46:43 -0800 Subject: [PATCH 14/89] Add CMake infrastructure to support PulseAudio linkage. --- CMakeLists.txt | 26 ++++++++++++++++++++++---- src/audio/AudioEngineFactory.cpp | 5 ++++- src/audio/CMakeLists.txt | 17 ++++++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 57a72532c..c3a47a9e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,8 @@ set(USE_STATIC_SPEEXDSP FALSE CACHE BOOL "Download and build static speex instead of the system library.") set(BOOTSTRAP_WXWIDGETS FALSE CACHE BOOL "Download and build static wxWidgets instead of the system library.") +set(USE_PULSEAUDIO FALSE CACHE BOOL + "Use PulseAudio instead of PortAudio for audio I/O.") if(USE_STATIC_DEPS) set(USE_STATIC_PORTAUDIO TRUE FORCE) @@ -284,9 +286,9 @@ endif(CODEC2_BUILD_DIR) # -# Find or build portaudio Library +# Find or build portaudio/PulseAudio Library # -if(NOT USE_STATIC_PORTAUDIO) +if(NOT USE_STATIC_PORTAUDIO AND NOT USE_PULSEAUDIO) message(STATUS "Looking for portaudio...") find_package(portaudio-2.0 REQUIRED) if(PORTAUDIO_FOUND) @@ -309,10 +311,26 @@ On Windows it's easiest to use the cmake option: USE_STATIC_PORTAUDIO" if(NOT ${PORTAUDIO_VERSION} EQUAL 19 AND NOT MINGW) message(WARNING "Portaudio versions other than 19 are known to have issues. You have been warned!") endif() -else(NOT USE_STATIC_PORTAUDIO) +elseif(USE_PULSEAUDIO) + message(STATUS "Finding PulseAudio...") + find_package(PulseAudio REQUIRED) + if(PULSEAUDIO_FOUND) + message(STATUS " PulseAudio library: ${PULSEAUDIO_LIBRARY}") + message(STATUS " PulseAudio headers: ${PULSEAUDIO_INCLUDE_DIR}") + list(APPEND FREEDV_LINK_LIBS ${PULSEAUDIO_LIBRARY}) + include_directories(${PULSEAUDIO_INCLUDE_DIR}) + add_definitions(-DAUDIO_ENGINE_PULSEAUDIO_ENABLE) + else() + message(FATAL_ERROR "PulseAudio library not found. +On Linux systems try installing: + pulseaudio-libs-devel (RPM based systems) + libpulse-dev (DEB based systems) +") + endif() +else() message(STATUS "Will attempt static build of portaudio.") include(cmake/Buildportaudio-2.0.cmake) -endif(NOT USE_STATIC_PORTAUDIO) +endif() # # Hamlib library diff --git a/src/audio/AudioEngineFactory.cpp b/src/audio/AudioEngineFactory.cpp index a09f0f2bf..5ad54e4b8 100644 --- a/src/audio/AudioEngineFactory.cpp +++ b/src/audio/AudioEngineFactory.cpp @@ -29,9 +29,12 @@ std::shared_ptr AudioEngineFactory::GetAudioEngine() { if (!SystemEngine_) { +#if defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) // TBD: support PulseAudio as well. +#else SystemEngine_ = std::shared_ptr(new PortAudioEngine()); +#endif // defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) } return SystemEngine_; -} \ No newline at end of file +} diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index c51cd363a..8dcca137c 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -1,8 +1,19 @@ +if(USE_PULSEAUDIO) +set(AUDIO_ENGINE_LIBRARY_SPECIFIC_FILES + ) + +else(USE_PULSEAUDIO) +set(AUDIO_ENGINE_LIBRARY_SPECIFIC_FILES + PortAudioDevice.cpp + PortAudioEngine.cpp + ) + +endif(USE_PULSEAUDIO) + add_library(fdv_audio STATIC AudioDeviceSpecification.cpp AudioEngineFactory.cpp IAudioDevice.cpp IAudioEngine.cpp - PortAudioDevice.cpp - PortAudioEngine.cpp -) \ No newline at end of file + ${AUDIO_ENGINE_LIBRARY_SPECIFIC_FILES} +) From fff5593e911552fe8c990863996df512d1585100 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 02:13:25 -0800 Subject: [PATCH 15/89] Initial PulseAudio code files. --- src/audio/AudioEngineFactory.cpp | 6 +- src/audio/CMakeLists.txt | 2 + src/audio/PulseAudioDevice.cpp | 170 +++++++++++++++++++ src/audio/PulseAudioDevice.h | 63 +++++++ src/audio/PulseAudioEngine.cpp | 282 +++++++++++++++++++++++++++++++ src/audio/PulseAudioEngine.h | 49 ++++++ 6 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 src/audio/PulseAudioDevice.cpp create mode 100644 src/audio/PulseAudioDevice.h create mode 100644 src/audio/PulseAudioEngine.cpp create mode 100644 src/audio/PulseAudioEngine.h diff --git a/src/audio/AudioEngineFactory.cpp b/src/audio/AudioEngineFactory.cpp index 5ad54e4b8..3b56d0a40 100644 --- a/src/audio/AudioEngineFactory.cpp +++ b/src/audio/AudioEngineFactory.cpp @@ -21,7 +21,11 @@ //========================================================================= #include "AudioEngineFactory.h" +#if defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) +#include "PulseAudioEngine.h" +#else #include "PortAudioEngine.h" +#endif // defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) std::shared_ptr AudioEngineFactory::SystemEngine_; @@ -30,7 +34,7 @@ std::shared_ptr AudioEngineFactory::GetAudioEngine() if (!SystemEngine_) { #if defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) - // TBD: support PulseAudio as well. + SystemEngine_ = std::shared_ptr(new PulseAudioEngine()); #else SystemEngine_ = std::shared_ptr(new PortAudioEngine()); #endif // defined(AUDIO_ENGINE_PULSEAUDIO_ENABLE) diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 8dcca137c..057a2fe08 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -1,5 +1,7 @@ if(USE_PULSEAUDIO) set(AUDIO_ENGINE_LIBRARY_SPECIFIC_FILES + PulseAudioDevice.cpp + PulseAudioEngine.cpp ) else(USE_PULSEAUDIO) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp new file mode 100644 index 000000000..ac532ac0c --- /dev/null +++ b/src/audio/PulseAudioDevice.cpp @@ -0,0 +1,170 @@ +//========================================================================= +// Name: PulseAudioDevice.cpp +// Purpose: Defines the interface to a PulseAudio device. +// +// 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 "PulseAudioDevice.h" + +PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) + : context_(context) + , mainloop_(mainloop) + , stream_(nullptr) + , devName_(devName) + , direction_(direction) + , sampleRate_(sampleRate) + , numChannels_(numChannels) +{ + // empty +} + +PulseAudioDevice::~PulseAudioDevice() +{ + if (stream_ != nullptr) + { + stop(); + } +} + +void PulseAudioDevice::start() +{ + pa_sample_spec sample_specification; + sample_specification.format = PA_SAMPLE_S16LE; + sample_specification.rate = sampleRate_; + sample_specification.channels = numChannels_; + + pa_threaded_mainloop_lock(mainloop_); + stream_ = pa_stream_new(context_, "PulseAudioDevice", &sample_specification, nullptr); + if (stream_ == nullptr) + { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, std::string("Could not create PulseAudio stream for ") + devName_, onAudioErrorState); + } + pa_threaded_mainloop_unlock(mainloop_); + return; + } + + pa_stream_set_underflow_callback(stream_, &PulseAudioDevice::StreamUnderflowCallback_, this); + pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); + + // recommended settings, i.e. server uses sensible values + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.tlength = (uint32_t) -1; + buffer_attr.prebuf = (uint32_t) -1; + buffer_attr.minreq = (uint32_t) -1; + + // Stream flags + pa_stream_flags_t flags = pa_stream_flags_t( + PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_AUTO_TIMING_UPDATE | + PA_STREAM_ADJUST_LATENCY); + + int result = 0; + if (direction_ == IAudioEngine::OUT) + { + pa_stream_set_write_callback(stream_, &PulseAudioDevice::StreamWriteCallback_, this); + result = pa_stream_connect_playback( + stream_, devName_.c_str(), &buffer_attr, + flags, NULL, NULL); + } + else + { + pa_stream_set_read_callback(stream_, &PulseAudioDevice::StreamReadCallback_, this); + result = pa_stream_connect_record( + stream_, devName_.c_str(), &buffer_attr, flags); + } + + if (result != 0) + { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, std::string("Could not connect PulseAudio stream to ") + devName_, onAudioErrorState); + } + + } + + pa_threaded_mainloop_unlock(mainloop_); +} + +void PulseAudioDevice::stop() +{ + if (stream_ != nullptr) + { + pa_threaded_mainloop_lock(mainloop_); + pa_stream_disconnect(stream_); + pa_threaded_mainloop_unlock(mainloop_); + + stream_ = nullptr; + } +} + +void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *userdata) +{ + const void* data = nullptr; + PulseAudioDevice* thisObj = static_cast(userdata); + + // Ignore errors here as they're not critical. + if (pa_stream_peek(s, &data, &length) >= 0) + { + if (thisObj->onAudioDataFunction) + { + thisObj->onAudioDataFunction(*thisObj, const_cast(data), length, thisObj->onAudioDataState); + } + + if (length > 0) + { + pa_stream_drop(s); + } + } +} + +void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *userdata) +{ + short data[length]; + PulseAudioDevice* thisObj = static_cast(userdata); + + if (thisObj->onAudioDataFunction) + { + thisObj->onAudioDataFunction(*thisObj, data, length, thisObj->onAudioDataState); + } + + pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); +} + +void PulseAudioDevice::StreamUnderflowCallback_(pa_stream *p, void *userdata) +{ + PulseAudioDevice* thisObj = static_cast(userdata); + + if (thisObj->onAudioUnderflowFunction) + { + thisObj->onAudioUnderflowFunction(*thisObj, thisObj->onAudioUnderflowState); + } +} + +void PulseAudioDevice::StreamOverflowCallback_(pa_stream *p, void *userdata) +{ + PulseAudioDevice* thisObj = static_cast(userdata); + + if (thisObj->onAudioOverflowFunction) + { + thisObj->onAudioOverflowFunction(*thisObj, thisObj->onAudioOverflowState); + } +} \ No newline at end of file diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h new file mode 100644 index 000000000..65baca989 --- /dev/null +++ b/src/audio/PulseAudioDevice.h @@ -0,0 +1,63 @@ +//========================================================================= +// Name: PulseAudioDevice.h +// Purpose: Defines the interface to a PulseAudio device. +// +// 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 PULSE_AUDIO_DEVICE_H +#define PULSE_AUDIO_DEVICE_H + +#include +#include +#include "IAudioEngine.h" +#include "IAudioDevice.h" + +class PulseAudioDevice : public IAudioDevice +{ +public: + virtual ~PulseAudioDevice(); + + virtual int getNumChannels() { return numChannels_; } + + virtual void start(); + virtual void stop(); + +protected: + // PulseAudioDevice cannot be created directly, only via PulseAudioEngine. + friend class PulseAudioEngine; + + PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); + +private: + pa_context* context_; + pa_threaded_mainloop* mainloop_; + pa_stream* stream_; + + std::string devName_; + IAudioEngine::AudioDirection direction_; + int sampleRate_; + int numChannels_; + + static void StreamReadCallback_(pa_stream *s, size_t length, void *userdata); + static void StreamWriteCallback_(pa_stream *s, size_t length, void *userdata); + static void StreamUnderflowCallback_(pa_stream *p, void *userdata); + static void StreamOverflowCallback_(pa_stream *p, void *userdata); +}; + +#endif // PULSE_AUDIO_DEVICE_H \ No newline at end of file diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp new file mode 100644 index 000000000..7b1fa2c64 --- /dev/null +++ b/src/audio/PulseAudioEngine.cpp @@ -0,0 +1,282 @@ +//========================================================================= +// Name: PulseAudioEngine.cpp +// Purpose: Defines the interface to the PulseAudio audio engine. +// +// 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 "PulseAudioDevice.h" +#include "PulseAudioEngine.h" + +PulseAudioEngine::PulseAudioEngine() + : initialized_(false) +{ + // empty +} + +PulseAudioEngine::~PulseAudioEngine() +{ + if (initialized_) + { + stop(); + } +} + +void PulseAudioEngine::start() +{ + // Allocate PA main loop and context. + mainloop_ = pa_threaded_mainloop_new(); + + if (mainloop_ == nullptr) + { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, "Could not allocate PulseAudio main loop.", onAudioErrorState); + } + return; + } + + mainloopApi_ = pa_threaded_mainloop_get_api(mainloop_); + context_ = pa_context_new(mainloopApi_, "FreeDV HF Digital Voice"); + + if (context_ == nullptr) + { + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, "Could not allocate PulseAudio context.", onAudioErrorState); + } + + pa_threaded_mainloop_free(mainloop_); + mainloop_ = nullptr; + return; + } + + pa_context_set_state_callback(context_, [](pa_context* context, void* mainloop) { + pa_threaded_mainloop *threadedML = static_cast(mainloop); + pa_threaded_mainloop_signal(threadedML, 0); + }, mainloop_); + + // Start main loop. + pa_threaded_mainloop_lock(mainloop_); + if (pa_threaded_mainloop_start(mainloop_) != 0) + { + pa_threaded_mainloop_unlock(mainloop_); + + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, "Could not start PulseAudio main loop.", onAudioErrorState); + } + + pa_context_unref(context_); + pa_threaded_mainloop_free(mainloop_); + mainloop_ = nullptr; + context_ = nullptr; + return; + } + + // Connect context to default PA server. + if (pa_context_connect(context_, NULL, PA_CONTEXT_NOFLAGS, NULL) != 0) + { + pa_threaded_mainloop_unlock(mainloop_); + + if (onAudioErrorFunction) + { + onAudioErrorFunction(*this, "Could not connect PulseAudio context.", onAudioErrorState); + } + + pa_threaded_mainloop_stop(mainloop_); + pa_context_unref(context_); + pa_threaded_mainloop_free(mainloop_); + return; + } + + // Wait for the context to be ready + for(;;) + { + pa_context_state_t context_state = pa_context_get_state(context_); + assert(PA_CONTEXT_IS_GOOD(context_state)); + if (context_state == PA_CONTEXT_READY) break; + pa_threaded_mainloop_wait(mainloop_); + } + + pa_threaded_mainloop_unlock(mainloop_); + initialized_ = true; +} + +void PulseAudioEngine::stop() +{ + if (initialized_) + { + pa_threaded_mainloop_lock(mainloop_); + pa_context_disconnect(context_); + pa_threaded_mainloop_unlock(mainloop_); + + pa_threaded_mainloop_stop(mainloop_); + pa_context_unref(context_); + pa_threaded_mainloop_free(mainloop_); + + mainloop_ = nullptr; + mainloopApi_ = nullptr; + context_ = nullptr; + initialized_ = false; + } +} + +struct PulseAudioDeviceListTemp +{ + std::vector result; + PulseAudioEngine* thisPtr; +}; + +std::vector PulseAudioEngine::getAudioDeviceList(AudioDirection direction) +{ + PulseAudioDeviceListTemp tempObj = { + .thisPtr = this + }; + + pa_operation* op = nullptr; + + pa_threaded_mainloop_lock(mainloop_); + if (direction == OUT) + { + op = pa_context_get_sink_info_list(context_, [](pa_context *c, const pa_sink_info *i, int eol, void *userdata) { + PulseAudioDeviceListTemp* tempObj = static_cast(userdata); + + AudioDeviceSpecification device; + device.deviceId = i->index; + device.name = i->name; + device.apiName = "PulseAudio"; + device.maxChannels = i->sample_spec.channels; + device.defaultSampleRate = i->sample_spec.rate; + + tempObj->result.push_back(device); + + if (eol) + { + pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); + } + }, &tempObj); + } + else + { + op = pa_context_get_source_info_list(context_, [](pa_context *c, const pa_source_info *i, int eol, void *userdata) { + PulseAudioDeviceListTemp* tempObj = static_cast(userdata); + + AudioDeviceSpecification device; + device.deviceId = i->index; + device.name = i->name; + device.apiName = "PulseAudio"; + device.maxChannels = i->sample_spec.channels; + device.defaultSampleRate = i->sample_spec.rate; + + tempObj->result.push_back(device); + + if (eol) + { + pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); + } + }, &tempObj); + } + + // Wait for the operation to complete + for(;;) + { + if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break; + pa_threaded_mainloop_wait(mainloop_); + } + + pa_threaded_mainloop_unlock(mainloop_); + + return tempObj.result; +} + +std::vector PulseAudioEngine::getSupportedSampleRates(std::string deviceName, AudioDirection direction) +{ + std::vector result; + + int index = 0; + while (IAudioEngine::StandardSampleRates[index] != -1) + { + result.push_back(IAudioEngine::StandardSampleRates[index++]); + } + + return result; +} + +struct PaDefaultAudioDeviceTemp +{ + std::string defaultSink; + std::string defaultSource; + pa_threaded_mainloop *mainloop; +}; + +AudioDeviceSpecification PulseAudioEngine::getDefaultAudioDevice(AudioDirection direction) +{ + PaDefaultAudioDeviceTemp tempData = { + .mainloop = mainloop_ + }; + + pa_threaded_mainloop_lock(mainloop_); + auto op = pa_context_get_server_info(context_, [](pa_context *c, const pa_server_info *i, void *userdata) { + PaDefaultAudioDeviceTemp* tempData = static_cast(userdata); + + tempData->defaultSink = i->default_sink_name; + tempData->defaultSource = i->default_source_name; + pa_threaded_mainloop_signal(tempData->mainloop, 0); + }, &tempData); + + // Wait for the operation to complete + for(;;) + { + if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break; + pa_threaded_mainloop_wait(mainloop_); + } + + pa_threaded_mainloop_unlock(mainloop_); + + auto devices = getAudioDeviceList(direction); + std::string defaultDeviceName = direction == IN ? tempData.defaultSource : tempData.defaultSink; + for (auto& device : devices) + { + if (device.name == defaultDeviceName) + { + return device; + } + } + + return AudioDeviceSpecification::GetInvalidDevice(); +} + +std::shared_ptr PulseAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) +{ + auto deviceList = getAudioDeviceList(direction); + + for (auto& dev : deviceList) + { + if (dev.name == deviceName) + { + auto devObj = + new PulseAudioDevice( + mainloop_, context_, deviceName, direction, sampleRate, + dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); + return std::shared_ptr(devObj); + } + } + + return nullptr; +} \ No newline at end of file diff --git a/src/audio/PulseAudioEngine.h b/src/audio/PulseAudioEngine.h new file mode 100644 index 000000000..013556b0c --- /dev/null +++ b/src/audio/PulseAudioEngine.h @@ -0,0 +1,49 @@ +//========================================================================= +// Name: PortAudioEngine.h +// Purpose: Defines the interface to the PortAudio audio engine. +// +// 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 PULSE_AUDIO_ENGINE_H +#define PULSE_AUDIO_ENGINE_H + +#include +#include "IAudioEngine.h" + +class PulseAudioEngine : public IAudioEngine +{ +public: + PulseAudioEngine(); + virtual ~PulseAudioEngine(); + + virtual void start(); + virtual void stop(); + virtual std::vector getAudioDeviceList(AudioDirection direction); + virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction); + virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels); + virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction); + +private: + bool initialized_; + pa_threaded_mainloop *mainloop_; + pa_mainloop_api *mainloopApi_; + pa_context* context_; +}; + +#endif // PULSE_AUDIO_ENGINE_H \ No newline at end of file From 6453e9bf4271f425800f9d7c5e8fa0a4d03ee248 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 03:31:47 -0800 Subject: [PATCH 16/89] WIP towards resolving crashes. --- src/audio/IAudioDevice.h | 2 +- src/audio/PortAudioDevice.cpp | 8 +++- src/audio/PulseAudioDevice.cpp | 4 +- src/audio/PulseAudioEngine.cpp | 23 +++++----- src/audio/PulseAudioEngine.h | 4 +- src/dlg_audiooptions.cpp | 11 +++-- src/main.cpp | 81 ++++++++-------------------------- src/main.h | 4 -- 8 files changed, 50 insertions(+), 87 deletions(-) diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index 0af7b6a92..d2c8bc854 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -82,4 +82,4 @@ class IAudioDevice void* onAudioErrorState; }; -#endif // I_AUDIO_DEVICE_H \ No newline at end of file +#endif // I_AUDIO_DEVICE_H diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index ccdced125..0135fff70 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -131,10 +131,16 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, const_cast(input) : const_cast(output); + if (thisObj->direction_ == IAudioEngine::OUT) + { + // Zero out samples by default in case we don't have any data available. + memset(dataPtr, 0, sizeof(short) * frameCount); + } + if (thisObj->onAudioDataFunction) { thisObj->onAudioDataFunction(*thisObj, dataPtr, frameCount, thisObj->onAudioDataState); } return paContinue; -} \ No newline at end of file +} diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index ac532ac0c..1ea45afb1 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -66,9 +66,9 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; - buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.maxlength = pa_usec_to_bytes(20000, &sample_specification); // 20ms of data at a time at most buffer_attr.tlength = (uint32_t) -1; - buffer_attr.prebuf = (uint32_t) -1; + buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; // Stream flags diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 7b1fa2c64..9973c808e 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -157,6 +157,12 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio op = pa_context_get_sink_info_list(context_, [](pa_context *c, const pa_sink_info *i, int eol, void *userdata) { PulseAudioDeviceListTemp* tempObj = static_cast(userdata); + if (eol) + { + pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); + return; + } + AudioDeviceSpecification device; device.deviceId = i->index; device.name = i->name; @@ -166,10 +172,6 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio tempObj->result.push_back(device); - if (eol) - { - pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); - } }, &tempObj); } else @@ -177,6 +179,12 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio op = pa_context_get_source_info_list(context_, [](pa_context *c, const pa_source_info *i, int eol, void *userdata) { PulseAudioDeviceListTemp* tempObj = static_cast(userdata); + if (eol) + { + pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); + return; + } + AudioDeviceSpecification device; device.deviceId = i->index; device.name = i->name; @@ -185,11 +193,6 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio device.defaultSampleRate = i->sample_spec.rate; tempObj->result.push_back(device); - - if (eol) - { - pa_threaded_mainloop_signal(tempObj->thisPtr->mainloop_, 0); - } }, &tempObj); } @@ -279,4 +282,4 @@ std::shared_ptr PulseAudioEngine::getAudioDevice(std::string devic } return nullptr; -} \ No newline at end of file +} diff --git a/src/audio/PulseAudioEngine.h b/src/audio/PulseAudioEngine.h index 013556b0c..27c526073 100644 --- a/src/audio/PulseAudioEngine.h +++ b/src/audio/PulseAudioEngine.h @@ -1,5 +1,5 @@ //========================================================================= -// Name: PortAudioEngine.h +// Name: PulseAudioEngine.h // Purpose: Defines the interface to the PortAudio audio engine. // // Authors: Mooneer Salem @@ -46,4 +46,4 @@ class PulseAudioEngine : public IAudioEngine pa_context* context_; }; -#endif // PULSE_AUDIO_ENGINE_H \ No newline at end of file +#endif // PULSE_AUDIO_ENGINE_H diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index dfddc61a2..7a33bdaf8 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -949,12 +949,13 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { short out48k_short[numSamples]; - short out48k_stereo_short[sampleRate]; - + short out48k_stereo_short[2*numSamples]; + int numChannels = devInfo.maxChannels >= 2 ? 2 : 1; + for(int j = 0; j < numSamples; j++, n++) { out48k_short[j] = 2000.0*cos(6.2832*(n)*400.0/sampleRate); - if (devInfo.maxChannels >= 2) { + if (numChannels == 2) { out48k_stereo_short[2*j] = out48k_short[j]; // left channel out48k_stereo_short[2*j+1] = out48k_short[j]; // right channel } @@ -963,13 +964,15 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * } } - memcpy(data, &out48k_stereo_short[0], numSamples); + memcpy(data, &out48k_stereo_short[0], sizeof(out48k_stereo_short)); { std::unique_lock callbackFifoLock(callbackFifoMutex); codec2_fifo_write(callbackFifo, out48k_short, numSamples); } callbackFifoCV.notify_one(); + + return numSamples; }, nullptr); device->start(); diff --git a/src/main.cpp b/src/main.cpp index c64fd4510..2ee7439ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2204,14 +2204,16 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - - int result = codec2_fifo_read(cbData->outfifo2, outdata, size); + size_t toRead = codec2_fifo_used(cbData->outfifo2); + toRead = toRead >= size ? size : toRead; + + int result = codec2_fifo_read(cbData->outfifo2, outdata, toRead); if (result == 0) { if (dev.getNumChannels() == 2) { // write signal to both channels */ - for(int i = 0; i < size; i++, audioData += 2) + for(int i = 0; i < toRead; i++, audioData += 2) { audioData[0] = outdata[i]; audioData[1] = outdata[i]; @@ -2219,7 +2221,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < size; i++, audioData++) + for(int i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2228,23 +2230,6 @@ void MainFrame::startRxStream() else { g_outfifo2_empty++; - - // zero output if no data available - if (dev.getNumChannels() == 2) - { - for(int i = 0; i < size; i++, audioData += 2) - { - audioData[0] = 0; - audioData[1] = 0; - } - } - else - { - for(int i = 0; i < size; i++, audioData++) - { - audioData[0] = 0; - } - } } }, g_rxUserdata); @@ -2291,8 +2276,10 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - - int result = codec2_fifo_read(cbData->outfifo1, outdata, size); + size_t toRead = codec2_fifo_used(cbData->outfifo1); + toRead = toRead >= size ? size : toRead; + + int result = codec2_fifo_read(cbData->outfifo1, outdata, toRead); if (result == 0) { // write signal to both channels if the device can support two channels. @@ -2300,7 +2287,7 @@ void MainFrame::startRxStream() // only to that channel. if (dev.getNumChannels() == 2) { - for(int i = 0; i < size; i++, audioData += 2) + for(int i = 0; i < toRead; i++, audioData += 2) { if (cbData->leftChannelVoxTone) { @@ -2316,7 +2303,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < size; i++, audioData++) + for(int i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2325,23 +2312,6 @@ void MainFrame::startRxStream() else { g_outfifo1_empty++; - - // zero output if no data available - if (dev.getNumChannels() == 2) - { - for(int i = 0; i < size; i++, audioData += 2) - { - audioData[0] = 0; - audioData[1] = 0; - } - } - else - { - for(int i = 0; i < size; i++, audioData++) - { - audioData[0] = 0; - } - } } }, g_rxUserdata); @@ -2364,14 +2334,16 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - - int result = codec2_fifo_read(cbData->outfifo1, outdata, size); + size_t toRead = codec2_fifo_used(cbData->outfifo1); + toRead = toRead >= size ? size : toRead; + + int result = codec2_fifo_read(cbData->outfifo1, outdata, toRead); if (result == 0) { if (dev.getNumChannels() == 2) { // write signal to both channels */ - for(int i = 0; i < size; i++, audioData += 2) + for(int i = 0; i < toRead; i++, audioData += 2) { audioData[0] = outdata[i]; audioData[1] = outdata[i]; @@ -2379,7 +2351,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < size; i++, audioData++) + for(int i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2388,23 +2360,6 @@ void MainFrame::startRxStream() else { g_outfifo2_empty++; - - // zero output if no data available - if (dev.getNumChannels() == 2) - { - for(int i = 0; i < size; i++, audioData += 2) - { - audioData[0] = 0; - audioData[1] = 0; - } - } - else - { - for(int i = 0; i < size; i++, audioData++) - { - audioData[0] = 0; - } - } } }, g_rxUserdata); diff --git a/src/main.h b/src/main.h index e5a59ceac..8104c27b7 100644 --- a/src/main.h +++ b/src/main.h @@ -669,10 +669,6 @@ class txRxThread : public wxThread wxThread::Sleep(20); } - auto engine = AudioEngineFactory::GetAudioEngine(); - engine->stop(); - engine->setOnEngineError(nullptr, nullptr); - return NULL; } From d52f13d2e0da2345699fa20dd52d4030b70f6320 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 03:58:05 -0800 Subject: [PATCH 17/89] Resolve buffer overflow with PulseAudio. --- src/audio/PulseAudioDevice.cpp | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 1ea45afb1..3859b97bc 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include "PulseAudioDevice.h" PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) @@ -29,7 +30,7 @@ PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* c , devName_(devName) , direction_(direction) , sampleRate_(sampleRate) - , numChannels_(numChannels) + , numChannels_(1 /*numChannels*/) { // empty } @@ -66,7 +67,7 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; - buffer_attr.maxlength = pa_usec_to_bytes(20000, &sample_specification); // 20ms of data at a time at most + buffer_attr.maxlength = pa_usec_to_bytes(100000, &sample_specification); // 100ms of data at a time at most buffer_attr.tlength = (uint32_t) -1; buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; @@ -98,7 +99,6 @@ void PulseAudioDevice::start() { onAudioErrorFunction(*this, std::string("Could not connect PulseAudio stream to ") + devName_, onAudioErrorState); } - } pa_threaded_mainloop_unlock(mainloop_); @@ -126,7 +126,7 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us { if (thisObj->onAudioDataFunction) { - thisObj->onAudioDataFunction(*thisObj, const_cast(data), length, thisObj->onAudioDataState); + thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / sizeof(short), thisObj->onAudioDataState); } if (length > 0) @@ -138,15 +138,20 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *userdata) { - short data[length]; - PulseAudioDevice* thisObj = static_cast(userdata); - - if (thisObj->onAudioDataFunction) + if (length > 0) { - thisObj->onAudioDataFunction(*thisObj, data, length, thisObj->onAudioDataState); - } + short data[length]; + memset(data, 0, sizeof(short) * length); + + PulseAudioDevice* thisObj = static_cast(userdata); - pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); + if (thisObj->onAudioDataFunction) + { + thisObj->onAudioDataFunction(*thisObj, data, length / sizeof(short), thisObj->onAudioDataState); + } + + pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); + } } void PulseAudioDevice::StreamUnderflowCallback_(pa_stream *p, void *userdata) @@ -167,4 +172,4 @@ void PulseAudioDevice::StreamOverflowCallback_(pa_stream *p, void *userdata) { thisObj->onAudioOverflowFunction(*thisObj, thisObj->onAudioOverflowState); } -} \ No newline at end of file +} From 02b1bd65a05d3a52386668881d5e6b19500b2bff Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 04:05:50 -0800 Subject: [PATCH 18/89] Add user friendly descriptions for pavucontrol. --- src/audio/IAudioDevice.cpp | 7 ++++++- src/audio/IAudioDevice.h | 5 +++++ src/audio/PulseAudioDevice.cpp | 5 +++-- src/dlg_audiooptions.cpp | 4 +++- src/main.cpp | 9 +++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/audio/IAudioDevice.cpp b/src/audio/IAudioDevice.cpp index fd71ac13e..240bc128e 100644 --- a/src/audio/IAudioDevice.cpp +++ b/src/audio/IAudioDevice.cpp @@ -22,6 +22,11 @@ #include "IAudioDevice.h" +void IAudioDevice::setDescription(std::string desc) +{ + description = desc; +} + void IAudioDevice::setOnAudioData(AudioDataCallbackFn fn, void* state) { onAudioDataFunction = fn; @@ -44,4 +49,4 @@ void IAudioDevice::setOnAudioError(AudioErrorCallbackFn fn, void* state) { onAudioErrorFunction = fn; onAudioErrorState = state; -} \ No newline at end of file +} diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index d2c8bc854..aff3fb7f3 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -40,6 +40,9 @@ class IAudioDevice virtual void start() = 0; virtual void stop() = 0; + + // Sets user friendly description of device. Not used by all engines. + void setDescription(std::string desc); // Set RX/TX ready callback. // Callback must take the following parameters: @@ -69,6 +72,8 @@ class IAudioDevice void setOnAudioError(AudioErrorCallbackFn fn, void* state); protected: + std::string description; + AudioDataCallbackFn onAudioDataFunction; void* onAudioDataState; diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 3859b97bc..55de328bb 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -32,7 +32,8 @@ PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* c , sampleRate_(sampleRate) , numChannels_(1 /*numChannels*/) { - // empty + // Set default description + setDescription("PulseAudio Device"); } PulseAudioDevice::~PulseAudioDevice() @@ -51,7 +52,7 @@ void PulseAudioDevice::start() sample_specification.channels = numChannels_; pa_threaded_mainloop_lock(mainloop_); - stream_ = pa_stream_new(context_, "PulseAudioDevice", &sample_specification, nullptr); + stream_ = pa_stream_new(context_, description.c_str(), &sample_specification, nullptr); if (stream_ == nullptr) { if (onAudioErrorFunction) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 7a33bdaf8..1d043c328 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -852,7 +852,8 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p } callbackFifoCV.notify_one(); }, nullptr); - + device->setDescription("Device Input Test"); + device->start(); while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { @@ -975,6 +976,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * return numSamples; }, nullptr); + device->setDescription("Device Output Test"); device->start(); while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { diff --git a/src/main.cpp b/src/main.cpp index 2ee7439ed..736e610cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1971,7 +1971,9 @@ void MainFrame::startRxStream() // RX-only setup. // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxInSoundDevice->setDescription("Radio to FreeDV"); rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + rxOutSoundDevice->setDescription("FreeDV to Speaker"); bool failed = false; if (!rxInSoundDevice) @@ -2011,9 +2013,16 @@ void MainFrame::startRxStream() // RX + TX setup // Same note as above re: number of channels. rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxInSoundDevice->setDescription("Radio to FreeDV"); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard2SampleRate, 2); + rxOutSoundDevice->setDescription("FreeDV to Speaker"); + txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard2SampleRate, 2); + txInSoundDevice->setDescription("Mic to FreeDV"); + txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + txOutSoundDevice->setDescription("FreeDV to Radio"); bool failed = false; if (!rxInSoundDevice) From 3e6fbc6ca5710848d6c848d628aa64f59291774c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 04:09:36 -0800 Subject: [PATCH 19/89] Fix GitHub automated build. --- src/audio/PortAudioDevice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 0135fff70..08c626bca 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include "PortAudioDevice.h" #include "portaudio.h" From 83dceecdb321ed1c01fd069d45042e5e038dee4e Mon Sep 17 00:00:00 2001 From: drowe67 Date: Thu, 23 Dec 2021 07:11:23 +1030 Subject: [PATCH 20/89] fix some warning building on gcc 9.3.0 --- src/dlg_audiooptions.cpp | 6 +++--- src/main.cpp | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 1d043c328..5e41678bc 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -838,11 +838,11 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p short in48k_short[numSamples]; if (devInfo.maxChannels >= 2) { - for(int j = 0; j < numSamples; j++) + for(size_t j = 0; j < numSamples; j++) in48k_short[j] = in48k_stereo_short[2*j]; // left channel only } else { - for(int j = 0; j < numSamples; j++) + for(size_t j = 0; j < numSamples; j++) in48k_short[j] = in48k_stereo_short[j]; } @@ -953,7 +953,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * short out48k_stereo_short[2*numSamples]; int numChannels = devInfo.maxChannels >= 2 ? 2 : 1; - for(int j = 0; j < numSamples; j++, n++) + for(size_t j = 0; j < numSamples; j++, n++) { out48k_short[j] = 2000.0*cos(6.2832*(n)*400.0/sampleRate); if (numChannels == 2) { diff --git a/src/main.cpp b/src/main.cpp index 736e610cd..3f8ab39c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1127,7 +1127,7 @@ void MainFrame::OnTimer(wxTimerEvent &evt) strncpy(callsign, (const char*) wxGetApp().m_callSign.mb_str(wxConvUTF8), MAX_CALLSIGN - 2); if (strlen(callsign) < MAX_CALLSIGN - 1) { - strncat(callsign, "\r", 1); + strncat(callsign, "\r", 2); } // buffer 1 txt message to ensure tx data fifo doesn't "run dry" @@ -2184,7 +2184,7 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short indata[size]; - for (int i = 0; i < size; i++, audioData += dev.getNumChannels()) + for (size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { indata[i] = audioData[0]; } @@ -2222,7 +2222,7 @@ void MainFrame::startRxStream() if (dev.getNumChannels() == 2) { // write signal to both channels */ - for(int i = 0; i < toRead; i++, audioData += 2) + for(size_t i = 0; i < toRead; i++, audioData += 2) { audioData[0] = outdata[i]; audioData[1] = outdata[i]; @@ -2230,7 +2230,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < toRead; i++, audioData++) + for(size_t i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2259,7 +2259,7 @@ void MainFrame::startRxStream() if (!endingTx) { - for(int i = 0; i < size; i++, audioData += dev.getNumChannels()) + for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { indata[i] = audioData[0]; } @@ -2296,7 +2296,7 @@ void MainFrame::startRxStream() // only to that channel. if (dev.getNumChannels() == 2) { - for(int i = 0; i < toRead; i++, audioData += 2) + for(size_t i = 0; i < toRead; i++, audioData += 2) { if (cbData->leftChannelVoxTone) { @@ -2312,7 +2312,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < toRead; i++, audioData++) + for(size_t i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2352,7 +2352,7 @@ void MainFrame::startRxStream() if (dev.getNumChannels() == 2) { // write signal to both channels */ - for(int i = 0; i < toRead; i++, audioData += 2) + for(size_t i = 0; i < toRead; i++, audioData += 2) { audioData[0] = outdata[i]; audioData[1] = outdata[i]; @@ -2360,7 +2360,7 @@ void MainFrame::startRxStream() } else { - for(int i = 0; i < toRead; i++, audioData++) + for(size_t i = 0; i < toRead; i++, audioData++) { audioData[0] = outdata[i]; } From 5be7f356c217348f78a0c05e5f347811960f99a6 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 22 Dec 2021 13:18:03 -0800 Subject: [PATCH 21/89] Resolve issue with sample .wav files playing 2x as fast as expeted with PulseAudio. --- src/audio/PulseAudioDevice.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 55de328bb..d38c82891 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -30,7 +30,7 @@ PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* c , devName_(devName) , direction_(direction) , sampleRate_(sampleRate) - , numChannels_(1 /*numChannels*/) + , numChannels_(numChannels) { // Set default description setDescription("PulseAudio Device"); @@ -127,7 +127,7 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us { if (thisObj->onAudioDataFunction) { - thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / sizeof(short), thisObj->onAudioDataState); + thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); } if (length > 0) @@ -148,7 +148,7 @@ void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *u if (thisObj->onAudioDataFunction) { - thisObj->onAudioDataFunction(*thisObj, data, length / sizeof(short), thisObj->onAudioDataState); + thisObj->onAudioDataFunction(*thisObj, data, length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); } pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); From d5845799bfce628492a1767d975c20ace53288dc Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 23 Dec 2021 00:54:21 -0800 Subject: [PATCH 22/89] Fix noise and plot weirdness in audio config. --- src/dlg_audiooptions.cpp | 366 ++++++++++++++++++++------------------- src/dlg_audiooptions.h | 2 +- 2 files changed, 187 insertions(+), 181 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 5e41678bc..ef3b20ebf 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -804,106 +804,109 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - SRC_STATE *src; - FIFO *fifo, *callbackFifo; - int src_error; - - fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); - src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); - - auto engine = AudioEngineFactory::GetAudioEngine(); - auto devList = engine->getAudioDeviceList(IAudioEngine::IN); - for (auto& devInfo : devList) - { - if (wxString(devInfo.name).Trim() == devName.Trim()) + m_audioPlotThread = new std::thread([&](std::string devNameAsCString, PlotScalar* ps) { + std::mutex callbackFifoMutex; + std::condition_variable callbackFifoCV; + SRC_STATE *src; + FIFO *fifo, *callbackFifo; + int src_error; + + fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); + src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); + + auto engine = AudioEngineFactory::GetAudioEngine(); + auto devList = engine->getAudioDeviceList(IAudioEngine::IN); + for (auto& devInfo : devList) { - int sampleCount = 0; - int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); - auto device = engine->getAudioDevice( - devInfo.name, - IAudioEngine::IN, - sampleRate, - devInfo.maxChannels >= 2 ? 2 : 1); - - if (device) + if (devInfo.name == devNameAsCString) { - std::mutex callbackFifoMutex; - std::condition_variable callbackFifoCV; - - callbackFifo = codec2_fifo_create(sampleRate); - assert(callbackFifo != nullptr); - - device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { - short* in48k_stereo_short = static_cast(data); - short in48k_short[numSamples]; - - if (devInfo.maxChannels >= 2) { - for(size_t j = 0; j < numSamples; j++) - in48k_short[j] = in48k_stereo_short[2*j]; // left channel only - } - else { - for(size_t j = 0; j < numSamples; j++) - in48k_short[j] = in48k_stereo_short[j]; - } + int sampleCount = 0; + int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); + auto device = engine->getAudioDevice( + devInfo.name, + IAudioEngine::IN, + sampleRate, + devInfo.maxChannels >= 2 ? 2 : 1); - { - std::unique_lock callbackFifoLock(callbackFifoMutex); - codec2_fifo_write(callbackFifo, in48k_short, numSamples); - } - callbackFifoCV.notify_one(); - }, nullptr); - device->setDescription("Device Input Test"); - - device->start(); - while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) + if (device) { - short in8k_short[TEST_BUF_SIZE]; - short in48k_short[TEST_BUF_SIZE]; - - { - std::unique_lock callbackFifoLock(callbackFifoMutex); - callbackFifoCV.wait_for(callbackFifoLock, std::chrono::milliseconds(1)); - if (!codec2_fifo_read(callbackFifo, in48k_short, TEST_BUF_SIZE)) + callbackFifo = codec2_fifo_create(sampleRate); + assert(callbackFifo != nullptr); + + device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { + short* in48k_stereo_short = static_cast(data); + short in48k_short[numSamples]; + + if (devInfo.maxChannels >= 2) { + for(size_t j = 0; j < numSamples; j++) + in48k_short[j] = in48k_stereo_short[2*j]; // left channel only + } + else { + for(size_t j = 0; j < numSamples; j++) + in48k_short[j] = in48k_stereo_short[j]; + } + { - continue; + std::unique_lock callbackFifoLock(callbackFifoMutex); + codec2_fifo_write(callbackFifo, in48k_short, numSamples); } - } - - // Process any pending UI events. - wxSafeYield(); - - int n8k = resample(src, in8k_short, in48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); - resample_for_plot(fifo, in8k_short, n8k, FS); + callbackFifoCV.notify_all(); + }, nullptr); + device->setDescription("Device Input Test"); + + device->start(); - short plotSamples[TEST_WAVEFORM_PLOT_BUF]; - if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) + while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { - // come back when the fifo is refilled - continue; + short in8k_short[TEST_BUF_SIZE]; + short in48k_short[TEST_BUF_SIZE]; + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait(callbackFifoLock); + if (codec2_fifo_read(callbackFifo, in48k_short, TEST_BUF_SIZE)) + { + continue; + } + } + + int n8k = resample(src, in8k_short, in48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); + resample_for_plot(fifo, in8k_short, n8k, FS); + + short plotSamples[TEST_WAVEFORM_PLOT_BUF]; + if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) + { + // come back when the fifo is refilled + continue; + } + + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + sampleCount += TEST_WAVEFORM_PLOT_BUF; + CallAfter(&AudioOptsDialog::UpdatePlot, ps); } - - ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); - sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, ps); + + device->stop(); + codec2_fifo_destroy(callbackFifo); } - - device->stop(); - - codec2_fifo_destroy(callbackFifo); + break; } - break; } - } - - codec2_fifo_destroy(fifo); - src_delete(src); + + codec2_fifo_destroy(fifo); + src_delete(src); + + CallAfter([&]() { + m_audioPlotThread->join(); + delete m_audioPlotThread; + m_audioPlotThread = nullptr; + + m_btnRxInTest->Enable(true); + m_btnRxOutTest->Enable(true); + m_btnTxInTest->Enable(true); + m_btnTxOutTest->Enable(true); + }); + }, std::string(devName.ToUTF8()), ps); - CallAfter([&]() { - m_btnRxInTest->Enable(true); - m_btnRxOutTest->Enable(true); - m_btnTxInTest->Enable(true); - m_btnTxOutTest->Enable(true); - }); } //------------------------------------------------------------------------- @@ -919,114 +922,117 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - SRC_STATE *src; - FIFO *fifo, *callbackFifo; - int src_error, n = 0; - - fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); - src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); - - auto engine = AudioEngineFactory::GetAudioEngine(); - auto devList = engine->getAudioDeviceList(IAudioEngine::OUT); - for (auto& devInfo : devList) - { - if (wxString(devInfo.name).Trim() == devName.Trim()) + m_audioPlotThread = new std::thread([&](std::string devNameAsCString, PlotScalar* ps) { + SRC_STATE *src; + FIFO *fifo, *callbackFifo; + int src_error, n = 0; + + fifo = codec2_fifo_create((int)(DT*TEST_WAVEFORM_PLOT_FS*2)); assert(fifo != NULL); + src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); + + auto engine = AudioEngineFactory::GetAudioEngine(); + auto devList = engine->getAudioDeviceList(IAudioEngine::OUT); + for (auto& devInfo : devList) { - int sampleCount = 0; - int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); - auto device = engine->getAudioDevice( - devInfo.name, - IAudioEngine::OUT, - sampleRate, - devInfo.maxChannels >= 2 ? 2 : 1); - - if (device) + if (devInfo.name == devNameAsCString) { - std::mutex callbackFifoMutex; - std::condition_variable callbackFifoCV; - - callbackFifo = codec2_fifo_create(sampleRate); - assert(callbackFifo != nullptr); + int sampleCount = 0; + int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); + auto device = engine->getAudioDevice( + devInfo.name, + IAudioEngine::OUT, + sampleRate, + devInfo.maxChannels >= 2 ? 2 : 1); - device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { - short out48k_short[numSamples]; - short out48k_stereo_short[2*numSamples]; - int numChannels = devInfo.maxChannels >= 2 ? 2 : 1; - - for(size_t j = 0; j < numSamples; j++, n++) - { - out48k_short[j] = 2000.0*cos(6.2832*(n)*400.0/sampleRate); - if (numChannels == 2) { - out48k_stereo_short[2*j] = out48k_short[j]; // left channel - out48k_stereo_short[2*j+1] = out48k_short[j]; // right channel + if (device) + { + std::mutex callbackFifoMutex; + std::condition_variable callbackFifoCV; + + callbackFifo = codec2_fifo_create(sampleRate); + assert(callbackFifo != nullptr); + + device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { + short out48k_short[numSamples]; + short out48k_stereo_short[2*numSamples]; + int numChannels = devInfo.maxChannels >= 2 ? 2 : 1; + + for(size_t j = 0; j < numSamples; j++, n++) + { + out48k_short[j] = 2000.0*cos(6.2832*(n)*400.0/sampleRate); + if (numChannels == 2) { + out48k_stereo_short[2*j] = out48k_short[j]; // left channel + out48k_stereo_short[2*j+1] = out48k_short[j]; // right channel + } + else { + out48k_stereo_short[j] = out48k_short[j]; // mono + } } - else { - out48k_stereo_short[j] = out48k_short[j]; // mono + + memcpy(data, &out48k_stereo_short[0], sizeof(out48k_stereo_short)); + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + codec2_fifo_write(callbackFifo, out48k_short, numSamples); } - } - - memcpy(data, &out48k_stereo_short[0], sizeof(out48k_stereo_short)); - - { - std::unique_lock callbackFifoLock(callbackFifoMutex); - codec2_fifo_write(callbackFifo, out48k_short, numSamples); - } - callbackFifoCV.notify_one(); - - return numSamples; - }, nullptr); - - device->setDescription("Device Output Test"); - device->start(); - while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) - { - short out8k_short[TEST_BUF_SIZE]; - short out48k_short[TEST_BUF_SIZE]; + callbackFifoCV.notify_one(); + + return numSamples; + }, nullptr); + device->setDescription("Device Output Test"); + device->start(); + while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { - std::unique_lock callbackFifoLock(callbackFifoMutex); - callbackFifoCV.wait_for(callbackFifoLock, std::chrono::milliseconds(1)); - if (!codec2_fifo_read(callbackFifo, out48k_short, TEST_BUF_SIZE)) + short out8k_short[TEST_BUF_SIZE]; + short out48k_short[TEST_BUF_SIZE]; + + { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait(callbackFifoLock); + if (codec2_fifo_read(callbackFifo, out48k_short, TEST_BUF_SIZE)) + { + continue; + } + } + + int n8k = resample(src, out8k_short, out48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); + resample_for_plot(fifo, out8k_short, n8k, FS); + + short plotSamples[TEST_WAVEFORM_PLOT_BUF]; + if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) { + // come back when the fifo is refilled continue; } + + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + sampleCount += TEST_WAVEFORM_PLOT_BUF; + CallAfter(&AudioOptsDialog::UpdatePlot, ps); } + + device->stop(); - // Process any pending UI events. - wxSafeYield(); - - int n8k = resample(src, out8k_short, out48k_short, 8000, sampleRate, TEST_BUF_SIZE, TEST_BUF_SIZE); - resample_for_plot(fifo, out8k_short, n8k, FS); - - short plotSamples[TEST_WAVEFORM_PLOT_BUF]; - if (codec2_fifo_read(fifo, plotSamples, TEST_WAVEFORM_PLOT_BUF)) - { - // come back when the fifo is refilled - continue; - } - - ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); - sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, ps); + codec2_fifo_destroy(callbackFifo); } - - device->stop(); - - codec2_fifo_destroy(callbackFifo); + break; } - break; } - } - - codec2_fifo_destroy(fifo); - src_delete(src); - - CallAfter([&]() { - m_btnRxInTest->Enable(true); - m_btnRxOutTest->Enable(true); - m_btnTxInTest->Enable(true); - m_btnTxOutTest->Enable(true); - }); + + codec2_fifo_destroy(fifo); + src_delete(src); + + CallAfter([&]() { + m_audioPlotThread->join(); + delete m_audioPlotThread; + m_audioPlotThread = nullptr; + + m_btnRxInTest->Enable(true); + m_btnRxOutTest->Enable(true); + m_btnTxInTest->Enable(true); + m_btnTxOutTest->Enable(true); + }); + }, std::string(devName.ToUTF8()), ps); } //------------------------------------------------------------------------- diff --git a/src/dlg_audiooptions.h b/src/dlg_audiooptions.h index 5e581e725..f00045bc4 100644 --- a/src/dlg_audiooptions.h +++ b/src/dlg_audiooptions.h @@ -48,7 +48,7 @@ class AudioInfoDisplay class AudioOptsDialog : public wxDialog { private: - + std::thread* m_audioPlotThread; protected: bool m_isPaInitialized; From 4b28f791f3c398bd89b5565c7de885fdddb2ecd9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 23 Dec 2021 01:11:54 -0800 Subject: [PATCH 23/89] Fix timing issue for audio config plots. --- src/dlg_audiooptions.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index ef3b20ebf..9ea2151da 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -826,7 +826,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p devInfo.name, IAudioEngine::IN, sampleRate, - devInfo.maxChannels >= 2 ? 2 : 1); + 2); if (device) { @@ -837,7 +837,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p short* in48k_stereo_short = static_cast(data); short in48k_short[numSamples]; - if (devInfo.maxChannels >= 2) { + if (device->getNumChannels() == 2) { for(size_t j = 0; j < numSamples; j++) in48k_short[j] = in48k_stereo_short[2*j]; // left channel only } @@ -852,8 +852,8 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p } callbackFifoCV.notify_all(); }, nullptr); + device->setDescription("Device Input Test"); - device->start(); while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) @@ -862,10 +862,10 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p short in48k_short[TEST_BUF_SIZE]; { - std::unique_lock callbackFifoLock(callbackFifoMutex); - callbackFifoCV.wait(callbackFifoLock); if (codec2_fifo_read(callbackFifo, in48k_short, TEST_BUF_SIZE)) { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait(callbackFifoLock); continue; } } @@ -942,7 +942,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * devInfo.name, IAudioEngine::OUT, sampleRate, - devInfo.maxChannels >= 2 ? 2 : 1); + 2); if (device) { @@ -954,8 +954,8 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * device->setOnAudioData([&](IAudioDevice&, void* data, size_t numSamples, void* state) { short out48k_short[numSamples]; - short out48k_stereo_short[2*numSamples]; - int numChannels = devInfo.maxChannels >= 2 ? 2 : 1; + int numChannels = device->getNumChannels(); + short out48k_stereo_short[numChannels*numSamples]; for(size_t j = 0; j < numSamples; j++, n++) { @@ -976,22 +976,21 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * codec2_fifo_write(callbackFifo, out48k_short, numSamples); } callbackFifoCV.notify_one(); - - return numSamples; }, nullptr); device->setDescription("Device Output Test"); device->start(); + while(sampleCount < (TEST_WAVEFORM_PLOT_TIME * TEST_WAVEFORM_PLOT_FS)) { short out8k_short[TEST_BUF_SIZE]; short out48k_short[TEST_BUF_SIZE]; { - std::unique_lock callbackFifoLock(callbackFifoMutex); - callbackFifoCV.wait(callbackFifoLock); if (codec2_fifo_read(callbackFifo, out48k_short, TEST_BUF_SIZE)) { + std::unique_lock callbackFifoLock(callbackFifoMutex); + callbackFifoCV.wait(callbackFifoLock); continue; } } From e04d24cae35faa76bf704c3d8f20d95d3cd1d438 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 23 Dec 2021 01:44:04 -0800 Subject: [PATCH 24/89] Resolve PulseAudio related memory leaks. --- src/audio/PulseAudioDevice.cpp | 1 + src/audio/PulseAudioEngine.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index d38c82891..6a7e185f7 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -112,6 +112,7 @@ void PulseAudioDevice::stop() pa_threaded_mainloop_lock(mainloop_); pa_stream_disconnect(stream_); pa_threaded_mainloop_unlock(mainloop_); + pa_stream_unref(stream_); stream_ = nullptr; } diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 9973c808e..07a0377fa 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -202,7 +202,8 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio if (pa_operation_get_state(op) != PA_OPERATION_RUNNING) break; pa_threaded_mainloop_wait(mainloop_); } - + + pa_operation_unref(op); pa_threaded_mainloop_unlock(mainloop_); return tempObj.result; @@ -250,6 +251,7 @@ AudioDeviceSpecification PulseAudioEngine::getDefaultAudioDevice(AudioDirection pa_threaded_mainloop_wait(mainloop_); } + pa_operation_unref(op); pa_threaded_mainloop_unlock(mainloop_); auto devices = getAudioDeviceList(direction); From b25ac24077da10a363ed3b9d1c13a33b8a29c5d2 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 23 Dec 2021 22:29:22 -0800 Subject: [PATCH 25/89] Minor read tweak. --- src/audio/PulseAudioDevice.cpp | 14 +++++++------- src/audio/PulseAudioDevice.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 6a7e185f7..24a6ae87c 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -72,6 +72,7 @@ void PulseAudioDevice::start() buffer_attr.tlength = (uint32_t) -1; buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; + buffer_attr.fragsize = (uint32_t) -1; // Stream flags pa_stream_flags_t flags = pa_stream_flags_t( @@ -123,19 +124,18 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us const void* data = nullptr; PulseAudioDevice* thisObj = static_cast(userdata); - // Ignore errors here as they're not critical. - if (pa_stream_peek(s, &data, &length) >= 0) + do { + pa_stream_peek(s, &data, &length); + if (!data || length == 0) break; + if (thisObj->onAudioDataFunction) { thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); } - if (length > 0) - { - pa_stream_drop(s); - } - } + pa_stream_drop(s); + } while (pa_stream_readable_size(s) > 0); } void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *userdata) diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 65baca989..4989d7e9c 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -60,4 +60,4 @@ class PulseAudioDevice : public IAudioDevice static void StreamOverflowCallback_(pa_stream *p, void *userdata); }; -#endif // PULSE_AUDIO_DEVICE_H \ No newline at end of file +#endif // PULSE_AUDIO_DEVICE_H From cdaa0e2e31e7745b8094bb5ad2f162c980706a3c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 22:32:38 -0800 Subject: [PATCH 26/89] Prevent adjustments of FreeDV audio settings by third party apps. --- src/audio/PulseAudioDevice.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 24a6ae87c..b3c93f485 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -78,6 +78,8 @@ void PulseAudioDevice::start() pa_stream_flags_t flags = pa_stream_flags_t( PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE | + PA_STREAM_DONT_MOVE | + PA_STREAM_FAIL_ON_SUSPEND | PA_STREAM_ADJUST_LATENCY); int result = 0; From 17d0cb05f9218edd56cb78519b8c0a6fdc7442e3 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 22:36:55 -0800 Subject: [PATCH 27/89] Update build_linux script to use PulseAudio. --- build_linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_linux.sh b/build_linux.sh index 7b7e77ef4..ae72d82fc 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -42,5 +42,5 @@ export LD_LIBRARY_PATH=$LPCNETDIR/build_linux/src # Finally, build freedv-gui cd $FREEDVGUIDIR && git pull mkdir -p build_linux && cd build_linux && rm -Rf * -cmake -DCMAKE_BUILD_TYPE=Debug -DCODEC2_BUILD_DIR=$CODEC2DIR/build_linux -DLPCNET_BUILD_DIR=$LPCNETDIR/build_linux .. +cmake -DUSE_PULSEAUDIO=1 -DCMAKE_BUILD_TYPE=Debug -DCODEC2_BUILD_DIR=$CODEC2DIR/build_linux -DLPCNET_BUILD_DIR=$LPCNETDIR/build_linux .. make VERBOSE=1 From c0df1e8b98fb1083ad0878de635100b24705b977 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 22:40:37 -0800 Subject: [PATCH 28/89] Add PulseAudio to GitHub workflow. --- .github/workflows/cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b34a645ad..ed52afb66 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -24,7 +24,7 @@ jobs: shell: bash run: | sudo apt-get update - sudo apt-get install libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.0-gtk3-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev + sudo apt-get install libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.0-gtk3-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev - name: Build freedv-gui shell: bash From 47d5b601e8ef4af9683e4c99c777bf7ac4e6e843 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 22:45:19 -0800 Subject: [PATCH 29/89] Modify README to include required Linux packages for PulseAudio. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b3acba49..42315968c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This document describes how to build the FreeDV GUI program for various operatin ``` $ sudo apt install libspeexdsp-dev libsamplerate0-dev sox git \ libwxgtk3.0-gtk3-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev \ - libgsm1-dev libsndfile-dev cmake module-assistant build-essential + libgsm1-dev libsndfile-dev cmake module-assistant build-essential libpulse-dev $ git clone https://github.com/drowe67/freedv-gui.git $ cd freedv-gui $ ./build_linux.sh @@ -31,7 +31,7 @@ This document describes how to build the FreeDV GUI program for various operatin $ sudo dnf groupinstall "Development Tools" $ sudo dnf install cmake wxGTK3-devel portaudio-devel libsamplerate-devel \ libsndfile-devel speexdsp-devel hamlib-devel alsa-lib-devel libao-devel \ - gsm-devel + gsm-devel pulseaudio-libs-devel $ git clone https://github.com/drowe67/freedv-gui.git $ cd freedv-gui $ ./build_linux.sh From d05ca3cf36ee55070d69e834f685f2187b1f3116 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 23:16:47 -0800 Subject: [PATCH 30/89] Fix Windows build issues. --- src/audio/IAudioEngine.h | 2 +- src/audio/PortAudioDevice.cpp | 10 +++++----- src/audio/PortAudioEngine.cpp | 12 ++++++------ src/audio/PulseAudioDevice.cpp | 2 +- src/audio/PulseAudioEngine.cpp | 4 ++-- src/dlg_audiooptions.cpp | 14 +++++++------- src/main.cpp | 20 ++++++++++---------- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h index 72828fc15..dac0161a5 100644 --- a/src/audio/IAudioEngine.h +++ b/src/audio/IAudioEngine.h @@ -36,7 +36,7 @@ class IAudioEngine public: typedef std::function AudioErrorCallbackFn; - enum AudioDirection { IN, OUT }; + enum AudioDirection { AUDIO_ENGINE_IN, AUDIO_ENGINE_OUT }; virtual void start() = 0; virtual void stop() = 0; diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 08c626bca..9fbfd06ec 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -54,8 +54,8 @@ void PortAudioDevice::start() auto error = Pa_OpenStream( &deviceStream_, - direction_ == IAudioEngine::IN ? &streamParameters : nullptr, - direction_ == IAudioEngine::OUT ? &streamParameters : nullptr, + direction_ == IAudioEngine::AUDIO_ENGINE_IN ? &streamParameters : nullptr, + direction_ == IAudioEngine::AUDIO_ENGINE_OUT ? &streamParameters : nullptr, sampleRate_, 0, paClipOff, @@ -104,7 +104,7 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, unsigned int overflowFlag = 0; unsigned int underflowFlag = 0; - if (thisObj->direction_ == IAudioEngine::IN) + if (thisObj->direction_ == IAudioEngine::AUDIO_ENGINE_IN) { underflowFlag = 0x1; overflowFlag = 0x2; @@ -128,11 +128,11 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, } void* dataPtr = - thisObj->direction_ == IAudioEngine::IN ? + thisObj->direction_ == IAudioEngine::AUDIO_ENGINE_IN ? const_cast(input) : const_cast(output); - if (thisObj->direction_ == IAudioEngine::OUT) + if (thisObj->direction_ == IAudioEngine::AUDIO_ENGINE_OUT) { // Zero out samples by default in case we don't have any data available. memset(dataPtr, 0, sizeof(short) * frameCount); diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index 6e3a764d4..1bcf491d8 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -77,15 +77,15 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD continue; } - if ((direction == IN && deviceInfo->maxInputChannels > 0) || - (direction == OUT && deviceInfo->maxOutputChannels > 0)) + if ((direction == AUDIO_ENGINE_IN && deviceInfo->maxInputChannels > 0) || + (direction == AUDIO_ENGINE_OUT && deviceInfo->maxOutputChannels > 0)) { AudioDeviceSpecification device; device.deviceId = index; device.name = deviceInfo->name; device.apiName = hostApiName; device.maxChannels = - direction == IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; + direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; device.defaultSampleRate = deviceInfo->defaultSampleRate; result.push_back(device); @@ -116,8 +116,8 @@ std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName while (IAudioEngine::StandardSampleRates[rateIndex] != -1) { PaError err = Pa_IsFormatSupported( - direction == IN ? &streamParameters : NULL, - direction == OUT ? &streamParameters : NULL, + direction == AUDIO_ENGINE_IN ? &streamParameters : NULL, + direction == AUDIO_ENGINE_OUT ? &streamParameters : NULL, IAudioEngine::StandardSampleRates[rateIndex]); if (err == paFormatIsSupported) @@ -137,7 +137,7 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d { auto devices = getAudioDeviceList(direction); PaDeviceIndex defaultDeviceIndex = - direction == IN ? Pa_GetDefaultInputDevice() : Pa_GetDefaultOutputDevice(); + direction == AUDIO_ENGINE_IN ? Pa_GetDefaultInputDevice() : Pa_GetDefaultOutputDevice(); if (defaultDeviceIndex != paNoDevice) { diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index b3c93f485..1d2ca5425 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -83,7 +83,7 @@ void PulseAudioDevice::start() PA_STREAM_ADJUST_LATENCY); int result = 0; - if (direction_ == IAudioEngine::OUT) + if (direction_ == IAudioEngine::AUDIO_ENGINE_OUT) { pa_stream_set_write_callback(stream_, &PulseAudioDevice::StreamWriteCallback_, this); result = pa_stream_connect_playback( diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 07a0377fa..906d794dd 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -152,7 +152,7 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio pa_operation* op = nullptr; pa_threaded_mainloop_lock(mainloop_); - if (direction == OUT) + if (direction == AUDIO_ENGINE_OUT) { op = pa_context_get_sink_info_list(context_, [](pa_context *c, const pa_sink_info *i, int eol, void *userdata) { PulseAudioDeviceListTemp* tempObj = static_cast(userdata); @@ -255,7 +255,7 @@ AudioDeviceSpecification PulseAudioEngine::getDefaultAudioDevice(AudioDirection pa_threaded_mainloop_unlock(mainloop_); auto devices = getAudioDeviceList(direction); - std::string defaultDeviceName = direction == IN ? tempData.defaultSource : tempData.defaultSink; + std::string defaultDeviceName = direction == AUDIO_ENGINE_IN ? tempData.defaultSource : tempData.defaultSink; for (auto& device : devices) { if (device.name == defaultDeviceName) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 9ea2151da..1a0e3856f 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -608,7 +608,7 @@ int AudioOptsDialog::ExchangeData(int inout) int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, wxString devName, int in_out) { auto engine = AudioEngineFactory::GetAudioEngine(); - auto deviceList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + auto deviceList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::AUDIO_ENGINE_IN : IAudioEngine::AUDIO_ENGINE_OUT); wxString str; int numSampleRates = 0; @@ -620,7 +620,7 @@ int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, w auto supportedSampleRates = engine->getSupportedSampleRates( dev.name, - in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + in_out == AUDIO_IN ? IAudioEngine::AUDIO_ENGINE_IN : IAudioEngine::AUDIO_ENGINE_OUT); for (auto& rate : supportedSampleRates) { @@ -646,7 +646,7 @@ void AudioOptsDialog::populateParams(AudioInfoDisplay ai) int col = 0, idx; auto engine = AudioEngineFactory::GetAudioEngine(); - auto devList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::IN : IAudioEngine::OUT); + auto devList = engine->getAudioDeviceList(in_out == AUDIO_IN ? IAudioEngine::AUDIO_ENGINE_IN : IAudioEngine::AUDIO_ENGINE_OUT); if(ctrl->GetColumnCount() > 0) { @@ -815,7 +815,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); auto engine = AudioEngineFactory::GetAudioEngine(); - auto devList = engine->getAudioDeviceList(IAudioEngine::IN); + auto devList = engine->getAudioDeviceList(IAudioEngine::AUDIO_ENGINE_IN); for (auto& devInfo : devList) { if (devInfo.name == devNameAsCString) @@ -824,7 +824,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); auto device = engine->getAudioDevice( devInfo.name, - IAudioEngine::IN, + IAudioEngine::AUDIO_ENGINE_IN, sampleRate, 2); @@ -931,7 +931,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * src = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(src != NULL); auto engine = AudioEngineFactory::GetAudioEngine(); - auto devList = engine->getAudioDeviceList(IAudioEngine::OUT); + auto devList = engine->getAudioDeviceList(IAudioEngine::AUDIO_ENGINE_OUT); for (auto& devInfo : devList) { if (devInfo.name == devNameAsCString) @@ -940,7 +940,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); auto device = engine->getAudioDevice( devInfo.name, - IAudioEngine::OUT, + IAudioEngine::AUDIO_ENGINE_OUT, sampleRate, 2); diff --git a/src/main.cpp b/src/main.cpp index 3f8ab39c4..6a82f4d6b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1970,9 +1970,9 @@ void MainFrame::startRxStream() { // RX-only setup. // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. - rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); - rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); bool failed = false; @@ -2012,16 +2012,16 @@ void MainFrame::startRxStream() { // RX + TX setup // Same note as above re: number of channels. - rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 2); + rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); - rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard2SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); - txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard2SampleRate, 2); + txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); - txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 2); + txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); txOutSoundDevice->setDescription("FreeDV to Radio"); bool failed = false; @@ -2807,10 +2807,10 @@ bool MainFrame::validateSoundCardSetup() engine->start(); // For the purposes of validation, number of channels isn't necessary. - auto soundCard1InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard1SampleRate, 1); - auto soundCard1OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard1SampleRate, 1); - auto soundCard2InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::IN, g_soundCard2SampleRate, 1); - auto soundCard2OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::OUT, g_soundCard2SampleRate, 1); + auto soundCard1InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 1); + auto soundCard1OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 1); + auto soundCard2InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 1); + auto soundCard2OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 1); if (wxGetApp().m_soundCard1InDeviceName != "none" && !soundCard1InDevice) { From 18b441caac68e1cee14e0a759a5351675b5ff955 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 24 Dec 2021 23:56:44 -0800 Subject: [PATCH 31/89] Go back to codec2 master for macOS builds. --- build_osx.sh | 2 +- build_windows.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build_osx.sh b/build_osx.sh index 144bf96eb..58b6ba836 100755 --- a/build_osx.sh +++ b/build_osx.sh @@ -26,7 +26,7 @@ make install # First build and install vanilla codec2 as we need -lcodec2 to build LPCNet cd $FREEDVGUIDIR git clone https://github.com/drowe67/codec2.git -cd codec2 && git checkout ms-reliable-text && git pull +cd codec2 && git checkout master && git pull mkdir -p build_osx && cd build_osx && rm -Rf * && cmake -DBUILD_OSX_UNIVERSAL=1 .. && make -j4 # OK, build and test LPCNet diff --git a/build_windows.sh b/build_windows.sh index ff01999b4..afb89d118 100755 --- a/build_windows.sh +++ b/build_windows.sh @@ -11,8 +11,10 @@ if [ $CMAKE = "mingw64-cmake" ]; then BUILD_DIR=build_win64 + CXX=mingw64-g++ else BUILD_DIR=build_win32 + CXX=mingw32-g++ fi export FREEDVGUIDIR=${PWD} export CODEC2DIR=$FREEDVGUIDIR/codec2 From 988ee5c64c2ba045ef3e66ba23c7cc096271aad2 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 00:13:48 -0800 Subject: [PATCH 32/89] Back out Windows changes that didn't work. --- build_windows.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build_windows.sh b/build_windows.sh index afb89d118..ff01999b4 100755 --- a/build_windows.sh +++ b/build_windows.sh @@ -11,10 +11,8 @@ if [ $CMAKE = "mingw64-cmake" ]; then BUILD_DIR=build_win64 - CXX=mingw64-g++ else BUILD_DIR=build_win32 - CXX=mingw32-g++ fi export FREEDVGUIDIR=${PWD} export CODEC2DIR=$FREEDVGUIDIR/codec2 From 6de7615474b5e5e8dbf5117e4e9f09c269ce7793 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 08:35:52 -0800 Subject: [PATCH 33/89] Use PulseAudio recommended maxlength. --- src/audio/PulseAudioDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index d38c82891..c3105f735 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -68,7 +68,7 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; - buffer_attr.maxlength = pa_usec_to_bytes(100000, &sample_specification); // 100ms of data at a time at most + buffer_attr.maxlength = (uint32_t)-1; buffer_attr.tlength = (uint32_t) -1; buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; From d3dbf9d2411a1af3f997a17d488b02c14bde66d6 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 08:43:36 -0800 Subject: [PATCH 34/89] Warning cleanup. --- src/freedv_interface.cpp | 4 ++-- src/hamlib.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/freedv_interface.cpp b/src/freedv_interface.cpp index 94776d642..44a5b37ad 100644 --- a/src/freedv_interface.cpp +++ b/src/freedv_interface.cpp @@ -167,7 +167,7 @@ void FreeDVInterface::start(int txMode, int fifoSizeMs, bool singleRxThread, boo } // Loop back through SNR adjust list and subtract minimum from each entry. - for (int index = 0; index < snrAdjust_.size(); index++) + for (size_t index = 0; index < snrAdjust_.size(); index++) { snrAdjust_[index] -= minimumSnr; } @@ -811,4 +811,4 @@ void FreeDVInterface::setReliableText(const char* callsign) { reliable_text_set_string(rt, callsign, strlen(callsign)); } -} \ No newline at end of file +} diff --git a/src/hamlib.cpp b/src/hamlib.cpp index b644cc077..ea9920f1e 100644 --- a/src/hamlib.cpp +++ b/src/hamlib.cpp @@ -38,8 +38,8 @@ static bool rig_cmp(const struct rig_caps *rig1, const struct rig_caps *rig2); static int build_list(const struct rig_caps *rig, rig_ptr_t); Hamlib::Hamlib() : - m_rig(NULL), m_rig_model(0), + m_rig(NULL), m_modeBox(NULL), m_freqBox(NULL), m_currFreq(0), From ce0763774b607c84776125281514a3f95f6a20f1 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 09:11:17 -0800 Subject: [PATCH 35/89] Move adding of plot samples to the GUI thread. --- src/dlg_audiooptions.cpp | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 1a0e3856f..7ac291c4e 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -879,10 +879,22 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p // come back when the fifo is refilled continue; } - - ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + + std::mutex plotUpdateMtx; + std::condition_variable plotUpdateCV; + CallAfter([&]() { + { + std::unique_lock plotUpdateLock(plotUpdateMtx); + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + UpdatePlot(ps); + } + plotUpdateCV.notify_one(); + }); + { + std::unique_lock plotUpdateLock(plotUpdateMtx); + plotUpdateCV.wait(plotUpdateLock); + } sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, ps); } device->stop(); @@ -1005,9 +1017,21 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * continue; } - ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + std::mutex plotUpdateMtx; + std::condition_variable plotUpdateCV; + CallAfter([&]() { + { + std::unique_lock plotUpdateLock(plotUpdateMtx); + ps->add_new_short_samples(0, plotSamples, TEST_WAVEFORM_PLOT_BUF, 32767); + UpdatePlot(ps); + } + plotUpdateCV.notify_one(); + }); + { + std::unique_lock plotUpdateLock(plotUpdateMtx); + plotUpdateCV.wait(plotUpdateLock); + } sampleCount += TEST_WAVEFORM_PLOT_BUF; - CallAfter(&AudioOptsDialog::UpdatePlot, ps); } device->stop(); From dfdf4172e8388990b2e8ec13ab17bba2f746c75b Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 09:17:58 -0800 Subject: [PATCH 36/89] Apply in Audio Options should not stop the audio engine. --- src/dlg_audiooptions.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 7ac291c4e..d5715323a 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -1118,13 +1118,6 @@ void AudioOptsDialog::OnRefreshClick(wxCommandEvent& event) void AudioOptsDialog::OnApplyAudioParameters(wxCommandEvent& event) { ExchangeData(EXCHANGE_DATA_OUT); - if(m_isPaInitialized) - { - auto engine = AudioEngineFactory::GetAudioEngine(); - engine->stop(); - engine->setOnEngineError(nullptr, nullptr); - m_isPaInitialized = false; - } } //------------------------------------------------------------------------- From be43831f317b1983ca33555f2f7dc182cf005d42 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 09:45:14 -0800 Subject: [PATCH 37/89] Resolve codec2 build issues in MinGW Docker container. --- docker/fdv_win_fedora/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/fdv_win_fedora/Dockerfile b/docker/fdv_win_fedora/Dockerfile index f7725516a..09bc9f2b5 100644 --- a/docker/fdv_win_fedora/Dockerfile +++ b/docker/fdv_win_fedora/Dockerfile @@ -8,7 +8,7 @@ FROM fedora:${FED_REL} # tar: bzip2 # arm-none-eabi-gdb: ncurses-compat-libs -RUN dnf -y install --setopt=install_weak_deps=False @development-tools cmake git speexdsp-devel libsamplerate-devel octave octave-signal gnuplot sox python3-numpy automake libtool libusb1-devel wget bc glibc.i686 which bzip2 ncurses-compat-libs && useradd -m build +RUN dnf -y install --setopt=install_weak_deps=False @development-tools cmake git speexdsp-devel libsamplerate-devel octave octave-signal gnuplot sox python3-numpy automake libtool libusb1-devel wget bc glibc.i686 which bzip2 ncurses-compat-libs gcc gcc-c++ && useradd -m build # specific for windows mingw build RUN dnf install -y dnf-plugins-core From a0722fe1227199d21cddcad06fd5c1e1746e976b Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 10:12:14 -0800 Subject: [PATCH 38/89] Reenable and actually handle when pavucontrol changes devices. --- src/audio/IAudioDevice.cpp | 6 +++++ src/audio/IAudioDevice.h | 13 +++++++++- src/audio/PulseAudioDevice.cpp | 15 +++++++++++- src/audio/PulseAudioDevice.h | 1 + src/main.cpp | 43 ++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/audio/IAudioDevice.cpp b/src/audio/IAudioDevice.cpp index 240bc128e..e7ac17259 100644 --- a/src/audio/IAudioDevice.cpp +++ b/src/audio/IAudioDevice.cpp @@ -50,3 +50,9 @@ void IAudioDevice::setOnAudioError(AudioErrorCallbackFn fn, void* state) onAudioErrorFunction = fn; onAudioErrorState = state; } + +void IAudioDevice::setOnAudioDeviceChanged(AudioDeviceChangedCallbackFn fn, void* state) +{ + onAudioDeviceChangedFunction = fn; + onAudioDeviceChangedState = state; +} \ No newline at end of file diff --git a/src/audio/IAudioDevice.h b/src/audio/IAudioDevice.h index aff3fb7f3..95f6ab716 100644 --- a/src/audio/IAudioDevice.h +++ b/src/audio/IAudioDevice.h @@ -35,6 +35,7 @@ class IAudioDevice typedef std::function AudioUnderflowCallbackFn; typedef std::function AudioOverflowCallbackFn; typedef std::function AudioErrorCallbackFn; + typedef std::function AudioDeviceChangedCallbackFn; virtual int getNumChannels() = 0; @@ -68,9 +69,16 @@ class IAudioDevice // Callback must take the following parameters: // 1. Audio device. // 2. String representing the error encountered. - // 3. Pointer to user-provided state object (typically onAudioUnderflowState, defined below). + // 3. Pointer to user-provided state object (typically onAudioErrorState, defined below). void setOnAudioError(AudioErrorCallbackFn fn, void* state); + // Set device changed callback. + // Callback must take the following parameters: + // 1. Audio device. + // 2. String representing the new name of the device. + // 3. Pointer to user-provided state object (typically onAudioDeviceChangedState, defined below). + void setOnAudioDeviceChanged(AudioDeviceChangedCallbackFn fn, void* state); + protected: std::string description; @@ -85,6 +93,9 @@ class IAudioDevice AudioErrorCallbackFn onAudioErrorFunction; void* onAudioErrorState; + + AudioDeviceChangedCallbackFn onAudioDeviceChangedFunction; + void* onAudioDeviceChangedState; }; #endif // I_AUDIO_DEVICE_H diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 9a087b960..3be2a2cad 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -65,6 +65,7 @@ void PulseAudioDevice::start() pa_stream_set_underflow_callback(stream_, &PulseAudioDevice::StreamUnderflowCallback_, this); pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); + pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; @@ -78,7 +79,6 @@ void PulseAudioDevice::start() pa_stream_flags_t flags = pa_stream_flags_t( PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE | - PA_STREAM_DONT_MOVE | PA_STREAM_FAIL_ON_SUSPEND | PA_STREAM_ADJUST_LATENCY); @@ -177,3 +177,16 @@ void PulseAudioDevice::StreamOverflowCallback_(pa_stream *p, void *userdata) thisObj->onAudioOverflowFunction(*thisObj, thisObj->onAudioOverflowState); } } + +void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) +{ + auto newDevName = pa_stream_get_device_name(p); + PulseAudioDevice* thisObj = static_cast(userdata); + + devName_ = newDevName; + + if (thisObj->onAudioDeviceChangedFunction) + { + thisObj->onAudioDeviceChangedFunction(*thisObj, devName_, thisObj->onAudioOverflowState); + } +} \ No newline at end of file diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 4989d7e9c..4e682da09 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -58,6 +58,7 @@ class PulseAudioDevice : public IAudioDevice static void StreamWriteCallback_(pa_stream *s, size_t length, void *userdata); static void StreamUnderflowCallback_(pa_stream *p, void *userdata); static void StreamOverflowCallback_(pa_stream *p, void *userdata); + static void StreamMovedCallback_(pa_stream *p, void *userdata); }; #endif // PULSE_AUDIO_DEVICE_H diff --git a/src/main.cpp b/src/main.cpp index 6a82f4d6b..ad91c07b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1972,8 +1972,23 @@ void MainFrame::startRxStream() // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); + rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); + pConfig->Flush(); + }); + }, nullptr); + rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); + rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); + pConfig->Flush(); + }); + }, nullptr); bool failed = false; if (!rxInSoundDevice) @@ -2014,15 +2029,43 @@ void MainFrame::startRxStream() // Same note as above re: number of channels. rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); + rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); + pConfig->Flush(); + }); + }, nullptr); rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); + rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard2OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard2OutDeviceName"), wxGetApp().m_soundCard2OutDeviceName); + pConfig->Flush(); + }); + }, nullptr); txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); + rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard2InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard2InDeviceName"), wxGetApp().m_soundCard2InDeviceName); + pConfig->Flush(); + }); + }, nullptr); txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); txOutSoundDevice->setDescription("FreeDV to Radio"); + txOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + CallAfter([&]() { + wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); + pConfig->Flush(); + }); + }, nullptr); bool failed = false; if (!rxInSoundDevice) From daeef19ffb596e27a010d9f3b510e63f313e3c04 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 10:24:05 -0800 Subject: [PATCH 39/89] Fix compile and logic bugs from the previous commit. --- src/audio/PulseAudioDevice.cpp | 11 +++++--- src/main.cpp | 48 +++++++++++++--------------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 3be2a2cad..28aa29d53 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -21,6 +21,8 @@ //========================================================================= #include +#include + #include "PulseAudioDevice.h" PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) @@ -182,11 +184,12 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) { auto newDevName = pa_stream_get_device_name(p); PulseAudioDevice* thisObj = static_cast(userdata); - - devName_ = newDevName; + + fprintf(stderr, "%s is being renamed to %s\n", thisObj->devName_.c_str(), newDevName); + thisObj->devName_ = newDevName; if (thisObj->onAudioDeviceChangedFunction) { - thisObj->onAudioDeviceChangedFunction(*thisObj, devName_, thisObj->onAudioOverflowState); + thisObj->onAudioDeviceChangedFunction(*thisObj, thisObj->devName_, thisObj->onAudioOverflowState); } -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index ad91c07b8..1f397a38f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1973,21 +1973,17 @@ void MainFrame::startRxStream() rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); + pConfig->Flush(); }, nullptr); rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); + pConfig->Flush(); }, nullptr); bool failed = false; @@ -2030,41 +2026,33 @@ void MainFrame::startRxStream() rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); + pConfig->Flush(); }, nullptr); rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard2OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard2OutDeviceName"), wxGetApp().m_soundCard2OutDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard2OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard2OutDeviceName"), wxGetApp().m_soundCard2OutDeviceName); + pConfig->Flush(); }, nullptr); txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard2InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard2InDeviceName"), wxGetApp().m_soundCard2InDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard2InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard2InDeviceName"), wxGetApp().m_soundCard2InDeviceName); + pConfig->Flush(); }, nullptr); txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); txOutSoundDevice->setDescription("FreeDV to Radio"); txOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { - CallAfter([&]() { - wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); - pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); - pConfig->Flush(); - }); + wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); + pConfig->Write(wxT("/Audio/soundCard1OutDeviceName"), wxGetApp().m_soundCard1OutDeviceName); + pConfig->Flush(); }, nullptr); bool failed = false; From 296933d32b12447eaf1460166e04d0c9cbc04486 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 10:38:11 -0800 Subject: [PATCH 40/89] CPack packages should depend on PulseAudio if compiled for it. --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c3a47a9e7..d91a9338f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -581,7 +581,12 @@ elseif(UNIX AND NOT APPLE) # Linux packaging SET(CPACK_GENERATOR "DEB") SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Mooneer Salem ") #required - SET(CPACK_DEBIAN_PACKAGE_DEPENDS "codec2 (>= 0.9.2), libspeexdsp1 (>= 1.2~rc1.2-1+b2), libsamplerate0 (>= 0.1.9-2), libwxgtk3.0-gtk3-0v5 (>= 3.0.4+dfsg-3), libportaudio2 (>= 19.6.0-1build1), libhamlib2 (>= 3.3-10build1), libasound2 (>= 1.1.8-1), libao4 (>= 1.2.2+20180113-1), libgsm1 (>= 1.0.18-2), libsndfile1 (>= 1.0.28-6)") + if(USE_PULSEAUDIO) + SET(CPACK_DEBIAN_PACKAGE_DEPENDS "codec2 (>= 1.0.0), libspeexdsp1 (>= 1.2~rc1.2-1+b2), libsamplerate0 (>= 0.1.9-2), libwxgtk3.0-gtk3-0v5 (>= 3.0.4+dfsg-3), libpulse0 (>= 14.2-2), libhamlib2 (>= 3.3-10build1), libasound2 (>= 1.1.8-1), libao4 (>= 1.2.2+20180113-1), libgsm1 (>= 1.0.18-2), libsndfile1 (>= 1.0.28-6)") + else(USE_PULSEAUDIO) + SET(CPACK_DEBIAN_PACKAGE_DEPENDS "codec2 (>= 1.0.0), libspeexdsp1 (>= 1.2~rc1.2-1+b2), libsamplerate0 (>= 0.1.9-2), libwxgtk3.0-gtk3-0v5 (>= 3.0.4+dfsg-3), libportaudio2 (>= 19.6.0-1build1), libhamlib2 (>= 3.3-10build1), libasound2 (>= 1.1.8-1), libao4 (>= 1.2.2+20180113-1), libgsm1 (>= 1.0.18-2), libsndfile1 (>= 1.0.28-6)") + endif(USE_PULSEAUDIO) + SET(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) include(CPack) From 8bd6ece1881bf05cee7a5178e3a5be80bcaaf8e7 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 16:07:52 -0800 Subject: [PATCH 41/89] Fix typo during initialization. --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 1f397a38f..cb97d2ff2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2025,7 +2025,7 @@ void MainFrame::startRxStream() // Same note as above re: number of channels. rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); - rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); pConfig->Write(wxT("/Audio/soundCard1InDeviceName"), wxGetApp().m_soundCard1InDeviceName); pConfig->Flush(); From 2fd647efe75d1e45febb6d6fc270cce5cc458781 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 25 Dec 2021 16:08:20 -0800 Subject: [PATCH 42/89] Forgot to fix additional typo during initialization. --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index cb97d2ff2..88ffdfc94 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2041,7 +2041,7 @@ void MainFrame::startRxStream() txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); - rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { + txInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard2InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); pConfig->Write(wxT("/Audio/soundCard2InDeviceName"), wxGetApp().m_soundCard2InDeviceName); pConfig->Flush(); From 5c4e410a69c56602755e921f941f10a87f154ee9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 26 Dec 2021 11:31:52 -0800 Subject: [PATCH 43/89] Minor cleanup. --- src/audio/PulseAudioDevice.cpp | 7 +++++-- src/main.cpp | 30 ++++++------------------------ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 28aa29d53..afc359377 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -72,7 +72,7 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; buffer_attr.maxlength = (uint32_t)-1; - buffer_attr.tlength = (uint32_t) -1; + buffer_attr.tlength = pa_usec_to_bytes(20000, &sample_specification); buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; buffer_attr.fragsize = (uint32_t) -1; @@ -131,7 +131,10 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us do { pa_stream_peek(s, &data, &length); - if (!data || length == 0) break; + if (!data || length == 0) + { + break; + } if (thisObj->onAudioDataFunction) { diff --git a/src/main.cpp b/src/main.cpp index 88ffdfc94..b749a7dae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2250,20 +2250,11 @@ void MainFrame::startRxStream() int result = codec2_fifo_read(cbData->outfifo2, outdata, toRead); if (result == 0) { - if (dev.getNumChannels() == 2) - { - // write signal to both channels */ - for(size_t i = 0; i < toRead; i++, audioData += 2) - { - audioData[0] = outdata[i]; - audioData[1] = outdata[i]; - } - } - else + for (size_t i = 0; i < toRead; i++) { - for(size_t i = 0; i < toRead; i++, audioData++) + for (int j = 0; j < dev.getNumChannels(); j++) { - audioData[0] = outdata[i]; + *audioData++ = outdata[i]; } } } @@ -2380,20 +2371,11 @@ void MainFrame::startRxStream() int result = codec2_fifo_read(cbData->outfifo1, outdata, toRead); if (result == 0) { - if (dev.getNumChannels() == 2) - { - // write signal to both channels */ - for(size_t i = 0; i < toRead; i++, audioData += 2) - { - audioData[0] = outdata[i]; - audioData[1] = outdata[i]; - } - } - else + for (size_t i = 0; i < toRead; i++) { - for(size_t i = 0; i < toRead; i++, audioData++) + for (int j = 0; j < dev.getNumChannels(); j++) { - audioData[0] = outdata[i]; + *audioData++ = outdata[i]; } } } From 88169333d00af556ca130d0ab8c82a13d6f76969 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 09:12:00 -0800 Subject: [PATCH 44/89] Trim whitespace in sound device names. --- src/audio/PortAudioEngine.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index 1bcf491d8..fa196b593 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -20,6 +20,7 @@ // //========================================================================= +#include #include "portaudio.h" #include "PortAudioDevice.h" #include "PortAudioEngine.h" @@ -99,10 +100,11 @@ std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName { std::vector result; auto devInfo = getAudioDeviceList(direction); + wxString wxDeviceName = wxString::FromUTF8(deviceName).Trim(); for (auto& device : devInfo) { - if (deviceName == device.name) + if (wxDeviceName == wxString::FromUTF8(device.name).Trim()) { PaStreamParameters streamParameters; @@ -156,10 +158,11 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d std::shared_ptr PortAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) { auto deviceList = getAudioDeviceList(direction); + wxString wxDeviceName = wxString::FromUTF8(deviceName).Trim(); for (auto& dev : deviceList) { - if (dev.name == deviceName) + if (wxString::FromUTF8(dev.name).Trim() == wxDeviceName) { auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); return std::shared_ptr(devObj); From f77a56a6d83febc90a03d4a7cea4d9d276a28ade Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 10:58:48 -0800 Subject: [PATCH 45/89] Remove strict 20ms wait and replace with notification of TX/RX threads on receipt of audio data. --- src/audio/PulseAudioDevice.cpp | 20 +- src/main.cpp | 395 ++++++++++++++++++--------------- src/main.h | 42 +++- 3 files changed, 269 insertions(+), 188 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index afc359377..8660f6f6d 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -127,7 +127,9 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us { const void* data = nullptr; PulseAudioDevice* thisObj = static_cast(userdata); - + void* fullBlock = nullptr; + size_t fullSize = 0; + do { pa_stream_peek(s, &data, &length); @@ -136,13 +138,21 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us break; } - if (thisObj->onAudioDataFunction) - { - thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); - } + fullSize += length; + fullBlock = realloc(fullBlock, fullSize); + assert(fullBlock != nullptr); + + memcpy(fullBlock + fullSize - length, data, length); pa_stream_drop(s); } while (pa_stream_readable_size(s) > 0); + + if (thisObj->onAudioDataFunction) + { + thisObj->onAudioDataFunction(*thisObj, fullBlock, fullSize / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); + } + + free(fullBlock); } void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *userdata) diff --git a/src/main.cpp b/src/main.cpp index b749a7dae..3df89465d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1872,10 +1872,13 @@ void MainFrame::stopRxStream() m_RxRunning = false; //fprintf(stderr, "waiting for thread to stop\n"); - m_txRxThread->m_run = 0; - m_txRxThread->Wait(); + m_txThread->terminateThread(); + m_txThread->Wait(); + m_rxThread->terminateThread(); + m_rxThread->Wait(); //fprintf(stderr, "thread stopped\n"); - delete m_txRxThread; + delete m_txThread; + delete m_rxThread; if (rxInSoundDevice) { @@ -2135,7 +2138,7 @@ void MainFrame::startRxStream() // create FIFOs used to interface between IAudioEngine and txRx // processing loop, which iterates about once every 20ms. // Sample rate conversion, stats for spectral plots, and - // transmit processng are all performed in the txRxProcessing + // transmit processng are all performed in the tx/rxProcessing // loop. int m_fifoSize_ms = wxGetApp().m_fifoSize_ms; @@ -2158,7 +2161,7 @@ void MainFrame::startRxStream() g_AEstatus1[i] = g_AEstatus2[i] = 0; } - // These FIFOs interface between the 20ms txRxProcessing() + // These FIFOs interface between the 20ms tx/rxProcessing() // loop and the demodulator, which requires a variable number // of input samples to adjust for timing clock differences // between remote tx and rx. These FIFOs also help with the @@ -2211,7 +2214,7 @@ void MainFrame::startRxStream() }); }; - rxInSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + rxInSoundDevice->setOnAudioData([&](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short indata[size]; @@ -2224,6 +2227,8 @@ void MainFrame::startRxStream() { g_infifo1_full++; } + + m_rxThread->notify(); }, g_rxUserdata); rxInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) @@ -2274,7 +2279,7 @@ void MainFrame::startRxStream() g_AEstatus2[2]++; }, nullptr); - txInSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { + txInSoundDevice->setOnAudioData([&](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short indata[size]; @@ -2291,6 +2296,8 @@ void MainFrame::startRxStream() g_infifo2_full++; } } + + m_txThread->notify(); }, g_rxUserdata); txInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) @@ -2409,18 +2416,20 @@ void MainFrame::startRxStream() // start tx/rx processing thread - m_txRxThread = new txRxThread; + m_txThread = new txRxThread(true); + m_rxThread = new txRxThread(false); - if ( m_txRxThread->Create() != wxTHREAD_NO_ERROR ) + if ( m_txThread->Create() != wxTHREAD_NO_ERROR || m_rxThread->Create() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't create thread!")); } if (wxGetApp().m_txRxThreadHighPriority) { - m_txRxThread->SetPriority(WXTHREAD_MAX_PRIORITY); + m_txThread->SetPriority(WXTHREAD_MAX_PRIORITY); + m_rxThread->SetPriority(WXTHREAD_MAX_PRIORITY); } - if ( m_txRxThread->Run() != wxTHREAD_NO_ERROR ) + if ( m_txThread->Run() != wxTHREAD_NO_ERROR || m_rxThread->Run() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't start thread!")); } @@ -2430,10 +2439,206 @@ void MainFrame::startRxStream() //--------------------------------------------------------------------------------------------- -// Main real time procesing for tx and rx of FreeDV signals, run in its own thread +// Main real time procesing for tx and rx of FreeDV signals, run in its own threads //--------------------------------------------------------------------------------------------- -void txRxProcessing() +void txProcessing() +{ + wxStopWatch sw; + + paCallBackData *cbData = g_rxUserdata; + + // Buffers re-used by tx and rx processing. We take samples from + // the sound card, and resample them for the freedv modem input + // sample rate. Typically the sound card is running at 48 or 44.1 + // kHz, and the modem at 8kHz, however some modems such as FreeDV + // 2400A/B run at 48 kHz. + + // allocate enough room for 20ms processing buffers at maximum + // sample rate of 48 kHz. Note these buffer are used by rx and tx + // side processing + + short infreedv[10*N48]; + short insound_card[10*N48]; + short outfreedv[10*N48]; + short outsound_card[10*N48]; + int nout, freedv_samplerate; + int nfreedv; + + // analog mode runs at the standard FS = 8000 Hz + if (g_analog) { + freedv_samplerate = FS; + } + else { + // Use the maximum modem sample rate. Any needed downconversion + // just prior to sending to Codec2 will happen in FreeDVInterface. + freedv_samplerate = freedvInterface.getRxModemSampleRate(); + } + //fprintf(stderr, "sample rate: %d\n", freedv_samplerate); + + // + // TX side processing -------------------------------------------- + // + + if (((g_nSoundCards == 2) && ((g_half_duplex && g_tx) || !g_half_duplex))) { + // Lock the mode mutex so that TX state doesn't change on us during processing. + txModeChangeMutex.Lock(); + + // This while loop locks the modulator to the sample rate of + // sound card 1. We want to make sure that modulator samples + // are uninterrupted by differences in sample rate between + // this sound card and sound card 2. + + // Run code inside this while loop as soon as we have enough + // room for one frame of modem samples. Aim is to keep + // outfifo1 nice and full so we don't have any gaps in tx + // signal. + + unsigned int nsam_one_modem_frame = g_soundCard1SampleRate * freedvInterface.getTxNNomModemSamples()/freedv_samplerate; + + if (g_dump_fifo_state) { + // If this drops to zero we have a problem as we will run out of output samples + // to send to the sound driver via PortAudio + if (g_verbose) fprintf(stderr, "outfifo1 used: %6d free: %6d nsam_one_modem_frame: %d\n", + codec2_fifo_used(cbData->outfifo1), codec2_fifo_free(cbData->outfifo1), nsam_one_modem_frame); + } + + int nsam_in_48 = g_soundCard2SampleRate * freedvInterface.getTxNumSpeechSamples()/freedvInterface.getTxSpeechSampleRate(); + assert(nsam_in_48 < 10*N48); + + while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { + // OK to generate a frame of modem output samples we need + // an input frame of speech samples from the microphone. + + // infifo2 is written to by another sound card so it may + // over or underflow, but we don't really care. It will + // just result in a short interruption in audio being fed + // to codec2_enc, possibly making a click every now and + // again in the decoded audio at the other end. + + // zero speech input just in case infifo2 underflows + memset(insound_card, 0, nsam_in_48*sizeof(short)); + + // There may be recorded audio left to encode while ending TX. To handle this, + // we keep reading from the FIFO until we have less than nsam_in_48 samples available. + int nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); + if (nread != 0 && endingTx) break; + + // optionally use file for mic input signal + if (g_playFileToMicIn && (g_sfPlayFile != NULL)) { + unsigned int nsf = nsam_in_48*g_sfTxFs/g_soundCard2SampleRate; + short insf[nsf]; + + int n = sf_read_short(g_sfPlayFile, insf, nsf); + nout = resample(cbData->insrctxsf, insound_card, insf, g_soundCard2SampleRate, g_sfTxFs, nsam_in_48, n); + + if (nout == 0) { + if (g_loopPlayFileToMicIn) + sf_seek(g_sfPlayFile, 0, SEEK_SET); + else { + printf("playFileFromRadio finished, issuing event!\n"); + g_parent->CallAfter(&MainFrame::StopPlayFileToMicIn); + } + } + } + + nout = resample(cbData->insrc2, infreedv, insound_card, freedvInterface.getTxSpeechSampleRate(), g_soundCard2SampleRate, 10*N48, nsam_in_48); + + // Optional Speex pre-processor for acoustic noise reduction + if (wxGetApp().m_speexpp_enable) { + speex_preprocess_run(g_speex_st, infreedv); + } + + // Optional Mic In EQ Filtering, need mutex as filter can change at run time + + g_mutexProtectingCallbackData.Lock(); + if (cbData->micInEQEnable) { + sox_biquad_filter(cbData->sbqMicInBass, infreedv, infreedv, nout); + sox_biquad_filter(cbData->sbqMicInTreble, infreedv, infreedv, nout); + sox_biquad_filter(cbData->sbqMicInMid, infreedv, infreedv, nout); + } + g_mutexProtectingCallbackData.Unlock(); + + resample_for_plot(g_plotSpeechInFifo, infreedv, nout, freedvInterface.getTxSpeechSampleRate()); + + nfreedv = freedvInterface.getTxNNomModemSamples(); + + if (g_analog) { + nfreedv = freedvInterface.getTxNumSpeechSamples(); + + // Boost the "from mic" -> "to radio" audio in analog + // mode. The need for the gain was found by + // experiment - analog SSB sounded too quiet compared + // to digital. With digital voice we generally drive + // the "to radio" (SSB radio mic input) at about 25% + // of the peak level for normal SSB voice. So we + // introduce 6dB gain to make analog SSB sound the + // same level as the digital. Watch out for clipping. + for(int i=0; i 32767) out = 32767.0; + if (out < -32767) out = -32767.0; + outfreedv[i] = out; + } + } + else { + if (g_mode == FREEDV_MODE_800XA || g_mode == FREEDV_MODE_2400B) { + /* 800XA doesn't support complex output just yet */ + freedvInterface.transmit(outfreedv, infreedv); + } + else { + freedvInterface.complexTransmit(outfreedv, infreedv, g_TxFreqOffsetHz, nfreedv); + } + } + + // Save modulated output file if requested + if (g_recFileFromModulator && (g_sfRecFileFromModulator != NULL)) { + if (g_recFromModulatorSamples < nfreedv) { + sf_write_short(g_sfRecFileFromModulator, outfreedv, g_recFromModulatorSamples); // try infreedv to bypass codec and modem, was outfreedv + + // call stop record menu item, should be thread safe + g_parent->CallAfter(&MainFrame::StopRecFileFromModulator); + + wxPrintf("write mod output to file complete\n", g_recFromModulatorSamples); // consider a popup + } + else { + sf_write_short(g_sfRecFileFromModulator, outfreedv, nfreedv); + g_recFromModulatorSamples -= nfreedv; + } + } + + // output one frame of modem signal + + if (g_analog) + nout = resample(cbData->outsrc1, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getTxSpeechSampleRate(), 10*N48, nfreedv); + else + nout = resample(cbData->outsrc1, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getTxModemSampleRate(), 10*N48, nfreedv); + + // Attenuate signal prior to output + double dbLoss = g_txLevel / 10.0; + double scaleFactor = exp(dbLoss/20.0 * log(10.0)); + + for (int i = 0; i < nout; i++) + { + outsound_card[i] *= scaleFactor; + } + + if (g_dump_fifo_state) { + fprintf(stderr, " nout: %d\n", nout); + } + + codec2_fifo_write(cbData->outfifo1, outsound_card, nout); + } + + txModeChangeMutex.Unlock(); + } + + if (g_dump_timing) { + fprintf(stderr, "%4ld", sw.Time()); + } +} + +void rxProcessing() { wxStopWatch sw; @@ -2484,10 +2689,9 @@ void txRxProcessing() int nsam = (int)(g_soundCard1SampleRate * FRAME_DURATION); assert(nsam <= 10*N48); assert(nsam != 0); - + // while we have enough input samples available ... while (codec2_fifo_read(cbData->infifo1, insound_card, nsam) == 0 && ((g_half_duplex && !g_tx) || !g_half_duplex)) { - /* convert sound card sample rate FreeDV input sample rate */ nfreedv = resample(cbData->insrc1, infreedv, insound_card, freedv_samplerate, g_soundCard1SampleRate, N48, nsam); assert(nfreedv <= N48); @@ -2641,167 +2845,6 @@ void txRxProcessing() codec2_fifo_write(cbData->outfifo2, outsound_card, nout); } } - - // - // TX side processing -------------------------------------------- - // - - if (((g_nSoundCards == 2) && ((g_half_duplex && g_tx) || !g_half_duplex))) { - // Lock the mode mutex so that TX state doesn't change on us during processing. - txModeChangeMutex.Lock(); - - // This while loop locks the modulator to the sample rate of - // sound card 1. We want to make sure that modulator samples - // are uninterrupted by differences in sample rate between - // this sound card and sound card 2. - - // Run code inside this while loop as soon as we have enough - // room for one frame of modem samples. Aim is to keep - // outfifo1 nice and full so we don't have any gaps in tx - // signal. - - unsigned int nsam_one_modem_frame = g_soundCard1SampleRate * freedvInterface.getTxNNomModemSamples()/freedv_samplerate; - - if (g_dump_fifo_state) { - // If this drops to zero we have a problem as we will run out of output samples - // to send to the sound driver via PortAudio - if (g_verbose) fprintf(stderr, "outfifo1 used: %6d free: %6d nsam_one_modem_frame: %d\n", - codec2_fifo_used(cbData->outfifo1), codec2_fifo_free(cbData->outfifo1), nsam_one_modem_frame); - } - - int nsam_in_48 = g_soundCard2SampleRate * freedvInterface.getTxNumSpeechSamples()/freedvInterface.getTxSpeechSampleRate(); - assert(nsam_in_48 < 10*N48); - - while((unsigned)codec2_fifo_free(cbData->outfifo1) >= nsam_one_modem_frame) { - // OK to generate a frame of modem output samples we need - // an input frame of speech samples from the microphone. - - // infifo2 is written to by another sound card so it may - // over or underflow, but we don't really care. It will - // just result in a short interruption in audio being fed - // to codec2_enc, possibly making a click every now and - // again in the decoded audio at the other end. - - // zero speech input just in case infifo2 underflows - memset(insound_card, 0, nsam_in_48*sizeof(short)); - - // There may be recorded audio left to encode while ending TX. To handle this, - // we keep reading from the FIFO until we have less than nsam_in_48 samples available. - int nread = codec2_fifo_read(cbData->infifo2, insound_card, nsam_in_48); - if (nread != 0 && endingTx) break; - - // optionally use file for mic input signal - if (g_playFileToMicIn && (g_sfPlayFile != NULL)) { - unsigned int nsf = nsam_in_48*g_sfTxFs/g_soundCard2SampleRate; - short insf[nsf]; - - int n = sf_read_short(g_sfPlayFile, insf, nsf); - nout = resample(cbData->insrctxsf, insound_card, insf, g_soundCard2SampleRate, g_sfTxFs, nsam_in_48, n); - - if (nout == 0) { - if (g_loopPlayFileToMicIn) - sf_seek(g_sfPlayFile, 0, SEEK_SET); - else { - printf("playFileFromRadio finished, issuing event!\n"); - g_parent->CallAfter(&MainFrame::StopPlayFileToMicIn); - } - } - } - - nout = resample(cbData->insrc2, infreedv, insound_card, freedvInterface.getTxSpeechSampleRate(), g_soundCard2SampleRate, 10*N48, nsam_in_48); - - // Optional Speex pre-processor for acoustic noise reduction - if (wxGetApp().m_speexpp_enable) { - speex_preprocess_run(g_speex_st, infreedv); - } - - // Optional Mic In EQ Filtering, need mutex as filter can change at run time - - g_mutexProtectingCallbackData.Lock(); - if (cbData->micInEQEnable) { - sox_biquad_filter(cbData->sbqMicInBass, infreedv, infreedv, nout); - sox_biquad_filter(cbData->sbqMicInTreble, infreedv, infreedv, nout); - sox_biquad_filter(cbData->sbqMicInMid, infreedv, infreedv, nout); - } - g_mutexProtectingCallbackData.Unlock(); - - resample_for_plot(g_plotSpeechInFifo, infreedv, nout, freedvInterface.getTxSpeechSampleRate()); - - nfreedv = freedvInterface.getTxNNomModemSamples(); - - if (g_analog) { - nfreedv = freedvInterface.getTxNumSpeechSamples(); - - // Boost the "from mic" -> "to radio" audio in analog - // mode. The need for the gain was found by - // experiment - analog SSB sounded too quiet compared - // to digital. With digital voice we generally drive - // the "to radio" (SSB radio mic input) at about 25% - // of the peak level for normal SSB voice. So we - // introduce 6dB gain to make analog SSB sound the - // same level as the digital. Watch out for clipping. - for(int i=0; i 32767) out = 32767.0; - if (out < -32767) out = -32767.0; - outfreedv[i] = out; - } - } - else { - if (g_mode == FREEDV_MODE_800XA || g_mode == FREEDV_MODE_2400B) { - /* 800XA doesn't support complex output just yet */ - freedvInterface.transmit(outfreedv, infreedv); - } - else { - freedvInterface.complexTransmit(outfreedv, infreedv, g_TxFreqOffsetHz, nfreedv); - } - } - - // Save modulated output file if requested - if (g_recFileFromModulator && (g_sfRecFileFromModulator != NULL)) { - if (g_recFromModulatorSamples < nfreedv) { - sf_write_short(g_sfRecFileFromModulator, outfreedv, g_recFromModulatorSamples); // try infreedv to bypass codec and modem, was outfreedv - - // call stop record menu item, should be thread safe - g_parent->CallAfter(&MainFrame::StopRecFileFromModulator); - - wxPrintf("write mod output to file complete\n", g_recFromModulatorSamples); // consider a popup - } - else { - sf_write_short(g_sfRecFileFromModulator, outfreedv, nfreedv); - g_recFromModulatorSamples -= nfreedv; - } - } - - // output one frame of modem signal - - if (g_analog) - nout = resample(cbData->outsrc1, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getTxSpeechSampleRate(), 10*N48, nfreedv); - else - nout = resample(cbData->outsrc1, outsound_card, outfreedv, g_soundCard1SampleRate, freedvInterface.getTxModemSampleRate(), 10*N48, nfreedv); - - // Attenuate signal prior to output - double dbLoss = g_txLevel / 10.0; - double scaleFactor = exp(dbLoss/20.0 * log(10.0)); - - for (int i = 0; i < nout; i++) - { - outsound_card[i] *= scaleFactor; - } - - if (g_dump_fifo_state) { - fprintf(stderr, " nout: %d\n", nout); - } - - codec2_fifo_write(cbData->outfifo1, outsound_card, nout); - } - - txModeChangeMutex.Unlock(); - } - - if (g_dump_timing) { - fprintf(stderr, "%4ld", sw.Time()); - } } bool MainFrame::validateSoundCardSetup() diff --git a/src/main.h b/src/main.h index 8104c27b7..e2f5d36f8 100644 --- a/src/main.h +++ b/src/main.h @@ -489,7 +489,8 @@ class MainFrame : public TopFrame bool m_RxRunning; - txRxThread* m_txRxThread; + txRxThread* m_txThread; + txRxThread* m_rxThread; bool OpenHamlibRig(); void OpenSerialPort(void); @@ -650,7 +651,8 @@ class MainFrame : public TopFrame bool validateSoundCardSetup(); }; -void txRxProcessing(); +void txProcessing(); +void rxProcessing(); //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= // class txRxThread - experimental tx/rx processing thread @@ -658,15 +660,23 @@ void txRxProcessing(); class txRxThread : public wxThread { public: - txRxThread(void) : wxThread(wxTHREAD_JOINABLE) { m_run = 1; } + txRxThread(bool tx) + : wxThread(wxTHREAD_JOINABLE) + , m_tx(tx) + , m_run(1) { /* empty */ } // thread execution starts here void *Entry() { - while (m_run) + while (true) { - txRxProcessing(); - wxThread::Sleep(20); + { + std::unique_lock lk(m_processingMutex); + m_processingCondVar.wait_for(lk, std::chrono::milliseconds(100)); + if (!m_run) break; + } + if (m_tx) txProcessing(); + else rxProcessing(); } return NULL; @@ -676,7 +686,25 @@ class txRxThread : public wxThread // stopped with Delete() (but not when it is Kill()ed!) void OnExit() { } -public: + void terminateThread() + { + m_run = 0; + notify(); + } + + void notify() + { + { + std::unique_lock lk(m_processingMutex); + m_processingCondVar.notify_all(); + } + } + + std::mutex m_processingMutex; + std::condition_variable m_processingCondVar; + +private: + bool m_tx; bool m_run; }; From 27797b1d873eaa6787f903026bf1048cc245dd20 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 11:12:58 -0800 Subject: [PATCH 46/89] Change version number for testing. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d91a9338f..df3101649 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,8 +49,8 @@ message(STATUS "Compilation date = XX${DATE_RESULT}XX") # set(FREEDV_VERSION_MAJOR 1) set(FREEDV_VERSION_MINOR 6) -set(FREEDV_VERSION_PATCH 1) -set(FREEDV_VERSION_SUFFIX "") +set(FREEDV_VERSION_PATCH 2) +set(FREEDV_VERSION_SUFFIX "pulseaudio-devel") set(FREEDV_VERSION ${FREEDV_VERSION_MAJOR}.${FREEDV_VERSION_MINOR}) if(FREEDV_VERSION_PATCH) From b38765e3fbd560440be22ef6fc75e41fbfb80fab Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 11:26:44 -0800 Subject: [PATCH 47/89] Add logging to see if we time out while waiting for audio. --- src/main.cpp | 39 +++++++++++++++++++++++++++++---------- src/main.h | 14 +++++++++++++- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3df89465d..ceb0f13c3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1872,13 +1872,19 @@ void MainFrame::stopRxStream() m_RxRunning = false; //fprintf(stderr, "waiting for thread to stop\n"); - m_txThread->terminateThread(); - m_txThread->Wait(); + if (m_txThread) + { + m_txThread->terminateThread(); + m_txThread->Wait(); + delete m_txThread; + m_txThread = nullptr; + } + m_rxThread->terminateThread(); m_rxThread->Wait(); //fprintf(stderr, "thread stopped\n"); - delete m_txThread; delete m_rxThread; + m_rxThread = nullptr; if (rxInSoundDevice) { @@ -2416,22 +2422,35 @@ void MainFrame::startRxStream() // start tx/rx processing thread - m_txThread = new txRxThread(true); - m_rxThread = new txRxThread(false); + if (txInSoundDevice && txOutSoundDevice) + { + m_txThread = new txRxThread(true); + if ( m_txThread->Create() != wxTHREAD_NO_ERROR ) + { + wxLogError(wxT("Can't create TX thread!")); + } + if (wxGetApp().m_txRxThreadHighPriority) { + m_txThread->SetPriority(WXTHREAD_MAX_PRIORITY); + } + if ( m_txThread->Run() != wxTHREAD_NO_ERROR ) + { + wxLogError(wxT("Can't start TX thread!")); + } + } - if ( m_txThread->Create() != wxTHREAD_NO_ERROR || m_rxThread->Create() != wxTHREAD_NO_ERROR ) + m_rxThread = new txRxThread(false); + if ( m_rxThread->Create() != wxTHREAD_NO_ERROR ) { - wxLogError(wxT("Can't create thread!")); + wxLogError(wxT("Can't create RX thread!")); } if (wxGetApp().m_txRxThreadHighPriority) { - m_txThread->SetPriority(WXTHREAD_MAX_PRIORITY); m_rxThread->SetPriority(WXTHREAD_MAX_PRIORITY); } - if ( m_txThread->Run() != wxTHREAD_NO_ERROR || m_rxThread->Run() != wxTHREAD_NO_ERROR ) + if ( m_rxThread->Run() != wxTHREAD_NO_ERROR ) { - wxLogError(wxT("Can't start thread!")); + wxLogError(wxT("Can't start RX thread!")); } } diff --git a/src/main.h b/src/main.h index e2f5d36f8..bf939fbb5 100644 --- a/src/main.h +++ b/src/main.h @@ -668,11 +668,23 @@ class txRxThread : public wxThread // thread execution starts here void *Entry() { + bool suppress_time_out = false; while (true) { { std::unique_lock lk(m_processingMutex); - m_processingCondVar.wait_for(lk, std::chrono::milliseconds(100)); + if (m_processingCondVar.wait_for(lk, std::chrono::milliseconds(100)) == std::cv_status::timeout) + { + if (!suppress_time_out) + { + fprintf(stderr, "txRxThread: timeout while waiting for CV, tx = %d\n", m_tx); + } + suppress_time_out = true; + } + else + { + suppress_time_out = false; + } if (!m_run) break; } if (m_tx) txProcessing(); From a210b0a1193d5cec7c619d5540e03ae8af9421a4 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 13:36:30 -0800 Subject: [PATCH 48/89] Start/stop sound devices after threads start/stop. --- src/main.cpp | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ceb0f13c3..4e506e509 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1871,21 +1871,6 @@ void MainFrame::stopRxStream() { m_RxRunning = false; - //fprintf(stderr, "waiting for thread to stop\n"); - if (m_txThread) - { - m_txThread->terminateThread(); - m_txThread->Wait(); - delete m_txThread; - m_txThread = nullptr; - } - - m_rxThread->terminateThread(); - m_rxThread->Wait(); - //fprintf(stderr, "thread stopped\n"); - delete m_rxThread; - m_rxThread = nullptr; - if (rxInSoundDevice) { rxInSoundDevice->stop(); @@ -1910,6 +1895,21 @@ void MainFrame::stopRxStream() txOutSoundDevice.reset(); } + //fprintf(stderr, "waiting for thread to stop\n"); + if (m_txThread) + { + m_txThread->terminateThread(); + m_txThread->Wait(); + delete m_txThread; + m_txThread = nullptr; + } + + m_rxThread->terminateThread(); + m_rxThread->Wait(); + //fprintf(stderr, "thread stopped\n"); + delete m_rxThread; + m_rxThread = nullptr; + destroy_fifos(); destroy_src(); @@ -2409,19 +2409,7 @@ void MainFrame::startRxStream() }, nullptr); } - // Start sound devices - rxInSoundDevice->start(); - rxOutSoundDevice->start(); - if (txInSoundDevice && txOutSoundDevice) - { - txInSoundDevice->start(); - txOutSoundDevice->start(); - } - - if (g_verbose) fprintf(stderr, "starting tx/rx processing thread\n"); - // start tx/rx processing thread - if (txInSoundDevice && txOutSoundDevice) { m_txThread = new txRxThread(true); @@ -2453,6 +2441,16 @@ void MainFrame::startRxStream() wxLogError(wxT("Can't start RX thread!")); } + // Start sound devices + rxInSoundDevice->start(); + rxOutSoundDevice->start(); + if (txInSoundDevice && txOutSoundDevice) + { + txInSoundDevice->start(); + txOutSoundDevice->start(); + } + + if (g_verbose) fprintf(stderr, "starting tx/rx processing thread\n"); } } From 9efa83756561730018b310ec4fbf0508b780e042 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 16:17:52 -0800 Subject: [PATCH 49/89] Create FIFOs for TX only if enabled. --- src/main.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 4e506e509..308f5482f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1929,8 +1929,8 @@ void MainFrame::destroy_fifos(void) { codec2_fifo_destroy(g_rxUserdata->infifo1); codec2_fifo_destroy(g_rxUserdata->outfifo1); - codec2_fifo_destroy(g_rxUserdata->infifo2); - codec2_fifo_destroy(g_rxUserdata->outfifo2); + if (g_rxUserdata->infifo2) codec2_fifo_destroy(g_rxUserdata->infifo2); + if (g_rxUserdata->outfifo2) codec2_fifo_destroy(g_rxUserdata->outfifo2); codec2_fifo_destroy(g_rxUserdata->rxinfifo); codec2_fifo_destroy(g_rxUserdata->rxoutfifo); } @@ -2149,15 +2149,21 @@ void MainFrame::startRxStream() int m_fifoSize_ms = wxGetApp().m_fifoSize_ms; int soundCard1FifoSizeSamples = m_fifoSize_ms*g_soundCard1SampleRate/1000; - int soundCard2FifoSizeSamples = m_fifoSize_ms*g_soundCard2SampleRate/1000; - g_rxUserdata->infifo1 = codec2_fifo_create(soundCard1FifoSizeSamples); g_rxUserdata->outfifo1 = codec2_fifo_create(soundCard1FifoSizeSamples); - g_rxUserdata->outfifo2 = codec2_fifo_create(soundCard2FifoSizeSamples); - g_rxUserdata->infifo2 = codec2_fifo_create(soundCard2FifoSizeSamples); - if (g_verbose) fprintf(stderr, "fifoSize_ms: %d infifo1/outfilo1: %d infifo2/outfilo2: %d\n", - wxGetApp().m_fifoSize_ms, soundCard1FifoSizeSamples, soundCard2FifoSizeSamples); + if (txInSoundDevice && txOutSoundDevice) + { + int soundCard2FifoSizeSamples = m_fifoSize_ms*g_soundCard2SampleRate/1000; + g_rxUserdata->outfifo2 = codec2_fifo_create(soundCard2FifoSizeSamples); + g_rxUserdata->infifo2 = codec2_fifo_create(soundCard2FifoSizeSamples); + + if (g_verbose) fprintf(stderr, "fifoSize_ms: %d infifo2/outfilo2: %d\n", + wxGetApp().m_fifoSize_ms, soundCard2FifoSizeSamples); + } + + if (g_verbose) fprintf(stderr, "fifoSize_ms: %d infifo1/outfilo1 %d\n", + wxGetApp().m_fifoSize_ms, soundCard1FifoSizeSamples); // reset debug stats for FIFOs From d14f6a6c3efdce124d16c7ad301e004ef16331f9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 16:32:14 -0800 Subject: [PATCH 50/89] Fragment size should match target size. --- src/audio/PulseAudioDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 8660f6f6d..cd8472b6d 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -75,7 +75,7 @@ void PulseAudioDevice::start() buffer_attr.tlength = pa_usec_to_bytes(20000, &sample_specification); buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; - buffer_attr.fragsize = (uint32_t) -1; + buffer_attr.fragsize = buffer_attr.tlength; // Stream flags pa_stream_flags_t flags = pa_stream_flags_t( From 80c25ae625559df9e4ae5519f0ad6bc1d43e8c81 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Mon, 27 Dec 2021 16:36:23 -0800 Subject: [PATCH 51/89] Prevent segfault if more than one thread executes stopRxStream at the same time. --- src/main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 308f5482f..c7ac4b437 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1862,11 +1862,15 @@ void MainFrame::OnTogBtnOnOff(wxCommandEvent& event) optionsDlg->setSessionActive(m_RxRunning); } +static std::mutex stoppingMutex; + //------------------------------------------------------------------------- // stopRxStream() //------------------------------------------------------------------------- void MainFrame::stopRxStream() { + std::unique_lock lk(stoppingMutex); + if(m_RxRunning) { m_RxRunning = false; From 7d62fddd25684bd0ef6c42739f6729f0a1d186bf Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 31 Dec 2021 10:56:17 -0800 Subject: [PATCH 52/89] Auto-build both PulseAudio and PortAudio variants on PR push. --- .github/workflows/cmake.yml | 9 +++++++-- build_linux.sh | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ed52afb66..802fc9a73 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -26,7 +26,12 @@ jobs: sudo apt-get update sudo apt-get install libpulse-dev libspeexdsp-dev libsamplerate0-dev sox git libwxgtk3.0-gtk3-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev - - name: Build freedv-gui + - name: Build freedv-gui using PortAudio shell: bash working-directory: ${{github.workspace}} - run: ./build_linux.sh + run: ./build_linux.sh portaudio + + - name: Build freedv-gui using PulseAudio + shell: bash + working-directory: ${{github.workspace}} + run: ./build_linux.sh pulseaudio diff --git a/build_linux.sh b/build_linux.sh index b9ee035bc..7860d7e31 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# build_ubuntu.sh +# build_linux.sh # # Build script for Ubuntu and Fedora Linux, git pulls codec2 and # lpcnet repos so they are available for parallel development. @@ -7,6 +7,13 @@ # Echo what you are doing, and fail if any of the steps fail: set -x -e +# Allow building of either PulseAudio or PortAudio variants +FREEDV_VARIANT=${1:-portaudio} +if [[ "$FREEDV_VARIANT" != "portaudio" && "$FREEDV_VARIANT" != "pulseaudio" ]]; then + echo "Usage: build_linux.sh [portaudio|pulseaudio]" + exit -1 +fi + export FREEDVGUIDIR=${PWD} export CODEC2DIR=$FREEDVGUIDIR/codec2 export LPCNETDIR=$FREEDVGUIDIR/LPCNet @@ -46,5 +53,8 @@ export LD_LIBRARY_PATH=$LPCNETDIR/build_linux/src # Finally, build freedv-gui cd $FREEDVGUIDIR && git pull mkdir -p build_linux && cd build_linux && rm -Rf * -cmake -DUSE_PULSEAUDIO=1 -DCMAKE_BUILD_TYPE=Debug -DCODEC2_BUILD_DIR=$CODEC2DIR/build_linux -DLPCNET_BUILD_DIR=$LPCNETDIR/build_linux .. +if [[ "$FREEDV_VARIANT" == "pulseaudio" ]]; then + PULSEAUDIO_PARAM="-DUSE_PULSEAUDIO=1" +fi +cmake $PULSEAUDIO_PARAM -DCMAKE_BUILD_TYPE=Debug -DCODEC2_BUILD_DIR=$CODEC2DIR/build_linux -DLPCNET_BUILD_DIR=$LPCNETDIR/build_linux .. make VERBOSE=1 From 0e1de24a4c0949d62b4f516d8a138dce542f516c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 31 Dec 2021 10:58:53 -0800 Subject: [PATCH 53/89] Resolve PortAudio related compiler errors. --- src/audio/PortAudioEngine.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index fa196b593..59e20f16f 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -100,11 +100,11 @@ std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName { std::vector result; auto devInfo = getAudioDeviceList(direction); - wxString wxDeviceName = wxString::FromUTF8(deviceName).Trim(); + wxString wxDeviceName = wxString::FromUTF8(deviceName.c_str()).Trim(); for (auto& device : devInfo) { - if (wxDeviceName == wxString::FromUTF8(device.name).Trim()) + if (wxDeviceName == wxString::FromUTF8(device.name.c_str()).Trim()) { PaStreamParameters streamParameters; @@ -158,11 +158,11 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d std::shared_ptr PortAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) { auto deviceList = getAudioDeviceList(direction); - wxString wxDeviceName = wxString::FromUTF8(deviceName).Trim(); + wxString wxDeviceName = wxString::FromUTF8(deviceName.c_str()).Trim(); for (auto& dev : deviceList) { - if (wxString::FromUTF8(dev.name).Trim() == wxDeviceName) + if (wxString::FromUTF8(dev.name.c_str()).Trim() == wxDeviceName) { auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); return std::shared_ptr(devObj); @@ -170,4 +170,4 @@ std::shared_ptr PortAudioEngine::getAudioDevice(std::string device } return nullptr; -} \ No newline at end of file +} From 8b6b72f8c6dcd155fcdb33bd0af14d24e487c565 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 31 Dec 2021 11:32:15 -0800 Subject: [PATCH 54/89] Ensure that PulseAudio stream termination is synchronous to avoid referring to already-freed memory. --- src/audio/PulseAudioDevice.cpp | 23 +++++++++++++++++++++-- src/audio/PulseAudioDevice.h | 7 ++++++- src/main.cpp | 6 ------ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index cd8472b6d..864ffd8e6 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -68,7 +68,8 @@ void PulseAudioDevice::start() pa_stream_set_underflow_callback(stream_, &PulseAudioDevice::StreamUnderflowCallback_, this); pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); - + pa_stream_set_state_callback(stream_, &PulseAudioDevice::StreamStateCallback_, this); + // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; buffer_attr.maxlength = (uint32_t)-1; @@ -114,11 +115,16 @@ void PulseAudioDevice::stop() { if (stream_ != nullptr) { + std::unique_lock lk(streamStateMutex_); + pa_threaded_mainloop_lock(mainloop_); pa_stream_disconnect(stream_); pa_threaded_mainloop_unlock(mainloop_); + + streamStateCondVar_.wait(lk); + pa_stream_unref(stream_); - + stream_ = nullptr; } } @@ -173,6 +179,19 @@ void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *u } } +void PulseAudioDevice::StreamStateCallback_(pa_stream *p, void *userdata) +{ + PulseAudioDevice* thisObj = static_cast(userdata); + + // This method is only used for termination to ensure that it's synchronous and + // does not accidentally refer to already freed memory. + if (pa_stream_get_state(p) == PA_STREAM_TERMINATED) + { + std::unique_lock lk(thisObj->streamStateMutex_); + thisObj->streamStateCondVar_.notify_all(); + } +} + void PulseAudioDevice::StreamUnderflowCallback_(pa_stream *p, void *userdata) { PulseAudioDevice* thisObj = static_cast(userdata); diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 4e682da09..25a86ead5 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -23,6 +23,8 @@ #ifndef PULSE_AUDIO_DEVICE_H #define PULSE_AUDIO_DEVICE_H +#include +#include #include #include #include "IAudioEngine.h" @@ -53,12 +55,15 @@ class PulseAudioDevice : public IAudioDevice IAudioEngine::AudioDirection direction_; int sampleRate_; int numChannels_; - + std::mutex streamStateMutex_; + std::condition_variable streamStateCondVar_; + static void StreamReadCallback_(pa_stream *s, size_t length, void *userdata); static void StreamWriteCallback_(pa_stream *s, size_t length, void *userdata); static void StreamUnderflowCallback_(pa_stream *p, void *userdata); static void StreamOverflowCallback_(pa_stream *p, void *userdata); static void StreamMovedCallback_(pa_stream *p, void *userdata); + static void StreamStateCallback_(pa_stream *p, void *userdata); }; #endif // PULSE_AUDIO_DEVICE_H diff --git a/src/main.cpp b/src/main.cpp index c7ac4b437..116989a38 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1908,12 +1908,6 @@ void MainFrame::stopRxStream() m_txThread = nullptr; } - m_rxThread->terminateThread(); - m_rxThread->Wait(); - //fprintf(stderr, "thread stopped\n"); - delete m_rxThread; - m_rxThread = nullptr; - destroy_fifos(); destroy_src(); From ea616b7bdea745d3747d971c4aa7cbb4655769b9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 31 Dec 2021 11:37:09 -0800 Subject: [PATCH 55/89] Set TX and RX thread pointers to default values. --- src/main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 116989a38..9dd77a95c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -585,6 +585,8 @@ MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent) #endif m_RxRunning = false; + m_txThread = nullptr; + m_rxThread = nullptr; #ifdef _USE_ONIDLE Connect(wxEVT_IDLE, wxIdleEventHandler(MainFrame::OnIdle), NULL, this); @@ -1908,6 +1910,14 @@ void MainFrame::stopRxStream() m_txThread = nullptr; } + if (m_rxThread) + { + m_rxThread->terminateThread(); + m_rxThread->Wait(); + delete m_txThread; + m_rxThread = nullptr; + } + destroy_fifos(); destroy_src(); From 5ecb957a8dc6af15dbd0951c0f882db7c6f4b6be Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 31 Dec 2021 11:43:19 -0800 Subject: [PATCH 56/89] Warning removal inside PulseAudioEngine. --- src/audio/PulseAudioDevice.cpp | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 864ffd8e6..819616b16 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -133,8 +133,6 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us { const void* data = nullptr; PulseAudioDevice* thisObj = static_cast(userdata); - void* fullBlock = nullptr; - size_t fullSize = 0; do { @@ -144,21 +142,13 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us break; } - fullSize += length; - fullBlock = realloc(fullBlock, fullSize); - assert(fullBlock != nullptr); + if (thisObj->onAudioDataFunction) + { + thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); + } - memcpy(fullBlock + fullSize - length, data, length); - pa_stream_drop(s); } while (pa_stream_readable_size(s) > 0); - - if (thisObj->onAudioDataFunction) - { - thisObj->onAudioDataFunction(*thisObj, fullBlock, fullSize / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); - } - - free(fullBlock); } void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *userdata) From 8e5981ecb3ea65636c7e0ef2a17dc8cef6d0aaa5 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 1 Jan 2022 11:22:53 -0800 Subject: [PATCH 57/89] Resolve UTF8 encoding issue with audio device names. --- src/audio/AudioDeviceSpecification.h | 6 +++--- src/audio/IAudioEngine.h | 4 ++-- src/audio/PortAudioEngine.cpp | 14 +++++++------- src/audio/PortAudioEngine.h | 4 ++-- src/audio/PulseAudioEngine.cpp | 6 +++--- src/audio/PulseAudioEngine.h | 4 ++-- src/dlg_audiooptions.cpp | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/audio/AudioDeviceSpecification.h b/src/audio/AudioDeviceSpecification.h index da2c5eea2..303de1ba1 100644 --- a/src/audio/AudioDeviceSpecification.h +++ b/src/audio/AudioDeviceSpecification.h @@ -23,14 +23,14 @@ #ifndef AUDIO_DEVICE_SPECIFICATION_H #define AUDIO_DEVICE_SPECIFICATION_H -#include +#include #include struct AudioDeviceSpecification { int deviceId; - std::string name; - std::string apiName; + wxString name; + wxString apiName; int defaultSampleRate; int maxChannels; diff --git a/src/audio/IAudioEngine.h b/src/audio/IAudioEngine.h index dac0161a5..828330dd2 100644 --- a/src/audio/IAudioEngine.h +++ b/src/audio/IAudioEngine.h @@ -42,8 +42,8 @@ class IAudioEngine virtual void stop() = 0; virtual std::vector getAudioDeviceList(AudioDirection direction) = 0; virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction) = 0; - virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) = 0; - virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction) = 0; + virtual std::shared_ptr getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels) = 0; + virtual std::vector getSupportedSampleRates(wxString deviceName, AudioDirection direction) = 0; // Set error callback. // Callback must take the following parameters: diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index 59e20f16f..e75675949 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -83,7 +83,7 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD { AudioDeviceSpecification device; device.deviceId = index; - device.name = deviceInfo->name; + device.name = wxString::FromUTF8(deviceInfo->name); device.apiName = hostApiName; device.maxChannels = direction == AUDIO_ENGINE_IN ? deviceInfo->maxInputChannels : deviceInfo->maxOutputChannels; @@ -96,15 +96,15 @@ std::vector PortAudioEngine::getAudioDeviceList(AudioD return result; } -std::vector PortAudioEngine::getSupportedSampleRates(std::string deviceName, AudioDirection direction) +std::vector PortAudioEngine::getSupportedSampleRates(wxString deviceName, AudioDirection direction) { std::vector result; auto devInfo = getAudioDeviceList(direction); - wxString wxDeviceName = wxString::FromUTF8(deviceName.c_str()).Trim(); + wxString wxDeviceName = deviceName.Trim(); for (auto& device : devInfo) { - if (wxDeviceName == wxString::FromUTF8(device.name.c_str()).Trim()) + if (wxDeviceName == device.name.Trim()) { PaStreamParameters streamParameters; @@ -155,14 +155,14 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d return AudioDeviceSpecification::GetInvalidDevice(); } -std::shared_ptr PortAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) +std::shared_ptr PortAudioEngine::getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels) { auto deviceList = getAudioDeviceList(direction); - wxString wxDeviceName = wxString::FromUTF8(deviceName.c_str()).Trim(); + wxString wxDeviceName = deviceName.Trim(); for (auto& dev : deviceList) { - if (wxString::FromUTF8(dev.name.c_str()).Trim() == wxDeviceName) + if (dev.name.Trim() == wxDeviceName) { auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); return std::shared_ptr(devObj); diff --git a/src/audio/PortAudioEngine.h b/src/audio/PortAudioEngine.h index a63c042ed..fde82319e 100644 --- a/src/audio/PortAudioEngine.h +++ b/src/audio/PortAudioEngine.h @@ -35,8 +35,8 @@ class PortAudioEngine : public IAudioEngine virtual void stop(); virtual std::vector getAudioDeviceList(AudioDirection direction); virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction); - virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels); - virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction); + virtual std::shared_ptr getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels); + virtual std::vector getSupportedSampleRates(wxString deviceName, AudioDirection direction); private: bool initialized_; diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index 906d794dd..d80ec834c 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -209,7 +209,7 @@ std::vector PulseAudioEngine::getAudioDeviceList(Audio return tempObj.result; } -std::vector PulseAudioEngine::getSupportedSampleRates(std::string deviceName, AudioDirection direction) +std::vector PulseAudioEngine::getSupportedSampleRates(wxString deviceName, AudioDirection direction) { std::vector result; @@ -267,13 +267,13 @@ AudioDeviceSpecification PulseAudioEngine::getDefaultAudioDevice(AudioDirection return AudioDeviceSpecification::GetInvalidDevice(); } -std::shared_ptr PulseAudioEngine::getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels) +std::shared_ptr PulseAudioEngine::getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels) { auto deviceList = getAudioDeviceList(direction); for (auto& dev : deviceList) { - if (dev.name == deviceName) + if (dev.name.Trim() == deviceName.Trim()) { auto devObj = new PulseAudioDevice( diff --git a/src/audio/PulseAudioEngine.h b/src/audio/PulseAudioEngine.h index 27c526073..7d4f5e937 100644 --- a/src/audio/PulseAudioEngine.h +++ b/src/audio/PulseAudioEngine.h @@ -36,8 +36,8 @@ class PulseAudioEngine : public IAudioEngine virtual void stop(); virtual std::vector getAudioDeviceList(AudioDirection direction); virtual AudioDeviceSpecification getDefaultAudioDevice(AudioDirection direction); - virtual std::shared_ptr getAudioDevice(std::string deviceName, AudioDirection direction, int sampleRate, int numChannels); - virtual std::vector getSupportedSampleRates(std::string deviceName, AudioDirection direction); + virtual std::shared_ptr getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels); + virtual std::vector getSupportedSampleRates(wxString deviceName, AudioDirection direction); private: bool initialized_; diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index d5715323a..6afd45dad 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -615,7 +615,7 @@ int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, w cbSampleRate->Clear(); for (auto& dev : deviceList) { - if (wxString(dev.name).Trim() == devName.Trim()) + if (dev.name.Trim() == devName.Trim()) { auto supportedSampleRates = engine->getSupportedSampleRates( From df63b025db799cd601e4785c43e0c9af76750937 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 1 Jan 2022 11:33:23 -0800 Subject: [PATCH 58/89] Resolve PulseAudio compiler errors. --- src/audio/PulseAudioDevice.cpp | 9 ++++----- src/audio/PulseAudioDevice.h | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 819616b16..363aa87dd 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -25,7 +25,7 @@ #include "PulseAudioDevice.h" -PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) +PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) , mainloop_(mainloop) , stream_(nullptr) @@ -59,7 +59,7 @@ void PulseAudioDevice::start() { if (onAudioErrorFunction) { - onAudioErrorFunction(*this, std::string("Could not create PulseAudio stream for ") + devName_, onAudioErrorState); + onAudioErrorFunction(*this, std::string("Could not create PulseAudio stream for ") + (const char*)devName_.ToUTF8(), onAudioErrorState); } pa_threaded_mainloop_unlock(mainloop_); return; @@ -104,7 +104,7 @@ void PulseAudioDevice::start() { if (onAudioErrorFunction) { - onAudioErrorFunction(*this, std::string("Could not connect PulseAudio stream to ") + devName_, onAudioErrorState); + onAudioErrorFunction(*this, std::string("Could not connect PulseAudio stream to ") + (const char*)devName_.ToUTF8(), onAudioErrorState); } } @@ -207,11 +207,10 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) auto newDevName = pa_stream_get_device_name(p); PulseAudioDevice* thisObj = static_cast(userdata); - fprintf(stderr, "%s is being renamed to %s\n", thisObj->devName_.c_str(), newDevName); thisObj->devName_ = newDevName; if (thisObj->onAudioDeviceChangedFunction) { - thisObj->onAudioDeviceChangedFunction(*thisObj, thisObj->devName_, thisObj->onAudioOverflowState); + thisObj->onAudioDeviceChangedFunction(*thisObj, (const char*)thisObj->devName_.ToUTF8(), thisObj->onAudioOverflowState); } } diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 25a86ead5..1d6a97922 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -25,7 +25,7 @@ #include #include -#include +#include #include #include "IAudioEngine.h" #include "IAudioDevice.h" @@ -44,14 +44,14 @@ class PulseAudioDevice : public IAudioDevice // PulseAudioDevice cannot be created directly, only via PulseAudioEngine. friend class PulseAudioEngine; - PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, std::string devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); + PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels); private: pa_context* context_; pa_threaded_mainloop* mainloop_; pa_stream* stream_; - std::string devName_; + wxString devName_; IAudioEngine::AudioDirection direction_; int sampleRate_; int numChannels_; From cb92b39e2d9b6a0f918e49ca52cfbfb88f410d46 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 2 Jan 2022 10:44:11 -0800 Subject: [PATCH 59/89] Stop trimming sound device names. --- src/audio/PortAudioEngine.cpp | 6 ++---- src/audio/PulseAudioEngine.cpp | 2 +- src/dlg_audiooptions.cpp | 16 ++++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/audio/PortAudioEngine.cpp b/src/audio/PortAudioEngine.cpp index e75675949..77dc87122 100644 --- a/src/audio/PortAudioEngine.cpp +++ b/src/audio/PortAudioEngine.cpp @@ -100,11 +100,10 @@ std::vector PortAudioEngine::getSupportedSampleRates(wxString deviceName, A { std::vector result; auto devInfo = getAudioDeviceList(direction); - wxString wxDeviceName = deviceName.Trim(); for (auto& device : devInfo) { - if (wxDeviceName == device.name.Trim()) + if (deviceName == device.name) { PaStreamParameters streamParameters; @@ -158,11 +157,10 @@ AudioDeviceSpecification PortAudioEngine::getDefaultAudioDevice(AudioDirection d std::shared_ptr PortAudioEngine::getAudioDevice(wxString deviceName, AudioDirection direction, int sampleRate, int numChannels) { auto deviceList = getAudioDeviceList(direction); - wxString wxDeviceName = deviceName.Trim(); for (auto& dev : deviceList) { - if (dev.name.Trim() == wxDeviceName) + if (dev.name == deviceName) { auto devObj = new PortAudioDevice(dev.deviceId, direction, sampleRate, dev.maxChannels >= numChannels ? numChannels : dev.maxChannels); return std::shared_ptr(devObj); diff --git a/src/audio/PulseAudioEngine.cpp b/src/audio/PulseAudioEngine.cpp index d80ec834c..43f4ed31c 100644 --- a/src/audio/PulseAudioEngine.cpp +++ b/src/audio/PulseAudioEngine.cpp @@ -273,7 +273,7 @@ std::shared_ptr PulseAudioEngine::getAudioDevice(wxString deviceNa for (auto& dev : deviceList) { - if (dev.name.Trim() == deviceName.Trim()) + if (dev.name == deviceName) { auto devObj = new PulseAudioDevice( diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index 6afd45dad..b09e48270 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -364,7 +364,7 @@ bool AudioOptsDialog::setTextCtrlIfDevNameValid(wxTextCtrl *textCtrl, wxListCtrl // ignore last list entry as it is the "none" entry for(int i = 0; i < listCtrl->GetItemCount() - 1; i++) { - if (devName.Trim() == listCtrl->GetItemText(i, 0).Trim()) + if (devName == listCtrl->GetItemText(i, 0)) { textCtrl->SetValue(listCtrl->GetItemText(i, 0)); if (g_verbose) fprintf(stderr,"setting focus of %d\n", i); @@ -567,17 +567,17 @@ int AudioOptsDialog::ExchangeData(int inout) if (valid_one_card_config) { - wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Trim(); - wxGetApp().m_soundCard1OutDeviceName = m_textCtrlRxOut->GetValue().Trim(); + wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue(); + wxGetApp().m_soundCard1OutDeviceName = m_textCtrlRxOut->GetValue(); wxGetApp().m_soundCard2InDeviceName = "none"; wxGetApp().m_soundCard2OutDeviceName = "none"; } else if (valid_two_card_config) { - wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue().Trim(); - wxGetApp().m_soundCard1OutDeviceName = m_textCtrlTxOut->GetValue().Trim(); - wxGetApp().m_soundCard2InDeviceName = m_textCtrlTxIn->GetValue().Trim(); - wxGetApp().m_soundCard2OutDeviceName = m_textCtrlRxOut->GetValue().Trim(); + wxGetApp().m_soundCard1InDeviceName = m_textCtrlRxIn->GetValue(); + wxGetApp().m_soundCard1OutDeviceName = m_textCtrlTxOut->GetValue(); + wxGetApp().m_soundCard2InDeviceName = m_textCtrlTxIn->GetValue(); + wxGetApp().m_soundCard2OutDeviceName = m_textCtrlRxOut->GetValue(); } else { @@ -615,7 +615,7 @@ int AudioOptsDialog::buildListOfSupportedSampleRates(wxComboBox *cbSampleRate, w cbSampleRate->Clear(); for (auto& dev : deviceList) { - if (dev.name.Trim() == devName.Trim()) + if (dev.name == devName) { auto supportedSampleRates = engine->getSupportedSampleRates( From 36c6b737eb31c4b4c786b91eae35f497b3de83db Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 2 Jan 2022 12:03:29 -0800 Subject: [PATCH 60/89] Remove unneeded/interfering wxString/std::string conversions. --- src/dlg_audiooptions.cpp | 12 ++++++------ src/main.cpp | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/dlg_audiooptions.cpp b/src/dlg_audiooptions.cpp index b09e48270..3f8291595 100644 --- a/src/dlg_audiooptions.cpp +++ b/src/dlg_audiooptions.cpp @@ -804,7 +804,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - m_audioPlotThread = new std::thread([&](std::string devNameAsCString, PlotScalar* ps) { + m_audioPlotThread = new std::thread([&](wxString devName, PlotScalar* ps) { std::mutex callbackFifoMutex; std::condition_variable callbackFifoCV; SRC_STATE *src; @@ -818,7 +818,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p auto devList = engine->getAudioDeviceList(IAudioEngine::AUDIO_ENGINE_IN); for (auto& devInfo : devList) { - if (devInfo.name == devNameAsCString) + if (devInfo.name == devName) { int sampleCount = 0; int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); @@ -917,7 +917,7 @@ void AudioOptsDialog::plotDeviceInputForAFewSecs(wxString devName, PlotScalar *p m_btnTxInTest->Enable(true); m_btnTxOutTest->Enable(true); }); - }, std::string(devName.ToUTF8()), ps); + }, devName, ps); } @@ -934,7 +934,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * m_btnTxInTest->Enable(false); m_btnTxOutTest->Enable(false); - m_audioPlotThread = new std::thread([&](std::string devNameAsCString, PlotScalar* ps) { + m_audioPlotThread = new std::thread([&](wxString devName, PlotScalar* ps) { SRC_STATE *src; FIFO *fifo, *callbackFifo; int src_error, n = 0; @@ -946,7 +946,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * auto devList = engine->getAudioDeviceList(IAudioEngine::AUDIO_ENGINE_OUT); for (auto& devInfo : devList) { - if (devInfo.name == devNameAsCString) + if (devInfo.name == devName) { int sampleCount = 0; int sampleRate = wxAtoi(m_cbSampleRateRxIn->GetValue()); @@ -1055,7 +1055,7 @@ void AudioOptsDialog::plotDeviceOutputForAFewSecs(wxString devName, PlotScalar * m_btnTxInTest->Enable(true); m_btnTxOutTest->Enable(true); }); - }, std::string(devName.ToUTF8()), ps); + }, devName, ps); } //------------------------------------------------------------------------- diff --git a/src/main.cpp b/src/main.cpp index 9dd77a95c..6e289b6a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1987,7 +1987,7 @@ void MainFrame::startRxStream() { // RX-only setup. // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. - rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); + rxInSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard1InDeviceName, IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -1995,7 +1995,7 @@ void MainFrame::startRxStream() pConfig->Flush(); }, nullptr); - rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard1OutDeviceName, IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -2040,7 +2040,7 @@ void MainFrame::startRxStream() { // RX + TX setup // Same note as above re: number of channels. - rxInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); + rxInSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard1InDeviceName, IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard1InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -2048,7 +2048,7 @@ void MainFrame::startRxStream() pConfig->Flush(); }, nullptr); - rxOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 2); + rxOutSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard2OutDeviceName, IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard2OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -2056,7 +2056,7 @@ void MainFrame::startRxStream() pConfig->Flush(); }, nullptr); - txInSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); + txInSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard2InDeviceName, IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); txInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard2InDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -2064,7 +2064,7 @@ void MainFrame::startRxStream() pConfig->Flush(); }, nullptr); - txOutSoundDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); + txOutSoundDevice = engine->getAudioDevice(wxGetApp().m_soundCard1OutDeviceName, IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 2); txOutSoundDevice->setDescription("FreeDV to Radio"); txOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { wxGetApp().m_soundCard1OutDeviceName = wxString::FromUTF8(newDeviceName.c_str()); @@ -2894,10 +2894,10 @@ bool MainFrame::validateSoundCardSetup() engine->start(); // For the purposes of validation, number of channels isn't necessary. - auto soundCard1InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 1); - auto soundCard1OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard1OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 1); - auto soundCard2InDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2InDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 1); - auto soundCard2OutDevice = engine->getAudioDevice(std::string(wxGetApp().m_soundCard2OutDeviceName.ToUTF8()), IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 1); + auto soundCard1InDevice = engine->getAudioDevice(wxGetApp().m_soundCard1InDeviceName, IAudioEngine::AUDIO_ENGINE_IN, g_soundCard1SampleRate, 1); + auto soundCard1OutDevice = engine->getAudioDevice(wxGetApp().m_soundCard1OutDeviceName, IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard1SampleRate, 1); + auto soundCard2InDevice = engine->getAudioDevice(wxGetApp().m_soundCard2InDeviceName, IAudioEngine::AUDIO_ENGINE_IN, g_soundCard2SampleRate, 1); + auto soundCard2OutDevice = engine->getAudioDevice(wxGetApp().m_soundCard2OutDeviceName, IAudioEngine::AUDIO_ENGINE_OUT, g_soundCard2SampleRate, 1); if (wxGetApp().m_soundCard1InDeviceName != "none" && !soundCard1InDevice) { From d29770955a1e227d2bdfe97732a4e62a474857d9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 2 Jan 2022 13:05:37 -0800 Subject: [PATCH 61/89] Update README to describe PortAudio and PulseAudio variants for Linux. --- README.md | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 42315968c..9328e0b9c 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ This document describes how to build the FreeDV GUI program for various operatin * [FreeDV GUI User Manual](USER_MANUAL.md) * [Building for Windows using Docker](docker/README_docker.md) -## Building on Ubuntu Linux (16-20) +## Building on Ubuntu Linux (16-20) with PortAudio ``` $ sudo apt install libspeexdsp-dev libsamplerate0-dev sox git \ libwxgtk3.0-gtk3-dev portaudio19-dev libhamlib-dev libasound2-dev libao-dev \ - libgsm1-dev libsndfile-dev cmake module-assistant build-essential libpulse-dev + libgsm1-dev libsndfile-dev cmake module-assistant build-essential $ git clone https://github.com/drowe67/freedv-gui.git $ cd freedv-gui - $ ./build_linux.sh + $ ./build_linux.sh portaudio ``` (For Ubuntu 20.04 the wxWidgets package is named `libwxgtk3.0-gtk3-dev`.) @@ -26,21 +26,54 @@ This document describes how to build the FreeDV GUI program for various operatin Note this builds all libraries locally, nothing is installed on your machine. ```make install``` is not required. -## Building on Fedora Linux +## Building on Fedora Linux with PortAudio ``` $ sudo dnf groupinstall "Development Tools" $ sudo dnf install cmake wxGTK3-devel portaudio-devel libsamplerate-devel \ libsndfile-devel speexdsp-devel hamlib-devel alsa-lib-devel libao-devel \ - gsm-devel pulseaudio-libs-devel + gsm-devel $ git clone https://github.com/drowe67/freedv-gui.git $ cd freedv-gui - $ ./build_linux.sh + $ ./build_linux.sh portaudio ``` Then run with: ``` $ ./build_linux/src/freedv ``` +## Building on Ubuntu Linux (16-20) with PulseAudio + ``` + $ sudo apt install libspeexdsp-dev libsamplerate0-dev sox git \ + libwxgtk3.0-gtk3-dev libhamlib-dev libasound2-dev libao-dev \ + libgsm1-dev libsndfile-dev cmake module-assistant build-essential libpulse-dev + $ git clone https://github.com/drowe67/freedv-gui.git + $ cd freedv-gui + $ ./build_linux.sh pulseaudio + ``` + (For Ubuntu 20.04 the wxWidgets package is named `libwxgtk3.0-gtk3-dev`.) + + Then run with: + ``` + $ ./build_linux/src/freedv + ``` + + Note this builds all libraries locally, nothing is installed on your machine. ```make install``` is not required. + +## Building on Fedora Linux with PulseAudio + ``` + $ sudo dnf groupinstall "Development Tools" + $ sudo dnf install cmake wxGTK3-devel libsamplerate-devel \ + libsndfile-devel speexdsp-devel hamlib-devel alsa-lib-devel libao-devel \ + gsm-devel pulseaudio-libs-devel + $ git clone https://github.com/drowe67/freedv-gui.git + $ cd freedv-gui + $ ./build_linux.sh pulseaudio + ``` + Then run with: + ``` + $ ./build_linux/src/freedv + ``` + ## Installing on Linux You need to install the codec2 and lpcnetfreedv shared libraries, and freedv-gui: @@ -130,7 +163,7 @@ Testing FreeDV API: $ play -t .s16 -r 16000 -b 16 out.raw ``` -## Building and installing on OSX +## Building and installing on macOS Please see [README.osx](README.osx). From 147109b674c5c8e463ac42ca45de8c261c25718c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 2 Jan 2022 21:07:57 +0000 Subject: [PATCH 62/89] latest user manual PDF --- USER_MANUAL.pdf | Bin 997426 -> 997695 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/USER_MANUAL.pdf b/USER_MANUAL.pdf index 1c7d73a5cb9129f220ee6abf803ddf3c6cc062b9..4e25aa40b8cc4cf69384964df7665f47e2ea36e7 100644 GIT binary patch delta 32252 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 Q949*?967nDycpd70rpe*1^@s6 delta 31971 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 mY?0gP<6&}iok&djBs?I@olD2yMkIt`=VpZ=Cl^ Date: Thu, 6 Jan 2022 21:51:55 -0800 Subject: [PATCH 63/89] Bump version to 1.7.0. --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index df3101649..7f8f0ec0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,9 +48,9 @@ message(STATUS "Compilation date = XX${DATE_RESULT}XX") # Set FreeDV version and generate src/version.h # set(FREEDV_VERSION_MAJOR 1) -set(FREEDV_VERSION_MINOR 6) -set(FREEDV_VERSION_PATCH 2) -set(FREEDV_VERSION_SUFFIX "pulseaudio-devel") +set(FREEDV_VERSION_MINOR 7) +set(FREEDV_VERSION_PATCH 0) +set(FREEDV_VERSION_SUFFIX "") set(FREEDV_VERSION ${FREEDV_VERSION_MAJOR}.${FREEDV_VERSION_MINOR}) if(FREEDV_VERSION_PATCH) From 4f2667a34f6a9317f7c4c3a2a28060f24ac1b4ad Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 7 Jan 2022 05:54:58 +0000 Subject: [PATCH 64/89] latest user manual PDF --- USER_MANUAL.pdf | Bin 999670 -> 998896 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/USER_MANUAL.pdf b/USER_MANUAL.pdf index abecfcba8f09d426deae48298be6b668ac579fcc..d78123d395c7e4ce469e5345f73408b4933ba70a 100644 GIT binary patch delta 30017 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 OcPzn?lZ(lV!~Gv#k7mRG delta 30798 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 V%NQ3xB7)^+VTGljkWi9@{Xfj&8>RpN From 32c4376f4d41a849bd0fda7220b19a7417ac7bb9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 7 Jan 2022 08:45:59 -0800 Subject: [PATCH 65/89] Always include patch version in version string. --- CMakeLists.txt | 5 +---- src/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f8f0ec0f..7a41928cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,10 +52,7 @@ set(FREEDV_VERSION_MINOR 7) set(FREEDV_VERSION_PATCH 0) set(FREEDV_VERSION_SUFFIX "") -set(FREEDV_VERSION ${FREEDV_VERSION_MAJOR}.${FREEDV_VERSION_MINOR}) -if(FREEDV_VERSION_PATCH) - set(FREEDV_VERSION ${FREEDV_VERSION}.${FREEDV_VERSION_PATCH}) -endif() +set(FREEDV_VERSION ${FREEDV_VERSION_MAJOR}.${FREEDV_VERSION_MINOR}.${FREEDV_VERSION_PATCH}) if(FREEDV_VERSION_SUFFIX) set(FREEDV_VERSION_STRING "${FREEDV_VERSION} ${FREEDV_VERSION_SUFFIX}") else() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3e9fd6876..180292270 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,7 +144,7 @@ if(APPLE) add_custom_command( TARGET FreeDV POST_BUILD - COMMAND rm -rf dist_tmp || true + COMMAND rm -rf dist_tmp FreeDV.dmg || true COMMAND DYLD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src:${LPCNET_BUILD_DIR}/src:${DYLD_LIBRARY_PATH} ${CMAKE_SOURCE_DIR}/macdylibbundler/dylibbundler ARGS -od -b -x FreeDV.app/Contents/MacOS/FreeDV -d FreeDV.app/Contents/libs -p @loader_path/../libs/ -i /usr/lib -s ${LPCNET_BUILD_DIR}/src -s ${CODEC2_BUILD_DIR}/src COMMAND cp ARGS ${CMAKE_CURRENT_SOURCE_DIR}/freedv.icns FreeDV.app/Contents/Resources COMMAND mkdir dist_tmp From ca8a0d60e58328f7dd171e2f649affbc97501176 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 11 Jan 2022 18:36:51 -0800 Subject: [PATCH 66/89] Bring over PA_FPB value from previous implementation instead of using 0. --- src/audio/PortAudioDevice.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 9fbfd06ec..ee6e394f8 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -24,6 +24,10 @@ #include "PortAudioDevice.h" #include "portaudio.h" +// 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 + PortAudioDevice::PortAudioDevice(int deviceId, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : deviceId_(deviceId) , direction_(direction) @@ -57,7 +61,7 @@ void PortAudioDevice::start() direction_ == IAudioEngine::AUDIO_ENGINE_IN ? &streamParameters : nullptr, direction_ == IAudioEngine::AUDIO_ENGINE_OUT ? &streamParameters : nullptr, sampleRate_, - 0, + PA_FPB, paClipOff, &OnPortAudioStreamCallback_, this From d0b6ea7eb2c7d67da7ad9297dea26609360218cb Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Tue, 11 Jan 2022 18:37:22 -0800 Subject: [PATCH 67/89] Suppress spurious timeout messages during start/stop. --- src/main.cpp | 86 ++++++++++++++++++++++++---------------------------- src/main.h | 14 ++------- 2 files changed, 42 insertions(+), 58 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 6e289b6a1..1fa137dca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1877,35 +1877,24 @@ void MainFrame::stopRxStream() { m_RxRunning = false; - if (rxInSoundDevice) - { - rxInSoundDevice->stop(); - rxInSoundDevice.reset(); - } - - if (rxOutSoundDevice) - { - rxOutSoundDevice->stop(); - rxOutSoundDevice.reset(); - } - - if (txInSoundDevice) - { - txInSoundDevice->stop(); - txInSoundDevice.reset(); - } - - if (txOutSoundDevice) - { - txOutSoundDevice->stop(); - txOutSoundDevice.reset(); - } - //fprintf(stderr, "waiting for thread to stop\n"); if (m_txThread) { m_txThread->terminateThread(); m_txThread->Wait(); + + if (txInSoundDevice) + { + txInSoundDevice->stop(); + txInSoundDevice.reset(); + } + + if (txOutSoundDevice) + { + txOutSoundDevice->stop(); + txOutSoundDevice.reset(); + } + delete m_txThread; m_txThread = nullptr; } @@ -1914,6 +1903,19 @@ void MainFrame::stopRxStream() { m_rxThread->terminateThread(); m_rxThread->Wait(); + + if (rxInSoundDevice) + { + rxInSoundDevice->stop(); + rxInSoundDevice.reset(); + } + + if (rxOutSoundDevice) + { + rxOutSoundDevice->stop(); + rxOutSoundDevice.reset(); + } + delete m_txThread; m_rxThread = nullptr; } @@ -2269,13 +2271,11 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - size_t toRead = codec2_fifo_used(cbData->outfifo2); - toRead = toRead >= size ? size : toRead; - int result = codec2_fifo_read(cbData->outfifo2, outdata, toRead); + int result = codec2_fifo_read(cbData->outfifo2, outdata, size); if (result == 0) { - for (size_t i = 0; i < toRead; i++) + for (size_t i = 0; i < size; i++) { for (int j = 0; j < dev.getNumChannels(); j++) { @@ -2334,10 +2334,8 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - size_t toRead = codec2_fifo_used(cbData->outfifo1); - toRead = toRead >= size ? size : toRead; - int result = codec2_fifo_read(cbData->outfifo1, outdata, toRead); + int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { // write signal to both channels if the device can support two channels. @@ -2345,7 +2343,7 @@ void MainFrame::startRxStream() // only to that channel. if (dev.getNumChannels() == 2) { - for(size_t i = 0; i < toRead; i++, audioData += 2) + for(size_t i = 0; i < size; i++, audioData += 2) { if (cbData->leftChannelVoxTone) { @@ -2361,7 +2359,7 @@ void MainFrame::startRxStream() } else { - for(size_t i = 0; i < toRead; i++, audioData++) + for(size_t i = 0; i < size; i++, audioData++) { audioData[0] = outdata[i]; } @@ -2392,13 +2390,11 @@ void MainFrame::startRxStream() paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; - size_t toRead = codec2_fifo_used(cbData->outfifo1); - toRead = toRead >= size ? size : toRead; - int result = codec2_fifo_read(cbData->outfifo1, outdata, toRead); + int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { - for (size_t i = 0; i < toRead; i++) + for (size_t i = 0; i < size; i++) { for (int j = 0; j < dev.getNumChannels(); j++) { @@ -2434,6 +2430,10 @@ void MainFrame::startRxStream() if (wxGetApp().m_txRxThreadHighPriority) { m_txThread->SetPriority(WXTHREAD_MAX_PRIORITY); } + + txInSoundDevice->start(); + txOutSoundDevice->start(); + if ( m_txThread->Run() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't start TX thread!")); @@ -2450,18 +2450,12 @@ void MainFrame::startRxStream() m_rxThread->SetPriority(WXTHREAD_MAX_PRIORITY); } - if ( m_rxThread->Run() != wxTHREAD_NO_ERROR ) - { - wxLogError(wxT("Can't start RX thread!")); - } - - // Start sound devices rxInSoundDevice->start(); rxOutSoundDevice->start(); - if (txInSoundDevice && txOutSoundDevice) + + if ( m_rxThread->Run() != wxTHREAD_NO_ERROR ) { - txInSoundDevice->start(); - txOutSoundDevice->start(); + wxLogError(wxT("Can't start RX thread!")); } if (g_verbose) fprintf(stderr, "starting tx/rx processing thread\n"); diff --git a/src/main.h b/src/main.h index bf939fbb5..c5e227575 100644 --- a/src/main.h +++ b/src/main.h @@ -668,24 +668,14 @@ class txRxThread : public wxThread // thread execution starts here void *Entry() { - bool suppress_time_out = false; - while (true) + while (m_run) { { std::unique_lock lk(m_processingMutex); if (m_processingCondVar.wait_for(lk, std::chrono::milliseconds(100)) == std::cv_status::timeout) { - if (!suppress_time_out) - { - fprintf(stderr, "txRxThread: timeout while waiting for CV, tx = %d\n", m_tx); - } - suppress_time_out = true; + fprintf(stderr, "txRxThread: timeout while waiting for CV, tx = %d\n", m_tx); } - else - { - suppress_time_out = false; - } - if (!m_run) break; } if (m_tx) txProcessing(); else rxProcessing(); From ff88cf150518287b057dff67eebc943367df60a6 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 12 Jan 2022 18:45:03 -0800 Subject: [PATCH 68/89] Create data collection thread so that PulseAudioEngine's behavior is as close to PortAudio's as possible. --- src/audio/PulseAudioDevice.cpp | 152 ++++++++++++++++++++++++++++++--- src/audio/PulseAudioDevice.h | 8 +- src/main.cpp | 2 +- 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 363aa87dd..d2e2d6b61 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -25,10 +25,17 @@ #include "PulseAudioDevice.h" +// Optimal settings based on ones used for PortAudio. +#define PULSE_FPB 256 +#define PULSE_TARGET_LATENCY_US 20000 + PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) , mainloop_(mainloop) , stream_(nullptr) + , outputPending_(nullptr) + , outputPendingLength_(0) + , outputPendingThreadActive_(false) , devName_(devName) , direction_(direction) , sampleRate_(sampleRate) @@ -73,7 +80,7 @@ void PulseAudioDevice::start() // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; buffer_attr.maxlength = (uint32_t)-1; - buffer_attr.tlength = pa_usec_to_bytes(20000, &sample_specification); + buffer_attr.tlength = pa_usec_to_bytes(PULSE_TARGET_LATENCY_US, &sample_specification); buffer_attr.prebuf = 0; // Ensure that we can recover during an underrun buffer_attr.minreq = (uint32_t) -1; buffer_attr.fragsize = buffer_attr.tlength; @@ -107,7 +114,96 @@ void PulseAudioDevice::start() onAudioErrorFunction(*this, std::string("Could not connect PulseAudio stream to ") + (const char*)devName_.ToUTF8(), onAudioErrorState); } } - + else + { + // Start data collection thread. This thread + // is necessary in order to ensure that we can + // provide data to PulseAudio at a rate expected + // for the actual latency of the sound device. + outputPending_ = nullptr; + outputPendingLength_ = 0; + outputPendingThreadActive_ = true; + if (direction_ == IAudioEngine::AUDIO_ENGINE_OUT) + { + outputPendingThread_ = new std::thread([&]() { + while(outputPendingThreadActive_) + { + short data[PULSE_FPB * getNumChannels()]; + memset(data, 0, sizeof(data)); + + if (onAudioDataFunction) + { + onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); + } + + { + std::unique_lock lk(outputPendingMutex_); + short* temp = new short[outputPendingLength_ + PULSE_FPB * getNumChannels()]; + assert(temp != nullptr); + + if (outputPendingLength_ > 0) + { + memcpy(temp, outputPending_, outputPendingLength_ * sizeof(short)); + + delete[] outputPending_; + outputPending_ = nullptr; + } + memcpy(temp + outputPendingLength_, data, sizeof(data)); + + outputPending_ = temp; + outputPendingLength_ += PULSE_FPB * getNumChannels(); + } + + // Sleep the required amount of time to ensure we call onAudioDataFunction + // every PULSE_FPB samples. + int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; + std::this_thread::sleep_for( + std::chrono::milliseconds(sleepTimeMilliseconds)); + } + }); + } + else + { + outputPendingThread_ = new std::thread([&]() { + while(outputPendingThreadActive_) + { + short data[PULSE_FPB * getNumChannels()]; + memset(data, 0, sizeof(data)); + + { + std::unique_lock lk(outputPendingMutex_); + int newLength = outputPendingLength_ - PULSE_FPB * getNumChannels(); + + if (newLength >= 0) + { + short* temp = new short[newLength]; + assert(temp != nullptr); + + memcpy(temp, outputPending_ + PULSE_FPB * getNumChannels(), newLength * sizeof(short)); + memcpy(data, outputPending_, PULSE_FPB * getNumChannels() * sizeof(short)); + delete[] outputPending_; + outputPending_ = temp; + outputPendingLength_ = newLength; + } + } + + if (onAudioDataFunction) + { + onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); + } + + // Sleep the required amount of time to ensure we call onAudioDataFunction + // every PULSE_FPB samples. + int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; + std::this_thread::sleep_for( + std::chrono::milliseconds(sleepTimeMilliseconds)); + } + }); + } + + assert(outputPendingThread_ != nullptr); + } + pa_threaded_mainloop_unlock(mainloop_); } @@ -126,6 +222,16 @@ void PulseAudioDevice::stop() pa_stream_unref(stream_); stream_ = nullptr; + + outputPendingThreadActive_ = false; + outputPendingThread_->join(); + + delete[] outputPending_; + outputPending_ = nullptr; + outputPendingLength_ = 0; + + delete outputPendingThread_; + outputPendingThread_ = nullptr; } } @@ -142,9 +248,22 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us break; } - if (thisObj->onAudioDataFunction) { - thisObj->onAudioDataFunction(*thisObj, const_cast(data), length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); + std::unique_lock lk(thisObj->outputPendingMutex_); + short* temp = new short[thisObj->outputPendingLength_ + length / sizeof(short)]; + assert(temp != nullptr); + + if (thisObj->outputPendingLength_ > 0) + { + memcpy(temp, thisObj->outputPending_, thisObj->outputPendingLength_ * sizeof(short)); + + delete[] thisObj->outputPending_; + thisObj->outputPending_ = nullptr; + } + memcpy(temp + thisObj->outputPendingLength_, data, length); + + thisObj->outputPending_ = temp; + thisObj->outputPendingLength_ += length / sizeof(short); } pa_stream_drop(s); @@ -155,16 +274,29 @@ void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *u { if (length > 0) { - short data[length]; - memset(data, 0, sizeof(short) * length); + // Note that PulseAudio gives us lengths in terms of number of bytes, not samples. + int numSamples = length / sizeof(short); + short data[numSamples]; + memset(data, 0, sizeof(data)); PulseAudioDevice* thisObj = static_cast(userdata); - - if (thisObj->onAudioDataFunction) { - thisObj->onAudioDataFunction(*thisObj, data, length / (sizeof(short) * thisObj->getNumChannels()), thisObj->onAudioDataState); + std::unique_lock lk(thisObj->outputPendingMutex_); + if (thisObj->outputPendingLength_ >= numSamples) + { + memcpy(data, thisObj->outputPending_, sizeof(data)); + + short* tmp = new short[thisObj->outputPendingLength_ - numSamples]; + assert(tmp != nullptr); + + thisObj->outputPendingLength_ -= numSamples; + memcpy(tmp, thisObj->outputPending_ + numSamples, sizeof(short) * thisObj->outputPendingLength_); + + delete[] thisObj->outputPending_; + thisObj->outputPending_ = tmp; + } } - + pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); } } diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 1d6a97922..ab78b4328 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -24,6 +24,7 @@ #define PULSE_AUDIO_DEVICE_H #include +#include #include #include #include @@ -50,7 +51,12 @@ class PulseAudioDevice : public IAudioDevice pa_context* context_; pa_threaded_mainloop* mainloop_; pa_stream* stream_; - + short* outputPending_; + int outputPendingLength_; + bool outputPendingThreadActive_; + std::mutex outputPendingMutex_; + std::thread* outputPendingThread_; + wxString devName_; IAudioEngine::AudioDirection direction_; int sampleRate_; diff --git a/src/main.cpp b/src/main.cpp index 1fa137dca..5d1efec71 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2404,7 +2404,7 @@ void MainFrame::startRxStream() } else { - g_outfifo2_empty++; + g_outfifo1_empty++; } }, g_rxUserdata); From 9d24a41aa0ea937a17ff700b27588de6443b6335 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 12 Jan 2022 18:59:09 -0800 Subject: [PATCH 69/89] Additional tuning to make PipeWire work correctly. --- src/audio/PulseAudioDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index d2e2d6b61..66c5e13a6 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 20000 +#define PULSE_TARGET_LATENCY_US 130000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) From b396254b8fcde4c2eac3d9a750cfd33b7d2624c7 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 12 Jan 2022 19:31:41 -0800 Subject: [PATCH 70/89] Cap pending data to the target latency. --- src/audio/PulseAudioDevice.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 66c5e13a6..9c885750b 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -152,6 +152,20 @@ void PulseAudioDevice::start() outputPending_ = temp; outputPendingLength_ += PULSE_FPB * getNumChannels(); + + // Trim any extra so our latency doesn't get crazy. + int targetLength = getNumChannels() * ((double)PULSE_TARGET_LATENCY_US / (double)1000000) * sampleRate_; + if (outputPendingLength_ >= targetLength) + { + temp = new short[targetLength]; + assert(temp != nullptr); + + memcpy(temp, outputPending_ + (outputPendingLength_ - targetLength), targetLength * sizeof(short)); + + delete[] outputPending_; + outputPending_ = temp; + outputPendingLength_ = targetLength; + } } // Sleep the required amount of time to ensure we call onAudioDataFunction @@ -264,6 +278,20 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us thisObj->outputPending_ = temp; thisObj->outputPendingLength_ += length / sizeof(short); + + // Trim any extra so our latency doesn't get crazy. + int targetLength = thisObj->getNumChannels() * ((double)PULSE_TARGET_LATENCY_US / (double)1000000) * thisObj->sampleRate_; + if (thisObj->outputPendingLength_ >= targetLength) + { + temp = new short[targetLength]; + assert(temp != nullptr); + + memcpy(temp, thisObj->outputPending_ + (thisObj->outputPendingLength_ - targetLength), targetLength * sizeof(short)); + + delete[] thisObj->outputPending_; + thisObj->outputPending_ = temp; + thisObj->outputPendingLength_ = targetLength; + } } pa_stream_drop(s); From 7c9c6dd863521f688023e836785c6649df50e163 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 12 Jan 2022 22:09:53 -0800 Subject: [PATCH 71/89] Auto-adjust based on reported latency. --- src/audio/PulseAudioDevice.cpp | 21 ++++++++++++++++++--- src/audio/PulseAudioDevice.h | 2 ++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 9c885750b..e8b60f437 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 130000 +#define PULSE_TARGET_LATENCY_US 20000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) @@ -55,6 +55,8 @@ PulseAudioDevice::~PulseAudioDevice() void PulseAudioDevice::start() { + streamLatency_ = PULSE_TARGET_LATENCY_US; + pa_sample_spec sample_specification; sample_specification.format = PA_SAMPLE_S16LE; sample_specification.rate = sampleRate_; @@ -76,6 +78,7 @@ void PulseAudioDevice::start() pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); pa_stream_set_state_callback(stream_, &PulseAudioDevice::StreamStateCallback_, this); + pa_stream_set_latency_update_callback(stream_, &PulseAudioDevice::StreamLatencyCallback_, this); // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; @@ -154,7 +157,7 @@ void PulseAudioDevice::start() outputPendingLength_ += PULSE_FPB * getNumChannels(); // Trim any extra so our latency doesn't get crazy. - int targetLength = getNumChannels() * ((double)PULSE_TARGET_LATENCY_US / (double)1000000) * sampleRate_; + int targetLength = 2 * getNumChannels() * ((double)streamLatency_ / (double)1000000) * sampleRate_; if (outputPendingLength_ >= targetLength) { temp = new short[targetLength]; @@ -280,7 +283,7 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us thisObj->outputPendingLength_ += length / sizeof(short); // Trim any extra so our latency doesn't get crazy. - int targetLength = thisObj->getNumChannels() * ((double)PULSE_TARGET_LATENCY_US / (double)1000000) * thisObj->sampleRate_; + int targetLength = 2 * thisObj->getNumChannels() * ((double)thisObj->streamLatency_ / (double)1000000) * thisObj->sampleRate_; if (thisObj->outputPendingLength_ >= targetLength) { temp = new short[targetLength]; @@ -374,3 +377,15 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) thisObj->onAudioDeviceChangedFunction(*thisObj, (const char*)thisObj->devName_.ToUTF8(), thisObj->onAudioOverflowState); } } + +void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) +{ + PulseAudioDevice* thisObj = static_cast(userdata); + pa_usec_t latency; + int isNeg; + + pa_stream_get_latency(p, &latency, &isNeg); + + thisObj->streamLatency_ = std::max(latency, (pa_usec_t)PULSE_TARGET_LATENCY_US); +} + diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index ab78b4328..ea2cdd636 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -56,6 +56,7 @@ class PulseAudioDevice : public IAudioDevice bool outputPendingThreadActive_; std::mutex outputPendingMutex_; std::thread* outputPendingThread_; + int streamLatency_; wxString devName_; IAudioEngine::AudioDirection direction_; @@ -70,6 +71,7 @@ class PulseAudioDevice : public IAudioDevice static void StreamOverflowCallback_(pa_stream *p, void *userdata); static void StreamMovedCallback_(pa_stream *p, void *userdata); static void StreamStateCallback_(pa_stream *p, void *userdata); + static void StreamLatencyCallback_(pa_stream *p, void *userdata); }; #endif // PULSE_AUDIO_DEVICE_H From 4261636e968e0e17e869e9722d613d7f356cda0c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Wed, 12 Jan 2022 22:21:03 -0800 Subject: [PATCH 72/89] Ensure that latency never decreases. --- src/audio/PulseAudioDevice.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index e8b60f437..2aa54e6ee 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 20000 +#define PULSE_TARGET_LATENCY_US 130000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) @@ -386,6 +386,6 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) pa_stream_get_latency(p, &latency, &isNeg); - thisObj->streamLatency_ = std::max(latency, (pa_usec_t)PULSE_TARGET_LATENCY_US); + thisObj->streamLatency_ = std::max((pa_usec_t)thisObj->streamLatency_, (pa_usec_t)PULSE_TARGET_LATENCY_US); } From 20e3004e0fc11063e7cf66577fa5c1c5af24faf7 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 13 Jan 2022 00:22:01 -0800 Subject: [PATCH 73/89] Fix Pulse RX decode issues by not feeding all zeroes if there's no data available. --- src/audio/PulseAudioDevice.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 2aa54e6ee..cc31b9e05 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -131,6 +131,8 @@ void PulseAudioDevice::start() outputPendingThread_ = new std::thread([&]() { while(outputPendingThreadActive_) { + auto currentTime = std::chrono::steady_clock::now(); + short data[PULSE_FPB * getNumChannels()]; memset(data, 0, sizeof(data)); @@ -174,7 +176,7 @@ void PulseAudioDevice::start() // Sleep the required amount of time to ensure we call onAudioDataFunction // every PULSE_FPB samples. int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - std::this_thread::sleep_for( + std::this_thread::sleep_until(currentTime + std::chrono::milliseconds(sleepTimeMilliseconds)); } }); @@ -184,6 +186,8 @@ void PulseAudioDevice::start() outputPendingThread_ = new std::thread([&]() { while(outputPendingThreadActive_) { + auto currentTime = std::chrono::steady_clock::now(); + bool dataAvailable = false; short data[PULSE_FPB * getNumChannels()]; memset(data, 0, sizeof(data)); @@ -201,10 +205,11 @@ void PulseAudioDevice::start() delete[] outputPending_; outputPending_ = temp; outputPendingLength_ = newLength; + dataAvailable = true; } } - if (onAudioDataFunction) + if (dataAvailable && onAudioDataFunction) { onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); } @@ -212,7 +217,7 @@ void PulseAudioDevice::start() // Sleep the required amount of time to ensure we call onAudioDataFunction // every PULSE_FPB samples. int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - std::this_thread::sleep_for( + std::this_thread::sleep_until(currentTime + std::chrono::milliseconds(sleepTimeMilliseconds)); } }); From 36c62fbe6bced6ca3d771cc0bc6661ceded4c9e5 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 13 Jan 2022 01:04:49 -0800 Subject: [PATCH 74/89] Don't start TX audio until after PTT's triggered. --- src/ongui.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ongui.cpp b/src/ongui.cpp index 3bdc39961..6b2591ccb 100644 --- a/src/ongui.cpp +++ b/src/ongui.cpp @@ -414,7 +414,7 @@ void MainFrame::togglePTT(void) { m_togBtnOnOff->Enable(false); } - g_tx = m_btnTogPTT->GetValue(); + bool newTxValue = m_btnTogPTT->GetValue(); // Hamlib PTT @@ -423,7 +423,7 @@ void MainFrame::togglePTT(void) { wxString hamlibError; if (wxGetApp().m_boolHamlibUseForPTT && hamlib != NULL) { // Update mode display on the bottom of the main UI. - if (hamlib->update_frequency_and_mode() != 0 || hamlib->ptt(g_tx, hamlibError) == false) { + if (hamlib->update_frequency_and_mode() != 0 || hamlib->ptt(newTxValue, hamlibError) == false) { wxMessageBox(wxString("Hamlib PTT Error: ") + hamlibError, wxT("Error"), wxOK | wxICON_ERROR, this); } } @@ -432,9 +432,12 @@ void MainFrame::togglePTT(void) { // Serial PTT if (wxGetApp().m_boolUseSerialPTT && (wxGetApp().m_serialport->isopen())) { - wxGetApp().m_serialport->ptt(g_tx); + wxGetApp().m_serialport->ptt(newTxValue); } + // Start routing TX audio out to the radio + g_tx = newTxValue; + // reset level gauge m_maxLevel = 0; From dfbffcbfccb8842b776968b8232138341e25fc5f Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 14 Jan 2022 00:38:02 -0800 Subject: [PATCH 75/89] Improve PulseAudio playback quality. --- src/audio/PulseAudioDevice.cpp | 56 ++++++++++++++++------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index cc31b9e05..b90fa9f0e 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -132,44 +132,40 @@ void PulseAudioDevice::start() while(outputPendingThreadActive_) { auto currentTime = std::chrono::steady_clock::now(); - - short data[PULSE_FPB * getNumChannels()]; - memset(data, 0, sizeof(data)); - - if (onAudioDataFunction) - { - onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); - } + int targetLength = getNumChannels() * ((double)streamLatency_ / (double)1000000) * sampleRate_; + int currentLength = 0; { std::unique_lock lk(outputPendingMutex_); - short* temp = new short[outputPendingLength_ + PULSE_FPB * getNumChannels()]; - assert(temp != nullptr); + currentLength = outputPendingLength_; + } - if (outputPendingLength_ > 0) + if (currentLength < targetLength) + { + short data[PULSE_FPB * getNumChannels()]; + memset(data, 0, sizeof(data)); + + if (onAudioDataFunction) { - memcpy(temp, outputPending_, outputPendingLength_ * sizeof(short)); - - delete[] outputPending_; - outputPending_ = nullptr; + onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); } - memcpy(temp + outputPendingLength_, data, sizeof(data)); - - outputPending_ = temp; - outputPendingLength_ += PULSE_FPB * getNumChannels(); - - // Trim any extra so our latency doesn't get crazy. - int targetLength = 2 * getNumChannels() * ((double)streamLatency_ / (double)1000000) * sampleRate_; - if (outputPendingLength_ >= targetLength) + { - temp = new short[targetLength]; + std::unique_lock lk(outputPendingMutex_); + short* temp = new short[outputPendingLength_ + PULSE_FPB * getNumChannels()]; assert(temp != nullptr); - - memcpy(temp, outputPending_ + (outputPendingLength_ - targetLength), targetLength * sizeof(short)); - - delete[] outputPending_; + + if (outputPendingLength_ > 0) + { + memcpy(temp, outputPending_, outputPendingLength_ * sizeof(short)); + + delete[] outputPending_; + outputPending_ = nullptr; + } + memcpy(temp + outputPendingLength_, data, sizeof(data)); + outputPending_ = temp; - outputPendingLength_ = targetLength; + outputPendingLength_ += PULSE_FPB * getNumChannels(); } } @@ -288,7 +284,7 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us thisObj->outputPendingLength_ += length / sizeof(short); // Trim any extra so our latency doesn't get crazy. - int targetLength = 2 * thisObj->getNumChannels() * ((double)thisObj->streamLatency_ / (double)1000000) * thisObj->sampleRate_; + int targetLength = thisObj->getNumChannels() * ((double)thisObj->streamLatency_ / (double)1000000) * thisObj->sampleRate_; if (thisObj->outputPendingLength_ >= targetLength) { temp = new short[targetLength]; From cda97628e8f49ce0116bbc20fdd44dae5c3e6e03 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 14 Jan 2022 00:38:19 -0800 Subject: [PATCH 76/89] Revert "Don't start TX audio until after PTT's triggered." This reverts commit 36c62fbe6bced6ca3d771cc0bc6661ceded4c9e5. --- src/ongui.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ongui.cpp b/src/ongui.cpp index 6b2591ccb..3bdc39961 100644 --- a/src/ongui.cpp +++ b/src/ongui.cpp @@ -414,7 +414,7 @@ void MainFrame::togglePTT(void) { m_togBtnOnOff->Enable(false); } - bool newTxValue = m_btnTogPTT->GetValue(); + g_tx = m_btnTogPTT->GetValue(); // Hamlib PTT @@ -423,7 +423,7 @@ void MainFrame::togglePTT(void) { wxString hamlibError; if (wxGetApp().m_boolHamlibUseForPTT && hamlib != NULL) { // Update mode display on the bottom of the main UI. - if (hamlib->update_frequency_and_mode() != 0 || hamlib->ptt(newTxValue, hamlibError) == false) { + if (hamlib->update_frequency_and_mode() != 0 || hamlib->ptt(g_tx, hamlibError) == false) { wxMessageBox(wxString("Hamlib PTT Error: ") + hamlibError, wxT("Error"), wxOK | wxICON_ERROR, this); } } @@ -432,12 +432,9 @@ void MainFrame::togglePTT(void) { // Serial PTT if (wxGetApp().m_boolUseSerialPTT && (wxGetApp().m_serialport->isopen())) { - wxGetApp().m_serialport->ptt(newTxValue); + wxGetApp().m_serialport->ptt(g_tx); } - // Start routing TX audio out to the radio - g_tx = newTxValue; - // reset level gauge m_maxLevel = 0; From fd8a8df244f7f751a7072df558b1d7905302ebff Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 14 Jan 2022 00:51:39 -0800 Subject: [PATCH 77/89] Clear second audio channel's memory if no data available. --- src/audio/PortAudioDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index ee6e394f8..911128bbc 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -139,7 +139,7 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, if (thisObj->direction_ == IAudioEngine::AUDIO_ENGINE_OUT) { // Zero out samples by default in case we don't have any data available. - memset(dataPtr, 0, sizeof(short) * frameCount); + memset(dataPtr, 0, sizeof(short) * getNumChannels() * frameCount); } if (thisObj->onAudioDataFunction) From c293da012c97a45f49d1b56ee32899c870719123 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 14 Jan 2022 00:53:16 -0800 Subject: [PATCH 78/89] Oops, fix compile error. --- src/audio/PortAudioDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio/PortAudioDevice.cpp b/src/audio/PortAudioDevice.cpp index 911128bbc..a1c5c2ebf 100644 --- a/src/audio/PortAudioDevice.cpp +++ b/src/audio/PortAudioDevice.cpp @@ -139,7 +139,7 @@ int PortAudioDevice::OnPortAudioStreamCallback_(const void *input, void *output, if (thisObj->direction_ == IAudioEngine::AUDIO_ENGINE_OUT) { // Zero out samples by default in case we don't have any data available. - memset(dataPtr, 0, sizeof(short) * getNumChannels() * frameCount); + memset(dataPtr, 0, sizeof(short) * thisObj->getNumChannels() * frameCount); } if (thisObj->onAudioDataFunction) From 5140d08249d724879b0ebc0c8dacdda0416755a3 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 16 Jan 2022 11:44:14 -0800 Subject: [PATCH 79/89] Experiment: see if we can reduce target latency. --- src/audio/PulseAudioDevice.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index b90fa9f0e..e4a5dbce3 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 130000 +#define PULSE_TARGET_LATENCY_US 20000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) @@ -387,6 +387,7 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) pa_stream_get_latency(p, &latency, &isNeg); - thisObj->streamLatency_ = std::max((pa_usec_t)thisObj->streamLatency_, (pa_usec_t)PULSE_TARGET_LATENCY_US); + thisObj->streamLatency_ = (0.9 * ((double)thisObj->streamLatency_)) + (0.1 * (double)latency); + fprintf(stderr, "%s latency: %d\n", (const char*)thisObj->devName_.ToUTF8(), thisObj->streamLatency_); } From 3da197745fa9b096ff165601b753a648c4bc9300 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sun, 16 Jan 2022 12:21:24 -0800 Subject: [PATCH 80/89] Revert "Experiment: see if we can reduce target latency." This reverts commit 5140d08249d724879b0ebc0c8dacdda0416755a3. --- src/audio/PulseAudioDevice.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index e4a5dbce3..b90fa9f0e 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 20000 +#define PULSE_TARGET_LATENCY_US 130000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) @@ -387,7 +387,6 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) pa_stream_get_latency(p, &latency, &isNeg); - thisObj->streamLatency_ = (0.9 * ((double)thisObj->streamLatency_)) + (0.1 * (double)latency); - fprintf(stderr, "%s latency: %d\n", (const char*)thisObj->devName_.ToUTF8(), thisObj->streamLatency_); + thisObj->streamLatency_ = std::max((pa_usec_t)thisObj->streamLatency_, (pa_usec_t)PULSE_TARGET_LATENCY_US); } From 1398f271eb84f5a85ee40fdab6903de5bd249af2 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 20 Jan 2022 01:52:54 -0800 Subject: [PATCH 81/89] Replace fixed sleeps with condition variables for PulseAudio audio input. --- src/audio/PulseAudioDevice.cpp | 21 ++++++++++++--------- src/audio/PulseAudioDevice.h | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index b90fa9f0e..53d857005 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -182,13 +182,12 @@ void PulseAudioDevice::start() outputPendingThread_ = new std::thread([&]() { while(outputPendingThreadActive_) { - auto currentTime = std::chrono::steady_clock::now(); - bool dataAvailable = false; short data[PULSE_FPB * getNumChannels()]; memset(data, 0, sizeof(data)); { std::unique_lock lk(outputPendingMutex_); + int newLength = outputPendingLength_ - PULSE_FPB * getNumChannels(); if (newLength >= 0) @@ -201,20 +200,22 @@ void PulseAudioDevice::start() delete[] outputPending_; outputPending_ = temp; outputPendingLength_ = newLength; - dataAvailable = true; } } - if (dataAvailable && onAudioDataFunction) + if (onAudioDataFunction) { onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); } - // Sleep the required amount of time to ensure we call onAudioDataFunction - // every PULSE_FPB samples. - int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - std::this_thread::sleep_until(currentTime + - std::chrono::milliseconds(sleepTimeMilliseconds)); + { + std::unique_lock lk(outputPendingMutex_); + + // Sleep the required amount of time to ensure we call onAudioDataFunction + // every PULSE_FPB samples. + int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; + outputPendingCV_.wait_for(lk, std::chrono::milliseconds(sleepTimeMilliseconds)); + } } }); } @@ -296,6 +297,8 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us thisObj->outputPending_ = temp; thisObj->outputPendingLength_ = targetLength; } + + thisObj->outputPendingCV_.notify_all(); } pa_stream_drop(s); diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index ea2cdd636..02d41dd2d 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -56,6 +56,7 @@ class PulseAudioDevice : public IAudioDevice bool outputPendingThreadActive_; std::mutex outputPendingMutex_; std::thread* outputPendingThread_; + std::condition_variable outputPendingCV_; int streamLatency_; wxString devName_; From 62ddc1bb79ad4c6ab7e65cc4dcde248a293a946c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 20 Jan 2022 02:52:15 -0800 Subject: [PATCH 82/89] Revert "Replace fixed sleeps with condition variables for PulseAudio audio input." This reverts commit 1398f271eb84f5a85ee40fdab6903de5bd249af2. --- src/audio/PulseAudioDevice.cpp | 21 +++++++++------------ src/audio/PulseAudioDevice.h | 1 - 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 53d857005..b90fa9f0e 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -182,12 +182,13 @@ void PulseAudioDevice::start() outputPendingThread_ = new std::thread([&]() { while(outputPendingThreadActive_) { + auto currentTime = std::chrono::steady_clock::now(); + bool dataAvailable = false; short data[PULSE_FPB * getNumChannels()]; memset(data, 0, sizeof(data)); { std::unique_lock lk(outputPendingMutex_); - int newLength = outputPendingLength_ - PULSE_FPB * getNumChannels(); if (newLength >= 0) @@ -200,22 +201,20 @@ void PulseAudioDevice::start() delete[] outputPending_; outputPending_ = temp; outputPendingLength_ = newLength; + dataAvailable = true; } } - if (onAudioDataFunction) + if (dataAvailable && onAudioDataFunction) { onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); } - { - std::unique_lock lk(outputPendingMutex_); - - // Sleep the required amount of time to ensure we call onAudioDataFunction - // every PULSE_FPB samples. - int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - outputPendingCV_.wait_for(lk, std::chrono::milliseconds(sleepTimeMilliseconds)); - } + // Sleep the required amount of time to ensure we call onAudioDataFunction + // every PULSE_FPB samples. + int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; + std::this_thread::sleep_until(currentTime + + std::chrono::milliseconds(sleepTimeMilliseconds)); } }); } @@ -297,8 +296,6 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us thisObj->outputPending_ = temp; thisObj->outputPendingLength_ = targetLength; } - - thisObj->outputPendingCV_.notify_all(); } pa_stream_drop(s); diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 02d41dd2d..ea2cdd636 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -56,7 +56,6 @@ class PulseAudioDevice : public IAudioDevice bool outputPendingThreadActive_; std::mutex outputPendingMutex_; std::thread* outputPendingThread_; - std::condition_variable outputPendingCV_; int streamLatency_; wxString devName_; From e1cf4ac0804dc014607eea8c2e2a9b7f0905350c Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 20 Jan 2022 23:26:40 -0800 Subject: [PATCH 83/89] Refactor PulseAudio sample handling logic. --- src/audio/PulseAudioDevice.cpp | 107 +++++++-------------------------- src/audio/PulseAudioDevice.h | 4 +- 2 files changed, 26 insertions(+), 85 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index b90fa9f0e..9788162b5 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -27,7 +27,7 @@ // Optimal settings based on ones used for PortAudio. #define PULSE_FPB 256 -#define PULSE_TARGET_LATENCY_US 130000 +#define PULSE_TARGET_LATENCY_US 20000 PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* context, wxString devName, IAudioEngine::AudioDirection direction, int sampleRate, int numChannels) : context_(context) @@ -36,6 +36,8 @@ PulseAudioDevice::PulseAudioDevice(pa_threaded_mainloop *mainloop, pa_context* c , outputPending_(nullptr) , outputPendingLength_(0) , outputPendingThreadActive_(false) + , outputPendingThread_(nullptr) + , targetOutputPendingLength_(PULSE_FPB * numChannels * 2) , devName_(devName) , direction_(direction) , sampleRate_(sampleRate) @@ -55,8 +57,6 @@ PulseAudioDevice::~PulseAudioDevice() void PulseAudioDevice::start() { - streamLatency_ = PULSE_TARGET_LATENCY_US; - pa_sample_spec sample_specification; sample_specification.format = PA_SAMPLE_S16LE; sample_specification.rate = sampleRate_; @@ -78,7 +78,9 @@ void PulseAudioDevice::start() pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); pa_stream_set_state_callback(stream_, &PulseAudioDevice::StreamStateCallback_, this); +#if 0 pa_stream_set_latency_update_callback(stream_, &PulseAudioDevice::StreamLatencyCallback_, this); +#endif // 0 // recommended settings, i.e. server uses sensible values pa_buffer_attr buffer_attr; @@ -125,6 +127,7 @@ void PulseAudioDevice::start() // for the actual latency of the sound device. outputPending_ = nullptr; outputPendingLength_ = 0; + targetOutputPendingLength_ = PULSE_FPB * getNumChannels() * 2; outputPendingThreadActive_ = true; if (direction_ == IAudioEngine::AUDIO_ENGINE_OUT) { @@ -132,7 +135,6 @@ void PulseAudioDevice::start() while(outputPendingThreadActive_) { auto currentTime = std::chrono::steady_clock::now(); - int targetLength = getNumChannels() * ((double)streamLatency_ / (double)1000000) * sampleRate_; int currentLength = 0; { @@ -140,7 +142,7 @@ void PulseAudioDevice::start() currentLength = outputPendingLength_; } - if (currentLength < targetLength) + if (currentLength < targetOutputPendingLength_) { short data[PULSE_FPB * getNumChannels()]; memset(data, 0, sizeof(data)); @@ -176,50 +178,8 @@ void PulseAudioDevice::start() std::chrono::milliseconds(sleepTimeMilliseconds)); } }); + assert(outputPendingThread_ != nullptr); } - else - { - outputPendingThread_ = new std::thread([&]() { - while(outputPendingThreadActive_) - { - auto currentTime = std::chrono::steady_clock::now(); - bool dataAvailable = false; - short data[PULSE_FPB * getNumChannels()]; - memset(data, 0, sizeof(data)); - - { - std::unique_lock lk(outputPendingMutex_); - int newLength = outputPendingLength_ - PULSE_FPB * getNumChannels(); - - if (newLength >= 0) - { - short* temp = new short[newLength]; - assert(temp != nullptr); - - memcpy(temp, outputPending_ + PULSE_FPB * getNumChannels(), newLength * sizeof(short)); - memcpy(data, outputPending_, PULSE_FPB * getNumChannels() * sizeof(short)); - delete[] outputPending_; - outputPending_ = temp; - outputPendingLength_ = newLength; - dataAvailable = true; - } - } - - if (dataAvailable && onAudioDataFunction) - { - onAudioDataFunction(*this, data, PULSE_FPB, onAudioDataState); - } - - // Sleep the required amount of time to ensure we call onAudioDataFunction - // every PULSE_FPB samples. - int sleepTimeMilliseconds = ((double)PULSE_FPB)/((double)sampleRate_) * 1000.0; - std::this_thread::sleep_until(currentTime + - std::chrono::milliseconds(sleepTimeMilliseconds)); - } - }); - } - - assert(outputPendingThread_ != nullptr); } pa_threaded_mainloop_unlock(mainloop_); @@ -242,14 +202,17 @@ void PulseAudioDevice::stop() stream_ = nullptr; outputPendingThreadActive_ = false; - outputPendingThread_->join(); + if (outputPendingThread_ != nullptr) + { + outputPendingThread_->join(); - delete[] outputPending_; - outputPending_ = nullptr; - outputPendingLength_ = 0; + delete[] outputPending_; + outputPending_ = nullptr; + outputPendingLength_ = 0; - delete outputPendingThread_; - outputPendingThread_ = nullptr; + delete outputPendingThread_; + outputPendingThread_ = nullptr; + } } } @@ -266,36 +229,9 @@ void PulseAudioDevice::StreamReadCallback_(pa_stream *s, size_t length, void *us break; } + if (thisObj->onAudioDataFunction) { - std::unique_lock lk(thisObj->outputPendingMutex_); - short* temp = new short[thisObj->outputPendingLength_ + length / sizeof(short)]; - assert(temp != nullptr); - - if (thisObj->outputPendingLength_ > 0) - { - memcpy(temp, thisObj->outputPending_, thisObj->outputPendingLength_ * sizeof(short)); - - delete[] thisObj->outputPending_; - thisObj->outputPending_ = nullptr; - } - memcpy(temp + thisObj->outputPendingLength_, data, length); - - thisObj->outputPending_ = temp; - thisObj->outputPendingLength_ += length / sizeof(short); - - // Trim any extra so our latency doesn't get crazy. - int targetLength = thisObj->getNumChannels() * ((double)thisObj->streamLatency_ / (double)1000000) * thisObj->sampleRate_; - if (thisObj->outputPendingLength_ >= targetLength) - { - temp = new short[targetLength]; - assert(temp != nullptr); - - memcpy(temp, thisObj->outputPending_ + (thisObj->outputPendingLength_ - targetLength), targetLength * sizeof(short)); - - delete[] thisObj->outputPending_; - thisObj->outputPending_ = temp; - thisObj->outputPendingLength_ = targetLength; - } + thisObj->onAudioDataFunction(*thisObj, (void*)data, length / thisObj->getNumChannels() / sizeof(short), thisObj->onAudioDataState); } pa_stream_drop(s); @@ -327,6 +263,8 @@ void PulseAudioDevice::StreamWriteCallback_(pa_stream *s, size_t length, void *u delete[] thisObj->outputPending_; thisObj->outputPending_ = tmp; } + + thisObj->targetOutputPendingLength_ = std::max(thisObj->targetOutputPendingLength_, 2 * numSamples); } pa_stream_write(s, &data[0], length, NULL, 0LL, PA_SEEK_RELATIVE); @@ -379,6 +317,7 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) } } +#if 0 void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) { PulseAudioDevice* thisObj = static_cast(userdata); @@ -389,4 +328,4 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) thisObj->streamLatency_ = std::max((pa_usec_t)thisObj->streamLatency_, (pa_usec_t)PULSE_TARGET_LATENCY_US); } - +#endif // 0 diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index ea2cdd636..4e24346a6 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -56,7 +56,7 @@ class PulseAudioDevice : public IAudioDevice bool outputPendingThreadActive_; std::mutex outputPendingMutex_; std::thread* outputPendingThread_; - int streamLatency_; + int targetOutputPendingLength_; wxString devName_; IAudioEngine::AudioDirection direction_; @@ -71,7 +71,9 @@ class PulseAudioDevice : public IAudioDevice static void StreamOverflowCallback_(pa_stream *p, void *userdata); static void StreamMovedCallback_(pa_stream *p, void *userdata); static void StreamStateCallback_(pa_stream *p, void *userdata); +#if 0 static void StreamLatencyCallback_(pa_stream *p, void *userdata); +#endif // 0 }; #endif // PULSE_AUDIO_DEVICE_H From 4b7244b36dec81d958797a2fe28b5a2153021e75 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Thu, 20 Jan 2022 23:40:50 -0800 Subject: [PATCH 84/89] Add debugging output to determine how much buffer PulseAudio is asking for. --- src/audio/PulseAudioDevice.cpp | 6 +++--- src/audio/PulseAudioDevice.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index 9788162b5..e63f2d733 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -78,7 +78,7 @@ void PulseAudioDevice::start() pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); pa_stream_set_state_callback(stream_, &PulseAudioDevice::StreamStateCallback_, this); -#if 0 +#if 1 pa_stream_set_latency_update_callback(stream_, &PulseAudioDevice::StreamLatencyCallback_, this); #endif // 0 @@ -317,7 +317,7 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) } } -#if 0 +#if 1 void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) { PulseAudioDevice* thisObj = static_cast(userdata); @@ -326,6 +326,6 @@ void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) pa_stream_get_latency(p, &latency, &isNeg); - thisObj->streamLatency_ = std::max((pa_usec_t)thisObj->streamLatency_, (pa_usec_t)PULSE_TARGET_LATENCY_US); + fprintf(stderr, "Current target buffer size for %s: %d\n", (const char*)thisObj->devName_.ToUTF8(), thisObj->targetOutputPendingLength_); } #endif // 0 diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 4e24346a6..9167113ca 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -71,7 +71,7 @@ class PulseAudioDevice : public IAudioDevice static void StreamOverflowCallback_(pa_stream *p, void *userdata); static void StreamMovedCallback_(pa_stream *p, void *userdata); static void StreamStateCallback_(pa_stream *p, void *userdata); -#if 0 +#if 1 static void StreamLatencyCallback_(pa_stream *p, void *userdata); #endif // 0 }; From c2a71170d2b9dddd1bc52dc72018b54faca426c9 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Fri, 4 Feb 2022 22:34:02 -0800 Subject: [PATCH 85/89] Warning cleanup. --- src/hamlib.cpp | 2 +- src/sox/formats.c | 1264 +-------------------------------------------- 2 files changed, 8 insertions(+), 1258 deletions(-) diff --git a/src/hamlib.cpp b/src/hamlib.cpp index ea9920f1e..b644cc077 100644 --- a/src/hamlib.cpp +++ b/src/hamlib.cpp @@ -38,8 +38,8 @@ static bool rig_cmp(const struct rig_caps *rig1, const struct rig_caps *rig2); static int build_list(const struct rig_caps *rig, rig_ptr_t); Hamlib::Hamlib() : - m_rig_model(0), m_rig(NULL), + m_rig_model(0), m_modeBox(NULL), m_freqBox(NULL), m_currFreq(0), diff --git a/src/sox/formats.c b/src/sox/formats.c index 3fcf4382b..ac1437296 100644 --- a/src/sox/formats.c +++ b/src/sox/formats.c @@ -45,120 +45,15 @@ #define PIPE_AUTO_DETECT_SIZE 256 /* Only as much as we can rewind a pipe */ #define AUTO_DETECT_SIZE 4096 /* For seekable file, so no restriction */ -static char const * auto_detect_format(sox_format_t * ft, char const * ext) +void sox_format_quit(void) /* Cleanup things. */ { - char data[AUTO_DETECT_SIZE]; - size_t len = lsx_readbuf(ft, data, ft->seekable? sizeof(data) : PIPE_AUTO_DETECT_SIZE); - #define CHECK(type, p2, l2, d2, p1, l1, d1) if (len >= p1 + l1 && \ - !memcmp(data + p1, d1, (size_t)l1) && !memcmp(data + p2, d2, (size_t)l2)) return #type; - CHECK(voc , 0, 0, "" , 0, 20, "Creative Voice File\x1a") - CHECK(smp , 0, 0, "" , 0, 17, "SOUND SAMPLE DATA") - CHECK(wve , 0, 0, "" , 0, 15, "ALawSoundFile**") - CHECK(gsrt , 0, 0, "" , 16, 9, "ring.bin") - CHECK(amr-wb, 0, 0, "" , 0, 9, "#!AMR-WB\n") - CHECK(prc , 0, 0, "" , 0, 8, "\x37\x00\x00\x10\x6d\x00\x00\x10") - CHECK(sph , 0, 0, "" , 0, 7, "NIST_1A") - CHECK(amr-nb, 0, 0, "" , 0, 6, "#!AMR\n") - CHECK(txw , 0, 0, "" , 0, 6, "LM8953") - CHECK(sndt , 0, 0, "" , 0, 6, "SOUND\x1a") - CHECK(vorbis, 0, 4, "OggS" , 29, 6, "vorbis") - CHECK(opus , 0, 4, "OggS" , 28, 8, "OpusHead") - CHECK(speex , 0, 4, "OggS" , 28, 6, "Speex") - CHECK(hcom ,65, 4, "FSSD" , 128,4, "HCOM") - CHECK(wav , 0, 4, "RIFF" , 8, 4, "WAVE") - CHECK(wav , 0, 4, "RIFX" , 8, 4, "WAVE") - CHECK(wav , 0, 4, "RF64" , 8, 4, "WAVE") - CHECK(aiff , 0, 4, "FORM" , 8, 4, "AIFF") - CHECK(aifc , 0, 4, "FORM" , 8, 4, "AIFC") - CHECK(8svx , 0, 4, "FORM" , 8, 4, "8SVX") - CHECK(maud , 0, 4, "FORM" , 8, 4, "MAUD") - CHECK(xa , 0, 0, "" , 0, 4, "XA\0\0") - CHECK(xa , 0, 0, "" , 0, 4, "XAI\0") - CHECK(xa , 0, 0, "" , 0, 4, "XAJ\0") - CHECK(au , 0, 0, "" , 0, 4, ".snd") - CHECK(au , 0, 0, "" , 0, 4, "dns.") - CHECK(au , 0, 0, "" , 0, 4, "\0ds.") - CHECK(au , 0, 0, "" , 0, 4, ".sd\0") - CHECK(flac , 0, 0, "" , 0, 4, "fLaC") - CHECK(avr , 0, 0, "" , 0, 4, "2BIT") - CHECK(caf , 0, 0, "" , 0, 4, "caff") - CHECK(wv , 0, 0, "" , 0, 4, "wvpk") - CHECK(paf , 0, 0, "" , 0, 4, " paf") - CHECK(sf , 0, 0, "" , 0, 4, "\144\243\001\0") - CHECK(sf , 0, 0, "" , 0, 4, "\0\001\243\144") - CHECK(sf , 0, 0, "" , 0, 4, "\144\243\002\0") - CHECK(sf , 0, 0, "" , 0, 4, "\0\002\243\144") - CHECK(sf , 0, 0, "" , 0, 4, "\144\243\003\0") - CHECK(sf , 0, 0, "" , 0, 4, "\0\003\243\144") - CHECK(sf , 0, 0, "" , 0, 4, "\144\243\004\0") - CHECK(sox , 0, 0, "" , 0, 4, ".SoX") - CHECK(sox , 0, 0, "" , 0, 4, "XoS.") - - if (ext && !strcasecmp(ext, "snd")) - CHECK(sndr , 7, 1, "" , 0, 2, "\0") - #undef CHECK - -#if HAVE_MAGIC - if (sox_globals.use_magic) { - static magic_t magic; - char const * filetype = NULL; - if (!magic) { - magic = magic_open(MAGIC_MIME | MAGIC_SYMLINK); - if (magic) - magic_load(magic, NULL); - } - if (magic) - filetype = magic_buffer(magic, data, len); - if (filetype && strncmp(filetype, "application/octet-stream", (size_t)24) && - !lsx_strends(filetype, "/unknown") && - strncmp(filetype, "text/plain", (size_t)10) ) - return filetype; - else if (filetype) - lsx_debug("libmagic detected %s", filetype); - } +#ifdef HAVE_LIBLTDL + int ret; + if (plugins_initted && (ret = lt_dlexit()) != 0) + lsx_fail("lt_dlexit failed with %d error(s): %s", ret, lt_dlerror()); + plugins_initted = sox_false; + nformats = NSTATIC_FORMATS; #endif - return NULL; -} - -static sox_encodings_info_t const s_sox_encodings_info[] = { - {sox_encodings_none , "n/a" , "Unknown or not applicable"}, - {sox_encodings_none , "Signed PCM" , "Signed Integer PCM"}, - {sox_encodings_none , "Unsigned PCM" , "Unsigned Integer PCM"}, - {sox_encodings_none , "F.P. PCM" , "Floating Point PCM"}, - {sox_encodings_none , "F.P. PCM" , "Floating Point (text) PCM"}, - {sox_encodings_none , "FLAC" , "FLAC"}, - {sox_encodings_none , "HCOM" , "HCOM"}, - {sox_encodings_none , "WavPack" , "WavPack"}, - {sox_encodings_none , "F.P. WavPack" , "Floating Point WavPack"}, - {sox_encodings_lossy1, "u-law" , "u-law"}, - {sox_encodings_lossy1, "A-law" , "A-law"}, - {sox_encodings_lossy1, "G.721 ADPCM" , "G.721 ADPCM"}, - {sox_encodings_lossy1, "G.723 ADPCM" , "G.723 ADPCM"}, - {sox_encodings_lossy1, "CL ADPCM (8)" , "CL ADPCM (from 8-bit)"}, - {sox_encodings_lossy1, "CL ADPCM (16)", "CL ADPCM (from 16-bit)"}, - {sox_encodings_lossy1, "MS ADPCM" , "MS ADPCM"}, - {sox_encodings_lossy1, "IMA ADPCM" , "IMA ADPCM"}, - {sox_encodings_lossy1, "OKI ADPCM" , "OKI ADPCM"}, - {sox_encodings_lossy1, "DPCM" , "DPCM"}, - {sox_encodings_none , "DWVW" , "DWVW"}, - {sox_encodings_none , "DWVWN" , "DWVWN"}, - {sox_encodings_lossy2, "GSM" , "GSM"}, - {sox_encodings_lossy2, "MPEG audio" , "MPEG audio (layer I, II or III)"}, - {sox_encodings_lossy2, "Vorbis" , "Vorbis"}, - {sox_encodings_lossy2, "AMR-WB" , "AMR-WB"}, - {sox_encodings_lossy2, "AMR-NB" , "AMR-NB"}, - {sox_encodings_lossy2, "CVSD" , "CVSD"}, - {sox_encodings_lossy2, "LPC10" , "LPC10"}, - {sox_encodings_lossy2, "Opus" , "Opus"}, -}; - -assert_static(array_length(s_sox_encodings_info) == SOX_ENCODINGS, - SIZE_MISMATCH_BETWEEN_sox_encoding_t_AND_sox_encodings_info); - -sox_encodings_info_t const * -sox_get_encodings_info(void) -{ - return s_sox_encodings_info; } unsigned sox_precision(sox_encoding_t encoding, unsigned bits_per_sample) @@ -204,1148 +99,3 @@ unsigned sox_precision(sox_encoding_t encoding, unsigned bits_per_sample) } return 0; } - -void sox_init_encodinginfo(sox_encodinginfo_t * e) -{ - e->reverse_bytes = sox_option_default; - e->reverse_nibbles = sox_option_default; - e->reverse_bits = sox_option_default; - e->compression = HUGE_VAL; -} - -/*--------------------------------- Comments ---------------------------------*/ - -size_t sox_num_comments(sox_comments_t comments) -{ - size_t result = 0; - if (!comments) - return 0; - while (*comments++) - ++result; - return result; -} - -void sox_append_comment(sox_comments_t * comments, char const * comment) -{ - size_t n = sox_num_comments(*comments); - *comments = lsx_realloc(*comments, (n + 2) * sizeof(**comments)); - assert(comment); - (*comments)[n++] = lsx_strdup(comment); - (*comments)[n] = 0; -} - -void sox_append_comments(sox_comments_t * comments, char const * comment) -{ - char * end; - if (comment) { - while ((end = strchr(comment, '\n'))) { - size_t len = end - comment; - char * c = lsx_malloc((len + 1) * sizeof(*c)); - strncpy(c, comment, len); - c[len] = '\0'; - sox_append_comment(comments, c); - comment += len + 1; - free(c); - } - if (*comment) - sox_append_comment(comments, comment); - } -} - -sox_comments_t sox_copy_comments(sox_comments_t comments) -{ - sox_comments_t result = 0; - - if (comments) while (*comments) - sox_append_comment(&result, *comments++); - return result; -} - -void sox_delete_comments(sox_comments_t * comments) -{ - sox_comments_t p = *comments; - - if (p) while (*p) - free(*p++); - free(*comments); - *comments = 0; -} - -char * lsx_cat_comments(sox_comments_t comments) -{ - sox_comments_t p = comments; - size_t len = 0; - char * result; - - if (p) while (*p) - len += strlen(*p++) + 1; - - result = lsx_calloc(len? len : 1, sizeof(*result)); - - if ((p = comments) && *p) { - strcpy(result, *p); - while (*++p) - strcat(strcat(result, "\n"), *p); - } - return result; -} - -char const * sox_find_comment(sox_comments_t comments, char const * id) -{ - size_t len = strlen(id); - - if (comments) for (;*comments; ++comments) - if (!strncasecmp(*comments, id, len) && (*comments)[len] == '=') - return *comments + len + 1; - return NULL; -} - -static void set_endiannesses(sox_format_t * ft) -{ - if (ft->handler.flags & SOX_FILE_ENDIAN) { - sox_bool file_is_bigendian = !(ft->handler.flags & SOX_FILE_ENDBIG); - - if (ft->encoding.opposite_endian) { - ft->encoding.reverse_bytes = file_is_bigendian != MACHINE_IS_BIGENDIAN; - lsx_report("`%s': overriding file-type byte-order", ft->filename); - } else if (ft->encoding.reverse_bytes == sox_option_default) { - ft->encoding.reverse_bytes = file_is_bigendian == MACHINE_IS_BIGENDIAN; - } - } else { - if (ft->encoding.opposite_endian) { - ft->encoding.reverse_bytes = sox_option_yes; - lsx_report("`%s': overriding machine byte-order", ft->filename); - } else if (ft->encoding.reverse_bytes == sox_option_default) { - ft->encoding.reverse_bytes = sox_option_no; - } - } - - /* FIXME: Change reports to suitable warnings if trying - * to override something that can't be overridden. */ - - if (ft->encoding.reverse_bits == sox_option_default) - ft->encoding.reverse_bits = !!(ft->handler.flags & SOX_FILE_BIT_REV); - else if (ft->encoding.reverse_bits == !(ft->handler.flags & SOX_FILE_BIT_REV)) - lsx_report("`%s': overriding file-type bit-order", ft->filename); - - if (ft->encoding.reverse_nibbles == sox_option_default) - ft->encoding.reverse_nibbles = !!(ft->handler.flags & SOX_FILE_NIB_REV); - else - if (ft->encoding.reverse_nibbles == !(ft->handler.flags & SOX_FILE_NIB_REV)) - lsx_report("`%s': overriding file-type nibble-order", ft->filename); -} - -static sox_bool is_seekable(sox_format_t const * ft) -{ - assert(ft); - if (!ft->fp) - return sox_false; - - return !fseek(ft->fp, 0, SEEK_CUR); -} - -/* check that all settings have been given */ -static int sox_checkformat(sox_format_t * ft) -{ - ft->sox_errno = SOX_SUCCESS; - - if (ft->signal.rate <= 0) { - lsx_fail_errno(ft, SOX_EFMT, "sample rate zero or negative"); - return SOX_EOF; - } - if (!ft->signal.precision) { - lsx_fail_errno(ft,SOX_EFMT,"data encoding or sample size was not specified"); - return SOX_EOF; - } - return SOX_SUCCESS; -} - -static sox_bool is_url(char const * text) /* detects only wget-supported URLs */ -{ - return !( - strncasecmp(text, "http:" , (size_t)5) && - strncasecmp(text, "https:", (size_t)6) && - strncasecmp(text, "ftp:" , (size_t)4)); -} - -static int xfclose(FILE * file, lsx_io_type io_type) -{ - return -#ifdef HAVE_POPEN - io_type != lsx_io_file? pclose(file) : -#endif - fclose(file); -} - -static void incr_pipe_size(FILE *f) -{ -/* - * Linux 2.6.35 and later has the ability to expand the pipe buffer - * Try to get it as big as possible to avoid stalls when SoX itself - * is using big buffers - */ -#if defined(F_GETPIPE_SZ) && defined(F_SETPIPE_SZ) - static long max_pipe_size; - - /* read the maximum size of the pipe the first time this is called */ - if (max_pipe_size == 0) { - const char path[] = "/proc/sys/fs/pipe-max-size"; - int fd = open(path, O_RDONLY); - - max_pipe_size = -1; - if (fd >= 0) { - char buf[80]; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - - if (r > 0) { - buf[r] = 0; - max_pipe_size = strtol(buf, NULL, 10); - - /* guard against obviously wrong values on messed up systems */ - if (max_pipe_size <= PIPE_BUF || max_pipe_size > INT_MAX) - max_pipe_size = -1; - } - close(fd); - } - } - - if (max_pipe_size > PIPE_BUF) { - int fd = fileno(f); - - if (fcntl(fd, F_SETPIPE_SZ, max_pipe_size) >= 0) - lsx_debug("got pipe %ld bytes\n", max_pipe_size); - else - lsx_warn("couldn't set pipe size to %ld bytes: %s\n", - max_pipe_size, strerror(errno)); - } -#endif /* do nothing for platforms without F_{GET,SET}PIPE_SZ */ -} - -static FILE * xfopen(char const * identifier, char const * mode, lsx_io_type * io_type) -{ - *io_type = lsx_io_file; - - if (*identifier == '|') { - FILE * f = NULL; -#ifdef HAVE_POPEN -#ifndef POPEN_MODE -#define POPEN_MODE "r" -#endif - f = popen(identifier + 1, POPEN_MODE); - *io_type = lsx_io_pipe; - incr_pipe_size(f); -#else - lsx_fail("this build of SoX cannot open pipes"); -#endif - return f; - } - else if (is_url(identifier)) { - FILE * f = NULL; -#ifdef HAVE_POPEN - char const * const command_format = "wget --no-check-certificate -q -O- \"%s\""; - char * command = lsx_malloc(strlen(command_format) + strlen(identifier)); - sprintf(command, command_format, identifier); - f = popen(command, POPEN_MODE); - incr_pipe_size(f); - free(command); - *io_type = lsx_io_url; -#else - lsx_fail("this build of SoX cannot open URLs"); -#endif - return f; - } - return fopen(identifier, mode); -} - -/* Hack to rewind pipes (a small amount). - * Works by resetting the FILE buffer pointer */ -static void UNUSED rewind_pipe(FILE * fp) -{ -/* _FSTDIO is for Torek stdio (i.e. most BSD-derived libc's) - * In theory, we no longer need to check _NEWLIB_VERSION or __APPLE__ */ -#if defined _FSTDIO || defined _NEWLIB_VERSION || defined __APPLE__ - fp->_p -= PIPE_AUTO_DETECT_SIZE; - fp->_r += PIPE_AUTO_DETECT_SIZE; -#elif defined __GLIBC__ - fp->_IO_read_ptr = fp->_IO_read_base; -#elif defined _MSC_VER && _MSC_VER >= 1900 - #define NO_REWIND_PIPE -#elif defined _MSC_VER || defined _WIN32 || defined _WIN64 || \ - defined _ISO_STDIO_ISO_H || defined __sgi - fp->_ptr = fp->_base; -#else - /* To fix this #error, either simply remove the #error line and live without - * file-type detection with pipes, or add support for your compiler in the - * lines above. Test with cat monkey.wav | ./sox --info - */ - #error FIX NEEDED HERE - #define NO_REWIND_PIPE - (void)fp; -#endif -} - -static sox_format_t * open_read( - char const * path, - void * buffer UNUSED, - size_t buffer_size UNUSED, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype) -{ - sox_format_t * ft = lsx_calloc(1, sizeof(*ft)); - sox_format_handler_t const * handler; - char const * const io_types[] = {"file", "pipe", "file URL"}; - char const * type = ""; - size_t input_bufsiz = sox_globals.input_bufsiz? - sox_globals.input_bufsiz : sox_globals.bufsiz; - - if (filetype) { - if (!(handler = sox_find_format(filetype, sox_false))) { - lsx_fail("no handler for given file type `%s'", filetype); - goto error; - } - ft->handler = *handler; - } - - if (!(ft->handler.flags & SOX_FILE_NOSTDIO)) { - if (!strcmp(path, "-")) { /* Use stdin if the filename is "-" */ - if (sox_globals.stdin_in_use_by) { - lsx_fail("`-' (stdin) already in use by `%s'", sox_globals.stdin_in_use_by); - goto error; - } - sox_globals.stdin_in_use_by = "audio input"; - SET_BINARY_MODE(stdin); - ft->fp = stdin; - } - else { - ft->fp = -#ifdef HAVE_FMEMOPEN - buffer? fmemopen(buffer, buffer_size, "rb") : -#endif - xfopen(path, "rb", &ft->io_type); - type = io_types[ft->io_type]; - if (ft->fp == NULL) { - lsx_fail("can't open input %s `%s': %s", type, path, strerror(errno)); - goto error; - } - } - if (setvbuf (ft->fp, NULL, _IOFBF, sizeof(char) * input_bufsiz)) { - lsx_fail("Can't set read buffer"); - goto error; - } - ft->seekable = is_seekable(ft); - } - - if (!filetype) { - if (ft->seekable) { - filetype = auto_detect_format(ft, lsx_find_file_extension(path)); - lsx_rewind(ft); - } -#ifndef NO_REWIND_PIPE - else if (!(ft->handler.flags & SOX_FILE_NOSTDIO) && - input_bufsiz >= PIPE_AUTO_DETECT_SIZE) { - filetype = auto_detect_format(ft, lsx_find_file_extension(path)); - rewind_pipe(ft->fp); - ft->tell_off = 0; - } -#endif - - if (filetype) { - lsx_report("detected file format type `%s'", filetype); - if (!(handler = sox_find_format(filetype, sox_false))) { - lsx_fail("no handler for detected file type `%s'", filetype); - goto error; - } - } - else { - if (ft->io_type == lsx_io_pipe) { - filetype = "sox"; /* With successful pipe rewind, this isn't useful */ - lsx_report("assuming input pipe `%s' has file-type `sox'", path); - } - else if (!(filetype = lsx_find_file_extension(path))) { - lsx_fail("can't determine type of %s `%s'", type, path); - goto error; - } - if (!(handler = sox_find_format(filetype, sox_true))) { - lsx_fail("no handler for file extension `%s'", filetype); - goto error; - } - } - ft->handler = *handler; - if (ft->handler.flags & SOX_FILE_NOSTDIO) { - xfclose(ft->fp, ft->io_type); - ft->fp = NULL; - } - } - if (!ft->handler.startread && !ft->handler.read) { - lsx_fail("file type `%s' isn't readable", filetype); - goto error; - } - - ft->mode = 'r'; - ft->filetype = lsx_strdup(filetype); - ft->filename = lsx_strdup(path); - if (signal) - ft->signal = *signal; - - if (encoding) - ft->encoding = *encoding; - else sox_init_encodinginfo(&ft->encoding); - set_endiannesses(ft); - - if ((ft->handler.flags & SOX_FILE_DEVICE) && !(ft->handler.flags & SOX_FILE_PHONY)) - lsx_set_signal_defaults(ft); - - ft->priv = lsx_calloc(1, ft->handler.priv_size); - /* Read and write starters can change their formats. */ - if (ft->handler.startread && (*ft->handler.startread)(ft) != SOX_SUCCESS) { - lsx_fail("can't open input %s `%s': %s", type, ft->filename, ft->sox_errstr); - goto error; - } - - /* Fill in some defaults: */ - if (sox_precision(ft->encoding.encoding, ft->encoding.bits_per_sample)) - ft->signal.precision = sox_precision(ft->encoding.encoding, ft->encoding.bits_per_sample); - if (!(ft->handler.flags & SOX_FILE_PHONY) && !ft->signal.channels) - ft->signal.channels = 1; - - if (sox_checkformat(ft) != SOX_SUCCESS) { - lsx_fail("bad input format for %s `%s': %s", type, ft->filename, ft->sox_errstr); - goto error; - } - - if (signal) { - if (signal->rate && signal->rate != ft->signal.rate) - lsx_warn("can't set sample rate %g; using %g", signal->rate, ft->signal.rate); - if (signal->channels && signal->channels != ft->signal.channels) - lsx_warn("can't set %u channels; using %u", signal->channels, ft->signal.channels); - } - return ft; - -error: - if (ft->fp && ft->fp != stdin) - xfclose(ft->fp, ft->io_type); - free(ft->priv); - free(ft->filename); - free(ft->filetype); - free(ft); - return NULL; -} - -sox_format_t * sox_open_read( - char const * path, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype) -{ - return open_read(path, NULL, (size_t)0, signal, encoding, filetype); -} - -sox_format_t * sox_open_mem_read( - void * buffer, - size_t buffer_size, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype) -{ - return open_read("", buffer, buffer_size, signal,encoding,filetype); -} - -sox_bool sox_format_supports_encoding( - char const * path, - char const * filetype, - sox_encodinginfo_t const * encoding) -{ - #define enc_arg(T) (T)handler->write_formats[i++] - sox_bool is_file_extension = filetype == NULL; - sox_format_handler_t const * handler; - unsigned i = 0, s; - sox_encoding_t e; - - assert(path || filetype); - assert(encoding); - if (!filetype) - filetype = lsx_find_file_extension(path); - - if (!filetype || !(handler = sox_find_format(filetype, is_file_extension)) || - !handler->write_formats) - return sox_false; - while ((e = enc_arg(sox_encoding_t))) { - if (e == encoding->encoding) { - sox_bool has_bits; - for (has_bits = sox_false; (s = enc_arg(unsigned)); has_bits = sox_true) - if (s == encoding->bits_per_sample) - return sox_true; - if (!has_bits && !encoding->bits_per_sample) - return sox_true; - break; - } - while (enc_arg(unsigned)); - } - return sox_false; - #undef enc_arg -} - -static void set_output_format(sox_format_t * ft) -{ - sox_encoding_t e = SOX_ENCODING_UNKNOWN; - unsigned i, s; - unsigned const * encodings = ft->handler.write_formats; -#define enc_arg(T) (T)encodings[i++] - - if (ft->handler.write_rates){ - if (!ft->signal.rate) - ft->signal.rate = ft->handler.write_rates[0]; - else { - sox_rate_t r; - i = 0; - while ((r = ft->handler.write_rates[i++])) { - if (r == ft->signal.rate) - break; - } - if (r != ft->signal.rate) { - sox_rate_t given = ft->signal.rate, max = 0; - ft->signal.rate = HUGE_VAL; - i = 0; - while ((r = ft->handler.write_rates[i++])) { - if (r > given && r < ft->signal.rate) - ft->signal.rate = r; - else max = max(r, max); - } - if (ft->signal.rate == HUGE_VAL) - ft->signal.rate = max; - lsx_warn("%s can't encode at %gHz; using %gHz", ft->handler.names[0], given, ft->signal.rate); - } - } - } - else if (!ft->signal.rate) - ft->signal.rate = SOX_DEFAULT_RATE; - - if (ft->handler.flags & SOX_FILE_CHANS) { - if (ft->signal.channels == 1 && !(ft->handler.flags & SOX_FILE_MONO)) { - ft->signal.channels = (ft->handler.flags & SOX_FILE_STEREO)? 2 : 4; - lsx_warn("%s can't encode mono; setting channels to %u", ft->handler.names[0], ft->signal.channels); - } else - if (ft->signal.channels == 2 && !(ft->handler.flags & SOX_FILE_STEREO)) { - ft->signal.channels = (ft->handler.flags & SOX_FILE_QUAD)? 4 : 1; - lsx_warn("%s can't encode stereo; setting channels to %u", ft->handler.names[0], ft->signal.channels); - } else - if (ft->signal.channels == 4 && !(ft->handler.flags & SOX_FILE_QUAD)) { - ft->signal.channels = (ft->handler.flags & SOX_FILE_STEREO)? 2 : 1; - lsx_warn("%s can't encode quad; setting channels to %u", ft->handler.names[0], ft->signal.channels); - } - } else ft->signal.channels = max(ft->signal.channels, 1); - - if (!encodings) - return; - /* If an encoding has been given, check if it supported by this handler */ - if (ft->encoding.encoding) { - i = 0; - while ((e = enc_arg(sox_encoding_t))) { - if (e == ft->encoding.encoding) - break; - while (enc_arg(unsigned)); - } - if (e != ft->encoding.encoding) { - lsx_warn("%s can't encode %s", ft->handler.names[0], sox_encodings_info[ft->encoding.encoding].desc); - ft->encoding.encoding = 0; - } - else { - unsigned max_p = 0; - unsigned max_p_s = 0; - unsigned given_size = 0; - sox_bool found = sox_false; - if (ft->encoding.bits_per_sample) - given_size = ft->encoding.bits_per_sample; - ft->encoding.bits_per_sample = 65; - while ((s = enc_arg(unsigned))) { - if (s == given_size) - found = sox_true; - if (sox_precision(e, s) >= ft->signal.precision) { - if (s < ft->encoding.bits_per_sample) - ft->encoding.bits_per_sample = s; - } - else if (sox_precision(e, s) > max_p) { - max_p = sox_precision(e, s); - max_p_s = s; - } - } - if (ft->encoding.bits_per_sample == 65) - ft->encoding.bits_per_sample = max_p_s; - if (given_size) { - if (found) - ft->encoding.bits_per_sample = given_size; - else lsx_warn("%s can't encode %s to %u-bit", ft->handler.names[0], sox_encodings_info[ft->encoding.encoding].desc, given_size); - } - } - } - - /* If a size has been given, check if it supported by this handler */ - if (!ft->encoding.encoding && ft->encoding.bits_per_sample) { - i = 0; - s= 0; - while (s != ft->encoding.bits_per_sample && (e = enc_arg(sox_encoding_t))) - while ((s = enc_arg(unsigned)) && s != ft->encoding.bits_per_sample); - if (s != ft->encoding.bits_per_sample) { - lsx_warn("%s can't encode to %u-bit", ft->handler.names[0], ft->encoding.bits_per_sample); - ft->encoding.bits_per_sample = 0; - } - else ft->encoding.encoding = e; - } - - /* Find the smallest lossless encoding with precision >= signal.precision */ - if (!ft->encoding.encoding) { - ft->encoding.bits_per_sample = 65; - i = 0; - while ((e = enc_arg(sox_encoding_t))) - while ((s = enc_arg(unsigned))) - if (!(sox_encodings_info[e].flags & (sox_encodings_lossy1 | sox_encodings_lossy2)) && - sox_precision(e, s) >= ft->signal.precision && s < ft->encoding.bits_per_sample) { - ft->encoding.encoding = e; - ft->encoding.bits_per_sample = s; - } - } - - /* Find the smallest lossy encoding with precision >= signal precision, - * or, if none such, the highest precision encoding */ - if (!ft->encoding.encoding) { - unsigned max_p = 0; - sox_encoding_t max_p_e = 0; - unsigned max_p_s = 0; - i = 0; - while ((e = enc_arg(sox_encoding_t))) - do { - s = enc_arg(unsigned); - if (sox_precision(e, s) >= ft->signal.precision) { - if (s < ft->encoding.bits_per_sample) { - ft->encoding.encoding = e; - ft->encoding.bits_per_sample = s; - } - } - else if (sox_precision(e, s) > max_p) { - max_p = sox_precision(e, s); - max_p_e = e; - max_p_s = s; - } - } while (s); - if (!ft->encoding.encoding) { - ft->encoding.encoding = max_p_e; - ft->encoding.bits_per_sample = max_p_s; - } - } - ft->signal.precision = sox_precision(ft->encoding.encoding, ft->encoding.bits_per_sample); - #undef enc_arg -} - -sox_format_handler_t const * sox_write_handler( - char const * path, - char const * filetype, - char const * * filetype1) -{ - sox_format_handler_t const * handler; - if (filetype) { - if (!(handler = sox_find_format(filetype, sox_false))) { - if (filetype1) - lsx_fail("no handler for given file type `%s'", filetype); - return NULL; - } - } - else if (path) { - if (!(filetype = lsx_find_file_extension(path))) { - if (filetype1) - lsx_fail("can't determine type of `%s'", path); - return NULL; - } - if (!(handler = sox_find_format(filetype, sox_true))) { - if (filetype1) - lsx_fail("no handler for file extension `%s'", filetype); - return NULL; - } - } - else return NULL; - if (!handler->startwrite && !handler->write) { - if (filetype1) - lsx_fail("file type `%s' isn't writable", filetype); - return NULL; - } - if (filetype1) - *filetype1 = filetype; - return handler; -} - -static sox_format_t * open_write( - char const * path, - void * buffer UNUSED, - size_t buffer_size UNUSED, - char * * buffer_ptr UNUSED, - size_t * buffer_size_ptr UNUSED, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype, - sox_oob_t const * oob, - sox_bool (*overwrite_permitted)(const char *filename)) -{ - sox_format_t * ft = lsx_calloc(sizeof(*ft), 1); - sox_format_handler_t const * handler; - - if (!path || !signal) { - lsx_fail("must specify file name and signal parameters to write file"); - goto error; - } - - if (!(handler = sox_write_handler(path, filetype, &filetype))) - goto error; - - ft->handler = *handler; - - if (!(ft->handler.flags & SOX_FILE_NOSTDIO)) { - if (!strcmp(path, "-")) { /* Use stdout if the filename is "-" */ - if (sox_globals.stdout_in_use_by) { - lsx_fail("`-' (stdout) already in use by `%s'", sox_globals.stdout_in_use_by); - goto error; - } - sox_globals.stdout_in_use_by = "audio output"; - SET_BINARY_MODE(stdout); - ft->fp = stdout; - } - else { - struct stat st; - if (!stat(path, &st) && (st.st_mode & S_IFMT) == S_IFREG && - (overwrite_permitted && !overwrite_permitted(path))) { - lsx_fail("permission to overwrite `%s' denied", path); - goto error; - } - ft->fp = -#ifdef HAVE_FMEMOPEN - buffer? fmemopen(buffer, buffer_size, "w+b") : - buffer_ptr? open_memstream(buffer_ptr, buffer_size_ptr) : -#endif - fopen(path, "w+b"); - if (ft->fp == NULL) { - lsx_fail("can't open output file `%s': %s", path, strerror(errno)); - goto error; - } - } - - /* stdout tends to be line-buffered. Override this */ - /* to be Full Buffering. */ - if (setvbuf (ft->fp, NULL, _IOFBF, sizeof(char) * sox_globals.bufsiz)) { - lsx_fail("Can't set write buffer"); - goto error; - } - ft->seekable = is_seekable(ft); - } - - ft->filetype = lsx_strdup(filetype); - ft->filename = lsx_strdup(path); - ft->mode = 'w'; - ft->signal = *signal; - - if (encoding) - ft->encoding = *encoding; - else sox_init_encodinginfo(&ft->encoding); - set_endiannesses(ft); - - if (oob) { - ft->oob = *oob; - /* deep copy: */ - ft->oob.comments = sox_copy_comments(oob->comments); - } - - set_output_format(ft); - - /* FIXME: doesn't cover the situation where - * codec changes audio length due to block alignment (e.g. 8svx, gsm): */ - if (signal->rate && signal->channels) - ft->signal.length = ft->signal.length * ft->signal.rate / signal->rate * - ft->signal.channels / signal->channels + .5; - - if ((ft->handler.flags & SOX_FILE_REWIND) && strcmp(ft->filetype, "sox") && !ft->signal.length && !ft->seekable) - lsx_warn("can't seek in output file `%s'; length in file header will be unspecified", ft->filename); - - ft->priv = lsx_calloc(1, ft->handler.priv_size); - /* Read and write starters can change their formats. */ - if (ft->handler.startwrite && (ft->handler.startwrite)(ft) != SOX_SUCCESS){ - lsx_fail("can't open output file `%s': %s", ft->filename, ft->sox_errstr); - goto error; - } - - if (sox_checkformat(ft) != SOX_SUCCESS) { - lsx_fail("bad format for output file `%s': %s", ft->filename, ft->sox_errstr); - goto error; - } - - if ((ft->handler.flags & SOX_FILE_DEVICE) && signal) { - if (signal->rate && signal->rate != ft->signal.rate) - lsx_report("can't set sample rate %g; using %g", signal->rate, ft->signal.rate); - if (signal->channels && signal->channels != ft->signal.channels) - lsx_report("can't set %u channels; using %u", signal->channels, ft->signal.channels); - } - return ft; - -error: - if (ft->fp && ft->fp != stdout) - xfclose(ft->fp, ft->io_type); - free(ft->priv); - free(ft->filename); - free(ft->filetype); - free(ft); - return NULL; -} - -sox_format_t * sox_open_write( - char const * path, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype, - sox_oob_t const * oob, - sox_bool (*overwrite_permitted)(const char *filename)) -{ - return open_write(path, NULL, (size_t)0, NULL, NULL, signal, encoding, filetype, oob, overwrite_permitted); -} - -sox_format_t * sox_open_mem_write( - void * buffer, - size_t buffer_size, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype, - sox_oob_t const * oob) -{ - return open_write("", buffer, buffer_size, NULL, NULL, signal, encoding, filetype, oob, NULL); -} - -sox_format_t * sox_open_memstream_write( - char * * buffer_ptr, - size_t * buffer_size_ptr, - sox_signalinfo_t const * signal, - sox_encodinginfo_t const * encoding, - char const * filetype, - sox_oob_t const * oob) -{ - return open_write("", NULL, (size_t)0, buffer_ptr, buffer_size_ptr, signal, encoding, filetype, oob, NULL); -} - -size_t sox_read(sox_format_t * ft, sox_sample_t * buf, size_t len) -{ - size_t actual; - if (ft->signal.length != SOX_UNSPEC) - len = min(len, ft->signal.length - ft->olength); - actual = ft->handler.read? (*ft->handler.read)(ft, buf, len) : 0; - actual = actual > len? 0 : actual; - ft->olength += actual; - return actual; -} - -size_t sox_write(sox_format_t * ft, const sox_sample_t *buf, size_t len) -{ - size_t actual = ft->handler.write? (*ft->handler.write)(ft, buf, len) : 0; - ft->olength += actual; - return actual; -} - -int sox_close(sox_format_t * ft) -{ - int result = SOX_SUCCESS; - - if (ft->mode == 'r') - result = ft->handler.stopread? (*ft->handler.stopread)(ft) : SOX_SUCCESS; - else { - if (ft->handler.flags & SOX_FILE_REWIND) { - if (ft->olength != ft->signal.length && ft->seekable) { - result = lsx_seeki(ft, (off_t)0, 0); - if (result == SOX_SUCCESS) - result = ft->handler.stopwrite? (*ft->handler.stopwrite)(ft) - : ft->handler.startwrite?(*ft->handler.startwrite)(ft) : SOX_SUCCESS; - } - } - else result = ft->handler.stopwrite? (*ft->handler.stopwrite)(ft) : SOX_SUCCESS; - } - - if (ft->fp == stdin) { - sox_globals.stdin_in_use_by = NULL; - } else if (ft->fp == stdout) { - fflush(stdout); - sox_globals.stdout_in_use_by = NULL; - } else if (ft->fp) { - xfclose(ft->fp, ft->io_type); - } - - free(ft->priv); - free(ft->filename); - free(ft->filetype); - sox_delete_comments(&ft->oob.comments); - - free(ft); - return result; -} - -int sox_seek(sox_format_t * ft, sox_uint64_t offset, int whence) -{ - /* FIXME: Implement SOX_SEEK_CUR and SOX_SEEK_END. */ - if (whence != SOX_SEEK_SET) - return SOX_EOF; /* FIXME: return SOX_EINVAL */ - - /* If file is a seekable file and this handler supports seeking, - * then invoke handler's function. - */ - if (ft->seekable && ft->handler.seek) - return (*ft->handler.seek)(ft, offset); - return SOX_EOF; /* FIXME: return SOX_EBADF */ -} - -static int strcaseends(char const * str, char const * end) -{ - size_t str_len = strlen(str), end_len = strlen(end); - return str_len >= end_len && !strcasecmp(str + str_len - end_len, end); -} - -typedef enum {None, M3u, Pls} playlist_t; - -static playlist_t playlist_type(char const * filename) -{ - char * x, * p; - playlist_t result = None; - - if (*filename == '|') - return result; - if (strcaseends(filename, ".m3u")) - return M3u; - if (strcaseends(filename, ".pls")) - return Pls; - x = lsx_strdup(filename); - p = strrchr(x, '?'); - if (p) { - *p = '\0'; - result = playlist_type(x); - } - free(x); - return result; -} - -sox_bool sox_is_playlist(char const * filename) -{ - return playlist_type(filename) != None; -} - -int sox_parse_playlist(sox_playlist_callback_t callback, void * p, char const * const listname) -{ - sox_bool const is_pls = playlist_type(listname) == Pls; - int const comment_char = "#;"[is_pls]; - size_t text_length = 100; - char * text = lsx_malloc(text_length + 1); - char * dirname = lsx_strdup(listname); - char * slash_pos = LAST_SLASH(dirname); - lsx_io_type io_type; - FILE * file = xfopen(listname, "r", &io_type); - char * filename; - int c, result = SOX_SUCCESS; - - if (!slash_pos) - *dirname = '\0'; - else - *slash_pos = '\0'; - - if (file == NULL) { - lsx_fail("Can't open playlist file `%s': %s", listname, strerror(errno)); - result = SOX_EOF; - } - else { - do { - size_t i = 0; - size_t begin = 0, end = 0; - - while (isspace(c = getc(file))); - if (c == EOF) - break; - while (c != EOF && !strchr("\r\n", c) && c != comment_char) { - if (i == text_length) - text = lsx_realloc(text, (text_length <<= 1) + 1); - text[i++] = c; - if (!strchr(" \t\f", c)) - end = i; - c = getc(file); - } - if (ferror(file)) - break; - if (c == comment_char) { - do c = getc(file); - while (c != EOF && !strchr("\r\n", c)); - if (ferror(file)) - break; - } - text[end] = '\0'; - if (is_pls) { - char dummy; - if (!strncasecmp(text, "file", (size_t) 4) && sscanf(text + 4, "%*u=%c", &dummy) == 1) - begin = strchr(text + 5, '=') - text + 1; - else end = 0; - } - if (begin != end) { - char const * id = text + begin; - - if (!dirname[0] || is_url(id) || IS_ABSOLUTE(id)) - filename = lsx_strdup(id); - else { - filename = lsx_malloc(strlen(dirname) + strlen(id) + 2); - sprintf(filename, "%s/%s", dirname, id); - } - if (sox_is_playlist(filename)) - sox_parse_playlist(callback, p, filename); - else if (callback(p, filename)) - c = EOF; - free(filename); - } - } while (c != EOF); - - if (ferror(file)) { - lsx_fail("error reading playlist file `%s': %s", listname, strerror(errno)); - result = SOX_EOF; - } - if (xfclose(file, io_type) && io_type == lsx_io_url) { - lsx_fail("error reading playlist file URL `%s'", listname); - result = SOX_EOF; - } - } - free(text); - free(dirname); - return result; -} - -/*----------------------------- Formats library ------------------------------*/ - -enum { - #define FORMAT(f) f, - #include "formats.h" - #undef FORMAT - NSTATIC_FORMATS -}; - -static sox_bool plugins_initted = sox_false; - -#ifdef HAVE_LIBLTDL /* Plugin format handlers */ - #define MAX_DYNAMIC_FORMATS 42 - #define MAX_FORMATS (NSTATIC_FORMATS + MAX_DYNAMIC_FORMATS) - #define MAX_FORMATS_1 (MAX_FORMATS + 1) - #define MAX_NAME_LEN (size_t)1024 /* FIXME: Use vasprintf */ -#else - #define MAX_FORMATS_1 -#endif - -#define FORMAT(f) extern sox_format_handler_t const * lsx_##f##_format_fn(void); -#include "formats.h" -#undef FORMAT - -static sox_format_tab_t s_sox_format_fns[MAX_FORMATS_1] = { - #define FORMAT(f) {NULL, lsx_##f##_format_fn}, - #include "formats.h" - #undef FORMAT - {NULL, NULL} -}; - -const sox_format_tab_t * -sox_get_format_fns(void) -{ - return s_sox_format_fns; -} - -static unsigned nformats = NSTATIC_FORMATS; - -#ifdef HAVE_LIBLTDL /* Plugin format handlers */ - - static int init_format(const char *file, lt_ptr data) - { - lt_dlhandle lth = lt_dlopenext(file); - const char *end = file + strlen(file); - const char prefix[] = "sox_fmt_"; - char fnname[MAX_NAME_LEN]; - char *start = strstr(file, prefix); - - (void)data; - if (start && (start += sizeof(prefix) - 1) < end) { - int ret = snprintf(fnname, MAX_NAME_LEN, - "lsx_%.*s_format_fn", (int)(end - start), start); - if (ret > 0 && ret < (int)MAX_NAME_LEN) { - union {sox_format_fn_t fn; lt_ptr ptr;} ltptr; - ltptr.ptr = lt_dlsym(lth, fnname); - lsx_debug("opening format plugin `%s': library %p, entry point %p\n", - fnname, (void *)lth, ltptr.ptr); - if (ltptr.fn && (ltptr.fn()->sox_lib_version_code & ~255) == - (SOX_LIB_VERSION_CODE & ~255)) { /* compatible version check */ - if (nformats == MAX_FORMATS) { - lsx_warn("too many plugin formats"); - return -1; - } - s_sox_format_fns[nformats++].fn = ltptr.fn; - } - } - } - return 0; - } -#endif - -int sox_format_init(void) /* Find & load format handlers. */ -{ - if (plugins_initted) - return SOX_EOF; - - plugins_initted = sox_true; -#ifdef HAVE_LIBLTDL - { - int error = lt_dlinit(); - if (error) { - lsx_fail("lt_dlinit failed with %d error(s): %s", error, lt_dlerror()); - return SOX_EOF; - } - lt_dlforeachfile(PKGLIBDIR, init_format, NULL); - } -#endif - return SOX_SUCCESS; -} - -void sox_format_quit(void) /* Cleanup things. */ -{ -#ifdef HAVE_LIBLTDL - int ret; - if (plugins_initted && (ret = lt_dlexit()) != 0) - lsx_fail("lt_dlexit failed with %d error(s): %s", ret, lt_dlerror()); - plugins_initted = sox_false; - nformats = NSTATIC_FORMATS; -#endif -} - -/* Find a named format in the formats library. - * - * (c) 2005-9 Chris Bagwell and SoX contributors. - * Copyright 1991 Lance Norskog And Sundry Contributors. - * - * This source code is freely redistributable and may be used for any - * purpose. This copyright notice must be maintained. - * - * Lance Norskog, Sundry Contributors, Chris Bagwell and SoX contributors - * are not responsible for the consequences of using this software. - */ -sox_format_handler_t const * sox_find_format(char const * name0, sox_bool no_dev) -{ - size_t f, n; - - if (name0) { - char * name = lsx_strdup(name0); - char * pos = strchr(name, ';'); - if (pos) /* Use only the 1st clause of a mime string */ - *pos = '\0'; - for (f = 0; f < nformats; ++f) { - sox_format_handler_t const * handler = s_sox_format_fns[f].fn(); - - if (!(no_dev && (handler->flags & SOX_FILE_DEVICE))) - for (n = 0; handler->names[n]; ++n) - if (!strcasecmp(handler->names[n], name)) { - free(name); - return handler; /* Found it. */ - } - } - free(name); - } - if (sox_format_init() == SOX_SUCCESS) /* Try again with plugins */ - return sox_find_format(name0, no_dev); - return NULL; -} From 0551fd61341b5026cc8d67258dc7859b08bef2b0 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 5 Feb 2022 08:23:33 -0800 Subject: [PATCH 86/89] Add release notes to user manual for 1.7.0. --- USER_MANUAL.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 91311f5a0..d87dec34f 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -760,6 +760,32 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes # Release Notes +## V1.7.0 February 2022 + +1. Bugfixes: + * Resolves issue with waterfall appearing garbled on some systems. (PR #205) + * Resolves issue with Restore Defaults restoring previous settings on exit. (PR #207) + * Resolves issue with some sound valid sound devices causing PortAudio errors during startup checks. (PR #192) +2. Enhancements: + * Removes requirement to restart FreeDV after using Restore Defaults. (PR #207) + * Hides frequency display on main window unless PSK Reporter reporting is turned on. (PR #207) + * Scales per-mode squelch settings when in multi-RX mode to reduce unwanted noise. (PR #186) + * Single-thread mode is now the default when multi-RX is turned on. (PR #175) + * Makes multi-RX mode the default. (PR #175) + * Mic In/Speaker Out volume controls added to Filter window. (PR #208) + * Cleans up UI for filters and makes the dialog non-modal. (PR #208) + * Adds optional support for PulseAudio on Linux systems. (PR #194) +3. Documentation: + * Adds section on creating Windows shortcuts to handle multiple configurations. (PR #204) + * Resolves issue with PDF image placement. (PR #203) +4. Build System: + * Uses more portable way of referring to Bash in build scripts. (PR #200) + * User manual now installed along with executable. (PR #187) + * macOS app bundle generated by CMake instead of manually. (PR #184) + * Fail as soon as a step in the build script fails. (PR #183) + * Have Windows uninstaller clean up Registry. (PR #182) + * Windows installer now installs sample .wav files. (PR #182) + ## V1.6.1 September 2021 1. Bugfixes: From 9c14527f237ba52bc43290cb6fab8f35c1476e97 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 5 Feb 2022 16:26:28 +0000 Subject: [PATCH 87/89] latest user manual PDF --- USER_MANUAL.html | 54 ++++++++++++++++++++++++++++++++++++++--------- USER_MANUAL.pdf | Bin 998896 -> 1001396 bytes 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/USER_MANUAL.html b/USER_MANUAL.html index 42e3b6f53..6b1cdd994 100644 --- a/USER_MANUAL.html +++ b/USER_MANUAL.html @@ -553,7 +553,41 @@

15 Glossary

16 Release Notes

-

16.1 V1.6.1 September 2021

+

16.1 V1.7.0 February 2022

+
    +
  1. Bugfixes: +
      +
    • Resolves issue with waterfall appearing garbled on some systems. (PR #205)
    • +
    • Resolves issue with Restore Defaults restoring previous settings on exit. (PR #207)
    • +
    • Resolves issue with some sound valid sound devices causing PortAudio errors during startup checks. (PR #192)
    • +
  2. +
  3. Enhancements: +
      +
    • Removes requirement to restart FreeDV after using Restore Defaults. (PR #207)
    • +
    • Hides frequency display on main window unless PSK Reporter reporting is turned on. (PR #207)
    • +
    • Scales per-mode squelch settings when in multi-RX mode to reduce unwanted noise. (PR #186)
    • +
    • Single-thread mode is now the default when multi-RX is turned on. (PR #175)
    • +
    • Makes multi-RX mode the default. (PR #175)
    • +
    • Mic In/Speaker Out volume controls added to Filter window. (PR #208)
    • +
    • Cleans up UI for filters and makes the dialog non-modal. (PR #208)
    • +
    • Adds optional support for PulseAudio on Linux systems. (PR #194)
    • +
  4. +
  5. Documentation: +
      +
    • Adds section on creating Windows shortcuts to handle multiple configurations. (PR #204)
    • +
    • Resolves issue with PDF image placement. (PR #203)
    • +
  6. +
  7. Build System: +
      +
    • Uses more portable way of referring to Bash in build scripts. (PR #200)
    • +
    • User manual now installed along with executable. (PR #187)
    • +
    • macOS app bundle generated by CMake instead of manually. (PR #184)
    • +
    • Fail as soon as a step in the build script fails. (PR #183)
    • +
    • Have Windows uninstaller clean up Registry. (PR #182)
    • +
    • Windows installer now installs sample .wav files. (PR #182)
    • +
  8. +
+

16.2 V1.6.1 September 2021

  1. Bugfixes:
      @@ -571,7 +605,7 @@

      16.1 V

Note: The PSK Reporter feature beginning in this release is incompatible with versions older than 1.6.1 due to a change in how callsigns are encoded.

-

16.2 V1.6.0 August 2021

+

16.3 V1.6.0 August 2021

  1. Bugfixes:
      @@ -606,43 +640,43 @@

      16.2 V1.6
    • Created “make dist” target for easy tarball generation. (PR #152)
-

16.3 V1.5.3 April 2021

+

16.4 V1.5.3 April 2021

  1. Simultaneous decode of 2020, 1600 and 700C/D/E (without needing to push Stop first, change the mode and push Start again).
  2. Dynamic switching of the current Tx mode between the aforementioned modes, again without needing to restart the session.
  3. A Tx level slider on the right hand side of the main screen to fine-tune transmit output (to more easily avoid clipping ALC and conflicting with other soundcard ham radio applications).
-

16.4 V1.5.2 January 2021

+

16.5 V1.5.2 January 2021

  1. Updates storage for sound card configuration to use device names instead of IDs.
  2. Detects changes to computer sound card configuration and notifies user when devices go away.
-

16.5 V1.5.1 January 2021

+

16.6 V1.5.1 January 2021

-

16.6 V1.5.0 December 2020

+

16.7 V1.5.0 December 2020

  1. FreeDV 700E, better performance than 700D on fast fading channels
  2. FreeDV 700D/700E clipper to increase average transmit power by 6dB
-

16.7 V1.4.3 August 2020

+

16.8 V1.4.3 August 2020

  1. Maintenance Release (no major new features)
  2. Changes to support wxWidgets 3.1 (but Windows versions built against wxWidgets 3.0)
  3. Under the hood - OFDM modem has been refactored, shouldn’t affect freedv-gui operation
-

16.8 V1.4.2 July 2020

+

16.9 V1.4.2 July 2020

  1. Maintenance Release (no major new features)
  2. Improved squelch/audio pass through on 700D/2020/2400B
  3. Under the hood - Codec2 library has been refactored, shouldn’t affect freedv-gui operation
  4. Removed Project Horus support (now being maintained outside of Codec2/FreeDV)
-

16.9 V1.4 June-October 2019

+

16.10 V1.4 June-October 2019

  1. FreeDV 2020, Project Horus Binary Modes.
  2. Improved OFDM Modem Acquisition, this will improve sync time on FreeDV 700D and 2020 on HF fading channels, and can also handle +/- 60 Hz frequency offsets when tuning.
  3. @@ -650,7 +684,7 @@

    16.9
  4. Wide bandwidth phase estimation and DPSK for OFDM modes (700D/2020) for fast fading/QO-100 channels (Tools-Options)
  5. Better speech quality on FreeDV 700C/700D with Auto equaliser (Tools-Filter)
-

16.10 V1.3 May 2018

+

16.11 V1.3 May 2018

  • FreeDV 700D
diff --git a/USER_MANUAL.pdf b/USER_MANUAL.pdf index d78123d395c7e4ce469e5345f73408b4933ba70a..7fdde7680bd29d8711ae81c04ba1de90166fe116 100644 GIT binary patch delta 49987 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 eK}>9h{{RjKJr5oWIW;jf3MC~)Peuw!LG7sk delta 47427 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 u8YqVt(JZ4_Jv)sKki>`7R+)QAsLGA^acO*rl8R From 2f1bd87b41885b29f94142be24edaae8db771ca0 Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 5 Feb 2022 12:32:51 -0800 Subject: [PATCH 88/89] Resolve merge issue causing segfaults. --- src/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 0d6c86551..69c1d0521 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -459,6 +459,8 @@ void MainFrame::loadConfiguration_() //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent) { + m_filterDialog = nullptr; + m_zoom = 1.; #ifdef __WXMSW__ From 8f6822fd25a4659271f003d8a8e8eb2ef917edab Mon Sep 17 00:00:00 2001 From: Mooneer Salem Date: Sat, 5 Feb 2022 15:10:25 -0800 Subject: [PATCH 89/89] Disable extra logging of Pulse latency. --- src/audio/PulseAudioDevice.cpp | 4 ++-- src/audio/PulseAudioDevice.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/audio/PulseAudioDevice.cpp b/src/audio/PulseAudioDevice.cpp index e63f2d733..ee92a3a89 100644 --- a/src/audio/PulseAudioDevice.cpp +++ b/src/audio/PulseAudioDevice.cpp @@ -78,7 +78,7 @@ void PulseAudioDevice::start() pa_stream_set_overflow_callback(stream_, &PulseAudioDevice::StreamOverflowCallback_, this); pa_stream_set_moved_callback(stream_, &PulseAudioDevice::StreamMovedCallback_, this); pa_stream_set_state_callback(stream_, &PulseAudioDevice::StreamStateCallback_, this); -#if 1 +#if 0 pa_stream_set_latency_update_callback(stream_, &PulseAudioDevice::StreamLatencyCallback_, this); #endif // 0 @@ -317,7 +317,7 @@ void PulseAudioDevice::StreamMovedCallback_(pa_stream *p, void *userdata) } } -#if 1 +#if 0 void PulseAudioDevice::StreamLatencyCallback_(pa_stream *p, void *userdata) { PulseAudioDevice* thisObj = static_cast(userdata); diff --git a/src/audio/PulseAudioDevice.h b/src/audio/PulseAudioDevice.h index 9167113ca..4e24346a6 100644 --- a/src/audio/PulseAudioDevice.h +++ b/src/audio/PulseAudioDevice.h @@ -71,7 +71,7 @@ class PulseAudioDevice : public IAudioDevice static void StreamOverflowCallback_(pa_stream *p, void *userdata); static void StreamMovedCallback_(pa_stream *p, void *userdata); static void StreamStateCallback_(pa_stream *p, void *userdata); -#if 1 +#if 0 static void StreamLatencyCallback_(pa_stream *p, void *userdata); #endif // 0 };
  1. Experimental support for reporting to PSK Reporter added.
  2. Bug fixes with audio configuration to allow mono devices to be used along with stereo ones.
  3. Tweaks to user interface and record/playback functionality to improve usability.
  4. Bug fixes and tweaks to improve voice keyer support.