File indexing completed on 2024-04-21 16:20:32

0001 /*
0002     SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "volumemonitor.h"
0008 
0009 #include <pulse/pulseaudio.h>
0010 
0011 #include "context.h"
0012 #include "debug.h"
0013 #include "sink.h"
0014 #include "sinkinput.h"
0015 #include "source.h"
0016 #include "sourceoutput.h"
0017 #include "volumeobject.h"
0018 
0019 #include <QtGlobal>
0020 
0021 using namespace QPulseAudio;
0022 
0023 VolumeMonitor::VolumeMonitor(QObject *parent)
0024     : QObject(parent)
0025 {
0026     Context::instance()->ref();
0027 }
0028 
0029 VolumeMonitor::~VolumeMonitor()
0030 {
0031     setTarget(nullptr);
0032     Context::instance()->unref();
0033 }
0034 
0035 bool VolumeMonitor::isAvailable() const
0036 {
0037     return m_stream != nullptr;
0038 }
0039 
0040 void VolumeMonitor::updateVolume(qreal volume)
0041 {
0042     // qFuzzyCompare cannot compare against 0.
0043     if (qFuzzyCompare(1 + m_volume, 1 + volume)) {
0044         return;
0045     }
0046 
0047     m_volume = volume;
0048     Q_EMIT volumeChanged();
0049 }
0050 
0051 QPulseAudio::VolumeObject *QPulseAudio::VolumeMonitor::target() const
0052 {
0053     return m_target;
0054 }
0055 
0056 void QPulseAudio::VolumeMonitor::setTarget(QPulseAudio::VolumeObject *target)
0057 {
0058     if (target == m_target) {
0059         return;
0060     }
0061 
0062     if (m_stream) {
0063         pa_stream_set_read_callback(m_stream, nullptr, nullptr);
0064         pa_stream_set_suspended_callback(m_stream, nullptr, nullptr);
0065         if (pa_stream_get_state(m_stream) == PA_STREAM_CREATING) {
0066             pa_stream_set_state_callback(
0067                 m_stream,
0068                 [](pa_stream *s, void *) {
0069                     pa_stream_disconnect(s);
0070                     pa_stream_set_state_callback(s, nullptr, nullptr);
0071                 },
0072                 nullptr);
0073         } else {
0074             pa_stream_disconnect(m_stream);
0075         }
0076         pa_stream_unref(m_stream);
0077         m_stream = nullptr;
0078         Q_EMIT availableChanged();
0079     }
0080 
0081     m_target = target;
0082 
0083     if (target) {
0084         connect(target, &QObject::destroyed, this, [this] {
0085             setTarget(nullptr);
0086         });
0087         createStream();
0088     }
0089 
0090     Q_EMIT targetChanged();
0091 }
0092 
0093 void VolumeMonitor::createStream()
0094 {
0095     Q_ASSERT(!m_stream);
0096 
0097     uint32_t sourceIdx = PA_INVALID_INDEX;
0098     uint32_t streamIdx = PA_INVALID_INDEX;
0099 
0100     if (auto *sinkInput = qobject_cast<SinkInput *>(m_target)) {
0101         Sink *sink = Context::instance()->sinks().data().value(sinkInput->deviceIndex());
0102         if (sink) {
0103             sourceIdx = sink->monitorIndex();
0104         }
0105         streamIdx = sinkInput->index();
0106     } else if (auto *sourceOutput = qobject_cast<SourceOutput *>(m_target)) {
0107         sourceIdx = sourceOutput->deviceIndex();
0108     } else if (auto *sink = qobject_cast<Sink *>(m_target)) {
0109         sourceIdx = sink->monitorIndex();
0110     } else if (auto *source = qobject_cast<Source *>(m_target)) {
0111         sourceIdx = source->index();
0112     } else {
0113         Q_UNREACHABLE();
0114     }
0115 
0116     if (sourceIdx == PA_INVALID_INDEX) {
0117         return;
0118     }
0119 
0120     char t[16];
0121     pa_buffer_attr attr;
0122     pa_sample_spec ss;
0123     pa_stream_flags_t flags;
0124 
0125     ss.channels = 1;
0126     ss.format = PA_SAMPLE_FLOAT32;
0127     ss.rate = 25;
0128 
0129     memset(&attr, 0, sizeof(attr));
0130     attr.fragsize = sizeof(float);
0131     attr.maxlength = (uint32_t)-1;
0132 
0133     snprintf(t, sizeof(t), "%u", sourceIdx);
0134 
0135     if (!(m_stream = pa_stream_new(Context::instance()->context(), "PlasmaPA-VolumeMeter", &ss, nullptr))) {
0136         qCWarning(PLASMAPA) << "Failed to create stream";
0137         return;
0138     }
0139 
0140     if (streamIdx != PA_INVALID_INDEX) {
0141         pa_stream_set_monitor_stream(m_stream, streamIdx);
0142     }
0143 
0144     pa_stream_set_read_callback(m_stream, read_callback, this);
0145     pa_stream_set_suspended_callback(m_stream, suspended_callback, this);
0146 
0147     flags = (pa_stream_flags_t)(PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | PA_STREAM_ADJUST_LATENCY);
0148 
0149     if (pa_stream_connect_record(m_stream, t, &attr, flags) < 0) {
0150         pa_stream_unref(m_stream);
0151         m_stream = nullptr;
0152         return;
0153     }
0154     Q_EMIT availableChanged();
0155 }
0156 
0157 void VolumeMonitor::suspended_callback(pa_stream *s, void *userdata)
0158 {
0159     auto *w = static_cast<VolumeMonitor *>(userdata);
0160     if (pa_stream_is_suspended(s)) {
0161         w->updateVolume(-1);
0162     }
0163 }
0164 
0165 void VolumeMonitor::read_callback(pa_stream *s, size_t length, void *userdata)
0166 {
0167     auto *w = static_cast<VolumeMonitor *>(userdata);
0168     const void *data;
0169     double volume;
0170 
0171     if (pa_stream_peek(s, &data, &length) < 0) {
0172         qCWarning(PLASMAPA) << "Failed to read data from stream";
0173         return;
0174     }
0175 
0176     if (!data) {
0177         /* nullptr data means either a hole or empty buffer.
0178          * Only drop the stream when there is a hole (length > 0) */
0179         if (length) {
0180             pa_stream_drop(s);
0181         }
0182         return;
0183     }
0184 
0185     Q_ASSERT(length > 0);
0186     Q_ASSERT(length % sizeof(float) == 0);
0187 
0188     volume = ((const float *)data)[length / sizeof(float) - 1];
0189 
0190     pa_stream_drop(s);
0191 
0192     volume = qBound(0.0, volume, 1.0);
0193     w->updateVolume(volume);
0194 }