Add audio module
parent
132c9f24e1
commit
a60274820b
@ -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)
|
Loading…
Reference in New Issue