You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lxqt-panel-packaging/plugin-volume/pulseaudioengine.cpp

427 lines
12 KiB

/* BEGIN_COMMON_COPYRIGHT_HEADER
* (c)LGPL2+
*
* LXDE-Qt - a lightweight, Qt based, desktop toolset
* http://razor-qt.org
*
* Copyright: 2012 Razor team
* Authors:
* Johannes Zellner <webmaster@nebulon.de>
*
* This program or 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) any later version.
*
* 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, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* END_COMMON_COPYRIGHT_HEADER */
#include "pulseaudioengine.h"
#include "audiodevice.h"
#include <QMetaType>
#include <QtDebug>
//#define PULSEAUDIO_ENGINE_DEBUG
static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int isLast, void *userdata)
{
PulseAudioEngine *pulseEngine = static_cast<PulseAudioEngine*>(userdata);
QMap<pa_sink_state, QString> stateMap;
stateMap[PA_SINK_INVALID_STATE] = "n/a";
stateMap[PA_SINK_RUNNING] = "RUNNING";
stateMap[PA_SINK_IDLE] = "IDLE";
stateMap[PA_SINK_SUSPENDED] = "SUSPENDED";
if (isLast < 0) {
pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
qWarning() << QString("Failed to get sink information: %1").arg(pa_strerror(pa_context_errno(context)));
return;
}
if (isLast) {
pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
return;
}
pulseEngine->addOrUpdateSink(info);
}
static void contextEventCallback(pa_context *context, const char *name, pa_proplist *p, void *userdata)
{
#ifdef PULSEAUDIO_ENGINE_DEBUG
qWarning("event received %s", name);
#endif
}
static void contextStateCallback(pa_context *context, void *userdata)
{
PulseAudioEngine *pulseEngine = reinterpret_cast<PulseAudioEngine*>(userdata);
// update internal state
pa_context_state_t state = pa_context_get_state(context);
pulseEngine->setContextState(state);
#ifdef PULSEAUDIO_ENGINE_DEBUG
switch (state) {
case PA_CONTEXT_UNCONNECTED:
qWarning("context unconnected");
break;
case PA_CONTEXT_CONNECTING:
qWarning("context connecting");
break;
case PA_CONTEXT_AUTHORIZING:
qWarning("context authorizing");
break;
case PA_CONTEXT_SETTING_NAME:
qWarning("context setting name");
break;
case PA_CONTEXT_READY:
qWarning("context ready");
break;
case PA_CONTEXT_FAILED:
qWarning("context failed");
break;
case PA_CONTEXT_TERMINATED:
qWarning("context terminated");
break;
default:
qWarning("we should never hit this state");
}
#endif
pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
}
static void contextSuccessCallback(pa_context *context, int success, void *userdata)
{
Q_UNUSED(context);
Q_UNUSED(success);
Q_UNUSED(userdata);
PulseAudioEngine *pulseEngine = reinterpret_cast<PulseAudioEngine*>(userdata);
pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0);
}
static void contextSubscriptionCallback(pa_context *context, pa_subscription_event_type_t t, uint32_t idx, void *userdata)
{
PulseAudioEngine *pulseEngine = reinterpret_cast<PulseAudioEngine*>(userdata);
if (PA_SUBSCRIPTION_EVENT_REMOVE == t)
pulseEngine->removeSink(idx);
else
pulseEngine->requestSinkInfoUpdate(idx);
}
PulseAudioEngine::PulseAudioEngine(QObject *parent) :
AudioEngine(parent),
m_context(0),
m_contextState(PA_CONTEXT_UNCONNECTED),
m_ready(false),
m_maximumVolume(PA_VOLUME_UI_MAX)
{
qRegisterMetaType<pa_context_state_t>("pa_context_state_t");
m_reconnectionTimer.setSingleShot(true);
m_reconnectionTimer.setInterval(100);
connect(&m_reconnectionTimer, SIGNAL(timeout()), this, SLOT(connectContext()));
m_mainLoop = pa_threaded_mainloop_new();
if (m_mainLoop == 0) {
qWarning("Unable to create pulseaudio mainloop");
return;
}
if (pa_threaded_mainloop_start(m_mainLoop) != 0) {
qWarning("Unable to start pulseaudio mainloop");
pa_threaded_mainloop_free(m_mainLoop);
m_mainLoop = 0;
return;
}
m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop);
connect(this, SIGNAL(contextStateChanged(pa_context_state_t)), this, SLOT(handleContextStateChanged()));
connectContext();
}
PulseAudioEngine::~PulseAudioEngine()
{
if (m_context) {
pa_context_unref(m_context);
m_context = 0;
}
if (m_mainLoop) {
pa_threaded_mainloop_free(m_mainLoop);
m_mainLoop = 0;
}
}
void PulseAudioEngine::removeSink(uint32_t idx)
{
auto dev_i = std::find_if(m_sinks.begin(), m_sinks.end(), [idx] (AudioDevice * dev) { return dev->index() == idx; });
if (m_sinks.end() == dev_i)
return;
QScopedPointer<AudioDevice> dev{*dev_i};
m_cVolumeMap.remove(dev.data());
m_sinks.erase(dev_i);
emit sinkListChanged();
}
void PulseAudioEngine::addOrUpdateSink(const pa_sink_info *info)
{
AudioDevice *dev = 0;
bool newSink = false;
QString name = QString::fromUtf8(info->name);
foreach (AudioDevice *device, m_sinks) {
if (device->name() == name) {
dev = device;
break;
}
}
if (!dev) {
dev = new AudioDevice(Sink, this);
newSink = true;
}
dev->setName(name);
dev->setIndex(info->index);
dev->setDescription(QString::fromUtf8(info->description));
dev->setMuteNoCommit(info->mute);
// TODO: save separately? alsa does not have it
m_cVolumeMap.insert(dev, info->volume);
pa_volume_t v = pa_cvolume_avg(&(info->volume));
// convert real volume to percentage
dev->setVolumeNoCommit(qRound((static_cast<double>(v) * 100.0) / m_maximumVolume));
if (newSink) {
//keep the sinks sorted by index()
m_sinks.insert(
std::lower_bound(m_sinks.begin(), m_sinks.end(), dev, [] (AudioDevice const * const a, AudioDevice const * const b) {
return a->name() < b->name();
})
, dev
);
emit sinkListChanged();
}
}
void PulseAudioEngine::requestSinkInfoUpdate(uint32_t idx)
{
emit sinkInfoChanged(idx);
}
void PulseAudioEngine::commitDeviceVolume(AudioDevice *device)
{
if (!device || !m_ready)
return;
// convert from percentage to real volume value
pa_volume_t v = ((double)device->volume() / 100.0) * m_maximumVolume;
pa_cvolume tmpVolume = m_cVolumeMap.value(device);
pa_cvolume *volume = pa_cvolume_set(&tmpVolume, tmpVolume.channels, v);
// qDebug() << "PulseAudioEngine::commitDeviceVolume" << v;
pa_threaded_mainloop_lock(m_mainLoop);
pa_operation *operation;
if (device->type() == Sink)
operation = pa_context_set_sink_volume_by_index(m_context, device->index(), volume, contextSuccessCallback, this);
else
operation = pa_context_set_source_volume_by_index(m_context, device->index(), volume, contextSuccessCallback, this);
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(m_mainLoop);
pa_operation_unref(operation);
pa_threaded_mainloop_unlock(m_mainLoop);
}
void PulseAudioEngine::retrieveSinks()
{
if (!m_ready)
return;
pa_threaded_mainloop_lock(m_mainLoop);
pa_operation *operation;
operation = pa_context_get_sink_info_list(m_context, sinkInfoCallback, this);
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(m_mainLoop);
pa_operation_unref(operation);
pa_threaded_mainloop_unlock(m_mainLoop);
}
void PulseAudioEngine::setupSubscription()
{
if (!m_ready)
return;
connect(this, &PulseAudioEngine::sinkInfoChanged, this, &PulseAudioEngine::retrieveSinkInfo, Qt::QueuedConnection);
pa_context_set_subscribe_callback(m_context, contextSubscriptionCallback, this);
pa_threaded_mainloop_lock(m_mainLoop);
pa_operation *operation;
operation = pa_context_subscribe(m_context, PA_SUBSCRIPTION_MASK_SINK, contextSuccessCallback, this);
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(m_mainLoop);
pa_operation_unref(operation);
pa_threaded_mainloop_unlock(m_mainLoop);
}
void PulseAudioEngine::handleContextStateChanged()
{
if (m_contextState == PA_CONTEXT_FAILED || m_contextState == PA_CONTEXT_TERMINATED) {
qWarning("LXQt-Volume: Context connection failed or terminated lets try to reconnect");
m_reconnectionTimer.start();
}
}
void PulseAudioEngine::connectContext()
{
bool keepGoing = true;
bool ok = false;
m_reconnectionTimer.stop();
if (!m_mainLoop)
return;
pa_threaded_mainloop_lock(m_mainLoop);
if (m_context) {
pa_context_unref(m_context);
m_context = 0;
}
m_context = pa_context_new(m_mainLoopApi, "lxqt-volume");
pa_context_set_state_callback(m_context, contextStateCallback, this);
pa_context_set_event_callback(m_context, contextEventCallback, this);
if (!m_context) {
pa_threaded_mainloop_unlock(m_mainLoop);
m_reconnectionTimer.start();
return;
}
if (pa_context_connect(m_context, NULL, (pa_context_flags_t)0, NULL) < 0) {
pa_threaded_mainloop_unlock(m_mainLoop);
m_reconnectionTimer.start();
return;
}
while (keepGoing) {
switch (m_contextState) {
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
keepGoing = false;
ok = true;
break;
case PA_CONTEXT_TERMINATED:
keepGoing = false;
break;
case PA_CONTEXT_FAILED:
default:
qWarning() << QString("Connection failure: %1").arg(pa_strerror(pa_context_errno(m_context)));
keepGoing = false;
}
if (keepGoing)
pa_threaded_mainloop_wait(m_mainLoop);
}
pa_threaded_mainloop_unlock(m_mainLoop);
if (ok) {
retrieveSinks();
setupSubscription();
} else {
m_reconnectionTimer.start();
}
}
void PulseAudioEngine::retrieveSinkInfo(uint32_t idx)
{
if (!m_ready)
return;
pa_threaded_mainloop_lock(m_mainLoop);
pa_operation *operation;
operation = pa_context_get_sink_info_by_index(m_context, idx, sinkInfoCallback, this);
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(m_mainLoop);
pa_operation_unref(operation);
pa_threaded_mainloop_unlock(m_mainLoop);
}
void PulseAudioEngine::setMute(AudioDevice *device, bool state)
{
if (!m_ready)
return;
pa_threaded_mainloop_lock(m_mainLoop);
pa_operation *operation;
operation = pa_context_set_sink_mute_by_index(m_context, device->index(), state, contextSuccessCallback, this);
while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(m_mainLoop);
pa_operation_unref(operation);
pa_threaded_mainloop_unlock(m_mainLoop);
}
void PulseAudioEngine::setContextState(pa_context_state_t state)
{
if (m_contextState == state)
return;
m_contextState = state;
// update ready member as it depends on state
if (m_ready == (m_contextState == PA_CONTEXT_READY))
return;
m_ready = (m_contextState == PA_CONTEXT_READY);
emit contextStateChanged(m_contextState);
emit readyChanged(m_ready);
}
void PulseAudioEngine::setIgnoreMaxVolume(bool ignore)
{
if (ignore)
m_maximumVolume = PA_VOLUME_UI_MAX;
else
m_maximumVolume = pa_sw_volume_from_dB(0);
}