From bb30a7c0412a908c509ac4813fbfc44bd1fc95ef Mon Sep 17 00:00:00 2001 From: Vasily Date: Fri, 17 Jan 2020 14:55:00 +0100 Subject: [PATCH] Add framework for implementing Linux AudioManager Source outputs must all be moved to new default source. Reference topic https://askubuntu.com/a/72076 * Adds microphone controls on Linux via PulseAudio (PA) for several points in https://github.com/OpenVR-Advanced-Settings/OpenVR-AdvancedSettings/issues/43. It can not be overstated how unsuited the public AudioManager API is for using the PA API. It can also not be overstated how undocumented and horrible the PA API is. The PA way of doing audio is very unlike the Windows way of doing audio, which the AudioManager is heavily influenced by. Since I don't have a Linux machine I have not been able to personally test this. https://github.com/feilen/ has graciously helped with testing. PulseAudio is very undocumented and not very widely used for what we need outside of official utilities. As such there's most likely a better way of doing things than I've done. Additional information can be found at https://github.com/username223/pulseaudio-device-changing. The "complex" PA API is asynchronous. Since our current AudioManager is running synchronously and would not work asynchronously (functions are expected to return valid data on function return) it is necessary to massage the PA API until it runs synchronously. This is done with a general workflow of 1. Register a callback function (`pa_context_get_source_info_list()`, for example). 2. Run the PA loop through `customPulseLoop()`. This will trigger any registered callbacks. Sometimes a `void*` is passed through the callback that can be used for custom data. `successCallback()` uses this to set the success variable. 3. Run the callback function and do required work. 4. Return to original function, do work or return values. It is not possible to run a `customPulseLoop()` inside of an already running loop (inside a callback), as this will lead to a PA assert error on runtime. --- .circleci/config.yml | 6 + .travis.yml | 2 +- build_scripts/qt/sources.pri | 15 +- docs/building_for_linux.md | 21 +- src/tabcontrollers/AudioTabController.cpp | 54 +- .../audiomanager/AudioManagerPulse.cpp | 153 +++++ .../audiomanager/AudioManagerPulse.h | 46 ++ .../audiomanager/AudioManagerPulse_internal.h | 649 ++++++++++++++++++ 8 files changed, 924 insertions(+), 22 deletions(-) create mode 100644 src/tabcontrollers/audiomanager/AudioManagerPulse.cpp create mode 100644 src/tabcontrollers/audiomanager/AudioManagerPulse.h create mode 100644 src/tabcontrollers/audiomanager/AudioManagerPulse_internal.h diff --git a/.circleci/config.yml b/.circleci/config.yml index d3b034e..a28aae2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,6 +54,9 @@ update_and_install_prereqs: &update_and_install_prereqs - run: name: Install X11 packages command: apt-get install libx11-dev libxt-dev libxtst-dev -y + - run: + name: Install pulse packages + command: apt-get install libpulse-dev -y - run: name: Install bear/clang-tidy command: apt-get install bear clang-tidy -y @@ -125,6 +128,9 @@ jobs: - run: name: Install X11 packages command: sudo apt-get install libx11-dev libxt-dev libxtst-dev -y + - run: + name: Install pulse packages + command: sudo apt-get install libpulse-dev -y - run: name: Install bear/clang-tidy command: sudo apt-get install bear clang-tidy -y diff --git a/.travis.yml b/.travis.yml index eccef5d..fa53af0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,7 +57,7 @@ install: - sudo apt-get update -qq - sudo apt-get install build-essential libgl1-mesa-dev - sudo apt-get install qt512-meta-minimal qt512multimedia qt512declarative qt512quickcontrols2 qt512tools qt512base - - sudo apt-get install libx11-dev libxt-dev libxtst-dev + - sudo apt-get install libx11-dev libxt-dev libxtst-dev libpulse-dev - sudo apt-get install bear clang-tidy - qtchooser -install opt-qt512 /opt/qt512/bin/qmake diff --git a/build_scripts/qt/sources.pri b/build_scripts/qt/sources.pri index 7a942d7..81c8689 100644 --- a/build_scripts/qt/sources.pri +++ b/build_scripts/qt/sources.pri @@ -37,7 +37,6 @@ HEADERS += src/overlaycontroller.h \ src/utils/Matrix.h \ src/utils/ChaperoneUtils.h \ src/quaternion/quaternion.h \ - src/tabcontrollers/audiomanager/AudioManagerDummy.h \ src/openvr/openvr_init.h \ src/openvr/ivrinput_action.h \ src/openvr/ivrinput_manifest.h \ @@ -86,8 +85,18 @@ unix:!macx { SOURCES += src/media_keys/media_keys_dummy.cpp } - SOURCES += src/tabcontrollers/audiomanager/AudioManagerDummy.cpp - HEADERS += src/tabcontrollers/audiomanager/AudioManagerDummy.h + !noPulse { + message(PulseAudio features enabled.) + SOURCES += src/tabcontrollers/audiomanager/AudioManagerPulse.cpp + HEADERS += src/tabcontrollers/audiomanager/AudioManagerPulse.h \ + src/tabcontrollers/audiomanager/AudioManagerPulse_internal.h + LIBS += -lpulse + } + else { + message(PulseAudio features disabled.) + SOURCES += src/tabcontrollers/audiomanager/AudioManagerDummy.cpp + HEADERS += src/tabcontrollers/audiomanager/AudioManagerDummy.h + } } macx { diff --git a/docs/building_for_linux.md b/docs/building_for_linux.md index 8a4d138..79d2ab6 100644 --- a/docs/building_for_linux.md +++ b/docs/building_for_linux.md @@ -16,6 +16,7 @@ * [qtchooser and versions](#qtchooser-and-versions) * [X11](#x11) * [DBUS](#dbus) + * [PulseAudio](#pulseaudio) * [SteamVR](#steamvr) * [clang-tidy and bear](#clang-tidy-and-bear) * [TL;DRs](#tldrs) @@ -188,6 +189,12 @@ DBUS is needed for controlling media players from VR. This feature and dependency can be disabled during compilation. +## PulseAudio + +PulseAudio is needed for controlling audio devices through the Audio tab. + +This feature and dependency can be disabled during compilation. + ## SteamVR On a completely fresh Ubuntu install the program also requires the following libraries to run. @@ -226,7 +233,7 @@ sudo apt-get -y install build-essential libgl1-mesa-dev # Install Qt sudo apt-get -y install qt512-meta-full qtchooser # Install additional features -sudo apt-get -y install libx11-dev libxt-dev libxtst-dev +sudo apt-get -y install libx11-dev libxt-dev libxtst-dev libpulse-dev # Install necessary SteamVR libraries sudo apt-get -y install libsdl2-dev libvulkan-dev # Set up qtchooser @@ -253,7 +260,7 @@ sudo apt-get -y install build-essential libgl1-mesa-dev # Install Qt sudo apt-get -y install qt512-meta-full qtchooser # Install additional features -sudo apt-get -y install libx11-dev libxt-dev libxtst-dev +sudo apt-get -y install libx11-dev libxt-dev libxtst-dev libpulse-dev # Install necessary SteamVR libraries sudo apt-get -y install libsdl2-dev libvulkan-dev # Set up qtchooser @@ -277,7 +284,7 @@ sudo apt-get -y install build-essential libgl1-mesa-dev # Install Qt sudo apt-get -y install qt5-default qtmultimedia5-dev libqt5multimediawidgets5 libqt5multimedia5-plugins libqt5multimedia5 qml-module-qtquick-extras qml-module-qtquick-controls2 qml-module-qtquick-dialogs qtdeclarative5-dev qtchooser # Install additional features -sudo apt-get -y install libx11-dev libxt-dev libxtst-dev +sudo apt-get -y install libx11-dev libxt-dev libxtst-dev libpulse-dev # Install necessary SteamVR libraries sudo apt-get -y install libsdl2-dev libvulkan-dev # Set up qtchooser @@ -300,7 +307,7 @@ sudo pacman --noconfirm -S base-devel mesa libudev0-shim # Install Qt sudo pacman --noconfirm -S qt5-declarative qt5-multimedia # Install additional features -sudo pacman --noconfirm -S xorg-server dbus +sudo pacman --noconfirm -S xorg-server dbus libpulse-dev # Compile qmake make -j2 @@ -334,6 +341,7 @@ The following values can be appended to the `CONFIG` internal variable in order | ----- | ------- | | `noX11` | Disables X11 specific features (VR to keyboard input). | | `noDBUS` | Disables DBUS specific features (control media players). | +| `noPulse` | Disables PulseAudio specific features (change audio devices). | | `debugSymbolsAndLogs` | Enables debug symbols and debug logging calls (while still having release optimizations). | The values are case sensitive. @@ -348,6 +356,11 @@ If you want to disable both X11 and DBUS features you would type qmake CONFIG+=noX11 CONFIG+=noDBUS ``` +If you want to disable X11, DBUS and PulseAudio features you would type +```bash +qmake CONFIG+=noX11 CONFIG+=noDBUS CONFIG+=noPulse +``` + The location for `make install` can be configured in the `qmake` step. This is done by setting the `PREFIX` variable. ```bash diff --git a/src/tabcontrollers/AudioTabController.cpp b/src/tabcontrollers/AudioTabController.cpp index b8a8d6e..94b4a92 100644 --- a/src/tabcontrollers/AudioTabController.cpp +++ b/src/tabcontrollers/AudioTabController.cpp @@ -6,6 +6,8 @@ #include "../settings/settings_object.h" #ifdef _WIN32 # include "audiomanager/AudioManagerWindows.h" +#elif __linux__ +# include "audiomanager/AudioManagerPulse.h" #else # include "audiomanager/AudioManagerDummy.h" #endif @@ -18,6 +20,8 @@ void AudioTabController::initStage1() vr::EVRSettingsError vrSettingsError; #ifdef _WIN32 audioManager.reset( new AudioManagerWindows() ); +#elif __linux__ + audioManager.reset( new AudioManagerPulse() ); #else audioManager.reset( new AudioManagerDummy() ); #endif @@ -453,29 +457,51 @@ int AudioTabController::getPlaybackDeviceCount() return static_cast( m_playbackDevices.size() ); } +bool indexIsValid( const int index, + const std::vector& devices, + const std::string& functionName ) noexcept +{ + if ( index < 0 ) + { + LOG( ERROR ) << functionName + << " called with index below zero: " << index; + return false; + } + + if ( static_cast( index ) >= devices.size() ) + { + LOG( ERROR ) << functionName + << " called with index greater than size of " + "devices '" + << devices.size() << "', index '" << index << "'."; + return false; + } + + return true; +} + QString AudioTabController::getPlaybackDeviceName( int index ) { - if ( index >= 0 && static_cast( index ) < m_playbackDevices.size() ) + if ( indexIsValid( index, m_playbackDevices, "getPlaybackDeviceName" ) ) { return QString::fromStdString( - m_playbackDevices[static_cast( index )].name() ); + m_playbackDevices.at( static_cast( index ) ).name() ); } else { - return ""; + return ""; } } std::string AudioTabController::getPlaybackDeviceID( int index ) { - if ( index >= 0 && static_cast( index ) < m_playbackDevices.size() ) + if ( indexIsValid( index, m_playbackDevices, "getPlaybackDeviceID" ) ) { - return m_playbackDevices[static_cast( index )].id(); + return m_playbackDevices.at( static_cast( index ) ).id(); } else { - LOG( ERROR ) << "Playback Device does not have a unique id"; - return ""; + return ""; } } @@ -491,27 +517,26 @@ int AudioTabController::getRecordingDeviceCount() std::string AudioTabController::getRecordingDeviceID( int index ) { - if ( index >= 0 && static_cast( index ) < m_playbackDevices.size() ) + if ( indexIsValid( index, m_recordingDevices, "getRecordingDeviceID" ) ) { - return m_recordingDevices[static_cast( index )].id(); + return m_recordingDevices.at( static_cast( index ) ).id(); } else { - LOG( ERROR ) << "Recording Device does not have a unique id"; - return ""; + return ""; } } QString AudioTabController::getRecordingDeviceName( int index ) { - if ( index >= 0 && static_cast( index ) < m_playbackDevices.size() ) + if ( indexIsValid( index, m_recordingDevices, "getRecordingDeviceName" ) ) { return QString::fromStdString( - m_recordingDevices[static_cast( index )].name() ); + m_recordingDevices.at( static_cast( index ) ).name() ); } else { - return ""; + return ""; } } @@ -628,6 +653,7 @@ void AudioTabController::setMicDeviceIndex( int index, bool notify ) // code to just change Mic audioManager->setMicDevice( m_recordingDevices[static_cast( index )].id(), notify ); + m_recordingDeviceIndex = index; if ( tempProx ) { setMicProximitySensorCanMute( true ); diff --git a/src/tabcontrollers/audiomanager/AudioManagerPulse.cpp b/src/tabcontrollers/audiomanager/AudioManagerPulse.cpp new file mode 100644 index 0000000..514e6ab --- /dev/null +++ b/src/tabcontrollers/audiomanager/AudioManagerPulse.cpp @@ -0,0 +1,153 @@ +#include +#include "AudioManagerPulse.h" +#include "AudioManagerPulse_internal.h" +#include "../AudioTabController.h" + +// Used to get the compiler to shut up about C4100: unreferenced formal +// parameter. The cast is to get GCC to shut up about it. +#define UNREFERENCED_PARAMETER( P ) static_cast( ( P ) ) + +// application namespace +namespace advsettings +{ +AudioManagerPulse::~AudioManagerPulse() +{ + restorePulseAudioState(); +} + +void AudioManagerPulse::init( AudioTabController* controller ) +{ + m_controller = controller; + + initializePulseAudio(); +} + +void AudioManagerPulse::setPlaybackDevice( const std::string& id, bool notify ) +{ + setPlaybackDeviceInternal( id ); + + if ( notify ) + { + m_controller->onNewPlaybackDevice(); + } +} + +std::string AudioManagerPulse::getPlaybackDevName() +{ + return getCurrentDefaultPlaybackDeviceName(); +} + +std::string AudioManagerPulse::getPlaybackDevId() +{ + return getCurrentDefaultPlaybackDeviceId(); +} + +void AudioManagerPulse::setMirrorDevice( const std::string& id, bool notify ) +{ + // noop + UNREFERENCED_PARAMETER( id ); + UNREFERENCED_PARAMETER( notify ); +} + +bool AudioManagerPulse::isMirrorValid() +{ + return false; +} + +std::string AudioManagerPulse::getMirrorDevName() +{ + return "dummy"; +} + +std::string AudioManagerPulse::getMirrorDevId() +{ + return "dummy"; +} + +float AudioManagerPulse::getMirrorVolume() +{ + return 0; +} + +bool AudioManagerPulse::setMirrorVolume( float value ) +{ + UNREFERENCED_PARAMETER( value ); + return false; +} + +bool AudioManagerPulse::getMirrorMuted() +{ + return true; +} + +bool AudioManagerPulse::setMirrorMuted( bool value ) +{ + UNREFERENCED_PARAMETER( value ); + return false; +} + +bool AudioManagerPulse::isMicValid() +{ + return isMicrophoneValid(); +} + +void AudioManagerPulse::setMicDevice( const std::string& id, bool notify ) +{ + setMicrophoneDevice( id ); + + if ( notify ) + { + m_controller->onNewRecordingDevice(); + } +} + +std::string AudioManagerPulse::getMicDevName() +{ + return getCurrentDefaultRecordingDeviceName(); +} + +std::string AudioManagerPulse::getMicDevId() +{ + return getCurrentDefaultRecordingDeviceId(); +} + +float AudioManagerPulse::getMicVolume() +{ + return getMicrophoneVolume(); +} + +bool AudioManagerPulse::setMicVolume( float value ) +{ + if ( value > 1.0f ) + { + value = 1.0f; + } + else if ( value < 0.0f ) + { + value = 0.0f; + } + + return setMicrophoneVolume( value ); +} + +bool AudioManagerPulse::getMicMuted() +{ + return getMicrophoneMuted(); +} + +bool AudioManagerPulse::setMicMuted( bool value ) +{ + return setMicMuteState( value ); +} + +std::vector AudioManagerPulse::getRecordingDevices() +{ + return returnRecordingDevices(); +} + +std::vector AudioManagerPulse::getPlaybackDevices() +{ + return returnPlaybackDevices(); +} + +} // namespace advsettings diff --git a/src/tabcontrollers/audiomanager/AudioManagerPulse.h b/src/tabcontrollers/audiomanager/AudioManagerPulse.h new file mode 100644 index 0000000..6583b14 --- /dev/null +++ b/src/tabcontrollers/audiomanager/AudioManagerPulse.h @@ -0,0 +1,46 @@ +#pragma once + +#include "AudioManager.h" +#include + +namespace advsettings +{ +class AudioManagerPulse : public AudioManager +{ +public: + virtual void init( AudioTabController* controller ) override; + ~AudioManagerPulse() override; + + virtual void setPlaybackDevice( const std::string& id, + bool notify = true ) override; + virtual std::string getPlaybackDevName() override; + virtual std::string getPlaybackDevId() override; + + virtual void setMirrorDevice( const std::string& id, + bool notify = true ) override; + virtual bool isMirrorValid() override; + virtual std::string getMirrorDevName() override; + virtual std::string getMirrorDevId() override; + virtual float getMirrorVolume() override; + virtual bool setMirrorVolume( float value ) override; + virtual bool getMirrorMuted() override; + virtual bool setMirrorMuted( bool value ) override; + + virtual bool isMicValid() override; + virtual void setMicDevice( const std::string& id, + bool notify = true ) override; + virtual std::string getMicDevName() override; + virtual std::string getMicDevId() override; + virtual float getMicVolume() override; + virtual bool setMicVolume( float value ) override; + virtual bool getMicMuted() override; + virtual bool setMicMuted( bool value ) override; + + virtual std::vector getRecordingDevices() override; + virtual std::vector getPlaybackDevices() override; + +private: + AudioTabController* m_controller; +}; + +} // namespace advsettings diff --git a/src/tabcontrollers/audiomanager/AudioManagerPulse_internal.h b/src/tabcontrollers/audiomanager/AudioManagerPulse_internal.h new file mode 100644 index 0000000..8272348 --- /dev/null +++ b/src/tabcontrollers/audiomanager/AudioManagerPulse_internal.h @@ -0,0 +1,649 @@ +#pragma once +#include +#include +#include +#include "AudioManager.h" + +// Used to get the compiler to shut up about C4100: unreferenced formal +// parameter. The cast is to get GCC to shut up about it. +#define UNREFERENCED_PARAMETER( P ) static_cast( ( P ) ) + +namespace advsettings +{ +enum class PulseAudioIsLastMeaning +{ + Error, + RealDevice, + PreviousDeviceWasLastReal, +}; + +enum class PulseAudioLoopControl +{ + Stop, + Run, +}; + +struct +{ + pa_mainloop* mainLoop; + pa_mainloop_api* api; + pa_context* context; +} pulseAudioPointers; + +struct +{ + std::string defaultSinkOutputDeviceId; + std::string defaultSourceInputDeviceId; + + std::string originalDefaultOutputDeviceId; + std::string originalDefaultInputDeviceId; + + float originalDefaultOutputDeviceVolume; + float originalDefaultInputDeviceVolume; + + std::vector sinkOutputDevices; + std::vector sourceInputDevices; + + pa_sink_info currentDefaultSinkInfo; + pa_source_info currentDefaultSourceInfo; +} pulseAudioData; + +static PulseAudioLoopControl loopControl = PulseAudioLoopControl::Run; + +void customPulseLoop() +{ + while ( loopControl == PulseAudioLoopControl::Run ) + { + constexpr auto noReturnValue = nullptr; + constexpr auto blockForEvents = 1; + pa_mainloop_iterate( + pulseAudioPointers.mainLoop, blockForEvents, noReturnValue ); + } + + loopControl = PulseAudioLoopControl::Run; +} + +// Error function +void dumpPulseAudioState() +{ + LOG( ERROR ) << "____"; + + LOG( ERROR ) << "Dumping PulseAudio state: "; + LOG( ERROR ) << "mainLoop: " << pulseAudioPointers.mainLoop; + LOG( ERROR ) << "api: " << pulseAudioPointers.api; + LOG( ERROR ) << "context: " << pulseAudioPointers.context; + + LOG( ERROR ) << ""; + + LOG( ERROR ) << "Data:"; + LOG( ERROR ) << "\tdefaultSinkOutputDeviceId: " + << pulseAudioData.defaultSinkOutputDeviceId; + LOG( ERROR ) << "\tdefaultSourceInputDeviceId: " + << pulseAudioData.defaultSourceInputDeviceId; + + LOG( ERROR ) << ""; + + LOG( ERROR ) << "\tcurrentDefaultSinkInfo name: " + << pulseAudioData.currentDefaultSinkInfo.name; + LOG( ERROR ) << "\tcurrentDefaultSourceInfo name: " + << pulseAudioData.currentDefaultSourceInfo.name; + + LOG( ERROR ) << ""; + + LOG( ERROR ) << "sinkOutputDevices: "; + LOG_IF( pulseAudioData.sinkOutputDevices.size() == 0, ERROR ) + << "\tOutput devices size zero."; + for ( const auto& device : pulseAudioData.sinkOutputDevices ) + { + LOG( ERROR ) << "\tDevice Name: " << device.name(); + LOG( ERROR ) << "\tDevice Id: " << device.id(); + } + + LOG( ERROR ) << ""; + + LOG( ERROR ) << "sourceInputDevices: "; + LOG_IF( pulseAudioData.sourceInputDevices.size() == 0, ERROR ) + << "\tInput devices size zero."; + for ( const auto& device : pulseAudioData.sourceInputDevices ) + { + LOG( ERROR ) << "\tDevice Name: " << device.name(); + LOG( ERROR ) << "\tDevice Id: " << device.id(); + } + + LOG( ERROR ) << "____"; +} + +PulseAudioIsLastMeaning getIsLastMeaning( const int isLast ) noexcept +{ + if ( isLast < 0 ) + { + LOG( ERROR ) << "Error in isLast."; + dumpPulseAudioState(); + return PulseAudioIsLastMeaning::Error; + } + + if ( isLast > 0 ) + { + return PulseAudioIsLastMeaning::PreviousDeviceWasLastReal; + } + + return PulseAudioIsLastMeaning::RealDevice; +} + +std::string getDeviceName( pa_proplist* p ) +{ + if ( !p ) + { + LOG( ERROR ) << "proplist not valid."; + } + + constexpr auto deviceDescription = "device.description"; + if ( !pa_proplist_contains( p, deviceDescription ) ) + { + LOG( ERROR ) << "proplist does not contain '" << deviceDescription + << "'."; + return "ERROR"; + } + + std::string s; + s.assign( pa_proplist_gets( p, deviceDescription ) ); + + return s; +} + +template void deviceCallback( const T* i, const int isLast ) +{ + static_assert( + std::is_same::value + || std::is_same::value, + "Function should only be used with pa_source_info or pa_sink_info." ); + + const auto deviceState = getIsLastMeaning( isLast ); + if ( deviceState == PulseAudioIsLastMeaning::PreviousDeviceWasLastReal ) + { + loopControl = PulseAudioLoopControl::Stop; + return; + } + else if ( deviceState == PulseAudioIsLastMeaning::Error ) + { + LOG( ERROR ) << "Error in deviceCallback function."; + dumpPulseAudioState(); + loopControl = PulseAudioLoopControl::Stop; + return; + } + + if constexpr ( std::is_same::value ) + { + if ( i->name == pulseAudioData.defaultSourceInputDeviceId ) + { + pulseAudioData.currentDefaultSourceInfo = *i; + } + + LOG( DEBUG ) << "Adding device to input: '" << i->name << "', '" + << getDeviceName( i->proplist ) << "'."; + pulseAudioData.sourceInputDevices.push_back( + AudioDevice( i->name, getDeviceName( i->proplist ) ) ); + } + + else if constexpr ( std::is_same::value ) + { + if ( i->name == pulseAudioData.defaultSinkOutputDeviceId ) + { + pulseAudioData.currentDefaultSinkInfo = *i; + } + + LOG( DEBUG ) << "Adding device to output: '" << i->name << "', '" + << getDeviceName( i->proplist ) << "'."; + pulseAudioData.sinkOutputDevices.push_back( + AudioDevice( i->name, getDeviceName( i->proplist ) ) ); + } +} + +void setInputDevicesCallback( pa_context* c, + const pa_source_info* i, + int isLast, + void* userdata ) +{ + UNREFERENCED_PARAMETER( userdata ); + UNREFERENCED_PARAMETER( c ); + + deviceCallback( i, isLast ); +} + +void setOutputDevicesCallback( pa_context* c, + const pa_sink_info* i, + int isLast, + void* userdata ) +{ + UNREFERENCED_PARAMETER( userdata ); + UNREFERENCED_PARAMETER( c ); + + deviceCallback( i, isLast ); +} + +void getDefaultDevicesCallback( pa_context* c, + const pa_server_info* i, + void* userdata ) +{ + UNREFERENCED_PARAMETER( c ); + UNREFERENCED_PARAMETER( userdata ); + + if ( !i ) + { + LOG( ERROR ) << "i == 0"; + pulseAudioData.defaultSinkOutputDeviceId = "DDO:ERROR"; + pulseAudioData.defaultSourceInputDeviceId = "DDI:ERROR"; + return; + } + + // Copy because we don't know how long the pa_server_info* lives for + pulseAudioData.defaultSinkOutputDeviceId.assign( i->default_sink_name ); + pulseAudioData.defaultSourceInputDeviceId.assign( i->default_source_name ); + + loopControl = PulseAudioLoopControl::Stop; + + LOG( DEBUG ) << "getDefaultDevicesCallback done with sink output device: '" + << pulseAudioData.defaultSinkOutputDeviceId + << "' and source input '" + << pulseAudioData.defaultSourceInputDeviceId << "'."; +} + +void stateCallbackFunction( pa_context* c, void* userdata ) +{ + UNREFERENCED_PARAMETER( c ); + UNREFERENCED_PARAMETER( userdata ); + + switch ( pa_context_get_state( c ) ) + { + case PA_CONTEXT_TERMINATED: + LOG( ERROR ) << "PA_CONTEXT_TERMINATED in stateCallbackFunction"; + dumpPulseAudioState(); + return; + case PA_CONTEXT_CONNECTING: + LOG( DEBUG ) << "PA_CONTEXT_CONNECTING"; + return; + case PA_CONTEXT_AUTHORIZING: + LOG( DEBUG ) << "PA_CONTEXT_AUTHORIZING"; + return; + case PA_CONTEXT_SETTING_NAME: + LOG( DEBUG ) << "PA_CONTEXT_SETTING_NAME"; + return; + case PA_CONTEXT_UNCONNECTED: + LOG( DEBUG ) << "PA_CONTEXT_UNCONNECTED"; + return; + case PA_CONTEXT_FAILED: + LOG( DEBUG ) << "PA_CONTEXT_FAILED"; + return; + + case PA_CONTEXT_READY: + LOG( DEBUG ) << "PA_CONTEXT_READY"; + loopControl = PulseAudioLoopControl::Stop; + return; + } +} + +void updateAllPulseData() +{ + constexpr auto noCustomUserdata = nullptr; + + pa_context_get_server_info( pulseAudioPointers.context, + getDefaultDevicesCallback, + noCustomUserdata ); + customPulseLoop(); + + pulseAudioData.sinkOutputDevices.clear(); + pa_context_get_sink_info_list( pulseAudioPointers.context, + setOutputDevicesCallback, + noCustomUserdata ); + customPulseLoop(); + + pulseAudioData.sourceInputDevices.clear(); + pa_context_get_source_info_list( + pulseAudioPointers.context, setInputDevicesCallback, noCustomUserdata ); + customPulseLoop(); + + LOG( DEBUG ) << "updateAllPulseData done."; +} + +void successCallback( pa_context* c, int success, void* successVariable ) +{ + UNREFERENCED_PARAMETER( c ); + + if ( successVariable != nullptr ) + { + *static_cast( successVariable ) = success; + } + + if ( !success ) + { + LOG( ERROR ) << "Non successful callback operation."; + dumpPulseAudioState(); + } + + loopControl = PulseAudioLoopControl::Stop; +} + +void setPlaybackDeviceInternal( const std::string& id ) +{ + updateAllPulseData(); + + auto success = false; + pa_context_set_default_sink( + pulseAudioPointers.context, id.c_str(), successCallback, &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "setPlaybackDeviceInternal failed to set default sink " + "for device '" + << id << "'."; + } + + LOG( DEBUG ) << "setPlaybackDeviceInternal done with id: " << id; +} + +std::string getCurrentDefaultPlaybackDeviceName() +{ + updateAllPulseData(); + + for ( const auto& dev : pulseAudioData.sinkOutputDevices ) + { + if ( dev.id() == pulseAudioData.defaultSinkOutputDeviceId ) + { + LOG( DEBUG ) << "getCurrentDefaultPlaybackDeviceName done with " + << dev.name(); + return dev.name(); + } + } + LOG( ERROR ) << "Unable to find default playback device."; + + return "ERROR"; +} + +std::string getCurrentDefaultPlaybackDeviceId() +{ + updateAllPulseData(); + + LOG( DEBUG ) << "getCurrentDefaultPlaybackDeviceId done with " + << pulseAudioData.defaultSinkOutputDeviceId; + + return pulseAudioData.defaultSinkOutputDeviceId; +} + +std::string getCurrentDefaultRecordingDeviceName() +{ + updateAllPulseData(); + + for ( const auto& dev : pulseAudioData.sourceInputDevices ) + { + if ( dev.id() == pulseAudioData.defaultSourceInputDeviceId ) + { + LOG( DEBUG ) << "getCurrentDefaultRecordingDeviceName done with: " + << dev.name(); + return dev.name(); + } + } + LOG( ERROR ) << "Unable to find default playback device."; + + return "ERROR"; +} + +std::string getCurrentDefaultRecordingDeviceId() +{ + updateAllPulseData(); + + LOG( DEBUG ) << "getCurrentDefaultRecordingDeviceId done with " + << pulseAudioData.defaultSourceInputDeviceId; + + return pulseAudioData.defaultSourceInputDeviceId; +} + +std::vector returnRecordingDevices() +{ + updateAllPulseData(); + + return pulseAudioData.sourceInputDevices; +} + +std::vector returnPlaybackDevices() +{ + updateAllPulseData(); + + return pulseAudioData.sinkOutputDevices; +} + +bool isMicrophoneValid() +{ + updateAllPulseData(); + + const auto valid = pulseAudioData.defaultSourceInputDeviceId != ""; + + return valid; +} + +float getMicrophoneVolume() +{ + updateAllPulseData(); + + const auto linearVolume = pa_sw_volume_to_linear( + pa_cvolume_avg( &pulseAudioData.currentDefaultSourceInfo.volume ) ); + + return static_cast( linearVolume ); +} + +bool getMicrophoneMuted() +{ + updateAllPulseData(); + + return pulseAudioData.currentDefaultSourceInfo.mute; +} + +void sourceOutputCallback( pa_context* c, + const pa_source_output_info* i, + int isLast, + void* success ) +{ + const auto deviceState = getIsLastMeaning( isLast ); + if ( deviceState == PulseAudioIsLastMeaning::PreviousDeviceWasLastReal ) + { + loopControl = PulseAudioLoopControl::Stop; + return; + } + else if ( deviceState == PulseAudioIsLastMeaning::Error ) + { + LOG( ERROR ) << "Error in sourceOutputCallback function."; + dumpPulseAudioState(); + return; + } + + const auto sourceOutputIndex = i->index; + const auto sourceIndex = pulseAudioData.currentDefaultSourceInfo.index; + + LOG( DEBUG ) << "Attempting to move sourceOutputIndex: '" + << sourceOutputIndex << "' to sourceIndex '" << sourceIndex + << "' with source output name " << i->name << "."; + + pa_context_move_source_output_by_index( + c, sourceOutputIndex, sourceIndex, successCallback, &success ); +} + +void setMicrophoneDevice( const std::string& id ) +{ + LOG( DEBUG ) << "setMicrophoneDevice called with 'id': " << id; + + updateAllPulseData(); + + auto success = false; + pa_context_set_default_source( + pulseAudioPointers.context, id.c_str(), successCallback, &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "Error setting microphone device for '" << id << "'."; + } + + updateAllPulseData(); + + pa_context_get_source_output_info_list( + pulseAudioPointers.context, sourceOutputCallback, &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "Error in moving source outputs to new source."; + } + + updateAllPulseData(); + + LOG( DEBUG ) << "setMicrophoneDevice done."; +} + +bool setPlaybackVolume( const float volume ) +{ + LOG( DEBUG ) << "setPlaybackVolume called with 'volume': " << volume; + + updateAllPulseData(); + + auto pulseVolume = pulseAudioData.currentDefaultSinkInfo.volume; + const auto vol = pa_sw_volume_from_linear( static_cast( volume ) ); + + pa_cvolume_set( &pulseVolume, pulseVolume.channels, vol ); + + auto success = false; + pa_context_set_sink_volume_by_name( + pulseAudioPointers.context, + pulseAudioData.defaultSinkOutputDeviceId.c_str(), + &pulseVolume, + successCallback, + &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "setPlaybackVolume failed to set volume '" << volume + << "' for device '" + << pulseAudioData.defaultSinkOutputDeviceId << "'."; + } + + LOG( DEBUG ) << "setPlaybackVolume done with 'success': " << success; + + return success; +} + +bool setMicrophoneVolume( const float volume ) +{ + LOG( DEBUG ) << "setMicrophoneVolume called with 'volume': " << volume; + + updateAllPulseData(); + + auto pulseVolume = pulseAudioData.currentDefaultSourceInfo.volume; + const auto vol = pa_sw_volume_from_linear( static_cast( volume ) ); + + pa_cvolume_set( &pulseVolume, pulseVolume.channels, vol ); + + auto success = false; + pa_context_set_source_volume_by_name( + pulseAudioPointers.context, + pulseAudioData.defaultSourceInputDeviceId.c_str(), + &pulseVolume, + successCallback, + &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "seMicrophoneVolume failed to set volume '" << volume + << "' for device '" + << pulseAudioData.defaultSourceInputDeviceId << "'."; + } + + LOG( DEBUG ) << "setMicrophoneVolume done with 'success': " << success; + + return success; +} + +bool setMicMuteState( const bool muted ) +{ + LOG( DEBUG ) << "setMicMuteState called with 'muted': " << muted; + bool success = false; + + pa_context_set_source_mute_by_name( + pulseAudioPointers.context, + pulseAudioData.defaultSourceInputDeviceId.c_str(), + muted, + successCallback, + &success ); + + customPulseLoop(); + + if ( !success ) + { + LOG( ERROR ) << "setMicMuteState failed to set muted '" << muted + << "' for device '" + << pulseAudioData.defaultSourceInputDeviceId << "'."; + } + + LOG( DEBUG ) << "setMicMuteState done with 'success': " << success; + + return success; +} + +void restorePulseAudioState() +{ + LOG( DEBUG ) << "restorePulseAudioState called."; + + setPlaybackDeviceInternal( pulseAudioData.originalDefaultOutputDeviceId ); + setPlaybackVolume( pulseAudioData.originalDefaultOutputDeviceVolume ); + + setMicrophoneDevice( pulseAudioData.originalDefaultInputDeviceId ); + setMicrophoneVolume( pulseAudioData.originalDefaultInputDeviceVolume ); + + LOG( DEBUG ) << "restorePulseAudioState done."; +} + +void initializePulseAudio() +{ + LOG( DEBUG ) << "initializePulseAudio called."; + + pulseAudioPointers.mainLoop = pa_mainloop_new(); + + pulseAudioPointers.api = pa_mainloop_get_api( pulseAudioPointers.mainLoop ); + + pulseAudioPointers.context + = pa_context_new( pulseAudioPointers.api, "openvr-advanced-settings" ); + + constexpr auto noCustomUserdata = nullptr; + pa_context_set_state_callback( + pulseAudioPointers.context, stateCallbackFunction, noCustomUserdata ); + + constexpr auto useDefaultServer = nullptr; + constexpr auto useDefaultSpawnApi = nullptr; + pa_context_connect( pulseAudioPointers.context, + useDefaultServer, + PA_CONTEXT_NOFLAGS, + useDefaultSpawnApi ); + customPulseLoop(); + + pulseAudioData.originalDefaultInputDeviceId + = getCurrentDefaultRecordingDeviceId(); + + pulseAudioData.originalDefaultInputDeviceVolume + = static_cast( pa_sw_volume_to_linear( pa_cvolume_avg( + &pulseAudioData.currentDefaultSourceInfo.volume ) ) ); + + pulseAudioData.originalDefaultOutputDeviceId + = getCurrentDefaultPlaybackDeviceId(); + + pulseAudioData.originalDefaultOutputDeviceVolume + = static_cast( pa_sw_volume_to_linear( + pa_cvolume_avg( &pulseAudioData.currentDefaultSinkInfo.volume ) ) ); + + LOG( DEBUG ) << "initializePulseAudio finished."; +} +} // namespace advsettings