summaryrefslogtreecommitdiffstats
path: root/src/PulseAudioQt
diff options
context:
space:
mode:
authorSimon Rettberg2022-08-18 20:27:25 +0200
committerSimon Rettberg2022-08-18 20:27:25 +0200
commitc48cebd620d3f5330c104d85ac32f0aaffadaa14 (patch)
tree8fc74d8aa05fcdc75bfcdf93fdc89c0f071692eb /src/PulseAudioQt
parentWhen changing default output of card, also set default sink (diff)
downloadpavucontrol-slx-c48cebd620d3f5330c104d85ac32f0aaffadaa14.tar.gz
pavucontrol-slx-c48cebd620d3f5330c104d85ac32f0aaffadaa14.tar.xz
pavucontrol-slx-c48cebd620d3f5330c104d85ac32f0aaffadaa14.zip
Replace everything with new "slxmix" (work in progress)
Diffstat (limited to 'src/PulseAudioQt')
-rw-r--r--src/PulseAudioQt/CMakeLists.txt49
-rw-r--r--src/PulseAudioQt/Card1
-rw-r--r--src/PulseAudioQt/CardPort1
-rw-r--r--src/PulseAudioQt/Client1
-rw-r--r--src/PulseAudioQt/Context1
-rw-r--r--src/PulseAudioQt/Device1
-rw-r--r--src/PulseAudioQt/IndexedPulseObject1
-rw-r--r--src/PulseAudioQt/Messages.sh4
-rw-r--r--src/PulseAudioQt/Models1
-rw-r--r--src/PulseAudioQt/Module1
-rw-r--r--src/PulseAudioQt/Port1
-rw-r--r--src/PulseAudioQt/Profile1
-rw-r--r--src/PulseAudioQt/PulseObject1
-rw-r--r--src/PulseAudioQt/Server1
-rw-r--r--src/PulseAudioQt/Sink1
-rw-r--r--src/PulseAudioQt/SinkInput1
-rw-r--r--src/PulseAudioQt/Source1
-rw-r--r--src/PulseAudioQt/SourceOutput1
-rw-r--r--src/PulseAudioQt/Stream1
-rw-r--r--src/PulseAudioQt/StreamRestore1
-rw-r--r--src/PulseAudioQt/VolumeObject1
-rw-r--r--src/PulseAudioQt/card.cpp168
-rw-r--r--src/PulseAudioQt/card.h59
-rw-r--r--src/PulseAudioQt/card_p.h35
-rw-r--r--src/PulseAudioQt/cardport.cpp26
-rw-r--r--src/PulseAudioQt/cardport.h36
-rw-r--r--src/PulseAudioQt/client.cpp37
-rw-r--r--src/PulseAudioQt/client.h32
-rw-r--r--src/PulseAudioQt/client_p.h26
-rw-r--r--src/PulseAudioQt/context.cpp842
-rw-r--r--src/PulseAudioQt/context.h238
-rw-r--r--src/PulseAudioQt/context_p.h85
-rw-r--r--src/PulseAudioQt/debug.h1
-rw-r--r--src/PulseAudioQt/device.cpp74
-rw-r--r--src/PulseAudioQt/device.h119
-rw-r--r--src/PulseAudioQt/device_p.h111
-rw-r--r--src/PulseAudioQt/indexedpulseobject.cpp39
-rw-r--r--src/PulseAudioQt/indexedpulseobject.h48
-rw-r--r--src/PulseAudioQt/indexedpulseobject_p.h35
-rw-r--r--src/PulseAudioQt/maps.cpp7
-rw-r--r--src/PulseAudioQt/maps.h165
-rw-r--r--src/PulseAudioQt/models.cpp396
-rw-r--r--src/PulseAudioQt/models.h163
-rw-r--r--src/PulseAudioQt/models_p.h33
-rw-r--r--src/PulseAudioQt/module.cpp53
-rw-r--r--src/PulseAudioQt/module.h39
-rw-r--r--src/PulseAudioQt/module_p.h29
-rw-r--r--src/PulseAudioQt/operation.cpp44
-rw-r--r--src/PulseAudioQt/operation.h56
-rw-r--r--src/PulseAudioQt/port.cpp35
-rw-r--r--src/PulseAudioQt/port.h70
-rw-r--r--src/PulseAudioQt/port_p.h47
-rw-r--r--src/PulseAudioQt/profile.cpp56
-rw-r--r--src/PulseAudioQt/profile.h81
-rw-r--r--src/PulseAudioQt/profile_p.h59
-rw-r--r--src/PulseAudioQt/pulseaudioqt_export.h192
-rw-r--r--src/PulseAudioQt/pulseobject.cpp86
-rw-r--r--src/PulseAudioQt/pulseobject.h76
-rw-r--r--src/PulseAudioQt/pulseobject_p.h57
-rw-r--r--src/PulseAudioQt/server.cpp137
-rw-r--r--src/PulseAudioQt/server.h57
-rw-r--r--src/PulseAudioQt/server_p.h28
-rw-r--r--src/PulseAudioQt/sink.cpp93
-rw-r--r--src/PulseAudioQt/sink.h57
-rw-r--r--src/PulseAudioQt/sink_p.h25
-rw-r--r--src/PulseAudioQt/sinkinput.cpp66
-rw-r--r--src/PulseAudioQt/sinkinput.h41
-rw-r--r--src/PulseAudioQt/sinkinput_p.h25
-rw-r--r--src/PulseAudioQt/source.cpp81
-rw-r--r--src/PulseAudioQt/source.h44
-rw-r--r--src/PulseAudioQt/source_p.h24
-rw-r--r--src/PulseAudioQt/sourceoutput.cpp65
-rw-r--r--src/PulseAudioQt/sourceoutput.h41
-rw-r--r--src/PulseAudioQt/sourceoutput_p.h26
-rw-r--r--src/PulseAudioQt/stream.cpp60
-rw-r--r--src/PulseAudioQt/stream.h60
-rw-r--r--src/PulseAudioQt/stream_p.h56
-rw-r--r--src/PulseAudioQt/streamrestore.cpp214
-rw-r--r--src/PulseAudioQt/streamrestore.h74
-rw-r--r--src/PulseAudioQt/streamrestore_p.h42
-rw-r--r--src/PulseAudioQt/volumeobject.cpp70
-rw-r--r--src/PulseAudioQt/volumeobject.h79
-rw-r--r--src/PulseAudioQt/volumeobject_p.h67
83 files changed, 5359 insertions, 0 deletions
diff --git a/src/PulseAudioQt/CMakeLists.txt b/src/PulseAudioQt/CMakeLists.txt
new file mode 100644
index 0000000..7578983
--- /dev/null
+++ b/src/PulseAudioQt/CMakeLists.txt
@@ -0,0 +1,49 @@
+add_library(KF5PulseAudioQt STATIC)
+
+project(PulseAudioQt)
+
+find_package(Qt5DBus ${QT_MINIMUM_VERSION} REQUIRED)
+find_package(Qt5Core ${QT_MINIMUM_VERSION} REQUIRED)
+find_package(Qt5Gui ${QT_MINIMUM_VERSION} REQUIRED)
+
+target_sources(KF5PulseAudioQt PRIVATE
+ card.cpp
+ cardport.cpp
+ client.cpp
+ context.cpp
+ device.cpp
+ maps.cpp
+ operation.cpp
+ port.cpp
+ profile.cpp
+ models.cpp
+ pulseobject.cpp
+ sink.cpp
+ sinkinput.cpp
+ source.cpp
+ sourceoutput.cpp
+ stream.cpp
+ volumeobject.cpp
+ server.cpp
+ streamrestore.cpp
+ module.cpp
+ indexedpulseobject.cpp
+)
+
+target_link_libraries(KF5PulseAudioQt
+ PUBLIC
+ Qt5::Core
+ PRIVATE
+ Qt5::Gui
+ Qt5::DBus
+ PkgConfig::LIBPULSE
+ PkgConfig::LIBPULSE_MAINLOOP
+)
+
+#target_include_directories(KF5PulseAudioQt INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF5}/KF5PulseAudioQt>" )
+
+#set_target_properties(KF5PulseAudioQt PROPERTIES VERSION ${PULSEAUDIOQT_VERSION}
+# SOVERSION ${PULSEAUDIOQT_SOVERSION}
+# EXPORT_NAME PulseAudioQt
+#)
+
diff --git a/src/PulseAudioQt/Card b/src/PulseAudioQt/Card
new file mode 100644
index 0000000..c618865
--- /dev/null
+++ b/src/PulseAudioQt/Card
@@ -0,0 +1 @@
+#include "card.h"
diff --git a/src/PulseAudioQt/CardPort b/src/PulseAudioQt/CardPort
new file mode 100644
index 0000000..71e4307
--- /dev/null
+++ b/src/PulseAudioQt/CardPort
@@ -0,0 +1 @@
+#include "cardport.h"
diff --git a/src/PulseAudioQt/Client b/src/PulseAudioQt/Client
new file mode 100644
index 0000000..f679c0d
--- /dev/null
+++ b/src/PulseAudioQt/Client
@@ -0,0 +1 @@
+#include "client.h"
diff --git a/src/PulseAudioQt/Context b/src/PulseAudioQt/Context
new file mode 100644
index 0000000..7e33fb6
--- /dev/null
+++ b/src/PulseAudioQt/Context
@@ -0,0 +1 @@
+#include "context.h"
diff --git a/src/PulseAudioQt/Device b/src/PulseAudioQt/Device
new file mode 100644
index 0000000..155c911
--- /dev/null
+++ b/src/PulseAudioQt/Device
@@ -0,0 +1 @@
+#include "device.h"
diff --git a/src/PulseAudioQt/IndexedPulseObject b/src/PulseAudioQt/IndexedPulseObject
new file mode 100644
index 0000000..491cb42
--- /dev/null
+++ b/src/PulseAudioQt/IndexedPulseObject
@@ -0,0 +1 @@
+#include "indexedpulseobject.h"
diff --git a/src/PulseAudioQt/Messages.sh b/src/PulseAudioQt/Messages.sh
new file mode 100644
index 0000000..fd6430e
--- /dev/null
+++ b/src/PulseAudioQt/Messages.sh
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/kcm_pulseaudio.pot
+rm -f rc.cpp
diff --git a/src/PulseAudioQt/Models b/src/PulseAudioQt/Models
new file mode 100644
index 0000000..4925970
--- /dev/null
+++ b/src/PulseAudioQt/Models
@@ -0,0 +1 @@
+#include "models.h"
diff --git a/src/PulseAudioQt/Module b/src/PulseAudioQt/Module
new file mode 100644
index 0000000..0275ca9
--- /dev/null
+++ b/src/PulseAudioQt/Module
@@ -0,0 +1 @@
+#include "module.h"
diff --git a/src/PulseAudioQt/Port b/src/PulseAudioQt/Port
new file mode 100644
index 0000000..877e571
--- /dev/null
+++ b/src/PulseAudioQt/Port
@@ -0,0 +1 @@
+#include "port.h"
diff --git a/src/PulseAudioQt/Profile b/src/PulseAudioQt/Profile
new file mode 100644
index 0000000..5b20bae
--- /dev/null
+++ b/src/PulseAudioQt/Profile
@@ -0,0 +1 @@
+#include "profile.h"
diff --git a/src/PulseAudioQt/PulseObject b/src/PulseAudioQt/PulseObject
new file mode 100644
index 0000000..4189e21
--- /dev/null
+++ b/src/PulseAudioQt/PulseObject
@@ -0,0 +1 @@
+#include "pulseobject.h"
diff --git a/src/PulseAudioQt/Server b/src/PulseAudioQt/Server
new file mode 100644
index 0000000..bce425e
--- /dev/null
+++ b/src/PulseAudioQt/Server
@@ -0,0 +1 @@
+#include "server.h"
diff --git a/src/PulseAudioQt/Sink b/src/PulseAudioQt/Sink
new file mode 100644
index 0000000..9d8afe8
--- /dev/null
+++ b/src/PulseAudioQt/Sink
@@ -0,0 +1 @@
+#include "sink.h"
diff --git a/src/PulseAudioQt/SinkInput b/src/PulseAudioQt/SinkInput
new file mode 100644
index 0000000..c6f8760
--- /dev/null
+++ b/src/PulseAudioQt/SinkInput
@@ -0,0 +1 @@
+#include "sinkinput.h"
diff --git a/src/PulseAudioQt/Source b/src/PulseAudioQt/Source
new file mode 100644
index 0000000..a5c5313
--- /dev/null
+++ b/src/PulseAudioQt/Source
@@ -0,0 +1 @@
+#include "source.h"
diff --git a/src/PulseAudioQt/SourceOutput b/src/PulseAudioQt/SourceOutput
new file mode 100644
index 0000000..7a9e223
--- /dev/null
+++ b/src/PulseAudioQt/SourceOutput
@@ -0,0 +1 @@
+#include "sourceoutput.h"
diff --git a/src/PulseAudioQt/Stream b/src/PulseAudioQt/Stream
new file mode 100644
index 0000000..65e61e9
--- /dev/null
+++ b/src/PulseAudioQt/Stream
@@ -0,0 +1 @@
+#include "stream.h"
diff --git a/src/PulseAudioQt/StreamRestore b/src/PulseAudioQt/StreamRestore
new file mode 100644
index 0000000..f775c8c
--- /dev/null
+++ b/src/PulseAudioQt/StreamRestore
@@ -0,0 +1 @@
+#include "streamrestore.h"
diff --git a/src/PulseAudioQt/VolumeObject b/src/PulseAudioQt/VolumeObject
new file mode 100644
index 0000000..2f0c160
--- /dev/null
+++ b/src/PulseAudioQt/VolumeObject
@@ -0,0 +1 @@
+#include "volumeobject.h"
diff --git a/src/PulseAudioQt/card.cpp b/src/PulseAudioQt/card.cpp
new file mode 100644
index 0000000..122ce33
--- /dev/null
+++ b/src/PulseAudioQt/card.cpp
@@ -0,0 +1,168 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "card.h"
+#include "card_p.h"
+#include "debug.h"
+
+#include "context.h"
+#include "indexedpulseobject_p.h"
+#include "port_p.h"
+#include "profile_p.h"
+
+namespace PulseAudioQt
+{
+Card::Card(QObject *parent)
+ : IndexedPulseObject(parent)
+ , d(new CardPrivate(this))
+{
+ connect(Context::instance(), &Context::sinkAdded, this, &Card::sinksChanged);
+ connect(Context::instance(), &Context::sinkRemoved, this, &Card::sinksChanged);
+
+ connect(Context::instance(), &Context::sourceAdded, this, &Card::sourcesChanged);
+ connect(Context::instance(), &Context::sourceRemoved, this, &Card::sourcesChanged);
+}
+
+Card::~Card()
+{
+ delete d;
+}
+
+CardPrivate::CardPrivate(Card *q)
+ : q(q)
+{
+}
+
+CardPrivate::~CardPrivate()
+{
+}
+
+void CardPrivate::update(const pa_card_info *info)
+{
+ q->IndexedPulseObject::d->updatePulseObject(info);
+ q->PulseObject::d->updateProperties(info);
+ m_description = q->PulseObject::d->m_properties.value(QLatin1String(PA_PROP_DEVICE_DESCRIPTION), QString()).toString();
+
+ QStringList newProfiles;
+ QStringList existingProfiles;
+
+ for (const Profile *profile : qAsConst(m_profiles)) {
+ existingProfiles << profile->name();
+ }
+
+ for (auto **it = info->profiles2; it && *it != nullptr; ++it) {
+ const QString name = QString::fromUtf8((*it)->name);
+ newProfiles << name;
+ Profile *profile = nullptr;
+ if (existingProfiles.contains(name)) {
+ profile = m_profiles[existingProfiles.indexOf(name)];
+ } else {
+ profile = new Profile(q);
+ m_profiles << profile;
+ }
+ profile->d->setInfo(*it);
+ }
+
+ for (Profile *profile : qAsConst(m_profiles)) {
+ if (!newProfiles.contains(profile->name())) {
+ m_profiles.removeOne(profile);
+ delete profile;
+ }
+ }
+
+ for (Profile *profile : qAsConst(m_profiles)) {
+ if (info->active_profile2->name == profile->name()) {
+ m_activeProfileIndex = m_profiles.indexOf(profile);
+ }
+ }
+
+ Q_EMIT q->profilesChanged();
+ Q_EMIT q->activeProfileIndexChanged();
+
+ QStringList newPorts;
+ QStringList existingPorts;
+
+ for (const Port *port : qAsConst(m_ports)) {
+ existingPorts << port->name();
+ }
+ for (auto **it = info->ports; it && *it != nullptr; ++it) {
+ const QString name = QString::fromUtf8((*it)->name);
+ newPorts << name;
+ CardPort *port = nullptr;
+ if (existingPorts.contains(name)) {
+ port = m_ports[existingPorts.indexOf(name)];
+ } else {
+ port = new CardPort(q);
+ m_ports << port;
+ }
+ port->d->setInfo(*it);
+ }
+
+ for (CardPort *port : qAsConst(m_ports)) {
+ if (!newPorts.contains(port->name())) {
+ m_ports.removeOne(port);
+ delete port;
+ }
+ }
+
+ Q_EMIT q->portsChanged();
+}
+
+QString Card::description() const
+{
+ return d->m_description;
+}
+
+QList<Profile *> Card::profiles() const
+{
+ return d->m_profiles;
+}
+
+quint32 Card::activeProfileIndex() const
+{
+ return d->m_activeProfileIndex;
+}
+
+void Card::setActiveProfileIndex(quint32 profileIndex)
+{
+ const Profile *profile = qobject_cast<Profile *>(profiles().at(profileIndex));
+ Context::instance()->setCardProfile(index(), profile->name());
+}
+
+QList<CardPort *> Card::ports() const
+{
+ return d->m_ports;
+}
+
+QList<Sink *> Card::sinks() const
+{
+ QList<Sink *> ret;
+
+ const auto allSinks = Context::instance()->sinks();
+ for (Sink *sink : allSinks) {
+ if (sink->cardIndex() == IndexedPulseObject::d->m_index) {
+ ret << sink;
+ }
+ }
+
+ return ret;
+}
+
+QList<Source *> Card::sources() const
+{
+ QList<Source *> ret;
+
+ const auto allSources = Context::instance()->sources();
+ for (Source *source : allSources) {
+ if (source->cardIndex() == IndexedPulseObject::d->m_index) {
+ ret << source;
+ }
+ }
+
+ return ret;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/card.h b/src/PulseAudioQt/card.h
new file mode 100644
index 0000000..2618ac7
--- /dev/null
+++ b/src/PulseAudioQt/card.h
@@ -0,0 +1,59 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef CARD_H
+#define CARD_H
+
+#include "cardport.h"
+#include "indexedpulseobject.h"
+#include "profile.h"
+#include "sink.h"
+#include "source.h"
+
+struct pa_card_info;
+
+namespace PulseAudioQt
+{
+class CardPort;
+class Profile;
+
+class PULSEAUDIOQT_EXPORT Card : public IndexedPulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QList<Profile *> profiles READ profiles NOTIFY profilesChanged)
+ Q_PROPERTY(quint32 activeProfileIndex READ activeProfileIndex WRITE setActiveProfileIndex NOTIFY activeProfileIndexChanged)
+ Q_PROPERTY(QList<CardPort *> ports READ ports NOTIFY portsChanged)
+ Q_PROPERTY(QList<Sink *> sinks READ sinks NOTIFY sinksChanged)
+ Q_PROPERTY(QList<Source *> sources READ sources NOTIFY sourcesChanged)
+
+public:
+ ~Card();
+
+ QString description() const;
+ QList<Profile *> profiles() const;
+ quint32 activeProfileIndex() const;
+ void setActiveProfileIndex(quint32 profileIndex);
+ QList<CardPort *> ports() const;
+ QList<Sink *> sinks() const;
+ QList<Source *> sources() const;
+
+Q_SIGNALS:
+ void profilesChanged();
+ void activeProfileIndexChanged();
+ void portsChanged();
+ void sinksChanged();
+ void sourcesChanged();
+
+private:
+ explicit Card(QObject *parent);
+
+ class CardPrivate *const d;
+ friend class MapBase<Card, pa_card_info>;
+};
+
+} // PulseAudioQt
+
+#endif // CARD_H
diff --git a/src/PulseAudioQt/card_p.h b/src/PulseAudioQt/card_p.h
new file mode 100644
index 0000000..890ba8f
--- /dev/null
+++ b/src/PulseAudioQt/card_p.h
@@ -0,0 +1,35 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef CARD_P_H
+#define CARD_P_H
+
+#include "card.h"
+#include "cardport.h"
+#include "profile.h"
+#include <QHash>
+#include <QVector>
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class CardPrivate
+{
+public:
+ explicit CardPrivate(Card *q);
+ virtual ~CardPrivate();
+
+ Card *q;
+
+ void update(const pa_card_info *info);
+
+ QString m_description;
+ QList<Profile *> m_profiles;
+ quint32 m_activeProfileIndex;
+ QList<CardPort *> m_ports;
+};
+}
+
+#endif
diff --git a/src/PulseAudioQt/cardport.cpp b/src/PulseAudioQt/cardport.cpp
new file mode 100644
index 0000000..863e247
--- /dev/null
+++ b/src/PulseAudioQt/cardport.cpp
@@ -0,0 +1,26 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#include "cardport.h"
+#include "port_p.h"
+
+namespace PulseAudioQt
+{
+CardPort::CardPort(QObject *parent)
+ : Port(parent)
+{
+}
+
+CardPort::~CardPort()
+{
+}
+
+void CardPort::update(const pa_card_port_info *info)
+{
+ Port::d->setInfo(info);
+ PulseObject::d->updateProperties(info);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/cardport.h b/src/PulseAudioQt/cardport.h
new file mode 100644
index 0000000..77d2c90
--- /dev/null
+++ b/src/PulseAudioQt/cardport.h
@@ -0,0 +1,36 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef CARDPORT_H
+#define CARDPORT_H
+
+#include "port.h"
+
+#include <QObject>
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+/**
+ * A Port associated with a Card.
+ */
+class PULSEAUDIOQT_EXPORT CardPort : public Port
+{
+ Q_OBJECT
+
+public:
+ ~CardPort();
+
+ void update(const pa_card_port_info *info);
+
+private:
+ explicit CardPort(QObject *parent);
+
+ friend class CardPrivate;
+};
+
+} // PulseAudioQt
+
+#endif
diff --git a/src/PulseAudioQt/client.cpp b/src/PulseAudioQt/client.cpp
new file mode 100644
index 0000000..427e34e
--- /dev/null
+++ b/src/PulseAudioQt/client.cpp
@@ -0,0 +1,37 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "client.h"
+#include "client_p.h"
+
+#include "debug.h"
+#include "indexedpulseobject_p.h"
+
+namespace PulseAudioQt
+{
+Client::Client(QObject *parent)
+ : IndexedPulseObject(parent)
+ , d(new ClientPrivate(this))
+{
+}
+
+ClientPrivate::ClientPrivate(Client *q)
+ : q(q)
+{
+}
+
+Client::~Client()
+{
+ delete d;
+}
+
+void ClientPrivate::update(const pa_client_info *info)
+{
+ q->IndexedPulseObject::d->updatePulseObject(info);
+ q->PulseObject::d->updateProperties(info);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/client.h b/src/PulseAudioQt/client.h
new file mode 100644
index 0000000..dfa2cf3
--- /dev/null
+++ b/src/PulseAudioQt/client.h
@@ -0,0 +1,32 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "indexedpulseobject.h"
+#include "pulseaudioqt_export.h"
+
+struct pa_client_info;
+
+namespace PulseAudioQt
+{
+class PULSEAUDIOQT_EXPORT Client : public IndexedPulseObject
+{
+ Q_OBJECT
+public:
+ ~Client();
+
+private:
+ explicit Client(QObject *parent);
+
+ class ClientPrivate *const d;
+ friend class MapBase<Client, pa_client_info>;
+};
+
+} // PulseAudioQt
+
+#endif // CLIENT_H
diff --git a/src/PulseAudioQt/client_p.h b/src/PulseAudioQt/client_p.h
new file mode 100644
index 0000000..0f62616
--- /dev/null
+++ b/src/PulseAudioQt/client_p.h
@@ -0,0 +1,26 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef CLIENT_P_H
+#define CLIENT_P_H
+
+#include "client.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class ClientPrivate
+{
+public:
+ ClientPrivate(Client *q);
+
+ void update(const pa_client_info *info);
+
+ Client *q;
+};
+
+} // PulseAudioQt
+
+#endif
diff --git a/src/PulseAudioQt/context.cpp b/src/PulseAudioQt/context.cpp
new file mode 100644
index 0000000..9967cea
--- /dev/null
+++ b/src/PulseAudioQt/context.cpp
@@ -0,0 +1,842 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "context.h"
+#include "server.h"
+
+#include "debug.h"
+#include <QAbstractEventDispatcher>
+#include <QDBusConnection>
+#include <QDBusServiceWatcher>
+#include <QGuiApplication>
+#include <QIcon>
+#include <QTimer>
+
+#include <memory>
+
+#include "card.h"
+#include "client.h"
+#include "module.h"
+#include "sink.h"
+#include "sinkinput.h"
+#include "source.h"
+#include "sourceoutput.h"
+#include "streamrestore.h"
+
+#include "context_p.h"
+#include "server_p.h"
+#include "streamrestore_p.h"
+
+namespace PulseAudioQt
+{
+qint64 normalVolume()
+{
+ return PA_VOLUME_NORM;
+}
+
+qint64 minimumVolume()
+{
+ return PA_VOLUME_MUTED;
+}
+
+qint64 maximumVolume()
+{
+ return PA_VOLUME_MAX;
+}
+
+qint64 maximumUIVolume()
+{
+ return PA_VOLUME_UI_MAX;
+}
+
+QString ContextPrivate::s_applicationId;
+
+#ifndef K_DOXYGEN
+
+static bool isGoodState(int eol)
+{
+ if (eol < 0) {
+ // Error
+ return false;
+ }
+
+ if (eol > 0) {
+ // End of callback chain
+ return false;
+ }
+
+ return true;
+}
+
+// --------------------------
+
+static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->sinkCallback(info);
+}
+
+static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ // pulsesink probe is used by gst-pulse only to query sink formats (not for playback)
+ if (qstrcmp(info->name, "pulsesink probe") == 0) {
+ return;
+ }
+ if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) {
+ if (qstrcmp(id, "sink-input-by-media-role:event") == 0) {
+ qDebug() << "Ignoring event role sink input.";
+ return;
+ }
+ }
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->sinkInputCallback(info);
+}
+
+static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ // FIXME: This forces excluding monitors
+ if (info->monitor_of_sink != PA_INVALID_INDEX)
+ return;
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->sourceCallback(info);
+}
+
+static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ // FIXME: This forces excluding these apps
+ if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) {
+ if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 //
+ || strcmp(app, "org.gnome.VolumeControl") == 0 //
+ || strcmp(app, "org.kde.kmixd") == 0 //
+ || strcmp(app, "org.kde.plasma-pa") == 0) //
+ return;
+ }
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->sourceOutputCallback(info);
+}
+
+static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->clientCallback(info);
+}
+
+static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->cardCallback(info);
+}
+
+static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol))
+ return;
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->moduleCallback(info);
+}
+
+static void server_cb(pa_context *context, const pa_server_info *info, void *data)
+{
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->serverCallback(info);
+}
+
+static void context_state_callback(pa_context *context, void *data)
+{
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->contextStateCallback(context);
+}
+
+static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data)
+{
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->subscribeCallback(context, type, index);
+}
+
+static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol)) {
+ return;
+ }
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ static_cast<ContextPrivate *>(data)->streamRestoreCallback(info);
+}
+
+static void ext_stream_restore_subscribe_cb(pa_context *context, void *data)
+{
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) {
+ qWarning() << "pa_ext_stream_restore_read() failed";
+ }
+}
+
+static void ext_stream_restore_change_sink_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol)) {
+ return;
+ }
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ if (qstrncmp(info->name, "sink-input-by", 13) == 0) {
+ ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
+ const QByteArray deviceData = contextp->m_newDefaultSink.toUtf8();
+ pa_ext_stream_restore_info newinfo;
+ newinfo.name = info->name;
+ newinfo.channel_map = info->channel_map;
+ newinfo.volume = info->volume;
+ newinfo.mute = info->mute;
+ newinfo.device = deviceData.constData();
+ contextp->streamRestoreWrite(&newinfo);
+ }
+}
+
+static void ext_stream_restore_change_source_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data)
+{
+ if (!isGoodState(eol)) {
+ return;
+ }
+ Q_ASSERT(context);
+ Q_ASSERT(data);
+ if (qstrncmp(info->name, "source-output-by", 16) == 0) {
+ ContextPrivate *contextp = static_cast<ContextPrivate *>(data);
+ const QByteArray deviceData = contextp->m_newDefaultSource.toUtf8();
+ pa_ext_stream_restore_info newinfo;
+ newinfo.name = info->name;
+ newinfo.channel_map = info->channel_map;
+ newinfo.volume = info->volume;
+ newinfo.mute = info->mute;
+ newinfo.device = deviceData.constData();
+ contextp->streamRestoreWrite(&newinfo);
+ }
+}
+
+#endif
+
+// --------------------------
+
+Context::Context(QObject *parent)
+ : QObject(parent)
+ , d(new ContextPrivate(this))
+{
+ d->m_server = new Server(this);
+ d->m_context = nullptr;
+ d->m_mainloop = nullptr;
+ d->m_references = 0;
+
+ d->connectToDaemon();
+
+ QDBusServiceWatcher *watcher =
+ new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
+ connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
+ d->connectToDaemon();
+ });
+
+ connect(&d->m_sinks, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT sinkAdded(static_cast<Sink *>(object));
+ });
+ connect(&d->m_sinks, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT sinkRemoved(static_cast<Sink *>(object));
+ });
+
+ connect(&d->m_sinkInputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT sinkInputAdded(static_cast<SinkInput *>(object));
+ });
+ connect(&d->m_sinkInputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT sinkInputRemoved(static_cast<SinkInput *>(object));
+ });
+
+ connect(&d->m_sources, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT sourceAdded(static_cast<Source *>(object));
+ });
+ connect(&d->m_sources, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT sourceRemoved(static_cast<Source *>(object));
+ });
+
+ connect(&d->m_sourceOutputs, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT sourceOutputAdded(static_cast<SourceOutput *>(object));
+ });
+ connect(&d->m_sourceOutputs, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT sourceOutputRemoved(static_cast<SourceOutput *>(object));
+ });
+
+ connect(&d->m_clients, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT clientAdded(static_cast<Client *>(object));
+ });
+ connect(&d->m_clients, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT clientRemoved(static_cast<Client *>(object));
+ });
+
+ connect(&d->m_cards, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT cardAdded(static_cast<Card *>(object));
+ });
+ connect(&d->m_cards, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT cardRemoved(static_cast<Card *>(object));
+ });
+
+ connect(&d->m_modules, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT moduleAdded(static_cast<Module *>(object));
+ });
+ connect(&d->m_modules, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT moduleRemoved(static_cast<Module *>(object));
+ });
+
+ connect(&d->m_streamRestores, &MapBaseQObject::added, this, [this](int, QObject *object) {
+ Q_EMIT streamRestoreAdded(static_cast<StreamRestore *>(object));
+ });
+ connect(&d->m_streamRestores, &MapBaseQObject::removed, this, [this](int, QObject *object) {
+ Q_EMIT streamRestoreRemoved(static_cast<StreamRestore *>(object));
+ });
+}
+
+ContextPrivate::ContextPrivate(Context *q)
+ : q(q)
+{
+}
+
+Context::~Context()
+{
+ delete d;
+}
+
+ContextPrivate::~ContextPrivate()
+{
+ if (m_context) {
+ pa_context_unref(m_context);
+ m_context = nullptr;
+ }
+
+ if (m_mainloop) {
+ pa_glib_mainloop_free(m_mainloop);
+ m_mainloop = nullptr;
+ }
+
+ reset();
+}
+
+Context *Context::instance()
+{
+ static std::unique_ptr<Context> context(new Context);
+ return context.get();
+}
+
+void ContextPrivate::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index)
+{
+ Q_ASSERT(context == m_context);
+
+ switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
+ case PA_SUBSCRIPTION_EVENT_SINK:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_sinks.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) {
+ qWarning() << "pa_context_get_sink_info_by_index() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SOURCE:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_sources.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) {
+ qWarning() << "pa_context_get_source_info_by_index() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_sinkInputs.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) {
+ qWarning() << "pa_context_get_sink_input_info() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_sourceOutputs.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) {
+ qWarning() << "pa_context_get_sink_input_info() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_CLIENT:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_clients.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) {
+ qWarning() << "pa_context_get_client_info() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_CARD:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_cards.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) {
+ qWarning() << "pa_context_get_card_info_by_index() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_MODULE:
+ if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ m_modules.removeEntry(index);
+ } else {
+ if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) {
+ qWarning() << "pa_context_get_module_info_list() failed";
+ return;
+ }
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SERVER:
+ if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) {
+ qWarning() << "pa_context_get_server_info() failed";
+ return;
+ }
+ break;
+ }
+}
+
+void ContextPrivate::contextStateCallback(pa_context *c)
+{
+ qDebug() << "state callback";
+ pa_context_state_t state = pa_context_get_state(c);
+ if (state == PA_CONTEXT_READY) {
+ qDebug() << "ready";
+
+ // 1. Register for the stream changes (except during probe)
+ if (m_context == c) {
+ pa_context_set_subscribe_callback(c, subscribe_cb, this);
+
+ if (!PAOperation(
+ pa_context_subscribe(c,
+ (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE | PA_SUBSCRIPTION_MASK_CLIENT
+ | PA_SUBSCRIPTION_MASK_SINK_INPUT | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT
+ | PA_SUBSCRIPTION_MASK_CARD | PA_SUBSCRIPTION_MASK_MODULE | PA_SUBSCRIPTION_MASK_SERVER),
+ nullptr,
+ nullptr))) {
+ qWarning() << "pa_context_subscribe() failed";
+ return;
+ }
+ }
+
+ if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) {
+ qWarning() << "pa_context_get_sink_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) {
+ qWarning() << "pa_context_get_source_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) {
+ qWarning() << "pa_context_client_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) {
+ qWarning() << "pa_context_get_card_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) {
+ qWarning() << "pa_context_get_sink_input_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) {
+ qWarning() << "pa_context_get_source_output_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) {
+ qWarning() << "pa_context_get_module_info_list() failed";
+ return;
+ }
+
+ if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) {
+ qWarning() << "pa_context_get_server_info() failed";
+ return;
+ }
+
+ if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) {
+ pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this);
+ PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this));
+ } else {
+ qWarning() << "Failed to initialize stream_restore extension";
+ }
+ } else if (!PA_CONTEXT_IS_GOOD(state)) {
+ qWarning() << "context kaput";
+ if (m_context) {
+ pa_context_unref(m_context);
+ m_context = nullptr;
+ }
+ reset();
+ QTimer::singleShot(1000, q, [this] {
+ connectToDaemon();
+ });
+ }
+}
+
+void ContextPrivate::sinkCallback(const pa_sink_info *info)
+{
+ // This parenting here is a bit weird
+ m_sinks.updateEntry(info, q);
+}
+
+void ContextPrivate::sinkInputCallback(const pa_sink_input_info *info)
+{
+ m_sinkInputs.updateEntry(info, q);
+}
+
+void ContextPrivate::sourceCallback(const pa_source_info *info)
+{
+ m_sources.updateEntry(info, q);
+}
+
+void ContextPrivate::sourceOutputCallback(const pa_source_output_info *info)
+{
+ m_sourceOutputs.updateEntry(info, q);
+}
+
+void ContextPrivate::clientCallback(const pa_client_info *info)
+{
+ m_clients.updateEntry(info, q);
+}
+
+void ContextPrivate::cardCallback(const pa_card_info *info)
+{
+ m_cards.updateEntry(info, q);
+}
+
+void ContextPrivate::moduleCallback(const pa_module_info *info)
+{
+ m_modules.updateEntry(info, q);
+}
+
+void ContextPrivate::streamRestoreCallback(const pa_ext_stream_restore_info *info)
+{
+ if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) {
+ return;
+ }
+
+ const int eventRoleIndex = 1;
+ StreamRestore *obj = qobject_cast<StreamRestore *>(m_streamRestores.data().value(eventRoleIndex));
+
+ if (!obj) {
+ QVariantMap props;
+ props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification"));
+ obj = new StreamRestore(eventRoleIndex, props, q);
+ obj->d->update(info);
+ m_streamRestores.insert(obj);
+ } else {
+ obj->d->update(info);
+ }
+}
+
+void ContextPrivate::serverCallback(const pa_server_info *info)
+{
+ m_server->d->update(info);
+}
+
+void Context::setCardProfile(quint32 index, const QString &profile)
+{
+ if (!d->m_context) {
+ return;
+ }
+ qDebug() << index << profile;
+ if (!PAOperation(pa_context_set_card_profile_by_index(d->m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) {
+ qWarning() << "pa_context_set_card_profile_by_index failed";
+ return;
+ }
+}
+
+void Context::setDefaultSink(const QString &name)
+{
+ if (!d->m_context) {
+ return;
+ }
+ const QByteArray nameData = name.toUtf8();
+ if (!PAOperation(pa_context_set_default_sink(d->m_context, nameData.constData(), nullptr, nullptr))) {
+ qWarning() << "pa_context_set_default_sink failed";
+ }
+
+ // Change device for all entries in stream-restore database
+ d->m_newDefaultSink = name;
+ if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_sink_cb, d))) {
+ qWarning() << "pa_ext_stream_restore_read failed";
+ }
+}
+
+void Context::setDefaultSource(const QString &name)
+{
+ if (!d->m_context) {
+ return;
+ }
+ const QByteArray nameData = name.toUtf8();
+ if (!PAOperation(pa_context_set_default_source(d->m_context, nameData.constData(), nullptr, nullptr))) {
+ qWarning() << "pa_context_set_default_source failed";
+ }
+
+ // Change device for all entries in stream-restore database
+ d->m_newDefaultSource = name;
+ if (!PAOperation(pa_ext_stream_restore_read(d->m_context, ext_stream_restore_change_source_cb, d))) {
+ qWarning() << "pa_ext_stream_restore_read failed";
+ }
+}
+
+void ContextPrivate::streamRestoreWrite(const pa_ext_stream_restore_info *info)
+{
+ if (!m_context) {
+ return;
+ }
+ if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) {
+ qWarning() << "pa_ext_stream_restore_write failed";
+ }
+}
+
+void ContextPrivate::connectToDaemon()
+{
+ if (m_context) {
+ return;
+ }
+
+ // We require a glib event loop
+ if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("Glib")) {
+ qWarning() << "Disabling PulseAudio integration for lack of GLib event loop";
+ return;
+ }
+
+ qDebug() << "Attempting connection to PulseAudio sound daemon";
+ if (!m_mainloop) {
+ m_mainloop = pa_glib_mainloop_new(nullptr);
+ Q_ASSERT(m_mainloop);
+ }
+
+ pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
+ Q_ASSERT(api);
+
+ pa_proplist *proplist = pa_proplist_new();
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QGuiApplication::applicationDisplayName().toUtf8().constData());
+ if (!s_applicationId.isEmpty()) {
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, s_applicationId.toUtf8().constData());
+ } else {
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, QGuiApplication::desktopFileName().toUtf8().constData());
+ }
+ pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, QGuiApplication::windowIcon().name().toUtf8().constData());
+ m_context = pa_context_new_with_proplist(api, nullptr, proplist);
+ pa_proplist_free(proplist);
+ Q_ASSERT(m_context);
+
+ if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) {
+ pa_context_unref(m_context);
+ pa_glib_mainloop_free(m_mainloop);
+ m_context = nullptr;
+ m_mainloop = nullptr;
+ return;
+ }
+ pa_context_set_state_callback(m_context, &context_state_callback, this);
+}
+
+void ContextPrivate::reset()
+{
+ m_sinks.reset();
+ m_sinkInputs.reset();
+ m_sources.reset();
+ m_sourceOutputs.reset();
+ m_clients.reset();
+ m_cards.reset();
+ m_modules.reset();
+ m_streamRestores.reset();
+ m_server->reset();
+}
+
+bool Context::isValid()
+{
+ return d->m_context && d->m_mainloop;
+}
+
+QVector<Sink *> Context::sinks() const
+{
+ return d->m_sinks.data();
+}
+
+QVector<SinkInput *> Context::sinkInputs() const
+{
+ return d->m_sinkInputs.data();
+}
+
+QVector<Source *> Context::sources() const
+{
+ return d->m_sources.data();
+}
+
+QVector<SourceOutput *> Context::sourceOutputs() const
+{
+ return d->m_sourceOutputs.data();
+}
+
+QVector<Client *> Context::clients() const
+{
+ return d->m_clients.data();
+}
+
+QVector<Card *> Context::cards() const
+{
+ return d->m_cards.data();
+}
+
+QVector<Module *> Context::modules() const
+{
+ return d->m_modules.data();
+}
+
+QVector<StreamRestore *> Context::streamRestores() const
+{
+ return d->m_streamRestores.data();
+}
+
+Server *Context::server() const
+{
+ return d->m_server;
+}
+
+void ContextPrivate::setGenericVolume(
+ quint32 index,
+ int channel,
+ qint64 newVolume,
+ pa_cvolume cVolume,
+ const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
+{
+ if (!m_context) {
+ return;
+ }
+ newVolume = qBound<qint64>(0, newVolume, PA_VOLUME_MAX);
+ pa_cvolume newCVolume = cVolume;
+ if (channel == -1) { // -1 all channels
+ const qint64 diff = newVolume - pa_cvolume_max(&cVolume);
+ for (int i = 0; i < newCVolume.channels; ++i) {
+ newCVolume.values[i] = qBound<qint64>(0, newCVolume.values[i] + diff, PA_VOLUME_MAX);
+ }
+ } else {
+ Q_ASSERT(newCVolume.channels > channel);
+ newCVolume.values[channel] = newVolume;
+ }
+ if (!pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr)) {
+ qWarning() << "pa_set_volume failed";
+ return;
+ }
+}
+
+void ContextPrivate::setGenericMute(quint32 index,
+ bool mute,
+ const std::function<pa_operation *(pa_context *, uint32_t, int, pa_context_success_cb_t, void *)> &pa_set_mute)
+{
+ if (!m_context) {
+ return;
+ }
+ if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) {
+ qWarning() << "pa_set_mute failed";
+ return;
+ }
+}
+
+void ContextPrivate::setGenericPort(quint32 index,
+ const QString &portName,
+ const std::function<pa_operation *(pa_context *, uint32_t, const char *, pa_context_success_cb_t, void *)> &pa_set_port)
+{
+ if (!m_context) {
+ return;
+ }
+ if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) {
+ qWarning() << "pa_set_port failed";
+ return;
+ }
+}
+
+void ContextPrivate::setGenericDeviceForStream(
+ quint32 streamIndex,
+ quint32 deviceIndex,
+ const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *)> &pa_move_stream_to_device)
+{
+ if (!m_context) {
+ return;
+ }
+ if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) {
+ qWarning() << "pa_move_stream_to_device failed";
+ return;
+ }
+}
+
+void ContextPrivate::setGenericVolumes(
+ quint32 index,
+ QVector<qint64> channelVolumes,
+ pa_cvolume cVolume,
+ const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &pa_set_volume)
+{
+ if (!m_context) {
+ return;
+ }
+ Q_ASSERT(channelVolumes.count() == cVolume.channels);
+
+ pa_cvolume newCVolume = cVolume;
+ for (int i = 0; i < channelVolumes.count(); ++i) {
+ newCVolume.values[i] = qBound<qint64>(0, channelVolumes.at(i), PA_VOLUME_MAX);
+ }
+
+ if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
+ qWarning() << "pa_set_volume failed";
+ return;
+ }
+}
+
+void Context::setApplicationId(const QString &applicationId)
+{
+ ContextPrivate::s_applicationId = applicationId;
+}
+
+pa_context *Context::context() const
+{
+ return d->m_context;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/context.h b/src/PulseAudioQt/context.h
new file mode 100644
index 0000000..f9cc9cf
--- /dev/null
+++ b/src/PulseAudioQt/context.h
@@ -0,0 +1,238 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef CONTEXT_H
+#define CONTEXT_H
+
+#include "pulseaudioqt_export.h"
+#include <QObject>
+
+struct pa_context;
+
+/**
+ * The primary namespace of PulseAudioQt.
+ */
+namespace PulseAudioQt
+{
+class Card;
+class Client;
+class Sink;
+class SinkInput;
+class Source;
+class SourceOutput;
+class StreamRestore;
+class Module;
+class Server;
+
+/**
+ * The normal volume (100%, 0 dB). Equivalent to PA_VOLUME_NORM.
+ */
+PULSEAUDIOQT_EXPORT qint64 normalVolume();
+/**
+ * The minimum volume (0%). Equivalent to PA_VOLUME_MUTED.
+ */
+PULSEAUDIOQT_EXPORT qint64 minimumVolume();
+/**
+ * The maximum volume PulseAudio can store. Equivalent to PA_VOLUME_MAX.
+ * \warning For UI elements like volume sliders use maximumUIVolume instead.
+ */
+PULSEAUDIOQT_EXPORT qint64 maximumVolume();
+
+/**
+ * The maximum volume suitable to display in a UI. Equivalent to PA_VOLUME_UI_MAX.
+ */
+PULSEAUDIOQT_EXPORT qint64 maximumUIVolume();
+
+class PULSEAUDIOQT_EXPORT Context : public QObject
+{
+ Q_OBJECT
+
+public:
+ ~Context();
+
+ static Context *instance();
+
+ /**
+ * Set the application id that is reported to PulseAudio.
+ * This needs to be called before accessing the context singleton the first time.
+ * If not set QGuiApplication::desktopFileName() is used.
+ */
+ static void setApplicationId(const QString &applicationId);
+
+ bool isValid();
+
+ /**
+ * Returns a list of all sinks.
+ *
+ * @return list of sinks
+ */
+ QVector<Sink *> sinks() const;
+
+ /**
+ * Returns a list of all sink inputs.
+ *
+ * @return list of sink inputs
+ */
+ QVector<SinkInput *> sinkInputs() const;
+
+ /**
+ * Returns a list of all sources.
+ *
+ * @return list of sources
+ */
+ QVector<Source *> sources() const;
+
+ /**
+ * Returns a list of all source outputs.
+ *
+ * @return list of source outputs
+ */
+ QVector<SourceOutput *> sourceOutputs() const;
+
+ /**
+ * Returns a list of all clients.
+ *
+ * @return list of clients
+ */
+ QVector<Client *> clients() const;
+
+ /**
+ * Returns a list of all cards.
+ *
+ * @return list of cards
+ */
+ QVector<Card *> cards() const;
+
+ /**
+ * Returns a list of all modules.
+ *
+ * @return list of modules
+ */
+ QVector<Module *> modules() const;
+
+ /**
+ * Returns a list of all stream restores.
+ *
+ * @return list of stream restores
+ */
+ QVector<StreamRestore *> streamRestores() const;
+
+ Server *server() const;
+
+ /**
+ * Returns a pointer to the raw PulseAudio context.
+ */
+ pa_context *context() const;
+
+ void setCardProfile(quint32 index, const QString &profile);
+ void setDefaultSink(const QString &name);
+ void setDefaultSource(const QString &name);
+
+Q_SIGNALS:
+ /**
+ * Indicates that sink was added.
+ */
+ void sinkAdded(PulseAudioQt::Sink *sink);
+
+ /**
+ * Indicates that sink was removed.
+ */
+ void sinkRemoved(PulseAudioQt::Sink *sink);
+
+ /**
+ * Indicates that sink input was added.
+ */
+ void sinkInputAdded(PulseAudioQt::SinkInput *sinkInput);
+
+ /**
+ * Indicates that sink input was removed.
+ */
+ void sinkInputRemoved(PulseAudioQt::SinkInput *sinkInput);
+
+ /**
+ * Indicates that source was added.
+ */
+ void sourceAdded(PulseAudioQt::Source *source);
+
+ /**
+ * Indicates that source was removed.
+ */
+ void sourceRemoved(PulseAudioQt::Source *source);
+
+ /**
+ * Indicates that source output was added.
+ */
+ void sourceOutputAdded(PulseAudioQt::SourceOutput *sourceOutput);
+
+ /**
+ * Indicates that source output was removed.
+ */
+ void sourceOutputRemoved(PulseAudioQt::SourceOutput *sourceOutput);
+
+ /**
+ * Indicates that client was added.
+ */
+ void clientAdded(PulseAudioQt::Client *client);
+
+ /**
+ * Indicates that client was removed.
+ */
+ void clientRemoved(PulseAudioQt::Client *client);
+
+ /**
+ * Indicates that card was added.
+ */
+ void cardAdded(PulseAudioQt::Card *card);
+
+ /**
+ * Indicates that card was removed.
+ */
+ void cardRemoved(PulseAudioQt::Card *card);
+
+ /**
+ * Indicates that module was added.
+ */
+ void moduleAdded(PulseAudioQt::Module *module);
+
+ /**
+ * Indicates that module was removed.
+ */
+ void moduleRemoved(PulseAudioQt::Module *module);
+
+ /**
+ * Indicates that stream restore was added.
+ */
+ void streamRestoreAdded(PulseAudioQt::StreamRestore *streamRestore);
+
+ /**
+ * Indicates that streamRestore was removed.
+ */
+ void streamRestoreRemoved(PulseAudioQt::StreamRestore *streamRestore);
+
+private:
+ explicit Context(QObject *parent = nullptr);
+
+ class ContextPrivate *const d;
+
+ friend class Sink;
+ friend class SinkInput;
+ friend class Source;
+ friend class SourceOutput;
+ friend class Stream;
+ friend class StreamRestorePrivate;
+ friend class Server;
+ friend class SinkModel;
+ friend class SinkInputModel;
+ friend class SourceModel;
+ friend class SourceOutputModel;
+ friend class StreamRestoreModel;
+ friend class CardModel;
+ friend class ModuleModel;
+};
+
+} // PulseAudioQt
+
+#endif // CONTEXT_H
diff --git a/src/PulseAudioQt/context_p.h b/src/PulseAudioQt/context_p.h
new file mode 100644
index 0000000..5b163a1
--- /dev/null
+++ b/src/PulseAudioQt/context_p.h
@@ -0,0 +1,85 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef CONTEXT_P_H
+#define CONTEXT_P_H
+
+#include "maps.h"
+#include "operation.h"
+#include <functional>
+#include <pulse/context.h>
+#include <pulse/ext-stream-restore.h>
+#include <pulse/glib-mainloop.h>
+#include <pulse/introspect.h>
+#include <qglobal.h>
+
+namespace PulseAudioQt
+{
+class Server;
+
+class ContextPrivate
+{
+public:
+ explicit ContextPrivate(Context *q);
+ virtual ~ContextPrivate();
+
+ // Don't forget to add things to reset().
+ SinkMap m_sinks;
+ SinkInputMap m_sinkInputs;
+ SourceMap m_sources;
+ SourceOutputMap m_sourceOutputs;
+ ClientMap m_clients;
+ CardMap m_cards;
+ ModuleMap m_modules;
+ StreamRestoreMap m_streamRestores;
+ Server *m_server;
+
+ pa_context *m_context;
+ pa_glib_mainloop *m_mainloop;
+
+ QString m_newDefaultSink;
+ QString m_newDefaultSource;
+
+ int m_references;
+
+ static QString s_applicationId;
+
+ void subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index);
+ void contextStateCallback(pa_context *context);
+ void sinkCallback(const pa_sink_info *info);
+ void sinkInputCallback(const pa_sink_input_info *info);
+ void sourceCallback(const pa_source_info *info);
+ void sourceOutputCallback(const pa_source_output_info *info);
+ void clientCallback(const pa_client_info *info);
+ void cardCallback(const pa_card_info *info);
+ void moduleCallback(const pa_module_info *info);
+ void streamRestoreCallback(const pa_ext_stream_restore_info *info);
+ void serverCallback(const pa_server_info *info);
+ void streamRestoreWrite(const pa_ext_stream_restore_info *info);
+ void setGenericVolume(quint32 index,
+ int channel,
+ qint64 newVolume,
+ pa_cvolume cVolume,
+ const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &);
+ void setGenericMute(quint32 index, bool mute, const std::function<pa_operation *(pa_context *, uint32_t, int, pa_context_success_cb_t, void *)> &);
+ void setGenericPort(quint32 index,
+ const QString &portName,
+ const std::function<pa_operation *(pa_context *, uint32_t, const char *, pa_context_success_cb_t, void *)> &);
+ void setGenericDeviceForStream(quint32 streamIndex,
+ quint32 deviceIndex,
+ const std::function<pa_operation *(pa_context *, uint32_t, uint32_t, pa_context_success_cb_t, void *)> &);
+ void setGenericVolumes(quint32 index,
+ QVector<qint64> channelVolumes,
+ pa_cvolume cVolume,
+ const std::function<pa_operation *(pa_context *, uint32_t, const pa_cvolume *, pa_context_success_cb_t, void *)> &);
+
+ void reset();
+ void connectToDaemon();
+
+ Context *q;
+};
+
+}
+#endif
diff --git a/src/PulseAudioQt/debug.h b/src/PulseAudioQt/debug.h
new file mode 100644
index 0000000..d3e83bc
--- /dev/null
+++ b/src/PulseAudioQt/debug.h
@@ -0,0 +1 @@
+#include <QDebug>
diff --git a/src/PulseAudioQt/device.cpp b/src/PulseAudioQt/device.cpp
new file mode 100644
index 0000000..c30df8b
--- /dev/null
+++ b/src/PulseAudioQt/device.cpp
@@ -0,0 +1,74 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "device.h"
+#include "device_p.h"
+
+namespace PulseAudioQt
+{
+Device::State Device::state() const
+{
+ return d->m_state;
+}
+
+QString Device::description() const
+{
+ return d->m_description;
+}
+
+QString Device::formFactor() const
+{
+ return d->m_formFactor;
+}
+
+quint32 Device::cardIndex() const
+{
+ return d->m_cardIndex;
+}
+
+QList<Port *> Device::ports() const
+{
+ return d->m_ports;
+}
+
+quint32 Device::activePortIndex() const
+{
+ return d->m_activePortIndex;
+}
+
+Device::Device(QObject *parent)
+ : VolumeObject(parent)
+ , d(new DevicePrivate(this))
+{
+}
+
+DevicePrivate::DevicePrivate(Device *q)
+ : q(q)
+{
+}
+
+Device::State DevicePrivate::stateFromPaState(int value) const
+{
+ switch (value) {
+ case -1: // PA_X_INVALID_STATE
+ return Device::InvalidState;
+ case 0: // PA_X_RUNNING
+ return Device::RunningState;
+ case 1: // PA_X_IDLE
+ return Device::IdleState;
+ case 2: // PA_X_SUSPENDED
+ return Device::SuspendedState;
+ default:
+ return Device::UnknownState;
+ }
+}
+
+Device::~Device()
+{
+ delete d;
+}
+
+} // namespace PulseAudioQt
diff --git a/src/PulseAudioQt/device.h b/src/PulseAudioQt/device.h
new file mode 100644
index 0000000..450ae6b
--- /dev/null
+++ b/src/PulseAudioQt/device.h
@@ -0,0 +1,119 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PA_DEVICE_H
+#define PA_DEVICE_H
+
+#include <QString>
+
+#include "port.h"
+#include "volumeobject.h"
+
+namespace PulseAudioQt
+{
+class Port;
+class DevicePrivate;
+
+/**
+ * A PulseAudio device. Can be either a Sink or Source.
+ */
+class PULSEAUDIOQT_EXPORT Device : public VolumeObject
+{
+ Q_OBJECT
+ Q_PROPERTY(State state READ state NOTIFY stateChanged)
+ Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
+ Q_PROPERTY(QString formFactor READ formFactor NOTIFY formFactorChanged)
+ Q_PROPERTY(quint32 cardIndex READ cardIndex NOTIFY cardIndexChanged)
+ Q_PROPERTY(QList<Port *> ports READ ports NOTIFY portsChanged)
+ Q_PROPERTY(quint32 activePortIndex READ activePortIndex WRITE setActivePortIndex NOTIFY activePortIndexChanged)
+ Q_PROPERTY(bool default READ isDefault WRITE setDefault NOTIFY defaultChanged)
+
+public:
+ enum State {
+ /** This state is used when the server does not support sink/source state introspection. */
+ InvalidState = 0,
+ /** Running, sink/source is playing/recording and used by at least one non-corked sink-input/source-output. */
+ RunningState,
+ /** When idle, the sink/source is playing/recording but there is no non-corked sink-input/source-output attached to it. */
+ IdleState,
+ /** When suspended, actual sink/source access can be closed, for instance. */
+ SuspendedState,
+ UnknownState,
+ };
+ Q_ENUM(State);
+
+ ~Device();
+
+ /**
+ * The state of this device.
+ */
+ State state() const;
+
+ /**
+ * A human readable description of this device.
+ */
+ QString description() const;
+
+ /**
+ * The device's form factor.
+ * One of "internal", "speaker", "handset", "tv", "webcam", "microphone", "headset", "headphone", "hands-free", "car", "hifi", "computer", "portable".
+ * This is based on PA_PROP_DEVICE_FORM_FACTOR.
+ */
+ QString formFactor() const;
+
+ /**
+ * Index of the card that owns this device.
+ */
+ quint32 cardIndex() const;
+
+ /**
+ * The ports associated with this device.
+ */
+ QList<Port *> ports() const;
+
+ /**
+ * The currently active port, by index.
+ */
+ quint32 activePortIndex() const;
+
+ /**
+ * Set the currently active port, by index.
+ */
+ virtual void setActivePortIndex(quint32 port_index) = 0;
+
+ /**
+ * Whether this is the default device.
+ */
+ virtual bool isDefault() const = 0;
+
+ /**
+ * Set whether this is the default device.
+ */
+ virtual void setDefault(bool enable) = 0;
+
+Q_SIGNALS:
+ void stateChanged();
+ void descriptionChanged();
+ void formFactorChanged();
+ void cardIndexChanged();
+ void portsChanged();
+ void activePortIndexChanged();
+ void defaultChanged();
+
+protected:
+ /** @private */
+ explicit Device(QObject *parent);
+ /** @private */
+ DevicePrivate *d;
+
+private:
+ friend class SinkPrivate;
+ friend class SourcePrivate;
+};
+
+} // PulseAudioQt
+
+#endif // DEVICE_H
diff --git a/src/PulseAudioQt/device_p.h b/src/PulseAudioQt/device_p.h
new file mode 100644
index 0000000..462862e
--- /dev/null
+++ b/src/PulseAudioQt/device_p.h
@@ -0,0 +1,111 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef DEVICE_P_H
+#define DEVICE_P_H
+
+#include <pulse/proplist.h>
+
+#include <QHash>
+#include <QVector>
+
+#include "device.h"
+#include "port.h"
+#include "port_p.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+class DevicePrivate
+{
+public:
+ explicit DevicePrivate(Device *q);
+
+ Device *q;
+
+ QString m_description;
+ QString m_formFactor;
+ quint32 m_cardIndex = -1;
+ QList<Port *> m_ports;
+ quint32 m_activePortIndex = -1;
+ Device::State m_state = Device::UnknownState;
+
+ Device::State stateFromPaState(int value) const;
+
+ template<typename PAInfo>
+ void updateDevice(const PAInfo *info)
+ {
+ q->VolumeObject::d->updateVolumeObject(info);
+
+ if (m_description != info->description) {
+ m_description = info->description;
+ Q_EMIT q->descriptionChanged();
+ }
+ const char *form_factor = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_FORM_FACTOR);
+ if (form_factor) {
+ QString formFactor = QString::fromUtf8(form_factor);
+ if (m_formFactor != formFactor) {
+ m_formFactor = formFactor;
+ Q_EMIT q->formFactorChanged();
+ }
+ }
+
+ m_cardIndex = info->card;
+ Q_EMIT q->cardIndexChanged();
+
+ QStringList newPorts;
+ QStringList existingPorts;
+
+ // Build list of existing ports
+ for (const Port *port : qAsConst(m_ports)) {
+ existingPorts << port->name();
+ }
+
+ // Add new ports from the updated port list and re/set port info
+ for (auto **it = info->ports; it && *it != nullptr; ++it) {
+ const QString name = QString::fromUtf8((*it)->name);
+ newPorts << name;
+
+ Port *port = nullptr;
+
+ if (existingPorts.contains(name)) {
+ port = m_ports[existingPorts.indexOf(name)];
+ } else {
+ port = new Port(q);
+ m_ports << port;
+ }
+
+ port->d->setInfo(*it);
+ }
+
+ // Remove ports that are not in the updated port list
+ for (Port *port : qAsConst(m_ports)) {
+ if (!newPorts.contains(port->name())) {
+ m_ports.removeOne(port);
+ delete port;
+ }
+ }
+
+ // Set active port
+ for (Port *port : qAsConst(m_ports)) {
+ if (info->active_port->name == port->name()) {
+ m_activePortIndex = m_ports.indexOf(port);
+ }
+ }
+
+ Q_EMIT q->portsChanged();
+ Q_EMIT q->activePortIndexChanged();
+
+ Device::State infoState = stateFromPaState(info->state);
+ if (infoState != m_state) {
+ m_state = infoState;
+ Q_EMIT q->stateChanged();
+ }
+ }
+};
+
+} // namespace PulseAudioQt
+
+#endif
diff --git a/src/PulseAudioQt/indexedpulseobject.cpp b/src/PulseAudioQt/indexedpulseobject.cpp
new file mode 100644
index 0000000..d8177b0
--- /dev/null
+++ b/src/PulseAudioQt/indexedpulseobject.cpp
@@ -0,0 +1,39 @@
+/*
+ SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "indexedpulseobject.h"
+#include "indexedpulseobject_p.h"
+
+#include "context.h"
+
+namespace PulseAudioQt
+{
+IndexedPulseObject::IndexedPulseObject(QObject *parent)
+ : PulseObject(parent)
+ , d(new IndexedPulseObjectPrivate(this))
+{
+}
+
+IndexedPulseObject::~IndexedPulseObject()
+{
+ delete d;
+}
+
+IndexedPulseObjectPrivate::IndexedPulseObjectPrivate(IndexedPulseObject *q)
+ : q(q)
+{
+}
+
+IndexedPulseObjectPrivate::~IndexedPulseObjectPrivate()
+{
+}
+
+quint32 IndexedPulseObject::index() const
+{
+ return d->m_index;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/indexedpulseobject.h b/src/PulseAudioQt/indexedpulseobject.h
new file mode 100644
index 0000000..135afb9
--- /dev/null
+++ b/src/PulseAudioQt/indexedpulseobject.h
@@ -0,0 +1,48 @@
+/*
+ SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef INDEXEDPULSEOBJECT_H
+#define INDEXEDPULSEOBJECT_H
+
+#include <QObject>
+
+#include "pulseaudioqt_export.h"
+#include "pulseobject.h"
+
+namespace PulseAudioQt
+{
+class PULSEAUDIOQT_EXPORT IndexedPulseObject : public PulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(quint32 index READ index CONSTANT)
+
+public:
+ ~IndexedPulseObject();
+
+ /**
+ * Index of this object.
+ */
+ quint32 index() const;
+
+protected:
+ /** @private */
+ explicit IndexedPulseObject(QObject *parent);
+ /** @private */
+ class IndexedPulseObjectPrivate *const d;
+
+private:
+ // Ensure that we get properly parented.
+ IndexedPulseObject();
+ friend class ClientPrivate;
+ friend class CardPrivate;
+ friend class ModulePrivate;
+ friend class VolumeObjectPrivate;
+ friend class ProfilePrivate;
+};
+
+} // PulseAudioQt
+
+#endif // INDEXEDPULSEOBJECT_H
diff --git a/src/PulseAudioQt/indexedpulseobject_p.h b/src/PulseAudioQt/indexedpulseobject_p.h
new file mode 100644
index 0000000..d37fae9
--- /dev/null
+++ b/src/PulseAudioQt/indexedpulseobject_p.h
@@ -0,0 +1,35 @@
+/*
+ SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef INDEXEDPULSEOBJECT_P_H
+#define INDEXEDPULSEOBJECT_P_H
+
+#include "debug.h"
+
+#include "pulseobject_p.h"
+
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class IndexedPulseObjectPrivate
+{
+public:
+ explicit IndexedPulseObjectPrivate(IndexedPulseObject *q);
+ virtual ~IndexedPulseObjectPrivate();
+
+ PulseObject *q;
+ quint32 m_index = 0;
+
+ template<typename PAInfo>
+ void updatePulseObject(PAInfo *info)
+ {
+ m_index = info->index;
+
+ q->PulseObject::d->updatePulseObject(info);
+ }
+};
+}
+#endif
diff --git a/src/PulseAudioQt/maps.cpp b/src/PulseAudioQt/maps.cpp
new file mode 100644
index 0000000..186c56a
--- /dev/null
+++ b/src/PulseAudioQt/maps.cpp
@@ -0,0 +1,7 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "maps.h"
diff --git a/src/PulseAudioQt/maps.h b/src/PulseAudioQt/maps.h
new file mode 100644
index 0000000..206c5cd
--- /dev/null
+++ b/src/PulseAudioQt/maps.h
@@ -0,0 +1,165 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+ SPDX-FileCopyrightText: 2018 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#pragma once
+
+#include <QHash>
+#include <QObject>
+#include <QSet>
+#include <QVector>
+
+#include <pulse/ext-stream-restore.h>
+#include <pulse/pulseaudio.h>
+
+#include "card_p.h"
+#include "client_p.h"
+#include "module_p.h"
+#include "sink_p.h"
+#include "sinkinput_p.h"
+#include "source_p.h"
+#include "sourceoutput_p.h"
+#include "streamrestore_p.h"
+
+namespace PulseAudioQt
+{
+// Used for typedefs.
+class Card;
+class Client;
+class Sink;
+class SinkInput;
+class Source;
+class SourceOutput;
+class StreamRestore;
+class Module;
+
+/**
+ * @see MapBase
+ * This class is nothing more than the QObject base since moc cannot handle
+ * templates.
+ */
+class MapBaseQObject : public QObject
+{
+ Q_OBJECT
+
+public:
+ virtual int count() const = 0;
+ virtual QObject *objectAt(int index) const = 0;
+ virtual int indexOfObject(QObject *object) const = 0;
+
+Q_SIGNALS:
+ void aboutToBeAdded(int index);
+ void added(int index, QObject *object);
+ void aboutToBeRemoved(int index);
+ void removed(int index, QObject *object);
+};
+
+/**
+ * Maps a specific index to a specific object pointer.
+ * This is used to give the unique arbitrary PulseAudio index of a PulseObject a
+ * serialized list index. Namely it enables us to translate a discrete list
+ * index to a pulse index to an object, and any permutation thereof.
+ */
+template<typename Type, typename PAInfo>
+class MapBase : public MapBaseQObject
+{
+public:
+ virtual ~MapBase()
+ {
+ }
+
+ const QVector<Type *> &data() const
+ {
+ return m_data;
+ }
+
+ int count() const override
+ {
+ return m_data.count();
+ }
+
+ int indexOfObject(QObject *object) const override
+ {
+ return m_data.indexOf(static_cast<Type *>(object));
+ }
+
+ QObject *objectAt(int index) const override
+ {
+ return m_data.at(index);
+ }
+
+ void reset()
+ {
+ while (!m_hash.isEmpty()) {
+ removeEntry(m_data.at(m_data.count() - 1)->index());
+ }
+ m_pendingRemovals.clear();
+ }
+
+ void insert(Type *object)
+ {
+ Q_ASSERT(!m_data.contains(object));
+
+ const int modelIndex = m_data.count();
+
+ Q_EMIT aboutToBeAdded(modelIndex);
+ m_data.append(object);
+ m_hash[object->index()] = object;
+ Q_EMIT added(modelIndex, object);
+ }
+
+ // Context is passed in as parent because context needs to include the maps
+ // so we'd cause a circular dep if we were to try to use the instance here.
+ // Plus that's weird separation anyway.
+ void updateEntry(const PAInfo *info, QObject *parent)
+ {
+ Q_ASSERT(info);
+
+ if (m_pendingRemovals.remove(info->index)) {
+ // Was already removed again.
+ return;
+ }
+
+ auto *obj = m_hash.value(info->index);
+ if (!obj) {
+ obj = new Type(parent);
+ obj->d->update(info);
+ insert(obj);
+ } else {
+ obj->d->update(info);
+ }
+ }
+
+ void removeEntry(quint32 index)
+ {
+ if (!m_hash.contains(index)) {
+ m_pendingRemovals.insert(index);
+ } else {
+ const int modelIndex = m_data.indexOf(m_hash.value(index));
+ Q_EMIT aboutToBeRemoved(modelIndex);
+ m_data.removeAt(modelIndex);
+ auto object = m_hash.take(index);
+ Q_EMIT removed(modelIndex, object);
+ delete object;
+ }
+ }
+
+protected:
+ QVector<Type *> m_data;
+ QHash<quint32, Type *> m_hash;
+ QSet<quint32> m_pendingRemovals;
+};
+
+typedef MapBase<Card, pa_card_info> CardMap;
+typedef MapBase<Client, pa_client_info> ClientMap;
+typedef MapBase<SinkInput, pa_sink_input_info> SinkInputMap;
+typedef MapBase<Sink, pa_sink_info> SinkMap;
+typedef MapBase<Source, pa_source_info> SourceMap;
+typedef MapBase<SourceOutput, pa_source_output_info> SourceOutputMap;
+typedef MapBase<StreamRestore, pa_ext_stream_restore_info> StreamRestoreMap;
+typedef MapBase<Module, pa_module_info> ModuleMap;
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/models.cpp b/src/PulseAudioQt/models.cpp
new file mode 100644
index 0000000..9fc3f32
--- /dev/null
+++ b/src/PulseAudioQt/models.cpp
@@ -0,0 +1,396 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "models.h"
+
+#include "card.h"
+#include "context.h"
+#include "context_p.h"
+#include "debug.h"
+#include "maps.h"
+#include "module.h"
+#include "server.h"
+#include "sink.h"
+#include "sinkinput.h"
+#include "source.h"
+#include "sourceoutput.h"
+#include "streamrestore.h"
+
+#include "models_p.h"
+#include <QMetaEnum>
+
+namespace PulseAudioQt
+{
+AbstractModel::AbstractModel(const MapBaseQObject *map, QObject *parent)
+ : QAbstractListModel(parent)
+ , d(new AbstractModelPrivate(this, map))
+{
+ connect(d->m_map, &MapBaseQObject::aboutToBeAdded, this, [this](int index) {
+ beginInsertRows(QModelIndex(), index, index);
+ });
+ connect(d->m_map, &MapBaseQObject::added, this, [this](int index) {
+ onDataAdded(index);
+ endInsertRows();
+ Q_EMIT countChanged();
+ });
+ connect(d->m_map, &MapBaseQObject::aboutToBeRemoved, this, [this](int index) {
+ beginRemoveRows(QModelIndex(), index, index);
+ });
+ connect(d->m_map, &MapBaseQObject::removed, this, [this](int index) {
+ Q_UNUSED(index);
+ endRemoveRows();
+ Q_EMIT countChanged();
+ });
+}
+
+AbstractModel::~AbstractModel()
+{
+ delete d;
+}
+
+AbstractModelPrivate::AbstractModelPrivate(AbstractModel *q, const MapBaseQObject *map)
+ : q(q)
+ , m_map(map)
+{
+}
+
+AbstractModelPrivate::~AbstractModelPrivate()
+{
+}
+
+QHash<int, QByteArray> AbstractModel::roleNames() const
+{
+ if (!d->m_roles.empty()) {
+ qDebug() << "returning roles" << d->m_roles;
+ return d->m_roles;
+ }
+ Q_UNREACHABLE();
+ return QHash<int, QByteArray>();
+}
+
+int AbstractModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return d->m_map->count();
+}
+
+QVariant AbstractModel::data(const QModelIndex &index, int role) const
+{
+ if (!hasIndex(index.row(), index.column())) {
+ return QVariant();
+ }
+ QObject *data = d->m_map->objectAt(index.row());
+ Q_ASSERT(data);
+ if (role == PulseObjectRole) {
+ return QVariant::fromValue(data);
+ } else if (role == Qt::DisplayRole) {
+ return static_cast<PulseObject *>(data)->name();
+ }
+ int property = d->m_objectProperties.value(role, -1);
+ if (property == -1) {
+ return QVariant();
+ }
+ return data->metaObject()->property(property).read(data);
+}
+
+bool AbstractModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (!hasIndex(index.row(), index.column())) {
+ return false;
+ }
+ int propertyIndex = d->m_objectProperties.value(role, -1);
+ if (propertyIndex == -1) {
+ return false;
+ }
+ QObject *data = d->m_map->objectAt(index.row());
+ auto property = data->metaObject()->property(propertyIndex);
+ return property.write(data, value);
+}
+
+int AbstractModel::role(const QByteArray &roleName) const
+{
+ qDebug() << roleName << d->m_roles.key(roleName, -1);
+ return d->m_roles.key(roleName, -1);
+}
+
+Context *AbstractModel::context() const
+{
+ return Context::instance();
+}
+
+void AbstractModel::initRoleNames(const QMetaObject &qobjectMetaObject)
+{
+ d->m_roles[PulseObjectRole] = QByteArrayLiteral("PulseObject");
+
+ QMetaEnum enumerator;
+ for (int i = 0; i < metaObject()->enumeratorCount(); ++i) {
+ if (metaObject()->enumerator(i).name() == QLatin1String("ItemRole")) {
+ enumerator = metaObject()->enumerator(i);
+ break;
+ }
+ }
+
+ for (int i = 0; i < enumerator.keyCount(); ++i) {
+ // Clip the Role suffix and glue it in the hash.
+ const int roleLength = 4;
+ QByteArray key(enumerator.key(i));
+ // Enum values must end in Role or the enum is crap
+ Q_ASSERT(key.right(roleLength) == QByteArrayLiteral("Role"));
+ key.chop(roleLength);
+ d->m_roles[enumerator.value(i)] = key;
+ }
+
+ int maxEnumValue = -1;
+ for (auto it = d->m_roles.constBegin(); it != d->m_roles.constEnd(); ++it) {
+ if (it.key() > maxEnumValue) {
+ maxEnumValue = it.key();
+ }
+ }
+ Q_ASSERT(maxEnumValue != -1);
+ auto mo = qobjectMetaObject;
+ for (int i = 0; i < mo.propertyCount(); ++i) {
+ QMetaProperty property = mo.property(i);
+ QString name(property.name());
+ name.replace(0, 1, name.at(0).toUpper());
+ d->m_roles[++maxEnumValue] = name.toLatin1();
+ d->m_objectProperties.insert(maxEnumValue, i);
+ if (!property.hasNotifySignal()) {
+ continue;
+ }
+ d->m_signalIndexToProperties.insert(property.notifySignalIndex(), i);
+ }
+ qDebug() << d->m_roles;
+
+ // Connect to property changes also with objects already in model
+ for (int i = 0; i < d->m_map->count(); ++i) {
+ onDataAdded(i);
+ }
+}
+
+void AbstractModel::propertyChanged()
+{
+ if (!sender() || senderSignalIndex() == -1) {
+ return;
+ }
+ int propertyIndex = d->m_signalIndexToProperties.value(senderSignalIndex(), -1);
+ if (propertyIndex == -1) {
+ return;
+ }
+ int role = d->m_objectProperties.key(propertyIndex, -1);
+ if (role == -1) {
+ return;
+ }
+ int index = d->m_map->indexOfObject(sender());
+ qDebug() << "PROPERTY CHANGED (" << index << ") :: " << role << roleNames().value(role);
+ Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {role});
+}
+
+void AbstractModel::onDataAdded(int index)
+{
+ QObject *data = d->m_map->objectAt(index);
+ const QMetaObject *mo = data->metaObject();
+ // We have all the data changed notify signals already stored
+ auto keys = d->m_signalIndexToProperties.keys();
+ foreach (int index, keys) {
+ QMetaMethod meth = mo->method(index);
+ connect(data, meth, this, propertyChangedMetaMethod());
+ }
+}
+
+QMetaMethod AbstractModel::propertyChangedMetaMethod() const
+{
+ auto mo = metaObject();
+ int methodIndex = mo->indexOfMethod("propertyChanged()");
+ if (methodIndex == -1) {
+ return QMetaMethod();
+ }
+ return mo->method(methodIndex);
+}
+
+SinkModel::SinkModel(QObject *parent)
+ : AbstractModel(&context()->d->m_sinks, parent)
+ , d(new SinkModelPrivate(this))
+{
+ initRoleNames(Sink::staticMetaObject);
+
+ for (int i = 0; i < context()->d->m_sinks.count(); ++i) {
+ sinkAdded(i);
+ }
+
+ connect(&context()->d->m_sinks, &MapBaseQObject::added, this, &SinkModel::sinkAdded);
+ connect(&context()->d->m_sinks, &MapBaseQObject::removed, this, &SinkModel::sinkRemoved);
+
+ connect(context()->server(), &Server::defaultSinkChanged, this, [this]() {
+ updatePreferredSink();
+ Q_EMIT defaultSinkChanged();
+ });
+}
+
+SinkModel::~SinkModel()
+{
+ delete d;
+}
+
+SinkModelPrivate::SinkModelPrivate(SinkModel *q)
+ : q(q)
+ , m_preferredSink(nullptr)
+{
+}
+
+SinkModelPrivate::~SinkModelPrivate()
+{
+}
+
+Sink *SinkModel::defaultSink() const
+{
+ return context()->server()->defaultSink();
+}
+
+Sink *SinkModel::preferredSink() const
+{
+ return d->m_preferredSink;
+}
+
+QVariant SinkModel::data(const QModelIndex &index, int role) const
+{
+ if (role == SortByDefaultRole) {
+ // Workaround QTBUG-1548
+ const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString();
+ const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString();
+ return defaultDevice + pulseIndex;
+ }
+ return AbstractModel::data(index, role);
+}
+
+void SinkModel::sinkAdded(int index)
+{
+ Q_ASSERT(qobject_cast<Sink *>(context()->d->m_sinks.objectAt(index)));
+ Sink *sink = static_cast<Sink *>(context()->d->m_sinks.objectAt(index));
+ connect(sink, &Sink::stateChanged, this, &SinkModel::updatePreferredSink);
+
+ updatePreferredSink();
+}
+
+void SinkModel::sinkRemoved(int index)
+{
+ Q_UNUSED(index);
+
+ updatePreferredSink();
+}
+
+void SinkModel::updatePreferredSink()
+{
+ Sink *sink = findPreferredSink();
+
+ if (sink != d->m_preferredSink) {
+ qDebug() << "Changing preferred sink to" << sink << (sink ? sink->name() : "");
+ d->m_preferredSink = sink;
+ Q_EMIT preferredSinkChanged();
+ }
+}
+
+Sink *SinkModel::findPreferredSink() const
+{
+ const auto &sinks = context()->d->m_sinks;
+
+ // Only one sink is the preferred one
+ if (sinks.count() == 1) {
+ return static_cast<Sink *>(sinks.objectAt(0));
+ }
+
+ auto lookForState = [&](Device::State state) {
+ Sink *ret = nullptr;
+ const auto data = sinks.data();
+ for (Sink *sink : data) {
+ if (sink->state() != state) {
+ continue;
+ }
+ if (!ret) {
+ ret = sink;
+ } else if (sink == defaultSink()) {
+ ret = sink;
+ break;
+ }
+ }
+ return ret;
+ };
+
+ Sink *preferred = nullptr;
+
+ // Look for playing sinks + prefer default sink
+ preferred = lookForState(Device::RunningState);
+ if (preferred) {
+ return preferred;
+ }
+
+ // Look for idle sinks + prefer default sink
+ preferred = lookForState(Device::IdleState);
+ if (preferred) {
+ return preferred;
+ }
+
+ // Fallback to default sink
+ return defaultSink();
+}
+
+SourceModel::SourceModel(QObject *parent)
+ : AbstractModel(&context()->d->m_sources, parent)
+{
+ initRoleNames(Source::staticMetaObject);
+
+ connect(context()->server(), &Server::defaultSourceChanged, this, &SourceModel::defaultSourceChanged);
+}
+
+Source *SourceModel::defaultSource() const
+{
+ return context()->server()->defaultSource();
+}
+
+QVariant SourceModel::data(const QModelIndex &index, int role) const
+{
+ if (role == SortByDefaultRole) {
+ // Workaround QTBUG-1548
+ const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString();
+ const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString();
+ return defaultDevice + pulseIndex;
+ }
+ return AbstractModel::data(index, role);
+}
+
+SinkInputModel::SinkInputModel(QObject *parent)
+ : AbstractModel(&context()->d->m_sinkInputs, parent)
+{
+ initRoleNames(SinkInput::staticMetaObject);
+}
+
+SourceOutputModel::SourceOutputModel(QObject *parent)
+ : AbstractModel(&context()->d->m_sourceOutputs, parent)
+{
+ initRoleNames(SourceOutput::staticMetaObject);
+}
+
+CardModel::CardModel(QObject *parent)
+ : AbstractModel(&context()->d->m_cards, parent)
+{
+ initRoleNames(Card::staticMetaObject);
+}
+
+StreamRestoreModel::StreamRestoreModel(QObject *parent)
+ : AbstractModel(&context()->d->m_streamRestores, parent)
+{
+ initRoleNames(StreamRestore::staticMetaObject);
+}
+
+ModuleModel::ModuleModel(QObject *parent)
+ : AbstractModel(&context()->d->m_modules, parent)
+{
+ initRoleNames(Module::staticMetaObject);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/models.h b/src/PulseAudioQt/models.h
new file mode 100644
index 0000000..733e9e4
--- /dev/null
+++ b/src/PulseAudioQt/models.h
@@ -0,0 +1,163 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PULSEAUDIO_H
+#define PULSEAUDIO_H
+
+#include <QAbstractListModel>
+
+#include "pulseaudioqt_export.h"
+
+namespace PulseAudioQt
+{
+class Context;
+class MapBaseQObject;
+class Sink;
+class Source;
+class AbstractModelPrivate;
+class SinkModelPrivate;
+
+class PULSEAUDIOQT_EXPORT AbstractModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ItemRole { PulseObjectRole = Qt::UserRole + 1 };
+
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+
+ ~AbstractModel() override;
+ QHash<int, QByteArray> roleNames() const final override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const final override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) final override;
+
+ Q_INVOKABLE int role(const QByteArray &roleName) const;
+
+Q_SIGNALS:
+ void countChanged();
+
+protected:
+ AbstractModel(const MapBaseQObject *map, QObject *parent);
+ void initRoleNames(const QMetaObject &qobjectMetaObject);
+ Context *context() const;
+
+private Q_SLOTS:
+ void propertyChanged();
+
+private:
+ void onDataAdded(int index);
+ void onDataRemoved(int index);
+ QMetaMethod propertyChangedMetaMethod() const;
+
+ AbstractModelPrivate *d;
+
+ // Prevent leaf-classes from default constructing as we want to enforce
+ // them passing us a context or explicit nullptrs.
+ AbstractModel()
+ {
+ }
+};
+
+class PULSEAUDIOQT_EXPORT CardModel : public AbstractModel
+{
+ Q_OBJECT
+public:
+ CardModel(QObject *parent = nullptr);
+
+private:
+ void *d;
+};
+
+class PULSEAUDIOQT_EXPORT SinkModel : public AbstractModel
+{
+ Q_OBJECT
+ Q_PROPERTY(PulseAudioQt::Sink *defaultSink READ defaultSink NOTIFY defaultSinkChanged)
+ Q_PROPERTY(PulseAudioQt::Sink *preferredSink READ preferredSink NOTIFY preferredSinkChanged)
+public:
+ enum ItemRole { SortByDefaultRole = PulseObjectRole + 1 };
+ Q_ENUM(ItemRole)
+
+ SinkModel(QObject *parent = nullptr);
+ virtual ~SinkModel();
+ Sink *defaultSink() const;
+ Sink *preferredSink() const;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+Q_SIGNALS:
+ void defaultSinkChanged();
+ void preferredSinkChanged();
+
+private:
+ void sinkAdded(int index);
+ void sinkRemoved(int index);
+ void updatePreferredSink();
+ Sink *findPreferredSink() const;
+ SinkModelPrivate *d;
+};
+
+class PULSEAUDIOQT_EXPORT SinkInputModel : public AbstractModel
+{
+ Q_OBJECT
+public:
+ SinkInputModel(QObject *parent = nullptr);
+
+private:
+ void *d;
+};
+
+class PULSEAUDIOQT_EXPORT SourceModel : public AbstractModel
+{
+ Q_OBJECT
+ Q_PROPERTY(PulseAudioQt::Source *defaultSource READ defaultSource NOTIFY defaultSourceChanged)
+public:
+ enum ItemRole { SortByDefaultRole = PulseObjectRole + 1 };
+ Q_ENUM(ItemRole)
+
+ SourceModel(QObject *parent = nullptr);
+ Source *defaultSource() const;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+Q_SIGNALS:
+ void defaultSourceChanged();
+
+private:
+ void *d;
+};
+
+class PULSEAUDIOQT_EXPORT SourceOutputModel : public AbstractModel
+{
+ Q_OBJECT
+public:
+ SourceOutputModel(QObject *parent = nullptr);
+
+private:
+ void *d;
+};
+
+class PULSEAUDIOQT_EXPORT StreamRestoreModel : public AbstractModel
+{
+ Q_OBJECT
+public:
+ StreamRestoreModel(QObject *parent = nullptr);
+
+private:
+ void *d;
+};
+
+class PULSEAUDIOQT_EXPORT ModuleModel : public AbstractModel
+{
+ Q_OBJECT
+public:
+ ModuleModel(QObject *parent = nullptr);
+
+private:
+ void *d;
+};
+
+} // PulseAudioQt
+
+#endif // PULSEAUDIO_H
diff --git a/src/PulseAudioQt/models_p.h b/src/PulseAudioQt/models_p.h
new file mode 100644
index 0000000..d0c9a50
--- /dev/null
+++ b/src/PulseAudioQt/models_p.h
@@ -0,0 +1,33 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#pragma once
+#include "maps.h"
+
+namespace PulseAudioQt
+{
+class AbstractModelPrivate
+{
+public:
+ explicit AbstractModelPrivate(AbstractModel *q, const MapBaseQObject *map);
+ virtual ~AbstractModelPrivate();
+
+ AbstractModel *q;
+ const MapBaseQObject *m_map;
+ QHash<int, QByteArray> m_roles;
+ QHash<int, int> m_objectProperties;
+ QHash<int, int> m_signalIndexToProperties;
+};
+
+class SinkModelPrivate
+{
+public:
+ explicit SinkModelPrivate(SinkModel *q);
+ virtual ~SinkModelPrivate();
+
+ SinkModel *q;
+ Sink *m_preferredSink;
+};
+}
diff --git a/src/PulseAudioQt/module.cpp b/src/PulseAudioQt/module.cpp
new file mode 100644
index 0000000..af80fb7
--- /dev/null
+++ b/src/PulseAudioQt/module.cpp
@@ -0,0 +1,53 @@
+/*
+ SPDX-FileCopyrightText: 2017 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "module.h"
+#include "debug.h"
+#include "module_p.h"
+
+#include "context.h"
+#include "indexedpulseobject_p.h"
+
+namespace PulseAudioQt
+{
+Module::Module(QObject *parent)
+ : IndexedPulseObject(parent)
+ , d(new ModulePrivate(this))
+{
+}
+
+ModulePrivate::ModulePrivate(Module *q)
+ : q(q)
+{
+}
+
+ModulePrivate::~ModulePrivate()
+{
+}
+
+void ModulePrivate::update(const pa_module_info *info)
+{
+ q->IndexedPulseObject::d->updatePulseObject(info);
+ q->PulseObject::d->updateProperties(info);
+
+ const QString infoArgument = QString::fromUtf8(info->argument);
+ if (m_argument != infoArgument) {
+ m_argument = infoArgument;
+ Q_EMIT q->argumentChanged();
+ }
+}
+
+Module::~Module()
+{
+ delete d;
+}
+
+QString Module::argument() const
+{
+ return d->m_argument;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/module.h b/src/PulseAudioQt/module.h
new file mode 100644
index 0000000..ef778d0
--- /dev/null
+++ b/src/PulseAudioQt/module.h
@@ -0,0 +1,39 @@
+/*
+ SPDX-FileCopyrightText: 2017 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef MODULE_H
+#define MODULE_H
+
+#include "indexedpulseobject.h"
+#include "pulseaudioqt_export.h"
+
+struct pa_module_info;
+
+namespace PulseAudioQt
+{
+class PULSEAUDIOQT_EXPORT Module : public IndexedPulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString argument READ argument NOTIFY argumentChanged)
+
+public:
+ ~Module();
+
+ QString argument() const;
+
+Q_SIGNALS:
+ void argumentChanged();
+
+private:
+ explicit Module(QObject *parent);
+
+ class ModulePrivate *const d;
+ friend class MapBase<Module, pa_module_info>;
+};
+
+} // PulseAudioQt
+
+#endif // MODULE_H
diff --git a/src/PulseAudioQt/module_p.h b/src/PulseAudioQt/module_p.h
new file mode 100644
index 0000000..36b7dab
--- /dev/null
+++ b/src/PulseAudioQt/module_p.h
@@ -0,0 +1,29 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef MODULE_P_H
+#define MODULE_P_H
+
+#include "module.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class ModulePrivate
+{
+public:
+ explicit ModulePrivate(Module *q);
+ virtual ~ModulePrivate();
+
+ void update(const pa_module_info *info);
+
+ Module *q;
+
+ QString m_argument;
+};
+
+} // PulseAudioQt
+
+#endif
diff --git a/src/PulseAudioQt/operation.cpp b/src/PulseAudioQt/operation.cpp
new file mode 100644
index 0000000..d3a0212
--- /dev/null
+++ b/src/PulseAudioQt/operation.cpp
@@ -0,0 +1,44 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "operation.h"
+
+namespace PulseAudioQt
+{
+PAOperation::PAOperation(pa_operation *operation)
+ : m_operation(operation)
+{
+}
+
+PAOperation::~PAOperation()
+{
+ if (m_operation) {
+ pa_operation_unref(m_operation);
+ }
+}
+
+PAOperation &PAOperation::operator=(pa_operation *operation)
+{
+ m_operation = operation;
+ return *this;
+}
+
+bool PAOperation::operator!()
+{
+ return !m_operation;
+}
+
+pa_operation *&PAOperation::operator*()
+{
+ return m_operation;
+}
+
+PAOperation::operator bool()
+{
+ return m_operation;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/operation.h b/src/PulseAudioQt/operation.h
new file mode 100644
index 0000000..771a09c
--- /dev/null
+++ b/src/PulseAudioQt/operation.h
@@ -0,0 +1,56 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef OPERATION_H
+#define OPERATION_H
+
+#include <pulse/operation.h>
+
+namespace PulseAudioQt
+{
+/**
+ * @brief The PAOperation class
+ * Helps with management of pa_operations. pa_operations need to be expicitly
+ * unref'd after use, so this class is essentially a fancy scoping helper where
+ * destruction of an instance would also unref the held operation (if there is
+ * one).
+ */
+class PAOperation
+{
+public:
+ /**
+ * @brief PAOperation
+ * @param operation operation to manage the scope of
+ */
+ PAOperation(pa_operation *operation = nullptr);
+ ~PAOperation();
+
+ PAOperation &operator=(pa_operation *operation);
+
+ /**
+ * @brief operator !
+ * @return whether or not there is an operation pointer
+ */
+ bool operator!();
+
+ /**
+ * @brief operator bool representing whether there is an operation
+ */
+ operator bool();
+
+ /**
+ * @brief operator *
+ * @return pointer to internal pa_operation object
+ */
+ pa_operation *&operator*();
+
+private:
+ pa_operation *m_operation;
+};
+
+} // PulseAudioQt
+
+#endif // OPERATION_H
diff --git a/src/PulseAudioQt/port.cpp b/src/PulseAudioQt/port.cpp
new file mode 100644
index 0000000..0fff169
--- /dev/null
+++ b/src/PulseAudioQt/port.cpp
@@ -0,0 +1,35 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "port.h"
+#include "port_p.h"
+namespace PulseAudioQt
+{
+Port::Port(QObject *parent)
+ : Profile(parent)
+ , d(new PortPrivate(this))
+{
+}
+
+Port::~Port()
+{
+}
+
+PortPrivate::PortPrivate(Port *q)
+ : q(q)
+{
+}
+
+PortPrivate::~PortPrivate()
+{
+}
+
+Port::Type Port::type() const
+{
+ return d->m_type;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/port.h b/src/PulseAudioQt/port.h
new file mode 100644
index 0000000..8473d96
--- /dev/null
+++ b/src/PulseAudioQt/port.h
@@ -0,0 +1,70 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PORT_H
+#define PORT_H
+
+#include "profile.h"
+#include "pulseaudioqt_export.h"
+
+namespace PulseAudioQt
+{
+/**
+ * A PulseAudio port.
+ */
+class PULSEAUDIOQT_EXPORT Port : public Profile
+{
+ Q_OBJECT
+ Q_PROPERTY(Type type READ type NOTIFY typeChanged)
+
+public:
+ ~Port();
+
+ enum Type {
+ Unknown,
+ AUX,
+ Speaker,
+ Headphones,
+ Line,
+ Mic,
+ Headset,
+ Handset,
+ Earpiece,
+ SPDIF,
+ HDMI,
+ TV,
+ Radio,
+ Video,
+ USB,
+ Bluetooth,
+ Portable,
+ Handsfree,
+ Car,
+ HiFi,
+ Phone,
+ Network,
+ Analog,
+ };
+ Q_ENUM(Type)
+
+ Type type() const;
+
+Q_SIGNALS:
+ void typeChanged();
+
+protected:
+ /** @private */
+ explicit Port(QObject *parent);
+ /** @private */
+ class PortPrivate *const d;
+
+ friend class DevicePrivate;
+ friend class CardPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // PORT_H
diff --git a/src/PulseAudioQt/port_p.h b/src/PulseAudioQt/port_p.h
new file mode 100644
index 0000000..7501c66
--- /dev/null
+++ b/src/PulseAudioQt/port_p.h
@@ -0,0 +1,47 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#pragma once
+
+#include "port.h"
+#include "profile_p.h"
+#include <pulse/def.h>
+
+namespace PulseAudioQt
+{
+class PortPrivate
+{
+public:
+ explicit PortPrivate(Port *q);
+ virtual ~PortPrivate();
+
+ Port *q;
+ Port::Type m_type = Port::Type::Unknown;
+
+ template<typename PAInfo>
+ void setInfo(const PAInfo *info)
+ {
+ Profile::Availability newAvailability;
+ switch (info->available) {
+ case PA_PORT_AVAILABLE_NO:
+ newAvailability = Profile::Unavailable;
+ break;
+ case PA_PORT_AVAILABLE_YES:
+ newAvailability = Profile::Available;
+ break;
+ default:
+ newAvailability = Profile::Unknown;
+ }
+
+#if PA_CHECK_VERSION(14, 0, 0)
+ m_type = static_cast<Port::Type>(info->type);
+#endif
+ Q_EMIT q->typeChanged();
+
+ q->Profile::d->setCommonInfo(info, newAvailability);
+ }
+};
+}
diff --git a/src/PulseAudioQt/profile.cpp b/src/PulseAudioQt/profile.cpp
new file mode 100644
index 0000000..6fb496a
--- /dev/null
+++ b/src/PulseAudioQt/profile.cpp
@@ -0,0 +1,56 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "profile.h"
+#include "profile_p.h"
+
+namespace PulseAudioQt
+{
+Profile::Profile(QObject *parent)
+ : PulseObject(parent)
+ , d(new ProfilePrivate(this))
+{
+}
+
+Profile::~Profile()
+{
+}
+
+ProfilePrivate::ProfilePrivate(Profile *q)
+ : q(q)
+{
+}
+
+ProfilePrivate::~ProfilePrivate()
+{
+}
+
+QString Profile::description() const
+{
+ return d->m_description;
+}
+
+quint32 Profile::priority() const
+{
+ return d->m_priority;
+}
+
+Profile::Availability Profile::availability() const
+{
+ return d->m_availability;
+}
+
+quint32 Profile::sources() const
+{
+ return d->m_sources;
+}
+
+quint32 Profile::sinks() const
+{
+ return d->m_sinks;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/profile.h b/src/PulseAudioQt/profile.h
new file mode 100644
index 0000000..79fe26f
--- /dev/null
+++ b/src/PulseAudioQt/profile.h
@@ -0,0 +1,81 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PROFILE_H
+#define PROFILE_H
+
+#include "pulseaudioqt_export.h"
+#include "pulseobject.h"
+#include <QObject>
+#include <QString>
+
+namespace PulseAudioQt
+{
+/**
+ * A PulseAudio profile.
+ */
+class PULSEAUDIOQT_EXPORT Profile : public PulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
+ Q_PROPERTY(quint32 priority READ priority NOTIFY priorityChanged)
+ Q_PROPERTY(Availability availability READ availability NOTIFY availabilityChanged)
+
+public:
+ enum Availability { Unknown, Available, Unavailable };
+ Q_ENUM(Availability)
+
+ ~Profile();
+
+ /**
+ * A human readable description.
+ */
+ QString description() const;
+
+ /**
+ * This object's priority. A higher number means higher priority.
+ */
+ quint32 priority() const;
+
+ /**
+ * Whether this object is available.
+ */
+ Availability availability() const;
+
+ quint32 sources() const;
+
+ quint32 sinks() const;
+
+Q_SIGNALS:
+ /**
+ * Emitted when the description changed.
+ */
+ void descriptionChanged();
+
+ /**
+ * Emitted when the priority changed.
+ */
+ void priorityChanged();
+
+ /**
+ * Emitted when the availability changed.
+ */
+ void availabilityChanged();
+
+protected:
+ /** @private */
+ explicit Profile(QObject *parent);
+ /** @private */
+ class ProfilePrivate *const d;
+
+ friend class Device;
+ friend class CardPrivate;
+ friend class PortPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // PROFILE_H
diff --git a/src/PulseAudioQt/profile_p.h b/src/PulseAudioQt/profile_p.h
new file mode 100644
index 0000000..7fa1f9d
--- /dev/null
+++ b/src/PulseAudioQt/profile_p.h
@@ -0,0 +1,59 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#pragma once
+
+#include "profile.h"
+#include "pulseobject_p.h"
+
+namespace PulseAudioQt
+{
+class ProfilePrivate
+{
+public:
+ explicit ProfilePrivate(Profile *q);
+ virtual ~ProfilePrivate();
+
+ Profile *q;
+
+ QString m_description;
+ quint32 m_priority = 0;
+ quint32 m_sources = 0;
+ quint32 m_sinks = 0;
+ Profile::Availability m_availability = Profile::Unknown;
+
+ template<typename PAInfo>
+ void setInfo(const PAInfo *info)
+ {
+ setCommonInfo(info, info->available ? Profile::Available : Profile::Unavailable);
+ m_sources = info->n_sources;
+ m_sinks = info->n_sinks;
+ }
+
+ template<typename PAInfo>
+ void setCommonInfo(const PAInfo *info, Profile::Availability newAvailability)
+ {
+ if (info->description) {
+ QString infoDescription = QString::fromUtf8(info->description);
+ if (m_description != infoDescription) {
+ m_description = infoDescription;
+ Q_EMIT q->descriptionChanged();
+ }
+ }
+ if (m_priority != info->priority) {
+ m_priority = info->priority;
+ Q_EMIT q->priorityChanged();
+ }
+
+ if (m_availability != newAvailability) {
+ m_availability = newAvailability;
+ Q_EMIT q->availabilityChanged();
+ }
+
+ q->PulseObject::d->updatePulseObject(info);
+ }
+};
+}
diff --git a/src/PulseAudioQt/pulseaudioqt_export.h b/src/PulseAudioQt/pulseaudioqt_export.h
new file mode 100644
index 0000000..53a6a24
--- /dev/null
+++ b/src/PulseAudioQt/pulseaudioqt_export.h
@@ -0,0 +1,192 @@
+
+#ifndef PULSEAUDIOQT_EXPORT_H
+#define PULSEAUDIOQT_EXPORT_H
+
+#ifdef PULSEAUDIOQT_STATIC_DEFINE
+# define PULSEAUDIOQT_EXPORT
+# define PULSEAUDIOQT_NO_EXPORT
+#else
+# ifndef PULSEAUDIOQT_EXPORT
+# ifdef KF5PulseAudioQt_EXPORTS
+ /* We are building this library */
+# define PULSEAUDIOQT_EXPORT __attribute__((visibility("default")))
+# else
+ /* We are using this library */
+# define PULSEAUDIOQT_EXPORT __attribute__((visibility("default")))
+# endif
+# endif
+
+# ifndef PULSEAUDIOQT_NO_EXPORT
+# define PULSEAUDIOQT_NO_EXPORT __attribute__((visibility("hidden")))
+# endif
+#endif
+
+#ifndef PULSEAUDIOQT_DECL_DEPRECATED
+# define PULSEAUDIOQT_DECL_DEPRECATED __attribute__ ((__deprecated__))
+#endif
+
+#ifndef PULSEAUDIOQT_DECL_DEPRECATED_EXPORT
+# define PULSEAUDIOQT_DECL_DEPRECATED_EXPORT PULSEAUDIOQT_EXPORT PULSEAUDIOQT_DECL_DEPRECATED
+#endif
+
+#ifndef PULSEAUDIOQT_DECL_DEPRECATED_NO_EXPORT
+# define PULSEAUDIOQT_DECL_DEPRECATED_NO_EXPORT PULSEAUDIOQT_NO_EXPORT PULSEAUDIOQT_DECL_DEPRECATED
+#endif
+
+#if 0 /* DEFINE_NO_DEPRECATED */
+# ifndef PULSEAUDIOQT_NO_DEPRECATED
+# define PULSEAUDIOQT_NO_DEPRECATED
+# endif
+#endif
+
+#define PULSEAUDIOQT_DECL_DEPRECATED_TEXT(text) __attribute__ ((__deprecated__(text)))
+
+#define ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, patch) ((major<<16)|(minor<<8)|(patch))
+
+/* Take any defaults from group settings */
+#if !defined(PULSEAUDIOQT_NO_DEPRECATED) && !defined(PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# ifdef KF_NO_DEPRECATED
+# define PULSEAUDIOQT_NO_DEPRECATED
+# elif defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT
+# endif
+#endif
+#if !defined(PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT) && defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT
+#endif
+
+#if !defined(PULSEAUDIOQT_NO_DEPRECATED_WARNINGS) && !defined(PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE)
+# ifdef KF_NO_DEPRECATED_WARNINGS
+# define PULSEAUDIOQT_NO_DEPRECATED_WARNINGS
+# elif defined(KF_DEPRECATED_WARNINGS_SINCE)
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE
+# endif
+#endif
+#if !defined(PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE) && defined(KF_DEPRECATED_WARNINGS_SINCE)
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE
+#endif
+
+#if defined(PULSEAUDIOQT_NO_DEPRECATED)
+# undef PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_NO_EXPORT
+#elif defined(PULSEAUDIOQT_NO_DEPRECATED_WARNINGS)
+# define PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_NO_EXPORT
+#else
+# define PULSEAUDIOQT_DEPRECATED PULSEAUDIOQT_DECL_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_DECL_DEPRECATED_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_DECL_DEPRECATED_NO_EXPORT
+#endif
+
+/* No deprecated API had been removed from build */
+#define PULSEAUDIOQT_EXCLUDE_DEPRECATED_BEFORE_AND_AT 0
+
+#define PULSEAUDIOQT_BUILD_DEPRECATED_SINCE(major, minor) 1
+
+#ifdef PULSEAUDIOQT_NO_DEPRECATED
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT 0x10300
+#endif
+#ifdef PULSEAUDIOQT_NO_DEPRECATED_WARNINGS
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE 0
+#endif
+
+#ifndef PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE
+# ifdef PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# else
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE 0x10300
+# endif
+#endif
+
+#ifndef PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT 0x10000
+#endif
+
+#ifdef PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_ENABLE_DEPRECATED_SINCE(major, minor) (ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, 0) > PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT)
+#else
+# define PULSEAUDIOQT_ENABLE_DEPRECATED_SINCE(major, minor) 0
+#endif
+
+#endif /* PULSEAUDIOQT_EXPORT_H */
+
+
+#ifndef ECM_GENERATEEXPORTHEADER_PULSEAUDIOQT_EXPORT_H
+#define ECM_GENERATEEXPORTHEADER_PULSEAUDIOQT_EXPORT_H
+
+
+#define PULSEAUDIOQT_DECL_DEPRECATED_TEXT(text) __attribute__ ((__deprecated__(text)))
+
+#define ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, patch) ((major<<16)|(minor<<8)|(patch))
+
+/* Take any defaults from group settings */
+#if !defined(PULSEAUDIOQT_NO_DEPRECATED) && !defined(PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# ifdef KF_NO_DEPRECATED
+# define PULSEAUDIOQT_NO_DEPRECATED
+# elif defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT
+# endif
+#endif
+#if !defined(PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT) && defined(KF_DISABLE_DEPRECATED_BEFORE_AND_AT)
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT KF_DISABLE_DEPRECATED_BEFORE_AND_AT
+#endif
+
+#if !defined(PULSEAUDIOQT_NO_DEPRECATED_WARNINGS) && !defined(PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE)
+# ifdef KF_NO_DEPRECATED_WARNINGS
+# define PULSEAUDIOQT_NO_DEPRECATED_WARNINGS
+# elif defined(KF_DEPRECATED_WARNINGS_SINCE)
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE
+# endif
+#endif
+#if !defined(PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE) && defined(KF_DEPRECATED_WARNINGS_SINCE)
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE KF_DEPRECATED_WARNINGS_SINCE
+#endif
+
+#if defined(PULSEAUDIOQT_NO_DEPRECATED)
+# undef PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_NO_EXPORT
+#elif defined(PULSEAUDIOQT_NO_DEPRECATED_WARNINGS)
+# define PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_NO_EXPORT
+#else
+# define PULSEAUDIOQT_DEPRECATED PULSEAUDIOQT_DECL_DEPRECATED
+# define PULSEAUDIOQT_DEPRECATED_EXPORT PULSEAUDIOQT_DECL_DEPRECATED_EXPORT
+# define PULSEAUDIOQT_DEPRECATED_NO_EXPORT PULSEAUDIOQT_DECL_DEPRECATED_NO_EXPORT
+#endif
+
+/* No deprecated API had been removed from build */
+#define PULSEAUDIOQT_EXCLUDE_DEPRECATED_BEFORE_AND_AT 0
+
+#define PULSEAUDIOQT_BUILD_DEPRECATED_SINCE(major, minor) 1
+
+#ifdef PULSEAUDIOQT_NO_DEPRECATED
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT 0x10300
+#endif
+#ifdef PULSEAUDIOQT_NO_DEPRECATED_WARNINGS
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE 0
+#endif
+
+#ifndef PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE
+# ifdef PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# else
+# define PULSEAUDIOQT_DEPRECATED_WARNINGS_SINCE 0x10300
+# endif
+#endif
+
+#ifndef PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT
+# define PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT 0x10000
+#endif
+
+#ifdef PULSEAUDIOQT_DEPRECATED
+# define PULSEAUDIOQT_ENABLE_DEPRECATED_SINCE(major, minor) (ECM_GENERATEEXPORTHEADER_VERSION_VALUE(major, minor, 0) > PULSEAUDIOQT_DISABLE_DEPRECATED_BEFORE_AND_AT)
+#else
+# define PULSEAUDIOQT_ENABLE_DEPRECATED_SINCE(major, minor) 0
+#endif
+
+
+#endif /* ECM_GENERATEEXPORTHEADER_PULSEAUDIOQT_EXPORT_H */
diff --git a/src/PulseAudioQt/pulseobject.cpp b/src/PulseAudioQt/pulseobject.cpp
new file mode 100644
index 0000000..ac9b975
--- /dev/null
+++ b/src/PulseAudioQt/pulseobject.cpp
@@ -0,0 +1,86 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "pulseobject.h"
+#include "pulseobject_p.h"
+
+#include "context.h"
+
+#include <QIcon>
+
+namespace PulseAudioQt
+{
+PulseObject::PulseObject(QObject *parent)
+ : QObject(parent)
+ , d(new PulseObjectPrivate(this))
+{
+}
+
+PulseObject::~PulseObject()
+{
+ delete d;
+}
+
+PulseObjectPrivate::PulseObjectPrivate(PulseObject *q)
+ : q(q)
+{
+}
+
+PulseObjectPrivate::~PulseObjectPrivate()
+{
+}
+
+QString PulseObject::name() const
+{
+ return d->m_name;
+}
+
+QString PulseObject::iconName() const
+{
+ QString name = d->m_properties.value(QStringLiteral("device.icon_name")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = d->m_properties.value(QStringLiteral("media.icon_name")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = d->m_properties.value(QStringLiteral("window.icon_name")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = d->m_properties.value(QStringLiteral("application.icon_name")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = d->m_properties.value(QStringLiteral("application.process.binary")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = d->m_properties.value(QStringLiteral("application.name")).toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ name = property("name").toString();
+ if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
+ return name;
+ }
+
+ return QString();
+}
+
+QVariantMap PulseObject::properties() const
+{
+ return d->m_properties;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/pulseobject.h b/src/PulseAudioQt/pulseobject.h
new file mode 100644
index 0000000..4d5b494
--- /dev/null
+++ b/src/PulseAudioQt/pulseobject.h
@@ -0,0 +1,76 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef PULSEOBJECT_H
+#define PULSEOBJECT_H
+
+#include <QObject>
+
+#include "pulseaudioqt_export.h"
+
+namespace PulseAudioQt
+{
+class Context;
+
+template<typename Type, typename PAInfo>
+class MapBase;
+
+/**
+ * Base class for most PulseAudio objects.
+ */
+class PULSEAUDIOQT_EXPORT PulseObject : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+ Q_PROPERTY(QString iconName READ iconName CONSTANT)
+ Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged)
+
+public:
+ ~PulseObject();
+
+ QString name() const;
+
+ /**
+ * A freedesktop.org icon name that fits this object.
+ */
+ QString iconName() const;
+
+ /**
+ * A map of properties associated with this object.
+ * The set of available properties depends on the type of object.
+ */
+ QVariantMap properties() const;
+
+Q_SIGNALS:
+ /**
+ * Emitted when any of the \ref properties changed.
+ */
+ void propertiesChanged();
+
+ void nameChanged();
+
+protected:
+ /** @private */
+ explicit PulseObject(QObject *parent);
+
+ /** @private */
+ class PulseObjectPrivate *const d;
+
+private:
+ // Ensure that we get properly parented.
+ PulseObject();
+ friend class IndexedPulseObjectPrivate;
+ friend class ClientPrivate;
+ friend class CardPrivate;
+ friend class ModulePrivate;
+ friend class VolumeObjectPrivate;
+ friend class ProfilePrivate;
+ friend class StreamRestorePrivate;
+};
+
+} // PulseAudioQt
+
+#endif // PULSEOBJECT_H
diff --git a/src/PulseAudioQt/pulseobject_p.h b/src/PulseAudioQt/pulseobject_p.h
new file mode 100644
index 0000000..946b66c
--- /dev/null
+++ b/src/PulseAudioQt/pulseobject_p.h
@@ -0,0 +1,57 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef PULSEOBJECT_P_H
+#define PULSEOBJECT_P_H
+
+#include "debug.h"
+
+#include <QVariantMap>
+
+#include <pulse/introspect.h>
+
+#include "context.h"
+
+namespace PulseAudioQt
+{
+class PulseObjectPrivate
+{
+public:
+ explicit PulseObjectPrivate(PulseObject *q);
+ virtual ~PulseObjectPrivate();
+
+ PulseObject *q;
+ QVariantMap m_properties;
+ QString m_name;
+
+ template<typename PAInfo>
+ void updatePulseObject(PAInfo *info)
+ {
+ if (m_name != QString::fromUtf8(info->name)) {
+ m_name = QString::fromUtf8(info->name);
+ Q_EMIT q->nameChanged();
+ }
+ }
+
+ template<typename PAInfo>
+ void updateProperties(PAInfo *info)
+ {
+ m_properties.clear();
+ void *it = nullptr;
+ while (const char *key = pa_proplist_iterate(info->proplist, &it)) {
+ Q_ASSERT(key);
+ const char *value = pa_proplist_gets(info->proplist, key);
+ if (!value) {
+ qDebug() << "property" << key << "not a string";
+ continue;
+ }
+ Q_ASSERT(value);
+ m_properties.insert(QString::fromUtf8(key), QString::fromUtf8(value));
+ }
+ Q_EMIT q->propertiesChanged();
+ }
+};
+}
+#endif
diff --git a/src/PulseAudioQt/server.cpp b/src/PulseAudioQt/server.cpp
new file mode 100644
index 0000000..c0b464b
--- /dev/null
+++ b/src/PulseAudioQt/server.cpp
@@ -0,0 +1,137 @@
+/*
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "server.h"
+#include "server_p.h"
+
+#include "context.h"
+#include "context_p.h"
+#include "debug.h"
+#include "sink.h"
+#include "source.h"
+
+namespace PulseAudioQt
+{
+Server::Server(Context *context)
+ : QObject(context)
+ , d(new ServerPrivate(this))
+{
+ Q_ASSERT(context);
+
+ connect(&context->d->m_sinks, &MapBaseQObject::added, this, &Server::updateDefaultDevices);
+ connect(&context->d->m_sinks, &MapBaseQObject::removed, this, &Server::updateDefaultDevices);
+ connect(&context->d->m_sources, &MapBaseQObject::added, this, &Server::updateDefaultDevices);
+ connect(&context->d->m_sources, &MapBaseQObject::removed, this, &Server::updateDefaultDevices);
+}
+
+Server::~Server()
+{
+}
+
+ServerPrivate::ServerPrivate(Server *q)
+ : q(q)
+ , m_defaultSink(nullptr)
+ , m_defaultSource(nullptr)
+{
+}
+
+ServerPrivate::~ServerPrivate()
+{
+}
+
+Sink *Server::defaultSink() const
+{
+ return d->m_defaultSink;
+}
+
+void Server::setDefaultSink(Sink *sink)
+{
+ Q_ASSERT(sink);
+ Context::instance()->setDefaultSink(sink->name());
+}
+
+Source *Server::defaultSource() const
+{
+ return d->m_defaultSource;
+}
+
+void Server::setDefaultSource(Source *source)
+{
+ Q_ASSERT(source);
+ Context::instance()->setDefaultSource(source->name());
+}
+
+void Server::reset()
+{
+ if (d->m_defaultSink) {
+ d->m_defaultSink = nullptr;
+ Q_EMIT defaultSinkChanged(d->m_defaultSink);
+ }
+
+ if (d->m_defaultSource) {
+ d->m_defaultSource = nullptr;
+ Q_EMIT defaultSourceChanged(d->m_defaultSource);
+ }
+}
+
+void ServerPrivate::update(const pa_server_info *info)
+{
+ m_defaultSinkName = QString::fromUtf8(info->default_sink_name);
+ m_defaultSourceName = QString::fromUtf8(info->default_source_name);
+
+ const bool isPw = QString::fromUtf8(info->server_name).contains("PipeWire");
+
+ if (isPw != m_isPipeWire) {
+ m_isPipeWire = isPw;
+ Q_EMIT q->isPipeWireChanged();
+ }
+
+ q->updateDefaultDevices();
+}
+
+/** @private */
+template<typename Type, typename Vector>
+static Type *findByName(const Vector &vector, const QString &name)
+{
+ Type *out = nullptr;
+ if (name.isEmpty()) {
+ return out;
+ }
+ for (Type *t : vector) {
+ out = t;
+ if (out->name() == name) {
+ return out;
+ }
+ }
+ qWarning() << "No object for name" << name;
+ return out;
+}
+
+void Server::updateDefaultDevices()
+{
+ Sink *sink = findByName<Sink>(Context::instance()->d->m_sinks.data(), d->m_defaultSinkName);
+ Source *source = findByName<Source>(Context::instance()->d->m_sources.data(), d->m_defaultSourceName);
+
+ if (d->m_defaultSink != sink) {
+ qDebug() << "Default sink changed" << sink;
+ d->m_defaultSink = sink;
+ Q_EMIT defaultSinkChanged(d->m_defaultSink);
+ }
+
+ if (d->m_defaultSource != source) {
+ qDebug() << "Default source changed" << source;
+ d->m_defaultSource = source;
+ Q_EMIT defaultSourceChanged(d->m_defaultSource);
+ }
+}
+
+bool Server::isPipeWire() const
+{
+ return d->m_isPipeWire;
+}
+
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/server.h b/src/PulseAudioQt/server.h
new file mode 100644
index 0000000..f388f5c
--- /dev/null
+++ b/src/PulseAudioQt/server.h
@@ -0,0 +1,57 @@
+/*
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef SERVER_H
+#define SERVER_H
+
+#include "pulseaudioqt_export.h"
+#include <QObject>
+
+namespace PulseAudioQt
+{
+class Sink;
+class Source;
+class Context;
+
+class PULSEAUDIOQT_EXPORT Server : public QObject
+{
+ Q_OBJECT
+
+public:
+ ~Server();
+
+ Sink *defaultSink() const;
+ void setDefaultSink(Sink *sink);
+
+ Source *defaultSource() const;
+ void setDefaultSource(Source *source);
+
+ /**
+ * Whether PulseAudio is provided via pipewire-pulse.
+ */
+ bool isPipeWire() const;
+
+Q_SIGNALS:
+ void defaultSinkChanged(PulseAudioQt::Sink *sink);
+ void defaultSourceChanged(PulseAudioQt::Source *source);
+ void isPipeWireChanged();
+
+private:
+ explicit Server(Context *context);
+
+ void reset();
+ void updateDefaultDevices();
+
+ class ServerPrivate *const d;
+
+ friend class ServerPrivate;
+ friend class Context;
+ friend class ContextPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // CONTEXT_H
diff --git a/src/PulseAudioQt/server_p.h b/src/PulseAudioQt/server_p.h
new file mode 100644
index 0000000..83550b4
--- /dev/null
+++ b/src/PulseAudioQt/server_p.h
@@ -0,0 +1,28 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#pragma once
+#include "server.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class ServerPrivate
+{
+public:
+ explicit ServerPrivate(Server *q);
+ virtual ~ServerPrivate();
+
+ Server *q;
+
+ QString m_defaultSinkName;
+ QString m_defaultSourceName;
+ Sink *m_defaultSink;
+ Source *m_defaultSource;
+ bool m_isPipeWire = false;
+
+ void update(const pa_server_info *info);
+};
+}
diff --git a/src/PulseAudioQt/sink.cpp b/src/PulseAudioQt/sink.cpp
new file mode 100644
index 0000000..6b36762
--- /dev/null
+++ b/src/PulseAudioQt/sink.cpp
@@ -0,0 +1,93 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "sink.h"
+#include "sink_p.h"
+
+#include "context.h"
+#include "context_p.h"
+#include "server.h"
+
+#include "device_p.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+Sink::Sink(QObject *parent)
+ : Device(parent)
+ , d(new SinkPrivate(this))
+{
+ connect(Context::instance()->server(), &Server::defaultSinkChanged, this, &Sink::defaultChanged);
+}
+
+SinkPrivate::SinkPrivate(Sink *q)
+ : q(q)
+{
+}
+
+Sink::~Sink()
+{
+ delete d;
+}
+
+void SinkPrivate::update(const pa_sink_info *info)
+{
+ q->Device::d->updateDevice(info);
+
+ if (m_monitorIndex != info->monitor_source) {
+ m_monitorIndex = info->monitor_source;
+ Q_EMIT q->monitorIndexChanged();
+ }
+}
+
+void Sink::setVolume(qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), -1, volume, VolumeObject::d->cvolume(), &pa_context_set_sink_volume_by_index);
+}
+
+void Sink::setMuted(bool muted)
+{
+ Context::instance()->d->setGenericMute(index(), muted, &pa_context_set_sink_mute_by_index);
+}
+
+void Sink::setActivePortIndex(quint32 port_index)
+{
+ Port *port = qobject_cast<Port *>(ports().at(port_index));
+ if (!port) {
+ qWarning() << "invalid port set request" << port_index;
+ return;
+ }
+ Context::instance()->d->setGenericPort(index(), port->name(), &pa_context_set_sink_port_by_index);
+}
+
+void Sink::setChannelVolume(int channel, qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), channel, volume, VolumeObject::d->cvolume(), &pa_context_set_sink_volume_by_index);
+}
+
+bool Sink::isDefault() const
+{
+ return Context::instance()->server()->defaultSink() == this;
+}
+
+void Sink::setDefault(bool enable)
+{
+ if (!isDefault() && enable) {
+ Context::instance()->server()->setDefaultSink(this);
+ }
+}
+
+quint32 Sink::monitorIndex() const
+{
+ return d->m_monitorIndex;
+}
+
+void Sink::setChannelVolumes(const QVector<qint64> &channelVolumes)
+{
+ Context::instance()->d->setGenericVolumes(index(), channelVolumes, VolumeObject::d->m_volume, &pa_context_set_sink_volume_by_index);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/sink.h b/src/PulseAudioQt/sink.h
new file mode 100644
index 0000000..9551763
--- /dev/null
+++ b/src/PulseAudioQt/sink.h
@@ -0,0 +1,57 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef SINK_H
+#define SINK_H
+
+#include "device.h"
+
+struct pa_sink_info;
+
+namespace PulseAudioQt
+{
+/**
+ * A PulseAudio sink. This class is based on https://freedesktop.org/software/pulseaudio/doxygen/structpa__sink__info.html.
+ */
+class PULSEAUDIOQT_EXPORT Sink : public Device
+{
+ Q_OBJECT
+
+public:
+ ~Sink();
+
+ void setVolume(qint64 volume) override;
+
+ void setMuted(bool muted) override;
+
+ void setActivePortIndex(quint32 port_index) override;
+
+ void setChannelVolume(int channel, qint64 volume) override;
+
+ bool isDefault() const override;
+
+ void setDefault(bool enable) override;
+
+ void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
+
+ /**
+ * Index of the monitor source for this sink.
+ */
+ quint32 monitorIndex() const;
+
+Q_SIGNALS:
+ void monitorIndexChanged();
+
+private:
+ explicit Sink(QObject *parent);
+
+ class SinkPrivate *const d;
+ friend class MapBase<Sink, pa_sink_info>;
+};
+
+} // PulseAudioQt
+
+#endif // SINK_H
diff --git a/src/PulseAudioQt/sink_p.h b/src/PulseAudioQt/sink_p.h
new file mode 100644
index 0000000..2b8efd2
--- /dev/null
+++ b/src/PulseAudioQt/sink_p.h
@@ -0,0 +1,25 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef SINK_P_H
+#define SINK_P_H
+
+#include "pulse/introspect.h"
+#include "sink.h"
+
+namespace PulseAudioQt
+{
+class SinkPrivate
+{
+public:
+ explicit SinkPrivate(Sink *q);
+
+ void update(const pa_sink_info *info);
+
+ Sink *q;
+ quint32 m_monitorIndex = -1;
+};
+}
+#endif
diff --git a/src/PulseAudioQt/sinkinput.cpp b/src/PulseAudioQt/sinkinput.cpp
new file mode 100644
index 0000000..235553f
--- /dev/null
+++ b/src/PulseAudioQt/sinkinput.cpp
@@ -0,0 +1,66 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "sinkinput.h"
+#include "sinkinput_p.h"
+
+#include "context.h"
+#include "context_p.h"
+#include "stream_p.h"
+
+namespace PulseAudioQt
+{
+SinkInput::SinkInput(QObject *parent)
+ : Stream(parent)
+ , d(new SinkInputPrivate(this))
+{
+}
+
+SinkInputPrivate::SinkInputPrivate(SinkInput *q)
+ : q(q)
+{
+}
+
+SinkInput::~SinkInput()
+{
+ delete d;
+}
+
+void SinkInputPrivate::update(const pa_sink_input_info *info)
+{
+ q->Stream::d->updateStream(info);
+ if (q->Stream::d->m_deviceIndex != info->sink) {
+ q->Stream::d->m_deviceIndex = info->sink;
+ Q_EMIT q->deviceIndexChanged();
+ }
+}
+
+void SinkInput::setDeviceIndex(quint32 deviceIndex)
+{
+ Context::instance()->d->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_sink_input_by_index);
+}
+
+void SinkInput::setVolume(qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), -1, volume, VolumeObject::d->cvolume(), &pa_context_set_sink_input_volume);
+}
+
+void SinkInput::setMuted(bool muted)
+{
+ Context::instance()->d->setGenericMute(index(), muted, &pa_context_set_sink_input_mute);
+}
+
+void SinkInput::setChannelVolume(int channel, qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), channel, volume, VolumeObject::d->cvolume(), &pa_context_set_sink_input_volume);
+}
+
+void SinkInput::setChannelVolumes(const QVector<qint64> &channelVolumes)
+{
+ Context::instance()->d->setGenericVolumes(index(), channelVolumes, VolumeObject::d->m_volume, &pa_context_set_sink_input_volume);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/sinkinput.h b/src/PulseAudioQt/sinkinput.h
new file mode 100644
index 0000000..85dce4e
--- /dev/null
+++ b/src/PulseAudioQt/sinkinput.h
@@ -0,0 +1,41 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef SINKINPUT_H
+#define SINKINPUT_H
+
+#include "stream.h"
+
+struct pa_sink_input_info;
+
+namespace PulseAudioQt
+{
+/**
+ * A SinkInput stream.
+ */
+class PULSEAUDIOQT_EXPORT SinkInput : public Stream
+{
+ Q_OBJECT
+
+public:
+ ~SinkInput();
+
+ void setVolume(qint64 volume) override;
+ void setMuted(bool muted) override;
+ void setChannelVolume(int channel, qint64 volume) override;
+ void setDeviceIndex(quint32 deviceIndex) override;
+ void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
+
+private:
+ SinkInput(QObject *parent);
+
+ class SinkInputPrivate *const d;
+ friend class MapBase<SinkInput, pa_sink_input_info>;
+};
+
+} // PulseAudioQt
+
+#endif // SINKINPUT_H
diff --git a/src/PulseAudioQt/sinkinput_p.h b/src/PulseAudioQt/sinkinput_p.h
new file mode 100644
index 0000000..f7748c9
--- /dev/null
+++ b/src/PulseAudioQt/sinkinput_p.h
@@ -0,0 +1,25 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef SINKINPUT_P_H
+#define SINKINPUT_P_H
+
+#include "sinkinput.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class SinkInputPrivate
+{
+public:
+ explicit SinkInputPrivate(SinkInput *q);
+
+ SinkInput *q;
+
+ void update(const pa_sink_input_info *info);
+};
+}
+
+#endif
diff --git a/src/PulseAudioQt/source.cpp b/src/PulseAudioQt/source.cpp
new file mode 100644
index 0000000..e6a34c8
--- /dev/null
+++ b/src/PulseAudioQt/source.cpp
@@ -0,0 +1,81 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "source.h"
+#include "source_p.h"
+
+#include "context.h"
+#include "context_p.h"
+#include "device_p.h"
+#include "server.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+Source::Source(QObject *parent)
+ : Device(parent)
+ , d(new SourcePrivate(this))
+{
+ connect(Context::instance()->server(), &Server::defaultSourceChanged, this, &Source::defaultChanged);
+}
+
+SourcePrivate::SourcePrivate(Source *q)
+ : q(q)
+{
+}
+
+void SourcePrivate::update(const pa_source_info *info)
+{
+ q->Device::d->updateDevice(info);
+}
+
+void Source::setVolume(qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), -1, volume, VolumeObject::d->cvolume(), &pa_context_set_source_volume_by_index);
+}
+
+void Source::setMuted(bool muted)
+{
+ Context::instance()->d->setGenericMute(index(), muted, &pa_context_set_source_mute_by_index);
+}
+
+void Source::setActivePortIndex(quint32 port_index)
+{
+ Port *port = qobject_cast<Port *>(ports().at(port_index));
+ if (!port) {
+ qWarning() << "invalid port set request" << port_index;
+ return;
+ }
+ Context::instance()->d->setGenericPort(index(), port->name(), &pa_context_set_source_port_by_index);
+}
+
+void Source::setChannelVolume(int channel, qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), channel, volume, VolumeObject::d->cvolume(), &pa_context_set_source_volume_by_index);
+}
+
+bool Source::isDefault() const
+{
+ return Context::instance()->server()->defaultSource() == this;
+}
+
+void Source::setDefault(bool enable)
+{
+ if (!isDefault() && enable) {
+ Context::instance()->server()->setDefaultSource(this);
+ }
+}
+
+void Source::setChannelVolumes(const QVector<qint64> &volumes)
+{
+ Context::instance()->d->setGenericVolumes(index(), volumes, VolumeObject::d->m_volume, &pa_context_set_source_volume_by_index);
+}
+
+Source::~Source()
+{
+ delete d;
+}
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/source.h b/src/PulseAudioQt/source.h
new file mode 100644
index 0000000..17c4cb9
--- /dev/null
+++ b/src/PulseAudioQt/source.h
@@ -0,0 +1,44 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef SOURCE_H
+#define SOURCE_H
+
+#include "device.h"
+
+struct pa_source_info;
+
+namespace PulseAudioQt
+{
+/**
+ * A PulseAudio source. This class is based on https://freedesktop.org/software/pulseaudio/doxygen/structpa__source__info.html.
+ */
+class PULSEAUDIOQT_EXPORT Source : public Device
+{
+ Q_OBJECT
+
+public:
+ ~Source();
+
+ void setVolume(qint64 volume) override;
+ void setMuted(bool muted) override;
+ void setActivePortIndex(quint32 port_index) override;
+ void setChannelVolume(int channel, qint64 volume) override;
+ void setChannelVolumes(const QVector<qint64> &volumes) override;
+
+ bool isDefault() const override;
+ void setDefault(bool enable) override;
+
+private:
+ explicit Source(QObject *parent);
+
+ class SourcePrivate *const d;
+ friend class MapBase<Source, pa_source_info>;
+};
+
+} // PulseAudioQt
+
+#endif // SOURCE_H
diff --git a/src/PulseAudioQt/source_p.h b/src/PulseAudioQt/source_p.h
new file mode 100644
index 0000000..46becdf
--- /dev/null
+++ b/src/PulseAudioQt/source_p.h
@@ -0,0 +1,24 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef SOURCE_P_H
+#define SOURCE_P_H
+
+#include "source.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class SourcePrivate
+{
+public:
+ explicit SourcePrivate(Source *q);
+
+ void update(const pa_source_info *info);
+
+ Source *q;
+};
+}
+#endif
diff --git a/src/PulseAudioQt/sourceoutput.cpp b/src/PulseAudioQt/sourceoutput.cpp
new file mode 100644
index 0000000..1420da3
--- /dev/null
+++ b/src/PulseAudioQt/sourceoutput.cpp
@@ -0,0 +1,65 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "sourceoutput.h"
+#include "sourceoutput_p.h"
+
+#include "context.h"
+#include "context_p.h"
+#include "stream_p.h"
+
+namespace PulseAudioQt
+{
+SourceOutput::SourceOutput(QObject *parent)
+ : Stream(parent)
+ , d(new SourceOutputPrivate(this))
+{
+}
+
+SourceOutput::~SourceOutput()
+{
+}
+
+SourceOutputPrivate::SourceOutputPrivate(SourceOutput *q)
+ : q(q)
+{
+}
+
+void SourceOutputPrivate::update(const pa_source_output_info *info)
+{
+ q->Stream::d->updateStream(info);
+ if (q->Stream::d->m_deviceIndex != info->source) {
+ q->Stream::d->m_deviceIndex = info->source;
+ Q_EMIT q->deviceIndexChanged();
+ }
+}
+
+void SourceOutput::setDeviceIndex(quint32 deviceIndex)
+{
+ Context::instance()->d->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_source_output_by_index);
+}
+
+void SourceOutput::setVolume(qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), -1, volume, VolumeObject::d->cvolume(), &pa_context_set_source_output_volume);
+}
+
+void SourceOutput::setMuted(bool muted)
+{
+ Context::instance()->d->setGenericMute(index(), muted, &pa_context_set_source_output_mute);
+}
+
+void SourceOutput::setChannelVolume(int channel, qint64 volume)
+{
+ Context::instance()->d->setGenericVolume(index(), channel, volume, VolumeObject::d->cvolume(), &pa_context_set_source_output_volume);
+}
+
+void SourceOutput::setChannelVolumes(const QVector<qint64> &channelVolumes)
+{
+ Context::instance()->d->setGenericVolumes(index(), channelVolumes, VolumeObject::d->m_volume, &pa_context_set_source_output_volume);
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/sourceoutput.h b/src/PulseAudioQt/sourceoutput.h
new file mode 100644
index 0000000..758f80a
--- /dev/null
+++ b/src/PulseAudioQt/sourceoutput.h
@@ -0,0 +1,41 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef SOURCEOUTPUT_H
+#define SOURCEOUTPUT_H
+
+#include "stream.h"
+
+struct pa_source_output_info;
+
+namespace PulseAudioQt
+{
+/**
+ * A SourceOutput Stream.
+ */
+class PULSEAUDIOQT_EXPORT SourceOutput : public Stream
+{
+ Q_OBJECT
+
+public:
+ ~SourceOutput();
+
+ void setVolume(qint64 volume) override;
+ void setMuted(bool muted) override;
+ void setChannelVolume(int channel, qint64 volume) override;
+ void setDeviceIndex(quint32 deviceIndex) override;
+ void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
+
+private:
+ explicit SourceOutput(QObject *parent);
+
+ class SourceOutputPrivate *const d;
+ friend class MapBase<SourceOutput, pa_source_output_info>;
+};
+
+} // PulseAudioQt
+
+#endif // SOURCEOUTPUT_H
diff --git a/src/PulseAudioQt/sourceoutput_p.h b/src/PulseAudioQt/sourceoutput_p.h
new file mode 100644
index 0000000..02d5d5a
--- /dev/null
+++ b/src/PulseAudioQt/sourceoutput_p.h
@@ -0,0 +1,26 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef SOURCEOUTPUT_P_H
+#define SOURCEOUTPUT_P_H
+
+#include "sourceoutput.h"
+#include <pulse/introspect.h>
+
+namespace PulseAudioQt
+{
+class SourceOutputPrivate
+{
+public:
+ explicit SourceOutputPrivate(SourceOutput *q);
+
+ void update(const pa_source_output_info *info);
+
+ SourceOutput *q;
+};
+
+}
+
+#endif
diff --git a/src/PulseAudioQt/stream.cpp b/src/PulseAudioQt/stream.cpp
new file mode 100644
index 0000000..8a392f0
--- /dev/null
+++ b/src/PulseAudioQt/stream.cpp
@@ -0,0 +1,60 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "stream.h"
+#include "context_p.h"
+#include "stream_p.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+Stream::Stream(QObject *parent)
+ : VolumeObject(parent)
+ , d(new StreamPrivate(this))
+{
+ VolumeObject::d->m_volumeWritable = false;
+}
+
+Stream::~Stream()
+{
+ delete d;
+}
+
+StreamPrivate::StreamPrivate(Stream *q)
+ : q(q)
+{
+}
+
+StreamPrivate::~StreamPrivate()
+{
+}
+
+Client *Stream::client() const
+{
+ return Context::instance()->d->m_clients.data().value(d->m_clientIndex, nullptr);
+}
+
+bool Stream::isVirtualStream() const
+{
+ return d->m_virtualStream;
+}
+
+quint32 Stream::deviceIndex() const
+{
+ return d->m_deviceIndex;
+}
+
+bool Stream::isCorked() const
+{
+ return d->m_corked;
+}
+
+bool Stream::hasVolume() const
+{
+ return d->m_hasVolume;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/stream.h b/src/PulseAudioQt/stream.h
new file mode 100644
index 0000000..c48a429
--- /dev/null
+++ b/src/PulseAudioQt/stream.h
@@ -0,0 +1,60 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef STREAM_H
+#define STREAM_H
+
+#include <QString>
+
+#include "volumeobject.h"
+
+// Properties need fully qualified classes even with pointers.
+#include "client.h"
+
+namespace PulseAudioQt
+{
+class StreamPrivate;
+
+class PULSEAUDIOQT_EXPORT Stream : public VolumeObject
+{
+ Q_OBJECT
+ Q_PROPERTY(PulseAudioQt::Client *client READ client NOTIFY clientChanged)
+ Q_PROPERTY(bool virtualStream READ isVirtualStream NOTIFY virtualStreamChanged)
+ Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged)
+ Q_PROPERTY(bool corked READ isCorked NOTIFY corkedChanged)
+ Q_PROPERTY(bool hasVolume READ hasVolume NOTIFY hasVolumeChanged)
+
+public:
+ ~Stream();
+
+ Client *client() const;
+ bool isVirtualStream() const;
+ quint32 deviceIndex() const;
+ bool isCorked() const;
+ bool hasVolume() const;
+
+ virtual void setDeviceIndex(quint32 deviceIndex) = 0;
+
+Q_SIGNALS:
+ void clientChanged();
+ void virtualStreamChanged();
+ void deviceIndexChanged();
+ void corkedChanged();
+ void hasVolumeChanged();
+
+protected:
+ /** @private */
+ explicit Stream(QObject *parent);
+ /** @private */
+ class StreamPrivate *const d;
+
+ friend class SinkInputPrivate;
+ friend class SourceOutputPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // STREAM_H
diff --git a/src/PulseAudioQt/stream_p.h b/src/PulseAudioQt/stream_p.h
new file mode 100644
index 0000000..7d2f2e2
--- /dev/null
+++ b/src/PulseAudioQt/stream_p.h
@@ -0,0 +1,56 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef STREAM_P_H
+#define STREAM_P_H
+
+#include "stream.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+class StreamPrivate
+{
+public:
+ explicit StreamPrivate(Stream *q);
+ virtual ~StreamPrivate();
+
+ Stream *q;
+
+ quint32 m_deviceIndex = PA_INVALID_INDEX;
+ quint32 m_clientIndex = PA_INVALID_INDEX;
+ bool m_virtualStream = false;
+ bool m_corked = false;
+ bool m_hasVolume = false;
+
+ template<typename PAInfo>
+ void updateStream(const PAInfo *info)
+ {
+ q->VolumeObject::d->updateVolumeObject(info);
+
+ if (m_hasVolume != info->has_volume) {
+ m_hasVolume = info->has_volume;
+ Q_EMIT q->hasVolumeChanged();
+ }
+ if (q->VolumeObject::d->m_volumeWritable != info->volume_writable) {
+ q->VolumeObject::d->m_volumeWritable = info->volume_writable;
+ Q_EMIT q->isVolumeWritableChanged();
+ }
+ if (m_clientIndex != info->client) {
+ m_clientIndex = info->client;
+ Q_EMIT q->clientChanged();
+ }
+ if (m_virtualStream != (info->client == PA_INVALID_INDEX)) {
+ m_virtualStream = info->client == PA_INVALID_INDEX;
+ Q_EMIT q->virtualStreamChanged();
+ }
+ if (m_corked != info->corked) {
+ m_corked = info->corked;
+ Q_EMIT q->corkedChanged();
+ }
+ }
+};
+}
+#endif
diff --git a/src/PulseAudioQt/streamrestore.cpp b/src/PulseAudioQt/streamrestore.cpp
new file mode 100644
index 0000000..fee9f88
--- /dev/null
+++ b/src/PulseAudioQt/streamrestore.cpp
@@ -0,0 +1,214 @@
+/*
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "streamrestore.h"
+#include "context.h"
+#include "context_p.h"
+#include "streamrestore_p.h"
+
+#include "debug.h"
+#include "pulseobject_p.h"
+
+namespace PulseAudioQt
+{
+StreamRestore::StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent)
+ : PulseObject(parent)
+ , d(new StreamRestorePrivate(this))
+{
+ memset(&d->m_volume, 0, sizeof(d->m_volume));
+ memset(&d->m_channelMap, 0, sizeof(d->m_channelMap));
+
+ d->m_index = index;
+ PulseObject::d->m_properties = properties;
+}
+
+StreamRestore::~StreamRestore()
+{
+}
+
+StreamRestorePrivate::StreamRestorePrivate(StreamRestore *q)
+ : q(q)
+{
+}
+
+StreamRestorePrivate::~StreamRestorePrivate()
+{
+}
+
+void StreamRestorePrivate::update(const pa_ext_stream_restore_info *info)
+{
+ q->PulseObject::d->updatePulseObject(info);
+ m_cache.valid = false;
+
+ const QString infoDevice = QString::fromUtf8(info->device);
+ if (m_device != infoDevice) {
+ m_device = infoDevice;
+ Q_EMIT q->deviceChanged();
+ }
+ if (m_muted != info->mute) {
+ m_muted = info->mute;
+ Q_EMIT q->mutedChanged();
+ }
+ if (!pa_cvolume_equal(&m_volume, &info->volume)) {
+ m_volume = info->volume;
+ Q_EMIT q->volumeChanged();
+ Q_EMIT q->channelVolumesChanged();
+ }
+ if (!pa_channel_map_equal(&m_channelMap, &info->channel_map)) {
+ m_channels.clear();
+ m_channels.reserve(info->channel_map.channels);
+ for (int i = 0; i < info->channel_map.channels; ++i) {
+ m_channels << QString::fromUtf8(pa_channel_position_to_pretty_string(info->channel_map.map[i]));
+ }
+ m_channelMap = info->channel_map;
+ Q_EMIT q->channelsChanged();
+ }
+}
+
+QString StreamRestore::device() const
+{
+ return d->m_device;
+}
+
+void StreamRestore::setDevice(const QString &device)
+{
+ if (d->m_cache.valid) {
+ if (d->m_cache.device != device) {
+ d->writeChanges(d->m_cache.volume, d->m_cache.muted, device);
+ }
+ } else {
+ if (d->m_device != device) {
+ d->writeChanges(d->m_volume, d->m_muted, device);
+ }
+ }
+}
+
+qint64 StreamRestore::volume() const
+{
+ return d->m_volume.values[0];
+}
+
+void StreamRestore::setVolume(qint64 volume)
+{
+ pa_cvolume vol = d->m_cache.valid ? d->m_cache.volume : d->m_volume;
+
+ // If no channel exists force one. We need one to be able to control the volume
+ // See https://bugs.kde.org/show_bug.cgi?id=407397
+ if (vol.channels == 0) {
+ vol.channels = 1;
+ }
+
+ for (int i = 0; i < vol.channels; ++i) {
+ vol.values[i] = volume;
+ }
+
+ if (d->m_cache.valid) {
+ d->writeChanges(vol, d->m_cache.muted, d->m_cache.device);
+ } else {
+ d->writeChanges(vol, d->m_muted, d->m_device);
+ }
+}
+
+bool StreamRestore::isMuted() const
+{
+ return d->m_muted;
+}
+
+void StreamRestore::setMuted(bool muted)
+{
+ if (d->m_cache.valid) {
+ if (d->m_cache.muted != muted) {
+ d->writeChanges(d->m_cache.volume, muted, d->m_cache.device);
+ }
+ } else {
+ if (d->m_muted != muted) {
+ d->writeChanges(d->m_volume, muted, d->m_device);
+ }
+ }
+}
+
+bool StreamRestore::hasVolume() const
+{
+ return true;
+}
+
+bool StreamRestore::isVolumeWritable() const
+{
+ return true;
+}
+
+QVector<QString> StreamRestore::channels() const
+{
+ return d->m_channels;
+}
+
+QVector<qreal> StreamRestore::channelVolumes() const
+{
+ QVector<qreal> ret;
+ ret.reserve(d->m_volume.channels);
+ for (int i = 0; i < d->m_volume.channels; ++i) {
+ ret << d->m_volume.values[i];
+ }
+ return ret;
+}
+
+void StreamRestore::setChannelVolume(int channel, qint64 volume)
+{
+ Q_ASSERT(channel >= 0 && channel < d->m_volume.channels);
+ pa_cvolume vol = d->m_cache.valid ? d->m_cache.volume : d->m_volume;
+ vol.values[channel] = volume;
+
+ if (d->m_cache.valid) {
+ d->writeChanges(vol, d->m_cache.muted, d->m_cache.device);
+ } else {
+ d->writeChanges(vol, d->m_muted, d->m_device);
+ }
+}
+
+quint32 StreamRestore::deviceIndex() const
+{
+ return PA_INVALID_INDEX;
+}
+
+void StreamRestore::setDeviceIndex(quint32 deviceIndex)
+{
+ Q_UNUSED(deviceIndex);
+ qWarning() << "Not implemented";
+}
+
+void StreamRestorePrivate::writeChanges(const pa_cvolume &volume, bool muted, const QString &device)
+{
+ const QByteArray nameData = q->name().toUtf8();
+ const QByteArray deviceData = device.toUtf8();
+
+ pa_ext_stream_restore_info info;
+ info.name = nameData.constData();
+ info.channel_map = m_channelMap;
+ info.volume = volume;
+ info.device = deviceData.isEmpty() ? nullptr : deviceData.constData();
+ info.mute = muted;
+
+ // If no channel exists force one. We need one to be able to control the volume
+ // See https://bugs.kde.org/show_bug.cgi?id=407397
+ if (info.channel_map.channels == 0) {
+ info.channel_map.channels = 1;
+ info.channel_map.map[0] = PA_CHANNEL_POSITION_MONO;
+ }
+
+ m_cache.valid = true;
+ m_cache.volume = volume;
+ m_cache.muted = muted;
+ m_cache.device = device;
+
+ Context::instance()->d->streamRestoreWrite(&info);
+}
+
+quint32 StreamRestore::index() const
+{
+ return d->m_index;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/streamrestore.h b/src/PulseAudioQt/streamrestore.h
new file mode 100644
index 0000000..cb74006
--- /dev/null
+++ b/src/PulseAudioQt/streamrestore.h
@@ -0,0 +1,74 @@
+/*
+ SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef STREAMRESTORE_H
+#define STREAMRESTORE_H
+
+#include "pulseobject.h"
+
+struct pa_ext_stream_restore_info;
+
+namespace PulseAudioQt
+{
+class PULSEAUDIOQT_EXPORT StreamRestore : public PulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString device READ device WRITE setDevice NOTIFY deviceChanged)
+ Q_PROPERTY(qint64 volume READ volume WRITE setVolume NOTIFY volumeChanged)
+ Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged)
+ Q_PROPERTY(bool hasVolume READ hasVolume CONSTANT)
+ Q_PROPERTY(bool volumeWritable READ isVolumeWritable CONSTANT)
+ Q_PROPERTY(QVector<QString> channels READ channels NOTIFY channelsChanged)
+ Q_PROPERTY(QVector<qreal> channelVolumes READ channelVolumes NOTIFY channelVolumesChanged)
+ Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged)
+ // Not a IndexedPulseObject since pa_ext_stream_restore_info does not have an index member
+ Q_PROPERTY(quint32 index READ index CONSTANT)
+
+public:
+ ~StreamRestore();
+
+ QString device() const;
+ void setDevice(const QString &device);
+
+ qint64 volume() const;
+ void setVolume(qint64 volume);
+
+ bool isMuted() const;
+ void setMuted(bool muted);
+
+ bool hasVolume() const;
+ bool isVolumeWritable() const;
+
+ QVector<QString> channels() const;
+
+ QVector<qreal> channelVolumes() const;
+
+ quint32 index() const;
+
+ quint32 deviceIndex() const;
+ void setDeviceIndex(quint32 deviceIndex);
+
+ void setChannelVolume(int channel, qint64 volume);
+
+Q_SIGNALS:
+ void deviceChanged();
+ void volumeChanged();
+ void mutedChanged();
+ void channelsChanged();
+ void channelVolumesChanged();
+ void deviceIndexChanged();
+
+private:
+ explicit StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent);
+
+ class StreamRestorePrivate *const d;
+ friend class MapBase<StreamRestore, pa_ext_stream_restore_info>;
+ friend class ContextPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // STREAMRESTORE_H
diff --git a/src/PulseAudioQt/streamrestore_p.h b/src/PulseAudioQt/streamrestore_p.h
new file mode 100644
index 0000000..80c32c5
--- /dev/null
+++ b/src/PulseAudioQt/streamrestore_p.h
@@ -0,0 +1,42 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef STREAMRESTORE_P_H
+#define STREAMRESTORE_P_H
+
+#include "streamrestore.h"
+#include <QVector>
+#include <pulse/ext-stream-restore.h>
+
+namespace PulseAudioQt
+{
+class StreamRestorePrivate
+{
+public:
+ explicit StreamRestorePrivate(StreamRestore *q);
+ virtual ~StreamRestorePrivate();
+
+ void writeChanges(const pa_cvolume &volume, bool muted, const QString &device);
+ void update(const pa_ext_stream_restore_info *info);
+
+ StreamRestore *q;
+ QString m_device;
+ pa_cvolume m_volume;
+ pa_channel_map m_channelMap;
+ QVector<QString> m_channels;
+ bool m_muted = false;
+ quint32 m_index = 0;
+
+ struct {
+ bool valid = false;
+ pa_cvolume volume;
+ bool muted;
+ QString device;
+ } m_cache;
+};
+
+}
+
+#endif
diff --git a/src/PulseAudioQt/volumeobject.cpp b/src/PulseAudioQt/volumeobject.cpp
new file mode 100644
index 0000000..a18a12a
--- /dev/null
+++ b/src/PulseAudioQt/volumeobject.cpp
@@ -0,0 +1,70 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include <pulse/volume.h>
+
+#include "volumeobject.h"
+#include "volumeobject_p.h"
+
+namespace PulseAudioQt
+{
+VolumeObject::VolumeObject(QObject *parent)
+ : IndexedPulseObject(parent)
+ , d(new VolumeObjectPrivate(this))
+{
+}
+
+VolumeObjectPrivate::VolumeObjectPrivate(VolumeObject *q)
+ : q(q)
+{
+ pa_cvolume_init(&m_volume);
+}
+
+VolumeObject::~VolumeObject()
+{
+}
+
+qint64 VolumeObject::volume() const
+{
+ return pa_cvolume_max(&d->m_volume);
+}
+
+bool VolumeObject::isMuted() const
+{
+ return d->m_muted;
+}
+
+pa_cvolume VolumeObjectPrivate::cvolume() const
+{
+ return m_volume;
+}
+
+bool VolumeObject::isVolumeWritable() const
+{
+ return d->m_volumeWritable;
+}
+
+QVector<QString> VolumeObject::channels() const
+{
+ return d->m_channels;
+}
+
+QStringList VolumeObject::rawChannels() const
+{
+ return d->m_rawChannels;
+}
+
+QVector<qint64> VolumeObject::channelVolumes() const
+{
+ QVector<qint64> ret;
+ ret.reserve(d->m_volume.channels);
+ for (int i = 0; i < d->m_volume.channels; ++i) {
+ ret << d->m_volume.values[i];
+ }
+ return ret;
+}
+
+} // PulseAudioQt
diff --git a/src/PulseAudioQt/volumeobject.h b/src/PulseAudioQt/volumeobject.h
new file mode 100644
index 0000000..d560963
--- /dev/null
+++ b/src/PulseAudioQt/volumeobject.h
@@ -0,0 +1,79 @@
+/*
+ SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef VOLUMEOBJECT_H
+#define VOLUMEOBJECT_H
+
+#include "indexedpulseobject.h"
+
+namespace PulseAudioQt
+{
+/**
+ * An PulseObject that has a volume. Can be a Device or a Stream.
+ */
+class PULSEAUDIOQT_EXPORT VolumeObject : public IndexedPulseObject
+{
+ Q_OBJECT
+ Q_PROPERTY(qint64 volume READ volume WRITE setVolume NOTIFY volumeChanged)
+ Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged)
+ Q_PROPERTY(bool volumeWritable READ isVolumeWritable NOTIFY isVolumeWritableChanged)
+ Q_PROPERTY(QVector<QString> channels READ channels NOTIFY channelsChanged)
+ Q_PROPERTY(QVector<qint64> channelVolumes READ channelVolumes WRITE setChannelVolumes NOTIFY channelVolumesChanged)
+ Q_PROPERTY(QStringList rawChannels READ rawChannels NOTIFY rawChannelsChanged)
+
+public:
+ ~VolumeObject();
+
+ /**
+ * This object's volume
+ */
+ qint64 volume() const;
+
+ /**
+ * Set the volume for this object.
+ * This affects all channels.
+ * The volume must be between PulseAudioQt::minimumVolume() and PulseAudioQt::maximumVolume().
+ */
+ virtual void setVolume(qint64 volume) = 0;
+
+ /**
+ * Whether this object is muted.
+ */
+ bool isMuted() const;
+
+ /**
+ * Set whether this object is muted.
+ */
+ virtual void setMuted(bool muted) = 0;
+
+ bool isVolumeWritable() const;
+
+ QVector<QString> channels() const;
+ QStringList rawChannels() const;
+ QVector<qint64> channelVolumes() const;
+ virtual void setChannelVolumes(const QVector<qint64> &channelVolumes) = 0;
+ Q_INVOKABLE virtual void setChannelVolume(int channel, qint64 volume) = 0;
+
+Q_SIGNALS:
+ void volumeChanged();
+ void mutedChanged();
+ void isVolumeWritableChanged();
+ void channelsChanged();
+ void rawChannelsChanged();
+ void channelVolumesChanged();
+
+protected:
+ /** @private */
+ explicit VolumeObject(QObject *parent);
+ /** @private */
+ class VolumeObjectPrivate *const d;
+ friend class DevicePrivate;
+ friend class StreamPrivate;
+};
+
+} // PulseAudioQt
+
+#endif // VOLUMEOBJECT_H
diff --git a/src/PulseAudioQt/volumeobject_p.h b/src/PulseAudioQt/volumeobject_p.h
new file mode 100644
index 0000000..7ac5b55
--- /dev/null
+++ b/src/PulseAudioQt/volumeobject_p.h
@@ -0,0 +1,67 @@
+/*
+ SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
+
+ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+#ifndef VOLUMEOBJECT_P_H
+#define VOLUMEOBJECT_P_H
+
+#include <pulse/volume.h>
+
+#include "indexedpulseobject_p.h"
+#include "volumeobject.h"
+
+namespace PulseAudioQt
+{
+class VolumeObjectPrivate
+{
+public:
+ explicit VolumeObjectPrivate(VolumeObject *q);
+
+ VolumeObject *q;
+ pa_channel_map foo;
+ pa_cvolume m_volume;
+ bool m_muted = true;
+ bool m_volumeWritable = true;
+ QVector<QString> m_channels;
+ QStringList m_rawChannels;
+
+ pa_cvolume cvolume() const;
+
+ template<typename PAInfo>
+ void updateVolumeObject(PAInfo *info)
+ {
+ q->IndexedPulseObject::d->updatePulseObject(info);
+ q->PulseObject::d->updateProperties(info);
+ if (m_muted != info->mute) {
+ m_muted = info->mute;
+ Q_EMIT q->mutedChanged();
+ }
+ if (!pa_cvolume_equal(&m_volume, &info->volume)) {
+ m_volume = info->volume;
+ Q_EMIT q->volumeChanged();
+ Q_EMIT q->channelVolumesChanged();
+ }
+ QVector<QString> infoChannels;
+ infoChannels.reserve(info->channel_map.channels);
+ for (int i = 0; i < info->channel_map.channels; ++i) {
+ infoChannels << QString::fromUtf8(pa_channel_position_to_pretty_string(info->channel_map.map[i]));
+ }
+ if (m_channels != infoChannels) {
+ m_channels = infoChannels;
+ Q_EMIT q->channelsChanged();
+ }
+
+ QStringList infoRawChannels;
+ infoRawChannels.reserve(info->channel_map.channels);
+ for (int i = 0; i < info->channel_map.channels; ++i) {
+ infoRawChannels << QString::fromUtf8(pa_channel_position_to_string(info->channel_map.map[i]));
+ }
+ if (m_rawChannels != infoRawChannels) {
+ m_rawChannels = infoRawChannels;
+ Q_EMIT q->rawChannelsChanged();
+ }
+ }
+};
+}
+#endif