diff --git a/packages/ghaf-audio-control/src/CMakeLists.txt b/packages/ghaf-audio-control/src/CMakeLists.txt index 6b37a5b..7563911 100644 --- a/packages/ghaf-audio-control/src/CMakeLists.txt +++ b/packages/ghaf-audio-control/src/CMakeLists.txt @@ -17,8 +17,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) #set(CMAKE_CXX_FLAGS_DEBUG -fsanitize=thread) #add_definitions(-DLIBCXX_HARDENING_MODE=debug) -#add_compile_options(-fsanitize=address -fsanitize-recover=all) -#add_link_options(-fsanitize=address -fsanitize-recover=all) +# add_compile_options(-fsanitize=address -fsanitize-recover=all) +# add_link_options(-fsanitize=address -fsanitize-recover=all) add_subdirectory(app) add_subdirectory(lib) diff --git a/packages/ghaf-audio-control/src/app/main.cpp b/packages/ghaf-audio-control/src/app/main.cpp index 655c8a1..9f5711a 100644 --- a/packages/ghaf-audio-control/src/app/main.cpp +++ b/packages/ghaf-audio-control/src/app/main.cpp @@ -7,9 +7,10 @@ #include #include -#include #include +#include + #include using namespace ghaf::AudioControl; diff --git a/packages/ghaf-audio-control/src/lib/CMakeLists.txt b/packages/ghaf-audio-control/src/lib/CMakeLists.txt index b026d91..ad04e9d 100644 --- a/packages/ghaf-audio-control/src/lib/CMakeLists.txt +++ b/packages/ghaf-audio-control/src/lib/CMakeLists.txt @@ -14,43 +14,59 @@ add_library(${LIBRARY_NAME}) target_sources(${LIBRARY_NAME} PRIVATE + src/AppList.cpp + src/AudioControl.cpp + src/Backends/PulseAudio/AudioControlBackend.cpp src/Backends/PulseAudio/GeneralDevide.cpp src/Backends/PulseAudio/Helpers.cpp src/Backends/PulseAudio/Sink.cpp + src/Backends/PulseAudio/SinkInput.cpp src/Backends/PulseAudio/Source.cpp + src/Backends/PulseAudio/SourceOutput.cpp src/Backends/PulseAudio/Volume.cpp + src/models/AppVmModel.cpp + src/models/DeviceModel.cpp + src/utils/ConnectionContainer.cpp src/utils/Logger.cpp - src/AppList.cpp - src/AppListRow.cpp - src/AudioControl.cpp + src/widgets/AppVmWidget.cpp + src/widgets/DeviceWidget.cpp + src/widgets/SinkWidget.cpp PUBLIC FILE_SET public_headers TYPE HEADERS BASE_DIRS include FILES + include/GhafAudioControl/AppList.hpp + include/GhafAudioControl/AudioControl.hpp + include/GhafAudioControl/Backends/PulseAudio/AudioControlBackend.hpp include/GhafAudioControl/Backends/PulseAudio/GeneralDevice.hpp include/GhafAudioControl/Backends/PulseAudio/Helpers.hpp include/GhafAudioControl/Backends/PulseAudio/Sink.hpp + include/GhafAudioControl/Backends/PulseAudio/SinkInput.hpp include/GhafAudioControl/Backends/PulseAudio/Source.hpp + include/GhafAudioControl/Backends/PulseAudio/SourceOutput.hpp include/GhafAudioControl/Backends/PulseAudio/Volume.hpp + include/GhafAudioControl/IAudioControlBackend.hpp + include/GhafAudioControl/Volume.hpp + + include/GhafAudioControl/models/AppVmModel.hpp + include/GhafAudioControl/models/DeviceModel.hpp + include/GhafAudioControl/utils/ConnectionContainer.hpp include/GhafAudioControl/utils/Logger.hpp include/GhafAudioControl/utils/RaiiWrap.hpp include/GhafAudioControl/utils/ScopeExit.hpp - - include/GhafAudioControl/AppList.hpp - include/GhafAudioControl/AppListRow.hpp - include/GhafAudioControl/AudioControl.hpp - include/GhafAudioControl/IAudioControlBackend.hpp - - include/GhafAudioControl/Volume.hpp + + include/GhafAudioControl/widgets/AppVmWidget.hpp + include/GhafAudioControl/widgets/DeviceWidget.hpp + include/GhafAudioControl/widgets/SinkWidget.hpp ) target_link_libraries( diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppList.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppList.hpp index 8c22661..74cf4a5 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppList.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppList.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -22,24 +22,15 @@ class AppList final : public Gtk::Box public: AppList(); - void addApp(AppRaw::AppIdType id, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source); - void updateApp(AppRaw::AppIdType id, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source); - void removeApp(AppRaw::AppIdType id); - void removeAllApps(); - -private: - void doUpdateApp(size_t modelIndex, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source); + void addDevice(IAudioControlBackend::ISinkInput::Ptr device); - [[nodiscard]] Gtk::Widget* createWidgetsForApp(const Glib::RefPtr& appVmPtr); + void removeAllApps(); private: Gtk::ListBox m_listBox; - Gtk::Separator m_separator; - Gtk::Stack m_stack; - Glib::RefPtr> m_appsModel; - std::map>> m_appsBindings; - sigc::connection m_connection; + Glib::RefPtr> m_appsModel; + std::map>> m_appsBindings; }; } // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppListRow.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppListRow.hpp deleted file mode 100644 index 020bd12..0000000 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AppListRow.hpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include -#include -#include - -#include -#include - -namespace ghaf::AudioControl -{ - -class AppRaw final : public Glib::Object -{ -public: - using AppIdType = uint32_t; - -private: - AppRaw(AppIdType id, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source); - -public: - static Glib::RefPtr create(AppIdType id, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source); - - static int compare(const Glib::RefPtr& a, const Glib::RefPtr& b); - - [[nodiscard]] AppIdType getId() const noexcept - { - return m_id; - } - - void updateSink(IAudioControlBackend::ISink::Ptr sink); - void updateSource(IAudioControlBackend::ISource::Ptr source); - - [[nodiscard]] auto getIsEnabledProperty() const - { - return m_isEnabled.get_proxy(); - } - - [[nodiscard]] auto getHasSinkProperty() const - { - return m_hasSink.get_proxy(); - } - - [[nodiscard]] auto getHasSourceProperty() const - { - return m_hasSource.get_proxy(); - } - - [[nodiscard]] auto getAppNameProperty() const - { - return m_appName.get_proxy(); - } - - [[nodiscard]] auto getIconUrlProperty() const - { - return m_iconUrl.get_proxy(); - } - - [[nodiscard]] auto getSoundEnabledProperty() - { - return m_isSoundEnabled.get_proxy(); - } - - [[nodiscard]] auto getSoundVolumeProperty() - { - return m_soundVolume.get_proxy(); - } - - [[nodiscard]] auto getMicroEnabledProperty() - { - return m_isMicroEnabled.get_proxy(); - } - - [[nodiscard]] auto getMicroVolumeProperty() - { - return m_microVolume.get_proxy(); - } - -private: - void onSoundEnabledChange(); - void onSoundVolumeChange(); - - void onMicroEnabledChange(); - void onMicroVolumeChange(); - -private: - const AppIdType m_id; - IAudioControlBackend::ISink::Ptr m_sink; - IAudioControlBackend::ISource::Ptr m_source; - - Glib::Property m_isEnabled{*this, "m_isEnabled", false}; - - Glib::Property m_hasSink{*this, "m_hasSink", false}; - Glib::Property m_hasSource{*this, "m_hasSource", false}; - - Glib::Property_ReadOnly m_iconUrl{*this, "m_iconUrl", "/usr/share/pixmaps/ubuntu-logo.svg"}; - Glib::Property m_appName; - - Glib::Property m_isSoundEnabled{*this, "m_isSoundEnabled", false}; - Glib::Property m_soundVolume{*this, "m_soundVolume", 0}; - - Glib::Property m_isMicroEnabled{*this, "m_isMicroEnabled", false}; - Glib::Property m_microVolume{*this, "m_microVolume", 0}; - - ConnectionContainer m_connections; -}; - -} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AudioControl.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AudioControl.hpp index 9950c8c..8ac4c0a 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AudioControl.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/AudioControl.hpp @@ -21,9 +21,7 @@ namespace ghaf::AudioControl class AudioControl final : public Gtk::Box { public: - static inline std::string ModuleName = "ghaf-audio-control"; - - AudioControl(std::unique_ptr backend); + explicit AudioControl(std::unique_ptr backend); ~AudioControl() override = default; AudioControl(AudioControl&) = delete; @@ -36,8 +34,15 @@ class AudioControl final : public Gtk::Box void init(); void onPulseSinksChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sinks::IndexT extIndex, IAudioControlBackend::Sinks::PtrT sink); - void onPulseSourcesChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sinks::IndexT extIndex, + + void onPulseSourcesChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sources::IndexT extIndex, IAudioControlBackend::Sources::PtrT source); + + void onPulseSinkInputsChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::SinkInputs::IndexT extIndex, + IAudioControlBackend::SinkInputs::PtrT sinkInput); + + void onPulseSourcesOutputsChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::SourceOutputs::IndexT extIndex, + IAudioControlBackend::SourceOutputs::PtrT sourceOutput); void onPulseError(std::string_view error); private: diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/AudioControlBackend.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/AudioControlBackend.hpp index 6f5fde0..3a9d40e 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/AudioControlBackend.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/AudioControlBackend.hpp @@ -39,6 +39,16 @@ class AudioControlBackend final : public IAudioControlBackend return m_sources.onChange(); } + SinkInputs::OnChangeSignal onSinkInputsChanged() const override + { + return m_sinkInputs.onChange(); + } + + SourceOutputs::OnChangeSignal onSourceOutputsChanged() const override + { + return m_sourceOutputs.onChange(); + } + OnErrorSignal onError() const override { return m_onError; @@ -49,6 +59,8 @@ class AudioControlBackend final : public IAudioControlBackend static void contextStateCallback(pa_context* context, void* data); static void sinkInfoCallback(pa_context* context, const pa_sink_info* info, int eol, void* data); static void sourceInfoCallback(pa_context* context, const pa_source_info* info, int eol, void* data); + static void sinkInputInfoCallback(pa_context* context, const pa_sink_input_info* info, int eol, void* data); + static void sourceOutputInfoCallback(pa_context* context, const pa_source_output_info* info, int eol, void* data); static void serverInfoCallback(pa_context* context, const pa_server_info* info, void* data); static void cardInfoCallback(pa_context* context, const pa_card_info* info, int eol, void* data); @@ -58,9 +70,17 @@ class AudioControlBackend final : public IAudioControlBackend void onSourceInfo(const pa_source_info& info); void deleteSource(Sources::IndexT index); + void onSinkInputInfo(const pa_sink_input_info& info); + void deleteSinkInput(SinkInputs::IndexT index); + + void onSourceOutputInfo(const pa_source_output_info& info); + void deleteSourceOutput(SourceOutputs::IndexT index); + private: Sinks m_sinks; Sources m_sources; + SinkInputs m_sinkInputs; + SourceOutputs m_sourceOutputs; OnErrorSignal m_onError; diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/GeneralDevice.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/GeneralDevice.hpp index 8f71a94..a34daa9 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/GeneralDevice.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/GeneralDevice.hpp @@ -26,6 +26,8 @@ class GeneralDeviceImpl final public: GeneralDeviceImpl(const pa_sink_info& info, pa_context& context); GeneralDeviceImpl(const pa_source_info& info, pa_context& context); + GeneralDeviceImpl(const pa_sink_input_info& info, pa_context& context); + GeneralDeviceImpl(const pa_source_output_info& info, pa_context& context); bool operator==(const GeneralDeviceImpl& other) const { @@ -39,8 +41,8 @@ class GeneralDeviceImpl final [[nodiscard]] uint32_t getCardIndex() const noexcept; + [[nodiscard]] bool isDeleted() const noexcept; [[nodiscard]] bool isEnabled() const noexcept; - [[nodiscard]] bool isMuted() const; [[nodiscard]] Volume getVolume() const; @@ -49,12 +51,6 @@ class GeneralDeviceImpl final [[nodiscard]] pa_cvolume getPulseChannelVolume() const noexcept; [[nodiscard]] std::string getName() const; - - [[nodiscard]] sigc::signal onChanged() const - { - return m_onChanged; - } - [[nodiscard]] std::string getDescription() const; [[nodiscard]] pa_context& getContext() const noexcept @@ -64,14 +60,23 @@ class GeneralDeviceImpl final void update(const pa_sink_info& info); void update(const pa_source_info& info); + void update(const pa_sink_input_info& info); + void update(const pa_source_output_info& info); + void update(const pa_card_info& info); + void markDeleted(); + [[nodiscard]] std::string toString() const; +private: + void deleteCheck(); + private: const uint32_t m_index; uint32_t m_cardIndex; + bool m_isDeleted = false; bool m_isEnabled = false; std::string m_name; std::string m_description; @@ -83,7 +88,6 @@ class GeneralDeviceImpl final bool m_isMuted; mutable std::mutex m_mutex; - sigc::signal m_onChanged; }; } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Sink.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Sink.hpp index 691c02f..903aa34 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Sink.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Sink.hpp @@ -21,6 +21,11 @@ class Sink final : public IAudioControlBackend::ISink bool operator==(const IDevice& other) const override; + Index getIndex() const override + { + return m_device.getIndex(); + } + std::string getName() const override { return m_device.getName(); @@ -65,10 +70,29 @@ class Sink final : public IAudioControlBackend::ISink void update(const pa_card_info& info) { m_device.update(info); + m_onUpdate(); + } + + void markDeleted(); + + OnUpdateSignal onUpdate() const override + { + return m_onUpdate; + } + + OnDeleteSignal onDelete() const override + { + return m_onDelete; } +private: + void deleteCheck(); + private: GeneralDeviceImpl m_device; + + OnUpdateSignal m_onUpdate; + OnDeleteSignal m_onDelete; }; } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SinkInput.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SinkInput.hpp new file mode 100644 index 0000000..a7e35c8 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SinkInput.hpp @@ -0,0 +1,89 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +namespace ghaf::AudioControl::Backend::PulseAudio +{ + +class SinkInput final : public IAudioControlBackend::ISinkInput +{ +public: + SinkInput(const pa_sink_input_info& info, pa_context& context); + + bool operator==(const IDevice& other) const override; + + Index getIndex() const override + { + return m_device.getIndex(); + } + + std::string getName() const override + { + return m_device.getName(); + } + + bool isEnabled() const override + { + return m_device.isEnabled(); + } + + bool isMuted() const override + { + return m_device.isMuted(); + } + + void setMuted(bool mute) override; + + Volume getVolume() const override + { + return m_device.getVolume(); + } + + void setVolume(Volume volume) override; + + uint32_t getCardIndex() const noexcept + { + return m_device.getCardIndex(); + } + + std::string toString() const override; + + std::string getDescription() const + { + return m_device.getDescription(); + } + + void update(const pa_sink_input_info& info); + + void markDeleted(); + + OnUpdateSignal onUpdate() const override + { + return m_onUpdate; + } + + OnDeleteSignal onDelete() const override + { + return m_onDelete; + } + +private: + void deleteCheck(); + +private: + GeneralDeviceImpl m_device; + + OnUpdateSignal m_onUpdate; + OnDeleteSignal m_onDelete; +}; + +} // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Source.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Source.hpp index f8c26d2..88eab63 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Source.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/Source.hpp @@ -21,6 +21,11 @@ class Source final : public IAudioControlBackend::ISource bool operator==(const IDevice& other) const override; + Index getIndex() const override + { + return m_device.getIndex(); + } + std::string getName() const override { return m_device.getName(); @@ -47,28 +52,33 @@ class Source final : public IAudioControlBackend::ISource std::string toString() const override; - std::string getDescription() const - { - return m_device.getDescription(); - } + std::string getDescription() const; - uint32_t getCardIndex() const noexcept - { - return m_device.getCardIndex(); - } + uint32_t getCardIndex() const; + + void update(const pa_source_info& info); + void update(const pa_card_info& info); + + void markDeleted(); - void update(const pa_source_info& info) + OnUpdateSignal onUpdate() const override { - m_device.update(info); + return m_onUpdate; } - void update(const pa_card_info& info) + OnDeleteSignal onDelete() const override { - m_device.update(info); + return m_onDelete; } +private: + void deleteCheck(); + private: GeneralDeviceImpl m_device; + + OnUpdateSignal m_onUpdate; + OnDeleteSignal m_onDelete; }; } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SourceOutput.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SourceOutput.hpp new file mode 100644 index 0000000..c515983 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/Backends/PulseAudio/SourceOutput.hpp @@ -0,0 +1,93 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +namespace ghaf::AudioControl::Backend::PulseAudio +{ + +class SourceOutput final : public IAudioControlBackend::ISourceOutput +{ +public: + SourceOutput(const pa_source_output_info& info, pa_context& context); + + bool operator==(const IDevice& other) const override; + + Index getIndex() const override + { + return m_device.getIndex(); + } + + std::string getName() const override + { + return m_device.getName(); + } + + bool isEnabled() const override + { + return m_device.isEnabled(); + } + + bool isMuted() const override + { + return m_device.isMuted(); + } + + void setMuted(bool mute) override; + + Volume getVolume() const override + { + return m_device.getVolume(); + } + + void setVolume(Volume volume) override; + + std::string toString() const override; + + std::string getDescription() const + { + return m_device.getDescription(); + } + + uint32_t getCardIndex() const noexcept + { + return m_device.getCardIndex(); + } + + void update(const pa_source_output_info& info) + { + m_device.update(info); + m_onUpdate(); + } + + void markDeleted(); + + OnUpdateSignal onUpdate() const override + { + return m_onUpdate; + } + + OnDeleteSignal onDelete() const override + { + return m_onDelete; + } + +private: + void deleteCheck(); + +private: + GeneralDeviceImpl m_device; + + OnUpdateSignal m_onUpdate; + OnDeleteSignal m_onDelete; +}; + +} // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/IAudioControlBackend.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/IAudioControlBackend.hpp index 71c9f96..0978895 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/IAudioControlBackend.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/IAudioControlBackend.hpp @@ -17,6 +17,8 @@ namespace ghaf::AudioControl { +using Index = uint64_t; + class IAudioControlBackend { public: @@ -27,7 +29,7 @@ class IAudioControlBackend Delete }; - template + template class SignalMap final { public: @@ -43,7 +45,7 @@ class IAudioControlBackend SignalMap() = default; - void add(const Index& key, PtrT&& data) + void add(Index key, PtrT&& data) { auto result = m_map.emplace(key, std::forward(data)); auto iter = result.first; @@ -51,7 +53,7 @@ class IAudioControlBackend m_onChange(EventType::Add, iter->first, iter->second); } - [[nodiscard]] std::optional findByKey(const Index& key) + [[nodiscard]] std::optional findByKey(Index key) { if (auto iter = m_map.find(key); iter != m_map.end()) return iter; @@ -80,8 +82,11 @@ class IAudioControlBackend m_onChange(EventType::Update, iter->first, ptr); } - void remove(Iter iter) + void remove(Iter iter, const std::function& deleteFunction) { + PtrT ptr = iter->second; + deleteFunction(*ptr); + const auto key = iter->first; std::ignore = m_map.erase(key); @@ -102,11 +107,17 @@ class IAudioControlBackend { public: using Ptr = std::shared_ptr; + using IntexT = Index; + + using OnUpdateSignal = sigc::signal; + using OnDeleteSignal = sigc::signal; virtual ~IDevice() = default; virtual bool operator==(const IDevice& other) const = 0; + [[nodiscard]] virtual Index getIndex() const = 0; + [[nodiscard]] virtual std::string getName() const = 0; [[nodiscard]] virtual bool isEnabled() const = 0; @@ -118,6 +129,9 @@ class IAudioControlBackend virtual void setVolume(Volume volume) = 0; [[nodiscard]] virtual std::string toString() const = 0; + + [[nodiscard]] virtual OnUpdateSignal onUpdate() const = 0; + [[nodiscard]] virtual OnDeleteSignal onDelete() const = 0; }; class ISink : public IDevice @@ -128,9 +142,19 @@ class IAudioControlBackend { }; - using Index = uint64_t; - using Sinks = SignalMap; - using Sources = SignalMap; + class ISinkInput : public IDevice + { + }; + + class ISourceOutput : public IDevice + { + }; + + using Sinks = SignalMap; + using Sources = SignalMap; + using SinkInputs = SignalMap; + using SourceOutputs = SignalMap; + using OnErrorSignal = sigc::signal; virtual ~IAudioControlBackend() = default; @@ -140,6 +164,9 @@ class IAudioControlBackend [[nodiscard]] virtual Sinks::OnChangeSignal onSinksChanged() const = 0; [[nodiscard]] virtual Sources::OnChangeSignal onSourcesChanged() const = 0; + [[nodiscard]] virtual SinkInputs::OnChangeSignal onSinkInputsChanged() const = 0; + [[nodiscard]] virtual SourceOutputs::OnChangeSignal onSourceOutputsChanged() const = 0; + [[nodiscard]] virtual OnErrorSignal onError() const = 0; }; diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/AppVmModel.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/AppVmModel.hpp new file mode 100644 index 0000000..b88661d --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/AppVmModel.hpp @@ -0,0 +1,67 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +#include + +namespace ghaf::AudioControl +{ + +class AppVmModel final : public Glib::Object +{ +public: + using AppIdType = std::string; + +private: + explicit AppVmModel(AppIdType id); + +public: + static Glib::RefPtr create(AppIdType id); + + static int compare(const Glib::RefPtr& a, const Glib::RefPtr& b); + + void addSinkInput(IAudioControlBackend::ISinkInput::Ptr sinkInput); + void deleteSinkInput(const IAudioControlBackend::ISinkInput::Ptr& sinkInput); + + [[nodiscard]] Glib::RefPtr> getDeviceModels() noexcept + { + return m_devices; + } + + [[nodiscard]] auto getIsEnabledProperty() const + { + return m_isEnabled.get_proxy(); + } + + [[nodiscard]] auto getAppNameProperty() const + { + return m_appName.get_proxy(); + } + + [[nodiscard]] auto getIconUrlProperty() const + { + return m_iconUrl.get_proxy(); + } + +private: + Glib::RefPtr> m_devices; + std::map m_deviceIndex; + + Glib::Property m_appName; + Glib::Property_ReadOnly m_iconUrl{*this, "m_iconUrl", "/usr/share/pixmaps/ubuntu-logo.svg"}; + + Glib::Property m_isEnabled{*this, "m_isEnabled", false}; + + std::map m_deviceConnections; +}; + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/DeviceModel.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/DeviceModel.hpp new file mode 100644 index 0000000..5aaf8ac --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/models/DeviceModel.hpp @@ -0,0 +1,85 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include +#include + +namespace ghaf::AudioControl +{ + +class DeviceModel : public Glib::Object +{ +public: + using Ptr = Glib::RefPtr; + +public: + DeviceModel(IAudioControlBackend::IDevice::Ptr device); + + static Glib::RefPtr create(IAudioControlBackend::IDevice::Ptr device); + + static int compare(const Glib::RefPtr& a, const Glib::RefPtr& b); + + virtual void updateDevice(); + + [[nodiscard]] virtual Index getIndex() const + { + return m_device->getIndex(); + } + + [[nodiscard]] virtual Glib::PropertyProxy_ReadOnly getIsEnabledProperty() const + { + return m_isEnabled.get_proxy(); + } + + [[nodiscard]] virtual Glib::PropertyProxy_ReadOnly getHasDeviceProperty() const + { + return m_hasDevice.get_proxy(); + } + + [[nodiscard]] auto getNameProperty() const + { + return m_name.get_proxy(); + } + + [[nodiscard]] auto getIconUrlProperty() const + { + return m_iconUrl.get_proxy(); + } + + [[nodiscard]] virtual Glib::PropertyProxy getSoundEnabledProperty() + { + return m_isSoundEnabled.get_proxy(); + } + + [[nodiscard]] virtual Glib::PropertyProxy getSoundVolumeProperty() + { + return m_soundVolume.get_proxy(); + } + +private: + void onSoundEnabledChange(); + void onSoundVolumeChange(); + +private: + IAudioControlBackend::IDevice::Ptr m_device; + + Glib::Property m_isEnabled; + Glib::Property m_hasDevice{*this, "m_hasDevice", false}; + + Glib::Property m_name{*this, "m_name", "Undefined"}; + Glib::Property_ReadOnly m_iconUrl{*this, "m_iconUrl", "/usr/share/pixmaps/ubuntu-logo.svg"}; + + Glib::Property m_isSoundEnabled{*this, "m_isSoundEnabled", false}; + Glib::Property m_soundVolume{*this, "m_soundVolume", 0}; + + ConnectionContainer m_connections; +}; +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/utils/ConnectionContainer.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/utils/ConnectionContainer.hpp index 7ffebf8..9e5d22d 100644 --- a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/utils/ConnectionContainer.hpp +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/utils/ConnectionContainer.hpp @@ -27,7 +27,7 @@ class ConnectionContainer final void add(sigc::connection&& connection) { - m_connections.emplace_back(std::move(connection)); + std::ignore = m_connections.emplace_back(std::move(connection)); } ScopeExit blockGuarded(); diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/AppVmWidget.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/AppVmWidget.hpp new file mode 100644 index 0000000..d138936 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/AppVmWidget.hpp @@ -0,0 +1,37 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace ghaf::AudioControl +{ + +class AppVmWidget final : public Gtk::Box +{ +public: + explicit AppVmWidget(Glib::RefPtr model); + + void reveal(bool reveal = true); + +private: + Glib::RefPtr m_model; + + Gtk::ListBox m_listBox; + Gtk::Button m_appNameButton; + Gtk::Revealer m_revealer; + + sigc::connection m_connection; +}; + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/DeviceWidget.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/DeviceWidget.hpp new file mode 100644 index 0000000..0f30c2c --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/DeviceWidget.hpp @@ -0,0 +1,40 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace ghaf::AudioControl +{ + +class DeviceWidget : public Gtk::Box +{ +public: + explicit DeviceWidget(DeviceModel::Ptr model); + +private: + DeviceModel::Ptr m_model; + + Gtk::Label* m_nameLabel; + Gtk::Switch* m_switch; + Gtk::Scale* m_scale; + + std::vector> m_bindings; +}; + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/SinkWidget.hpp b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/SinkWidget.hpp new file mode 100644 index 0000000..2b008a6 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/include/GhafAudioControl/widgets/SinkWidget.hpp @@ -0,0 +1,19 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace ghaf::AudioControl +{ + +class SinkWidget : public DeviceModel +{ +public: + SinkWidget() = default; +}; + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/AppList.cpp b/packages/ghaf-audio-control/src/lib/src/AppList.cpp index 4fb4740..da5ea2f 100644 --- a/packages/ghaf-audio-control/src/lib/src/AppList.cpp +++ b/packages/ghaf-audio-control/src/lib/src/AppList.cpp @@ -4,13 +4,16 @@ */ #include +#include #include #include +#include #include #include #include +#include #include #include @@ -22,180 +25,72 @@ namespace ghaf::AudioControl namespace { -const auto ScaleSize = 200; -const auto ScaleOrientation = Gtk::Orientation::ORIENTATION_HORIZONTAL; -const auto ScaleInitialValue = 0.0; -const auto ScaleLowerLimit = 0.0; -const auto ScaleUpperLimit = 100.0; - -const auto SwitchSize = 10; -const auto IconSize = 25; -const auto NameLabelSize = 75; -const auto BoxPlaces = 4; - -std::optional GetIndexByAppId(const Glib::RefPtr>& model, AppRaw::AppIdType id) noexcept +std::optional GetIndexByAppId(const Glib::RefPtr>& model, const AppVmModel::AppIdType& id) noexcept { for (size_t index = 0; index < model->get_n_items(); ++index) - if (id == model->get_item(index)->getId()) + if (id == model->get_item(index)->getAppNameProperty().get_value()) return index; return std::nullopt; } -Gtk::Scale* MakeScaleWidget() +std::string GetAppNameFromSinkInput(const IAudioControlBackend::ISinkInput::Ptr& device) { - auto adjustment = Gtk::Adjustment::create(ScaleInitialValue, ScaleLowerLimit, ScaleUpperLimit); + return "Other"; +} + +Gtk::Widget* CreateWidgetsForApp(const Glib::RefPtr& appVmModelPtr) +{ + if (!appVmModelPtr) + { + Logger::error("AppList: appVmModelPtr is nullptr"); + return nullptr; + } - auto* scale = Gtk::make_managed(std::move(adjustment), ScaleOrientation); - scale->set_size_request(ScaleSize); - scale->set_digits(0); + auto appVmModel = Glib::RefPtr::cast_dynamic(appVmModelPtr); + if (!appVmModel) + { + Logger::error("AppList: appVmModel is not an AppVmModel"); + return nullptr; + } - return scale; + return Gtk::make_managed(appVmModel); } } // namespace AppList::AppList() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) - , m_separator(Gtk::Orientation::ORIENTATION_VERTICAL) - , m_appsModel(Gio::ListStore::create()) + , m_appsModel(Gio::ListStore::create()) { - m_listBox.bind_model(m_appsModel, sigc::mem_fun(*this, &AppList::createWidgetsForApp)); + m_listBox.bind_model(m_appsModel, &CreateWidgetsForApp); m_listBox.set_can_focus(false); m_listBox.set_selection_mode(Gtk::SelectionMode::SELECTION_SINGLE); - m_connection = m_listBox.signal_row_selected().connect( - [this](const Gtk::ListBoxRow* row) - { - if (row == nullptr) - { - Logger::error("AppList: ListBoxRow is nullptr"); - return; - } - - m_stack.set_visible_child(std::to_string(m_appsModel->get_item(row->get_index())->getId())); - }); - - m_stack.set_transition_type(Gtk::StackTransitionType::STACK_TRANSITION_TYPE_SLIDE_UP_DOWN); - - pack_start(m_listBox, Gtk::PACK_SHRINK); - pack_start(m_separator, Gtk::PACK_SHRINK); - pack_start(m_stack, Gtk::PACK_EXPAND_WIDGET); + pack_start(m_listBox, Gtk::PACK_EXPAND_WIDGET); } -void AppList::addApp(AppRaw::AppIdType id, IAudioControlBackend::IDevice::Ptr sink, IAudioControlBackend::IDevice::Ptr source) +void AppList::addDevice(IAudioControlBackend::ISinkInput::Ptr device) { - if (const auto index = GetIndexByAppId(m_appsModel, id)) - doUpdateApp(*index, std::move(sink), std::move(source)); - else - m_appsModel->append(AppRaw::create(id, std::move(sink), std::move(source))); -} + const std::string appName = GetAppNameFromSinkInput(device); -void AppList::updateApp(AppRaw::AppIdType id, IAudioControlBackend::IDevice::Ptr sink, IAudioControlBackend::IDevice::Ptr source) -{ - if (const auto index = GetIndexByAppId(m_appsModel, id)) - doUpdateApp(*index, std::move(sink), std::move(source)); + if (const auto index = GetIndexByAppId(m_appsModel, appName)) + m_appsModel->get_item(*index)->addSinkInput(std::move(device)); else - Logger::error(std::format("AppList::updateApp: no app with id: {}", id)); -} - -void AppList::removeAllApps() -{ - m_appsBindings.clear(); - m_appsModel->remove_all(); -} - -void AppList::doUpdateApp(size_t modelIndex, IAudioControlBackend::IDevice::Ptr sink, IAudioControlBackend::IDevice::Ptr source) -{ - auto item = m_appsModel->get_item(modelIndex); - - if (sink) - item->updateSink(std::move(sink)); + { + Logger::error(std::format("AppList::addDevice: add new app with name: {}", appName)); - if (source) - item->updateSource(std::move(source)); -} + auto appVmModel = AppVmModel::create(appName); + appVmModel->addSinkInput(std::move(device)); -void AppList::removeApp(AppRaw::AppIdType id) -{ - if (const auto index = GetIndexByAppId(m_appsModel, id)) - { - std::ignore = m_appsBindings.erase(id); - m_appsModel->remove(*index); + m_appsModel->append(appVmModel); } - else - Logger::error(std::format("AppList::deleteApp: no app with id: {}", id)); } -Gtk::Widget* AppList::createWidgetsForApp(const Glib::RefPtr& appVmPtr) +void AppList::removeAllApps() { - if (!appVmPtr) - { - Logger::error("AppList: appVmPtr is nullptr"); - return nullptr; - } - - auto appVm = Glib::RefPtr::cast_dynamic(appVmPtr); - if (!appVm) - { - Logger::error("AppList: appVm is not an AppRaw"); - return nullptr; - } - - auto* icon = Gtk::make_managed("/usr/share/pixmaps/ubuntu-logo.svg"); - auto* nameLabel = Gtk::make_managed(); - auto* soundEnableSwitch = Gtk::make_managed(); - auto* soundScale = MakeScaleWidget(); - auto* microEnableSwitch = Gtk::make_managed(); - auto* microScale = MakeScaleWidget(); - - icon->set_size_request(IconSize); - nameLabel->set_size_request(NameLabelSize); - nameLabel->set_halign(Gtk::Align::ALIGN_START); - nameLabel->set_can_focus(false); - nameLabel->set_selectable(false); - nameLabel->set_focus_on_click(false); - - soundEnableSwitch->set_size_request(SwitchSize); - microEnableSwitch->set_size_request(SwitchSize); - - const auto bind = [](const auto& appProp, const auto& widgetProp, bool readonly = false) - { - auto flag = Glib::BindingFlags::BINDING_SYNC_CREATE; - if (!readonly) - flag |= Glib::BindingFlags::BINDING_BIDIRECTIONAL; - - return Glib::Binding::bind_property(appProp, widgetProp, flag); - }; - - m_appsBindings[appVm->getId()] = {bind(appVm->getHasSinkProperty(), soundEnableSwitch->property_sensitive(), true), - bind(appVm->getHasSinkProperty(), soundScale->property_sensitive(), true), - bind(appVm->getHasSourceProperty(), microEnableSwitch->property_sensitive(), true), - bind(appVm->getHasSourceProperty(), microScale->property_sensitive(), true), - - bind(appVm->getAppNameProperty(), nameLabel->property_label(), true), - bind(appVm->getSoundEnabledProperty(), soundEnableSwitch->property_state()), - bind(appVm->getSoundVolumeProperty(), soundScale->get_adjustment()->property_value()), - bind(appVm->getMicroEnabledProperty(), microEnableSwitch->property_state()), - bind(appVm->getMicroVolumeProperty(), microScale->get_adjustment()->property_value())}; - - auto* stackGrid = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, BoxPlaces); - stackGrid->add(*soundEnableSwitch); - stackGrid->add(*soundScale); - - // Disable micro control for now - // stackGrid->add(*microEnableSwitch); - // stackGrid->add(*microScale); - - stackGrid->set_valign(Gtk::ALIGN_START); - - auto* sidebarGrid = Gtk::make_managed(); - sidebarGrid->add(*icon); - sidebarGrid->add(*nameLabel); - - m_stack.add(*stackGrid, std::to_string(appVm->getId())); - - return sidebarGrid; + m_appsBindings.clear(); + m_appsModel->remove_all(); } } // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/AppListRow.cpp b/packages/ghaf-audio-control/src/lib/src/AppListRow.cpp deleted file mode 100644 index f552796..0000000 --- a/packages/ghaf-audio-control/src/lib/src/AppListRow.cpp +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -#include - -#include - -#include - -namespace ghaf::AudioControl -{ - -namespace -{ - -template -void LazySet(Glib::Property& property, const typename Glib::Property::PropertyType& newValue) -{ - if (property == newValue) - return; - - property = newValue; -} - -} // namespace - -AppRaw::AppRaw(AppIdType id, IAudioControlBackend::ISink::Ptr sink, IAudioControlBackend::ISource::Ptr source) - : Glib::ObjectBase(typeid(AppRaw)) - , m_id(id) - , m_appName(*this, "m_appName", "Undefined") - , m_connections{m_isSoundEnabled.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &AppRaw::onSoundEnabledChange)), - m_isMicroEnabled.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &AppRaw::onMicroEnabledChange)), - - m_soundVolume.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &AppRaw::onSoundVolumeChange)), - m_microVolume.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &AppRaw::onMicroVolumeChange))} -{ - updateSink(std::move(sink)); - updateSource(std::move(source)); -} - -Glib::RefPtr AppRaw::create(AppIdType id, IAudioControlBackend::IDevice::Ptr sink, IAudioControlBackend::IDevice::Ptr source) -{ - return Glib::RefPtr(new AppRaw(id, std::move(sink), std::move(source))); -} - -int AppRaw::compare(const Glib::RefPtr& a, const Glib::RefPtr& b) -{ - if (!a || !b) - return 0; - - return a->m_appName.get_value().compare(b->m_appName.get_value()); -} - -void AppRaw::updateSink(IAudioControlBackend::ISink::Ptr sink) -{ - { - const auto scopeExit = m_connections.blockGuarded(); - - LazySet(m_isSoundEnabled, sink ? !sink->isMuted() : false); - LazySet(m_soundVolume, sink ? sink->getVolume().getPercents() : 0); - } - - m_isEnabled = (sink ? sink->isEnabled() : false) || (m_source ? m_source->isEnabled() : false); - - if (sink) - LazySet(m_appName, "sink: " + sink->getName() + (m_isEnabled ? " enabled" : " disabled")); - - m_sink = std::move(sink); - m_hasSink = m_sink != nullptr; -} - -void AppRaw::updateSource(IAudioControlBackend::ISource::Ptr source) -{ - { - const auto scopeExit = m_connections.blockGuarded(); - - LazySet(m_isMicroEnabled, source ? !source->isMuted() : false); - LazySet(m_microVolume, source ? source->getVolume().getPercents() : 0); - } - - if (source) - LazySet(m_appName, "source: " + source->getName() + (m_isEnabled ? " enabled" : " disabled")); - - m_isEnabled = (m_sink ? m_sink->isEnabled() : false) || (source ? source->isEnabled() : false); - m_source = std::move(source); - m_hasSource = m_source != nullptr; -} - -void AppRaw::onSoundEnabledChange() -{ - const auto isEnabled = m_isSoundEnabled.get_value(); - - Logger::debug(std::format("SoundEnabled has changed to: {0}", isEnabled)); - - if (m_sink) - m_sink->setMuted(!isEnabled); -} - -void AppRaw::onSoundVolumeChange() -{ - Logger::debug(std::format("SoundVolume has changed to: {0}", m_soundVolume.get_value())); - - if (m_sink) - m_sink->setVolume(Volume::fromPercents(m_soundVolume.get_value())); -} - -void AppRaw::onMicroEnabledChange() -{ - const auto isEnabled = m_isMicroEnabled.get_value(); - - Logger::debug(std::format("MicroEnabled has changed to: {0}", isEnabled)); - - if (m_source) - m_source->setMuted(!isEnabled); - // if (!isEnabled) - // lazySet(m_microVolume, 0); -} - -void AppRaw::onMicroVolumeChange() -{ - Logger::debug(std::format("MicroVolume has changed to: {0}", m_microVolume.get_value())); - - if (m_source) - m_source->setVolume(Volume::fromPercents(m_microVolume.get_value())); -} - -} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/AudioControl.cpp b/packages/ghaf-audio-control/src/lib/src/AudioControl.cpp index ed5c834..7cab247 100644 --- a/packages/ghaf-audio-control/src/lib/src/AudioControl.cpp +++ b/packages/ghaf-audio-control/src/lib/src/AudioControl.cpp @@ -21,6 +21,49 @@ namespace ghaf::AudioControl { +template +void OnPulseDeviceChanged(IAudioControlBackend::EventType eventType, IndexT index, DevicePtrT device, AppList& appList) +{ + std::string deviceType; + + if constexpr (std::is_same()) + { + deviceType = "sink"; + } + else if constexpr (std::is_same()) + { + deviceType = "source"; + } + else if constexpr (std::is_same()) + { + deviceType = "sinkInput"; + } + else if constexpr (std::is_same()) + { + deviceType = "sourceOutput"; + } + else + { + static_assert(true, "Unknow type"); + } + + switch (eventType) + { + case IAudioControlBackend::EventType::Add: + Logger::debug(std::format("OnPulseDeviceChanged: ADD {}: {}", deviceType, device->toString())); + appList.addDevice(std::move(device)); + break; + + case IAudioControlBackend::EventType::Update: + Logger::debug(std::format("OnPulseDeviceChanged: UPDATE {}: {}", deviceType, device->toString())); + break; + + case IAudioControlBackend::EventType::Delete: + Logger::debug(std::format("OnPulseDeviceChanged: DELETE {} with index: {}", deviceType, index)); + break; + } +} + AudioControl::AudioControl(std::unique_ptr backend) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) , m_audioControl(std::move(backend)) @@ -34,8 +77,10 @@ void AudioControl::init() { pack_start(m_appList); - m_connections.add(m_audioControl->onSinksChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSinksChanged))); - m_connections.add(m_audioControl->onSourcesChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSourcesChanged))); + // m_connections.add(m_audioControl->onSinksChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSinksChanged))); + // m_connections.add(m_audioControl->onSourcesChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSourcesChanged))); + m_connections.add(m_audioControl->onSinkInputsChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSinkInputsChanged))); + // m_connections.add(m_audioControl->onSourceOutputsChanged().connect(sigc::mem_fun(*this, &AudioControl::onPulseSourcesOutputsChanged))); m_connections.add(m_audioControl->onError().connect(sigc::mem_fun(*this, &AudioControl::onPulseError))); show_all_children(); @@ -48,53 +93,32 @@ void AudioControl::init() } } -void AudioControl::onPulseSinksChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sinks::IndexT index, +void AudioControl::onPulseSinksChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sinks::IndexT extIndex, IAudioControlBackend::Sinks::PtrT sink) { - switch (eventType) - { - case IAudioControlBackend::EventType::Add: - Logger::debug(std::format("onPulseSinksChanged: ADD sink: {}", sink->toString())); - m_appList.addApp(index, std::move(sink), nullptr); - break; - - case IAudioControlBackend::EventType::Update: - Logger::debug(std::format("onPulseSinksChanged: UPDATE sink: {}", sink->toString())); - m_appList.updateApp(index, std::move(sink), nullptr); - break; - - case IAudioControlBackend::EventType::Delete: - Logger::debug(std::format("onPulseSinksChanged: DELETE sink with index: {}", index)); - m_appList.removeApp(index); - break; - } - + OnPulseDeviceChanged(eventType, extIndex, std::move(sink), m_appList); show_all_children(); } -void AudioControl::onPulseSourcesChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sources::IndexT index, +void AudioControl::onPulseSourcesChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::Sources::IndexT extIndex, IAudioControlBackend::Sources::PtrT source) { - // Disable sources for now - return; - - switch (eventType) - { - case IAudioControlBackend::EventType::Add: - Logger::debug(std::format("onPulseSourcesChanged: ADD source: {}", source->toString())); - m_appList.addApp(index, nullptr, std::move(source)); - break; + OnPulseDeviceChanged(eventType, extIndex, std::move(source), m_appList); + show_all_children(); +} - case IAudioControlBackend::EventType::Update: - Logger::debug(std::format("onPulseSourcesChanged: UPDATE source: {}", source->toString())); - m_appList.updateApp(index, nullptr, std::move(source)); - break; +void AudioControl::onPulseSinkInputsChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::SinkInputs::IndexT extIndex, + IAudioControlBackend::SinkInputs::PtrT sinkInput) +{ + OnPulseDeviceChanged(eventType, extIndex, std::move(sinkInput), m_appList); + show_all_children(); +} - case IAudioControlBackend::EventType::Delete: - Logger::debug(std::format("onPulseSourcesChanged: DELETE source with index: {}", index)); - m_appList.removeApp(index); - break; - } +void AudioControl::onPulseSourcesOutputsChanged(IAudioControlBackend::EventType eventType, IAudioControlBackend::SourceOutputs::IndexT extIndex, + IAudioControlBackend::SourceOutputs::PtrT sourceOutput) +{ + OnPulseDeviceChanged(eventType, extIndex, std::move(sourceOutput), m_appList); + show_all_children(); } void AudioControl::onPulseError(std::string_view error) diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp index 356b01e..e7949c9 100644 --- a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include @@ -26,6 +28,37 @@ namespace ghaf::AudioControl::Backend::PulseAudio namespace { +template +void OnPulseDeviceInfo(const InfoT& info, IAudioControlBackend::SignalMap& map, pa_context& context) +{ + const Index index = info.index; + + if (auto deviceIt = map.findByKey(index)) + { + const IDeviceT& device = *deviceIt.value()->second; + + Logger::debug(std::format("Updating... Before: {}", device.toString())); + map.update(*deviceIt, [&info](IDeviceT& device) { dynamic_cast(device).update(info); }); + Logger::debug(std::format("Updating... After: {}", device.toString())); + } + else + { + map.add(index, std::make_shared(info, context)); + } +} + +template +void DeletePulseDevice(IAudioControlBackend::SignalMap& map, IndexT index) +{ + if (auto deviceIt = map.findByKey(index)) + { + Logger::debug(std::format("AudioControlBackend::DeletePulseDevice: delete device with id: {}", index)); + map.remove(*deviceIt, [](IDeviceT& device) { dynamic_cast(device).markDeleted(); }); + } + else + Logger::error(std::format("AudioControlBackend::DeletePulseDevice: no device with id: {}", index)); +} + std::string ToString(const pa_card_port_info& port) { return std::format("Port. Name: {}, descript: {}, available: {}", port.name, port.description, port.available); @@ -124,52 +157,42 @@ void AudioControlBackend::stop() void AudioControlBackend::onSinkInfo(const pa_sink_info& info) { - const Index index = info.index; - - if (auto sinkIt = m_sinks.findByKey(index)) - { - const ISink& sink = *sinkIt.value()->second; - - Logger::debug(std::format("Updating... Before: {}", sink.toString())); - m_sinks.update(*sinkIt, [&info](ISink& sink) { dynamic_cast(sink).update(info); }); - Logger::debug(std::format("Updating... After: {}", sink.toString())); - } - else - { - m_sinks.add(index, std::make_shared(info, *m_context->get())); - } + OnPulseDeviceInfo(info, m_sinks, *m_context->get()); } void AudioControlBackend::deleteSink(Sinks::IndexT index) { - if (auto sinkIt = m_sinks.findByKey(index)) - { - Logger::debug(std::format("AudioControlBackend::deleteSink: delete sink with id: {}", index)); - m_sinks.remove(*sinkIt); - } - else - Logger::error(std::format("AudioControlBackend::deleteSink: no sink with id: {}", index)); + DeletePulseDevice(m_sinks, index); } void AudioControlBackend::onSourceInfo(const pa_source_info& info) { - const Index index = info.index; - - if (auto sourceIt = m_sources.findByKey(index)) - m_sources.update(*sourceIt, [&info](ISource& source) { dynamic_cast(source).update(info); }); - else - m_sources.add(index, std::make_shared(info, *m_context->get())); + OnPulseDeviceInfo(info, m_sources, *m_context->get()); } void AudioControlBackend::deleteSource(Sources::IndexT index) { - if (auto sourceIt = m_sources.findByKey(index)) - { - Logger::debug(std::format("AudioControlBackend::deleteSource: delete source with id: {}", index)); - m_sources.remove(*sourceIt); - } - else - Logger::error(std::format("AudioControlBackend::deleteSource: no source with id: {}", index)); + DeletePulseDevice(m_sources, index); +} + +void AudioControlBackend::onSinkInputInfo(const pa_sink_input_info& info) +{ + OnPulseDeviceInfo(info, m_sinkInputs, *m_context->get()); +} + +void AudioControlBackend::deleteSinkInput(SinkInputs::IndexT index) +{ + DeletePulseDevice(m_sinkInputs, index); +} + +void AudioControlBackend::onSourceOutputInfo(const pa_source_output_info& info) +{ + OnPulseDeviceInfo(info, m_sourceOutputs, *m_context->get()); +} + +void AudioControlBackend::deleteSourceOutput(SourceOutputs::IndexT index) +{ + DeletePulseDevice(m_sourceOutputs, index); } void AudioControlBackend::subscribeCallback(pa_context* context, pa_subscription_event_type_t type, uint32_t index, void* data) @@ -193,7 +216,11 @@ void AudioControlBackend::subscribeCallback(pa_context* context, pa_subscription break; case pa_subscription_event_type::PA_SUBSCRIPTION_EVENT_SINK_INPUT: - ExecutePulseFunc(pa_context_get_sink_info_list, context, sinkInfoCallback, data); + if (needRemove) + self->deleteSinkInput(index); + else + ExecutePulseFunc(pa_context_get_sink_input_info, context, index, sinkInputInfoCallback, data); + break; case pa_subscription_event_type::PA_SUBSCRIPTION_EVENT_SOURCE: @@ -205,7 +232,11 @@ void AudioControlBackend::subscribeCallback(pa_context* context, pa_subscription break; case pa_subscription_event_type::PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: - ExecutePulseFunc(pa_context_get_source_info_list, context, sourceInfoCallback, data); + if (needRemove) + self->deleteSourceOutput(index); + else + ExecutePulseFunc(pa_context_get_source_output_info, context, index, sourceOutputInfoCallback, data); + break; case pa_subscription_event_type::PA_SUBSCRIPTION_EVENT_CARD: @@ -265,6 +296,22 @@ void AudioControlBackend::sourceInfoCallback(pa_context* context, const pa_sourc static_cast(data)->onSourceInfo(*info); } +void AudioControlBackend::sinkInputInfoCallback(pa_context* context, const pa_sink_input_info* info, int eol, void* data) +{ + if (!PulseCallbackCheck(context, eol, __FUNCTION__) || info == nullptr) + return; + + static_cast(data)->onSinkInputInfo(*info); +} + +void AudioControlBackend::sourceOutputInfoCallback(pa_context* context, const pa_source_output_info* info, int eol, void* data) +{ + if (!PulseCallbackCheck(context, eol, __FUNCTION__) || info == nullptr) + return; + + static_cast(data)->onSourceOutputInfo(*info); +} + void AudioControlBackend::serverInfoCallback(pa_context* context, const pa_server_info* info, void* data) { if (info == nullptr) @@ -276,6 +323,8 @@ void AudioControlBackend::serverInfoCallback(pa_context* context, const pa_serve ExecutePulseFunc(pa_context_get_sink_info_list, context, sinkInfoCallback, data); ExecutePulseFunc(pa_context_get_source_info_list, context, sourceInfoCallback, data); + ExecutePulseFunc(pa_context_get_sink_input_info_list, context, sinkInputInfoCallback, data); + ExecutePulseFunc(pa_context_get_source_output_info_list, context, sourceOutputInfoCallback, data); ExecutePulseFunc(pa_context_get_card_info_list, context, cardInfoCallback, data); } diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/GeneralDevide.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/GeneralDevide.cpp index 7076e51..a432605 100644 --- a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/GeneralDevide.cpp +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/GeneralDevide.cpp @@ -36,12 +36,40 @@ GeneralDeviceImpl::GeneralDeviceImpl(const pa_source_info& info, pa_context& con { } +GeneralDeviceImpl::GeneralDeviceImpl(const pa_sink_input_info& info, pa_context& context) + : m_index(info.index) + , m_cardIndex(0) + , m_name(info.name) + , m_context(context) + , m_channel_map(info.channel_map) + , m_volume(info.volume) + , m_isMuted(static_cast(info.mute)) +{ +} + +GeneralDeviceImpl::GeneralDeviceImpl(const pa_source_output_info& info, pa_context& context) + : m_index(info.index) + , m_cardIndex(0) + , m_name(info.name) + , m_context(context) + , m_channel_map(info.channel_map) + , m_volume(info.volume) + , m_isMuted(static_cast(info.mute)) +{ +} + [[nodiscard]] uint32_t GeneralDeviceImpl::getCardIndex() const noexcept { const std::lock_guard l{m_mutex}; return m_cardIndex; } +[[nodiscard]] bool GeneralDeviceImpl::isDeleted() const noexcept +{ + const std::lock_guard l{m_mutex}; + return m_isDeleted; +} + [[nodiscard]] bool GeneralDeviceImpl::isEnabled() const noexcept { const std::lock_guard l{m_mutex}; @@ -74,13 +102,7 @@ GeneralDeviceImpl::GeneralDeviceImpl(const pa_source_info& info, pa_context& con [[nodiscard]] std::string GeneralDeviceImpl::getName() const { const std::lock_guard l{m_mutex}; - return std::format("#{}. {}", m_index, m_description); -} - -[[nodiscard]] std::string GeneralDeviceImpl::toString() const -{ - const std::lock_guard l{m_mutex}; - return std::format("index: {}, name: {}, volume: {}, isMuted: {}, cardId: {}", m_index, m_description, m_volume.values[0], m_isMuted, m_cardIndex); + return m_name; } [[nodiscard]] std::string GeneralDeviceImpl::getDescription() const @@ -91,68 +113,95 @@ GeneralDeviceImpl::GeneralDeviceImpl(const pa_source_info& info, pa_context& con void GeneralDeviceImpl::update(const pa_sink_info& info) { - { - const std::lock_guard l{m_mutex}; - - m_cardIndex = info.card; - m_name = info.name; - m_description = info.description; - m_channel_map = info.channel_map; - m_volume = info.volume; - m_isMuted = static_cast(info.mute); - } + const std::lock_guard l{m_mutex}; - m_onChanged(); + m_cardIndex = info.card; + m_name = info.name; + m_description = info.description; + m_channel_map = info.channel_map; + m_volume = info.volume; + m_isMuted = static_cast(info.mute); } void GeneralDeviceImpl::update(const pa_source_info& info) { - { - const std::lock_guard l{m_mutex}; - - m_cardIndex = info.card; - m_name = info.name; - m_description = info.description; - m_channel_map = info.channel_map; - m_volume = info.volume; - m_isMuted = static_cast(info.mute); - } + const std::lock_guard l{m_mutex}; + + m_cardIndex = info.card; + m_name = info.name; + m_description = info.description; + m_channel_map = info.channel_map; + m_volume = info.volume; + m_isMuted = static_cast(info.mute); +} + +void GeneralDeviceImpl::update(const pa_sink_input_info& info) +{ + const std::lock_guard l{m_mutex}; - m_onChanged(); + m_name = info.name; + m_channel_map = info.channel_map; + m_volume = info.volume; + m_isMuted = static_cast(info.mute); +} + +void GeneralDeviceImpl::update(const pa_source_output_info& info) +{ + const std::lock_guard l{m_mutex}; + + m_name = info.name; + m_channel_map = info.channel_map; + m_volume = info.volume; + m_isMuted = static_cast(info.mute); } void GeneralDeviceImpl::update(const pa_card_info& info) { - { - const std::lock_guard l{m_mutex}; + const std::lock_guard l{m_mutex}; - m_isEnabled = true; + m_isEnabled = false; - if (m_cardIndex != info.index) - return; + if (m_cardIndex != info.index) + return; - for (size_t i = 0; i < info.n_ports; ++i) - { - pa_card_port_info const& port = *info.ports[i]; + for (size_t i = 0; i < info.n_ports; ++i) + { + pa_card_port_info const& port = *info.ports[i]; - if (m_description.ends_with(port.description)) + if (m_description.ends_with(port.description)) + { + switch (port.type) { - switch (port.type) - { - case pa_device_port_type::PA_DEVICE_PORT_TYPE_HDMI: - m_isEnabled = port.available == pa_port_available::PA_PORT_AVAILABLE_YES; - break; - - default: - m_isEnabled = true; - } + case pa_device_port_type::PA_DEVICE_PORT_TYPE_HDMI: + m_isEnabled = port.available == pa_port_available::PA_PORT_AVAILABLE_YES; + break; + default: + m_isEnabled = true; break; } + + break; } } +} - m_onChanged(); +void GeneralDeviceImpl::markDeleted() +{ + const std::lock_guard l{m_mutex}; + m_isDeleted = true; +} + +[[nodiscard]] std::string GeneralDeviceImpl::toString() const +{ + const std::lock_guard l{m_mutex}; + return std::format("index: {}, name: {}, volume: {}, isMuted: {}, cardId: {}, description: {}", + m_index, + m_name, + m_volume.values[0], + m_isMuted, + m_cardIndex, + m_description); } } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Sink.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Sink.cpp index 3825fbc..34071b3 100644 --- a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Sink.cpp +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Sink.cpp @@ -28,11 +28,14 @@ bool Sink::operator==(const IDevice& other) const void Sink::setMuted(bool mute) { + deleteCheck(); ExecutePulseFunc(pa_context_set_sink_mute_by_index, &m_device.getContext(), m_device.getIndex(), mute, nullptr, nullptr); } void Sink::setVolume(Volume volume) { + deleteCheck(); + pa_cvolume paChannelVolume; std::ignore = pa_cvolume_set(&paChannelVolume, m_device.getPulseChannelVolume().channels, ToPulseAudioVolume(volume)); @@ -44,4 +47,16 @@ std::string Sink::toString() const return std::format("PulseSink: [ {} ]", m_device.toString()); } +void Sink::markDeleted() +{ + m_device.markDeleted(); + m_onDelete(); +} + +void Sink::deleteCheck() +{ + if (m_device.isDeleted()) + throw std::logic_error{std::format("Using deleted device: {}", toString())}; +} + } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SinkInput.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SinkInput.cpp new file mode 100644 index 0000000..f4d16e3 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SinkInput.cpp @@ -0,0 +1,68 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include + +namespace ghaf::AudioControl::Backend::PulseAudio +{ + +SinkInput::SinkInput(const pa_sink_input_info& info, pa_context& context) + : m_device(info, context) +{ +} + +bool SinkInput::operator==(const IDevice& other) const +{ + if (const auto* otherSink = dynamic_cast(&other)) + return m_device == otherSink->m_device; + + return false; +} + +void SinkInput::setMuted(bool mute) +{ + deleteCheck(); + ExecutePulseFunc(pa_context_set_sink_input_mute, &m_device.getContext(), m_device.getIndex(), mute, nullptr, nullptr); +} + +void SinkInput::setVolume(Volume volume) +{ + deleteCheck(); + + pa_cvolume paChannelVolume; + std::ignore = pa_cvolume_set(&paChannelVolume, m_device.getPulseChannelVolume().channels, ToPulseAudioVolume(volume)); + + ExecutePulseFunc(pa_context_set_sink_input_volume, &m_device.getContext(), m_device.getIndex(), &paChannelVolume, nullptr, nullptr); +} + +std::string SinkInput::toString() const +{ + return std::format("PulseSinkInput: [ {} ]", m_device.toString()); +} + +void SinkInput::update(const pa_sink_input_info& info) +{ + m_device.update(info); + m_onUpdate(); +} + +void SinkInput::markDeleted() +{ + m_device.markDeleted(); + m_onDelete(); +} + +void SinkInput::deleteCheck() +{ + if (m_device.isDeleted()) + throw std::logic_error{std::format("Using deleted device: {}", toString())}; +} + +} // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Source.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Source.cpp index 187be21..3db877f 100644 --- a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Source.cpp +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/Source.cpp @@ -28,11 +28,14 @@ bool Source::operator==(const IDevice& other) const void Source::setMuted(bool mute) { - ExecutePulseFunc(pa_context_set_sink_mute_by_index, &m_device.getContext(), m_device.getIndex(), mute, nullptr, nullptr); + deleteCheck(); + ExecutePulseFunc(pa_context_set_source_mute_by_index, &m_device.getContext(), m_device.getIndex(), mute, nullptr, nullptr); } void Source::setVolume(Volume volume) { + deleteCheck(); + pa_cvolume paChannelVolume; std::ignore = pa_cvolume_set(&paChannelVolume, m_device.getPulseChannelVolume().channels, ToPulseAudioVolume(volume)); @@ -44,4 +47,37 @@ std::string Source::toString() const return std::format("PulseSource: [ {} ]", m_device.toString()); } +std::string Source::getDescription() const +{ + return m_device.getDescription(); +} + +void Source::markDeleted() +{ + m_device.markDeleted(); + m_onDelete(); +} + +void Source::deleteCheck() +{ + if (m_device.isDeleted()) + throw std::logic_error{std::format("Using deleted device: {}", toString())}; +} + +uint32_t Source::getCardIndex() const +{ + return m_device.getCardIndex(); +} + +void Source::update(const pa_source_info& info) +{ + m_device.update(info); +} + +void Source::update(const pa_card_info& info) +{ + m_device.update(info); + m_onUpdate(); +} + } // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SourceOutput.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SourceOutput.cpp new file mode 100644 index 0000000..fdf8f64 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/SourceOutput.cpp @@ -0,0 +1,62 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include + +namespace ghaf::AudioControl::Backend::PulseAudio +{ + +SourceOutput::SourceOutput(const pa_source_output_info& info, pa_context& context) + : m_device(info, context) +{ +} + +bool SourceOutput::operator==(const IDevice& other) const +{ + if (const auto* otherSource = dynamic_cast(&other)) + return m_device == otherSource->m_device; + + return false; +} + +void SourceOutput::setMuted(bool mute) +{ + deleteCheck(); + ExecutePulseFunc(pa_context_set_source_output_mute, &m_device.getContext(), m_device.getIndex(), mute, nullptr, nullptr); +} + +void SourceOutput::setVolume(Volume volume) +{ + deleteCheck(); + + pa_cvolume paChannelVolume; + std::ignore = pa_cvolume_set(&paChannelVolume, m_device.getPulseChannelVolume().channels, ToPulseAudioVolume(volume)); + + ExecutePulseFunc(pa_context_set_source_output_volume, &m_device.getContext(), m_device.getIndex(), &paChannelVolume, nullptr, nullptr); +} + +std::string SourceOutput::toString() const +{ + return std::format("PulseSourceOutput: [ {} ]", m_device.toString()); +} + +void SourceOutput::markDeleted() +{ + m_device.markDeleted(); + m_onDelete(); +} + +void SourceOutput::deleteCheck() +{ + if (m_device.isDeleted()) + throw std::logic_error{std::format("Using deleted device: {}", toString())}; +} + +} // namespace ghaf::AudioControl::Backend::PulseAudio diff --git a/packages/ghaf-audio-control/src/lib/src/models/AppVmModel.cpp b/packages/ghaf-audio-control/src/lib/src/models/AppVmModel.cpp new file mode 100644 index 0000000..659f657 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/models/AppVmModel.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +namespace ghaf::AudioControl +{ + +namespace +{ + +std::optional GetDeviceIndex(const Gio::ListStore& list, Index index) +{ + const guint size = list.get_n_items(); + + for (guint i = 0; i < size; i++) + { + if (list.get_item(i)->getIndex() == index) + return i; + } + + return std::nullopt; +} + +} // namespace + +AppVmModel::AppVmModel(AppIdType id) + : Glib::ObjectBase(typeid(AppVmModel)) + , m_devices(Gio::ListStore::create()) + , m_appName(*this, "m_appName", std::move(id)) +{ +} + +Glib::RefPtr AppVmModel::create(AppIdType id) +{ + return Glib::RefPtr(new AppVmModel(std::move(id))); +} + +int AppVmModel::compare(const Glib::RefPtr& a, const Glib::RefPtr& b) +{ + if (!a || !b) + return 0; + + return a->m_appName.get_value().compare(b->m_appName.get_value()); +} + +void AppVmModel::addSinkInput(IAudioControlBackend::ISinkInput::Ptr sinkInput) +{ + const Index deviceIndex = sinkInput->getIndex(); + + if (GetDeviceIndex(*m_devices.get(), deviceIndex)) + { + Logger::error("AppVmModel: ignore doubling"); + return; + } + + m_devices->append(DeviceModel::create(sinkInput)); + + m_deviceConnections[deviceIndex] = sinkInput->onDelete().connect( + [this, deviceIndex] + { + if (const auto index = GetDeviceIndex(*m_devices.get(), deviceIndex)) + m_devices->remove(*index); + else + Logger::error("AppVmModel::addSinkInput couldn't found sinkInput"); + + m_deviceConnections.erase(deviceIndex); + }); +} + +void AppVmModel::deleteSinkInput(const IAudioControlBackend::IDevice::Ptr& sinkInput) +{ +} + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/models/DeviceModel.cpp b/packages/ghaf-audio-control/src/lib/src/models/DeviceModel.cpp new file mode 100644 index 0000000..bfecd38 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/models/DeviceModel.cpp @@ -0,0 +1,90 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include + +#include + +namespace ghaf::AudioControl +{ + +namespace +{ + +template +void LazySet(Glib::Property& property, const typename Glib::Property::PropertyType& newValue) +{ + if (property == newValue) + return; + + property = newValue; +} + +auto Bind(const auto& appProp, const auto& widgetProp, bool readonly = false) +{ + auto flag = Glib::BindingFlags::BINDING_SYNC_CREATE; + if (!readonly) + flag |= Glib::BindingFlags::BINDING_BIDIRECTIONAL; + + return Glib::Binding::bind_property(appProp, widgetProp, flag); +} + +} // namespace + +DeviceModel::DeviceModel(IAudioControlBackend::IDevice::Ptr device) + : Glib::ObjectBase(typeid(DeviceModel)) + , m_device(std::move(device)) + , m_isEnabled{*this, "m_isEnabled", false} + , m_connections{m_isSoundEnabled.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &DeviceModel::onSoundEnabledChange)), + m_soundVolume.get_proxy().signal_changed().connect(sigc::mem_fun(*this, &DeviceModel::onSoundVolumeChange)), + m_device->onUpdate().connect([this] { updateDevice(); })} +{ + updateDevice(); +} + +Glib::RefPtr DeviceModel::create(IAudioControlBackend::IDevice::Ptr device) +{ + return Glib::RefPtr(new DeviceModel(std::move(device))); +} + +int DeviceModel::compare(const Glib::RefPtr& a, const Glib::RefPtr& b) +{ + if (!a || !b) + return 0; + + return 1; +} + +void DeviceModel::updateDevice() +{ + { + const auto scopeExit = m_connections.blockGuarded(); + + LazySet(m_isSoundEnabled, !m_device->isMuted()); + LazySet(m_soundVolume, m_device->getVolume().getPercents()); + } + + LazySet(m_name, m_device->getName()); +} + +void DeviceModel::onSoundEnabledChange() +{ + const auto isEnabled = m_isSoundEnabled.get_value(); + + Logger::debug(std::format("SoundEnabled has changed to: {0}", isEnabled)); + m_device->setMuted(!isEnabled); +} + +void DeviceModel::onSoundVolumeChange() +{ + Logger::debug(std::format("SoundVolume has changed to: {0}", m_soundVolume.get_value())); + m_device->setVolume(Volume::fromPercents(m_soundVolume.get_value())); +} + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/widgets/AppVmWidget.cpp b/packages/ghaf-audio-control/src/lib/src/widgets/AppVmWidget.cpp new file mode 100644 index 0000000..d5daa63 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/widgets/AppVmWidget.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +namespace ghaf::AudioControl +{ + +namespace +{ + +constexpr auto RevealerTransitionType = Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_DOWN; +constexpr auto RevealerAnimationTimeMs = 1000; + +Gtk::Widget* CreateDeviceWidget(const Glib::RefPtr& deviceModelPtr) +{ + if (!deviceModelPtr) + { + Logger::error("AppVmWidget: deviceModelPtr is nullptr"); + return nullptr; + } + + auto deviceModel = Glib::RefPtr::cast_dynamic(deviceModelPtr); + if (!deviceModel) + { + Logger::error("AppVmWidget: deviceModel is not an AppVmModel"); + return nullptr; + } + + return Gtk::make_managed(deviceModel); +} + +} // namespace + +AppVmWidget::AppVmWidget(Glib::RefPtr model) + : Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL) + , m_model(std::move(model)) + , m_appNameButton("AppVm: " + m_model->getAppNameProperty()) + , m_connection(m_appNameButton.signal_clicked().connect([this] { m_revealer.set_reveal_child(!m_revealer.get_reveal_child()); })) +{ + m_revealer.add(m_listBox); + m_revealer.set_transition_type(RevealerTransitionType); + m_revealer.set_transition_duration(RevealerAnimationTimeMs); + m_revealer.set_reveal_child(); + + add(m_appNameButton); + add(m_revealer); + + m_listBox.bind_model(m_model->getDeviceModels(), &CreateDeviceWidget); + m_listBox.set_can_focus(false); + m_listBox.set_selection_mode(Gtk::SelectionMode::SELECTION_SINGLE); +} + +void AppVmWidget::reveal(bool reveal) +{ + m_revealer.set_reveal_child(reveal); +} + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/widgets/DeviceWidget.cpp b/packages/ghaf-audio-control/src/lib/src/widgets/DeviceWidget.cpp new file mode 100644 index 0000000..2a7f7a4 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/widgets/DeviceWidget.cpp @@ -0,0 +1,86 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +namespace ghaf::AudioControl +{ + +namespace +{ + +constexpr auto ScaleSize = 200; +constexpr auto ScaleOrientation = Gtk::Orientation::ORIENTATION_HORIZONTAL; +constexpr auto ScaleInitialValue = 0.0; +constexpr auto ScaleLowerLimit = 0.0; +constexpr auto ScaleUpperLimit = 100.0; + +constexpr auto DeviceWidgetSpacing = 5; + +auto Bind(const auto& appProp, const auto& widgetProp, bool readonly = false) +{ + auto flag = Glib::BindingFlags::BINDING_SYNC_CREATE; + if (!readonly) + flag |= Glib::BindingFlags::BINDING_BIDIRECTIONAL; + + return Glib::Binding::bind_property(appProp, widgetProp, flag); +} + +Gtk::Scale* MakeScaleWidget() +{ + auto adjustment = Gtk::Adjustment::create(ScaleInitialValue, ScaleLowerLimit, ScaleUpperLimit); + + auto* scale = Gtk::make_managed(std::move(adjustment), ScaleOrientation); + scale->set_size_request(ScaleSize); + scale->set_digits(0); + + return scale; +} + +} // namespace + +DeviceWidget::DeviceWidget(DeviceModel::Ptr model) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , m_model(std::move(model)) + , m_nameLabel(Gtk::make_managed()) + , m_switch(Gtk::make_managed()) + , m_scale(MakeScaleWidget()) + , m_bindings({Bind(m_model->getNameProperty(), m_nameLabel->property_label(), true), + Bind(m_model->getSoundVolumeProperty(), m_scale->get_adjustment()->property_value()), + Bind(m_model->getSoundEnabledProperty(), m_switch->property_state())}) +{ + const auto setup = [](Gtk::Widget& widget) + { + widget.set_hexpand(false); + widget.set_vexpand(false); + widget.set_halign(Gtk::Align::ALIGN_START); + widget.set_valign(Gtk::Align::ALIGN_END); + }; + + set_homogeneous(true); + set_spacing(DeviceWidgetSpacing); + + setup(*m_nameLabel); + setup(*m_switch); + setup(*m_scale); + + add(*m_nameLabel); + add(*m_switch); + add(*m_scale); + + m_nameLabel->set_can_focus(false); + m_nameLabel->set_selectable(false); + m_nameLabel->set_focus_on_click(false); + + m_switch->set_halign(Gtk::Align::ALIGN_END); + m_scale->set_halign(Gtk::Align::ALIGN_END); + + set_valign(Gtk::ALIGN_START); + show_all_children(); +} + +} // namespace ghaf::AudioControl diff --git a/packages/ghaf-audio-control/src/lib/src/widgets/SinkWidget.cpp b/packages/ghaf-audio-control/src/lib/src/widgets/SinkWidget.cpp new file mode 100644 index 0000000..1e29ef3 --- /dev/null +++ b/packages/ghaf-audio-control/src/lib/src/widgets/SinkWidget.cpp @@ -0,0 +1,6 @@ +/* + * Copyright 2022-2024 TII (SSRC) and the Ghaf contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include