Skip to content
Open
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
7 changes: 7 additions & 0 deletions include/modules/wireplumber.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class Wireplumber : public ALabel {

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

uint32_t resolvePhysicalSink(uint32_t start_id);
uint32_t findPlaybackNodeId(const gchar* description);
uint32_t get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id);
uint32_t get_linked_node_from_output_ports(WpObjectManager* om, uint32_t from_node_id);

WpCore* wp_core_;
GPtrArray* apis_;
WpObjectManager* om_;
Expand All @@ -54,6 +59,8 @@ class Wireplumber : public ALabel {
bool source_muted_;
double source_volume_;
gchar* default_source_name_;
bool only_physical_;
bool resolved_physical_;
};

} // namespace waybar::modules
260 changes: 243 additions & 17 deletions src/modules/wireplumber.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "modules/wireplumber.hpp"
#include <unordered_set>

#include <spdlog/spdlog.h>

Expand All @@ -25,7 +26,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
source_node_id_(0),
source_muted_(false),
source_volume_(0.0),
default_source_name_(nullptr) {
default_source_name_(nullptr),
only_physical_(false) {
waybar::modules::Wireplumber::modules.push_back(this);

wp_init(WP_INIT_PIPEWIRE);
Expand All @@ -35,6 +37,7 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val

type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str()
: "Audio/Sink");
only_physical_ = config_["only-physical"].isBool() ? config_["only-physical"].asBool() : false;

prepare(this);

Expand Down Expand Up @@ -226,28 +229,34 @@ void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wir
spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_);

// Handle sink
uint32_t defaultNodeId;
uint32_t defaultNodeId = 0;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId);

if (isValidNodeId(defaultNodeId)) {
uint32_t effective_id = self->only_physical_ ? self->resolvePhysicalSink(defaultNodeId) : defaultNodeId;

if (self->only_physical_ && effective_id != defaultNodeId) {
spdlog::info("[{}]: only-physical enabled: using sink {} instead of default {}", self->name_, effective_id, defaultNodeId);
}

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

if (node != nullptr) {
const gchar* defaultNodeName =
const gchar* effectiveNodeName =
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name");

if (g_strcmp0(self->default_node_name_, defaultNodeName) != 0 ||
self->node_id_ != defaultNodeId) {
spdlog::debug("[{}]: Default sink changed to -> Node(name: {}, id: {})", self->name_,
defaultNodeName, defaultNodeId);
if (g_strcmp0(self->default_node_name_, effectiveNodeName) != 0 ||
self->node_id_ != effective_id) {
spdlog::debug("[{}]: Default sink resolved to -> Node(name: {}, id: {})", self->name_,
effectiveNodeName, effective_id);

g_free(self->default_node_name_);
self->default_node_name_ = g_strdup(defaultNodeName);
self->node_id_ = defaultNodeId;
updateVolume(self, defaultNodeId);
updateNodeName(self, defaultNodeId);
self->default_node_name_ = g_strdup(effectiveNodeName);
self->node_id_ = effective_id;
updateVolume(self, effective_id);
updateNodeName(self, effective_id);
}
}
}
Expand Down Expand Up @@ -300,7 +309,16 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir
// Get default 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", self->type_, &self->node_id_);
uint32_t initial_sink_id = 0;
g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &initial_sink_id);

if (self->only_physical_ && isValidNodeId(initial_sink_id)) {
self->node_id_ = self->resolvePhysicalSink(initial_sink_id);
spdlog::info("[{}]: only-physical enabled: initial physical sink {} (default was {})",
self->name_, self->node_id_, initial_sink_id);
} else {
self->node_id_ = initial_sink_id;
}

// Get default source
g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Source",
Expand Down Expand Up @@ -356,10 +374,17 @@ void waybar::modules::Wireplumber::activatePlugins() {

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", self->type_, nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Source", nullptr);
if(only_physical_){
wp_object_manager_add_interest(om_, WP_TYPE_NODE, nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_LINK, nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_PORT, nullptr);
wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, WP_OBJECT_FEATURES_ALL);
} else {
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", self->type_, nullptr);
wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class",
"=s", "Audio/Source", nullptr);
}
}

