Commit f836a0e1 authored by David Edmundson's avatar David Edmundson

Introduce VolumeMonitor object to show volume of active streams/sinks/outputs

Several volume control UIs show a meter of which streams are actively
playing audio.

VolumeObject gains a property a virtual to create a relevant stream to
monitor and a QML exposed class is created to expose this in a very
QML-friendly consumable way.
parent bc79ca32
......@@ -17,6 +17,7 @@ set(cpp_SRCS
source.cpp
sourceoutput.cpp
stream.cpp
volumemonitor.cpp
volumeobject.cpp
debug.cpp
server.cpp
......
......@@ -37,6 +37,8 @@
#include "streamrestore.h"
#include "module.h"
#include <KLocalizedString>
namespace QPulseAudio
{
......@@ -111,7 +113,8 @@ static void source_output_cb(pa_context *context, const pa_source_output_info *i
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.kmixd") == 0
|| strcmp(app, "org.kde.plasma-pa") == 0)
return;
}
Q_ASSERT(context);
......@@ -610,7 +613,13 @@ void Context::connectToDaemon()
pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop);
Q_ASSERT(api);
m_context = pa_context_new(api, "QPulse");
pa_proplist *proplist = pa_proplist_new();
pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, i18nc("Name shown in debug pulseaudio tools", "Plasma PA").toUtf8().constData());
pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "org.kde.plasma-pa");
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) {
......
......@@ -56,6 +56,8 @@ public:
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; }
......
......@@ -30,6 +30,7 @@
#include "modulemanager.h"
#include "profile.h"
#include "port.h"
#include "volumemonitor.h"
#include "globalactioncollection.h"
#include "microphoneindicator.h"
......@@ -57,6 +58,7 @@ void Plugin::registerTypes(const char* uri)
qmlRegisterType<QPulseAudio::SourceOutputModel>(uri, 0, 1, "SourceOutputModel");
qmlRegisterType<QPulseAudio::StreamRestoreModel>(uri, 0, 1, "StreamRestoreModel");
qmlRegisterType<QPulseAudio::ModuleModel>(uri, 0, 1, "ModuleModel");
qmlRegisterType<QPulseAudio::VolumeMonitor>(uri, 0, 01, "VolumeMonitor");
qmlRegisterUncreatableType<QPulseAudio::Profile>(uri, 0, 1, "Profile", QString());
qmlRegisterUncreatableType<QPulseAudio::Port>(uri, 0, 1, "Port", QString());
qmlRegisterType<GlobalAction>(uri, 0, 1, "GlobalAction");
......@@ -74,9 +76,11 @@ void Plugin::registerTypes(const char* uri)
qmlRegisterType<QPulseAudio::Client>();
qmlRegisterType<QPulseAudio::Sink>();
qmlRegisterType<QPulseAudio::Source>();
qmlRegisterType<QPulseAudio::VolumeObject>();
#else
qmlRegisterAnonymousType<QPulseAudio::Client>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::Sink>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::Source>(uri, 1);
qmlRegisterAnonymousType<QPulseAudio::VolumeObject>(uri, 1);
#endif
}
......@@ -43,6 +43,10 @@ Sink::~Sink()
void Sink::update(const pa_sink_info *info)
{
updateDevice(info);
if (m_sourceIndex != info->monitor_source) {
m_sourceIndex = info->monitor_source;
Q_EMIT sourceIndexChanged();
}
}
void Sink::setVolume(qint64 volume)
......@@ -129,4 +133,9 @@ void Sink::switchStreams()
});
}
quint32 Sink::sourceIndex() const
{
return m_sourceIndex;
}
} // QPulseAudio
......@@ -47,8 +47,13 @@ public:
void switchStreams() override;
quint32 sourceIndex() const override;
public Q_SLOTS:
void testChannel(const QString &name);
private:
quint32 m_sourceIndex = -1;
};
} // QPulseAudio
......
......@@ -22,6 +22,8 @@
#include "context.h"
#include "sink.h"
namespace QPulseAudio
{
......@@ -37,6 +39,10 @@ void SinkInput::update(const pa_sink_input_info *info)
m_deviceIndex = info->sink;
Q_EMIT deviceIndexChanged();
}
if (m_sinkIndex != info->sink) {
m_sinkIndex = info->sink;
Q_EMIT sourceIndexChanged();
}
}
void SinkInput::setDeviceIndex(quint32 deviceIndex)
......@@ -64,4 +70,18 @@ void SinkInput::setChannelVolumes(const QVector<qint64> &channelVolumes)
context()->setGenericVolumes(index(), channelVolumes, cvolume(), &pa_context_set_sink_input_volume);
}
quint32 SinkInput::sourceIndex() const
{
auto sink = qobject_cast<Sink*>(context()->sinks().data().value(m_sinkIndex));
if (!sink) {
return -1;
}
return sink->sourceIndex();
}
quint32 SinkInput::streamIndex() const
{
return m_index;
}
} // QPulseAudio
......@@ -41,6 +41,12 @@ public:
void setChannelVolume(int channel, qint64 volume) override;
void setChannelVolumes(const QVector<qint64> &channelVolumes) override;
void setDeviceIndex(quint32 deviceIndex) override;
quint32 sourceIndex() const override;
quint32 streamIndex() const override;
private:
quint32 m_sinkIndex = -1;
};
} // QPulseAudio
......
......@@ -88,4 +88,9 @@ void Source::switchStreams()
});
}
quint32 Source::sourceIndex() const
{
return index();
}
} // QPulseAudio
......@@ -43,6 +43,8 @@ public:
void setDefault(bool enable) override;
void switchStreams() override;
quint32 sourceIndex() const override;
};
} // QPulseAudio
......
/*
Copyright 2020 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "volumemonitor.h"
#include <pulse/pulseaudio.h>
#include "context.h"
#include "debug.h"
#include "volumeobject.h"
#include <QtGlobal>
using namespace QPulseAudio;
VolumeMonitor::VolumeMonitor(QObject* parent)
: QObject(parent)
{
Context::instance()->ref();
}
VolumeMonitor::~VolumeMonitor()
{
if (m_stream) {
pa_stream_unref(m_stream);
}
Context::instance()->unref();
}
bool VolumeMonitor::isAvailable() const
{
return m_stream != nullptr;
}
void VolumeMonitor::updateVolume(qreal volume)
{
m_volume = volume;
Q_EMIT volumeChanged();
}
QPulseAudio::VolumeObject * QPulseAudio::VolumeMonitor::target() const
{
return m_target.data();
}
void QPulseAudio::VolumeMonitor::setTarget(QPulseAudio::VolumeObject* target)
{
if (target == m_target) {
return;
}
m_target = target;
if (target) {
createStream();
} else if (m_stream) {
pa_stream_unref(m_stream);
m_stream = nullptr;
Q_EMIT availableChanged();
}
}
void VolumeMonitor::createStream()
{
Q_ASSERT(!m_stream);
uint32_t sourceIdx = m_target->sourceIndex();
uint32_t streamIdx = m_target->streamIndex();
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);
}
/*
Copyright 2020 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#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);
QPointer<VolumeObject> m_target;
pa_stream *m_stream = nullptr;
qreal m_volume = 0;
};
}
......@@ -92,6 +92,9 @@ public:
virtual void setChannelVolumes(const QVector<qint64> &channelVolumes) = 0;
Q_INVOKABLE virtual void setChannelVolume(int channel, qint64 volume) = 0;
virtual quint32 sourceIndex() const {return -1;};
virtual quint32 streamIndex() const {return -1;};
Q_SIGNALS:
void volumeChanged();
void mutedChanged();
......@@ -100,6 +103,7 @@ Q_SIGNALS:
void channelsChanged();
void rawChannelsChanged();
void channelVolumesChanged();
void sourceIndexChanged();
protected:
pa_cvolume cvolume() const;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment