Add audio module

pull/8/head
reionwong 4 years ago
parent 132c9f24e1
commit a60274820b

@ -9,11 +9,13 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/)
include(GenerateExportHeader)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
set(QT Core Gui Quick QuickControls2 DBus Xml Concurrent)
set(QT Core Gui Quick QuickControls2 Widgets DBus Xml Concurrent)
find_package(Qt5 REQUIRED ${QT})
# Get the installation directory from qmake
@ -39,3 +41,4 @@ add_subdirectory(mpris)
add_subdirectory(networkmanagement)
add_subdirectory(screen)
add_subdirectory(system)
add_subdirectory(audio)

@ -0,0 +1,82 @@
add_definitions(-DTRANSLATION_DOMAIN=\"cutefish_pulseaudio\")
set(USE_GSETTINGS False)
set(USE_GCONF False)
configure_file(config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h)
set(audio_SRCS
card.cpp
client.cpp
context.cpp
device.cpp
maps.cpp
operation.cpp
port.cpp
profile.cpp
pulseaudio.cpp
pulseobject.cpp
sink.cpp
sinkinput.cpp
modulemanager.cpp
source.cpp
sourceoutput.cpp
stream.cpp
volumemonitor.cpp
volumeobject.cpp
debug.cpp
server.cpp
streamrestore.cpp
module.cpp
canberracontext.cpp
speakertest.cpp
qml/listitemmenu.cpp
qml/plugin.cpp
# qml/microphoneindicator.cpp
# qml/volumeosd.cpp
qml/volumefeedback.cpp
model/sortfiltermodel.cpp
)
set(qml_SRCS
qml/qmldir
qml/PulseObjectFilterModel.qml
)
find_package(PkgConfig)
find_package(Canberra REQUIRED)
find_package(CanberraPulse)
set_package_properties(CanberraPulse PROPERTIES
DESCRIPTION "Pulseaudio backend for libcanberra"
PURPOSE "Required for volume feedback sounds"
TYPE RUNTIME
)
find_package(SoundThemeFreedesktop)
set_package_properties(SoundThemeFreedesktop PROPERTIES
DESCRIPTION "The standard freedesktop sound theme"
PURPOSE "Required for volume feedback sounds"
URL "https://www.freedesktop.org/wiki/Specifications/sound-theme-spec/"
TYPE RUNTIME
)
pkg_check_modules(LIBPULSE libpulse REQUIRED IMPORTED_TARGET)
pkg_check_modules(LIBPULSE_MAINLOOP libpulse-mainloop-glib REQUIRED IMPORTED_TARGET)
add_library(cutefishaudio_qmlplugins SHARED ${audio_SRCS})
target_link_libraries(cutefishaudio_qmlplugins
Qt5::Core
Qt5::Qml
Qt5::Gui
Qt5::Widgets
Qt5::DBus
Qt5::Quick
PkgConfig::LIBPULSE
PkgConfig::LIBPULSE_MAINLOOP
)
install(TARGETS cutefishaudio_qmlplugins DESTINATION ${INSTALL_QMLDIR}/Cutefish/Audio)
install(FILES ${qml_SRCS} DESTINATION ${INSTALL_QMLDIR}/Cutefish/Audio)

@ -0,0 +1,51 @@
/*
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 "canberracontext.h"
namespace QPulseAudio
{
CanberraContext *CanberraContext::s_context = nullptr;
CanberraContext *CanberraContext::instance()
{
if (!s_context) {
s_context = new CanberraContext;
}
return s_context;
}
CanberraContext::CanberraContext(QObject *parent)
: QObject(parent)
{
ca_context_create(&m_canberra);
}
CanberraContext::~CanberraContext()
{
if (m_canberra) {
ca_context_destroy(m_canberra);
}
}
ca_context *CanberraContext::canberra()
{
return m_canberra;
}
void CanberraContext::ref()
{
++m_references;
}
void CanberraContext::unref()
{
if (--m_references == 0) {
delete this;
s_context = nullptr;
}
}
}

@ -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
*/
#pragma once
#include <QObject>
#include <canberra.h>
namespace QPulseAudio
{
class CanberraContext : public QObject
{
Q_OBJECT
public:
explicit CanberraContext(QObject *parent = nullptr);
virtual ~CanberraContext();
static CanberraContext *instance();
ca_context *canberra();
void ref();
void unref();
private:
ca_context *m_canberra = nullptr;
int m_references = 0;
static CanberraContext *s_context;
};
}

@ -0,0 +1,112 @@
/*
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 "debug.h"
#include "context.h"
namespace QPulseAudio
{
Card::Card(QObject *parent)
: PulseObject(parent)
{
}
void Card::update(const pa_card_info *info)
{
updatePulseObject(info);
QString infoName = QString::fromUtf8(info->name);
if (m_name != infoName) {
m_name = infoName;
Q_EMIT nameChanged();
}
const quint32 oldActiveProfileIndex = m_activeProfileIndex;
bool profilesHaveChanged = false;
int i = 0;
for (auto **it = info->profiles2; it && *it != nullptr; ++it) {
if (i < m_profiles.count()) {
Profile *profile = static_cast<Profile *>(m_profiles.at(i));
profilesHaveChanged |= profile->setInfo(*it);
} else {
Profile *profile = new Profile(this);
profile->setInfo(*it);
m_profiles.append(profile);
profilesHaveChanged = true;
}
if (info->active_profile2 == *it) {
m_activeProfileIndex = i;
}
++i;
}
while (m_profiles.count() > i) {
delete m_profiles.takeLast();
profilesHaveChanged = true;
}
if (profilesHaveChanged) {
Q_EMIT profilesChanged();
}
if (profilesHaveChanged || m_activeProfileIndex != oldActiveProfileIndex) {
Q_EMIT activeProfileIndexChanged();
}
bool portsHaveChanged = false;
i = 0;
for (auto **ports = info->ports; ports && *ports != nullptr; ++ports) {
if (i < m_ports.count()) {
Port *port = static_cast<Port *>(m_ports.at(i));
portsHaveChanged |= port->setInfo(*ports);
} else {
Port *port = new Port(this);
port->setInfo(*ports);
m_ports.append(port);
portsHaveChanged = true;
}
++i;
}
while (m_ports.count() > i) {
delete m_ports.takeLast();
portsHaveChanged = true;
}
if (portsHaveChanged) {
Q_EMIT portsChanged();
}
}
QString Card::name() const
{
return m_name;
}
QList<QObject *> Card::profiles() const
{
return m_profiles;
}
quint32 Card::activeProfileIndex() const
{
return m_activeProfileIndex;
}
void Card::setActiveProfileIndex(quint32 profileIndex)
{
const Profile *profile = qobject_cast<Profile *>(profiles().at(profileIndex));
context()->setCardProfile(index(), profile->name());
}
QList<QObject *> Card::ports() const
{
return m_ports;
}
} // QPulseAudio

@ -0,0 +1,108 @@
/*
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 <pulse/introspect.h>
#include <QMap>
#include <QVariant>
#include "port.h"
#include "profile.h"
#include "pulseobject.h"
namespace QPulseAudio
{
class CardPort : public Port
{
Q_OBJECT
Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged)
public:
explicit CardPort(QObject *parent = nullptr)
: Port(parent)
{
}
~CardPort() override
{
}
// int direction; /**< A #pa_direction enum, indicating the direction of this port. */
// uint32_t n_profiles; /**< Number of entries in profile array */
// pa_card_profile_info** profiles; /**< \deprecated Superseded by profiles2 */
// int64_t latency_offset; /**< Latency offset of the port that gets added to the sink/source latency when the port is active. \since 3.0 */
// pa_card_profile_info2** profiles2; /**< Array of pointers to available profiles, or NULL. Array is terminated by an entry set to NULL. \since 5.0 */
void update(const pa_card_port_info *info)
{
setInfo(info);
QVariantMap properties;
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) {
qCDebug(PLASMAPA) << "property" << key << "not a string";
continue;
}
Q_ASSERT(value);
properties.insert(QString::fromUtf8(key), QString::fromUtf8(value));
}
if (m_properties != properties) {
m_properties = properties;
Q_EMIT propertiesChanged();
}
}
QVariantMap properties() const
{
return m_properties;
}
Q_SIGNALS:
void propertiesChanged();
private:
QVariantMap m_properties;
};
class Card : public PulseObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QList<QObject *> profiles READ profiles NOTIFY profilesChanged)
Q_PROPERTY(quint32 activeProfileIndex READ activeProfileIndex WRITE setActiveProfileIndex NOTIFY activeProfileIndexChanged)
Q_PROPERTY(QList<QObject *> ports READ ports NOTIFY portsChanged)
public:
explicit Card(QObject *parent);
void update(const pa_card_info *info);
QString name() const;
QList<QObject *> profiles() const;
quint32 activeProfileIndex() const;
void setActiveProfileIndex(quint32 profileIndex);
QList<QObject *> ports() const;
Q_SIGNALS:
void nameChanged();
void profilesChanged();
void activeProfileIndexChanged();
void portsChanged();
private:
QString m_name;
QList<QObject *> m_profiles;
quint32 m_activeProfileIndex = -1;
QList<QObject *> m_ports;
};
} // QPulseAudio
#endif // CARD_H

@ -0,0 +1,38 @@
/*
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 "debug.h"
namespace QPulseAudio
{
Client::Client(QObject *parent)
: PulseObject(parent)
{
}
Client::~Client()
{
}
void Client::update(const pa_client_info *info)
{
updatePulseObject(info);
QString infoName = QString::fromUtf8(info->name);
if (m_name != infoName) {
m_name = infoName;
Q_EMIT nameChanged();
}
}
QString Client::name() const
{
return m_name;
}
} // QPulseAudio

@ -0,0 +1,39 @@
/*
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 <pulse/introspect.h>
#include <QMap>
#include "pulseobject.h"
namespace QPulseAudio
{
class Client : public PulseObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
explicit Client(QObject *parent);
~Client() override;
void update(const pa_client_info *info);
QString name() const;
Q_SIGNALS:
void nameChanged();
private:
QString m_name;
};
} // QPulseAudio
#endif // CLIENT_H

@ -0,0 +1,4 @@
/* config.h. Generated by cmake from config.h.cmake */
#cmakedefine01 USE_GSETTINGS
#cmakedefine01 USE_GCONF

