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