2018-08-09 21:55:38 +00:00
|
|
|
#include "modules/pulseaudio.hpp"
|
2019-03-15 01:35:16 +00:00
|
|
|
#include <array>
|
2018-08-09 21:55:38 +00:00
|
|
|
|
2018-12-18 16:30:54 +00:00
|
|
|
waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value &config)
|
2018-10-29 16:18:35 +00:00
|
|
|
: ALabel(config, "{volume}%"),
|
|
|
|
mainloop_(nullptr),
|
|
|
|
mainloop_api_(nullptr),
|
|
|
|
context_(nullptr),
|
|
|
|
sink_idx_(0),
|
|
|
|
volume_(0),
|
|
|
|
muted_(false),
|
|
|
|
scrolling_(false) {
|
2018-08-16 12:29:41 +00:00
|
|
|
label_.set_name("pulseaudio");
|
2018-12-18 16:30:54 +00:00
|
|
|
if (!id.empty()) {
|
|
|
|
label_.get_style_context()->add_class(id);
|
|
|
|
}
|
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_NOAUTOSPAWN,
|
|
|
|
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) {
|
2018-08-09 22:34:13 +00:00
|
|
|
throw std::runtime_error("pa_mainloop_run() failed.");
|
2018-08-16 12:29:41 +00:00
|
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(mainloop_);
|
2018-10-29 16:18:35 +00:00
|
|
|
|
|
|
|
// define the pulse scroll events only when no user provided
|
|
|
|
// events are configured
|
|
|
|
if (!config["on-scroll-up"].isString() &&
|
|
|
|
!config["on-scroll-down"].isString()) {
|
2019-02-16 08:56:53 +00:00
|
|
|
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
2018-10-29 16:18:35 +00:00
|
|
|
event_box_.signal_scroll_event().connect(
|
|
|
|
sigc::mem_fun(*this, &Pulseaudio::handleScroll));
|
|
|
|
}
|
2018-08-18 09:43:48 +00:00
|
|
|
}
|
2018-08-09 21:55:38 +00:00
|
|
|
|
2018-10-29 16:18:35 +00:00
|
|
|
waybar::modules::Pulseaudio::~Pulseaudio() {
|
2018-08-19 11:39:57 +00:00
|
|
|
mainloop_api_->quit(mainloop_api_, 0);
|
|
|
|
pa_threaded_mainloop_stop(mainloop_);
|
|
|
|
pa_threaded_mainloop_free(mainloop_);
|
|
|
|
}
|
|
|
|
|
2018-08-16 12:29:41 +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);
|
|
|
|
pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK, 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-29 16:18:35 +00:00
|
|
|
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
|
|
|
|
// Avoid concurrent scroll event
|
|
|
|
bool direction_up = false;
|
2018-11-09 21:48:27 +00:00
|
|
|
uint16_t change = config_["scroll-step"].isUInt() ? config_["scroll-step"].asUInt() * 100 : 100;
|
2018-10-29 16:18:35 +00:00
|
|
|
pa_cvolume pa_volume = pa_volume_;
|
|
|
|
|
|
|
|
if (scrolling_) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
scrolling_ = true;
|
|
|
|
if (e->direction == GDK_SCROLL_UP) {
|
|
|
|
direction_up = true;
|
|
|
|
}
|
|
|
|
if (e->direction == GDK_SCROLL_DOWN) {
|
|
|
|
direction_up = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e->direction == GDK_SCROLL_SMOOTH) {
|
|
|
|
gdouble delta_x, delta_y;
|
|
|
|
gdk_event_get_scroll_deltas(reinterpret_cast<const GdkEvent *>(e), &delta_x,
|
|
|
|
&delta_y);
|
|
|
|
if (delta_y < 0) {
|
|
|
|
direction_up = true;
|
|
|
|
} else if (delta_y > 0) {
|
|
|
|
direction_up = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (direction_up) {
|
|
|
|
if (volume_ + 1 < 100) pa_cvolume_inc(&pa_volume, change);
|
|
|
|
} else {
|
|
|
|
if (volume_ - 1 > 0) pa_cvolume_dec(&pa_volume, change);
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2018-08-16 12:29:41 +00:00
|
|
|
void waybar::modules::Pulseaudio::subscribeCb(pa_context* context,
|
|
|
|
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;
|
|
|
|
|
|
|
|
switch (facility) {
|
2018-08-16 12:29:41 +00:00
|
|
|
case PA_SUBSCRIPTION_EVENT_SINK:
|
|
|
|
pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2018-08-09 21:55:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-29 16:18:35 +00:00
|
|
|
/*
|
|
|
|
* Called in response to a volume change request
|
|
|
|
*/
|
|
|
|
void waybar::modules::Pulseaudio::volumeModifyCb(pa_context *c, int success,
|
|
|
|
void *data) {
|
|
|
|
auto pa = static_cast<waybar::modules::Pulseaudio *>(data);
|
|
|
|
if (success) {
|
|
|
|
pa_context_get_sink_info_by_index(pa->context_, pa->sink_idx_, sinkInfoCb,
|
|
|
|
data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-09 21:55:38 +00:00
|
|
|
/*
|
|
|
|
* Called when the requested sink information is ready.
|
|
|
|
*/
|
2018-10-29 16:18:35 +00:00
|
|
|
void waybar::modules::Pulseaudio::sinkInfoCb(pa_context * /*context*/,
|
|
|
|
const pa_sink_info *i, int /*eol*/,
|
|
|
|
void *data) {
|
2018-08-16 12:29:41 +00:00
|
|
|
if (i != nullptr) {
|
2018-08-09 21:55:38 +00:00
|
|
|
auto pa = static_cast<waybar::modules::Pulseaudio *>(data);
|
2018-10-29 16:18:35 +00:00
|
|
|
pa->pa_volume_ = i->volume;
|
|
|
|
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;
|
2018-10-25 11:57:35 +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;
|
2018-10-27 09:23:43 +00:00
|
|
|
pa->port_name_ = i->active_port ? i->active_port->name : "Unknown";
|
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.
|
|
|
|
*/
|
2018-08-16 12:29:41 +00:00
|
|
|
void waybar::modules::Pulseaudio::serverInfoCb(pa_context *context,
|
2018-08-09 21:55:38 +00:00
|
|
|
const pa_server_info *i, void *data)
|
|
|
|
{
|
2018-08-26 23:36:25 +00:00
|
|
|
pa_context_get_sink_info_by_name(context, i->default_sink_name,
|
|
|
|
sinkInfoCb, data);
|
2018-08-09 21:55:38 +00:00
|
|
|
}
|
|
|
|
|
2019-03-15 01:35:16 +00:00
|
|
|
static const std::array<std::string, 9> ports = {
|
|
|
|
"headphones",
|
|
|
|
"speaker",
|
|
|
|
"hdmi",
|
|
|
|
"headset",
|
|
|
|
"handsfree",
|
|
|
|
"portable",
|
|
|
|
"car",
|
|
|
|
"hifi",
|
|
|
|
"phone",
|
|
|
|
};
|
|
|
|
|
2018-08-29 21:50:41 +00:00
|
|
|
const std::string waybar::modules::Pulseaudio::getPortIcon() const
|
|
|
|
{
|
2019-03-15 01:08:12 +00:00
|
|
|
std::string nameLC = port_name_;
|
|
|
|
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
|
2018-09-04 21:50:08 +00:00
|
|
|
for (auto const& port : ports) {
|
2019-03-15 01:08:12 +00:00
|
|
|
if (nameLC.find(port) != std::string::npos) {
|
2018-08-29 21:50:41 +00:00
|
|
|
return port;
|
|
|
|
}
|
|
|
|
}
|
2018-12-20 08:54:10 +00:00
|
|
|
return port_name_;
|
2018-08-29 21:50:41 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 21:55:38 +00:00
|
|
|
auto waybar::modules::Pulseaudio::update() -> void
|
|
|
|
{
|
2018-08-26 23:36:25 +00:00
|
|
|
auto format = format_;
|
|
|
|
if (muted_) {
|
|
|
|
format =
|
2018-10-26 07:27:16 +00:00
|
|
|
config_["format-muted"].isString() ? config_["format-muted"].asString() : format;
|
2018-08-26 23:36:25 +00:00
|
|
|
label_.get_style_context()->add_class("muted");
|
|
|
|
} else {
|
|
|
|
label_.get_style_context()->remove_class("muted");
|
2019-02-18 11:30:41 +00:00
|
|
|
if (port_name_.find("a2dp_sink") != std::string::npos) {
|
|
|
|
format = config_["format-bluetooth"].isString()
|
|
|
|
? config_["format-bluetooth"].asString() : format;
|
|
|
|
label_.get_style_context()->add_class("bluetooth");
|
|
|
|
} else {
|
|
|
|
label_.get_style_context()->remove_class("bluetooth");
|
|
|
|
}
|
2018-08-16 12:29:41 +00:00
|
|
|
}
|
2018-12-17 08:00:40 +00:00
|
|
|
label_.set_markup(
|
2018-10-29 16:18:35 +00:00
|
|
|
fmt::format(format, fmt::arg("volume", volume_),
|
|
|
|
fmt::arg("icon", getIcon(volume_, getPortIcon()))));
|
2019-02-22 10:35:26 +00:00
|
|
|
if (tooltipEnabled()) {
|
|
|
|
label_.set_tooltip_text(desc_);
|
|
|
|
}
|
2018-10-29 16:18:35 +00:00
|
|
|
if (scrolling_) {
|
|
|
|
scrolling_ = false;
|
|
|
|
}
|
2018-08-13 20:33:07 +00:00
|
|
|
}
|