diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp index d0b17e47..eead664f 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -1,54 +1,27 @@ #pragma once #include -#include -#include #include #include +#include #include "ALabel.hpp" +#include "util/audio_backend.hpp" namespace waybar::modules { class Pulseaudio : public ALabel { public: Pulseaudio(const std::string&, const Json::Value&); - virtual ~Pulseaudio(); + virtual ~Pulseaudio() = default; auto update() -> void override; private: - static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); - static void contextStateCb(pa_context*, void*); - static void sinkInfoCb(pa_context*, const pa_sink_info*, int, void*); - static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); - static void serverInfoCb(pa_context*, const pa_server_info*, void*); - static void volumeModifyCb(pa_context*, int, void*); - bool handleScroll(GdkEventScroll* e) override; const std::vector getPulseIcon() const; - pa_threaded_mainloop* mainloop_; - pa_mainloop_api* mainloop_api_; - pa_context* context_; - // SINK - uint32_t sink_idx_{0}; - uint16_t volume_; - pa_cvolume pa_volume_; - bool muted_; - std::string port_name_; - std::string form_factor_; - std::string desc_; - std::string monitor_; - std::string current_sink_name_; - bool current_sink_running_; - // SOURCE - uint32_t source_idx_{0}; - uint16_t source_volume_; - bool source_muted_; - std::string source_port_name_; - std::string source_desc_; - std::string default_source_name_; + std::shared_ptr backend = nullptr; }; } // namespace waybar::modules diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp new file mode 100644 index 00000000..9435a842 --- /dev/null +++ b/include/util/audio_backend.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace waybar::util { + +enum class ChangeType : char { Increase, Decrease }; + +void noop(); + +class AudioBackend { + private: + static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); + static void contextStateCb(pa_context*, void*); + static void sinkInfoCb(pa_context*, const pa_sink_info*, int, void*); + static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); + static void serverInfoCb(pa_context*, const pa_server_info*, void*); + static void volumeModifyCb(pa_context*, int, void*); + + pa_threaded_mainloop* mainloop_; + pa_mainloop_api* mainloop_api_; + pa_context* context_; + pa_cvolume pa_volume_; + + // SINK + uint32_t sink_idx_{0}; + uint16_t volume_; + bool muted_; + std::string port_name_; + std::string form_factor_; + std::string desc_; + std::string monitor_; + std::string current_sink_name_; + bool current_sink_running_; + // SOURCE + uint32_t source_idx_{0}; + uint16_t source_volume_; + bool source_muted_; + std::string source_port_name_; + std::string source_desc_; + std::string default_source_name_; + + std::vector ignored_sinks_; + + std::function on_updated_cb_ = noop; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct private_constructor_tag {}; + + public: + static std::shared_ptr getInstance(std::function on_updated_cb = noop); + + AudioBackend(std::function on_updated_cb, private_constructor_tag tag); + ~AudioBackend(); + + void changeVolume(ChangeType change_type, double step = 1, int max_volume = 100); + + void setIgnoredSinks(const Json::Value& config); + + std::string getSinkPortName() const { return port_name_; } + std::string getFormFactor() const { return form_factor_; } + std::string getSinkDesc() const { return desc_; } + std::string getMonitor() const { return monitor_; } + std::string getCurrentSinkName() const { return current_sink_name_; } + bool getCurrentSinkRunning() const { return current_sink_running_; } + uint16_t getSinkVolume() const { return volume_; } + bool getSinkMuted() const { return muted_; } + uint16_t getSourceVolume() const { return source_volume_; } + bool getSourceMuted() const { return source_muted_; } + std::string getSourcePortName() const { return source_port_name_; } + std::string getSourceDesc() const { return source_desc_; } + std::string getDefaultSourceName() const { return default_source_name_; } + + bool isBluetooth(); +}; + +} // namespace waybar::util \ No newline at end of file diff --git a/meson.build b/meson.build index 15bab87c..e85785ad 100644 --- a/meson.build +++ b/meson.build @@ -178,7 +178,8 @@ src_files = files( 'src/util/sanitize_str.cpp', 'src/util/rewrite_string.cpp', 'src/util/gtk_icon.cpp', - 'src/util/regex_collection.cpp' + 'src/util/regex_collection.cpp', + 'src/util/audio_backend.cpp' ) inc_dirs = ['include'] diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index d35e2983..d7dc80d3 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -1,74 +1,12 @@ #include "modules/pulseaudio.hpp" waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config) - : ALabel(config, "pulseaudio", id, "{volume}%"), - mainloop_(nullptr), - mainloop_api_(nullptr), - context_(nullptr), - sink_idx_(0), - volume_(0), - muted_(false), - source_idx_(0), - source_volume_(0), - source_muted_(false) { - mainloop_ = pa_threaded_mainloop_new(); - if (mainloop_ == nullptr) { - throw std::runtime_error("pa_mainloop_new() failed."); - } - pa_threaded_mainloop_lock(mainloop_); - mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_); - context_ = pa_context_new(mainloop_api_, "waybar"); - if (context_ == nullptr) { - throw std::runtime_error("pa_context_new() failed."); - } - if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { - auto err = - fmt::format("pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_))); - throw std::runtime_error(err); - } - pa_context_set_state_callback(context_, contextStateCb, this); - if (pa_threaded_mainloop_start(mainloop_) < 0) { - throw std::runtime_error("pa_mainloop_run() failed."); - } - pa_threaded_mainloop_unlock(mainloop_); + : ALabel(config, "pulseaudio", id, "{volume}%") { event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll)); -} -waybar::modules::Pulseaudio::~Pulseaudio() { - pa_context_disconnect(context_); - mainloop_api_->quit(mainloop_api_, 0); - pa_threaded_mainloop_stop(mainloop_); - pa_threaded_mainloop_free(mainloop_); -} - -void waybar::modules::Pulseaudio::contextStateCb(pa_context *c, void *data) { - auto pa = static_cast(data); - switch (pa_context_get_state(c)) { - case PA_CONTEXT_TERMINATED: - pa->mainloop_api_->quit(pa->mainloop_api_, 0); - break; - case PA_CONTEXT_READY: - pa_context_get_server_info(c, serverInfoCb, data); - pa_context_set_subscribe_callback(c, subscribeCb, data); - pa_context_subscribe(c, - static_cast( - static_cast(PA_SUBSCRIPTION_MASK_SERVER) | - static_cast(PA_SUBSCRIPTION_MASK_SINK) | - static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), - nullptr, nullptr); - break; - case PA_CONTEXT_FAILED: - pa->mainloop_api_->quit(pa->mainloop_api_, 1); - break; - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - default: - break; - } + backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); + backend->setIgnoredSinks(config_["ignored-sinks"]); } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { @@ -81,9 +19,6 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { if (dir == SCROLL_DIR::NONE) { return true; } - double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_volume_t change = volume_tick; - pa_cvolume pa_volume = pa_volume_; int max_volume = 100; double step = 1; // isDouble returns true for integers as well, just in case @@ -91,152 +26,24 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isInt()) { - max_volume = std::min(config_["max-volume"].asInt(), static_cast(PA_VOLUME_UI_MAX)); + max_volume = config_["max-volume"].asInt(); } - if (dir == SCROLL_DIR::UP) { - if (volume_ < max_volume) { - if (volume_ + step > max_volume) { - change = round((max_volume - volume_) * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_inc(&pa_volume, change); - } - } else if (dir == SCROLL_DIR::DOWN) { - if (volume_ > 0) { - if (volume_ - step < 0) { - change = round(volume_ * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_dec(&pa_volume, change); - } - } - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + auto change_type = (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::RIGHT) + ? util::ChangeType::Increase + : util::ChangeType::Decrease; + + backend->changeVolume(change_type, step, max_volume); return true; } -/* - * Called when an event we subscribed to occurs. - */ -void waybar::modules::Pulseaudio::subscribeCb(pa_context *context, - pa_subscription_event_type_t type, uint32_t idx, - void *data) { - unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; - unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; - if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) { - return; - } - if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { - pa_context_get_server_info(context, serverInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { - pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { - pa_context_get_sink_info_list(context, sinkInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { - pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { - pa_context_get_source_info_list(context, sourceInfoCb, data); - } -} - -/* - * Called in response to a volume change request - */ -void waybar::modules::Pulseaudio::volumeModifyCb(pa_context *c, int success, void *data) { - auto pa = static_cast(data); - if (success != 0) { - pa_context_get_sink_info_by_index(pa->context_, pa->sink_idx_, sinkInfoCb, data); - } -} - -/* - * Called when the requested source information is ready. - */ -void waybar::modules::Pulseaudio::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, - int /*eol*/, void *data) { - auto pa = static_cast(data); - if (i != nullptr && pa->default_source_name_ == i->name) { - auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; - pa->source_volume_ = std::round(source_volume * 100.0F); - pa->source_idx_ = i->index; - pa->source_muted_ = i->mute != 0; - pa->source_desc_ = i->description; - pa->source_port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; - pa->dp.emit(); - } -} - -/* - * Called when the requested sink information is ready. - */ -void waybar::modules::Pulseaudio::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, - int /*eol*/, void *data) { - if (i == nullptr) return; - - auto pa = static_cast(data); - - if (pa->config_["ignored-sinks"].isArray()) { - for (const auto &ignored_sink : pa->config_["ignored-sinks"]) { - if (ignored_sink.asString() == i->description) { - return; - } - } - } - - if (pa->current_sink_name_ == i->name) { - if (i->state != PA_SINK_RUNNING) { - pa->current_sink_running_ = false; - } else { - pa->current_sink_running_ = true; - } - } - - if (!pa->current_sink_running_ && i->state == PA_SINK_RUNNING) { - pa->current_sink_name_ = i->name; - pa->current_sink_running_ = true; - } - - if (pa->current_sink_name_ == i->name) { - pa->pa_volume_ = i->volume; - float volume = static_cast(pa_cvolume_avg(&(pa->pa_volume_))) / float{PA_VOLUME_NORM}; - pa->sink_idx_ = i->index; - pa->volume_ = std::round(volume * 100.0F); - pa->muted_ = i->mute != 0; - pa->desc_ = i->description; - pa->monitor_ = i->monitor_source_name; - pa->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; - if (auto ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { - pa->form_factor_ = ff; - } else { - pa->form_factor_ = ""; - } - pa->dp.emit(); - } -} - -/* - * Called when the requested information on the server is ready. This is - * used to find the default PulseAudio sink. - */ -void waybar::modules::Pulseaudio::serverInfoCb(pa_context *context, const pa_server_info *i, - void *data) { - auto pa = static_cast(data); - pa->current_sink_name_ = i->default_sink_name; - pa->default_source_name_ = i->default_source_name; - - pa_context_get_sink_info_list(context, sinkInfoCb, data); - pa_context_get_source_info_list(context, sourceInfoCb, data); -} - static const std::array ports = { "headphone", "speaker", "hdmi", "headset", "hands-free", "portable", "car", "hifi", "phone", }; const std::vector waybar::modules::Pulseaudio::getPulseIcon() const { - std::vector res = {current_sink_name_, default_source_name_}; - std::string nameLC = port_name_ + form_factor_; + std::vector res = {backend->getCurrentSinkName(), backend->getDefaultSourceName()}; + std::string nameLC = backend->getSinkPortName() + backend->getFormFactor(); std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { if (nameLC.find(port) != std::string::npos) { @@ -250,17 +57,16 @@ const std::vector waybar::modules::Pulseaudio::getPulseIcon() const auto waybar::modules::Pulseaudio::update() -> void { auto format = format_; std::string tooltip_format; + auto sink_volume = backend->getSinkVolume(); if (!alt_) { std::string format_name = "format"; - if (monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio - monitor_.find("a2dp-sink") != std::string::npos || // PipeWire - monitor_.find("bluez") != std::string::npos) { + if (backend->isBluetooth()) { format_name = format_name + "-bluetooth"; label_.get_style_context()->add_class("bluetooth"); } else { label_.get_style_context()->remove_class("bluetooth"); } - if (muted_) { + if (backend->getSinkMuted()) { // Check muted bluetooth format exist, otherwise fallback to default muted format if (format_name != "format" && !config_[format_name + "-muted"].isString()) { format_name = "format"; @@ -272,7 +78,7 @@ auto waybar::modules::Pulseaudio::update() -> void { label_.get_style_context()->remove_class("muted"); label_.get_style_context()->remove_class("sink-muted"); } - auto state = getState(volume_, true); + auto state = getState(sink_volume, true); if (!state.empty() && config_[format_name + "-" + state].isString()) { format = config_[format_name + "-" + state].asString(); } else if (config_[format_name].isString()) { @@ -281,7 +87,7 @@ auto waybar::modules::Pulseaudio::update() -> void { } // TODO: find a better way to split source/sink std::string format_source = "{volume}%"; - if (source_muted_) { + if (backend->getSourceMuted()) { label_.get_style_context()->add_class("source-muted"); if (config_["format-source-muted"].isString()) { format_source = config_["format-source-muted"].asString(); @@ -292,11 +98,16 @@ auto waybar::modules::Pulseaudio::update() -> void { format_source = config_["format-source"].asString(); } } - format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume_)); + + auto source_volume = backend->getSourceVolume(); + auto sink_desc = backend->getSinkDesc(); + auto source_desc = backend->getSourceDesc(); + + format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume)); auto text = fmt::format( - fmt::runtime(format), fmt::arg("desc", desc_), fmt::arg("volume", volume_), - fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_), - fmt::arg("source_desc", source_desc_), fmt::arg("icon", getIcon(volume_, getPulseIcon()))); + fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), + fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), + fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))); if (text.empty()) { label_.hide(); } else { @@ -310,12 +121,12 @@ auto waybar::modules::Pulseaudio::update() -> void { } if (!tooltip_format.empty()) { label_.set_tooltip_text(fmt::format( - fmt::runtime(tooltip_format), fmt::arg("desc", desc_), fmt::arg("volume", volume_), - fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_), - fmt::arg("source_desc", source_desc_), - fmt::arg("icon", getIcon(volume_, getPulseIcon())))); + fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc), + fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), + fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), + fmt::arg("icon", getIcon(sink_volume, getPulseIcon())))); } else { - label_.set_tooltip_text(desc_); + label_.set_tooltip_text(sink_desc); } } diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp new file mode 100644 index 00000000..c230c1c9 --- /dev/null +++ b/src/util/audio_backend.cpp @@ -0,0 +1,251 @@ +#include "util/audio_backend.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace waybar::util { + +void noop() {} + +AudioBackend::AudioBackend(std::function on_updated_cb, private_constructor_tag tag) + : mainloop_(nullptr), + mainloop_api_(nullptr), + context_(nullptr), + sink_idx_(0), + volume_(0), + muted_(false), + source_idx_(0), + source_volume_(0), + source_muted_(false), + on_updated_cb_(on_updated_cb) { + mainloop_ = pa_threaded_mainloop_new(); + if (mainloop_ == nullptr) { + throw std::runtime_error("pa_mainloop_new() failed."); + } + pa_threaded_mainloop_lock(mainloop_); + mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_); + context_ = pa_context_new(mainloop_api_, "waybar"); + if (context_ == nullptr) { + throw std::runtime_error("pa_context_new() failed."); + } + if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { + auto err = + fmt::format("pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_))); + throw std::runtime_error(err); + } + pa_context_set_state_callback(context_, contextStateCb, this); + if (pa_threaded_mainloop_start(mainloop_) < 0) { + throw std::runtime_error("pa_mainloop_run() failed."); + } + pa_threaded_mainloop_unlock(mainloop_); +} + +AudioBackend::~AudioBackend() { + if (context_ != nullptr) { + pa_context_disconnect(context_); + } + + if (mainloop_ != nullptr) { + mainloop_api_->quit(mainloop_api_, 0); + pa_threaded_mainloop_stop(mainloop_); + pa_threaded_mainloop_free(mainloop_); + } +} + +std::shared_ptr AudioBackend::getInstance(std::function on_updated_cb) { + private_constructor_tag tag; + return std::make_shared(on_updated_cb, tag); +} + +void AudioBackend::contextStateCb(pa_context *c, void *data) { + auto backend = static_cast(data); + switch (pa_context_get_state(c)) { + case PA_CONTEXT_TERMINATED: + backend->mainloop_api_->quit(backend->mainloop_api_, 0); + break; + case PA_CONTEXT_READY: + pa_context_get_server_info(c, serverInfoCb, data); + pa_context_set_subscribe_callback(c, subscribeCb, data); + pa_context_subscribe(c, + static_cast( + static_cast(PA_SUBSCRIPTION_MASK_SERVER) | + static_cast(PA_SUBSCRIPTION_MASK_SINK) | + static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), + nullptr, nullptr); + break; + case PA_CONTEXT_FAILED: + backend->mainloop_api_->quit(backend->mainloop_api_, 1); + break; + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + } +} + +/* + * Called when an event we subscribed to occurs. + */ +void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t type, uint32_t idx, + void *data) { + unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) { + return; + } + if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { + pa_context_get_server_info(context, serverInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { + pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { + pa_context_get_sink_info_list(context, sinkInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { + pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { + pa_context_get_source_info_list(context, sourceInfoCb, data); + } +} + +/* + * Called in response to a volume change request + */ +void AudioBackend::volumeModifyCb(pa_context *c, int success, void *data) { + auto backend = static_cast(data); + if (success != 0) { + pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + } +} + +/* + * Called when the requested sink information is ready. + */ +void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, int /*eol*/, + void *data) { + if (i == nullptr) return; + + auto backend = static_cast(data); + + if (!backend->ignored_sinks_.empty()) { + for (const auto &ignored_sink : backend->ignored_sinks_) { + if (ignored_sink == i->description) { + return; + } + } + } + + if (backend->current_sink_name_ == i->name) { + if (i->state != PA_SINK_RUNNING) { + backend->current_sink_running_ = false; + } else { + backend->current_sink_running_ = true; + } + } + + if (!backend->current_sink_running_ && i->state == PA_SINK_RUNNING) { + backend->current_sink_name_ = i->name; + backend->current_sink_running_ = true; + } + + if (backend->current_sink_name_ == i->name) { + backend->pa_volume_ = i->volume; + float volume = + static_cast(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM}; + backend->sink_idx_ = i->index; + backend->volume_ = std::round(volume * 100.0F); + backend->muted_ = i->mute != 0; + backend->desc_ = i->description; + backend->monitor_ = i->monitor_source_name; + backend->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; + if (auto ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { + backend->form_factor_ = ff; + } else { + backend->form_factor_ = ""; + } + backend->on_updated_cb_(); + } +} + +/* + * Called when the requested source information is ready. + */ +void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, int /*eol*/, + void *data) { + auto backend = static_cast(data); + if (i != nullptr && backend->default_source_name_ == i->name) { + auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; + backend->source_volume_ = std::round(source_volume * 100.0F); + backend->source_idx_ = i->index; + backend->source_muted_ = i->mute != 0; + backend->source_desc_ = i->description; + backend->source_port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; + backend->on_updated_cb_(); + } +} + +/* + * Called when the requested information on the server is ready. This is + * used to find the default PulseAudio sink. + */ +void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) { + auto backend = static_cast(data); + backend->current_sink_name_ = i->default_sink_name; + backend->default_source_name_ = i->default_source_name; + + pa_context_get_sink_info_list(context, sinkInfoCb, data); + pa_context_get_source_info_list(context, sourceInfoCb, data); +} + +void AudioBackend::changeVolume(ChangeType change_type, double step, int max_volume) { + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t change = volume_tick; + pa_cvolume pa_volume = pa_volume_; + + max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); + + if (change_type == ChangeType::Increase) { + if (volume_ < max_volume) { + if (volume_ + step > max_volume) { + change = round((max_volume - volume_) * volume_tick); + } else { + change = round(step * volume_tick); + } + pa_cvolume_inc(&pa_volume, change); + } + } else if (change_type == ChangeType::Decrease) { + if (volume_ > 0) { + if (volume_ - step < 0) { + change = round(volume_ * volume_tick); + } else { + change = round(step * volume_tick); + } + pa_cvolume_dec(&pa_volume, change); + } + } + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); +} + +bool AudioBackend::isBluetooth() { + return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio + monitor_.find("a2dp-sink") != std::string::npos || // PipeWire + monitor_.find("bluez") != std::string::npos; +} + +void AudioBackend::setIgnoredSinks(const Json::Value &config) { + if (config.isArray()) { + for (const auto &ignored_sink : config) { + if (ignored_sink.isString()) { + ignored_sinks_.push_back(ignored_sink.asString()); + } + } + } +} + +} // namespace waybar::util \ No newline at end of file