@ -0,0 +1,627 @@
/*
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 <QMutexLocker>
#include <QTimer>
#include "card.h"
#include "client.h"
#include "module.h"
#include "sink.h"
#include "sinkinput.h"
#include "source.h"
#include "sourceoutput.h"
#include "streamrestore.h"
namespace QPulseAudio
{
Context *Context::s_context = nullptr;
QString Context::s_applicationId;
const qint64 Context::NormalVolume = PA_VOLUME_NORM;
const qint64 Context::MinimalVolume = 0;
const qint64 Context::MaximalVolume = (PA_VOLUME_NORM / 100.0) * 150;
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);
((Context *)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) {
qCDebug(PLASMAPA) << "Ignoring event role sink input.";
return;
}
}
Q_ASSERT(context);
Q_ASSERT(data);
((Context *)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);
((Context *)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);
((Context *)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);
((Context *)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);
((Context *)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);
((Context *)data)->moduleCallback(info);
}
static void server_cb(pa_context *context, const pa_server_info *info, void *data)
{
Q_ASSERT(context);
Q_ASSERT(data);
((Context *)data)->serverCallback(info);
}
static void context_state_callback(pa_context *context, void *data)
{
Q_ASSERT(data);
((Context *)data)->contextStateCallback(context);
}
static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data)
{
Q_ASSERT(data);
((Context *)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);
((Context *)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))) {
qCWarning(PLASMAPA) << "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) {
Context *context = static_cast<Context *>(data);
const QByteArray deviceData = context->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();
context->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) {
Context *context = static_cast<Context *>(data);
const QByteArray deviceData = context->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();
context->streamRestoreWrite(&newinfo);
}
}
// --------------------------
Context::Context(QObject *parent)
: QObject(parent)
, m_server(new Server(this))
, m_context(nullptr)
, m_mainloop(nullptr)
, m_references(0)
{
QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QStringLiteral("org.pulseaudio.Server"), //
QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForRegistration,
this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &Context::connectToDaemon);
connectToDaemon();
}
Context::~Context()
{
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()
{
if (!s_context) {
s_context = new Context;
}
return s_context;
}
void Context::ref()
{
++m_references;
}
void Context::unref()
{
if (--m_references == 0) {
delete this;
s_context = nullptr;
}
}
void Context::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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "pa_context_get_server_info() failed";
return;
}
break;
}
}
void Context::contextStateCallback(pa_context *c)
{
qCDebug(PLASMAPA) << "state callback";
pa_context_state_t state = pa_context_get_state(c);
if (state == PA_CONTEXT_READY) {
qCDebug(PLASMAPA) << "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))) {
qCWarning(PLASMAPA) << "pa_context_subscribe() failed";
return;
}
}
if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_get_sink_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_get_source_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_client_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_get_card_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) {
qCWarning(PLASMAPA) << "pa_context_get_sink_input_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_get_source_output_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) {
qCWarning(PLASMAPA) << "pa_context_get_module_info_list() failed";
return;
}
if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) {
qCWarning(PLASMAPA) << "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 {
qCWarning(PLASMAPA) << "Failed to initialize stream_restore extension";
}
} else if (!PA_CONTEXT_IS_GOOD(state)) {
qCWarning(PLASMAPA) << "context kaput";
if (m_context) {
pa_context_unref(m_context);
m_context = nullptr;
}
reset();
QTimer::singleShot(1000, this, &Context::connectToDaemon);
}
}
void Context::sinkCallback(const pa_sink_info *info)
{
// This parenting here is a bit weird
m_sinks.updateEntry(info, this);
}
void Context::sinkInputCallback(const pa_sink_input_info *info)
{
m_sinkInputs.updateEntry(info, this);
}
void Context::sourceCallback(const pa_source_info *info)
{
m_sources.updateEntry(info, this);
}
void Context::sourceOutputCallback(const pa_source_output_info *info)
{
m_sourceOutputs.updateEntry(info, this);
}
void Context::clientCallback(const pa_client_info *info)
{
m_clients.updateEntry(info, this);
}
void Context::cardCallback(const pa_card_info *info)
{
m_cards.updateEntry(info, this);
}
void Context::moduleCallback(const pa_module_info *info)
{
m_modules.updateEntry(info, this);
}
void Context::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, this);
obj->update(info);
m_streamRestores.insert(obj);
} else {
obj->update(info);
}
}
void Context::serverCallback(const pa_server_info *info)
{
m_server->update(info);
}
void Context::setCardProfile(quint32 index, const QString &profile)
{
if (!m_context) {
return;
}
qCDebug(PLASMAPA) << index << profile;
if (!PAOperation(pa_context_set_card_profile_by_index(m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_context_set_card_profile_by_index failed";
return;
}
}
void Context::setDefaultSink(const QString &name)
{
if (!m_context) {
return;
}
const QByteArray nameData = name.toUtf8();
if (!PAOperation(pa_context_set_default_sink(m_context, nameData.constData(), nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_context_set_default_sink failed";
}
// Change device for all entries in stream-restore database
m_newDefaultSink = name;
if (!PAOperation(pa_ext_stream_restore_read(m_context, ext_stream_restore_change_sink_cb, this))) {
qCWarning(PLASMAPA) << "pa_ext_stream_restore_read failed";
}
}
void Context::setDefaultSource(const QString &name)
{
if (!m_context) {
return;
}
const QByteArray nameData = name.toUtf8();
if (!PAOperation(pa_context_set_default_source(m_context, nameData.constData(), nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_context_set_default_source failed";
}
// Change device for all entries in stream-restore database
m_newDefaultSource = name;
if (!PAOperation(pa_ext_stream_restore_read(m_context, ext_stream_restore_change_source_cb, this))) {
qCWarning(PLASMAPA) << "pa_ext_stream_restore_read failed";
}
}
void Context::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))) {
qCWarning(PLASMAPA) << "pa_ext_stream_restore_write failed";
}
}
void Context::connectToDaemon()
{
if (m_context) {
return;
}
// We require a glib event loop
if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib")
&& !QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("GlibEventDispatcher")) {
qCWarning(PLASMAPA) << "Disabling PulseAudio integration for lack of GLib event loop";
return;
}
qCDebug(PLASMAPA) << "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, QString("Cutefish PA").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, "audio-card");
m_context = pa_context_new_with_proplist(api, nullptr, proplist);
pa_proplist_free(proplist);
Q_ASSERT(m_context);
if (pa_context_connect(m_context, nullptr, 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 Context::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();
}
void Context::setApplicationId(const QString &applicationId)
{
s_applicationId = applicationId;
}
} // QPulseAudio

@ -0,0 +1,223 @@
/*
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 <QMutex>
#include <QObject>
#include <QSet>
#include <pulse/ext-stream-restore.h>
#include <pulse/glib-mainloop.h>
#include <pulse/mainloop.h>
#include <pulse/pulseaudio.h>
#include "maps.h"
#include "operation.h"
namespace QPulseAudio
{
class Server;
class Context : public QObject
{
Q_OBJECT
public:
explicit Context(QObject *parent = nullptr);
~Context() override;
static Context *instance();
static const qint64 NormalVolume;
static const qint64 MinimalVolume;
static const qint64 MaximalVolume;
void ref();
void unref();
bool isValid()
{
return m_context && m_mainloop;
}
pa_context *context() const
{
return m_context;
}
const SinkMap &sinks() const
{
return m_sinks;
}
const SinkInputMap &sinkInputs() const
{
return m_sinkInputs;
}
const SourceMap &sources() const
{
return m_sources;
}
const SourceOutputMap &sourceOutputs() const
{
return m_sourceOutputs;
}
const ClientMap &clients() const
{
return m_clients;
}
const CardMap &cards() const
{
return m_cards;
}
const ModuleMap &modules() const
{
return m_modules;
}
const StreamRestoreMap &streamRestores() const
{
return m_streamRestores;
}
Server *server() const
{
return m_server;
}
QString newDefaultSink() const
{
return m_newDefaultSink;
}
QString newDefaultSource() const
{
return m_newDefaultSource;
}
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 setCardProfile(quint32 index, const QString &profile);
void setDefaultSink(const QString &name);
void setDefaultSource(const QString &name);
void streamRestoreWrite(const pa_ext_stream_restore_info *info);
static void setApplicationId(const QString &applicationId);
template<typename PAFunction>
void setGenericVolume(quint32 index, int channel, qint64 newVolume, pa_cvolume cVolume, PAFunction 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 (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_set_volume failed";
return;
}
}
template<typename PAFunction>
void setGenericVolumes(quint32 index, QVector<qint64> channelVolumes, pa_cvolume cVolume, PAFunction 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))) {
qCWarning(PLASMAPA) << "pa_set_volume failed";
return;
}
}
template<typename PAFunction>
void setGenericMute(quint32 index, bool mute, PAFunction pa_set_mute)
{
if (!m_context) {
return;
}
if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_set_mute failed";
return;
}
}
template<typename PAFunction>
void setGenericPort(quint32 index, const QString &portName, PAFunction pa_set_port)
{
if (!m_context) {
return;
}
if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_set_port failed";
return;
}
}
template<typename PAFunction>
void setGenericDeviceForStream(quint32 streamIndex, quint32 deviceIndex, PAFunction pa_move_stream_to_device)
{
if (!m_context) {
return;
}
if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) {
qCWarning(PLASMAPA) << "pa_move_stream_to_device failed";
return;
}
}
private:
void connectToDaemon();
void reset();
// 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 Context *s_context;
static QString s_applicationId;
};
} // QPulseAudio
#endif // CONTEXT_H

@ -0,0 +1,9 @@
/* This file is part of the KDE project
SPDX-FileCopyrightText: 2015 Bhushan Shah <bshah@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "debug.h"
Q_LOGGING_CATEGORY(PLASMAPA, "com.cutefish.pulseaudio", QtWarningMsg)

@ -0,0 +1,13 @@
/* This file is part of the KDE project
SPDX-FileCopyrightText: 2015 Bhushan Shah <bshah@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef DEBUG_H
#define DEBUG_H
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(PLASMAPA)
#endif

@ -0,0 +1,68 @@
/*
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"
QPulseAudio::Device::State QPulseAudio::Device::state() const
{
return m_state;
}
QString QPulseAudio::Device::name() const
{
return m_name;
}
QString QPulseAudio::Device::description() const
{
return m_description;
}
QString QPulseAudio::Device::formFactor() const
{
return m_formFactor;
}
quint32 QPulseAudio::Device::cardIndex() const
{
return m_cardIndex;
}
QList<QObject *> QPulseAudio::Device::ports() const
{
return m_ports;
}
quint32 QPulseAudio::Device::activePortIndex() const
{
return m_activePortIndex;
}
bool QPulseAudio::Device::isVirtualDevice() const
{
return m_virtualDevice;
}
QPulseAudio::Device::Device(QObject *parent)
: VolumeObject(parent)
{
}
QPulseAudio::Device::State QPulseAudio::Device::stateFromPaState(int value) const
{
switch (value) {
case -1: // PA_X_INVALID_STATE
return InvalidState;
case 0: // PA_X_RUNNING
return RunningState;
case 1: // PA_X_IDLE
return IdleState;
case 2: // PA_X_SUSPENDED
return SuspendedState;
default:
return UnknownState;
}
}

@ -0,0 +1,160 @@
/*
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 DEVICE_H
#define DEVICE_H
#include <QString>
#include <pulse/volume.h>
#include "port.h"
#include "pulseobject.h"
#include "volumeobject.h"
namespace QPulseAudio
{
class Device : public VolumeObject
{
Q_OBJECT
Q_PROPERTY(State state READ state NOTIFY stateChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
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<QObject *> 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)
Q_PROPERTY(bool virtualDevice READ isVirtualDevice NOTIFY virtualDeviceChanged)
public:
enum State {
InvalidState = 0,
RunningState,
IdleState,
SuspendedState,
UnknownState,
};
Q_ENUMS(State);
~Device() override
{
}
template<typename PAInfo>
void updateDevice(const PAInfo *info)
{
updateVolumeObject(info);
if (m_name != info->name) {
m_name = info->name;
Q_EMIT nameChanged();
}
if (m_description != info->description) {
m_description = info->description;
Q_EMIT 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 formFactorChanged();
}
}
if (m_cardIndex != info->card) {
m_cardIndex = info->card;
Q_EMIT cardIndexChanged();
}
const quint32 oldActivePortIndex = m_activePortIndex;
bool portsHaveChanged = false;
int i = 0;
for (auto **ports = info->ports; ports && *ports != nullptr; ++ports) {
if (i < m_ports.count()) {
Port *port = static_cast<Port *>(m_ports.at(i));
portsHaveChanged |= port->setInfo(*ports);
} else {
Port *port = new Port(this);
port->setInfo(*ports);
m_ports.append(port);
portsHaveChanged = true;
}
if (info->active_port == *ports) {
m_activePortIndex = i;
}
++i;
}
while (m_ports.count() > i) {
delete m_ports.takeLast();
portsHaveChanged = true;
}
if (portsHaveChanged) {
Q_EMIT portsChanged();
}
if (portsHaveChanged || m_activePortIndex != oldActivePortIndex) {
Q_EMIT activePortIndexChanged();
}
State infoState = stateFromPaState(info->state);
if (infoState != m_state) {
m_state = infoState;
Q_EMIT stateChanged();
}
const bool isVirtual = !(info->flags & 4); // PA_X_HARDWARE
if (m_virtualDevice != isVirtual) {
m_virtualDevice = isVirtual;
Q_EMIT virtualDeviceChanged();
}
}
State state() const;
QString name() const;
QString description() const;
QString formFactor() const;
quint32 cardIndex() const;
QList<QObject *> ports() const;
quint32 activePortIndex() const;
virtual void setActivePortIndex(quint32 port_index) = 0;
virtual bool isDefault() const = 0;
virtual void setDefault(bool enable) = 0;
bool isVirtualDevice() const;
virtual Q_INVOKABLE void switchStreams() = 0;
Q_SIGNALS:
void stateChanged();
void nameChanged();
void descriptionChanged();
void formFactorChanged();
void cardIndexChanged();
void portsChanged();
void activePortIndexChanged();
void defaultChanged();
void virtualDeviceChanged();
protected:
explicit Device(QObject *parent);
private:
State stateFromPaState(int value) const;
QString m_name;
QString m_description;
QString m_formFactor;
quint32 m_cardIndex = -1;
QList<QObject *> m_ports;
quint32 m_activePortIndex = -1;
State m_state = UnknownState;
bool m_virtualDevice = false;
};
} // QPulseAudio
#endif // DEVICE_H

@ -0,0 +1,313 @@
/*
* SPDX-FileCopyrightText: 2009 Nokia Corporation and/or its subsidiary(-ies).
* SPDX-FileCopyrightText: 2016 David Edmundson <davidedmundson@kde.org>
* SPDX-FileContributor: Marius Vollmer <marius.vollmer@nokia.com>
*
* SPDX-License-Identifier: LGPL-2.1-only
*/
#include <QByteArray>
#include <QDebug>
#include <QString>
#include <QVariant>
#include "gconfitem.h"
#include <gconf/gconf-client.h>
#include <gconf/gconf-value.h>
#include <glib.h>
struct GConfItemPrivate {
QString root;
QVariant value;
guint notify_id;
static void notify_trampoline(GConfClient *, guint, GConfEntry *, gpointer);
};
#define withClient(c) for (GConfClient *c = (gconf_client_get_default()); c; g_object_unref(c), c = NULL)
static QByteArray convertKey(QString key)
{
if (key.startsWith('/')) {
return key.toUtf8();
} else {
qWarning() << "Using dot-separated key names with GConfItem is deprecated.";
qWarning() << "Please use" << '/' + key.replace('.', '/') << "instead of" << key;
return '/' + key.replace('.', '/').toUtf8();
}
}
static QString convertKey(const char *key)
{
return QString::fromUtf8(key);
}
static QVariant convertValue(GConfValue *src)
{
if (!src) {
return QVariant();
} else {
switch (src->type) {
case GCONF_VALUE_INVALID:
return QVariant(QVariant::Invalid);
case GCONF_VALUE_BOOL:
return QVariant((bool)gconf_value_get_bool(src));
case GCONF_VALUE_INT:
return QVariant(gconf_value_get_int(src));
case GCONF_VALUE_FLOAT:
return QVariant(gconf_value_get_float(src));
case GCONF_VALUE_STRING:
return QVariant(QString::fromUtf8(gconf_value_get_string(src)));
case GCONF_VALUE_LIST:
switch (gconf_value_get_list_type(src)) {
case GCONF_VALUE_STRING: {
QStringList result;
for (GSList *elts = gconf_value_get_list(src); elts; elts = elts->next)
result.append(QString::fromUtf8(gconf_value_get_string((GConfValue *)elts->data)));
return QVariant(result);
}
default: {
QList<QVariant> result;
for (GSList *elts = gconf_value_get_list(src); elts; elts = elts->next)
result.append(convertValue((GConfValue *)elts->data));
return QVariant(result);
}
}
case GCONF_VALUE_SCHEMA:
default:
return QVariant();
}
}
}
static GConfValue *convertString(const QString &str)
{
GConfValue *v = gconf_value_new(GCONF_VALUE_STRING);
gconf_value_set_string(v, str.toUtf8().data());
return v;
}
static GConfValueType primitiveType(const QVariant &elt)
{
switch (elt.type()) {
case QVariant::String:
return GCONF_VALUE_STRING;
case QVariant::Int:
return GCONF_VALUE_INT;
case QVariant::Double:
return GCONF_VALUE_FLOAT;
case QVariant::Bool:
return GCONF_VALUE_BOOL;
default:
return GCONF_VALUE_INVALID;
}
}
static GConfValueType uniformType(const QList<QVariant> &list)
{
GConfValueType result = GCONF_VALUE_INVALID;
Q_FOREACH (const QVariant &elt, list) {
GConfValueType elt_type = primitiveType(elt);
if (elt_type == GCONF_VALUE_INVALID)
return GCONF_VALUE_INVALID;
if (result == GCONF_VALUE_INVALID)
result = elt_type;
else if (result != elt_type)
return GCONF_VALUE_INVALID;
}
if (result == GCONF_VALUE_INVALID)
return GCONF_VALUE_STRING; // empty list.
else
return result;
}
static int convertValue(const QVariant &src, GConfValue **valp)
{
GConfValue *v;
switch (src.type()) {
case QVariant::Invalid:
v = nullptr;
break;
case QVariant::Bool:
v = gconf_value_new(GCONF_VALUE_BOOL);
gconf_value_set_bool(v, src.toBool());
break;
case QVariant::Int:
v = gconf_value_new(GCONF_VALUE_INT);
gconf_value_set_int(v, src.toInt());
break;
case QVariant::Double:
v = gconf_value_new(GCONF_VALUE_FLOAT);
gconf_value_set_float(v, src.toDouble());
break;
case QVariant::String:
v = convertString(src.toString());
break;
case QVariant::StringList: {
GSList *elts = nullptr;
v = gconf_value_new(GCONF_VALUE_LIST);
gconf_value_set_list_type(v, GCONF_VALUE_STRING);
Q_FOREACH (const QString &str, src.toStringList())
elts = g_slist_prepend(elts, convertString(str));
gconf_value_set_list_nocopy(v, g_slist_reverse(elts));
break;
}
case QVariant::List: {
GConfValueType elt_type = uniformType(src.toList());
if (elt_type == GCONF_VALUE_INVALID)
v = nullptr;
else {
GSList *elts = nullptr;
v = gconf_value_new(GCONF_VALUE_LIST);
gconf_value_set_list_type(v, elt_type);
Q_FOREACH (const QVariant &elt, src.toList()) {
GConfValue *val = nullptr;
convertValue(elt, &val); // guaranteed to succeed.
elts = g_slist_prepend(elts, val);
}
gconf_value_set_list_nocopy(v, g_slist_reverse(elts));
}
break;
}
default:
return 0;
}
*valp = v;
return 1;
}
void GConfItemPrivate::notify_trampoline(GConfClient *, guint, GConfEntry *entry, gpointer data)
{
GConfItem *item = (GConfItem *)data;
item->update_value(true, entry->key, convertValue(entry->value));
}
void GConfItem::update_value(bool emit_signal, const QString &key, const QVariant &value)
{
QVariant new_value;
if (emit_signal) {
subtreeChanged(key, value);
}
}
QString GConfItem::root() const
{
return priv->root;
}
QVariant GConfItem::value(const QString &subKey) const
{
QVariant new_value;
withClient(client)
{
GError *error = nullptr;
QByteArray k = convertKey(priv->root + '/' + subKey);
GConfValue *v = gconf_client_get(client, k.data(), &error);
if (error) {
qWarning() << error->message;
g_error_free(error);
new_value = QVariant();
} else {
new_value = convertValue(v);
if (v)
gconf_value_free(v);
}
}
return new_value;
}
void GConfItem::set(const QString &subKey, const QVariant &val)
{
withClient(client)
{
QByteArray k = convertKey(priv->root + '/' + subKey);
GConfValue *v;
if (convertValue(val, &v)) {
GError *error = nullptr;
if (v) {
gconf_client_set(client, k.data(), v, &error);
gconf_value_free(v);
} else {
gconf_client_unset(client, k.data(), &error);
}
if (error) {
qWarning() << error->message;
g_error_free(error);
}
} else {
qWarning() << "Can't store a" << val.typeName();
}
}
}
QList<QString> GConfItem::listDirs() const
{
QList<QString> children;
withClient(client)
{
QByteArray k = convertKey(priv->root);
GSList *dirs = gconf_client_all_dirs(client, k.data(), nullptr);
for (GSList *d = dirs; d; d = d->next) {
children.append(convertKey((char *)d->data));
g_free(d->data);
}
g_slist_free(dirs);
}
return children;
}
QList<QString> GConfItem::listEntries() const
{
QList<QString> children;
withClient(client)
{
QByteArray k = convertKey(priv->root);
GSList *entries = gconf_client_all_entries(client, k.data(), nullptr);
for (GSList *e = entries; e; e = e->next) {
children.append(convertKey(((GConfEntry *)e->data)->key));
gconf_entry_free((GConfEntry *)e->data);
}
g_slist_free(entries);
}
return children;
}
GConfItem::GConfItem(const QString &key, QObject *parent)
: QObject(parent)
, priv(new GConfItemPrivate)
{
priv->root = key;
withClient(client)
{
QByteArray k = convertKey(priv->root);
gconf_client_add_dir(client, k.data(), GCONF_CLIENT_PRELOAD_ONELEVEL, nullptr);
priv->notify_id = gconf_client_notify_add(client, k.data(), GConfItemPrivate::notify_trampoline, this, nullptr, nullptr);
}
}
GConfItem::~GConfItem()
{
withClient(client)
{
QByteArray k = convertKey(priv->root);
gconf_client_notify_remove(client, priv->notify_id);
gconf_client_remove_dir(client, k.data(), nullptr);
}
delete priv;
}

@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2009 Nokia Corporation.
* SPDX-FileCopyrightText: 2016 David Edmundson <davidedmundson@kde.org>
*
* Contact: Marius Vollmer <marius.vollmer@nokia.com>
*
* SPDX-License-Identifier: LGPL-2.1-only
*
*/
#ifndef GCONFITEM_H
#define GCONFITEM_H
#include <QObject>
#include <QStringList>
#include <QVariant>
/*!
\brief GConfItem is a simple C++ wrapper for GConf.
Creating a GConfItem instance gives you access to a single GConf
key. You can get and set its value, and connect to its
valueChanged() signal to be notified about changes.
The value of a GConf key is returned to you as a QVariant, and you
pass in a QVariant when setting the value. GConfItem converts
between a QVariant and GConf values as needed, and according to the
following rules:
- A QVariant of type QVariant::Invalid denotes an unset GConf key.
- QVariant::Int, QVariant::Double, QVariant::Bool are converted to
and from the obvious equivalents.
- QVariant::String is converted to/from a GConf string and always
uses the UTF-8 encoding. No other encoding is supported.
- QVariant::StringList is converted to a list of UTF-8 strings.
- QVariant::List (which denotes a QList<QVariant>) is converted
to/from a GConf list. All elements of such a list must have the
same type, and that type must be one of QVariant::Int,
QVariant::Double, QVariant::Bool, or QVariant::String. (A list of
strings is returned as a QVariant::StringList, however, when you
get it back.)
- Any other QVariant or GConf value is essentially ignored.
- This is fored by Dave from libqtgconf to really reduce the amount of QObjects needed
to manipulate various items in a tree.
\warning GConfItem is as thread-safe as GConf.
*/
class GConfItem : public QObject
{
Q_OBJECT
public:
/*! Initializes a GConfItem to access the GConf key denoted by
\a key. Key names should follow the normal GConf conventions
like "/myapp/settings/first".
\param key The name of the key.
\param parent Parent object
*/
explicit GConfItem(const QString &keyRoot, QObject *parent = nullptr);
/*! Finalizes a GConfItem.
*/
~GConfItem() override;
/*! Returns the root of this item, as given to the constructor.
*/
QString root() const;
/*! Returns the current value of this item, as a QVariant.
* subkey is relative to the provided root.
*/
QVariant value(const QString &subKey) const;
/*! Returns the current value of this item, as a QVariant. If
* there is no value for this item, return \a def instead.
*/
void set(const QString &subKey, const QVariant &val);
/*! Return a list of the directories below this item. The
returned strings are absolute key names like
"/myapp/settings".
A directory is a key that has children. The same key might
also have a value, but that is confusing and best avoided.
*/
QList<QString> listDirs() const;
/*! Return a list of entries below this item. The returned
strings are absolute key names like "/myapp/settings/first".
A entry is a key that has a value. The same key might also
have children, but that is confusing and is best avoided.
*/
QList<QString> listEntries() const;
Q_SIGNALS:
/*! Emitted when some value in subtree of this item changes
*/
void subtreeChanged(const QString &key, const QVariant &value);
private:
friend struct GConfItemPrivate;
struct GConfItemPrivate *priv;
void update_value(bool emit_signal, const QString &key, const QVariant &value);
};
#endif // GCONFITEM_H

