refactor: move pulseaudio handling to separate class
parent
44d8245605
commit
64d7fae03a
|
@ -1,54 +1,27 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include <pulse/pulseaudio.h>
|
|
||||||
#include <pulse/volume.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "ALabel.hpp"
|
#include "ALabel.hpp"
|
||||||
|
#include "util/audio_backend.hpp"
|
||||||
|
|
||||||
namespace waybar::modules {
|
namespace waybar::modules {
|
||||||
|
|
||||||
class Pulseaudio : public ALabel {
|
class Pulseaudio : public ALabel {
|
||||||
public:
|
public:
|
||||||
Pulseaudio(const std::string&, const Json::Value&);
|
Pulseaudio(const std::string&, const Json::Value&);
|
||||||
virtual ~Pulseaudio();
|
virtual ~Pulseaudio() = default;
|
||||||
auto update() -> void override;
|
auto update() -> void override;
|
||||||
|
|
||||||
private:
|
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;
|
bool handleScroll(GdkEventScroll* e) override;
|
||||||
const std::vector<std::string> getPulseIcon() const;
|
const std::vector<std::string> getPulseIcon() const;
|
||||||
|
|
||||||
pa_threaded_mainloop* mainloop_;
|
std::shared_ptr<util::AudioBackend> backend = nullptr;
|
||||||
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_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace waybar::modules
|
} // namespace waybar::modules
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <json/value.h>
|
||||||
|
#include <pulse/context.h>
|
||||||
|
#include <pulse/introspect.h>
|
||||||
|
#include <pulse/thread-mainloop.h>
|
||||||
|
#include <pulse/volume.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<std::string> ignored_sinks_;
|
||||||
|
|
||||||
|
std::function<void()> 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<AudioBackend> getInstance(std::function<void()> on_updated_cb = noop);
|
||||||
|
|
||||||
|
AudioBackend(std::function<void()> 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
|
|
@ -178,7 +178,8 @@ src_files = files(
|
||||||
'src/util/sanitize_str.cpp',
|
'src/util/sanitize_str.cpp',
|
||||||
'src/util/rewrite_string.cpp',
|
'src/util/rewrite_string.cpp',
|
||||||
'src/util/gtk_icon.cpp',
|
'src/util/gtk_icon.cpp',
|
||||||
'src/util/regex_collection.cpp'
|
'src/util/regex_collection.cpp',
|
||||||
|
'src/util/audio_backend.cpp'
|
||||||
)
|
)
|
||||||
|
|
||||||
inc_dirs = ['include']
|
inc_dirs = ['include']
|
||||||
|
|
|
@ -1,74 +1,12 @@
|
||||||
#include "modules/pulseaudio.hpp"
|
#include "modules/pulseaudio.hpp"
|
||||||
|
|
||||||
waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config)
|
waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config)
|
||||||
: ALabel(config, "pulseaudio", id, "{volume}%"),
|
: 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_);
|
|
||||||
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
||||||
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll));
|
event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll));
|
||||||
}
|
|
||||||
|
|
||||||
waybar::modules::Pulseaudio::~Pulseaudio() {
|
backend = util::AudioBackend::getInstance([this] { this->dp.emit(); });
|
||||||
pa_context_disconnect(context_);
|
backend->setIgnoredSinks(config_["ignored-sinks"]);
|
||||||
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<waybar::modules::Pulseaudio *>(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<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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
|
bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
|
||||||
|
@ -81,9 +19,6 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
|
||||||
if (dir == SCROLL_DIR::NONE) {
|
if (dir == SCROLL_DIR::NONE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
|
|
||||||
pa_volume_t change = volume_tick;
|
|
||||||
pa_cvolume pa_volume = pa_volume_;
|
|
||||||
int max_volume = 100;
|
int max_volume = 100;
|
||||||
double step = 1;
|
double step = 1;
|
||||||
// isDouble returns true for integers as well, just in case
|
// 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();
|
step = config_["scroll-step"].asDouble();
|
||||||
}
|
}
|
||||||
if (config_["max-volume"].isInt()) {
|
if (config_["max-volume"].isInt()) {
|
||||||
max_volume = std::min(config_["max-volume"].asInt(), static_cast<int>(PA_VOLUME_UI_MAX));
|
max_volume = config_["max-volume"].asInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir == SCROLL_DIR::UP) {
|
auto change_type = (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::RIGHT)
|
||||||
if (volume_ < max_volume) {
|
? util::ChangeType::Increase
|
||||||
if (volume_ + step > max_volume) {
|
: util::ChangeType::Decrease;
|
||||||
change = round((max_volume - volume_) * volume_tick);
|
|
||||||
} else {
|
backend->changeVolume(change_type, step, max_volume);
|
||||||
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);
|
|
||||||
return true;
|
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<waybar::modules::Pulseaudio *>(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<waybar::modules::Pulseaudio *>(data);
|
|
||||||
if (i != nullptr && pa->default_source_name_ == i->name) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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<waybar::modules::Pulseaudio *>(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<float>(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<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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const std::array<std::string, 9> ports = {
|
static const std::array<std::string, 9> ports = {
|
||||||
"headphone", "speaker", "hdmi", "headset", "hands-free", "portable", "car", "hifi", "phone",
|
"headphone", "speaker", "hdmi", "headset", "hands-free", "portable", "car", "hifi", "phone",
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const {
|
const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const {
|
||||||
std::vector<std::string> res = {current_sink_name_, default_source_name_};
|
std::vector<std::string> res = {backend->getCurrentSinkName(), backend->getDefaultSourceName()};
|
||||||
std::string nameLC = port_name_ + form_factor_;
|
std::string nameLC = backend->getSinkPortName() + backend->getFormFactor();
|
||||||
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
|
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
|
||||||
for (auto const &port : ports) {
|
for (auto const &port : ports) {
|
||||||
if (nameLC.find(port) != std::string::npos) {
|
if (nameLC.find(port) != std::string::npos) {
|
||||||
|
@ -250,17 +57,16 @@ const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const
|
||||||
auto waybar::modules::Pulseaudio::update() -> void {
|
auto waybar::modules::Pulseaudio::update() -> void {
|
||||||
auto format = format_;
|
auto format = format_;
|
||||||
std::string tooltip_format;
|
std::string tooltip_format;
|
||||||
|
auto sink_volume = backend->getSinkVolume();
|
||||||
if (!alt_) {
|
if (!alt_) {
|
||||||
std::string format_name = "format";
|
std::string format_name = "format";
|
||||||
if (monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio
|
if (backend->isBluetooth()) {
|
||||||
monitor_.find("a2dp-sink") != std::string::npos || // PipeWire
|
|
||||||
monitor_.find("bluez") != std::string::npos) {
|
|
||||||
format_name = format_name + "-bluetooth";
|
format_name = format_name + "-bluetooth";
|
||||||
label_.get_style_context()->add_class("bluetooth");
|
label_.get_style_context()->add_class("bluetooth");
|
||||||
} else {
|
} else {
|
||||||
label_.get_style_context()->remove_class("bluetooth");
|
label_.get_style_context()->remove_class("bluetooth");
|
||||||
}
|
}
|
||||||
if (muted_) {
|
if (backend->getSinkMuted()) {
|
||||||
// Check muted bluetooth format exist, otherwise fallback to default muted format
|
// Check muted bluetooth format exist, otherwise fallback to default muted format
|
||||||
if (format_name != "format" && !config_[format_name + "-muted"].isString()) {
|
if (format_name != "format" && !config_[format_name + "-muted"].isString()) {
|
||||||
format_name = "format";
|
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("muted");
|
||||||
label_.get_style_context()->remove_class("sink-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()) {
|
if (!state.empty() && config_[format_name + "-" + state].isString()) {
|
||||||
format = config_[format_name + "-" + state].asString();
|
format = config_[format_name + "-" + state].asString();
|
||||||
} else if (config_[format_name].isString()) {
|
} 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
|
// TODO: find a better way to split source/sink
|
||||||
std::string format_source = "{volume}%";
|
std::string format_source = "{volume}%";
|
||||||
if (source_muted_) {
|
if (backend->getSourceMuted()) {
|
||||||
label_.get_style_context()->add_class("source-muted");
|
label_.get_style_context()->add_class("source-muted");
|
||||||
if (config_["format-source-muted"].isString()) {
|
if (config_["format-source-muted"].isString()) {
|
||||||
format_source = config_["format-source-muted"].asString();
|
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 = 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(
|
auto text = fmt::format(
|
||||||
fmt::runtime(format), fmt::arg("desc", desc_), fmt::arg("volume", volume_),
|
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("format_source", format_source), fmt::arg("source_volume", source_volume),
|
||||||
fmt::arg("source_desc", source_desc_), fmt::arg("icon", getIcon(volume_, getPulseIcon())));
|
fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon())));
|
||||||
if (text.empty()) {
|
if (text.empty()) {
|
||||||
label_.hide();
|
label_.hide();
|
||||||
} else {
|
} else {
|
||||||
|
@ -310,12 +121,12 @@ auto waybar::modules::Pulseaudio::update() -> void {
|
||||||
}
|
}
|
||||||
if (!tooltip_format.empty()) {
|
if (!tooltip_format.empty()) {
|
||||||
label_.set_tooltip_text(fmt::format(
|
label_.set_tooltip_text(fmt::format(
|
||||||
fmt::runtime(tooltip_format), fmt::arg("desc", desc_), fmt::arg("volume", volume_),
|
fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc),
|
||||||
fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_),
|
fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source),
|
||||||
fmt::arg("source_desc", source_desc_),
|
fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc),
|
||||||
fmt::arg("icon", getIcon(volume_, getPulseIcon()))));
|
fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))));
|
||||||
} else {
|
} else {
|
||||||
label_.set_tooltip_text(desc_);
|
label_.set_tooltip_text(sink_desc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
#include "util/audio_backend.hpp"
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <pulse/error.h>
|
||||||
|
#include <pulse/subscribe.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace waybar::util {
|
||||||
|
|
||||||
|
void noop() {}
|
||||||
|
|
||||||
|
AudioBackend::AudioBackend(std::function<void()> 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> AudioBackend::getInstance(std::function<void()> on_updated_cb) {
|
||||||
|
private_constructor_tag tag;
|
||||||
|
return std::make_shared<AudioBackend>(on_updated_cb, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioBackend::contextStateCb(pa_context *c, void *data) {
|
||||||
|
auto backend = static_cast<AudioBackend *>(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<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);
|
||||||
|
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<AudioBackend *>(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<AudioBackend *>(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<float>(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<AudioBackend *>(data);
|
||||||
|
if (i != nullptr && backend->default_source_name_ == i->name) {
|
||||||
|
auto source_volume = static_cast<float>(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<AudioBackend *>(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<double>(PA_VOLUME_NORM) / 100;
|
||||||
|
pa_volume_t change = volume_tick;
|
||||||
|
pa_cvolume pa_volume = pa_volume_;
|
||||||
|
|
||||||
|
max_volume = std::min(max_volume, static_cast<int>(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
|
Loading…
Reference in New Issue