mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
fix(pipewire): include device route mute in effective source/sink state
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user