@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: LGPL-2.1-only
*
*/
#include <QString>
#include "debug.h"
#include "gsettingsitem.h"
QVariant GSettingsItem::value(const QString &key) const
{
if (!m_settings) {
return QVariant();
}
GVariant *gvalue = g_settings_get_value(m_settings, key.toLatin1().data());
QVariant toReturn;
switch (g_variant_classify(gvalue)) {
case G_VARIANT_CLASS_BOOLEAN:
toReturn = QVariant((bool)g_variant_get_boolean(gvalue));
break;
case G_VARIANT_CLASS_STRING:
toReturn = QVariant(QString::fromUtf8(g_variant_get_string(gvalue, nullptr)));
break;
default:
qCWarning(PLASMAPA()) << "Unhandled variant type in value()";
}
g_variant_unref(gvalue);
return toReturn;
}
void GSettingsItem::set(const QString &key, const QVariant &val)
{
if (!m_settings) {
return;
}
// It might be hard to detect the right GVariant type from
// complex QVariant types such as string lists or more detailed
// types such as integers (GVariant has different sizes),
// therefore we get the current value for the key and convert
// to QVariant using the GVariant type
GVariant *oldValue = g_settings_get_value(m_settings, key.toLatin1().data());
GVariant *newValue = nullptr;
switch (g_variant_type_peek_string(g_variant_get_type(oldValue))[0]) {
case G_VARIANT_CLASS_BOOLEAN:
newValue = g_variant_new_boolean(val.toBool());
break;
case G_VARIANT_CLASS_STRING:
newValue = g_variant_new_string(val.toString().toUtf8().constData());
break;
default:
qCWarning(PLASMAPA()) << "Unhandled variant type in set()";
}
if (newValue) {
g_settings_set_value(m_settings, key.toLatin1().data(), newValue);
}
g_variant_unref(oldValue);
}
bool GSettingsItem::isValid() const
{
return m_settings;
}
GSettingsItem::GSettingsItem(const QString &key, QObject *parent)
: QObject(parent)
{
const char schemaId[] = "org.freedesktop.pulseaudio.module-group";
// g_settings_new_with_path asserts if the schema doesn't exist, check this manually to avoid an abort.
auto *defaultSource = g_settings_schema_source_get_default();
if (!defaultSource) {
qCWarning(PLASMAPA) << "No GSettings schemas are installed on the system";
return;
}
auto *schema = g_settings_schema_source_lookup(defaultSource, schemaId, true /*recursive*/);
if (!schema) {
qCWarning(PLASMAPA) << "Settings schema" << schemaId << "is not installed";
return;
}
m_settings = g_settings_new_with_path(schemaId, key.toLatin1().data());
g_settings_schema_unref(schema);
g_signal_connect(m_settings, "changed", G_CALLBACK(GSettingsItem::settingChanged), this);
}
GSettingsItem::~GSettingsItem()
{
g_settings_sync();
if (m_settings) {
g_object_unref(m_settings);
}
}

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: LGPL-2.1-only
*
*/
#ifndef GSETTINGSITEM_H
#define GSETTINGSITEM_H
#include <QObject>
#include <QStringList>
#include <QVariant>
#include <gio/gio.h>
class GSettingsItem : public QObject
{
Q_OBJECT
public:
explicit GSettingsItem(const QString &key, QObject *parent = nullptr);
virtual ~GSettingsItem() override;
QVariant value(const QString &key) const;
void set(const QString &key, const QVariant &val);
bool isValid() const;
Q_SIGNALS:
void subtreeChanged();
private:
GSettings *m_settings = nullptr;
static void settingChanged(GSettings *settings, const gchar *key, gpointer data)
{
Q_UNUSED(settings)
Q_UNUSED(key)
GSettingsItem *self = static_cast<GSettingsItem *>(data);
Q_EMIT self->subtreeChanged();
}
};
#endif // GCONFITEM_H

@ -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"

@ -0,0 +1,170 @@
/*
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 MAPS_H
#define MAPS_H
#include "debug.h"
#include <QMap>
#include <QObject>
#include <pulse/ext-stream-restore.h>
#include <pulse/pulseaudio.h>
namespace QPulseAudio
{
// 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);
void aboutToBeRemoved(int index);
void removed(int index);
};
/**
* 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:
~MapBase() override
{
}
const QMap<quint32, Type *> &data() const
{
return m_data;
}
int count() const override
{
return m_data.count();
}
int indexOfObject(QObject *object) const override
{
int index = 0;
QMapIterator<quint32, Type *> it(m_data);
while (it.hasNext()) {
it.next();
if (it.value() == object) {
return index;
}
index++;
}
return -1;
}
QObject *objectAt(int index) const override
{
return (m_data.constBegin() + index).value();
}
void reset()
{
while (!m_data.isEmpty()) {
removeEntry(m_data.lastKey());
}
m_pendingRemovals.clear();
}
void insert(Type *object)
{
Q_ASSERT(!m_data.contains(object->index()));
int modelIndex = 0;
for (auto it = m_data.constBegin(); it != m_data.constEnd(); ++it) {
if (object->index() < it.key()) {
break;
}
modelIndex++;
}
Q_EMIT aboutToBeAdded(modelIndex);
m_data.insert(object->index(), object);
Q_ASSERT(modelIndex == m_data.keys().indexOf(object->index()));
Q_EMIT added(modelIndex);
}
// 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_data.value(info->index, nullptr);
if (!obj) {
obj = new Type(parent);
}
obj->update(info);
if (!m_data.contains(info->index)) {
insert(obj);
}
}
void removeEntry(quint32 index)
{
if (!m_data.contains(index)) {
m_pendingRemovals.insert(index);
} else {
const int modelIndex = m_data.keys().indexOf(index);
Q_EMIT aboutToBeRemoved(modelIndex);
delete m_data.take(index);
Q_EMIT removed(modelIndex);
}
}
protected:
QMap<quint32, Type *> m_data;
QSet<quint32> m_pendingRemovals;
};
typedef MapBase<Sink, pa_sink_info> SinkMap;
typedef MapBase<SinkInput, pa_sink_input_info> SinkInputMap;
typedef MapBase<Source, pa_source_info> SourceMap;
typedef MapBase<SourceOutput, pa_source_output_info> SourceOutputMap;
typedef MapBase<Client, pa_client_info> ClientMap;
typedef MapBase<Card, pa_card_info> CardMap;
typedef MapBase<Module, pa_module_info> ModuleMap;
typedef MapBase<StreamRestore, pa_ext_stream_restore_info> StreamRestoreMap;
} // QPulseAudio
#endif // MAPS_H

@ -0,0 +1,208 @@
#include "sortfiltermodel.h"
#include <QQmlContext>
#include <QQmlEngine>
SortFilterModel::SortFilterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
setObjectName(QStringLiteral("SortFilterModel"));
setDynamicSortFilter(true);
connect(this, &QAbstractItemModel::rowsInserted, this, &SortFilterModel::countChanged);
connect(this, &QAbstractItemModel::rowsRemoved, this, &SortFilterModel::countChanged);
connect(this, &QAbstractItemModel::modelReset, this, &SortFilterModel::countChanged);
connect(this, &SortFilterModel::countChanged, this, &SortFilterModel::syncRoleNames);
}
SortFilterModel::~SortFilterModel()
{
}
void SortFilterModel::syncRoleNames()
{
if (!sourceModel()) {
return;
}
m_roleIds.clear();
const QHash<int, QByteArray> rNames = roleNames();
m_roleIds.reserve(rNames.count());
for (auto i = rNames.constBegin(); i != rNames.constEnd(); ++i) {
m_roleIds[QString::fromUtf8(i.value())] = i.key();
}
setFilterRole(m_filterRole);
setSortRole(m_sortRole);
}
QHash<int, QByteArray> SortFilterModel::roleNames() const
{
if (sourceModel()) {
return sourceModel()->roleNames();
}
return {};
}
int SortFilterModel::roleNameToId(const QString &name) const
{
return m_roleIds.value(name, Qt::DisplayRole);
}
void SortFilterModel::setModel(QAbstractItemModel *model)
{
if (model == sourceModel()) {
return;
}
if (sourceModel()) {
disconnect(sourceModel(), &QAbstractItemModel::modelReset, this, &SortFilterModel::syncRoleNames);
}
QSortFilterProxyModel::setSourceModel(model);
if (model) {
connect(model, &QAbstractItemModel::modelReset, this, &SortFilterModel::syncRoleNames);
syncRoleNames();
}
Q_EMIT sourceModelChanged(model);
}
bool SortFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (m_filterCallback.isCallable()) {
QJSValueList args;
args << QJSValue(source_row);
const QModelIndex idx = sourceModel()->index(source_row, filterKeyColumn(), source_parent);
QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine();
args << engine->toScriptValue<QVariant>(idx.data(m_roleIds.value(m_filterRole)));
return const_cast<SortFilterModel *>(this)->m_filterCallback.call(args).toBool();
}
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}
void SortFilterModel::setFilterRegExp(const QString &exp)
{
if (exp == filterRegExp()) {
return;
}
QSortFilterProxyModel::setFilterRegExp(QRegExp(exp, Qt::CaseInsensitive));
Q_EMIT filterRegExpChanged(exp);
}
QString SortFilterModel::filterRegExp() const
{
return QSortFilterProxyModel::filterRegExp().pattern();
}
void SortFilterModel::setFilterString(const QString &filterString)
{
if (filterString == m_filterString) {
return;
}
m_filterString = filterString;
QSortFilterProxyModel::setFilterFixedString(filterString);
Q_EMIT filterStringChanged(filterString);
}
QString SortFilterModel::filterString() const
{
return m_filterString;
}
QJSValue SortFilterModel::filterCallback() const
{
return m_filterCallback;
}
void SortFilterModel::setFilterCallback(const QJSValue &callback)
{
if (m_filterCallback.strictlyEquals(callback)) {
return;
}
if (!callback.isNull() && !callback.isCallable()) {
return;
}
m_filterCallback = callback;
invalidateFilter();
Q_EMIT filterCallbackChanged(callback);
}
void SortFilterModel::setFilterRole(const QString &role)
{
QSortFilterProxyModel::setFilterRole(roleNameToId(role));
m_filterRole = role;
}
QString SortFilterModel::filterRole() const
{
return m_filterRole;
}
void SortFilterModel::setSortRole(const QString &role)
{
m_sortRole = role;
if (role.isEmpty()) {
sort(-1, Qt::AscendingOrder);
} else if (sourceModel()) {
QSortFilterProxyModel::setSortRole(roleNameToId(role));
sort(sortColumn(), sortOrder());
}
}
QString SortFilterModel::sortRole() const
{
return m_sortRole;
}
void SortFilterModel::setSortOrder(const Qt::SortOrder order)
{
if (order == sortOrder()) {
return;
}
sort(sortColumn(), order);
}
void SortFilterModel::setSortColumn(int column)
{
if (column == sortColumn()) {
return;
}
sort(column, sortOrder());
Q_EMIT sortColumnChanged();
}
QVariantMap SortFilterModel::get(int row) const
{
QModelIndex idx = index(row, 0);
QVariantMap hash;
const QHash<int, QByteArray> rNames = roleNames();
for (auto i = rNames.begin(); i != rNames.end(); ++i) {
hash[QString::fromUtf8(i.value())] = data(idx, i.key());
}
return hash;
}
int SortFilterModel::mapRowToSource(int row) const
{
QModelIndex idx = index(row, 0);
return mapToSource(idx).row();
}
int SortFilterModel::mapRowFromSource(int row) const
{
if (!sourceModel()) {
qWarning() << "No source model defined!";
return -1;
}
QModelIndex idx = sourceModel()->index(row, 0);
return mapFromSource(idx).row();
}

@ -0,0 +1,132 @@
#ifndef DATAMODEL_H
#define DATAMODEL_H
#include <QAbstractItemModel>
#include <QJSValue>
#include <QRegExp>
#include <QSortFilterProxyModel>
#include <QVector>
class SortFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
/**
* The source model of this sorting proxy model. It has to inherit QAbstractItemModel (ListModel is not supported)
*/
Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setModel NOTIFY sourceModelChanged)
/**
* The regular expression for the filter, only items with their filterRole matching filterRegExp will be displayed
*/
Q_PROPERTY(QString filterRegExp READ filterRegExp WRITE setFilterRegExp NOTIFY filterRegExpChanged)
/**
* The string for the filter, only items with their filterRole matching filterString will be displayed
*/
Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged REVISION 1)
/**
* A JavaScript callable that is passed the source model row index as first argument and the value
* of filterRole as second argument. The callable's return value is evaluated as boolean to determine
* whether the row is accepted (true) or filtered out (false). It overrides the default implementation
* that uses filterRegExp or filterString; while filterCallable is set those two properties are
* ignored. Attempts to write a non-callable to this property are silently ignored, but you can set
* it to null.
*/
Q_PROPERTY(QJSValue filterCallback READ filterCallback WRITE setFilterCallback NOTIFY filterCallbackChanged REVISION 1)
/**
* The role of the sourceModel on which filterRegExp must be applied.
*/
Q_PROPERTY(QString filterRole READ filterRole WRITE setFilterRole)
/**
* The role of the sourceModel that will be used for sorting. if empty the order will be left unaltered
*/
Q_PROPERTY(QString sortRole READ sortRole WRITE setSortRole)
/**
* One of Qt.Ascending or Qt.Descending
*/
Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder)
/**
* Specify which column should be used for sorting
*/
Q_PROPERTY(int sortColumn READ sortColumn WRITE setSortColumn NOTIFY sortColumnChanged)
/**
* How many items are in this model
*/
Q_PROPERTY(int count READ count NOTIFY countChanged)
friend class DataModel;
public:
explicit SortFilterModel(QObject *parent = nullptr);
~SortFilterModel() override;
void setModel(QAbstractItemModel *source);
void setFilterRegExp(const QString &exp);
QString filterRegExp() const;
void setFilterString(const QString &filterString);
QString filterString() const;
void setFilterCallback(const QJSValue &callback);
QJSValue filterCallback() const;
void setFilterRole(const QString &role);
QString filterRole() const;
void setSortRole(const QString &role);
QString sortRole() const;
void setSortOrder(const Qt::SortOrder order);
void setSortColumn(int column);
int count() const
{
return QSortFilterProxyModel::rowCount();
}
/**
* Returns the item at index in the list model.
* This allows the item data to be accessed (but not modified) from JavaScript.
* It returns an Object with a property for each role.
*
* @param i the row we want
*/
Q_INVOKABLE QVariantMap get(int i) const;
Q_INVOKABLE int mapRowToSource(int i) const;
Q_INVOKABLE int mapRowFromSource(int i) const;
Q_SIGNALS:
void countChanged();
void sortColumnChanged();
void sourceModelChanged(QObject *);
void filterRegExpChanged(const QString &);
Q_REVISION(1) void filterStringChanged(const QString &);
Q_REVISION(1) void filterCallbackChanged(const QJSValue &);
protected:
int roleNameToId(const QString &name) const;
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
QHash<int, QByteArray> roleNames() const override;
protected Q_SLOTS:
void syncRoleNames();
private:
QString m_filterRole;
QString m_sortRole;
QString m_filterString;
QJSValue m_filterCallback;
QHash<QString, int> m_roleIds;
};
#endif

