Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolves #3092 Add source support to wireplumber module #3638

Merged
merged 2 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion include/modules/wireplumber.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Wireplumber : public ALabel {

private:
void asyncLoadRequiredApiModules();
void prepare();
void prepare(waybar::modules::Wireplumber* self);
void activatePlugins();
static void updateVolume(waybar::modules::Wireplumber* self, uint32_t id);
static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id);
Expand All @@ -32,6 +32,8 @@ class Wireplumber : public ALabel {

bool handleScroll(GdkEventScroll* e) override;

static std::list<waybar::modules::Wireplumber*> modules;

WpCore* wp_core_;
GPtrArray* apis_;
WpObjectManager* om_;
Expand All @@ -44,6 +46,7 @@ class Wireplumber : public ALabel {
double min_step_;
uint32_t node_id_{0};
std::string node_name_;
gchar* type_;
};

} // namespace waybar::modules
27 changes: 27 additions & 0 deletions man/waybar-wireplumber.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber.
typeof: string ++
This format is used when the sound is muted.

*node-type*: ++
typeof: string ++
default: *Audio/Sink* ++
The WirePlumber node type to attach to. Use *Audio/Source* to manage microphones etc.

*tooltip*: ++
typeof: bool ++
default: *true* ++
Expand Down Expand Up @@ -108,6 +113,8 @@ The *wireplumber* module displays the current volume reported by WirePlumber.

# EXAMPLES

## Basic:

```
"wireplumber": {
"format": "{volume}%",
Expand All @@ -116,6 +123,26 @@ The *wireplumber* module displays the current volume reported by WirePlumber.
}
```

## Separate Sink and Source Widgets

```
"wireplumber#sink": {
"format": "{volume}% {icon}",
"format-muted": "",
"format-icons": ["", "", ""],
"on-click": "helvum",
"on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"scroll-step": 5
},
"wireplumber#source": {
"node-type": "Audio/Source",
"format": "{volume}% ",
"format-muted": "",
"on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle",
"scroll-step": 5
}
```

# STYLE

- *#wireplumber*
Expand Down
98 changes: 61 additions & 37 deletions src/modules/wireplumber.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; }

std::list<waybar::modules::Wireplumber*> waybar::modules::Wireplumber::modules;

waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config)
: ALabel(config, "wireplumber", id, "{volume}%"),
wp_core_(nullptr),
Expand All @@ -16,43 +18,52 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
muted_(false),
volume_(0.0),
min_step_(0.0),
node_id_(0) {
node_id_(0),
type_(nullptr) {
waybar::modules::Wireplumber::modules.push_back(this);

wp_init(WP_INIT_PIPEWIRE);
wp_core_ = wp_core_new(nullptr, nullptr, nullptr);
apis_ = g_ptr_array_new_with_free_func(g_object_unref);
om_ = wp_object_manager_new();

prepare();
type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str()
: "Audio/Sink");

spdlog::debug("[{}]: connecting to pipewire...", name_);
prepare(this);

spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_);

if (wp_core_connect(wp_core_) == 0) {
spdlog::error("[{}]: Could not connect to PipeWire", name_);
spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_);
throw std::runtime_error("Could not connect to PipeWire\n");
}

spdlog::debug("[{}]: connected!", name_);
spdlog::debug("[{}]: {} connected!", name_, type_);

g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this);

asyncLoadRequiredApiModules();
}

waybar::modules::Wireplumber::~Wireplumber() {
waybar::modules::Wireplumber::modules.remove(this);
wp_core_disconnect(wp_core_);
g_clear_pointer(&apis_, g_ptr_array_unref);
g_clear_object(&om_);
g_clear_object(&wp_core_);
g_clear_object(&mixer_api_);
g_clear_object(&def_nodes_api_);
g_free(default_node_name_);
g_free(type_);
}

void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: updating node name with node.id {}", self->name_, id);
spdlog::debug("[{}]: updating '{}' node name with node.id {}", self->name_, self->type_, id);

if (!isValidNodeId(id)) {
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node name update.", self->name_, id);
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node name update.", self->name_,
id, self->type_);
return;
}

Expand Down Expand Up @@ -80,15 +91,16 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber*
self->node_name_ = nick != nullptr ? nick
: description != nullptr ? description
: "Unknown node name";
spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_);
spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_);
}

void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: updating volume", self->name_);
GVariant* variant = nullptr;

