Waybar/src/modules/pulseaudio.cpp

328 lines
12 KiB
C++
Raw Normal View History

2018-08-09 21:55:38 +00:00
#include "modules/pulseaudio.hpp"
2019-04-18 15:52:00 +00:00
waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config)
2022-11-24 11:28:52 +00:00
: ALabel(config, "pulseaudio", id, "{volume}%"),
mainloop_(nullptr),
mainloop_api_(nullptr),
context_(nullptr),
sink_idx_(0),
volume_(0),
muted_(false),
2019-05-21 12:53:31 +00:00
source_idx_(0),
source_volume_(0),
source_muted_(false) {
2018-08-16 12:29:41 +00:00
mainloop_ = pa_threaded_mainloop_new();
if (mainloop_ == nullptr) {
2018-08-09 21:55:38 +00:00
throw std::runtime_error("pa_mainloop_new() failed.");
2018-08-16 12:29:41 +00:00
}
pa_threaded_mainloop_lock(mainloop_);
mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_);
context_ = pa_context_new(mainloop_api_, "waybar");
if (context_ == nullptr) {
2018-08-09 21:55:38 +00:00
throw std::runtime_error("pa_context_new() failed.");
2018-08-16 12:29:41 +00:00
}
if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) {
2019-04-18 15:52:00 +00:00
auto err =
fmt::format("pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_)));
2018-08-16 12:29:41 +00:00
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.");
2018-08-16 12:29:41 +00:00
}
pa_threaded_mainloop_unlock(mainloop_);
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll));
}
2018-08-09 21:55:38 +00:00
waybar::modules::Pulseaudio::~Pulseaudio() {
2022-10-16 07:24:17 +00:00
pa_context_disconnect(context_);
2018-08-19 11:39:57 +00:00
mainloop_api_->quit(mainloop_api_, 0);
pa_threaded_mainloop_stop(mainloop_);
pa_threaded_mainloop_free(mainloop_);
}
2019-04-18 15:52:00 +00:00
void waybar::modules::Pulseaudio::contextStateCb(pa_context *c, void *data) {
2018-08-09 21:55:38 +00:00
auto pa = static_cast<waybar::modules::Pulseaudio *>(data);
switch (pa_context_get_state(c)) {
case PA_CONTEXT_TERMINATED:
2018-08-16 12:29:41 +00:00
pa->mainloop_api_->quit(pa->mainloop_api_, 0);
2018-08-09 21:55:38 +00:00
break;
case PA_CONTEXT_READY:
2018-08-16 12:29:41 +00:00
pa_context_get_server_info(c, serverInfoCb, data);
pa_context_set_subscribe_callback(c, subscribeCb, data);
2022-04-06 06:37:19 +00:00
pa_context_subscribe(c,
static_cast<enum pa_subscription_mask>(
static_cast<int>(PA_SUBSCRIPTION_MASK_SERVER) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK_INPUT) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)),
nullptr, nullptr);
2018-08-09 21:55:38 +00:00
break;
2019-03-08 14:30:41 +00:00
case PA_CONTEXT_FAILED:
pa->mainloop_api_->quit(pa->mainloop_api_, 1);
break;
2018-08-09 21:55:38 +00:00
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
default:
break;
}
}
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
// change the pulse volume only when no user provided
// events are configured
2019-06-16 13:13:40 +00:00
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
return AModule::handleScroll(e);
}
auto dir = AModule::getScrollDir(e);
2019-06-15 12:57:52 +00:00
if (dir == SCROLL_DIR::NONE) {
return true;
}
2022-04-06 06:37:19 +00:00
if (config_["reverse-scrolling"].asInt() == 1) {
if (dir == SCROLL_DIR::UP) {
dir = SCROLL_DIR::DOWN;
} else if (dir == SCROLL_DIR::DOWN) {
dir = SCROLL_DIR::UP;
}
}
2022-04-06 06:37:19 +00:00
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
2019-05-14 07:24:06 +00:00
pa_volume_t change = volume_tick;
2022-04-06 06:37:19 +00:00
pa_cvolume pa_volume = pa_volume_;
int max_volume = 100;
double step = 1;
2019-05-14 07:24:06 +00:00
// isDouble returns true for integers as well, just in case
if (config_["scroll-step"].isDouble()) {
step = config_["scroll-step"].asDouble();
2019-05-14 07:24:06 +00:00
}
if (config_["max-volume"].isInt()) {
max_volume = std::min(config_["max-volume"].asInt(), static_cast<int>(PA_VOLUME_UI_MAX));
}
2019-06-15 12:57:52 +00:00
if (dir == SCROLL_DIR::UP) {
if (volume_ < max_volume) {
if (volume_ + step > max_volume) {
change = round((max_volume - volume_) * volume_tick);
2022-10-17 07:19:00 +00:00
} else {
change = round(step * volume_tick);
}
2019-04-24 10:37:24 +00:00
pa_cvolume_inc(&pa_volume, change);
}
2019-06-15 12:57:52 +00:00
} else if (dir == SCROLL_DIR::DOWN) {
if (volume_ > 0) {
if (volume_ - step < 0) {
change = round(volume_ * volume_tick);
2022-10-17 07:19:00 +00:00
} else {
change = round(step * volume_tick);
}
2019-04-24 10:37:24 +00:00
pa_cvolume_dec(&pa_volume, change);
}
}
2019-04-18 15:52:00 +00:00
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
return true;
}
2018-08-09 21:55:38 +00:00
/*
* Called when an event we subscribed to occurs.
*/
2022-04-06 06:37:19 +00:00
void waybar::modules::Pulseaudio::subscribeCb(pa_context *context,
2019-04-18 15:52:00 +00:00
pa_subscription_event_type_t type, uint32_t idx,
void *data) {
2018-08-09 21:55:38 +00:00
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) {
2019-04-24 10:37:24 +00:00
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);
2019-05-21 12:53:31 +00:00
} 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);
2018-08-09 21:55:38 +00:00
}
}
/*
* Called in response to a volume change request
*/
2019-04-18 15:52:00 +00:00
void waybar::modules::Pulseaudio::volumeModifyCb(pa_context *c, int success, void *data) {
auto pa = static_cast<waybar::modules::Pulseaudio *>(data);
2019-04-24 10:37:24 +00:00
if (success != 0) {
2019-04-18 15:52:00 +00:00
pa_context_get_sink_info_by_index(pa->context_, pa->sink_idx_, sinkInfoCb, data);
}
}
2019-05-21 12:53:31 +00:00
/*
* 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<waybar::modules::Pulseaudio *>(data);
if (i != nullptr && pa->default_source_name_ == i->name) {
2019-05-21 12:53:31 +00:00
auto source_volume = static_cast<float>(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();
2019-05-21 12:53:31 +00:00
}
}
2018-08-09 21:55:38 +00:00
/*
* Called when the requested sink information is ready.
*/
2019-04-18 15:52:00 +00:00
void waybar::modules::Pulseaudio::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i,
int /*eol*/, void *data) {
2022-04-06 06:37:19 +00:00
if (i == nullptr) return;
auto pa = static_cast<waybar::modules::Pulseaudio *>(data);
if (pa->config_["ignored-sinks"].isArray()) {
2022-10-17 07:19:00 +00:00
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;
2019-04-18 15:52:00 +00:00
float volume = static_cast<float>(pa_cvolume_avg(&(pa->pa_volume_))) / float{PA_VOLUME_NORM};
2018-08-16 12:29:41 +00:00
pa->sink_idx_ = i->index;
2019-04-24 10:37:24 +00:00
pa->volume_ = std::round(volume * 100.0F);
2018-08-16 12:29:41 +00:00
pa->muted_ = i->mute != 0;
pa->desc_ = i->description;
pa->monitor_ = i->monitor_source_name;
2019-04-24 10:37:24 +00:00
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_ = "";
}
2018-08-20 12:50:45 +00:00
pa->dp.emit();
2018-08-09 21:55:38 +00:00
}
}
/*
* Called when the requested information on the server is ready. This is
* used to find the default PulseAudio sink.
*/
2019-04-18 15:52:00 +00:00
void waybar::modules::Pulseaudio::serverInfoCb(pa_context *context, const pa_server_info *i,
void *data) {
auto pa = static_cast<waybar::modules::Pulseaudio *>(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);
2018-08-09 21:55:38 +00:00
}
static const std::array<std::string, 9> ports = {
2022-04-06 06:37:19 +00:00
"headphone", "speaker", "hdmi", "headset", "hands-free", "portable", "car", "hifi", "phone",
};
2021-07-15 06:52:02 +00:00
const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const {
std::vector<std::string> res = {current_sink_name_, default_source_name_};
std::string nameLC = port_name_ + form_factor_;
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
2019-04-18 15:52:00 +00:00
for (auto const &port : ports) {
if (nameLC.find(port) != std::string::npos) {
2021-07-15 06:52:02 +00:00
res.push_back(port);
return res;
2018-08-29 21:50:41 +00:00
}
}
2021-07-15 06:52:02 +00:00
return res;
2018-08-29 21:50:41 +00:00
}
2019-04-18 15:52:00 +00:00
auto waybar::modules::Pulseaudio::update() -> void {
2018-08-26 23:36:25 +00:00
auto format = format_;
2022-04-06 06:37:19 +00:00
std::string tooltip_format;
2020-02-16 21:48:22 +00:00
if (!alt_) {
std::string format_name = "format";
2022-04-06 06:37:19 +00:00
if (monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio
monitor_.find("a2dp-sink") != std::string::npos || // PipeWire
monitor_.find("bluez") != std::string::npos) {
2020-02-16 21:48:22 +00:00
format_name = format_name + "-bluetooth";
2022-11-24 11:28:52 +00:00
label_.get_style_context()->add_class("bluetooth");
2020-02-16 21:48:22 +00:00
} else {
2022-11-24 11:28:52 +00:00
label_.get_style_context()->remove_class("bluetooth");
2020-02-16 21:48:22 +00:00
}
if (muted_) {
// Check muted bluetooth format exist, otherwise fallback to default muted format
if (format_name != "format" && !config_[format_name + "-muted"].isString()) {
format_name = "format";
}
2020-02-16 21:48:22 +00:00
format_name = format_name + "-muted";
2022-11-24 11:28:52 +00:00
label_.get_style_context()->add_class("muted");
label_.get_style_context()->add_class("sink-muted");
2020-02-16 21:48:22 +00:00
} else {
2022-11-24 11:28:52 +00:00
label_.get_style_context()->remove_class("muted");
label_.get_style_context()->remove_class("sink-muted");
2020-02-16 21:48:22 +00:00
}
2022-04-06 06:37:19 +00:00
format = config_[format_name].isString() ? config_[format_name].asString() : format;
2020-02-16 21:48:22 +00:00
}
2019-05-21 12:53:31 +00:00
// TODO: find a better way to split source/sink
2019-05-21 12:55:17 +00:00
std::string format_source = "{volume}%";
if (source_muted_) {
2022-11-24 11:28:52 +00:00
label_.get_style_context()->add_class("source-muted");
if (config_["format-source-muted"].isString()) {
format_source = config_["format-source-muted"].asString();
}
} else {
2022-11-24 11:28:52 +00:00
label_.get_style_context()->remove_class("source-muted");
if (config_["format-source-muted"].isString()) {
format_source = config_["format-source"].asString();
}
2019-05-21 12:53:31 +00:00
}
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_),
2022-04-06 06:37:19 +00:00
fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_),
fmt::arg("source_desc", source_desc_), fmt::arg("icon", getIcon(volume_, getPulseIcon())));
2022-12-02 16:32:03 +00:00
if (text.empty()) {
label_.hide();
} else {
label_.set_markup(text);
label_.show();
}
getState(volume_);
2019-02-22 10:35:26 +00:00
if (tooltipEnabled()) {
if (tooltip_format.empty() && config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
2022-11-24 11:28:52 +00:00
label_.set_tooltip_text(fmt::format(
fmt::runtime(tooltip_format), fmt::arg("desc", desc_), fmt::arg("volume", volume_),
2022-04-06 06:37:19 +00:00
fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_),
fmt::arg("source_desc", source_desc_),
fmt::arg("icon", getIcon(volume_, getPulseIcon()))));
} else {
2022-11-24 11:28:52 +00:00
label_.set_tooltip_text(desc_);
}
2019-02-22 10:35:26 +00:00
}
2020-04-12 16:30:21 +00:00
// Call parent update
2022-11-24 11:28:52 +00:00
ALabel::update();
2018-08-13 20:33:07 +00:00
}