void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res,
Expand Down Expand Up @@ -530,3 +555,204 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) {
}
return true;
}

// Finds the output node for filter chains defined in pipewire,
// since their input nodes are NOT providing actual outputs
uint32_t waybar::modules::Wireplumber::findPlaybackNodeId(const gchar* description) {
if (!description || *description == '\0') {
return 0;
}

spdlog::debug("[{}]: Searching for playback node with node.description = {}", name_, description);

g_autoptr(WpIterator) it = wp_object_manager_new_filtered_iterator(
om_, WP_TYPE_NODE,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.description", "=s", description,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Stream/Output/Audio",
nullptr);

uint32_t playback_id = 0;

g_auto(GValue) item = G_VALUE_INIT;
if(wp_iterator_next(it, &item)) {
WpNode* output_node = WP_NODE(g_value_get_object(&item));
playback_id = wp_proxy_get_bound_id(WP_PROXY(output_node));

spdlog::debug("[{}]: Found matching playback node id {}", name_, playback_id);
}
g_value_unset(&item);

if (playback_id == 0) {
spdlog::debug("[{}]: No playback node found with description '{}'", name_, description);
}

return playback_id;
}

uint32_t waybar::modules::Wireplumber::get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id) {
spdlog::debug("[{}]: Searching for links connected to node {}", name_, from_node_id);

g_autoptr(WpIterator) out_it = wp_object_manager_new_filtered_iterator(
om, WP_TYPE_LINK,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "link.output.node", "=u", from_node_id,
nullptr);

g_auto(GValue) item = G_VALUE_INIT;
if(wp_iterator_next(out_it, &item)) {
WpLink* link = WP_LINK(g_value_get_object(&item));
guint32 out_node, out_port, in_node, in_port;
wp_link_get_linked_object_ids(link, &out_node, &out_port, &in_node, &in_port);

spdlog::debug("[{}]: Found outgoing link {} -> {}", name_, out_node, in_node);

g_value_unset(&item);
return in_node;
}
g_value_unset(&item);

spdlog::debug("[{}]: No links found connected to node {}", name_, from_node_id);
return 0;
}

// Follow non-monitor output ports to the next node
uint32_t waybar::modules::Wireplumber::get_linked_node_from_output_ports(WpObjectManager* om, uint32_t from_node_id) {
spdlog::debug("[{}]: Searching for non-monitor output ports on node {}", name_, from_node_id);

g_autoptr(WpIterator) port_it = wp_object_manager_new_filtered_iterator(
om, WP_TYPE_PORT,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.id", "=u", from_node_id,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.direction", "=s", "out",
nullptr);

g_auto(GValue) port_item = G_VALUE_INIT;
while (wp_iterator_next(port_it, &port_item)) {
WpPort* port = WP_PORT(g_value_get_object(&port_item));

g_autoptr(WpProperties) port_props = wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(port));
if (!port_props) {
g_value_unset(&port_item);
continue;
}

const gchar* name = wp_properties_get(port_props, "port.name");

// WP_CONSTRAINT_VERB_MATCHES uses GPatternSpec and it is glob-like. Unfortunately, there is no way to
// express "not beginning with a string" in glob-style regex. Or at least I didn't figure out how to do that.
// Therefore, just filter them out with a conditional. Performance difference should be negligible anyway.
if (!name || g_str_has_prefix(name, "monitor_")) {
g_value_unset(&port_item);
continue;
}

spdlog::debug("[{}]: Found non-monitor output port with name '{}'", name_, name);

// Find outgoing link from this port
uint32_t port_id = wp_proxy_get_bound_id(WP_PROXY(port));

g_autoptr(WpIterator) link_it = wp_object_manager_new_filtered_iterator(
om, WP_TYPE_LINK,
WP_CONSTRAINT_TYPE_PW_PROPERTY, "link.output.port", "=u", port_id,
nullptr);

g_auto(GValue) link_item = G_VALUE_INIT;
if (wp_iterator_next(link_it, &link_item)) {
WpLink* link = WP_LINK(g_value_get_object(&link_item));
guint32 out_node, out_port, in_node, in_port;
wp_link_get_linked_object_ids(link, &out_node, &out_port, &in_node, &in_port);

spdlog::debug("[{}]: Found link from port {} (node {}) -> node {}", name_, port_id, from_node_id, in_node);
g_value_unset(&link_item);
g_value_unset(&port_item);
return in_node;
}
g_value_unset(&link_item);

g_value_unset(&port_item);
}
g_value_unset(&port_item);