@ -0,0 +1,46 @@
/*
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 "context.h"
namespace QPulseAudio
{
Module::Module(QObject *parent)
: PulseObject(parent)
{
}
void Module::update(const pa_module_info *info)
{
updatePulseObject(info);
const QString infoName = QString::fromUtf8(info->name);
if (m_name != infoName) {
m_name = infoName;
Q_EMIT nameChanged();
}
const QString infoArgument = QString::fromUtf8(info->argument);
if (m_argument != infoArgument) {
m_argument = infoArgument;
Q_EMIT argumentChanged();
}
}
QString Module::name() const
{
return m_name;
}
QString Module::argument() const
{
return m_argument;
}
} // QPulseAudio

@ -0,0 +1,44 @@
/*
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 <pulse/introspect.h>
#include <QMap>
#include <QVariant>
#include "pulseobject.h"
namespace QPulseAudio
{
class Module : public PulseObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString argument READ argument NOTIFY argumentChanged)
public:
explicit Module(QObject *parent);
void update(const pa_module_info *info);
QString name() const;
QString argument() const;
Q_SIGNALS:
void nameChanged();
void argumentChanged();
private:
QString m_name;
QString m_argument;
};
} // QPulseAudio
#endif // MODULE_H

@ -0,0 +1,192 @@
/*
SPDX-FileCopyrightText: 2016 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "modulemanager.h"
#include "config.h"
#include "module.h"
#include "server.h"
#if USE_GSETTINGS
#include "gsettingsitem.h"
#define PA_SETTINGS_PATH_MODULES "/org/freedesktop/pulseaudio/module-groups"
#endif
#if USE_GCONF
#include "gconfitem.h"
#define PA_SETTINGS_PATH_MODULES "/system/pulseaudio/modules"
#endif
#include <QTimer>
namespace QPulseAudio
{
#if USE_GCONF || USE_GSETTINGS
#if USE_GSETTINGS
class ConfigModule : public GSettingsItem
#elif USE_GCONF
class ConfigModule : public GConfItem
#endif
{
public:
ConfigModule(const QString &configName, const QString &moduleName, QObject *parent);
bool isEnabled() const;
void setEnabled(bool enabled, const QVariant &args = QVariant());
private:
QString m_moduleName;
};
ConfigModule::ConfigModule(const QString &configName, const QString &moduleName, QObject *parent)
:
#if USE_GSETTINGS
GSettingsItem(QStringLiteral(PA_SETTINGS_PATH_MODULES "/") + configName + QStringLiteral("/"), parent)
,
#elif USE_GCONF
GConfItem(QStringLiteral(PA_SETTINGS_PATH_MODULES "/") + configName, parent)
,
#endif
m_moduleName(moduleName)
{
}
bool ConfigModule::isEnabled() const
{
return value(QStringLiteral("enabled")).toBool();
}
void ConfigModule::setEnabled(bool enabled, const QVariant &args)
{
set(QStringLiteral("locked"), true);
if (enabled) {
set(QStringLiteral("name0"), m_moduleName);
set(QStringLiteral("args0"), args);
set(QStringLiteral("enabled"), true);
} else {
set(QStringLiteral("enabled"), false);
}
set(QStringLiteral("locked"), false);
}
#endif
ModuleManager::ModuleManager(QObject *parent)
: QObject(parent)
{
#if USE_GCONF || USE_GSETTINGS
m_combineSinks = new ConfigModule(QStringLiteral("combine"), QStringLiteral("module-combine"), this);
m_switchOnConnect = new ConfigModule(QStringLiteral("switch-on-connect"), QStringLiteral("module-switch-on-connect"), this);
m_deviceManager = new ConfigModule(QStringLiteral("device-manager"), QStringLiteral("module-device-manager"), this);
connect(m_combineSinks, &ConfigModule::subtreeChanged, this, &ModuleManager::combineSinksChanged);
connect(m_switchOnConnect, &ConfigModule::subtreeChanged, this, &ModuleManager::switchOnConnectChanged);
connect(m_deviceManager, &ConfigModule::subtreeChanged, this, &ModuleManager::switchOnConnectChanged);
#endif
connect(Context::instance()->server(), &Server::updated, this, &ModuleManager::serverUpdated);
QTimer *updateModulesTimer = new QTimer(this);
updateModulesTimer->setInterval(500);
updateModulesTimer->setSingleShot(true);
connect(updateModulesTimer, &QTimer::timeout, this, &ModuleManager::updateLoadedModules);
connect(&Context::instance()->modules(), &MapBaseQObject::added, updateModulesTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start));
connect(&Context::instance()->modules(), &MapBaseQObject::removed, updateModulesTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start));
updateLoadedModules();
}
ModuleManager::~ModuleManager(){};
bool ModuleManager::settingsSupported() const
{
// PipeWire does not (yet) have support for module-switch-on-connect and module-combine-sink
// Also switching streams is the default there
// TODO Check whether there is a PipeWire-specific way to do these
if (Context::instance()->server()->isPipeWire()) {
return false;
}
#if USE_GCONF || USE_GSETTINGS
return true;
#else
return false;
#endif
}
bool ModuleManager::combineSinks() const
{
#if USE_GCONF || USE_GSETTINGS
return m_combineSinks->isEnabled();
#else
return false;
#endif
}
void ModuleManager::setCombineSinks(bool combineSinks)
{
#if USE_GCONF || USE_GSETTINGS
m_combineSinks->setEnabled(combineSinks);
#else
Q_UNUSED(combineSinks)
#endif
}
bool ModuleManager::switchOnConnect() const
{
#if USE_GCONF || USE_GSETTINGS
// switch on connect and device-manager do the same task. Only one should be enabled
// Note on the first run m_deviceManager will appear to be disabled even though it's actually running
// because there is no gconf entry, however m_switchOnConnect will only exist if set by Plasma PA
// hence only check this entry
return m_switchOnConnect->isEnabled();
#else
return false;
#endif
}
void ModuleManager::setSwitchOnConnect(bool switchOnConnect)
{
#if USE_GCONF || USE_GSETTINGS
m_deviceManager->setEnabled(!switchOnConnect);
m_switchOnConnect->setEnabled(switchOnConnect);
#else
Q_UNUSED(switchOnConnect)
#endif
}
QStringList ModuleManager::loadedModules() const
{
return m_loadedModules;
}
void ModuleManager::updateLoadedModules()
{
m_loadedModules.clear();
const auto modules = Context::instance()->modules().data();
for (Module *module : modules) {
m_loadedModules.append(module->name());
}
Q_EMIT loadedModulesChanged();
}
bool ModuleManager::configModuleLoaded() const
{
return m_loadedModules.contains(configModuleName());
}
QString ModuleManager::configModuleName() const
{
#if USE_GCONF
return QStringLiteral("module-gconf");
#elif USE_GSETTINGS
return QStringLiteral("module-gsettings");
#else
return QString();
#endif
}
}

@ -0,0 +1,61 @@
/*
SPDX-FileCopyrightText: 2016 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef MODULEMANAGER_H
#define MODULEMANAGER_H
#include <QString>
#include <pulse/introspect.h>
#include "context.h"
// Properties need fully qualified classes even with pointers.
#include "client.h"
namespace QPulseAudio
{
class ConfigModule;
class ModuleManager : public QObject
{
Q_OBJECT
Q_PROPERTY(bool settingsSupported READ settingsSupported NOTIFY serverUpdated)
Q_PROPERTY(bool combineSinks READ combineSinks WRITE setCombineSinks NOTIFY combineSinksChanged)
Q_PROPERTY(bool switchOnConnect READ switchOnConnect WRITE setSwitchOnConnect NOTIFY switchOnConnectChanged)
Q_PROPERTY(bool configModuleLoaded READ configModuleLoaded NOTIFY loadedModulesChanged)
Q_PROPERTY(QString configModuleName READ configModuleName CONSTANT)
Q_PROPERTY(QStringList loadedModules READ loadedModules NOTIFY loadedModulesChanged)
public:
explicit ModuleManager(QObject *parent = nullptr);
~ModuleManager() override;
bool settingsSupported() const;
bool combineSinks() const;
void setCombineSinks(bool combineSinks);
bool switchOnConnect() const;
void setSwitchOnConnect(bool switchOnConnect);
QStringList loadedModules() const;
bool configModuleLoaded() const;
QString configModuleName() const;
Q_SIGNALS:
void combineSinksChanged();
void switchOnConnectChanged();
void loadedModulesChanged();
void serverUpdated();
private:
void updateLoadedModules();
ConfigModule *m_combineSinks;
ConfigModule *m_switchOnConnect;
ConfigModule *m_deviceManager;
QStringList m_loadedModules;
};
} // QPulseAudio
#endif // STREAM_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
*/
#include "operation.h"
namespace QPulseAudio
{
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;
}
} // QPulseAudio

@ -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 QPulseAudio
{
/**
* @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
*/
explicit 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;
};
} // QPulseAudio
#endif // OPERATION_H

@ -0,0 +1,20 @@
/*
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"
namespace QPulseAudio
{
Port::Port(QObject *parent)
: Profile(parent)
{
}
Port::~Port()
{
}
} // QPulseAudio

@ -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 PORT_H
#define PORT_H
#include "profile.h"
#include <pulse/def.h>
namespace QPulseAudio
{
class Port : public Profile
{
Q_OBJECT
public:
explicit Port(QObject *parent);
~Port() override;
template<typename PAInfo>
bool setInfo(const PAInfo *info)
{
Availability newAvailability;
switch (info->available) {
case PA_PORT_AVAILABLE_NO:
newAvailability = Unavailable;
break;
case PA_PORT_AVAILABLE_YES:
newAvailability = Available;
break;
default:
newAvailability = Unknown;
}
return setCommonInfo(info, newAvailability);
}
};
} // QPulseAudio
#endif // PORT_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
*/
#include "profile.h"
namespace QPulseAudio
{
Profile::Profile(QObject *parent)
: QObject(parent)
, m_name()
, m_description()
, m_priority(0)
, m_availability(Unknown)
{
}
Profile::~Profile()
{
}
QString Profile::name() const
{
return m_name;
}
QString Profile::description() const
{
return m_description;
}
quint32 Profile::priority() const
{
return m_priority;
}
Profile::Availability Profile::availability() const
{
return m_availability;
}
} // QPulseAudio

