diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 0a1e1b6a..649b9c64 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,7 @@ #include #include "core.hpp" +#include "node.hpp" namespace qs::service::pipewire { @@ -104,30 +106,43 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { qint32 device = 0; qint32 index = 0; + spa_pod* props = nullptr; + // clang-format off quint32 id = SPA_PARAM_Route; spa_pod_parser_get_object( &parser, SPA_TYPE_OBJECT_ParamRoute, &id, SPA_PARAM_ROUTE_device, SPA_POD_Int(&device), - SPA_PARAM_ROUTE_index, SPA_POD_Int(&index) + SPA_PARAM_ROUTE_index, SPA_POD_Int(&index), + SPA_PARAM_ROUTE_props, SPA_POD_PodObject(&props) ); // clang-format on - this->stagingIndexes.insert(device, index); + auto volumeProps = PwVolumeProps::parseSpaPod(props); + + this->stagingIndexes.append(device); // Insert into the main map as well, staging's purpose is to remove old entries. this->routeDeviceIndexes.insert(device, index); qCDebug(logDevice).nospace() << "Registered device/index pair for " << this << ": [device: " << device << ", index: " << index << ']'; + + emit this->routeVolumesChanged(device, volumeProps); } 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. - if (!this->stagingIndexes.isEmpty() && this->stagingIndexes != this->routeDeviceIndexes) { - this->routeDeviceIndexes = this->stagingIndexes; - qCDebug(logDevice) << "Updated device/index pair list for" << this << "to" - << this->routeDeviceIndexes; + if (!this->stagingIndexes.isEmpty()) { + this->routeDeviceIndexes.removeIf([&](const std::pair& entry) { + if (!stagingIndexes.contains(entry.first)) { + qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first + << ", index: " << entry.second << "] for" << this; + return true; + } + + return false; + }); } } diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 2e14d615..1a1f705f 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include "core.hpp" +#include "node.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -32,6 +34,7 @@ public: signals: void deviceReady(); + void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); private slots: void polled(); @@ -43,7 +46,7 @@ private: onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); QHash routeDeviceIndexes; - QHash stagingIndexes; + QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); bool diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 2aff5958..ed65fcaf 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -255,6 +255,13 @@ void PwNode::onParam( PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): node(node) { if (node->device) { QObject::connect(node->device, &PwDevice::deviceReady, this, &PwNodeBoundAudio::onDeviceReady); + + QObject::connect( + node->device, + &PwDevice::routeVolumesChanged, + this, + &PwNodeBoundAudio::onDeviceVolumesChanged + ); } } @@ -278,13 +285,17 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { if (id == SPA_PARAM_Props && index == 0) { - this->updateVolumeProps(param); + if (this->node->device) { + qCDebug(logNode) << "Skipping node volume props update for" << this->node + << "in favor of device updates."; + return; + } + + this->updateVolumeProps(PwVolumeProps::parseSpaPod(param)); } } -void PwNodeBoundAudio::updateVolumeProps(const spa_pod* param) { - auto volumeProps = PwVolumeProps::parseSpaPod(param); - +void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) { if (volumeProps.volumes.size() != volumeProps.channels.size()) { qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelVolumes and channelMap are not the same size. Sizes:" @@ -489,6 +500,18 @@ void PwNodeBoundAudio::onDeviceReady() { } } +void PwNodeBoundAudio::onDeviceVolumesChanged( + qint32 routeDevice, + const PwVolumeProps& volumeProps +) { + if (this->node->device && this->node->routeDevice == routeDevice) { + qCDebug(logNode) << "Got updated device volume props for" << this->node << "via" + << this->node->device; + + this->updateVolumeProps(volumeProps); + } +} + PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { auto props = PwVolumeProps(); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index aca949f2..0235217c 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -203,9 +203,10 @@ signals: private slots: void onDeviceReady(); + void onDeviceVolumesChanged(qint32 routeDevice, const PwVolumeProps& props); private: - void updateVolumeProps(const spa_pod* param); + void updateVolumeProps(const PwVolumeProps& volumeProps); bool mMuted = false; QVector mChannels; diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 2ff7a7a3..5bcc70d4 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -219,22 +219,22 @@ class PwNodeAudioIface: public QObject { // clang-format off /// If the node is currently muted. Setting this property changes the mute state. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged); /// The average volume over all channels of the node. /// Setting this property modifies the volume of all channels proportionately. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged); /// The audio channels present on the node. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); /// The volumes of each audio channel individually. Each entry corrosponds to /// the volume of the channel at the same index in @@channels. @@volumes and @@channels /// will always be the same length. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); // clang-format on QML_NAMED_ELEMENT(PwNodeAudio); @@ -300,7 +300,7 @@ class PwNodeIface: public PwObjectIface { /// - `media.title` - The title of the currently playing media. /// - `media.artist` - The artist of the currently playing media. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); /// Extra information present only if the node sends or receives audio. /// @@ -357,7 +357,7 @@ class PwLinkIface: public PwObjectIface { Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLink); QML_UNCREATABLE("PwLinks cannot be created directly"); @@ -392,7 +392,7 @@ class PwLinkGroupIface Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link group. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(qs::service::pipewire::PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLinkGroup); QML_UNCREATABLE("PwLinkGroups cannot be created directly");