fix(pipewire): include device route mute in effective source/sink state

This commit is contained in:
Ly-sec
2026-05-03 10:45:16 +02:00
parent 32f28589aa
commit 4aafcf5a31
2 changed files with 100 additions and 71 deletions
+92 -71
View File
@@ -22,6 +22,7 @@
#include <spa/pod/builder.h>
#include <spa/pod/iter.h>
#include <spa/pod/parser.h>
#include <spa/utils/defs.h>
#include <spa/utils/result.h>
#include <string>
#include <string_view>
@@ -193,26 +194,6 @@ namespace {
return std::nullopt;
}
void applyVolumePropsFromDict(PipeWireService::NodeData& nd, const spa_dict* props) {
if (props == nullptr) {
return;
}
if (const auto maybeChannelmixVolume = parseFloat(dictGet(props, "channelmix.volume"));
maybeChannelmixVolume.has_value()) {
nd.volume = std::clamp(*maybeChannelmixVolume, 0.0f, 1.5f);
} else if (const auto maybeVolume = parseFloat(dictGet(props, "volume")); maybeVolume.has_value()) {
nd.volume = std::clamp(*maybeVolume, 0.0f, 1.5f);
}
if (const auto maybeChannelmixMuted = parseBool(dictGet(props, "channelmix.mute"));
maybeChannelmixMuted.has_value()) {
nd.muted = *maybeChannelmixMuted;
} else if (const auto maybeMuted = parseBool(dictGet(props, "mute")); maybeMuted.has_value()) {
nd.muted = *maybeMuted;
}
}
bool applyClientPropsFromDict(PipeWireService::ClientData& client, const spa_dict* props) {
if (props == nullptr) {
return false;
@@ -623,6 +604,12 @@ void PipeWireService::onRegistryGlobalRemove(std::uint32_t id) {
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(it->second.proxy));
}
m_devices.erase(it);
for (auto& [nid, node] : m_nodes) {
if (node != nullptr && node->deviceId == id) {
recomputeEffectiveMute(*node);
}
}
rebuildState();
return;
}
@@ -755,13 +742,14 @@ void PipeWireService::onNodeParam(std::uint32_t id, std::uint32_t paramId, std::
auto* propsObj = reinterpret_cast<spa_pod_object*>(const_cast<spa_pod*>(routeProps));
SPA_POD_OBJECT_FOREACH(propsObj, prop) {
if (prop->key == SPA_PROP_mute) {
bool muted = false;
if (spa_pod_get_bool(&prop->value, &muted) == 0) {
nd.muted = muted;
bool routeMuted = false;
if (spa_pod_get_bool(&prop->value, &routeMuted) == 0) {
nd.nodeRouteMute = routeMuted;
}
}
}
}
recomputeEffectiveMute(nd);
rebuildState();
}
return;
@@ -794,9 +782,9 @@ void PipeWireService::onNodeParam(std::uint32_t id, std::uint32_t paramId, std::
parseVolumeArrayProp(prop, parsedSoftVolumes);
hasSoftVolumes = true;
} else if (prop->key == SPA_PROP_mute) {
bool muted = false;
if (spa_pod_get_bool(&prop->value, &muted) == 0) {
nd.muted = muted;
bool swMuted = false;
if (spa_pod_get_bool(&prop->value, &swMuted) == 0) {
nd.swMute = swMuted;
}
}
}
@@ -810,6 +798,8 @@ void PipeWireService::onNodeParam(std::uint32_t id, std::uint32_t paramId, std::
nd.volume = parsedSoftVolumes;
}
recomputeEffectiveMute(nd);
if (isProgramStreamClass(nd.mediaClass)) {
kLog.debug("[program-stream] node-param id={} class='{}' volume={:.3f} muted={} channels={}", id, nd.mediaClass,
nd.volume, nd.muted, nd.channelCount);
@@ -908,6 +898,13 @@ void PipeWireService::onDeviceParam(std::uint32_t id, std::uint32_t paramId, std
existing->direction = routeDirection;
existing->muted = muted;
}
for (auto& [nid, node] : m_nodes) {
if (node != nullptr && node->deviceId == id) {
recomputeEffectiveMute(*node);
}
}
rebuildState();
}
void PipeWireService::parseDefaultNodes(const spa_dict* props) {
@@ -1000,6 +997,56 @@ void PipeWireService::rebuildState() {
emitChanged();
}
bool PipeWireService::deviceRouteIndicatesMuted(const NodeData& nd) const {
if (nd.deviceId == 0) {
return false;
}
const auto it = m_devices.find(nd.deviceId);
if (it == m_devices.end()) {
return false;
}
std::uint32_t wantDir = 0;
if (nd.mediaClass == "Audio/Source") {
wantDir = SPA_DIRECTION_INPUT;
} else if (nd.mediaClass == "Audio/Sink") {
wantDir = SPA_DIRECTION_OUTPUT;
} else {
return false;
}
for (const auto& r : it->second.routes) {
if (r.direction == wantDir && r.index >= 0 && r.muted) {
return true;
}
}
return false;
}
void PipeWireService::recomputeEffectiveMute(NodeData& nd) {
nd.muted = nd.swMute || nd.nodeRouteMute || deviceRouteIndicatesMuted(nd);
}
void PipeWireService::applyVolumePropsFromDict(NodeData& nd, const spa_dict* props) {
if (props == nullptr) {
return;
}
if (const auto maybeChannelmixVolume = parseFloat(dictGet(props, "channelmix.volume"));
maybeChannelmixVolume.has_value()) {
nd.volume = std::clamp(*maybeChannelmixVolume, 0.0f, 1.5f);
} else if (const auto maybeVolume = parseFloat(dictGet(props, "volume")); maybeVolume.has_value()) {
nd.volume = std::clamp(*maybeVolume, 0.0f, 1.5f);
}
if (const auto maybeChannelmixMuted = parseBool(dictGet(props, "channelmix.mute"));
maybeChannelmixMuted.has_value()) {
nd.swMute = *maybeChannelmixMuted;
} else if (const auto maybeMuted = parseBool(dictGet(props, "mute")); maybeMuted.has_value()) {
nd.swMute = *maybeMuted;
}
recomputeEffectiveMute(nd);
}
void PipeWireService::scheduleVolumeFlush() {
const auto now = std::chrono::steady_clock::now();
const auto earliest = m_lastVolumeFlushValid ? (m_lastVolumeFlushAt + kVolumeApplyMinInterval)
@@ -1116,53 +1163,23 @@ void PipeWireService::setNodeMuted(std::uint32_t id, bool muted) {
return;
}
// Match WirePlumber session policy (same as set-volume) so mute state stays consistent
// with wpctl and survives odd daemon/node prop ordering after resume/reboot.
const bool isDeviceNode = nd.mediaClass == "Audio/Sink" || nd.mediaClass == "Audio/Source";
if (isDeviceNode && nd.deviceId != 0) {
auto devIt = m_devices.find(nd.deviceId);
if (devIt != m_devices.end() && devIt->second.proxy != nullptr) {
const std::uint32_t targetDirection =
(nd.mediaClass == "Audio/Source") ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT;
bool wroteDeviceRoute = false;
for (const auto& route : devIt->second.routes) {
if (route.index < 0 || route.direction != targetDirection) {
continue;
}
std::uint8_t routeBuffer[512];
spa_pod_builder routeBuilder;
spa_pod_builder_init(&routeBuilder, routeBuffer, sizeof(routeBuffer));
spa_pod_frame routeFrame;
spa_pod_builder_push_object(&routeBuilder, &routeFrame, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route);
spa_pod_builder_prop(&routeBuilder, SPA_PARAM_ROUTE_index, 0);
spa_pod_builder_int(&routeBuilder, route.index);
spa_pod_builder_prop(&routeBuilder, SPA_PARAM_ROUTE_direction, 0);
spa_pod_builder_id(&routeBuilder, route.direction);
spa_pod_builder_prop(&routeBuilder, SPA_PARAM_ROUTE_device, 0);
spa_pod_builder_int(&routeBuilder, route.device);
spa_pod_builder_prop(&routeBuilder, SPA_PARAM_ROUTE_props, 0);
spa_pod_frame routePropsFrame;
spa_pod_builder_push_object(&routeBuilder, &routePropsFrame, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
spa_pod_builder_prop(&routeBuilder, SPA_PROP_mute, 0);
spa_pod_builder_bool(&routeBuilder, muted);
spa_pod_builder_pop(&routeBuilder, &routePropsFrame);
spa_pod_builder_prop(&routeBuilder, SPA_PARAM_ROUTE_save, 0);
spa_pod_builder_bool(&routeBuilder, true);
auto* routePod = static_cast<spa_pod*>(spa_pod_builder_pop(&routeBuilder, &routeFrame));
pw_device_set_param(devIt->second.proxy, SPA_PARAM_Route, 0, routePod);
wroteDeviceRoute = true;
}
if (wroteDeviceRoute) {
if (nd.muted != muted) {
nd.muted = muted;
rebuildState();
}
return;
if (isDeviceNode) {
const bool updatedViaWpctl = process::runSync({"wpctl", "set-mute", std::to_string(id), muted ? "1" : "0"});
if (updatedViaWpctl) {
const bool before = nd.muted;
nd.swMute = muted;
recomputeEffectiveMute(nd);
if (before != nd.muted) {
rebuildState();
}
return;
}
}
// Program streams, or device fallback when wpctl is unavailable.
if (nd.hasRoute && nd.routeIndex >= 0) {
std::uint8_t routeBuffer[512];
spa_pod_builder routeBuilder;
@@ -1200,9 +1217,13 @@ void PipeWireService::setNodeMuted(std::uint32_t id, bool muted) {
pw_node_set_param(nd.proxy, SPA_PARAM_Props, 0, pod);
// Apply optimistic local state while PipeWire publishes props.
if (nd.muted != muted) {
nd.muted = muted;
const bool before = nd.muted;
nd.swMute = muted;
if (nd.hasRoute && nd.routeIndex >= 0) {
nd.nodeRouteMute = muted;
}
recomputeEffectiveMute(nd);
if (before != nd.muted) {
rebuildState();
}
}
+8
View File
@@ -17,6 +17,7 @@ struct pw_device;
struct pw_loop;
struct pw_registry;
struct spa_hook;
struct spa_dict;
class ConfigService;
class IpcService;
@@ -112,6 +113,10 @@ public:
std::string iconName;
std::string mediaClass;
float volume = 1.0f;
// Software / node-route mute from PipeWire props (SPA_PARAM_Props, node routes).
bool swMute = false;
bool nodeRouteMute = false;
// Effective mute for UI (includes device-route mute, e.g. USB mic hardware switch).
bool muted = false;
std::uint32_t channelCount = 0;
std::uint32_t deviceId = 0;
@@ -159,6 +164,9 @@ public:
private:
void rebuildState();
void refreshNodeIdentity(NodeData& nd);
void applyVolumePropsFromDict(NodeData& nd, const spa_dict* props);
void recomputeEffectiveMute(NodeData& nd);
[[nodiscard]] bool deviceRouteIndicatesMuted(const NodeData& nd) const;
void setNodeVolume(std::uint32_t id, float volume);
void setNodeMuted(std::uint32_t id, bool muted);
void setDefaultNode(std::uint32_t id, const char* key);