diff options
author | Simon Rettberg | 2022-08-18 20:27:25 +0200 |
---|---|---|
committer | Simon Rettberg | 2022-08-18 20:27:25 +0200 |
commit | c48cebd620d3f5330c104d85ac32f0aaffadaa14 (patch) | |
tree | 8fc74d8aa05fcdc75bfcdf93fdc89c0f071692eb /src/PulseAudioQt | |
parent | When changing default output of card, also set default sink (diff) | |
download | pavucontrol-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')
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 |