@ -0,0 +1,96 @@
/*
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 <QObject>
#include <QString>
namespace QPulseAudio
{
class Profile : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
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)
explicit Profile(QObject *parent);
~Profile() override;
template<typename PAInfo>
bool setInfo(const PAInfo *info)
{
return setCommonInfo(info, info->available ? Available : Unavailable);
}
QString name() const;
QString description() const;
quint32 priority() const;
Availability availability() const;
Q_SIGNALS:
void nameChanged();
void descriptionChanged();
void priorityChanged();
void availabilityChanged();
protected:
template<typename PAInfo>
bool setCommonInfo(const PAInfo *info, Availability newAvailability)
{
bool changed = false;
// Description is optional. Name not so much as we need some ID.
Q_ASSERT(info->name);
QString infoName = QString::fromUtf8(info->name);
if (m_name != infoName) {
m_name = infoName;
Q_EMIT nameChanged();
changed = true;
}
if (info->description) {
QString infoDescription = QString::fromUtf8(info->description);
if (m_description != infoDescription) {
m_description = infoDescription;
Q_EMIT descriptionChanged();
changed = true;
}
}
if (m_priority != info->priority) {
m_priority = info->priority;
Q_EMIT priorityChanged();
changed = true;
}
if (m_availability != newAvailability) {
m_availability = newAvailability;
Q_EMIT availabilityChanged();
changed = true;
}
return changed;
}
private:
QString m_name;
QString m_description;
quint32 m_priority;
Availability m_availability;
};
} // QPulseAudio
#endif // PROFILE_H

@ -0,0 +1,372 @@
/*
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 "pulseaudio.h"
#include "card.h"
#include "debug.h"
#include "module.h"
#include "server.h"
#include "sink.h"
#include "sinkinput.h"
#include "source.h"
#include "sourceoutput.h"
#include "streamrestore.h"
#include <QMetaEnum>
namespace QPulseAudio
{
AbstractModel::AbstractModel(const MapBaseQObject *map, QObject *parent)
: QAbstractListModel(parent)
, m_map(map)
{
Context::instance()->ref();
connect(m_map, &MapBaseQObject::aboutToBeAdded, this, [this](int index) {
beginInsertRows(QModelIndex(), index, index);
});
connect(m_map, &MapBaseQObject::added, this, [this](int index) {
onDataAdded(index);
endInsertRows();
Q_EMIT countChanged();
});
connect(m_map, &MapBaseQObject::aboutToBeRemoved, this, [this](int index) {
beginRemoveRows(QModelIndex(), index, index);
});
connect(m_map, &MapBaseQObject::removed, this, [this](int index) {
Q_UNUSED(index);
endRemoveRows();
Q_EMIT countChanged();
});
}
AbstractModel::~AbstractModel()
{
// deref context after we've deleted this object
// see https://bugs.kde.org/show_bug.cgi?id=371215
Context::instance()->unref();
}
QHash<int, QByteArray> AbstractModel::roleNames() const
{
if (!m_roles.empty()) {
qCDebug(PLASMAPA) << "returning roles" << m_roles;
return m_roles;
}
Q_UNREACHABLE();
return QHash<int, QByteArray>();
}
int AbstractModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_map->count();
}
QVariant AbstractModel::data(const QModelIndex &index, int role) const
{
if (!hasIndex(index.row(), index.column())) {
return QVariant();
}
QObject *data = 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)->properties().value(QStringLiteral("name")).toString();
}
int property = 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 = m_objectProperties.value(role, -1);
if (propertyIndex == -1) {
return false;
}
QObject *data = m_map->objectAt(index.row());
auto property = data->metaObject()->property(propertyIndex);
return property.write(data, value);
}
int AbstractModel::role(const QByteArray &roleName) const
{
qCDebug(PLASMAPA) << roleName << m_roles.key(roleName, -1);
return m_roles.key(roleName, -1);
}
Context *AbstractModel::context() const
{
return Context::instance();
}
void AbstractModel::initRoleNames(const QMetaObject &qobjectMetaObject)
{
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);
m_roles[enumerator.value(i)] = key;
}
int maxEnumValue = -1;
for (auto it = m_roles.constBegin(); it != 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());
m_roles[++maxEnumValue] = name.toLatin1();
m_objectProperties.insert(maxEnumValue, i);
if (!property.hasNotifySignal()) {
continue;
}
m_signalIndexToProperties.insert(property.notifySignalIndex(), i);
}
qCDebug(PLASMAPA) << m_roles;
// Connect to property changes also with objects already in model
for (int i = 0; i < m_map->count(); ++i) {
onDataAdded(i);
}
}
void AbstractModel::propertyChanged()
{
if (!sender() || senderSignalIndex() == -1) {
return;
}
int propertyIndex = m_signalIndexToProperties.value(senderSignalIndex(), -1);
if (propertyIndex == -1) {
return;
}
int role = m_objectProperties.key(propertyIndex, -1);
if (role == -1) {
return;
}
int index = m_map->indexOfObject(sender());
qCDebug(PLASMAPA) << "PROPERTY CHANGED (" << index << ") :: " << role << roleNames().value(role);
Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0), {role});
}
void AbstractModel::onDataAdded(int index)
{
QObject *data = m_map->objectAt(index);
const QMetaObject *mo = data->metaObject();
// We have all the data changed notify signals already stored
auto keys = m_signalIndexToProperties.keys();
Q_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()->sinks(), parent)
, m_preferredSink(nullptr)
{
initRoleNames(Sink::staticMetaObject);
for (int i = 0; i < context()->sinks().count(); ++i) {
sinkAdded(i);
}
connect(&context()->sinks(), &MapBaseQObject::added, this, &SinkModel::sinkAdded);
connect(&context()->sinks(), &MapBaseQObject::removed, this, &SinkModel::sinkRemoved);
connect(context()->server(), &Server::defaultSinkChanged, this, [this]() {
updatePreferredSink();
Q_EMIT defaultSinkChanged();
});
}
Sink *SinkModel::defaultSink() const
{
return context()->server()->defaultSink();
}
Sink *SinkModel::preferredSink() const
{
return 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()->sinks().objectAt(index)));
Sink *sink = static_cast<Sink *>(context()->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 != m_preferredSink) {
qCDebug(PLASMAPA) << "Changing preferred sink to" << sink << (sink ? sink->name() : "");
m_preferredSink = sink;
Q_EMIT preferredSinkChanged();
}
}
Sink *SinkModel::findPreferredSink() const
{
const auto &sinks = context()->sinks();
// Only one sink is the preferred one
if (sinks.count() == 1) {
return static_cast<Sink *>(sinks.objectAt(0));
}
auto lookForState = [this](Device::State state) {
Sink *ret = nullptr;
QMapIterator<quint32, Sink *> it(context()->sinks().data());
while (it.hasNext()) {
it.next();
if ((it.value()->isVirtualDevice() && !it.value()->isDefault()) || it.value()->state() != state) {
continue;
}
if (!ret) {
ret = it.value();
} else if (it.value() == defaultSink()) {
ret = it.value();
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()->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()->sinkInputs(), parent)
{
initRoleNames(SinkInput::staticMetaObject);
}
SourceOutputModel::SourceOutputModel(QObject *parent)
: AbstractModel(&context()->sourceOutputs(), parent)
{
initRoleNames(SourceOutput::staticMetaObject);
}
CardModel::CardModel(QObject *parent)
: AbstractModel(&context()->cards(), parent)
{
initRoleNames(Card::staticMetaObject);
}
StreamRestoreModel::StreamRestoreModel(QObject *parent)
: AbstractModel(&context()->streamRestores(), parent)
{
initRoleNames(StreamRestore::staticMetaObject);
}
ModuleModel::ModuleModel(QObject *parent)
: AbstractModel(&context()->modules(), parent)
{
initRoleNames(Module::staticMetaObject);
}
} // QPulseAudio

@ -0,0 +1,151 @@
/*
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 "maps.h"
namespace QPulseAudio
{
class Context;
class AbstractModel : public QAbstractListModel
{
Q_OBJECT
public:
enum ItemRole {
PulseObjectRole = Qt::UserRole + 1,
};
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_ENUM(ItemRole)
~AbstractModel() override;
QHash<int, QByteArray> roleNames() const final;
int rowCount(const QModelIndex &parent = QModelIndex()) const final;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) final;
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;
const MapBaseQObject *m_map;
QHash<int, QByteArray> m_roles;
QHash<int, int> m_objectProperties;
QHash<int, int> m_signalIndexToProperties;
private:
// Prevent leaf-classes from default constructing as we want to enforce
// them passing us a context or explicit nullptrs.
AbstractModel()
{
}
};
class CardModel : public AbstractModel
{
Q_OBJECT
public:
explicit CardModel(QObject *parent = nullptr);
};
class SinkModel : public AbstractModel
{
Q_OBJECT
Q_PROPERTY(QPulseAudio::Sink *defaultSink READ defaultSink NOTIFY defaultSinkChanged)
Q_PROPERTY(QPulseAudio::Sink *preferredSink READ preferredSink NOTIFY preferredSinkChanged)
public:
enum ItemRole {
SortByDefaultRole = PulseObjectRole + 1,
};
Q_ENUM(ItemRole)
explicit SinkModel(QObject *parent = nullptr);
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;
Sink *m_preferredSink;
};
class SinkInputModel : public AbstractModel
{
Q_OBJECT
public:
explicit SinkInputModel(QObject *parent = nullptr);
};
class SourceModel : public AbstractModel
{
Q_OBJECT
Q_PROPERTY(QPulseAudio::Source *defaultSource READ defaultSource NOTIFY defaultSourceChanged)
public:
enum ItemRole {
SortByDefaultRole = PulseObjectRole + 1,
};
Q_ENUM(ItemRole)
explicit SourceModel(QObject *parent = nullptr);
Source *defaultSource() const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Q_SIGNALS:
void defaultSourceChanged();
};
class SourceOutputModel : public AbstractModel
{
Q_OBJECT
public:
explicit SourceOutputModel(QObject *parent = nullptr);
};
class StreamRestoreModel : public AbstractModel
{
Q_OBJECT
public:
explicit StreamRestoreModel(QObject *parent = nullptr);
};
class ModuleModel : public AbstractModel
{
Q_OBJECT
public:
explicit ModuleModel(QObject *parent = nullptr);
};
} // QPulseAudio
#endif // PULSEAUDIO_H

@ -0,0 +1,80 @@
/*
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 "context.h"
#include <QIcon>
namespace QPulseAudio
{
PulseObject::PulseObject(QObject *parent)
: QObject(parent)
, m_index(0)
{
}
PulseObject::~PulseObject()
{
}
Context *PulseObject::context() const
{
return Context::instance();
}
uint32_t PulseObject::index() const
{
return m_index;
}
QString PulseObject::iconName() const
{
QString name = m_properties.value(QStringLiteral("device.icon_name")).toString();
if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
return name;
}
name = m_properties.value(QStringLiteral("media.icon_name")).toString();
if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
return name;
}
name = m_properties.value(QStringLiteral("window.icon_name")).toString();
if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
return name;
}
name = m_properties.value(QStringLiteral("application.icon_name")).toString();
if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
return name;
}
name = m_properties.value(QStringLiteral("application.process.binary")).toString();
if (!name.isEmpty() && QIcon::hasThemeIcon(name)) {
return name;
}
name = 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 m_properties;
}
} // QPulseAudio

@ -0,0 +1,72 @@
/*
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 "debug.h"
#include <QObject>
#include <pulse/introspect.h>
namespace QPulseAudio
{
class Context;
class PulseObject : public QObject
{
Q_OBJECT
Q_PROPERTY(quint32 index READ index CONSTANT)
Q_PROPERTY(QString iconName READ iconName CONSTANT)
Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged)
public:
template<typename PAInfo>
void updatePulseObject(PAInfo *info)
{
m_index = info->index;
QVariantMap properties;
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) {
qCDebug(PLASMAPA) << "property" << key << "not a string";
continue;
}
Q_ASSERT(value);
properties.insert(QString::fromUtf8(key), QString::fromUtf8(value));
}
if (m_properties != properties) {
m_properties = properties;
Q_EMIT propertiesChanged();
}
}
quint32 index() const;
QString iconName() const;
QVariantMap properties() const;
Q_SIGNALS:
void propertiesChanged();
protected:
explicit PulseObject(QObject *parent);
~PulseObject() override;
Context *context() const;
quint32 m_index;
QVariantMap m_properties;
private:
// Ensure that we get properly parented.
PulseObject();
};
} // QPulseAudio
#endif // PULSEOBJECT_H

@ -0,0 +1,39 @@
import Cutefish.Audio 1.0
SortFilterModel {
property var filters: []
property bool filterOutInactiveDevices: false
function role(name) {
return sourceModel.role(name);
}
// filterCallback: function(source_row, value) {
// var idx = sourceModel.index(source_row, 0);
// // Don't ever show the dummy output, that's silly
// var dummyOutputName = "auto_null"
// if (sourceModel.data(idx, sourceModel.role("Name")) === dummyOutputName) {
// return false;
// }
// // Optionally run the role-based filters
// if (filters.length > 0) {
// for (var i = 0; i < filters.length; ++i) {
// var filter = filters[i];
// if (sourceModel.data(idx, sourceModel.role(filter.role)) != filter.value) {
// return false;
// }
// }
// }
// // Optionally exclude inactive devices
// if (filterOutInactiveDevices) {
// var ports = sourceModel.data(idx, sourceModel.role("PulseObject")).ports;
// if (ports.length === 1 && ports[0].availability == Port.Unavailable) {
// return false;
// }
// }
// return true;
// }
}

@ -0,0 +1,509 @@
/*
SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "listitemmenu.h"
#include <QAbstractItemModel>
#include <QMenu>
#include <QQuickItem>
#include <QQuickWindow>
#include <QWindow>
#include "card.h"
#include "debug.h"
#include "device.h"
#include "port.h"
#include "pulseaudio.h"
#include "pulseobject.h"
#include "stream.h"
using namespace QPulseAudio;
static const auto s_offProfile = QLatin1String("off");
ListItemMenu::ListItemMenu(QObject *parent)
: QObject(parent)
{
}
ListItemMenu::~ListItemMenu() = default;
void ListItemMenu::classBegin()
{
}
void ListItemMenu::componentComplete()
{
m_complete = true;
update();
}
ListItemMenu::ItemType ListItemMenu::itemType() const
{
return m_itemType;
}
void ListItemMenu::setItemType(ItemType itemType)
{
if (m_itemType != itemType) {
m_itemType = itemType;
update();
Q_EMIT itemTypeChanged();
}
}
QPulseAudio::PulseObject *ListItemMenu::pulseObject() const
{
return m_pulseObject.data();
}
void ListItemMenu::setPulseObject(QPulseAudio::PulseObject *pulseObject)
{
if (m_pulseObject.data() != pulseObject) {
// TODO is Qt clever enough to catch the disconnect from base class?
if (m_pulseObject) {
disconnect(m_pulseObject, nullptr, this, nullptr);
}
m_pulseObject = pulseObject;
if (auto *device = qobject_cast<QPulseAudio::Device *>(m_pulseObject.data())) {
connect(device, &Device::activePortIndexChanged, this, &ListItemMenu::update);
connect(device, &Device::portsChanged, this, &ListItemMenu::update);
}
update();
Q_EMIT pulseObjectChanged();
}
}
QAbstractItemModel *ListItemMenu::sourceModel() const
{
return m_sourceModel.data();
}
void ListItemMenu::setSourceModel(QAbstractItemModel *sourceModel)
{
if (m_sourceModel.data() == sourceModel) {
return;
}
if (m_sourceModel) {
disconnect(m_sourceModel, nullptr, this, nullptr);
}
m_sourceModel = sourceModel;
if (m_sourceModel) {
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ListItemMenu::update);
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &ListItemMenu::update);
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &ListItemMenu::update);
}
update();
Q_EMIT sourceModelChanged();
}
QPulseAudio::CardModel *ListItemMenu::cardModel() const
{
return m_cardModel.data();
}
void ListItemMenu::setCardModel(QPulseAudio::CardModel *cardModel)
{
if (m_cardModel.data() == cardModel) {
return;
}
if (m_cardModel) {
disconnect(m_cardModel, nullptr, this, nullptr);
}
m_cardModel = cardModel;
if (m_cardModel) {
const int profilesRole = m_cardModel->role("Profiles");
Q_ASSERT(profilesRole > -1);
connect(m_cardModel, &CardModel::dataChanged, this, [this, profilesRole](const QModelIndex &, const QModelIndex &, const QVector<int> &roles) {
if (roles.isEmpty() || roles.contains(profilesRole)) {
update();
}
});
}
update();
Q_EMIT cardModelChanged();
}
bool ListItemMenu::isVisible() const
{
return m_visible;
}
void ListItemMenu::setVisible(bool visible)
{
if (m_visible != visible) {
m_visible = visible;
Q_EMIT visibleChanged();
}
}
bool ListItemMenu::hasContent() const
{
return m_hasContent;
}
QQuickItem *ListItemMenu::visualParent() const
{
return m_visualParent.data();
}
void ListItemMenu::setVisualParent(QQuickItem *visualParent)
{
if (m_visualParent.data() != visualParent) {
m_visualParent = visualParent;
Q_EMIT visualParentChanged();
}
}
bool ListItemMenu::checkHasContent()
{
// If there are at least two sink/source devices to choose from.
if (m_sourceModel && m_sourceModel->rowCount() > 1) {
return true;
}
auto *device = qobject_cast<QPulseAudio::Device *>(m_pulseObject.data());
if (device) {
const auto ports = device->ports();
if (ports.length() > 1) {
// In case an unavailable port is active.
if (device->activePortIndex() != static_cast<quint32>(-1)) {
auto *activePort = static_cast<Port *>(ports.at(device->activePortIndex()));
if (activePort->availability() == Port::Unavailable) {
return true;
}
}
// If there are at least two available ports.
int availablePorts = 0;
for (auto *portObject : ports) {
auto *port = static_cast<Port *>(portObject);
if (port->availability() == Port::Unavailable) {
continue;
}
if (++availablePorts == 2) {
return true;
}
}
}
if (m_cardModel) {
const int cardModelPulseObjectRole = m_cardModel->role("PulseObject");
Q_ASSERT(cardModelPulseObjectRole != -1);
for (int i = 0; i < m_cardModel->rowCount(); ++i) {
const QModelIndex cardIdx = m_cardModel->index(i, 0);
Card *card = qobject_cast<Card *>(cardIdx.data(cardModelPulseObjectRole).value<QObject *>());
if (card->index() == device->cardIndex()) {
// If there are at least two available profiles on the corresponding card.
const auto profiles = card->profiles();
int availableProfiles = 0;
for (auto *profileObject : profiles) {
auto *profile = static_cast<Profile *>(profileObject);
if (profile->availability() == Profile::Unavailable) {
continue;
}
if (profile->name() == s_offProfile) {
continue;
}
// TODO should we also check "if current profile is unavailable" like with ports?
if (++availableProfiles == 2) {
return true;
}
}
}
}
}
}
return false;
}
void ListItemMenu::update()
{
if (!m_complete) {
return;
}
const bool hasContent = checkHasContent();
if (m_hasContent != hasContent) {
m_hasContent = hasContent;
Q_EMIT hasContentChanged();
}
}
void ListItemMenu::open(int x, int y)
{
auto *menu = createMenu();
if (!menu) {
return;
}
const QPoint pos = m_visualParent->mapToGlobal(QPointF(x, y)).toPoint();
menu->popup(pos);
setVisible(true);
}
// to the bottom left of visualParent
void ListItemMenu::openRelative()
{
auto *menu = createMenu();
if (!menu) {
return;
}
menu->adjustSize();
QPoint pos = m_visualParent->mapToGlobal(QPointF(m_visualParent->width(), m_visualParent->height())).toPoint();
pos.rx() -= menu->width();
// TODO do we still need this ungrab mouse hack?
menu->popup(pos);
setVisible(true);
}
static int getModelRole(QObject *model, const QByteArray &name)
{
// Can either be an AbstractModel, then it's easy
if (auto *abstractModel = qobject_cast<AbstractModel *>(model)) {
return abstractModel->role(name);
}
// or that PulseObjectFilterModel from QML where everything is a QVariant...
QVariant roleVariant;
bool ok = QMetaObject::invokeMethod(model, "role", Q_RETURN_ARG(QVariant, roleVariant), Q_ARG(QVariant, QVariant(name)));
if (!ok) {
qCCritical(PLASMAPA) << "Failed to invoke 'role' on" << model;
return -1;
}
int role = roleVariant.toInt(&ok);
if (!ok) {
qCCritical(PLASMAPA) << "Return value from 'role' is bogus" << roleVariant;
return -1;
}
return role;
}
QMenu *ListItemMenu::createMenu()
{
if (m_visible) {
return nullptr;
}
if (!m_visualParent || !m_visualParent->window()) {
qCWarning(PLASMAPA) << "Cannot prepare menu without visualParent or a window";
return nullptr;
}
QMenu *menu = new QMenu();
menu->setAttribute(Qt::WA_DeleteOnClose);
connect(menu, &QMenu::aboutToHide, this, [this] {
setVisible(false);
});
if (auto *device = qobject_cast<QPulseAudio::Device *>(m_pulseObject.data())) {
// Switch all streams of the relevant kind to this device
if (m_sourceModel->rowCount() > 1) {
QAction *switchStreamsAction = nullptr;
if (m_itemType == Sink) {
switchStreamsAction = menu->addAction(
QIcon::fromTheme(QStringLiteral("audio-on"),
QIcon::fromTheme(QStringLiteral("audio-ready"), QIcon::fromTheme(QStringLiteral("audio-speakers-symbolic")))),
tr("Play all audio via this device"));
} else if (m_itemType == Source) {
switchStreamsAction = menu->addAction(
QIcon::fromTheme(QStringLiteral("mic-on"),
QIcon::fromTheme(QStringLiteral("mic-ready"), QIcon::fromTheme(QStringLiteral("audio-input-microphone-symbolic")))),
tr("Record all audio via this device"));
}
if (switchStreamsAction) {
connect(switchStreamsAction, &QAction::triggered, device, &Device::switchStreams);
}
}
// Ports
const auto ports = device->ports();
bool activePortUnavailable = false;
if (device->activePortIndex() != static_cast<quint32>(-1)) {
auto *activePort = static_cast<Port *>(ports.at(device->activePortIndex()));
activePortUnavailable = activePort->availability() == Port::Unavailable;
}
QMap<int, Port *> availablePorts;
for (int i = 0; i < ports.count(); ++i) {
auto *port = static_cast<Port *>(ports.at(i));
// If an unavailable port is active, show all the ports,
// otherwise show only the available ones
if (activePortUnavailable || port->availability() != Port::Unavailable) {
availablePorts.insert(i, port);
}
}
if (availablePorts.count() > 1) {
menu->addSection(tr("Heading for a list of ports of a device (for example built-in laptop speakers or a plug for headphones)", "Ports"));
auto *portGroup = new QActionGroup(menu);
for (auto it = availablePorts.constBegin(), end = availablePorts.constEnd(); it != end; ++it) {
const int i = it.key();
Port *port = it.value();
QAction *item = nullptr;
if (port->availability() == Port::Unavailable) {
if (port->name() == QLatin1String("analog-output-speaker") || port->name() == QLatin1String("analog-input-microphone-internal")) {
item = menu->addAction(tr("%1 (unavailable)").arg(port->description()));
} else {
item = menu->addAction(tr("%1 (unplugged)").arg(port->description()));
}
} else {
item = menu->addAction(port->description());
}
item->setCheckable(true);
item->setChecked(static_cast<quint32>(i) == device->activePortIndex());
connect(item, &QAction::triggered, device, [device, i] {
device->setActivePortIndex(i);
});
portGroup->addAction(item);
}
}
// Submenu with profiles
if (m_cardModel) {
const int cardModelPulseObjectRole = m_cardModel->role("PulseObject");
Q_ASSERT(cardModelPulseObjectRole != -1);
Card *card = nullptr;
for (int i = 0; i < m_cardModel->rowCount(); ++i) {
const QModelIndex cardIdx = m_cardModel->index(i, 0);
Card *candidateCard = qobject_cast<Card *>(cardIdx.data(cardModelPulseObjectRole).value<QObject *>());
if (candidateCard && candidateCard->index() == device->cardIndex()) {
card = candidateCard;
break;
}
}
if (card) {
QMap<int, Profile *> availableProfiles;
const auto profiles = card->profiles();
for (int i = 0; i < profiles.count(); ++i) {
auto *profile = static_cast<Profile *>(profiles.at(i));
// TODO should we also check "if current profile is unavailable" like with ports?
if (profile->availability() == Profile::Unavailable) {
continue;
}
// Don't let user easily remove a device with no obvious way to get it back
// Only let that be done from the KCM where one can just flip the ComboBox back.
if (profile->name() == s_offProfile) {
continue;
}
availableProfiles.insert(i, profile);
}
if (availableProfiles.count() > 1) {
// If there's too many profiles, put them in a submenu, unless the menu is empty, otherwise as a section
QMenu *profilesMenu = menu;
const QString title = tr("Heading for a list of device profiles (5.1 surround sound, stereo, speakers only, ...)", "Profiles");
// "10" is catered around laptop speakers (internal, stereo, duplex) plus one HDMI port (stereo, surround 5.1, 7.1, in and out, etc)
if (availableProfiles.count() > 10 && !menu->actions().isEmpty()) {
profilesMenu = menu->addMenu(title);
} else {
menu->addSection(title);
}
QActionGroup *profileGroup = new QActionGroup(profilesMenu);
for (auto it = availableProfiles.constBegin(), end = availableProfiles.constEnd(); it != end; ++it) {
const int i = it.key();
Profile *profile = it.value();
auto *profileAction = profilesMenu->addAction(profile->description());
profileAction->setCheckable(true);
profileAction->setChecked(static_cast<quint32>(i) == card->activeProfileIndex());
connect(profileAction, &QAction::triggered, card, [card, i] {
card->setActiveProfileIndex(i);
});
profileGroup->addAction(profileAction);
}
}
} else {
qCWarning(PLASMAPA) << "Failed to find card at" << device->cardIndex() << "for" << device->description() << device->index();
}
}
}
// Choose output / input device
auto *stream = qobject_cast<QPulseAudio::Stream *>(m_pulseObject.data());
if (stream && m_sourceModel && m_sourceModel->rowCount() > 1) {
if (m_itemType == SinkInput || m_itemType == SourceOutput) {
if (m_itemType == SinkInput) {
menu->addSection(tr("Heading for a list of possible output devices (speakers, headphones, ...) to choose", "Play audio using"));
} else {
menu->addSection(tr("Heading for a list of possible input devices (built-in microphone, headset, ...) to choose", "Record audio using"));
}
const int indexRole = getModelRole(m_sourceModel, "Index");
Q_ASSERT(indexRole > -1);
const int descriptionRole = getModelRole(m_sourceModel, "Description");
Q_ASSERT(descriptionRole > -1);
auto *deviceGroup = new QActionGroup(menu);
for (int i = 0; i < m_sourceModel->rowCount(); ++i) {
const QModelIndex idx = m_sourceModel->index(i, 0);
const auto index = idx.data(indexRole).toUInt();
auto *item = menu->addAction(idx.data(descriptionRole).toString());
item->setCheckable(true);
item->setChecked(index == stream->deviceIndex());
connect(item, &QAction::triggered, stream, [stream, index] {
stream->setDeviceIndex(index);
});
deviceGroup->addAction(item);
}
}
}
if (menu->isEmpty()) {
delete menu;
return nullptr;
}
menu->winId();
menu->windowHandle()->setTransientParent(m_visualParent->window());
return menu;
}

@ -0,0 +1,103 @@
/*
SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <QPointer>
#include <QQmlParserStatus>
class QAbstractItemModel;
class QMenu;
class QQuickItem;
namespace QPulseAudio
{
class CardModel;
class PulseObject;
}
class ListItemMenu : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(ItemType itemType READ itemType WRITE setItemType NOTIFY itemTypeChanged)
Q_PROPERTY(QPulseAudio::PulseObject *pulseObject READ pulseObject WRITE setPulseObject NOTIFY pulseObjectChanged)
Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
Q_PROPERTY(QPulseAudio::CardModel *cardModel READ cardModel WRITE setCardModel NOTIFY cardModelChanged)
Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged)
Q_PROPERTY(bool hasContent READ hasContent NOTIFY hasContentChanged)
Q_PROPERTY(QQuickItem *visualParent READ visualParent WRITE setVisualParent NOTIFY visualParentChanged)
public:
explicit ListItemMenu(QObject *parent = nullptr);
~ListItemMenu() override;
enum ItemType {
None,
Sink,
SinkInput,
Source,
SourceOutput,
};
Q_ENUM(ItemType)
ItemType itemType() const;
void setItemType(ItemType itemType);
Q_SIGNAL void itemTypeChanged();
QPulseAudio::PulseObject *pulseObject() const;
void setPulseObject(QPulseAudio::PulseObject *pulseObject);
Q_SIGNAL void pulseObjectChanged();
QAbstractItemModel *sourceModel() const;
void setSourceModel(QAbstractItemModel *sourceModel);
Q_SIGNAL void sourceModelChanged();
QPulseAudio::CardModel *cardModel() const;
void setCardModel(QPulseAudio::CardModel *cardModel);
Q_SIGNAL void cardModelChanged();
bool isVisible() const;
Q_SIGNAL void visibleChanged();
bool hasContent() const;
Q_SIGNAL void hasContentChanged();
QQuickItem *visualParent() const;
void setVisualParent(QQuickItem *visualParent);
Q_SIGNAL void visualParentChanged();
void classBegin() override;
void componentComplete() override;
Q_INVOKABLE void open(int x, int y);
Q_INVOKABLE void openRelative();
private:
void setVisible(bool visible);
void update();
bool checkHasContent();
QMenu *createMenu();
bool m_complete = false;
bool m_visible = false;
bool m_hasContent = false;
QPointer<QQuickItem> m_visualParent;
ItemType m_itemType = None;
QPointer<QPulseAudio::PulseObject> m_pulseObject;
QPointer<QAbstractItemModel> m_sourceModel;
QPointer<QPulseAudio::CardModel> m_cardModel;
};

@ -0,0 +1,331 @@
/*
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-FileCopyrightText: 2020 MBition GmbH
Author: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "microphoneindicator.h"
#include <QAction>
#include <QIcon>
#include <QMenu>
#include <QTimer>
#include <KStatusNotifierItem>
#include "client.h"
#include "context.h"
#include "pulseaudio.h"
#include "source.h"
#include "volumeosd.h"
using namespace QPulseAudio;
MicrophoneIndicator::MicrophoneIndicator(QObject *parent)
: QObject(parent)
, m_sourceModel(new SourceModel(this))
, m_sourceOutputModel(new SourceOutputModel(this))
, m_updateTimer(new QTimer(this))
{
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate);
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate);
connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate);
connect(m_sourceOutputModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate);
connect(m_sourceOutputModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate);
connect(m_sourceOutputModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate);
m_updateTimer->setInterval(0);
m_updateTimer->setSingleShot(true);
connect(m_updateTimer, &QTimer::timeout, this, &MicrophoneIndicator::update);
scheduleUpdate();
}
MicrophoneIndicator::~MicrophoneIndicator() = default;
void MicrophoneIndicator::init()
{
// does nothing, just prompts QML engine to create an instance of the singleton
}
void MicrophoneIndicator::scheduleUpdate()
{
if (!m_updateTimer->isActive()) {
m_updateTimer->start();
}
}
void MicrophoneIndicator::update()
{
const auto apps = recordingApplications();
if (apps.isEmpty()) {
m_showOsdOnUpdate = false;
delete m_sni;
m_sni = nullptr;
return;
}
if (!m_sni) {
m_sni = new KStatusNotifierItem(QStringLiteral("microphone"));
m_sni->setCategory(KStatusNotifierItem::Hardware);
// always Active since it is completely removed when microphone isn't in use
m_sni->setStatus(KStatusNotifierItem::Active);
// but also middle click to be consistent with volume icon
connect(m_sni, &KStatusNotifierItem::secondaryActivateRequested, this, &MicrophoneIndicator::toggleMuted);
connect(m_sni, &KStatusNotifierItem::activateRequested, this, &MicrophoneIndicator::toggleMuted);
connect(m_sni, &KStatusNotifierItem::scrollRequested, this, [this](int delta, Qt::Orientation orientation) {
if (orientation != Qt::Vertical) {
return;
}
m_wheelDelta += delta;
while (m_wheelDelta >= 120) {
m_wheelDelta -= 120;
adjustVolume(+1);
}
while (m_wheelDelta <= -120) {
m_wheelDelta += 120;
adjustVolume(-1);
}
});
QMenu *menu = m_sni->contextMenu();
m_muteAction = menu->addAction(QIcon::fromTheme(QStringLiteral("microphone-sensitivity-muted")), i18n("Mute"));
m_muteAction->setCheckable(true);
connect(m_muteAction.data(), &QAction::triggered, this, &MicrophoneIndicator::setMuted);
// don't let it quit plasmashell
m_sni->setStandardActionsEnabled(false);
}
const bool allMuted = muted();
QString iconName;
if (allMuted) {
iconName = QStringLiteral("microphone-sensitivity-muted");
} else {
if (Source *defaultSource = m_sourceModel->defaultSource()) {
const int percent = volumePercent(defaultSource);
iconName = QStringLiteral("microphone-sensitivity");
// it deliberately never shows the "muted" icon unless *all* microphones are muted
if (percent <= 25) {
iconName.append(QStringLiteral("-low"));
} else if (percent <= 75) {
iconName.append(QStringLiteral("-medium"));
} else {
iconName.append(QStringLiteral("-high"));
}
} else {
iconName = QStringLiteral("microphone-sensitivity-high");
}
}
m_sni->setTitle(i18n("Microphone"));
m_sni->setIconByName(iconName);
m_sni->setToolTip(QIcon::fromTheme(iconName), allMuted ? i18n("Microphone Muted") : i18n("Microphone"), toolTipForApps(apps));
if (m_muteAction) {
m_muteAction->setChecked(allMuted);
}
if (m_showOsdOnUpdate) {
showOsd();
m_showOsdOnUpdate = false;
}
}
bool MicrophoneIndicator::muted() const
{
static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted"));
Q_ASSERT(s_mutedRole > -1);
for (int row = 0; row < m_sourceModel->rowCount(); ++row) {
const QModelIndex idx = m_sourceModel->index(row);
if (!idx.data(s_mutedRole).toBool()) {
// this is deliberately checking if *all* microphones are muted rather than the preferred one
return false;
}
}
return true;
}
void MicrophoneIndicator::setMuted(bool muted)
{
static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted"));
Q_ASSERT(s_mutedRole > -1);
m_showOsdOnUpdate = true;
if (muted) {
for (int row = 0; row < m_sourceModel->rowCount(); ++row) {
const QModelIndex idx = m_sourceModel->index(row);
if (!idx.data(s_mutedRole).toBool()) {
m_sourceModel->setData(idx, true, s_mutedRole);
m_mutedIndices.append(QPersistentModelIndex(idx));
continue;
}
}
return;
}
// If we didn't mute it, unmute all
if (m_mutedIndices.isEmpty()) {
for (int i = 0; i < m_sourceModel->rowCount(); ++i) {
m_sourceModel->setData(m_sourceModel->index(i), false, s_mutedRole);
}
return;
}
// Otherwise unmute the devices we muted
for (auto &idx : qAsConst(m_mutedIndices)) {
if (!idx.isValid()) {
continue;
}
m_sourceModel->setData(idx, false, s_mutedRole);
}
m_mutedIndices.clear();
// no update() needed as the model signals a change
}
void MicrophoneIndicator::toggleMuted()
{
setMuted(!muted());
}
void MicrophoneIndicator::adjustVolume(int direction)
{
Source *source = m_sourceModel->defaultSource();
if (!source) {
return;
}
const int step = qRound(5 * Context::NormalVolume / 100.0);
const auto newVolume = qBound(Context::MinimalVolume, //
source->volume() + direction * step, //
Context::NormalVolume);
source->setVolume(newVolume);
source->setMuted(newVolume == Context::MinimalVolume);
m_showOsdOnUpdate = true;
}
int MicrophoneIndicator::volumePercent(Source *source)
{
return source->isMuted() ? 0 : qRound(source->volume() / static_cast<qreal>(Context::NormalVolume) * 100);
}
void MicrophoneIndicator::showOsd()
{
if (!m_osd) {
m_osd = new VolumeOSD(this);
}
auto *preferredSource = m_sourceModel->defaultSource();
if (!preferredSource) {
return;
}
m_osd->showMicrophone(volumePercent(preferredSource));
}
QVector<QModelIndex> MicrophoneIndicator::recordingApplications() const
{
QVector<QModelIndex> indices;
// If there are no microphones present, there's nothing to record
if (m_sourceModel->rowCount() == 0) {
return indices;
}
static const int s_virtualStreamRole = m_sourceOutputModel->role(QByteArrayLiteral("VirtualStream"));
Q_ASSERT(s_virtualStreamRole > -1);
indices.reserve(m_sourceOutputModel->rowCount());
for (int i = 0; i < m_sourceOutputModel->rowCount(); ++i) {
const QModelIndex idx = m_sourceOutputModel->index(i);
if (idx.data(s_virtualStreamRole).toBool()) {
continue;
}
indices.append(idx);
}
return indices;
}
QString MicrophoneIndicator::toolTipForApps(const QVector<QModelIndex> &apps) const
{
Q_ASSERT(!apps.isEmpty());
if (apps.count() > 1) {
QStringList names;
names.reserve(apps.count());
for (const QModelIndex &idx : apps) {
names.append(sourceOutputDisplayName(idx));
}
names.removeDuplicates();
// Still more than one app?
if (names.count() > 1) {
return i18nc("List of apps is using mic", "%1 are using the microphone", names.join(i18nc("list separator", ", ")));
}
}
const QModelIndex appIdx = apps.constFirst();
// If there is more than one microphone, show which one is being used.
// An app could record multiple microphones simultaneously, or the user having the same app running
// multiple times recording the same microphone, but this isn't covered here for simplicity.
if (apps.count() == 1 && m_sourceModel->rowCount() > 1) {
static const int s_sourceModelDescriptionRole = m_sourceModel->role(QByteArrayLiteral("Description"));
Q_ASSERT(s_sourceModelDescriptionRole > -1);
static const int s_sourceModelIndexRole = m_sourceModel->role("Index");
Q_ASSERT(s_sourceModelIndexRole > -1);
static const int s_sourceOutputModelDeviceIndexRole = m_sourceOutputModel->role("DeviceIndex");
Q_ASSERT(s_sourceOutputModelDeviceIndexRole > -1);
const int sourceOutputDeviceIndex = appIdx.data(s_sourceOutputModelDeviceIndexRole).toInt();
for (int i = 0; i < m_sourceModel->rowCount(); ++i) {
const QModelIndex sourceDeviceIdx = m_sourceModel->index(i, 0);
const int sourceDeviceIndex = sourceDeviceIdx.data(s_sourceModelIndexRole).toInt();
if (sourceDeviceIndex == sourceOutputDeviceIndex) {
return i18nc("App %1 is using mic with name %2",
"%1 is using the microphone (%2)",
sourceOutputDisplayName(appIdx),
sourceDeviceIdx.data(s_sourceModelDescriptionRole).toString());
}
}
}
return i18nc("App is using mic", "%1 is using the microphone", sourceOutputDisplayName(appIdx));
}
QString MicrophoneIndicator::sourceOutputDisplayName(const QModelIndex &idx) const
{
Q_ASSERT(idx.model() == m_sourceOutputModel);
static const int s_nameRole = m_sourceOutputModel->role(QByteArrayLiteral("Name"));
Q_ASSERT(s_nameRole > -1);
static const int s_clientRole = m_sourceOutputModel->role(QByteArrayLiteral("Client"));
Q_ASSERT(s_clientRole > -1);
auto *client = qobject_cast<Client *>(idx.data(s_clientRole).value<QObject *>());
return client ? client->name() : idx.data(s_nameRole).toString();
}

@ -0,0 +1,75 @@
/*
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-FileCopyrightText: 2020 MBition GmbH
Author: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <QPersistentModelIndex>
#include <QPointer>
#include <QVector>
class QAction;
class QTimer;
class KStatusNotifierItem;
class VolumeOSD;
namespace QPulseAudio
{
class Source;
class SourceModel;
class SourceOutputModel;
}
class MicrophoneIndicator : public QObject
{
Q_OBJECT
public:
explicit MicrophoneIndicator(QObject *parent = nullptr);
~MicrophoneIndicator() override;
Q_INVOKABLE void init();
Q_SIGNALS:
void enabledChanged();
private:
void scheduleUpdate();
void update();
bool muted() const;
void setMuted(bool muted);
void toggleMuted();
void adjustVolume(int direction);
static int volumePercent(QPulseAudio::Source *source);
void showOsd();
QVector<QModelIndex> recordingApplications() const;
QString toolTipForApps(const QVector<QModelIndex> &apps) const;
QString sourceOutputDisplayName(const QModelIndex &idx) const;
QPulseAudio::SourceModel *m_sourceModel = nullptr; // microphone devices
QPulseAudio::SourceOutputModel *m_sourceOutputModel = nullptr; // recording streams
KStatusNotifierItem *m_sni = nullptr;
QPointer<QAction> m_muteAction;
QPointer<QAction> m_dontAgainAction;
QVector<QPersistentModelIndex> m_mutedIndices;
VolumeOSD *m_osd = nullptr;
bool m_showOsdOnUpdate = false;
int m_wheelDelta = 0;
QTimer *m_updateTimer;
};

@ -0,0 +1,71 @@
/*
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 "plugin.h"
#include <QQmlEngine>
#include "client.h"
#include "context.h"
#include "modulemanager.h"
#include "port.h"
#include "profile.h"
#include "pulseaudio.h"
#include "sink.h"
#include "source.h"
#include "volumemonitor.h"
#include "listitemmenu.h"
// #include "microphoneindicator.h"
#include "speakertest.h"
#include "volumefeedback.h"
#include "model/sortfiltermodel.h"
// #include "volumeosd.h"
static QJSValue pulseaudio_singleton(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
QJSValue object = scriptEngine->newObject();
object.setProperty(QStringLiteral("NormalVolume"), (double)QPulseAudio::Context::NormalVolume);
object.setProperty(QStringLiteral("MinimalVolume"), (double)QPulseAudio::Context::MinimalVolume);
object.setProperty(QStringLiteral("MaximalVolume"), (double)QPulseAudio::Context::MaximalVolume);
return object;
}
void Plugin::registerTypes(const char *uri)
{
QPulseAudio::Context::setApplicationId(QStringLiteral("Cutefish.Audio"));
qmlRegisterType<SortFilterModel>(uri, 1, 0, "SortFilterModel");
qmlRegisterType<QPulseAudio::CardModel>(uri, 1, 0, "CardModel");
qmlRegisterType<QPulseAudio::SinkModel>(uri, 1, 0, "SinkModel");
qmlRegisterType<QPulseAudio::SinkInputModel>(uri, 1, 0, "SinkInputModel");
qmlRegisterType<QPulseAudio::SourceModel>(uri, 1, 0, "SourceModel");
qmlRegisterType<QPulseAudio::ModuleManager>(uri, 1, 0, "ModuleManager");
qmlRegisterType<QPulseAudio::SourceOutputModel>(uri, 1, 0, "SourceOutputModel");
qmlRegisterType<QPulseAudio::StreamRestoreModel>(uri, 1, 0, "StreamRestoreModel");
qmlRegisterType<QPulseAudio::ModuleModel>(uri, 1, 0, "ModuleModel");
qmlRegisterType<QPulseAudio::VolumeMonitor>(uri, 0, 01, "VolumeMonitor");
qmlRegisterUncreatableType<QPulseAudio::PulseObject>(uri, 1, 0, "PulseObject", QString());
qmlRegisterUncreatableType<QPulseAudio::Profile>(uri, 1, 0, "Profile", QString());
qmlRegisterUncreatableType<QPulseAudio::Port>(uri, 1, 0, "Port", QString());
qmlRegisterType<ListItemMenu>(uri, 1, 0, "ListItemMenu");
// qmlRegisterType<VolumeOSD>(uri, 1, 0, "VolumeOSD");
qmlRegisterType<VolumeFeedback>(uri, 1, 0, "VolumeFeedback");
qmlRegisterType<SpeakerTest>(uri, 1, 0, "SpeakerTest");
qmlRegisterSingletonType(uri, 1, 0, "PulseAudio", pulseaudio_singleton);
// qmlRegisterSingletonType<MicrophoneIndicator>(uri, 1, 0, "MicrophoneIndicator", [](QQmlEngine *engine, QJSEngine *jsEngine) -> QObject * {
// Q_UNUSED(engine);
// Q_UNUSED(jsEngine);
// return new MicrophoneIndicator();
// });
qmlRegisterAnonymousType<QPulseAudio::Client>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::Sink>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::Source>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::VolumeObject>(uri, 1);
}

@ -0,0 +1,20 @@
/*
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 PLUGIN_H
#define PLUGIN_H
#include <QQmlExtensionPlugin>
class Plugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
public:
void registerTypes(const char *uri) override;
};
#endif // PLUGIN_H

@ -0,0 +1,4 @@
module Cutefish.Audio
plugin cutefishaudio_qmlplugins
PulseObjectFilterModel 1.0 PulseObjectFilterModel.qml

@ -0,0 +1,67 @@
/*
SPDX-FileCopyrightText: 2008 Helio Chissini de Castro <helio@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 "volumefeedback.h"
#include "canberracontext.h"
VolumeFeedback::VolumeFeedback(QObject *parent)
: QObject(parent)
{
QPulseAudio::CanberraContext::instance()->ref();
if (ca_context_set_driver(QPulseAudio::CanberraContext::instance()->canberra(), "pulse") != CA_SUCCESS) {
return;
}
}
VolumeFeedback::~VolumeFeedback()
{
QPulseAudio::CanberraContext::instance()->unref();
}
bool VolumeFeedback::isValid() const
{
return QPulseAudio::CanberraContext::instance()->canberra();
}
void VolumeFeedback::play(quint32 sinkIndex)
{
auto context = QPulseAudio::CanberraContext::instance()->canberra();
if (!context) {
return;
}
int playing = 0;
const int cindex = 2; // Note "2" is simply the index we've picked. It's somewhat irrelevant.
ca_context_playing(context, cindex, &playing);
// NB Depending on how this is desired to work, we may want to simply
// skip playing, or cancel the currently playing sound and play our
// new one... for now, let's do the latter.
if (playing) {
ca_context_cancel(context, cindex);
}
char dev[64];
snprintf(dev, sizeof(dev), "%lu", (unsigned long)sinkIndex);
ca_context_change_device(context, dev);
// Ideally we'd use something like ca_gtk_play_for_widget()...
ca_context_play(context,
cindex,
CA_PROP_EVENT_DESCRIPTION,
"Volume Control Feedback Sound",
CA_PROP_EVENT_ID,
"audio-volume-change",
CA_PROP_CANBERRA_CACHE_CONTROL,
"permanent",
CA_PROP_CANBERRA_ENABLE,
"1",
nullptr);
ca_context_change_device(context, nullptr);
}

@ -0,0 +1,29 @@
/*
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 VOLUMEFEEDBACK_H
#define VOLUMEFEEDBACK_H
#include <QObject>
#include <canberra.h>
class VolumeFeedback : public QObject
{
Q_OBJECT
Q_PROPERTY(bool valid READ isValid CONSTANT)
public:
explicit VolumeFeedback(QObject *parent = nullptr);
~VolumeFeedback() override;
bool isValid() const;
public Q_SLOTS:
void play(quint32 sinkIndex);
};
#endif // VOLUMEFEEDBACK_H

@ -0,0 +1,36 @@
/*
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 "volumeosd.h"
#include "osdservice.h"
#define SERVICE QLatin1String("org.kde.plasmashell")
#define PATH QLatin1String("/org/kde/osdService")
#define CONNECTION QDBusConnection::sessionBus()
VolumeOSD::VolumeOSD(QObject *parent)
: QObject(parent)
{
}
void VolumeOSD::show(int percent, int maximumPercent)
{
OsdServiceInterface osdService(SERVICE, PATH, CONNECTION);
osdService.volumeChanged(percent, maximumPercent);
}
void VolumeOSD::showMicrophone(int percent)
{
OsdServiceInterface osdService(SERVICE, PATH, CONNECTION);
osdService.microphoneVolumeChanged(percent);
}
void VolumeOSD::showText(const QString &iconName, const QString &text)
{
OsdServiceInterface osdService(SERVICE, PATH, CONNECTION);
osdService.showText(iconName, text);
}

@ -0,0 +1,24 @@
/*
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 VOLUMEOSD_H
#define VOLUMEOSD_H
#include <QObject>
class VolumeOSD : public QObject
{
Q_OBJECT
public:
explicit VolumeOSD(QObject *parent = nullptr);
public Q_SLOTS:
void show(int percent, int maximumPercent = 100);
void showMicrophone(int percent);
void showText(const QString &iconName, const QString &text);
};
#endif // VOLUMEOSD_H

@ -0,0 +1,117 @@
/*
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 "context.h"
#include "debug.h"
#include "sink.h"
#include "source.h"
namespace QPulseAudio
{
Server::Server(Context *context)
: QObject(context)
, m_defaultSink(nullptr)
, m_defaultSource(nullptr)
, m_isPipeWire(false)
{
Q_ASSERT(context);
connect(&context->sinks(), &MapBaseQObject::added, this, &Server::updateDefaultDevices);
connect(&context->sinks(), &MapBaseQObject::removed, this, &Server::updateDefaultDevices);
connect(&context->sources(), &MapBaseQObject::added, this, &Server::updateDefaultDevices);
connect(&context->sources(), &MapBaseQObject::removed, this, &Server::updateDefaultDevices);
}
Sink *Server::defaultSink() const
{
return m_defaultSink;
}
void Server::setDefaultSink(Sink *sink)
{
Q_ASSERT(sink);
Context::instance()->setDefaultSink(sink->name());
}
Source *Server::defaultSource() const
{
return m_defaultSource;
}
void Server::setDefaultSource(Source *source)
{
Q_ASSERT(source);
Context::instance()->setDefaultSource(source->name());
}
void Server::reset()
{
if (m_defaultSink) {
m_defaultSink = nullptr;
Q_EMIT defaultSinkChanged(m_defaultSink);
}
if (m_defaultSource) {
m_defaultSource = nullptr;
Q_EMIT defaultSourceChanged(m_defaultSource);
}
}
void Server::update(const pa_server_info *info)
{
m_defaultSinkName = QString::fromUtf8(info->default_sink_name);
m_defaultSourceName = QString::fromUtf8(info->default_source_name);
m_isPipeWire = QString::fromUtf8(info->server_name).contains("PipeWire");
updateDefaultDevices();
Q_EMIT updated();
}
template<typename Type, typename Map>
static Type *findByName(const Map &map, const QString &name)
{
Type *out = nullptr;
if (name.isEmpty()) {
return out;
}
QMapIterator<quint32, Type *> it(map);
while (it.hasNext()) {
it.next();
out = it.value();
if (out->name() == name) {
return out;
}
}
qCWarning(PLASMAPA) << "No object for name" << name;
return out;
}
void Server::updateDefaultDevices()
{
Sink *sink = findByName<Sink>(Context::instance()->sinks().data(), m_defaultSinkName);
Source *source = findByName<Source>(Context::instance()->sources().data(), m_defaultSourceName);
if (m_defaultSink != sink) {
qCDebug(PLASMAPA) << "Default sink changed" << sink;
m_defaultSink = sink;
Q_EMIT defaultSinkChanged(m_defaultSink);
}
if (m_defaultSource != source) {
qCDebug(PLASMAPA) << "Default source changed" << source;
m_defaultSource = source;
Q_EMIT defaultSourceChanged(m_defaultSource);
}
}
bool Server::isPipeWire() const
{
return m_isPipeWire;
}
} // QPulseAudio

@ -0,0 +1,54 @@
/*
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 <QObject>
#include <pulse/introspect.h>
namespace QPulseAudio
{
class Sink;
class Source;
class Context;
class Server : public QObject
{
Q_OBJECT
public:
explicit Server(Context *context);
Sink *defaultSink() const;
void setDefaultSink(Sink *sink);
Source *defaultSource() const;
void setDefaultSource(Source *source);
void reset();
void update(const pa_server_info *info);
bool isPipeWire() const;
Q_SIGNALS:
void defaultSinkChanged(Sink *sink);
void defaultSourceChanged(Source *source);
void updated();
private:
void updateDefaultDevices();
QString m_defaultSinkName;
QString m_defaultSourceName;
Sink *m_defaultSink;
Source *m_defaultSource;
bool m_isPipeWire;
};
} // QPulseAudio
#endif // CONTEXT_H

@ -0,0 +1,89 @@
/*
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 "context.h"
#include "server.h"
#include "sinkinput.h"
namespace QPulseAudio
{
Sink::Sink(QObject *parent)
: Device(parent)
{
connect(context()->server(), &Server::defaultSinkChanged, this, &Sink::defaultChanged);
}
Sink::~Sink()
{
}
void Sink::update(const pa_sink_info *info)
{
updateDevice(info);
if (m_monitorIndex != info->monitor_source) {
m_monitorIndex = info->monitor_source;
Q_EMIT monitorIndexChanged();
}
}
void Sink::setVolume(qint64 volume)
{
context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_sink_volume_by_index);
}
void Sink::setMuted(bool muted)
{
context()->setGenericMute(m_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) {
qCWarning(PLASMAPA) << "invalid port set request" << port_index;
return;
}
context()->setGenericPort(index(), port->name(), &pa_context_set_sink_port_by_index);
}
void Sink::setChannelVolume(int channel, qint64 volume)
{
context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_sink_volume_by_index);
}
void Sink::setChannelVolumes(const QVector<qint64> &channelVolumes)
{
context()->setGenericVolumes(index(), channelVolumes, cvolume(), &pa_context_set_sink_volume_by_index);
}
bool Sink::isDefault() const
{
return context()->server()->defaultSink() == this;
}
void Sink::setDefault(bool enable)
{
if (!isDefault() && enable) {
context()->server()->setDefaultSink(this);
}
}
void Sink::switchStreams()
{
auto data = context()->sinkInputs().data();
std::for_each(data.begin(), data.end(), [this](SinkInput *paObj) {
paObj->setDeviceIndex(m_index);
});
}
quint32 Sink::monitorIndex() const
{
return m_monitorIndex;
}
} // QPulseAudio

@ -0,0 +1,45 @@
/*
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"
#include <pulse/channelmap.h>
namespace QPulseAudio
{
class Sink : public Device
{
Q_OBJECT
public:
explicit Sink(QObject *parent);
virtual ~Sink();
void update(const pa_sink_info *info);
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> &channelVolumes) override;
bool isDefault() const override;
void setDefault(bool enable) override;
void switchStreams() override;
quint32 monitorIndex() const;
Q_SIGNALS:
void monitorIndexChanged();
private:
quint32 m_monitorIndex = -1;
};
} // QPulseAudio
#endif // SINK_H

@ -0,0 +1,54 @@
/*
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 "context.h"
#include "sink.h"
namespace QPulseAudio
{
SinkInput::SinkInput(QObject *parent)
: Stream(parent)
{
}
void SinkInput::update(const pa_sink_input_info *info)
{
updateStream(info);
if (m_deviceIndex != info->sink) {
m_deviceIndex = info->sink;
Q_EMIT deviceIndexChanged();
}
}
void SinkInput::setDeviceIndex(quint32 deviceIndex)
{
context()->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_sink_input_by_index);
}
void SinkInput::setVolume(qint64 volume)
{
context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_sink_input_volume);
}
void SinkInput::setMuted(bool muted)
{
context()->setGenericMute(index(), muted, &pa_context_set_sink_input_mute);
}
void SinkInput::setChannelVolume(int channel, qint64 volume)
{
context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_sink_input_volume);
}
void SinkInput::setChannelVolumes(const QVector<qint64> &channelVolumes)
{
context()->setGenericVolumes(index(), channelVolumes, cvolume(), &pa_context_set_sink_input_volume);
}
} // QPulseAudio

@ -0,0 +1,31 @@
/*
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"
namespace QPulseAudio
{
class SinkInput : public Stream
{
Q_OBJECT
public:
explicit SinkInput(QObject *parent);
void update(const pa_sink_input_info *info);
void setVolume(qint64 volume) override;
void setMuted(bool muted) override;
void setChannelVolume(int channel, qint64 volume) override;
void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
void setDeviceIndex(quint32 deviceIndex) override;
};
} // QPulseAudio
#endif // SINKINPUT_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
*/
#include "source.h"
#include "context.h"
#include "server.h"
#include "sourceoutput.h"
namespace QPulseAudio
{
Source::Source(QObject *parent)
: Device(parent)
{
connect(context()->server(), &Server::defaultSourceChanged, this, &Source::defaultChanged);
}
void Source::update(const pa_source_info *info)
{
updateDevice(info);
}
void Source::setVolume(qint64 volume)
{
context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_source_volume_by_index);
}
void Source::setMuted(bool muted)
{
context()->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) {
qCWarning(PLASMAPA) << "invalid port set request" << port_index;
return;
}
context()->setGenericPort(index(), port->name(), &pa_context_set_source_port_by_index);
}
void Source::setChannelVolume(int channel, qint64 volume)
{
context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_source_volume_by_index);
}
void Source::setChannelVolumes(const QVector<qint64> &volumes)
{
context()->setGenericVolumes(index(), volumes, cvolume(), &pa_context_set_source_volume_by_index);
}
bool Source::isDefault() const
{
return context()->server()->defaultSource() == this;
}
void Source::setDefault(bool enable)
{
if (!isDefault() && enable) {
context()->server()->setDefaultSource(this);
}
}
void Source::switchStreams()
{
auto data = context()->sourceOutputs().data();
std::for_each(data.begin(), data.end(), [this](SourceOutput *paObj) {
paObj->setDeviceIndex(m_index);
});
}
} // QPulseAudio