spdlog::debug("[{}]: No non-monitor output ports with links on node {}", name_, from_node_id);
return 0;
}

uint32_t waybar::modules::Wireplumber::resolvePhysicalSink(uint32_t start_id) {
if (!isValidNodeId(start_id) || !only_physical_) {
return start_id;
}

std::unordered_set<uint32_t> visited;
uint32_t current_id = start_id;
int depth = 0;
const int max_depth = 10;

spdlog::debug("[{}]: Starting physical sink resolution from id {}", name_, start_id);

// Follow the output node chain until a physical device is found
while (visited.insert(current_id).second && depth++ < max_depth) {
g_autoptr(WpProxy) proxy = static_cast<WpProxy*>(wp_object_manager_lookup(
om_, WP_TYPE_GLOBAL_PROXY,
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", current_id,
nullptr));

if (!proxy || !WP_IS_PIPEWIRE_OBJECT(proxy)) {
spdlog::warn("[{}]: Node {} not found during resolution", name_, current_id);
break;
}

// 1: If it has a device.id, we found the physical sink
g_autoptr(WpProperties) props = wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy));
if (!props) props = wp_properties_new_empty();

const gchar* device_id = wp_properties_get(props, "device.id");
if (device_id != nullptr) {
spdlog::debug("[{}]: Found physical sink {} (device.id = {})", name_, current_id, device_id);
break;
}

spdlog::debug("[{}]: Node {} is virtual, trying direct output ports", name_, current_id);

// 2: Try following non-monitor output ports
uint32_t next_id = get_linked_node_from_output_ports(om_, current_id);
if (next_id != 0) {
spdlog::debug("[{}]: Following direct output port link to node {}", name_, next_id);
current_id = next_id;
continue;
}

// 3: Search for audio stream node
// (pipewire filter chains create a node for input and a separate node for output)
const gchar* description = wp_properties_get(props, "node.description");
if (!description || *description == '\0') {
spdlog::warn("[{}]: Virtual node {} has no description/nick - cannot search playback node", name_, current_id);
break;
}

spdlog::debug("[{}]: No direct output ports, searching playback node for description '{}'", name_, description);

uint32_t playback_id = findPlaybackNodeId(description);
if (playback_id == 0) {
spdlog::warn("[{}]: No playback node found for virtual sink {} - stopping at virtual sink", name_, current_id);
break;
}

next_id = get_linked_sink_id(om_, playback_id);
if (next_id == 0) {
spdlog::warn("[{}]: Playback node {} has no outgoing links - stopping at virtual sink {}", name_, playback_id, current_id);
break;
}

spdlog::debug("[{}]: Following playback node link to node {}", name_, next_id);
current_id = next_id;
}


GVariant* variant = nullptr;
g_signal_emit_by_name(mixer_api_, "get-volume", current_id, &variant);

if (variant == nullptr) {
spdlog::warn("[{}]: Node {} does not support volume - fallback to default sink id", name_, current_id);
current_id = start_id;
}
spdlog::info("[{}]: Final resolved sink id {}", name_, current_id);
return current_id;
}