diff --git i/src/services/pipewire/device.cpp w/src/services/pipewire/device.cpp index 616e7d0..0c55008 100644 --- i/src/services/pipewire/device.cpp +++ w/src/services/pipewire/device.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,8 @@ #include #include #include +#include +#include #include "../../core/logcat.hpp" #include "core.hpp" @@ -46,6 +49,25 @@ void PwDevice::unbindHooks() { this->mWaitingForDevice = false; } +void PwDevice::initProps(const spa_dict* props) { + if (const auto* deviceName = spa_dict_lookup(props, SPA_KEY_DEVICE_NAME)) { + this->name = deviceName; + } + + if (const auto* deviceDesc = spa_dict_lookup(props, SPA_KEY_DEVICE_DESCRIPTION)) { + this->description = deviceDesc; + } + + if (const auto* deviceNick = spa_dict_lookup(props, SPA_KEY_DEVICE_NICK)) { + this->nick = deviceNick; + } + + if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) { + this->type = mediaClass; + } +} + + const pw_device_events PwDevice::EVENTS = { .version = PW_VERSION_DEVICE_EVENTS, .info = &PwDevice::onInfo, @@ -71,6 +93,11 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) { } break; + } else if (param.id == SPA_PARAM_EnumProfile && param.flags & SPA_PARAM_INFO_READ) { + self->validProfiles.clear(); + pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); + } else if (param.id == SPA_PARAM_Profile && param.flags & SPA_PARAM_INFO_READ) { + pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); } } } @@ -97,6 +124,15 @@ void PwDevice::onParam( } self->addDeviceIndexPairs(param); + } else if (id == SPA_PARAM_EnumProfile) { + PwProfile profile = PwProfile::parseSpaPod(param); + self->profilesUpdated = true; + self->profiles.insertOrAssign(profile.index, profile); + self->validProfiles.insert(profile.index); + } else if (id == SPA_PARAM_Profile) { + PwProfile profile = PwProfile::parseSpaPod(param); + self->currentProfileUpdated = true; + self->currentProfile = profile; } } @@ -145,6 +181,21 @@ void PwDevice::polled() { return false; }); } + if (this->profilesUpdated) { + this->profiles.removeIf([&](const std::pair& entry) { + return !this->validProfiles.contains(entry.first); + }); + this->profilesUpdated = false; + QList profiles = this->profiles.values(); + std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; }); + emit this->profilesChanged(profiles); + } + if (this->currentProfileUpdated) { + this->currentProfileUpdated = false; + if (this->currentProfile) { + emit this->currentProfileChanged(*this->currentProfile); + } + } } bool PwDevice::setVolumes(qint32 routeDevice, const QVector& volumes) { @@ -182,6 +233,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) { }); } +void PwDevice::setProfile(qint32 profileIndex) { + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + auto* pod = spa_pod_builder_add_object(&builder, + SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, + SPA_PARAM_PROFILE_index, SPA_POD_Int(profileIndex)); + pw_device_set_param(this->proxy(), SPA_PARAM_Profile, 0, static_cast(pod)); +} + void PwDevice::waitForDevice() { this->mWaitingForDevice = true; } bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; } @@ -222,4 +282,24 @@ bool PwDevice::setRouteProps( return true; } +PwProfile PwProfile::parseSpaPod(const spa_pod* param) { + PwProfile profile; + + const auto* indexProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_index); + const auto* descProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_description); + const auto* nameProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_name); + + spa_pod_get_int(&indexProp->value, &profile.index); + + const char* desc_cstr = nullptr; + spa_pod_get_string(&descProp->value, &desc_cstr); + profile.description = QString(desc_cstr); + + const char* name_cstr = nullptr; + spa_pod_get_string(&nameProp->value, &name_cstr); + profile.name = QString(name_cstr); + + return profile; +} + } // namespace qs::service::pipewire diff --git i/src/services/pipewire/device.hpp w/src/services/pipewire/device.hpp index 1a1f705..ee64858 100644 --- i/src/services/pipewire/device.hpp +++ w/src/services/pipewire/device.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -17,6 +18,20 @@ namespace qs::service::pipewire { +struct PwProfile { + Q_GADGET; + Q_PROPERTY(qint32 index MEMBER index) + Q_PROPERTY(QString description MEMBER description) + Q_PROPERTY(QString name MEMBER name) + +public: + qint32 index; + QString description; + QString name; + + static PwProfile parseSpaPod(const spa_pod* param); +}; + class PwDevice; class PwDevice: public PwBindable { @@ -25,6 +40,12 @@ class PwDevice: public PwBindable& volumes); bool setMuted(qint32 routeDevice, bool muted); @@ -32,9 +53,16 @@ public: void waitForDevice(); [[nodiscard]] bool waitingForDevice() const; + void setProfile(qint32 profileIndex); + + QHash profiles; + std::optional currentProfile; + signals: void deviceReady(); void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); + void profilesChanged(QList profiles); + void currentProfileChanged(PwProfile profile); private slots: void polled(); @@ -49,6 +77,11 @@ private: QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); + bool profilesUpdated = false; + QSet validProfiles; + + bool currentProfileUpdated = false; + bool setRouteProps(qint32 routeDevice, const std::function& propsCallback); diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp index 9efb17e..921d12a 100644 --- i/src/services/pipewire/qml.cpp +++ w/src/services/pipewire/qml.cpp @@ -9,6 +9,9 @@ #include #include +#include +#include + #include "../../core/model.hpp" #include "connection.hpp" #include "defaults.hpp" @@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded); + for (auto* device: connection->registry.devices.values()) { + this->onDeviceAdded(device); + } + + QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded); + for (auto* link: connection->registry.links.values()) { this->onLinkAdded(link); } @@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) { this->mNodes.removeObject(iface); } +ObjectModel* Pipewire::devices() { return &this->mDevices; } + +void Pipewire::onDeviceAdded(PwDevice* device) { + auto* iface = PwDeviceIface::instance(device); + QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved); + this->mDevices.insertObject(iface); +} + +void Pipewire::onDeviceRemoved(QObject* object) { + auto* iface = static_cast(object); // NOLINT + this->mDevices.removeObject(iface); +} + ObjectModel* Pipewire::links() { return &this->mLinks; } void Pipewire::onLinkAdded(PwLink* link) { @@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const { PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; } +PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); } + PwNodeIface* PwNodeIface::instance(PwNode* node) { if (node == nullptr) return nullptr; @@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) { emit this->objectsChanged(); } +PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) { + QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged); + QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged); +} + +void PwDeviceIface::deviceProfilesChanged(QList) { emit this->profilesChanged(); } +void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); } + +quint32 PwDeviceIface::id() const { return this->mDevice->id; } +QString PwDeviceIface::name() const { return this->mDevice->name; } +QString PwDeviceIface::description() const { return this->mDevice->description; } +QString PwDeviceIface::nickname() const { return this->mDevice->nick; } +QString PwDeviceIface::type() const { return this->mDevice->type; } +QList PwDeviceIface::profiles() const { + QList profiles = this->mDevice->profiles.values(); + std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; }); + return profiles; +} +qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; } + +PwDeviceIface* PwDeviceIface::instance(PwDevice* device) { + if (device == nullptr) return nullptr; + + auto v = device->property("iface"); + if (v.canConvert()) { + return v.value(); + } + + auto* instance = new PwDeviceIface(device); + device->setProperty("iface", QVariant::fromValue(instance)); + + return instance; +} + +void PwDeviceIface::setProfile(qint32 profileIndex) { + this->mDevice->setProfile(profileIndex); +} + } // namespace qs::service::pipewire diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp index e3489a1..e5e1891 100644 --- i/src/services/pipewire/qml.hpp +++ w/src/services/pipewire/qml.hpp @@ -12,11 +12,13 @@ #include "../../core/model.hpp" #include "link.hpp" #include "node.hpp" +#include "device.hpp" #include "registry.hpp" namespace qs::service::pipewire { class PwNodeIface; +class PwDeviceIface; class PwLinkIface; class PwLinkGroupIface; @@ -65,6 +67,8 @@ class Pipewire: public QObject { /// - @@PwNode.audio - if non null the node is an audio node. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); /// All links present in pipewire. /// /// Links connect pipewire nodes to each other, and can be used to determine @@ -134,6 +138,7 @@ public: explicit Pipewire(QObject* parent = nullptr); [[nodiscard]] ObjectModel* nodes(); + [[nodiscard]] ObjectModel* devices(); [[nodiscard]] ObjectModel* links(); [[nodiscard]] ObjectModel* linkGroups(); @@ -159,7 +164,9 @@ signals: private slots: void onNodeAdded(PwNode* node); + void onDeviceAdded(PwDevice* node); void onNodeRemoved(QObject* object); + void onDeviceRemoved(QObject* object); void onLinkAdded(PwLink* link); void onLinkRemoved(QObject* object); void onLinkGroupAdded(PwLinkGroup* group); @@ -167,6 +174,7 @@ private slots: private: ObjectModel mNodes {this}; + ObjectModel mDevices {this}; ObjectModel mLinks {this}; ObjectModel mLinkGroups {this}; }; @@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface { /// > [!NOTE] The node may be used before it is fully bound, but some data /// > may be missing or incorrect. Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged); + Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT); QML_NAMED_ELEMENT(PwNode); QML_UNCREATABLE("PwNodes cannot be created directly"); @@ -332,6 +341,7 @@ public: [[nodiscard]] PwNodeType::Flags type() const; [[nodiscard]] QVariantMap properties() const; [[nodiscard]] PwNodeAudioIface* audio() const; + [[nodiscard]] PwDeviceIface* device() const; static PwNodeIface* instance(PwNode* node); @@ -344,6 +354,44 @@ private: PwNodeAudioIface* audioIface = nullptr; }; +class PwDeviceIface: public PwObjectIface { + Q_OBJECT; + Q_PROPERTY(quint32 id READ id CONSTANT); + Q_PROPERTY(QString name READ name CONSTANT); + Q_PROPERTY(QString description READ description CONSTANT); + Q_PROPERTY(QString nickname READ nickname CONSTANT); + Q_PROPERTY(QString type READ type CONSTANT); + Q_PROPERTY(QList profiles READ profiles NOTIFY profilesChanged); + Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged); + + QML_NAMED_ELEMENT(PwDevice); + QML_UNCREATABLE("PwDevices cannot be created directly"); + +signals: + void profilesChanged(); + void currentProfileChanged(); + +public: + explicit PwDeviceIface(PwDevice* node); + + [[nodiscard]] quint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] QString nickname() const; + [[nodiscard]] QString type() const; + QList profiles() const; + qint32 currentProfile() const; + + Q_INVOKABLE void setProfile(qint32 profileIndex); + + static PwDeviceIface* instance(PwDevice* node); +private: + PwDevice* mDevice; + + void deviceProfilesChanged(QList profiles); + void deviceCurrentProfileChanged(PwProfile profile); +}; + ///! A connection between pipewire nodes. /// Note that there is one link per *channel* of a connection between nodes. /// You usually want @@PwLinkGroup. diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp index c08fc1d..50c6d7a 100644 --- i/src/services/pipewire/registry.cpp +++ w/src/services/pipewire/registry.cpp @@ -196,6 +196,7 @@ void PwRegistry::onGlobal( device->initProps(props); self->devices.emplace(id, device); + emit self->deviceAdded(device); } } @@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) { } else if (auto* node = self->nodes.value(id)) { self->nodes.remove(id); node->safeDestroy(); + } else if (auto* device = self->devices.value(id)) { + self->devices.remove(id); + device->safeDestroy(); } } diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp index 8473f04..87e0766 100644 --- i/src/services/pipewire/registry.hpp +++ w/src/services/pipewire/registry.hpp @@ -132,6 +132,7 @@ public: signals: void nodeAdded(PwNode* node); + void deviceAdded(PwDevice* node); void linkAdded(PwLink* link); void linkGroupAdded(PwLinkGroup* group); void metadataAdded(PwMetadata* metadata);