/* 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 * * 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 #include //#define PULSEAUDIO_ENGINE_DEBUG static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int isLast, void *userdata) { PulseAudioEngine *pulseEngine = static_cast(userdata); QMap 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(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(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(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"); 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 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(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); }