if (!isValidNodeId(id)) {
spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id);
spdlog::error("[{}]: '{}' is not a valid '{}' node ID. Ignoring volume update.", self->name_,
id, self->type_);
return;
}

Expand All @@ -109,40 +121,50 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se
}

void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) {
spdlog::debug("[{}]: (onMixerChanged) - id: {}", self->name_, id);

g_autoptr(WpNode) node = static_cast<WpNode*>(wp_object_manager_lookup(
self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr));

if (node == nullptr) {
spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id);
// log a warning only if no other widget is targeting the id.
// this reduces log spam when multiple instances of the module are used on different node types.
if (id != self->node_id_) {
for (auto const& module : waybar::modules::Wireplumber::modules) {
if (module->node_id_ == id) {
return;
}
}
}

spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_,
self->type_, id);
return;
}

const gchar* name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");

if (self->node_id_ != id) {
spdlog::debug(
"[{}]: (onMixerChanged) - ignoring mixer update for node: id: {}, name: {} as it is not "
"the default node: {} with id: {}",
self->name_, id, name, self->default_node_name_, self->node_id_);
"[{}]: (onMixerChanged: {}) - ignoring mixer update for node: id: {}, name: {} as it is "
"not the default node: {} with id: {}",
self->name_, self->type_, id, name, self->default_node_name_, self->node_id_);
return;
}

spdlog::debug("[{}]: (onMixerChanged) - Need to update volume for node with id {} and name {}",
self->name_, id, name);
spdlog::debug(
"[{}]: (onMixerChanged: {}) - Need to update volume for node with id {} and name {}",
self->name_, self->type_, id, name);
updateVolume(self, id);
}

void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) {
spdlog::debug("[{}]: (onDefaultNodesApiChanged)", self->name_);
spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_);

uint32_t defaultNodeId;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &defaultNodeId);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId);

if (!isValidNodeId(defaultNodeId)) {
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_,
defaultNodeId);
spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node change.", self->name_,
defaultNodeId, self->type_);
return;
}

Expand All @@ -151,30 +173,31 @@ void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wir
"=u", defaultNodeId, nullptr));

if (node == nullptr) {
spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_,
defaultNodeId);
spdlog::warn("[{}]: (onDefaultNodesApiChanged: {}) - Object with id {} not found", self->name_,
self->type_, defaultNodeId);
return;
}

const gchar* defaultNodeName =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");

spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - got the following default node: Node(name: {}, id: {})",
self->name_, defaultNodeName, defaultNodeId);
"[{}]: (onDefaultNodesApiChanged: {}) - got the following default node: Node(name: {}, id: "
"{})",
self->name_, self->type_, defaultNodeName, defaultNodeId);

if (g_strcmp0(self->default_node_name_, defaultNodeName) == 0 &&
self->node_id_ == defaultNodeId) {
spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). "
"Ignoring.",
self->name_, self->default_node_name_, defaultNodeId);
"[{}]: (onDefaultNodesApiChanged: {}) - Default node has not changed. Node(name: {}, id: "
"{}). Ignoring.",
self->name_, self->type_, self->default_node_name_, defaultNodeId);
return;
}

spdlog::debug(
"[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})",
self->name_, defaultNodeName, defaultNodeId);
"[{}]: (onDefaultNodesApiChanged: {}) - Default node changed to -> Node(name: {}, id: {})",
self->name_, self->type_, defaultNodeName, defaultNodeId);

g_free(self->default_node_name_);
self->default_node_name_ = g_strdup(defaultNodeName);
Expand All @@ -200,13 +223,14 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir
throw std::runtime_error("Mixer api is not loaded\n");
}

g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Sink",
g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_,
&self->default_node_name_);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &self->node_id_);
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_);

if (self->default_node_name_ != nullptr) {
spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}",
self->name_, self->default_node_name_, self->node_id_);
spdlog::debug(
"[{}]: (onObjectManagerInstalled: {}) - default configured node name: {} and id: {}",
self->name_, self->type_, self->default_node_name_, self->node_id_);
}

updateVolume(self, self->node_id_);
Expand Down Expand Up @@ -243,10 +267,10 @@ void waybar::modules::Wireplumber::activatePlugins() {
}
}

void waybar::modules::Wireplumber::prepare() {
spdlog::debug("[{}]: preparing object manager", name_);
void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) {
spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_);
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Sink", nullptr);
"=s", self->type_, nullptr);
}

void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res,
Expand Down