@ -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
*/
#ifndef SOURCE_H
#define SOURCE_H
#include "device.h"
namespace QPulseAudio
{
class Source : public Device
{
Q_OBJECT
public:
explicit Source(QObject *parent);
void update(const pa_source_info *info);
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;
void switchStreams() override;
};
} // QPulseAudio
#endif // SOURCE_H

@ -0,0 +1,52 @@
/*
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 "context.h"
namespace QPulseAudio
{
SourceOutput::SourceOutput(QObject *parent)
: Stream(parent)
{
}
void SourceOutput::update(const pa_source_output_info *info)
{
updateStream(info);
if (m_deviceIndex != info->source) {
m_deviceIndex = info->source;
Q_EMIT deviceIndexChanged();
}
}
void SourceOutput::setDeviceIndex(quint32 deviceIndex)
{
context()->setGenericDeviceForStream(index(), deviceIndex, &pa_context_move_source_output_by_index);
}
void SourceOutput::setVolume(qint64 volume)
{
context()->setGenericVolume(index(), -1, volume, cvolume(), &pa_context_set_source_output_volume);
}
void SourceOutput::setMuted(bool muted)
{
context()->setGenericMute(index(), muted, &pa_context_set_source_output_mute);
}
void SourceOutput::setChannelVolume(int channel, qint64 volume)
{
context()->setGenericVolume(index(), channel, volume, cvolume(), &pa_context_set_source_output_volume);
}
void SourceOutput::setChannelVolumes(const QVector<qint64> &channelVolumes)
{
context()->setGenericVolumes(index(), channelVolumes, cvolume(), &pa_context_set_source_output_volume);
}
} // QPulseAudio

@ -0,0 +1,31 @@
/*
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"
namespace QPulseAudio
{
class SourceOutput : public Stream
{
Q_OBJECT
public:
explicit SourceOutput(QObject *parent);
void update(const pa_source_output_info *info);
void setVolume(qint64 volume) override;
void setMuted(bool muted) override;
void setChannelVolume(int channel, qint64 volume) override;
void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
void setDeviceIndex(quint32 deviceIndex) override;
};
} // QPulseAudio
#endif // SOURCEOUTPUT_H

@ -0,0 +1,60 @@
/*
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2021 Nicolas Fella
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "speakertest.h"
#include "canberracontext.h"
QPulseAudio::Sink *SpeakerTest::sink() const
{
return m_sink;
}
void SpeakerTest::setSink(QPulseAudio::Sink *sink)
{
if (m_sink != sink) {
m_sink = sink;
Q_EMIT sinkChanged();
}
}
void SpeakerTest::testChannel(const QString &name)
{
auto context = QPulseAudio::CanberraContext::instance()->canberra();
if (!context) {
return;
}
ca_context_set_driver(context, "pulse");
char dev[64];
snprintf(dev, sizeof(dev), "%lu", (unsigned long)m_sink->index());
ca_context_change_device(context, dev);
QString sound_name = QStringLiteral("audio-channel-") + name;
ca_proplist *proplist;
ca_proplist_create(&proplist);
ca_proplist_sets(proplist, CA_PROP_MEDIA_ROLE, "test");
ca_proplist_sets(proplist, CA_PROP_MEDIA_NAME, name.toLatin1().constData());
ca_proplist_sets(proplist, CA_PROP_CANBERRA_FORCE_CHANNEL, name.toLatin1().data());
ca_proplist_sets(proplist, CA_PROP_CANBERRA_ENABLE, "1");
ca_proplist_sets(proplist, CA_PROP_EVENT_ID, sound_name.toLatin1().data());
if (ca_context_play_full(context, 0, proplist, nullptr, NULL) != CA_SUCCESS) {
// Try a different sound name.
ca_proplist_sets(proplist, CA_PROP_EVENT_ID, "audio-test-signal");
if (ca_context_play_full(context, 0, proplist, nullptr, NULL) != CA_SUCCESS) {
// Finaly try this... if this doesn't work, then stuff it.
ca_proplist_sets(proplist, CA_PROP_EVENT_ID, "bell-window-system");
ca_context_play_full(context, 0, proplist, nullptr, NULL);
}
}
ca_context_change_device(context, nullptr);
ca_proplist_destroy(proplist);
}

@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2021 Nicolas Fella
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include "sink.h"
#include <QObject>
class SpeakerTest : public QObject
{
Q_OBJECT
Q_PROPERTY(QPulseAudio::Sink *sink READ sink WRITE setSink NOTIFY sinkChanged)
public:
QPulseAudio::Sink *sink() const;
void setSink(QPulseAudio::Sink *sink);
Q_SIGNAL void sinkChanged();
Q_INVOKABLE void testChannel(const QString &name);
private:
QPulseAudio::Sink *m_sink;
};

@ -0,0 +1,51 @@
/*
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"
namespace QPulseAudio
{
Stream::Stream(QObject *parent)
: VolumeObject(parent)
, m_deviceIndex(PA_INVALID_INDEX)
, m_clientIndex(PA_INVALID_INDEX)
, m_virtualStream(false)
, m_corked(false)
{
m_volumeWritable = false;
m_hasVolume = false;
}
Stream::~Stream()
{
}
QString Stream::name() const
{
return m_name;
}
Client *Stream::client() const
{
return context()->clients().data().value(m_clientIndex, nullptr);
}
bool Stream::isVirtualStream() const
{
return m_virtualStream;
}
quint32 Stream::deviceIndex() const
{
return m_deviceIndex;
}
bool Stream::isCorked() const
{
return m_corked;
}
} // QPulseAudio

@ -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
*/
#ifndef STREAM_H
#define STREAM_H
#include <QString>
#include <pulse/volume.h>
#include "pulseobject.h"
#include "volumeobject.h"
#include "context.h"
// Properties need fully qualified classes even with pointers.
#include "client.h"
namespace QPulseAudio
{
class Stream : public VolumeObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QPulseAudio::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)
public:
template<typename PAInfo>
void updateStream(const PAInfo *info)
{
updateVolumeObject(info);
if (m_name != QString::fromUtf8(info->name)) {
m_name = QString::fromUtf8(info->name);
Q_EMIT nameChanged();
}
if (m_hasVolume != info->has_volume) {
m_hasVolume = info->has_volume;
Q_EMIT hasVolumeChanged();
}
if (m_volumeWritable != info->volume_writable) {
m_volumeWritable = info->volume_writable;
Q_EMIT isVolumeWritableChanged();
}
if (m_clientIndex != info->client) {
m_clientIndex = info->client;
Q_EMIT clientChanged();
}
if (m_virtualStream != (info->client == PA_INVALID_INDEX)) {
m_virtualStream = info->client == PA_INVALID_INDEX;
Q_EMIT virtualStreamChanged();
}
if (m_corked != info->corked) {
m_corked = info->corked;
Q_EMIT corkedChanged();
}
}
QString name() const;
Client *client() const;
bool isVirtualStream() const;
quint32 deviceIndex() const;
bool isCorked() const;
virtual void setDeviceIndex(quint32 deviceIndex) = 0;
Q_SIGNALS:
void nameChanged();
void clientChanged();
void virtualStreamChanged();
void deviceIndexChanged();
void corkedChanged();
protected:
explicit Stream(QObject *parent);
~Stream() override;
quint32 m_deviceIndex;
private:
QString m_name;
quint32 m_clientIndex;
bool m_virtualStream;
bool m_corked;
};
} // QPulseAudio
#endif // STREAM_H

