From a00ff0394431d1fe3f33ae0934c981930e2a1efb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 14 Nov 2025 02:12:42 -0800 Subject: [PATCH] services/pipewire: cache route device volumes to initialize nodes Nodes referencing a device can be bound later than the device is bound. If this happens, the node will not receive an initial route device volume change event. This change caches the last known route device volume and initializes the device with it if present. --- changelog/next.md | 1 + src/services/pipewire/device.cpp | 10 ++++++++++ src/services/pipewire/device.hpp | 3 +++ src/services/pipewire/node.cpp | 13 ++++++++++++- src/services/pipewire/node.hpp | 2 ++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/changelog/next.md b/changelog/next.md index 5f5aa34..4b255ff 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -24,6 +24,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed escape sequence handling in desktop entries. +- Fixed volumes not initializing if a pipewire device was already loaded before its node. ## Packaging Changes diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 0c111fa..314fd63 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -125,12 +125,22 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { // Insert into the main map as well, staging's purpose is to remove old entries. this->routeDeviceIndexes.insert(device, index); + // Used for initial node volume if the device is bound before the node + // (e.g. multiple nodes pointing to the same device) + this->routeDeviceVolumes.insert(device, volumeProps); + qCDebug(logDevice).nospace() << "Registered device/index pair for " << this << ": [device: " << device << ", index: " << index << ']'; emit this->routeVolumesChanged(device, volumeProps); } +bool PwDevice::tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps) { + if (!this->routeDeviceVolumes.contains(routeDevice)) return false; + volumeProps = this->routeDeviceVolumes.value(routeDevice); + return true; +} + void PwDevice::polled() { // It is far more likely that the list content has not come in yet than it having no entries, // and there isn't a way to check in the case that there *aren't* actually any entries. diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 1a1f705..22af699 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -32,6 +32,8 @@ public: void waitForDevice(); [[nodiscard]] bool waitingForDevice() const; + [[nodiscard]] bool tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps); + signals: void deviceReady(); void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); @@ -46,6 +48,7 @@ private: onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); QHash routeDeviceIndexes; + QHash routeDeviceVolumes; QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index f336558..1eceab9 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -218,6 +218,7 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { } self->routeDevice = id; + if (self->boundData) self->boundData->onDeviceChanged(); } else { qCCritical(logNode) << self << "has attached device" << self->device << "but no card.profile.device property."; @@ -277,6 +278,15 @@ PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): QObject(node), node(node) { } } +void PwNodeBoundAudio::onDeviceChanged() { + PwVolumeProps volumeProps; + if (this->node->device->tryLoadVolumeProps(this->node->routeDevice, volumeProps)) { + qCDebug(logNode) << "Initializing volume props for" << this->node + << "with known values from backing device."; + this->updateVolumeProps(volumeProps); + } +} + void PwNodeBoundAudio::onInfo(const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) != 0) { for (quint32 i = 0; i < info->n_params; i++) { @@ -299,7 +309,8 @@ void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* para if (id == SPA_PARAM_Props && index == 0) { if (this->node->shouldUseDevice()) { qCDebug(logNode) << "Skipping node volume props update for" << this->node - << "in favor of device updates."; + << "in favor of device updates from routeDevice" << this->node->routeDevice + << "of" << this->node->device; return; } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 359c0f3..e3e1913 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -169,6 +169,7 @@ public: virtual ~PwNodeBoundData() = default; Q_DISABLE_COPY_MOVE(PwNodeBoundData); + virtual void onDeviceChanged() {}; virtual void onInfo(const pw_node_info* /*info*/) {} virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {} virtual void onUnbind() {} @@ -182,6 +183,7 @@ class PwNodeBoundAudio public: explicit PwNodeBoundAudio(PwNode* node); + void onDeviceChanged() override; void onInfo(const pw_node_info* info) override; void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override; void onUnbind() override;