#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