@ -0,0 +1,200 @@
/*
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 "debug.h"
namespace QPulseAudio
{
StreamRestore::StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent)
: PulseObject(parent)
, m_muted(false)
{
memset(&m_volume, 0, sizeof(m_volume));
memset(&m_channelMap, 0, sizeof(m_channelMap));
m_index = index;
m_properties = properties;
}
void StreamRestore::update(const pa_ext_stream_restore_info *info)
{
m_cache.valid = false;
const QString infoName = QString::fromUtf8(info->name);
if (m_name != infoName) {
m_name = infoName;
Q_EMIT nameChanged();
}
const QString infoDevice = QString::fromUtf8(info->device);
if (m_device != infoDevice) {
m_device = infoDevice;
Q_EMIT deviceChanged();
}
if (m_muted != info->mute) {
m_muted = info->mute;
Q_EMIT mutedChanged();
}
if (!pa_cvolume_equal(&m_volume, &info->volume)) {
m_volume = info->volume;
Q_EMIT volumeChanged();
Q_EMIT 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 channelsChanged();
}
}
QString StreamRestore::name() const
{
return m_name;
}
QString StreamRestore::device() const
{
return m_device;
}
void StreamRestore::setDevice(const QString &device)
{
if (m_cache.valid) {
if (m_cache.device != device) {
writeChanges(m_cache.volume, m_cache.muted, device);
}
} else {
if (m_device != device) {
writeChanges(m_volume, m_muted, device);
}
}
}
qint64 StreamRestore::volume() const
{
return m_volume.values[0];
}
void StreamRestore::setVolume(qint64 volume)
{
pa_cvolume vol = m_cache.valid ? m_cache.volume : 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 (m_cache.valid) {
writeChanges(vol, m_cache.muted, m_cache.device);
} else {
writeChanges(vol, m_muted, m_device);
}
}
bool StreamRestore::isMuted() const
{
return m_muted;
}
void StreamRestore::setMuted(bool muted)
{
if (m_cache.valid) {
if (m_cache.muted != muted) {
writeChanges(m_cache.volume, muted, m_cache.device);
}
} else {
if (m_muted != muted) {
writeChanges(m_volume, muted, m_device);
}
}
}
bool StreamRestore::hasVolume() const
{
return true;
}
bool StreamRestore::isVolumeWritable() const
{
return true;
}
QStringList StreamRestore::channels() const
{
return m_channels;
}
QList<qreal> StreamRestore::channelVolumes() const
{
QList<qreal> ret;
ret.reserve(m_volume.channels);
for (int i = 0; i < m_volume.channels; ++i) {
ret << m_volume.values[i];
}
return ret;
}
void StreamRestore::setChannelVolume(int channel, qint64 volume)
{
Q_ASSERT(channel >= 0 && channel < m_volume.channels);
pa_cvolume vol = m_cache.valid ? m_cache.volume : m_volume;
vol.values[channel] = volume;
if (m_cache.valid) {
writeChanges(vol, m_cache.muted, m_cache.device);
} else {
writeChanges(vol, m_muted, m_device);
}
}
quint32 StreamRestore::deviceIndex() const
{
return PA_INVALID_INDEX;
}
void StreamRestore::setDeviceIndex(quint32 deviceIndex)
{
Q_UNUSED(deviceIndex);
qCWarning(PLASMAPA) << "Not implemented";
}
void StreamRestore::writeChanges(const pa_cvolume &volume, bool muted, const QString &device)
{
const QByteArray nameData = m_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()->streamRestoreWrite(&info);
}
} // QPulseAudio

@ -0,0 +1,85 @@
/*
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"
#include <pulse/ext-stream-restore.h>
namespace QPulseAudio
{
class StreamRestore : public PulseObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
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(QStringList channels READ channels NOTIFY channelsChanged)
Q_PROPERTY(QList<qreal> channelVolumes READ channelVolumes NOTIFY channelVolumesChanged)
Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged)
public:
StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent);
void update(const pa_ext_stream_restore_info *info);
QString name() const;
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;
QStringList channels() const;
QList<qreal> channelVolumes() const;
quint32 deviceIndex() const;
void setDeviceIndex(quint32 deviceIndex);
Q_INVOKABLE void setChannelVolume(int channel, qint64 volume);
Q_SIGNALS:
void nameChanged();
void deviceChanged();
void volumeChanged();
void mutedChanged();
void channelsChanged();
void channelVolumesChanged();
void deviceIndexChanged();
private:
void writeChanges(const pa_cvolume &volume, bool muted, const QString &device);
QString m_name;
QString m_device;
pa_cvolume m_volume;
pa_channel_map m_channelMap;
QStringList m_channels;
bool m_muted;
struct {
bool valid = false;
pa_cvolume volume;
bool muted;
QString device;
} m_cache;
};
} // QPulseAudio
#endif // STREAMRESTORE_H

@ -0,0 +1,195 @@
/*
SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "volumemonitor.h"
#include <pulse/pulseaudio.h>
#include "context.h"
#include "debug.h"
#include "sink.h"
#include "sinkinput.h"
#include "source.h"
#include "sourceoutput.h"
#include "volumeobject.h"
#include <QtGlobal>
using namespace QPulseAudio;
VolumeMonitor::VolumeMonitor(QObject *parent)
: QObject(parent)
{
Context::instance()->ref();
}
VolumeMonitor::~VolumeMonitor()
{
setTarget(nullptr);
Context::instance()->unref();
}
bool VolumeMonitor::isAvailable() const
{
return m_stream != nullptr;
}
void VolumeMonitor::updateVolume(qreal volume)
{
// qFuzzyCompare cannot compare against 0.
if (qFuzzyCompare(1 + m_volume, 1 + volume)) {
return;
}
m_volume = volume;
Q_EMIT volumeChanged();
}
QPulseAudio::VolumeObject *QPulseAudio::VolumeMonitor::target() const
{
return m_target;
}
void QPulseAudio::VolumeMonitor::setTarget(QPulseAudio::VolumeObject *target)
{
if (target == m_target) {
return;
}
if (m_stream) {
pa_stream_set_read_callback(m_stream, nullptr, nullptr);
pa_stream_set_suspended_callback(m_stream, nullptr, nullptr);
if (pa_stream_get_state(m_stream) == PA_STREAM_CREATING) {
pa_stream_set_state_callback(
m_stream,
[](pa_stream *s, void *) {
pa_stream_disconnect(s);
pa_stream_set_state_callback(s, nullptr, nullptr);
},
nullptr);
} else {
pa_stream_disconnect(m_stream);
}
pa_stream_unref(m_stream);
m_stream = nullptr;
Q_EMIT availableChanged();
}
m_target = target;
if (target) {
connect(target, &QObject::destroyed, this, [this] {
setTarget(nullptr);
});
createStream();
}
Q_EMIT targetChanged();
}
void VolumeMonitor::createStream()
{
Q_ASSERT(!m_stream);
uint32_t sourceIdx = PA_INVALID_INDEX;
uint32_t streamIdx = PA_INVALID_INDEX;
if (auto *sinkInput = qobject_cast<SinkInput *>(m_target)) {
Sink *sink = Context::instance()->sinks().data().value(sinkInput->deviceIndex());
if (sink) {
sourceIdx = sink->monitorIndex();
}
streamIdx = sinkInput->index();
} else if (auto *sourceOutput = qobject_cast<SourceOutput *>(m_target)) {
sourceIdx = sourceOutput->deviceIndex();
streamIdx = sourceOutput->index();
} else if (auto *sink = qobject_cast<Sink *>(m_target)) {
sourceIdx = sink->monitorIndex();
} else if (auto *source = qobject_cast<Source *>(m_target)) {
sourceIdx = source->index();
} else {
Q_UNREACHABLE();
}
if (sourceIdx == PA_INVALID_INDEX) {
return;
}
char t[16];
pa_buffer_attr attr;
pa_sample_spec ss;
pa_stream_flags_t flags;
ss.channels = 1;
ss.format = PA_SAMPLE_FLOAT32;
ss.rate = 25;
memset(&attr, 0, sizeof(attr));
attr.fragsize = sizeof(float);
attr.maxlength = (uint32_t)-1;
snprintf(t, sizeof(t), "%u", sourceIdx);
if (!(m_stream = pa_stream_new(Context::instance()->context(), "PlasmaPA-VolumeMeter", &ss, nullptr))) {
qCWarning(PLASMAPA) << "Failed to create stream";
return;
}
if (streamIdx != PA_INVALID_INDEX) {
pa_stream_set_monitor_stream(m_stream, streamIdx);
}
pa_stream_set_read_callback(m_stream, read_callback, this);
pa_stream_set_suspended_callback(m_stream, suspended_callback, this);
flags = (pa_stream_flags_t)(PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | PA_STREAM_ADJUST_LATENCY);
if (pa_stream_connect_record(m_stream, t, &attr, flags) < 0) {
pa_stream_unref(m_stream);
m_stream = nullptr;
return;
}
Q_EMIT availableChanged();
}
void VolumeMonitor::suspended_callback(pa_stream *s, void *userdata)
{
VolumeMonitor *w = static_cast<VolumeMonitor *>(userdata);
if (pa_stream_is_suspended(s)) {
w->updateVolume(-1);
}
}
void VolumeMonitor::read_callback(pa_stream *s, size_t length, void *userdata)
{
VolumeMonitor *w = static_cast<VolumeMonitor *>(userdata);
const void *data;
double volume;
if (pa_stream_peek(s, &data, &length) < 0) {
qCWarning(PLASMAPA) << "Failed to read data from stream";
return;
}
if (!data) {
/* nullptr data means either a hole or empty buffer.
* Only drop the stream when there is a hole (length > 0) */
if (length) {
pa_stream_drop(s);
}
return;
}
Q_ASSERT(length > 0);
Q_ASSERT(length % sizeof(float) == 0);
volume = ((const float *)data)[length / sizeof(float) - 1];
pa_stream_drop(s);
volume = qBound(0.0, volume, 1.0);
w->updateVolume(volume);
}

@ -0,0 +1,68 @@
/*
SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <QPointer>
#include <QQmlParserStatus>
struct pa_stream;
namespace QPulseAudio
{
class VolumeObject;
/**
* This class provides a way to see the "peak" volume currently playing of any VolumeObject
*/
class VolumeMonitor : public QObject
{
Q_OBJECT
/**
* Object to monitor the volume of
* This is the "PulseObject" role of any SinkInput, Sink or Output model
* Setting to null will stop streaming
*/
Q_PROPERTY(QPulseAudio::VolumeObject *target READ target WRITE setTarget NOTIFY targetChanged)
/**
* The peak output for the volume at any given moment
* Value is normalised between 0 and 1
*/
Q_PROPERTY(qreal volume MEMBER m_volume NOTIFY volumeChanged)
/**
* Whether monitoring is available
*/
Q_PROPERTY(bool available READ isAvailable NOTIFY availableChanged)
public:
VolumeMonitor(QObject *parent = nullptr);
~VolumeMonitor();
bool isAvailable() const;
VolumeObject *target() const;
void setTarget(VolumeObject *target);
Q_SIGNALS:
void volumeChanged();
void targetChanged();
void availableChanged();
private:
void createStream();
void updateVolume(qreal volume);
static void read_callback(pa_stream *s, size_t length, void *userdata);
static void suspended_callback(pa_stream *s, void *userdata);
VolumeObject *m_target;
pa_stream *m_stream = nullptr;
qreal m_volume = 0;
};
}

@ -0,0 +1,69 @@
/*
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 "volumeobject.h"
namespace QPulseAudio
{
VolumeObject::VolumeObject(QObject *parent)
: PulseObject(parent)
, m_muted(true)
, m_hasVolume(true)
, m_volumeWritable(true)
{
pa_cvolume_init(&m_volume);
}
VolumeObject::~VolumeObject()
{
}
qint64 VolumeObject::volume() const
{
return pa_cvolume_max(&m_volume);
}
bool VolumeObject::isMuted() const
{
return m_muted;
}
pa_cvolume VolumeObject::cvolume() const
{
return m_volume;
}
bool VolumeObject::hasVolume() const
{
return m_hasVolume;
}
bool VolumeObject::isVolumeWritable() const
{
return m_volumeWritable;
}
QStringList VolumeObject::channels() const
{
return m_channels;
}
QStringList VolumeObject::rawChannels() const
{
return m_rawChannels;
}
QVector<qint64> VolumeObject::channelVolumes() const
{
QVector<qint64> ret;
ret.reserve(m_volume.channels);
for (int i = 0; i < m_volume.channels; ++i) {
ret << m_volume.values[i];
}
return ret;
}
} // QPulseAudio

@ -0,0 +1,102 @@
/*
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 <pulse/volume.h>
#include "pulseobject.h"
namespace QPulseAudio
{
class VolumeObject : public PulseObject
{
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 hasVolume READ hasVolume NOTIFY hasVolumeChanged)
Q_PROPERTY(bool volumeWritable READ isVolumeWritable NOTIFY isVolumeWritableChanged)
Q_PROPERTY(QStringList channels READ channels NOTIFY channelsChanged)
Q_PROPERTY(QStringList rawChannels READ rawChannels NOTIFY rawChannelsChanged)
Q_PROPERTY(QVector<qint64> channelVolumes READ channelVolumes WRITE setChannelVolumes NOTIFY channelVolumesChanged)
public:
explicit VolumeObject(QObject *parent);
~VolumeObject() override;
template<typename PAInfo>
void updateVolumeObject(PAInfo *info)
{
updatePulseObject(info);
if (m_muted != info->mute) {
m_muted = info->mute;
Q_EMIT mutedChanged();
}
if (!pa_cvolume_equal(&m_volume, &info->volume)) {
m_volume = info->volume;
Q_EMIT volumeChanged();
Q_EMIT channelVolumesChanged();
}
QStringList 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 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 rawChannelsChanged();
}
}
qint64 volume() const;
virtual void setVolume(qint64 volume) = 0;
bool isMuted() const;
virtual void setMuted(bool muted) = 0;
bool hasVolume() const;
bool isVolumeWritable() const;
QStringList 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 hasVolumeChanged();
void isVolumeWritableChanged();
void channelsChanged();
void rawChannelsChanged();
void channelVolumesChanged();
protected:
pa_cvolume cvolume() const;
pa_cvolume m_volume;
bool m_muted;
bool m_hasVolume;
bool m_volumeWritable;
QStringList m_channels;
QStringList m_rawChannels;
};
} // QPulseAudio
#endif // VOLUMEOBJECT_H

@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: 2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
# SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause
#[=======================================================================[.rst:
FindCanberra
------------
Try to find Canberra event sound library.
This will define the following variables:
``Canberra_FOUND``
True if (the requested version of) Canberra is available
``Canberra_VERSION``
The version of Canberra
``Canberra_LIBRARIES``
The libraries of Canberra for use with target_link_libraries()
``Canberra_INCLUDE_DIRS``
The include dirs of Canberra for use with target_include_directories()
If ``Canberra_FOUND`` is TRUE, it will also define the following imported
target:
``Canberra::Canberra``
The Canberra library
In general we recommend using the imported target, as it is easier to use.
Bear in mind, however, that if the target is in the link interface of an
exported library, it must be made available by the package config file.
Since 5.56.0.
#]=======================================================================]
find_package(PkgConfig QUIET)
pkg_check_modules(PC_Canberra libcanberra QUIET)
find_library(Canberra_LIBRARIES
NAMES canberra
HINTS ${PC_Canberra_LIBRARY_DIRS}
)
find_path(Canberra_INCLUDE_DIRS
NAMES canberra.h
HINTS ${PC_Canberra_INCLUDE_DIRS}
)
set(Canberra_VERSION ${PC_Canberra_VERSION})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Canberra
FOUND_VAR
Canberra_FOUND
REQUIRED_VARS
Canberra_LIBRARIES
Canberra_INCLUDE_DIRS
VERSION_VAR
Canberra_VERSION
)
if(Canberra_FOUND AND NOT TARGET Canberra::Canberra)
add_library(Canberra::Canberra UNKNOWN IMPORTED)
set_target_properties(Canberra::Canberra PROPERTIES
IMPORTED_LOCATION "${Canberra_LIBRARIES}"
INTERFACE_COMPILE_OPTIONS "${PC_Canberra_CFLAGS}"
INTERFACE_INCLUDE_DIRECTORIES "${Canberra_INCLUDE_DIRS}"
)
endif()
mark_as_advanced(Canberra_LIBRARIES Canberra_INCLUDE_DIRS Canberra_VERSION)
include(FeatureSummary)
set_package_properties(Canberra PROPERTIES
DESCRIPTION "Event sound library"
URL "http://0pointer.de/lennart/projects/libcanberra"
)
# Compatibility variables. In a previous life FindCanberra lived
# in a number of different repos: don't break them if they use ECM but have not
# been updated for this finder.
set(CANBERRA_FOUND ${Canberra_FOUND})
set(CANBERRA_VERSION ${Canberra_VERSION})
set(CANBERRA_LIBRARIES ${Canberra_LIBRARIES})
set(CANBERRA_INCLUDE_DIRS ${Canberra_INCLUDE_DIRS})
mark_as_advanced(CANBERRA_VERSION CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS)

@ -0,0 +1,24 @@
# - Find libcanberra's pulseaudio backend.
# This module defines the following variables:
#
# CanberraPulse_FOUND - true if the backend was found
#
# SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause
find_package(Canberra)
find_library(CanberraPulse_LIBRARY canberra-pulse
PATH_SUFFIXES libcanberra libcanberra-${Canberra_VERSION}
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(CanberraPulse
FOUND_VAR CanberraPulse_FOUND
REQUIRED_VARS CanberraPulse_LIBRARY
)
mark_as_advanced(CanberraPulse_LIBRARY)
# NB: CanberraPulse_LIBRARY is intentionally not documented as it serves no
# public purpose (it's a plugin, not a library).

@ -0,0 +1,21 @@
# - Find sound-theme-freedesktop via XDG_DATA_DIRS
# This module defines the following variables:
#
# SoundThemeFreeDesktop_FOUND - true if the sound theme is found
# SoundThemeFreeDesktop_PATH - path to the index.theme file
#
# SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause
find_file(SoundThemeFreeDesktop_PATH "sounds/freedesktop/index.theme"
PATHS ENV XDG_DATA_DIRS /usr/local/share/ /usr/share/
NO_DEFAULT_PATH
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SoundThemeFreeDesktop
FOUND_VAR SoundThemeFreeDesktop_FOUND
REQUIRED_VARS SoundThemeFreeDesktop_PATH
)
mark_as_advanced(SoundThemeFreeDesktop_PATH)

3
debian/control vendored

@ -11,6 +11,9 @@ Build-Depends: cmake,
libkf5bluezqt-dev,
modemmanager-qt-dev,
libqt5sensors5-dev,
libcanberra-dev,
libpulse-dev,
sound-theme-freedesktop,
qtbase5-dev,
qtdeclarative5-dev,
qtquickcontrols2-5-dev,

Loading…
Cancel
Save