diff options
Diffstat (limited to 'src/multimedia/pulseaudio')
-rw-r--r-- | src/multimedia/pulseaudio/qaudioengine_pulse.cpp | 413 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qaudioengine_pulse_p.h | 40 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiodevice.cpp | 40 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiodevice_p.h | 42 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp | 62 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h | 47 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink.cpp | 518 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink_p.h | 71 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosource.cpp | 448 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosource_p.h | 60 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulsehelpers.cpp | 122 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulsehelpers_p.h | 111 |
12 files changed, 800 insertions, 1174 deletions
diff --git a/src/multimedia/pulseaudio/qaudioengine_pulse.cpp b/src/multimedia/pulseaudio/qaudioengine_pulse.cpp index 7b3cf57b1..5fac7234a 100644 --- a/src/multimedia/pulseaudio/qaudioengine_pulse.cpp +++ b/src/multimedia/pulseaudio/qaudioengine_pulse.cpp @@ -1,101 +1,139 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QtCore/qdebug.h> #include <qaudiodevice.h> +#include <QGuiApplication> +#include <QIcon> #include <QTimer> #include "qaudioengine_pulse_p.h" #include "qpulseaudiodevice_p.h" #include "qpulsehelpers_p.h" #include <sys/types.h> #include <unistd.h> +#include <mutex> // for lock_guard QT_BEGIN_NAMESPACE +template<typename Info> +static bool updateDevicesMap(QReadWriteLock &lock, QByteArray defaultDeviceId, + QMap<int, QAudioDevice> &devices, QAudioDevice::Mode mode, + const Info &info) +{ + QWriteLocker locker(&lock); + + bool isDefault = defaultDeviceId == info.name; + auto newDeviceInfo = std::make_unique<QPulseAudioDeviceInfo>(info.name, info.description, isDefault, mode); + newDeviceInfo->channelConfiguration = QPulseAudioInternal::channelConfigFromMap(info.channel_map); + newDeviceInfo->preferredFormat = QPulseAudioInternal::sampleSpecToAudioFormat(info.sample_spec); + newDeviceInfo->preferredFormat.setChannelConfig(newDeviceInfo->channelConfiguration); + + auto &device = devices[info.index]; + if (device.handle() && *newDeviceInfo == *device.handle()) + return false; + + device = newDeviceInfo.release()->create(); + return true; +} + +static bool updateDevicesMap(QReadWriteLock &lock, QByteArray defaultDeviceId, + QMap<int, QAudioDevice> &devices) +{ + QWriteLocker locker(&lock); + + bool result = false; + + for (QAudioDevice &device : devices) { + auto deviceInfo = device.handle(); + const auto isDefault = deviceInfo->id == defaultDeviceId; + if (deviceInfo->isDefault != isDefault) { + Q_ASSERT(dynamic_cast<const QPulseAudioDeviceInfo *>(deviceInfo)); + auto newDeviceInfo = std::make_unique<QPulseAudioDeviceInfo>( + *static_cast<const QPulseAudioDeviceInfo *>(deviceInfo)); + newDeviceInfo->isDefault = isDefault; + device = newDeviceInfo.release()->create(); + result = true; + } + } + + return result; +}; + static void serverInfoCallback(pa_context *context, const pa_server_info *info, void *userdata) { + using namespace Qt::Literals; + using namespace QPulseAudioInternal; + if (!info) { - qWarning() << QString::fromLatin1("Failed to get server information: %s").arg(QString::fromUtf8(pa_strerror(pa_context_errno(context)))); + qWarning() << "Failed to get server information:" << currentError(context); return; } -#ifdef DEBUG_PULSE - char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX]; - - pa_sample_spec_snprint(ss, sizeof(ss), &info->sample_spec); - pa_channel_map_snprint(cm, sizeof(cm), &info->channel_map); - - qDebug() << QString("User name: %1\n" - "Host Name: %2\n" - "Server Name: %3\n" - "Server Version: %4\n" - "Default Sample Specification: %5\n" - "Default Channel Map: %6\n" - "Default Sink: %7\n" - "Default Source: %8\n").arg( - info->user_name, - info->host_name, - info->server_name, - info->server_version, - ss, - cm, - info->default_sink_name, - info->default_source_name); -#endif + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) { + char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX]; + + pa_sample_spec_snprint(ss, sizeof(ss), &info->sample_spec); + pa_channel_map_snprint(cm, sizeof(cm), &info->channel_map); + + qCDebug(qLcPulseAudioEngine) + << QStringLiteral("User name: %1\n" + "Host Name: %2\n" + "Server Name: %3\n" + "Server Version: %4\n" + "Default Sample Specification: %5\n" + "Default Channel Map: %6\n" + "Default Sink: %7\n" + "Default Source: %8\n") + .arg(QString::fromUtf8(info->user_name), + QString::fromUtf8(info->host_name), + QString::fromUtf8(info->server_name), + QLatin1StringView(info->server_version), QLatin1StringView(ss), + QLatin1StringView(cm), QString::fromUtf8(info->default_sink_name), + QString::fromUtf8(info->default_source_name)); + } QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); - pulseEngine->m_serverLock.lockForWrite(); - pulseEngine->m_defaultSink = info->default_sink_name; - pulseEngine->m_defaultSource = info->default_source_name; - // ### ensure the QAudioDevices are updated if default changes and emit changed signal in the device manager - pulseEngine->m_serverLock.unlock(); + + bool defaultSinkChanged = false; + bool defaultSourceChanged = false; + + { + QWriteLocker locker(&pulseEngine->m_serverLock); + + if (pulseEngine->m_defaultSink != info->default_sink_name) { + pulseEngine->m_defaultSink = info->default_sink_name; + defaultSinkChanged = true; + } + + if (pulseEngine->m_defaultSource != info->default_source_name) { + pulseEngine->m_defaultSource = info->default_source_name; + defaultSourceChanged = true; + } + } + + if (defaultSinkChanged + && updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, + pulseEngine->m_sinks)) + emit pulseEngine->audioOutputsChanged(); + + if (defaultSourceChanged + && updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource, + pulseEngine->m_sources)) + emit pulseEngine->audioInputsChanged(); pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); } static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int isLast, void *userdata) { - QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + using namespace Qt::Literals; + using namespace QPulseAudioInternal; + + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine *>(userdata); if (isLast < 0) { - qWarning() << QString::fromLatin1("Failed to get sink information: %s").arg(QString::fromUtf8(pa_strerror(pa_context_errno(context)))); + qWarning() << "Failed to get sink information:" << currentError(context); return; } @@ -106,36 +144,32 @@ static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int Q_ASSERT(info); -#ifdef DEBUG_PULSE - 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"; - - qDebug() << QString("Sink #%1\n" - "\tState: %2\n" - "\tName: %3\n" - "\tDescription: %4\n" - ).arg(QString::number(info->index), - stateMap.value(info->state), - info->name, - info->description); -#endif - - QWriteLocker locker(&pulseEngine->m_sinkLock); - bool isDefault = pulseEngine->m_defaultSink == info->name; - auto *dinfo = new QPulseAudioDeviceInfo(info->name, info->description, isDefault, QAudioDevice::Output); - dinfo->channelMap = info->channel_map; - dinfo->channelConfiguration = QPulseAudioInternal::channelConfigFromMap(info->channel_map); - dinfo->preferredFormat = QPulseAudioInternal::sampleSpecToAudioFormat(info->sample_spec); - dinfo->preferredFormat.setChannelConfig(dinfo->channelConfiguration); - pulseEngine->m_sinks.insert(info->index, dinfo->create()); - emit pulseEngine->audioOutputsChanged(); + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) { + static const QMap<pa_sink_state, QString> stateMap{ + { PA_SINK_INVALID_STATE, u"n/a"_s }, { PA_SINK_RUNNING, u"RUNNING"_s }, + { PA_SINK_IDLE, u"IDLE"_s }, { PA_SINK_SUSPENDED, u"SUSPENDED"_s }, + { PA_SINK_UNLINKED, u"UNLINKED"_s }, + }; + + qCDebug(qLcPulseAudioEngine) + << QStringLiteral("Sink #%1\n" + "\tState: %2\n" + "\tName: %3\n" + "\tDescription: %4\n") + .arg(QString::number(info->index), stateMap.value(info->state), + QString::fromUtf8(info->name), + QString::fromUtf8(info->description)); + } + + if (updateDevicesMap(pulseEngine->m_sinkLock, pulseEngine->m_defaultSink, pulseEngine->m_sinks, + QAudioDevice::Output, *info)) + emit pulseEngine->audioOutputsChanged(); } static void sourceInfoCallback(pa_context *context, const pa_source_info *info, int isLast, void *userdata) { + using namespace Qt::Literals; + Q_UNUSED(context); QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); @@ -146,35 +180,30 @@ static void sourceInfoCallback(pa_context *context, const pa_source_info *info, Q_ASSERT(info); -#ifdef DEBUG_PULSE - QMap<pa_source_state, QString> stateMap; - stateMap[PA_SOURCE_INVALID_STATE] = "n/a"; - stateMap[PA_SOURCE_RUNNING] = "RUNNING"; - stateMap[PA_SOURCE_IDLE] = "IDLE"; - stateMap[PA_SOURCE_SUSPENDED] = "SUSPENDED"; - - qDebug() << QString("Source #%1\n" - "\tState: %2\n" - "\tName: %3\n" - "\tDescription: %4\n" - ).arg(QString::number(info->index), - stateMap.value(info->state), - info->name, - info->description); -#endif - - QWriteLocker locker(&pulseEngine->m_sourceLock); + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) { + static const QMap<pa_source_state, QString> stateMap{ + { PA_SOURCE_INVALID_STATE, u"n/a"_s }, { PA_SOURCE_RUNNING, u"RUNNING"_s }, + { PA_SOURCE_IDLE, u"IDLE"_s }, { PA_SOURCE_SUSPENDED, u"SUSPENDED"_s }, + { PA_SOURCE_UNLINKED, u"UNLINKED"_s }, + }; + + qCDebug(qLcPulseAudioEngine) + << QStringLiteral("Source #%1\n" + "\tState: %2\n" + "\tName: %3\n" + "\tDescription: %4\n") + .arg(QString::number(info->index), stateMap.value(info->state), + QString::fromUtf8(info->name), + QString::fromUtf8(info->description)); + } + // skip monitor channels if (info->monitor_of_sink != PA_INVALID_INDEX) return; - bool isDefault = pulseEngine->m_defaultSink == info->name; - auto *dinfo = new QPulseAudioDeviceInfo(info->name, info->description, isDefault, QAudioDevice::Input); - dinfo->channelMap = info->channel_map; - dinfo->channelConfiguration = QPulseAudioInternal::channelConfigFromMap(info->channel_map); - dinfo->preferredFormat = QPulseAudioInternal::sampleSpecToAudioFormat(info->sample_spec); - dinfo->preferredFormat.setChannelConfig(dinfo->channelConfiguration); - pulseEngine->m_sources.insert(info->index, dinfo->create()); - emit pulseEngine->audioInputsChanged(); + + if (updateDevicesMap(pulseEngine->m_sourceLock, pulseEngine->m_defaultSource, + pulseEngine->m_sources, QAudioDevice::Input, *info)) + emit pulseEngine->audioInputsChanged(); } static void event_cb(pa_context* context, pa_subscription_event_type_t t, uint32_t index, void* userdata) @@ -189,27 +218,23 @@ static void event_cb(pa_context* context, pa_subscription_event_type_t t, uint32 case PA_SUBSCRIPTION_EVENT_CHANGE: switch (facility) { case PA_SUBSCRIPTION_EVENT_SERVER: { - pa_operation *op = pa_context_get_server_info(context, serverInfoCallback, userdata); - if (op) - pa_operation_unref(op); - else - qWarning("PulseAudioService: failed to get server info"); + PAOperationUPtr op(pa_context_get_server_info(context, serverInfoCallback, userdata)); + if (!op) + qWarning() << "PulseAudioService: failed to get server info"; break; } case PA_SUBSCRIPTION_EVENT_SINK: { - pa_operation *op = pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata); - if (op) - pa_operation_unref(op); - else - qWarning("PulseAudioService: failed to get sink info"); + PAOperationUPtr op( + pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata)); + if (!op) + qWarning() << "PulseAudioService: failed to get sink info"; break; } case PA_SUBSCRIPTION_EVENT_SOURCE: { - pa_operation *op = pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata); - if (op) - pa_operation_unref(op); - else - qWarning("PulseAudioService: failed to get source info"); + PAOperationUPtr op(pa_context_get_source_info_by_index(context, index, + sourceInfoCallback, userdata)); + if (!op) + qWarning() << "PulseAudioService: failed to get source info"; break; } default: @@ -218,16 +243,16 @@ static void event_cb(pa_context* context, pa_subscription_event_type_t t, uint32 break; case PA_SUBSCRIPTION_EVENT_REMOVE: switch (facility) { - case PA_SUBSCRIPTION_EVENT_SINK: - pulseEngine->m_sinkLock.lockForWrite(); + case PA_SUBSCRIPTION_EVENT_SINK: { + QWriteLocker locker(&pulseEngine->m_sinkLock); pulseEngine->m_sinks.remove(index); - pulseEngine->m_sinkLock.unlock(); break; - case PA_SUBSCRIPTION_EVENT_SOURCE: - pulseEngine->m_sourceLock.lockForWrite(); + } + case PA_SUBSCRIPTION_EVENT_SOURCE: { + QWriteLocker locker(&pulseEngine->m_sourceLock); pulseEngine->m_sources.remove(index); - pulseEngine->m_sourceLock.unlock(); break; + } default: break; } @@ -240,9 +265,10 @@ static void event_cb(pa_context* context, pa_subscription_event_type_t t, uint32 static void contextStateCallbackInit(pa_context *context, void *userdata) { Q_UNUSED(context); -#ifdef DEBUG_PULSE - qDebug() << QPulseAudioInternal::stateToQString(pa_context_get_state(context)); -#endif + + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) + qCDebug(qLcPulseAudioEngine) << pa_context_get_state(context); + QPulseAudioEngine *pulseEngine = reinterpret_cast<QPulseAudioEngine*>(userdata); pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); } @@ -252,9 +278,8 @@ static void contextStateCallback(pa_context *c, void *userdata) QPulseAudioEngine *self = reinterpret_cast<QPulseAudioEngine*>(userdata); pa_context_state_t state = pa_context_get_state(c); -#ifdef DEBUG_PULSE - qDebug() << QPulseAudioInternal::stateToQString(state); -#endif + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) + qCDebug(qLcPulseAudioEngine) << state; if (state == PA_CONTEXT_FAILED) QMetaObject::invokeMethod(self, "onContextFailed", Qt::QueuedConnection); @@ -279,17 +304,18 @@ QPulseAudioEngine::~QPulseAudioEngine() void QPulseAudioEngine::prepare() { + using namespace QPulseAudioInternal; bool keepGoing = true; bool ok = true; m_mainLoop = pa_threaded_mainloop_new(); if (m_mainLoop == nullptr) { - qWarning("PulseAudioService: unable to create pulseaudio mainloop"); + qWarning() << "PulseAudioService: unable to create pulseaudio mainloop"; return; } if (pa_threaded_mainloop_start(m_mainLoop) != 0) { - qWarning("PulseAudioService: unable to start pulseaudio mainloop"); + qWarning() << "PulseAudioService: unable to start pulseaudio mainloop"; pa_threaded_mainloop_free(m_mainLoop); m_mainLoop = nullptr; return; @@ -299,10 +325,19 @@ void QPulseAudioEngine::prepare() lock(); - m_context = pa_context_new(m_mainLoopApi, QString(QLatin1String("QtPulseAudio:%1")).arg(::getpid()).toLatin1().constData()); + pa_proplist *proplist = pa_proplist_new(); + if (!QGuiApplication::applicationDisplayName().isEmpty()) + pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, qUtf8Printable(QGuiApplication::applicationDisplayName())); + if (!QGuiApplication::desktopFileName().isEmpty()) + pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, qUtf8Printable(QGuiApplication::desktopFileName())); + if (const QString windowIconName = QGuiApplication::windowIcon().name(); !windowIconName.isEmpty()) + pa_proplist_sets(proplist, PA_PROP_WINDOW_ICON_NAME, qUtf8Printable(windowIconName)); + + m_context = pa_context_new_with_proplist(m_mainLoopApi, nullptr, proplist); + pa_proplist_free(proplist); if (m_context == nullptr) { - qWarning("PulseAudioService: Unable to create new pulseaudio context"); + qWarning() << "PulseAudioService: Unable to create new pulseaudio context"; pa_threaded_mainloop_unlock(m_mainLoop); pa_threaded_mainloop_free(m_mainLoop); m_mainLoop = nullptr; @@ -312,8 +347,8 @@ void QPulseAudioEngine::prepare() pa_context_set_state_callback(m_context, contextStateCallbackInit, this); - if (pa_context_connect(m_context, nullptr, (pa_context_flags_t)0, nullptr) < 0) { - qWarning("PulseAudioService: pa_context_connect() failed"); + if (pa_context_connect(m_context, nullptr, static_cast<pa_context_flags_t>(0), nullptr) < 0) { + qWarning() << "PulseAudioService: pa_context_connect() failed"; pa_context_unref(m_context); pa_threaded_mainloop_unlock(m_mainLoop); pa_threaded_mainloop_free(m_mainLoop); @@ -332,9 +367,7 @@ void QPulseAudioEngine::prepare() break; case PA_CONTEXT_READY: -#ifdef DEBUG_PULSE - qDebug("Connection established."); -#endif + qCDebug(qLcPulseAudioEngine) << "Connection established."; keepGoing = false; break; @@ -346,8 +379,8 @@ void QPulseAudioEngine::prepare() case PA_CONTEXT_FAILED: default: - qCritical() << QString::fromLatin1("PulseAudioService: Connection failure: %1") - .arg(QString::fromUtf8(pa_strerror(pa_context_errno(m_context)))); + qCritical() << "PulseAudioService: Connection failure:" + << currentError(m_context); keepGoing = false; ok = false; } @@ -360,15 +393,13 @@ void QPulseAudioEngine::prepare() pa_context_set_state_callback(m_context, contextStateCallback, this); pa_context_set_subscribe_callback(m_context, event_cb, this); - pa_operation *op = pa_context_subscribe(m_context, - pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK | - PA_SUBSCRIPTION_MASK_SOURCE | - PA_SUBSCRIPTION_MASK_SERVER), - nullptr, nullptr); - if (op) - pa_operation_unref(op); - else - qWarning("PulseAudioService: failed to subscribe to context notifications"); + PAOperationUPtr op(pa_context_subscribe( + m_context, + pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE + | PA_SUBSCRIPTION_MASK_SERVER), + nullptr, nullptr)); + if (!op) + qWarning() << "PulseAudioService: failed to subscribe to context notifications"; } else { pa_context_unref(m_context); m_context = nullptr; @@ -408,39 +439,34 @@ void QPulseAudioEngine::release() void QPulseAudioEngine::updateDevices() { - lock(); + std::lock_guard lock(*this); // Get default input and output devices - pa_operation *operation = pa_context_get_server_info(m_context, serverInfoCallback, this); + PAOperationUPtr operation(pa_context_get_server_info(m_context, serverInfoCallback, this)); if (operation) { - while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m_mainLoop); - pa_operation_unref(operation); } else { - qWarning("PulseAudioService: failed to get server info"); + qWarning() << "PulseAudioService: failed to get server info"; } // Get output devices - operation = pa_context_get_sink_info_list(m_context, sinkInfoCallback, this); + operation.reset(pa_context_get_sink_info_list(m_context, sinkInfoCallback, this)); if (operation) { - while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m_mainLoop); - pa_operation_unref(operation); } else { - qWarning("PulseAudioService: failed to get sink info"); + qWarning() << "PulseAudioService: failed to get sink info"; } // Get input devices - operation = pa_context_get_source_info_list(m_context, sourceInfoCallback, this); + operation.reset(pa_context_get_source_info_list(m_context, sourceInfoCallback, this)); if (operation) { - while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m_mainLoop); - pa_operation_unref(operation); } else { - qWarning("PulseAudioService: failed to get source info"); + qWarning() << "PulseAudioService: failed to get source info"; } - - unlock(); } void QPulseAudioEngine::onContextFailed() @@ -451,7 +477,7 @@ void QPulseAudioEngine::onContextFailed() release(); // Try to reconnect later - QTimer::singleShot(3000, this, SLOT(prepare())); + QTimer::singleShot(3000, this, &QPulseAudioEngine::prepare); } QPulseAudioEngine *QPulseAudioEngine::instance() @@ -461,24 +487,17 @@ QPulseAudioEngine *QPulseAudioEngine::instance() QList<QAudioDevice> QPulseAudioEngine::availableDevices(QAudioDevice::Mode mode) const { - QList<QAudioDevice> devices; - QByteArray defaultDevice; - - m_serverLock.lockForRead(); - if (mode == QAudioDevice::Output) { QReadLocker locker(&m_sinkLock); - devices = m_sinks.values(); - defaultDevice = m_defaultSink; - } else { - QReadLocker locker(&m_sourceLock); - devices = m_sources.values(); - defaultDevice = m_defaultSource; + return m_sinks.values(); } - m_serverLock.unlock(); + if (mode == QAudioDevice::Input) { + QReadLocker locker(&m_sourceLock); + return m_sources.values(); + } - return devices; + return {}; } QByteArray QPulseAudioEngine::defaultDevice(QAudioDevice::Mode mode) const diff --git a/src/multimedia/pulseaudio/qaudioengine_pulse_p.h b/src/multimedia/pulseaudio/qaudioengine_pulse_p.h index 79a3d8d3d..2ed1fa0b1 100644 --- a/src/multimedia/pulseaudio/qaudioengine_pulse_p.h +++ b/src/multimedia/pulseaudio/qaudioengine_pulse_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QPULSEAUDIOENGINE_H #define QPULSEAUDIOENGINE_H diff --git a/src/multimedia/pulseaudio/qpulseaudiodevice.cpp b/src/multimedia/pulseaudio/qpulseaudiodevice.cpp index c25467a8f..487b88f6d 100644 --- a/src/multimedia/pulseaudio/qpulseaudiodevice.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiodevice.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpulseaudiodevice_p.h" #include "qaudioengine_pulse_p.h" diff --git a/src/multimedia/pulseaudio/qpulseaudiodevice_p.h b/src/multimedia/pulseaudio/qpulseaudiodevice_p.h index 0175efca1..b44c71e0d 100644 --- a/src/multimedia/pulseaudio/qpulseaudiodevice_p.h +++ b/src/multimedia/pulseaudio/qpulseaudiodevice_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QAUDIODEVICEINFOPULSE_H #define QAUDIODEVICEINFOPULSE_H @@ -69,8 +33,6 @@ class QPulseAudioDeviceInfo : public QAudioDevicePrivate public: QPulseAudioDeviceInfo(const char *device, const char *description, bool isDefault, QAudioDevice::Mode mode); ~QPulseAudioDeviceInfo() {} - - pa_channel_map channelMap; }; QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp b/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp index 75058a887..4deee6033 100644 --- a/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpulseaudiomediadevices_p.h" #include "qmediadevices.h" @@ -52,6 +16,13 @@ QPulseAudioMediaDevices::QPulseAudioMediaDevices() : QPlatformMediaDevices() { pulseEngine = new QPulseAudioEngine(); + + // TODO: it might make sense to connect device changing signals + // to each added QMediaDevices + QObject::connect(pulseEngine, &QPulseAudioEngine::audioInputsChanged, + this, &QPulseAudioMediaDevices::audioInputsChanged, Qt::DirectConnection); + QObject::connect(pulseEngine, &QPulseAudioEngine::audioOutputsChanged, + this, &QPulseAudioMediaDevices::audioOutputsChanged, Qt::DirectConnection); } QPulseAudioMediaDevices::~QPulseAudioMediaDevices() @@ -69,19 +40,16 @@ QList<QAudioDevice> QPulseAudioMediaDevices::audioOutputs() const return pulseEngine->availableDevices(QAudioDevice::Output); } -QList<QCameraDevice> QPulseAudioMediaDevices::videoInputs() const -{ - return {}; -} - -QPlatformAudioSource *QPulseAudioMediaDevices::createAudioSource(const QAudioDevice &deviceInfo) +QPlatformAudioSource *QPulseAudioMediaDevices::createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) { - return new QPulseAudioSource(deviceInfo.id()); + return new QPulseAudioSource(deviceInfo.id(), parent); } -QPlatformAudioSink *QPulseAudioMediaDevices::createAudioSink(const QAudioDevice &deviceInfo) +QPlatformAudioSink *QPulseAudioMediaDevices::createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) { - return new QPulseAudioSink(deviceInfo.id()); + return new QPulseAudioSink(deviceInfo.id(), parent); } QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h b/src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h index dd811f550..094dc3907 100644 --- a/src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h +++ b/src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QPULSEAUDIOMEDIADEVICES_H #define QPULSEAUDIOMEDIADEVICES_H @@ -67,9 +31,10 @@ public: QList<QAudioDevice> audioInputs() const override; QList<QAudioDevice> audioOutputs() const override; - QList<QCameraDevice> videoInputs() const override; - QPlatformAudioSource *createAudioSource(const QAudioDevice &deviceInfo) override; - QPlatformAudioSink *createAudioSink(const QAudioDevice &deviceInfo) override; + QPlatformAudioSource *createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) override; + QPlatformAudioSink *createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) override; private: QPulseAudioEngine *pulseEngine; diff --git a/src/multimedia/pulseaudio/qpulseaudiosink.cpp b/src/multimedia/pulseaudio/qpulseaudiosink.cpp index b3e39d746..610677eeb 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosink.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiosink.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QtCore/qcoreapplication.h> #include <QtCore/qdebug.h> @@ -43,21 +7,20 @@ #include <private/qaudiohelpers_p.h> #include "qpulseaudiosink_p.h" -#include "qpulseaudiodevice_p.h" #include "qaudioengine_pulse_p.h" #include "qpulsehelpers_p.h" #include <sys/types.h> #include <unistd.h> - -Q_DECLARE_LOGGING_CATEGORY(qLcPulseAudio) +#include <mutex> // for std::lock_guard QT_BEGIN_NAMESPACE -const int PeriodTimeMs = 20; +static constexpr uint SinkPeriodTimeMs = 20; +static constexpr uint DefaultBufferLengthMs = 100; #define LOW_LATENCY_CATEGORY_NAME "game" -static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) +static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) { Q_UNUSED(stream); Q_UNUSED(userdata); @@ -72,17 +35,19 @@ static void outputStreamStateCallback(pa_stream *stream, void *userdata) pa_stream_state_t state = pa_stream_get_state(stream); qCDebug(qLcPulseAudioOut) << "Stream state callback:" << state; switch (state) { - case PA_STREAM_CREATING: - case PA_STREAM_READY: - case PA_STREAM_TERMINATED: - break; - - case PA_STREAM_FAILED: - default: - qWarning() << QString::fromLatin1("Stream error: %1").arg(QString::fromUtf8(pa_strerror(pa_context_errno(pa_stream_get_context(stream))))); - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); - break; + case PA_STREAM_CREATING: + case PA_STREAM_READY: + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_FAILED: + default: + qWarning() << QStringLiteral("Stream error: %1") + .arg(QString::fromUtf8(pa_strerror( + pa_context_errno(pa_stream_get_context(stream))))); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + break; } } @@ -133,90 +98,73 @@ static void outputStreamDrainComplete(pa_stream *stream, int success, void *user { Q_UNUSED(stream); - qCDebug(qLcPulseAudioOut) << "Stream drained:" << bool(success) << userdata; - if (success && userdata) + qCDebug(qLcPulseAudioOut) << "Stream drained:" << static_cast<bool>(success) << userdata; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + + if (userdata && success) static_cast<QPulseAudioSink *>(userdata)->streamDrainedCallback(); } +static void outputStreamFlushComplete(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + + qCDebug(qLcPulseAudioOut) << "Stream flushed:" << static_cast<bool>(success) << userdata; +} + static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void *userdata) { Q_UNUSED(stream); Q_UNUSED(success); Q_UNUSED(userdata); - qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:" << bool(success); + qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:" << static_cast<bool>(success); } - -QPulseAudioSink::QPulseAudioSink(const QByteArray &device) - : m_device(device) +QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent) + : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this) { } QPulseAudioSink::~QPulseAudioSink() { - close(); - QCoreApplication::processEvents(); -} - -void QPulseAudioSink::setError(QAudio::Error error) -{ - if (m_errorState == error) - return; - - m_errorState = error; - emit errorChanged(error); + if (auto notifier = m_stateMachine.stop()) + close(); } QAudio::Error QPulseAudioSink::error() const { - return m_errorState; -} - -void QPulseAudioSink::setState(QAudio::State state) -{ - if (m_deviceState == state) - return; - - m_deviceState = state; - emit stateChanged(state); + return m_stateMachine.error(); } QAudio::State QPulseAudioSink::state() const { - return m_deviceState; + return m_stateMachine.state(); } void QPulseAudioSink::streamUnderflowCallback() { - if (m_audioSource && m_audioSource->atEnd()) { + bool atEnd = m_audioSource && m_audioSource->atEnd(); + if (atEnd && m_stateMachine.state() != QAudio::StoppedState) { qCDebug(qLcPulseAudioOut) << "Draining stream at end of buffer"; - pa_operation *o = pa_stream_drain(m_stream, outputStreamDrainComplete, this); - if (o) - pa_operation_unref(o); - } else if (m_deviceState != QAudio::IdleState && !m_resuming) { - setError(QAudio::UnderrunError); - setState(QAudio::IdleState); + exchangeDrainOperation(pa_stream_drain(m_stream, outputStreamDrainComplete, this)); } + + m_stateMachine.updateActiveOrIdle( + false, (m_pullMode && atEnd) ? QAudio::NoError : QAudio::UnderrunError); } void QPulseAudioSink::streamDrainedCallback() { - setError(QAudio::NoError); - setState(QAudio::IdleState); + if (!exchangeDrainOperation(nullptr)) + return; } void QPulseAudioSink::start(QIODevice *device) { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - // Handle change of mode - if (m_audioSource && !m_pullMode) - delete m_audioSource; - m_audioSource = nullptr; - - close(); + reset(); m_pullMode = true; m_audioSource = device; @@ -230,27 +178,29 @@ void QPulseAudioSink::start(QIODevice *device) gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; - connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startReading); - setState(QAudio::ActiveState); + connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startPulling); + + m_stateMachine.start(); } -void QPulseAudioSink::startReading() +void QPulseAudioSink::startPulling() { - if (!m_tickTimer.isActive()) - m_tickTimer.start(m_periodTime, this); + Q_ASSERT(m_pullMode); + if (m_tickTimer.isActive()) + return; + + m_tickTimer.start(m_pullingPeriodTime, this); } -QIODevice *QPulseAudioSink::start() +void QPulseAudioSink::stopTimer() { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - // Handle change of mode - if (m_audioSource && !m_pullMode) - delete m_audioSource; - m_audioSource = nullptr; + if (m_tickTimer.isActive()) + m_tickTimer.stop(); +} - close(); +QIODevice *QPulseAudioSink::start() +{ + reset(); m_pullMode = false; @@ -258,13 +208,13 @@ QIODevice *QPulseAudioSink::start() return nullptr; m_audioSource = new PulseOutputPrivate(this); - m_audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + m_audioSource->open(QIODevice::WriteOnly | QIODevice::Unbuffered); // ensure we only process timing infos that are up to date gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; - setState(QAudio::IdleState); + m_stateMachine.start(false); return m_audioSource; } @@ -276,10 +226,9 @@ bool QPulseAudioSink::open() QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { - setError(QAudio::FatalError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + if (!pulseEngine->context() + || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + m_stateMachine.stopOrUpdateError(QAudio::FatalError); return false; } @@ -288,9 +237,7 @@ bool QPulseAudioSink::open() Q_ASSERT(spec.channels == channel_map.channels); if (!pa_sample_spec_valid(&spec)) { - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -298,11 +245,12 @@ bool QPulseAudioSink::open() m_totalTimeValue = 0; if (m_streamName.isNull()) - m_streamName = QString(QLatin1String("QtmPulseStream-%1-%2")).arg(::getpid()).arg(quintptr(this)).toUtf8(); + m_streamName = + QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8(); if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { qCDebug(qLcPulseAudioOut) << "Opening stream with."; - qCDebug(qLcPulseAudioOut) << "\tFormat: " << QPulseAudioInternal::sampleFormatToQString(spec.format); + qCDebug(qLcPulseAudioOut) << "\tFormat: " << spec.format; qCDebug(qLcPulseAudioOut) << "\tRate: " << spec.rate; qCDebug(qLcPulseAudioOut) << "\tChannels: " << spec.channels; qCDebug(qLcPulseAudioOut) << "\tFrame size: " << pa_frame_size(&spec); @@ -310,7 +258,6 @@ bool QPulseAudioSink::open() pulseEngine->lock(); - pa_proplist *propList = pa_proplist_new(); #if 0 qint64 bytesPerSecond = m_format.sampleRate() * m_format.bytesPerFrame(); @@ -332,18 +279,18 @@ bool QPulseAudioSink::open() pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, r); #endif - m_stream = pa_stream_new_with_proplist(pulseEngine->context(), m_streamName.constData(), &m_spec, &channel_map, propList); + m_stream = pa_stream_new_with_proplist(pulseEngine->context(), m_streamName.constData(), + &m_spec, &channel_map, propList); + pa_proplist_free(propList); + if (!m_stream) { qCWarning(qLcPulseAudioOut) << "QAudioSink: pa_stream_new_with_proplist() failed!"; pulseEngine->unlock(); - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } - pa_proplist_free(propList); - pa_stream_set_state_callback(m_stream, outputStreamStateCallback, this); pa_stream_set_write_callback(m_stream, outputStreamWriteCallback, this); @@ -352,21 +299,26 @@ bool QPulseAudioSink::open() pa_stream_set_latency_update_callback(m_stream, outputStreamLatencyCallback, this); pa_buffer_attr requestedBuffer; - requestedBuffer.fragsize = (uint32_t)-1; - requestedBuffer.maxlength = (uint32_t)-1; - requestedBuffer.minreq = (uint32_t)-1; - requestedBuffer.prebuf = (uint32_t)-1; - requestedBuffer.tlength = m_bufferSize; - - pa_stream_flags flags = pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_ADJUST_LATENCY); - if (pa_stream_connect_playback(m_stream, m_device.data(), (m_bufferSize > 0) ? &requestedBuffer : nullptr, flags, nullptr, nullptr) < 0) { + // Request a target buffer size + auto targetBufferSize = m_userBufferSize ? *m_userBufferSize : defaultBufferSize(); + requestedBuffer.tlength = + targetBufferSize ? static_cast<uint32_t>(targetBufferSize) : static_cast<uint32_t>(-1); + // Rest should be determined by PulseAudio + requestedBuffer.fragsize = static_cast<uint32_t>(-1); + requestedBuffer.maxlength = static_cast<uint32_t>(-1); + requestedBuffer.minreq = static_cast<uint32_t>(-1); + requestedBuffer.prebuf = static_cast<uint32_t>(-1); + + pa_stream_flags flags = + pa_stream_flags(PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY); + if (pa_stream_connect_playback(m_stream, m_device.data(), &requestedBuffer, flags, nullptr, + nullptr) + < 0) { qCWarning(qLcPulseAudioOut) << "pa_stream_connect_playback() failed!"; pa_stream_unref(m_stream); m_stream = nullptr; pulseEngine->unlock(); - setError(QAudio::OpenError); - setState(QAudio::StoppedState); - emit stateChanged(m_deviceState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -374,20 +326,25 @@ bool QPulseAudioSink::open() pa_threaded_mainloop_wait(pulseEngine->mainloop()); const pa_buffer_attr *buffer = pa_stream_get_buffer_attr(m_stream); - m_periodTime = PeriodTimeMs; - m_periodSize = pa_usec_to_bytes(m_periodTime * 1000, &m_spec); m_bufferSize = buffer->tlength; - m_maxBufferSize = buffer->maxlength; - m_audioBuffer = new char[m_maxBufferSize]; + + if (m_pullMode) { + // Adjust period time to reduce chance of it being higher than amount of bytes requested by + // PulseAudio server + m_pullingPeriodTime = + qMin(SinkPeriodTimeMs, pa_bytes_to_usec(m_bufferSize, &m_spec) / 1000 / 2); + m_pullingPeriodSize = pa_usec_to_bytes(m_pullingPeriodTime * 1000, &m_spec); + } + + m_audioBuffer.resize(buffer->maxlength); const qint64 streamSize = m_audioSource ? m_audioSource->size() : 0; if (m_pullMode && streamSize > 0 && static_cast<qint64>(buffer->prebuf) > streamSize) { pa_buffer_attr newBufferAttr; newBufferAttr = *buffer; newBufferAttr.prebuf = streamSize; - pa_operation *o = pa_stream_set_buffer_attr(m_stream, &newBufferAttr, streamAdjustPrebufferCallback, nullptr); - if (o) - pa_operation_unref(o); + PAOperationUPtr(pa_stream_set_buffer_attr(m_stream, &newBufferAttr, + streamAdjustPrebufferCallback, nullptr)); } if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { @@ -401,11 +358,13 @@ bool QPulseAudioSink::open() pulseEngine->unlock(); - connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSink::onPulseContextFailed); + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSink::onPulseContextFailed); m_opened = true; - startReading(); + if (m_pullMode) + startPulling(); m_elapsedTimeOffset = 0; @@ -417,12 +376,12 @@ void QPulseAudioSink::close() if (!m_opened) return; - m_tickTimer.stop(); + stopTimer(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); if (m_stream) { - pulseEngine->lock(); + std::lock_guard lock(*pulseEngine); pa_stream_set_state_callback(m_stream, nullptr, nullptr); pa_stream_set_write_callback(m_stream, nullptr, nullptr); @@ -430,37 +389,37 @@ void QPulseAudioSink::close() pa_stream_set_overflow_callback(m_stream, nullptr, nullptr); pa_stream_set_latency_update_callback(m_stream, nullptr, nullptr); - pa_operation *o = pa_stream_drain(m_stream, outputStreamDrainComplete, nullptr); - if (o) - pa_operation_unref(o); + if (auto prevOp = exchangeDrainOperation(nullptr)) + // cancel draining operation to prevent calling draining callback after closing. + pa_operation_cancel(prevOp.get()); + + PAOperationUPtr operation(pa_stream_flush(m_stream, outputStreamFlushComplete, nullptr)); pa_stream_disconnect(m_stream); pa_stream_unref(m_stream); m_stream = nullptr; - - pulseEngine->unlock(); } - disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSink::onPulseContextFailed); + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSink::onPulseContextFailed); if (m_audioSource) { if (m_pullMode) { disconnect(m_audioSource, &QIODevice::readyRead, this, nullptr); + m_audioSource->reset(); } else { delete m_audioSource; m_audioSource = nullptr; } } + m_opened = false; - if (m_audioBuffer) { - delete[] m_audioBuffer; - m_audioBuffer = nullptr; - } + m_audioBuffer.clear(); } void QPulseAudioSink::timerEvent(QTimerEvent *event) { - if (event->timerId() == m_tickTimer.timerId()) + if (event->timerId() == m_tickTimer.timerId() && m_pullMode) userFeed(); QPlatformAudioSink::timerEvent(event); @@ -468,54 +427,48 @@ void QPulseAudioSink::timerEvent(QTimerEvent *event) void QPulseAudioSink::userFeed() { - if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState) + int writableSize = bytesFree(); + + if (writableSize == 0) { + // PulseAudio server doesn't want any more data + m_stateMachine.activateFromIdle(); return; + } - m_resuming = false; + // Write up to writableSize + const int inputSize = + std::min({ m_pullingPeriodSize, static_cast<int>(m_audioBuffer.size()), writableSize }); - if (m_pullMode) { - setError(QAudio::NoError); - setState(QAudio::ActiveState); - int writableSize = bytesFree(); - int chunks = writableSize / m_periodSize; - if (chunks == 0) - return; - - int input = m_periodSize; // always request 1 chunk of data from user - if (input > m_maxBufferSize) - input = m_maxBufferSize; - - Q_ASSERT(m_audioBuffer); - int audioBytesPulled = m_audioSource->read(m_audioBuffer, input); - Q_ASSERT(audioBytesPulled <= input); - if (audioBytesPulled > 0) { - if (audioBytesPulled > input) { - qCWarning(qLcPulseAudioOut) << "Invalid audio data size provided by pull source:" - << audioBytesPulled << "should be less than" << input; - audioBytesPulled = input; - } - qint64 bytesWritten = write(m_audioBuffer, audioBytesPulled); - Q_ASSERT(bytesWritten == audioBytesPulled); //unfinished write should not happen since the data provided is less than writableSize - Q_UNUSED(bytesWritten); - - if (chunks > 1) { - // PulseAudio needs more data. Ask for it immediately. - QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); - } - } else if (audioBytesPulled == 0) { - m_tickTimer.stop(); - qCDebug(qLcPulseAudioOut) << "No more data available, source is done:" << m_audioSource->atEnd(); - setError(m_audioSource->atEnd() ? QAudio::NoError : QAudio::UnderrunError); - setState(QAudio::IdleState); + Q_ASSERT(!m_audioBuffer.empty()); + int audioBytesPulled = m_audioSource->read(m_audioBuffer.data(), inputSize); + Q_ASSERT(audioBytesPulled <= inputSize); + + if (audioBytesPulled > 0) { + if (audioBytesPulled > inputSize) { + qCWarning(qLcPulseAudioOut) + << "Invalid audio data size provided by pull source:" << audioBytesPulled + << "should be less than" << inputSize; + audioBytesPulled = inputSize; } + auto bytesWritten = write(m_audioBuffer.data(), audioBytesPulled); + if (bytesWritten != audioBytesPulled) + qWarning() << "Unfinished write:" << bytesWritten << "vs" << audioBytesPulled; + + m_stateMachine.activateFromIdle(); + + if (inputSize < writableSize) // PulseAudio needs more data. + QMetaObject::invokeMethod(this, &QPulseAudioSink::userFeed, Qt::QueuedConnection); + } else if (audioBytesPulled == 0) { + stopTimer(); + const auto atEnd = m_audioSource->atEnd(); + qCDebug(qLcPulseAudioOut) << "No more data available, source is done:" << atEnd; } - - if (m_deviceState != QAudio::ActiveState) - return; } qint64 QPulseAudioSink::write(const char *data, qint64 len) { + using namespace QPulseAudioInternal; + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); pulseEngine->lock(); @@ -524,9 +477,10 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) void *dest = nullptr; if (pa_stream_begin_write(m_stream, &dest, &nbytes) < 0) { - qCWarning(qLcPulseAudioOut) << "pa_stream_begin_write error:" - << pa_strerror(pa_context_errno(pulseEngine->context())); - setError(QAudio::IOError); + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_begin_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } @@ -543,65 +497,76 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) data = reinterpret_cast<char *>(dest); if ((pa_stream_write(m_stream, data, len, nullptr, 0, PA_SEEK_RELATIVE)) < 0) { - qCWarning(qLcPulseAudioOut) << "pa_stream_write error:" - << pa_strerror(pa_context_errno(pulseEngine->context())); - setError(QAudio::IOError); + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } pulseEngine->unlock(); m_totalTimeValue += len; - setError(QAudio::NoError); - setState(QAudio::ActiveState); - + m_stateMachine.updateActiveOrIdle(true); return len; } void QPulseAudioSink::stop() { - if (m_deviceState == QAudio::StoppedState) - return; + if (auto notifier = m_stateMachine.stop()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + std::lock_guard lock(*pulseEngine); - close(); + if (auto prevOp = exchangeDrainOperation(nullptr)) + // cancel the draining callback that is not relevant already + pa_operation_cancel(prevOp.get()); - setError(QAudio::NoError); - setState(QAudio::StoppedState); + PAOperationUPtr drainOp(pa_stream_drain(m_stream, outputStreamDrainComplete, nullptr)); + pulseEngine->wait(drainOp.get()); + } + + close(); + } } qsizetype QPulseAudioSink::bytesFree() const { - if (m_deviceState != QAudio::ActiveState && m_deviceState != QAudio::IdleState) + if (!m_stateMachine.isActiveOrIdle()) return 0; - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pulseEngine->lock(); - int writableSize = pa_stream_writable_size(m_stream); - pulseEngine->unlock(); - return writableSize; + std::lock_guard lock(*QPulseAudioEngine::instance()); + return pa_stream_writable_size(m_stream); } void QPulseAudioSink::setBufferSize(qsizetype value) { - m_bufferSize = value; + m_userBufferSize = value; } qsizetype QPulseAudioSink::bufferSize() const { - return m_bufferSize; + if (m_bufferSize) + return m_bufferSize; + + if (m_userBufferSize) + return *m_userBufferSize; + + return defaultBufferSize(); } static qint64 operator-(timeval t1, timeval t2) { constexpr qint64 secsToUSecs = 1000000; - return (t1.tv_sec - t2.tv_sec)*secsToUSecs + (t1.tv_usec - t2.tv_usec); + return (t1.tv_sec - t2.tv_sec) * secsToUSecs + (t1.tv_usec - t2.tv_usec); } qint64 QPulseAudioSink::processedUSecs() const { - if (!m_stream || m_deviceState == QAudio::StoppedState) + const auto state = this->state(); + if (!m_stream || state == QAudio::StoppedState) return 0; - if (m_deviceState == QAudio::SuspendedState) + if (state == QAudio::SuspendedState) return lastProcessedUSecs; auto info = pa_stream_get_timing_info(m_stream); @@ -612,13 +577,16 @@ qint64 QPulseAudioSink::processedUSecs() const if (info->timestamp - lastTimingInfo > 0) { lastTimingInfo.tv_sec = info->timestamp.tv_sec; lastTimingInfo.tv_usec = info->timestamp.tv_usec; - averageLatency = 0; // also use that as long as we don't have valid data from the timing info + averageLatency = + 0; // also use that as long as we don't have valid data from the timing info // Only use timing values when playing, otherwise the latency numbers can be way off - if (info->since_underrun >= 0 && pa_bytes_to_usec(info->since_underrun, &m_spec) > info->sink_usec) { + if (info->since_underrun >= 0 + && pa_bytes_to_usec(info->since_underrun, &m_spec) > info->sink_usec) { latencyList.append(info->sink_usec); // Average over the last X timing infos to keep numbers more stable. - // 10 seems to be a decent number that keeps values relatively stable but doesn't make the list too big + // 10 seems to be a decent number that keeps values relatively stable but doesn't make + // the list too big const int latencyListMaxSize = 10; if (latencyList.size() > latencyListMaxSize) latencyList.pop_front(); @@ -631,7 +599,8 @@ qint64 QPulseAudioSink::processedUSecs() const } const qint64 usecsRead = info->read_index < 0 ? 0 : pa_bytes_to_usec(info->read_index, &m_spec); - const qint64 usecsWritten = info->write_index < 0 ? 0 : pa_bytes_to_usec(info->write_index, &m_spec); + const qint64 usecsWritten = + info->write_index < 0 ? 0 : pa_bytes_to_usec(info->write_index, &m_spec); // processed data is the amount read by the server minus its latency qint64 usecs = usecsRead - averageLatency; @@ -659,27 +628,22 @@ qint64 QPulseAudioSink::processedUSecs() const void QPulseAudioSink::resume() { - if (m_deviceState == QAudio::SuspendedState) { - m_resuming = true; - - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - - pulseEngine->lock(); - - pa_operation *operation = pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + if (auto notifier = m_stateMachine.resume()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - operation = pa_stream_trigger(m_stream, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + std::lock_guard lock(*pulseEngine); - pulseEngine->unlock(); + PAOperationUPtr operation( + pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); - m_tickTimer.start(m_periodTime, this); + operation.reset(pa_stream_trigger(m_stream, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } - setState(m_pullMode ? QAudio::ActiveState : QAudio::IdleState); - setError(QAudio::NoError); + if (m_pullMode) + startPulling(); } } @@ -695,33 +659,28 @@ QAudioFormat QPulseAudioSink::format() const void QPulseAudioSink::suspend() { - if (m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState) { - setError(QAudio::NoError); - setState(QAudio::SuspendedState); - - m_tickTimer.stop(); + if (auto notifier = m_stateMachine.suspend()) { + stopTimer(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_operation *operation; - - pulseEngine->lock(); - operation = pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + std::lock_guard lock(*pulseEngine); - pulseEngine->unlock(); + PAOperationUPtr operation( + pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); } } void QPulseAudioSink::reset() { - stop(); + if (auto notifier = m_stateMachine.stopOrUpdateError()) + close(); } PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio) { - m_audioDevice = qobject_cast<QPulseAudioSink*>(audio); + m_audioDevice = qobject_cast<QPulseAudioSink *>(audio); } qint64 PulseOutputPrivate::readData(char *data, qint64 len) @@ -736,13 +695,13 @@ qint64 PulseOutputPrivate::writeData(const char *data, qint64 len) { qint64 written = 0; - if ((m_audioDevice->m_deviceState == QAudio::ActiveState - || m_audioDevice->m_deviceState == QAudio::IdleState)) { - while(written < len) { - int chunk = m_audioDevice->write(data+written, (len-written)); + const auto state = m_audioDevice->state(); + if (state == QAudio::ActiveState || state == QAudio::IdleState) { + while (written < len) { + int chunk = m_audioDevice->write(data + written, (len - written)); if (chunk <= 0) return written; - written+=chunk; + written += chunk; } } @@ -764,10 +723,25 @@ qreal QPulseAudioSink::volume() const void QPulseAudioSink::onPulseContextFailed() { - close(); + if (auto notifier = m_stateMachine.stop(QAudio::FatalError)) + close(); +} - setError(QAudio::FatalError); - setState(QAudio::StoppedState); +PAOperationUPtr QPulseAudioSink::exchangeDrainOperation(pa_operation *newOperation) +{ + return PAOperationUPtr(m_drainOperation.exchange(newOperation)); +} + +qsizetype QPulseAudioSink::defaultBufferSize() const +{ + if (m_spec.rate > 0) + return pa_usec_to_bytes(DefaultBufferLengthMs * 1000, &m_spec); + + auto spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + if (pa_sample_spec_valid(&spec)) + return pa_usec_to_bytes(DefaultBufferLengthMs * 1000, &spec); + + return 0; } QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulseaudiosink_p.h b/src/multimedia/pulseaudio/qpulseaudiosink_p.h index 1a7b6d212..cf0b181ec 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosink_p.h +++ b/src/multimedia/pulseaudio/qpulseaudiosink_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QAUDIOOUTPUTPULSE_H #define QAUDIOOUTPUTPULSE_H @@ -60,8 +24,10 @@ #include "qaudio.h" #include "qaudiodevice.h" -#include <private/qaudiosystem_p.h> +#include "pulseaudio/qpulsehelpers_p.h" +#include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> #include <pulse/pulseaudio.h> QT_BEGIN_NAMESPACE @@ -72,7 +38,7 @@ class QPulseAudioSink : public QPlatformAudioSink Q_OBJECT public: - QPulseAudioSink(const QByteArray &device); + QPulseAudioSink(const QByteArray &device, QObject *parent); ~QPulseAudioSink(); void start(QIODevice *device) override; @@ -100,9 +66,8 @@ protected: void timerEvent(QTimerEvent *event) override; private: - void setState(QAudio::State state); - void setError(QAudio::Error error); - void startReading(); + void startPulling(); + void stopTimer(); bool open(); void close(); @@ -112,7 +77,11 @@ private Q_SLOTS: void userFeed(); void onPulseContextFailed(); + PAOperationUPtr exchangeDrainOperation(pa_operation *newOperation); + private: + qsizetype defaultBufferSize() const; + pa_sample_spec m_spec = {}; // calculate timing manually, as pulseaudio doesn't give us good enough data mutable timeval lastTimingInfo = {}; @@ -126,7 +95,7 @@ private: QIODevice *m_audioSource = nullptr; pa_stream *m_stream = nullptr; - char *m_audioBuffer = nullptr; + std::vector<char> m_audioBuffer; qint64 m_totalTimeValue = 0; qint64 m_elapsedTimeOffset = 0; @@ -134,15 +103,15 @@ private: mutable qint64 lastProcessedUSecs = 0; qreal m_volume = 1.0; - QAudio::Error m_errorState = QAudio::NoError; - QAudio::State m_deviceState = QAudio::StoppedState; - int m_periodSize = 0; - int m_bufferSize = 0; - int m_maxBufferSize = 0; - int m_periodTime = 0; + std::atomic<pa_operation *> m_drainOperation = nullptr; + qsizetype m_bufferSize = 0; + std::optional<qsizetype> m_userBufferSize = std::nullopt; + int m_pullingPeriodSize = 0; + int m_pullingPeriodTime = 0; bool m_pullMode = true; bool m_opened = false; - bool m_resuming = false; + + QAudioStateMachine m_stateMachine; }; class PulseOutputPrivate : public QIODevice diff --git a/src/multimedia/pulseaudio/qpulseaudiosource.cpp b/src/multimedia/pulseaudio/qpulseaudiosource.cpp index 4db294b55..488daa48b 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosource.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiosource.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QtCore/qcoreapplication.h> #include <QtCore/qdebug.h> @@ -48,10 +12,11 @@ #include "qpulsehelpers_p.h" #include <sys/types.h> #include <unistd.h> +#include <mutex> // for lock_guard QT_BEGIN_NAMESPACE -const int PeriodTimeMs = 50; +const int SourcePeriodTimeMs = 50; static void inputStreamReadCallback(pa_stream *stream, size_t length, void *userdata) { @@ -64,37 +29,38 @@ static void inputStreamReadCallback(pa_stream *stream, size_t length, void *user static void inputStreamStateCallback(pa_stream *stream, void *userdata) { + using namespace QPulseAudioInternal; + Q_UNUSED(userdata); pa_stream_state_t state = pa_stream_get_state(stream); -#ifdef DEBUG_PULSE - qDebug() << "Stream state: " << QPulseAudioInternal::stateToQString(state); -#endif + qCDebug(qLcPulseAudioIn) << "Stream state: " << state; switch (state) { - case PA_STREAM_CREATING: + case PA_STREAM_CREATING: break; - case PA_STREAM_READY: { -#ifdef DEBUG_PULSE - QPulseAudioSource *audioInput = static_cast<QPulseAudioSource*>(userdata); + case PA_STREAM_READY: + if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) { + QPulseAudioSource *audioInput = static_cast<QPulseAudioSource *>(userdata); const pa_buffer_attr *buffer_attr = pa_stream_get_buffer_attr(stream); - qDebug() << "*** maxlength: " << buffer_attr->maxlength; - qDebug() << "*** prebuf: " << buffer_attr->prebuf; - qDebug() << "*** fragsize: " << buffer_attr->fragsize; - qDebug() << "*** minreq: " << buffer_attr->minreq; - qDebug() << "*** tlength: " << buffer_attr->tlength; - - pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(audioInput->format()); - qDebug() << "*** bytes_to_usec: " << pa_bytes_to_usec(buffer_attr->fragsize, &spec); -#endif - } - break; - case PA_STREAM_TERMINATED: - break; - case PA_STREAM_FAILED: - default: - qWarning() << QString::fromLatin1("Stream error: %1").arg(QString::fromUtf8(pa_strerror(pa_context_errno(pa_stream_get_context(stream))))); - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); - break; + qCDebug(qLcPulseAudioIn) << "*** maxlength: " << buffer_attr->maxlength; + qCDebug(qLcPulseAudioIn) << "*** prebuf: " << buffer_attr->prebuf; + qCDebug(qLcPulseAudioIn) << "*** fragsize: " << buffer_attr->fragsize; + qCDebug(qLcPulseAudioIn) << "*** minreq: " << buffer_attr->minreq; + qCDebug(qLcPulseAudioIn) << "*** tlength: " << buffer_attr->tlength; + + pa_sample_spec spec = + QPulseAudioInternal::audioFormatToSampleSpec(audioInput->format()); + qCDebug(qLcPulseAudioIn) + << "*** bytes_to_usec: " << pa_bytes_to_usec(buffer_attr->fragsize, &spec); + } + break; + case PA_STREAM_TERMINATED: + break; + case PA_STREAM_FAILED: + default: + qWarning() << "Stream error: " << currentError(stream); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + break; } } @@ -118,71 +84,49 @@ static void inputStreamSuccessCallback(pa_stream *stream, int success, void *use Q_UNUSED(userdata); Q_UNUSED(success); - //if (!success) - //TODO: Is cork success? i->operation_success = success; + // if (!success) + // TODO: Is cork success? i->operation_success = success; QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); } -QPulseAudioSource::QPulseAudioSource(const QByteArray &device) - : m_totalTimeValue(0) - , m_audioSource(nullptr) - , m_errorState(QAudio::NoError) - , m_deviceState(QAudio::StoppedState) - , m_volume(qreal(1.0f)) - , m_pullMode(true) - , m_opened(false) - , m_bytesAvailable(0) - , m_bufferSize(0) - , m_periodSize(0) - , m_periodTime(PeriodTimeMs) - , m_stream(nullptr) - , m_device(device) +QPulseAudioSource::QPulseAudioSource(const QByteArray &device, QObject *parent) + : QPlatformAudioSource(parent), + m_totalTimeValue(0), + m_audioSource(nullptr), + m_volume(qreal(1.0f)), + m_pullMode(true), + m_opened(false), + m_bufferSize(0), + m_periodSize(0), + m_periodTime(SourcePeriodTimeMs), + m_stream(nullptr), + m_device(device), + m_stateMachine(*this) { - m_timer = new QTimer(this); - connect(m_timer, SIGNAL(timeout()), SLOT(userFeed())); } QPulseAudioSource::~QPulseAudioSource() { - close(); - disconnect(m_timer, SIGNAL(timeout())); - QCoreApplication::processEvents(); - delete m_timer; -} - -void QPulseAudioSource::setError(QAudio::Error error) -{ - if (m_errorState == error) - return; - - m_errorState = error; - emit errorChanged(error); + // TODO: Investigate draining the stream + if (auto notifier = m_stateMachine.stop()) + close(); } QAudio::Error QPulseAudioSource::error() const { - return m_errorState; -} - -void QPulseAudioSource::setState(QAudio::State state) -{ - if (m_deviceState == state) - return; - - m_deviceState = state; - emit stateChanged(state); + return m_stateMachine.error(); } QAudio::State QPulseAudioSource::state() const { - return m_deviceState; + return m_stateMachine.state(); } void QPulseAudioSource::setFormat(const QAudioFormat &format) { - if (m_deviceState == QAudio::StoppedState) + if (!m_stateMachine.isActiveOrIdle()) m_format = format; } @@ -193,15 +137,7 @@ QAudioFormat QPulseAudioSource::format() const void QPulseAudioSource::start(QIODevice *device) { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - if (!m_pullMode && m_audioSource) { - delete m_audioSource; - m_audioSource = nullptr; - } - - close(); + reset(); if (!open()) return; @@ -209,20 +145,12 @@ void QPulseAudioSource::start(QIODevice *device) m_pullMode = true; m_audioSource = device; - setState(QAudio::ActiveState); + m_stateMachine.start(); } QIODevice *QPulseAudioSource::start() { - setState(QAudio::StoppedState); - setError(QAudio::NoError); - - if (!m_pullMode && m_audioSource) { - delete m_audioSource; - m_audioSource = nullptr; - } - - close(); + reset(); if (!open()) return nullptr; @@ -231,20 +159,15 @@ QIODevice *QPulseAudioSource::start() m_audioSource = new PulseInputPrivate(this); m_audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered); - setState(QAudio::IdleState); + m_stateMachine.start(false); return m_audioSource; } void QPulseAudioSource::stop() { - if (m_deviceState == QAudio::StoppedState) - return; - - close(); - - setError(QAudio::NoError); - setState(QAudio::StoppedState); + if (auto notifier = m_stateMachine.stop()) + close(); } bool QPulseAudioSource::open() @@ -254,9 +177,9 @@ bool QPulseAudioSource::open() QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { - setError(QAudio::FatalError); - setState(QAudio::StoppedState); + if (!pulseEngine->context() + || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + m_stateMachine.stopOrUpdateError(QAudio::FatalError); return false; } @@ -265,27 +188,27 @@ bool QPulseAudioSource::open() Q_ASSERT(spec.channels == channel_map.channels); if (!pa_sample_spec_valid(&spec)) { - setError(QAudio::OpenError); - setState(QAudio::StoppedState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } m_spec = spec; -#ifdef DEBUG_PULSE -// QTime now(QTime::currentTime()); -// qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; -#endif + //if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg)) { + // QTime now(QTime::currentTime()); + // qCDebug(qLcPulseAudioIn) << now.second() << "s " << now.msec() << "ms :open()"; + //} if (m_streamName.isNull()) - m_streamName = QString(QLatin1String("QtmPulseStream-%1-%2")).arg(::getpid()).arg(quintptr(this)).toUtf8(); - -#ifdef DEBUG_PULSE - qDebug() << "Format: " << QPulseAudioInternal::sampleFormatToQString(spec.format); - qDebug() << "Rate: " << spec.rate; - qDebug() << "Channels: " << spec.channels; - qDebug() << "Frame size: " << pa_frame_size(&spec); -#endif + m_streamName = + QStringLiteral("QtmPulseStream-%1-%2").arg(::getpid()).arg(quintptr(this)).toUtf8(); + + if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) { + qCDebug(qLcPulseAudioIn) << "Format: " << spec.format; + qCDebug(qLcPulseAudioIn) << "Rate: " << spec.rate; + qCDebug(qLcPulseAudioIn) << "Channels: " << spec.channels; + qCDebug(qLcPulseAudioIn) << "Frame size: " << pa_frame_size(&spec); + } pulseEngine->lock(); @@ -297,37 +220,41 @@ bool QPulseAudioSource::open() pa_stream_set_underflow_callback(m_stream, inputStreamUnderflowCallback, this); pa_stream_set_overflow_callback(m_stream, inputStreamOverflowCallback, this); - m_periodSize = pa_usec_to_bytes(PeriodTimeMs*1000, &spec); + m_periodSize = pa_usec_to_bytes(SourcePeriodTimeMs * 1000, &spec); int flags = 0; pa_buffer_attr buffer_attr; - buffer_attr.maxlength = (uint32_t) -1; - buffer_attr.prebuf = (uint32_t) -1; - buffer_attr.tlength = (uint32_t) -1; - buffer_attr.minreq = (uint32_t) -1; + buffer_attr.maxlength = static_cast<uint32_t>(-1); + buffer_attr.prebuf = static_cast<uint32_t>(-1); + buffer_attr.tlength = static_cast<uint32_t>(-1); + buffer_attr.minreq = static_cast<uint32_t>(-1); flags |= PA_STREAM_ADJUST_LATENCY; if (m_bufferSize > 0) - buffer_attr.fragsize = (uint32_t) m_bufferSize; + buffer_attr.fragsize = static_cast<uint32_t>(m_bufferSize); else - buffer_attr.fragsize = (uint32_t) m_periodSize; + buffer_attr.fragsize = static_cast<uint32_t>(m_periodSize); + + flags |= PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING; - flags |= PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_INTERPOLATE_TIMING; - if (pa_stream_connect_record(m_stream, m_device.data(), &buffer_attr, (pa_stream_flags_t)flags) < 0) { + int connectionResult = pa_stream_connect_record(m_stream, m_device.data(), &buffer_attr, + static_cast<pa_stream_flags_t>(flags)); + if (connectionResult < 0) { qWarning() << "pa_stream_connect_record() failed!"; pa_stream_unref(m_stream); m_stream = nullptr; pulseEngine->unlock(); - setError(QAudio::OpenError); - setState(QAudio::StoppedState); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } -// auto *ss = pa_stream_get_sample_spec(m_stream); -// qDebug() << "connected stream:"; -// qDebug() << " channels" << ss->channels << spec.channels; -// qDebug() << " format" << ss->format << spec.format; -// qDebug() << " rate" << ss->rate << spec.rate; + //if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) { + // auto *ss = pa_stream_get_sample_spec(m_stream); + // qCDebug(qLcPulseAudioIn) << "connected stream:"; + // qCDebug(qLcPulseAudioIn) << " channels" << ss->channels << spec.channels; + // qCDebug(qLcPulseAudioIn) << " format" << ss->format << spec.format; + // qCDebug(qLcPulseAudioIn) << " rate" << ss->rate << spec.rate; + //} while (pa_stream_get_state(m_stream) != PA_STREAM_READY) pa_threaded_mainloop_wait(pulseEngine->mainloop()); @@ -335,15 +262,16 @@ bool QPulseAudioSource::open() const pa_buffer_attr *actualBufferAttr = pa_stream_get_buffer_attr(m_stream); m_periodSize = actualBufferAttr->fragsize; m_periodTime = pa_bytes_to_usec(m_periodSize, &spec) / 1000; - if (actualBufferAttr->tlength != (uint32_t)-1) + if (actualBufferAttr->tlength != static_cast<uint32_t>(-1)) m_bufferSize = actualBufferAttr->tlength; pulseEngine->unlock(); - connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSource::onPulseContextFailed); + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSource::onPulseContextFailed); m_opened = true; - m_timer->start(m_periodTime); + m_timer.start(m_periodTime, this); m_elapsedTimeOffset = 0; m_totalTimeValue = 0; @@ -356,12 +284,12 @@ void QPulseAudioSource::close() if (!m_opened) return; - m_timer->stop(); + m_timer.stop(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); if (m_stream) { - pulseEngine->lock(); + std::lock_guard lock(*pulseEngine); pa_stream_set_state_callback(m_stream, nullptr, nullptr); pa_stream_set_read_callback(m_stream, nullptr, nullptr); @@ -371,11 +299,10 @@ void QPulseAudioSource::close() pa_stream_disconnect(m_stream); pa_stream_unref(m_stream); m_stream = nullptr; - - pulseEngine->unlock(); } - disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioSource::onPulseContextFailed); + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSource::onPulseContextFailed); if (!m_pullMode && m_audioSource) { delete m_audioSource; @@ -384,32 +311,31 @@ void QPulseAudioSource::close() m_opened = false; } -int QPulseAudioSource::checkBytesReady() +qsizetype QPulseAudioSource::bytesReady() const { - if (m_deviceState != QAudio::ActiveState && m_deviceState != QAudio::IdleState) { - m_bytesAvailable = 0; - } else { - m_bytesAvailable = pa_stream_readable_size(m_stream); - } + using namespace QPulseAudioInternal; - return m_bytesAvailable; -} + if (!m_stateMachine.isActiveOrIdle()) + return 0; -qsizetype QPulseAudioSource::bytesReady() const -{ - return qMax(m_bytesAvailable, 0); + std::lock_guard lock(*QPulseAudioEngine::instance()); + + int bytes = pa_stream_readable_size(m_stream); + if (bytes < 0) { + qWarning() << "pa_stream_readable_size() failed:" << currentError(m_stream); + return 0; + } + + return static_cast<qsizetype>(bytes); } qint64 QPulseAudioSource::read(char *data, qint64 len) { - Q_ASSERT(data != nullptr || len == 0); + using namespace QPulseAudioInternal; - m_bytesAvailable = checkBytesReady(); - - setError(QAudio::NoError); - if (state() == QAudio::IdleState) - setState(QAudio::ActiveState); + Q_ASSERT(data != nullptr || len == 0); + m_stateMachine.updateActiveOrIdle(true, QAudio::NoError); int readBytes = 0; if (!m_pullMode && !m_tempBuffer.isEmpty()) { @@ -429,21 +355,22 @@ qint64 QPulseAudioSource::read(char *data, qint64 len) while (pa_stream_readable_size(m_stream) > 0) { size_t readLength = 0; -#ifdef DEBUG_PULSE - qDebug() << "QPulseAudioSource::read -- " << pa_stream_readable_size(m_stream) << " bytes available from pulse audio"; -#endif + if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg))) { + auto readableSize = pa_stream_readable_size(m_stream); + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- " << readableSize + << " bytes available from pulse audio"; + } QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); pulseEngine->lock(); const void *audioBuffer; - // Second and third parameters (audioBuffer and length) to pa_stream_peek are output parameters, - // the audioBuffer pointer is set to point to the actual pulse audio data, - // and the length is set to the length of this data. + // Second and third parameters (audioBuffer and length) to pa_stream_peek are output + // parameters, the audioBuffer pointer is set to point to the actual pulse audio data, and + // the length is set to the length of this data. if (pa_stream_peek(m_stream, &audioBuffer, &readLength) < 0) { - qWarning() << QString::fromLatin1("pa_stream_peek() failed: %1") - .arg(QString::fromUtf8(pa_strerror(pa_context_errno(pa_stream_get_context(m_stream))))); + qWarning() << "pa_stream_peek() failed:" << currentError(m_stream); pulseEngine->unlock(); return 0; } @@ -456,10 +383,7 @@ qint64 QPulseAudioSource::read(char *data, qint64 len) if (actualLength < qint64(readLength)) { pulseEngine->unlock(); - - setError(QAudio::UnderrunError); - setState(QAudio::IdleState); - + m_stateMachine.updateActiveOrIdle(false, QAudio::UnderrunError); return actualLength; } } else { @@ -467,18 +391,19 @@ qint64 QPulseAudioSource::read(char *data, qint64 len) applyVolume(audioBuffer, data + readBytes, actualLength); } -#ifdef DEBUG_PULSE - qDebug() << "QPulseAudioSource::read -- wrote " << actualLength << " to client"; -#endif + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- wrote " << actualLength + << " to client"; if (actualLength < qint64(readLength)) { -#ifdef DEBUG_PULSE - qDebug() << "QPulseAudioSource::read -- appending " << readLength - actualLength << " bytes of data to temp buffer"; -#endif int diff = readLength - actualLength; int oldSize = m_tempBuffer.size(); + + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- appending " << diff + << " bytes of data to temp buffer"; + m_tempBuffer.resize(m_tempBuffer.size() + diff); - applyVolume(static_cast<const char *>(audioBuffer) + actualLength, m_tempBuffer.data() + oldSize, diff); + applyVolume(static_cast<const char *>(audioBuffer) + actualLength, + m_tempBuffer.data() + oldSize, diff); QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); } @@ -492,9 +417,8 @@ qint64 QPulseAudioSource::read(char *data, qint64 len) break; } -#ifdef DEBUG_PULSE - qDebug() << "QPulseAudioSource::read -- returning after reading " << readBytes << " bytes"; -#endif + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- returning after reading " << readBytes + << " bytes"; return readBytes; } @@ -510,22 +434,18 @@ void QPulseAudioSource::applyVolume(const void *src, void *dest, int len) void QPulseAudioSource::resume() { - if (m_deviceState == QAudio::SuspendedState || m_deviceState == QAudio::IdleState) { - QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_operation *operation; - - pulseEngine->lock(); - - operation = pa_stream_cork(m_stream, 0, inputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); + if (auto notifier = m_stateMachine.resume()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pulseEngine->unlock(); + std::lock_guard lock(*pulseEngine); - m_timer->start(m_periodTime); + PAOperationUPtr operation( + pa_stream_cork(m_stream, 0, inputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } - setState(QAudio::ActiveState); - setError(QAudio::NoError); + m_timer.start(m_periodTime, this); } } @@ -559,81 +479,69 @@ qint64 QPulseAudioSource::processedUSecs() const pa_usec_t usecs = 0; int result = pa_stream_get_time(m_stream, &usecs); Q_UNUSED(result); -// if (result != 0) -// qWarning() << "no timing info from pulse"; + //if (result != 0) + // qWarning() << "no timing info from pulse"; return usecs; } void QPulseAudioSource::suspend() { - if (m_deviceState == QAudio::ActiveState) { - setError(QAudio::NoError); - setState(QAudio::SuspendedState); - - m_timer->stop(); + if (auto notifier = m_stateMachine.suspend()) { + m_timer.stop(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); - pa_operation *operation; - pulseEngine->lock(); + std::lock_guard lock(*pulseEngine); - operation = pa_stream_cork(m_stream, 1, inputStreamSuccessCallback, nullptr); - pulseEngine->wait(operation); - pa_operation_unref(operation); - - pulseEngine->unlock(); + PAOperationUPtr operation(pa_stream_cork(m_stream, 1, inputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); } } -void QPulseAudioSource::userFeed() +void QPulseAudioSource::timerEvent(QTimerEvent *event) { - if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState) - return; -#ifdef DEBUG_PULSE -// QTime now(QTime::currentTime()); -// qDebug()<< now.second() << "s " << now.msec() << "ms :userFeed() IN"; -#endif - deviceReady(); + if (event->timerId() == m_timer.timerId()) + userFeed(); + + QPlatformAudioSource::timerEvent(event); } -bool QPulseAudioSource::deviceReady() +void QPulseAudioSource::userFeed() { - if (m_pullMode) { + if (!m_stateMachine.isActiveOrIdle()) + return; + + //if (Q_UNLIKELY(qLcPulseAudioIn().isEnabled(QtDebugMsg)) { + // QTime now(QTime::currentTime()); + // qCDebug(qLcPulseAudioIn) << now.second() << "s " << now.msec() << "ms :userFeed() IN"; + //} + + if (m_pullMode) { // reads some audio data and writes it to QIODevice read(nullptr,0); - } else { + } else if (m_audioSource != nullptr) { // emits readyRead() so user will call read() on QIODevice to get some audio data - if (m_audioSource != nullptr) { - PulseInputPrivate *a = qobject_cast<PulseInputPrivate*>(m_audioSource); - a->trigger(); - } + PulseInputPrivate *a = qobject_cast<PulseInputPrivate*>(m_audioSource); + a->trigger(); } - m_bytesAvailable = checkBytesReady(); - - if (m_deviceState != QAudio::ActiveState) - return true; - - return true; } void QPulseAudioSource::reset() { - stop(); - m_bytesAvailable = 0; + if (auto notifier = m_stateMachine.stopOrUpdateError()) + close(); } void QPulseAudioSource::onPulseContextFailed() { - close(); - - setError(QAudio::FatalError); - setState(QAudio::StoppedState); + if (auto notifier = m_stateMachine.stopOrUpdateError(QAudio::FatalError)) + close(); } PulseInputPrivate::PulseInputPrivate(QPulseAudioSource *audio) { - m_audioDevice = qobject_cast<QPulseAudioSource*>(audio); + m_audioDevice = qobject_cast<QPulseAudioSource *>(audio); } qint64 PulseInputPrivate::readData(char *data, qint64 len) diff --git a/src/multimedia/pulseaudio/qpulseaudiosource_p.h b/src/multimedia/pulseaudio/qpulseaudiosource_p.h index 7a9f047dd..d652f81a0 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosource_p.h +++ b/src/multimedia/pulseaudio/qpulseaudiosource_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only // // W A R N I N G @@ -61,6 +25,7 @@ #include "qaudio.h" #include "qaudiodevice.h" #include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> #include <pulse/pulseaudio.h> @@ -73,7 +38,7 @@ class QPulseAudioSource : public QPlatformAudioSource Q_OBJECT public: - QPulseAudioSource(const QByteArray &device); + QPulseAudioSource(const QByteArray &device, QObject *parent); ~QPulseAudioSource(); qint64 read(char *data, qint64 len); @@ -99,38 +64,35 @@ public: qint64 m_totalTimeValue; QIODevice *m_audioSource; QAudioFormat m_format; - QAudio::Error m_errorState; - QAudio::State m_deviceState; qreal m_volume; +protected: + void timerEvent(QTimerEvent *event) override; + private slots: void userFeed(); - bool deviceReady(); void onPulseContextFailed(); private: - void setState(QAudio::State state); - void setError(QAudio::Error error); - void applyVolume(const void *src, void *dest, int len); - int checkBytesReady(); bool open(); void close(); bool m_pullMode; bool m_opened; - int m_bytesAvailable; int m_bufferSize; int m_periodSize; unsigned int m_periodTime; - QTimer *m_timer; + QBasicTimer m_timer; qint64 m_elapsedTimeOffset; pa_stream *m_stream; QByteArray m_streamName; QByteArray m_device; QByteArray m_tempBuffer; pa_sample_spec m_spec; + + QAudioStateMachine m_stateMachine; }; class PulseInputPrivate : public QIODevice @@ -138,7 +100,7 @@ class PulseInputPrivate : public QIODevice Q_OBJECT public: PulseInputPrivate(QPulseAudioSource *audio); - ~PulseInputPrivate() {}; + ~PulseInputPrivate() override = default; qint64 readData(char *data, qint64 len) override; qint64 writeData(const char *data, qint64 len) override; diff --git a/src/multimedia/pulseaudio/qpulsehelpers.cpp b/src/multimedia/pulseaudio/qpulsehelpers.cpp index 0136fd125..bc03e133f 100644 --- a/src/multimedia/pulseaudio/qpulsehelpers.cpp +++ b/src/multimedia/pulseaudio/qpulsehelpers.cpp @@ -1,47 +1,13 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpulsehelpers_p.h" QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcPulseAudioOut, "qt.multimedia.pulseaudio.output") +Q_LOGGING_CATEGORY(qLcPulseAudioIn, "qt.multimedia.pulseaudio.input") +Q_LOGGING_CATEGORY(qLcPulseAudioEngine, "qt.multimedia.pulseaudio.engine") namespace QPulseAudioInternal { @@ -233,6 +199,86 @@ QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec) return format; } +QUtf8StringView currentError(const pa_context *context) +{ + return pa_strerror(pa_context_errno(context)); +} + +QUtf8StringView currentError(const pa_stream *stream) +{ + return currentError(pa_stream_get_context(stream)); +} + +} // namespace QPulseAudioInternal + +static QLatin1StringView stateToQStringView(pa_stream_state_t state) +{ + using namespace Qt::StringLiterals; + switch (state) + { + case PA_STREAM_UNCONNECTED: return "Unconnected"_L1; + case PA_STREAM_CREATING: return "Creating"_L1; + case PA_STREAM_READY: return "Ready"_L1; + case PA_STREAM_FAILED: return "Failed"_L1; + case PA_STREAM_TERMINATED: return "Terminated"_L1; + default: Q_UNREACHABLE_RETURN("Unknown stream state"_L1); + } +} + +static QLatin1StringView sampleFormatToQStringView(pa_sample_format format) +{ + using namespace Qt::StringLiterals; + switch (format) + { + case PA_SAMPLE_U8: return "Unsigned 8 Bit PCM."_L1; + case PA_SAMPLE_ALAW: return "8 Bit a-Law "_L1; + case PA_SAMPLE_ULAW: return "8 Bit mu-Law"_L1; + case PA_SAMPLE_S16LE: return "Signed 16 Bit PCM, little endian (PC)."_L1; + case PA_SAMPLE_S16BE: return "Signed 16 Bit PCM, big endian."_L1; + case PA_SAMPLE_FLOAT32LE: return "32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0"_L1; + case PA_SAMPLE_FLOAT32BE: return "32 Bit IEEE floating point, big endian, range -1.0 to 1.0"_L1; + case PA_SAMPLE_S32LE: return "Signed 32 Bit PCM, little endian (PC)."_L1; + case PA_SAMPLE_S32BE: return "Signed 32 Bit PCM, big endian."_L1; + case PA_SAMPLE_S24LE: return "Signed 24 Bit PCM packed, little endian (PC)."_L1; + case PA_SAMPLE_S24BE: return "Signed 24 Bit PCM packed, big endian."_L1; + case PA_SAMPLE_S24_32LE: return "Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC)."_L1; + case PA_SAMPLE_S24_32BE: return "Signed 24 Bit PCM in LSB of 32 Bit words, big endian."_L1; + case PA_SAMPLE_MAX: return "Upper limit of valid sample types."_L1; + case PA_SAMPLE_INVALID: return "Invalid sample format"_L1; + default: Q_UNREACHABLE_RETURN("Unknown sample format"_L1); + } +} + +static QLatin1StringView stateToQStringView(pa_context_state_t state) +{ + using namespace Qt::StringLiterals; + switch (state) + { + case PA_CONTEXT_UNCONNECTED: return "Unconnected"_L1; + case PA_CONTEXT_CONNECTING: return "Connecting"_L1; + case PA_CONTEXT_AUTHORIZING: return "Authorizing"_L1; + case PA_CONTEXT_SETTING_NAME: return "Setting Name"_L1; + case PA_CONTEXT_READY: return "Ready"_L1; + case PA_CONTEXT_FAILED: return "Failed"_L1; + case PA_CONTEXT_TERMINATED: return "Terminated"_L1; + default: Q_UNREACHABLE_RETURN("Unknown context state"_L1); + } +} + + +QDebug operator<<(QDebug dbg, pa_stream_state_t state) +{ + return dbg << stateToQStringView(state); +} + +QDebug operator<<(QDebug dbg, pa_sample_format format) +{ + return dbg << sampleFormatToQStringView(format); +} + +QDebug operator<<(QDebug dbg, pa_context_state_t state) +{ + return dbg << stateToQStringView(state); } QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulsehelpers_p.h b/src/multimedia/pulseaudio/qpulsehelpers_p.h index 00f26757e..d271fde48 100644 --- a/src/multimedia/pulseaudio/qpulsehelpers_p.h +++ b/src/multimedia/pulseaudio/qpulsehelpers_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QPULSEHELPER_H #define QPULSEHELPER_H @@ -55,10 +19,20 @@ #include <qaudioformat.h> #include <pulse/pulseaudio.h> #include <QtCore/QLoggingCategory> +#include <QtCore/qdebug.h> QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(qLcPulseAudioOut) +Q_DECLARE_LOGGING_CATEGORY(qLcPulseAudioIn) +Q_DECLARE_LOGGING_CATEGORY(qLcPulseAudioEngine) + +struct PAOperationDeleter +{ + void operator()(pa_operation *op) const { pa_operation_unref(op); } +}; + +using PAOperationUPtr = std::unique_ptr<pa_operation, PAOperationDeleter>; namespace QPulseAudioInternal { @@ -67,63 +41,14 @@ QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec); pa_channel_map channelMapForAudioFormat(const QAudioFormat &format); QAudioFormat::ChannelConfig channelConfigFromMap(const pa_channel_map &map); -static inline QString stateToQString(pa_stream_state_t state) -{ - using namespace Qt::StringLiterals; - switch (state) - { - case PA_STREAM_UNCONNECTED: return "Unconnected"_L1; - case PA_STREAM_CREATING: return "Creating"_L1; - case PA_STREAM_READY: return "Ready"_L1; - case PA_STREAM_FAILED: return "Failed"_L1; - case PA_STREAM_TERMINATED: return "Terminated"_L1; - } - - return u"Unknown state: %0"_s.arg(int(state)); -} +QUtf8StringView currentError(const pa_context *); +QUtf8StringView currentError(const pa_stream *); -static inline QString sampleFormatToQString(pa_sample_format format) -{ - using namespace Qt::StringLiterals; - switch (format) - { - case PA_SAMPLE_U8: return "Unsigned 8 Bit PCM."_L1; - case PA_SAMPLE_ALAW: return "8 Bit a-Law "_L1; - case PA_SAMPLE_ULAW: return "8 Bit mu-Law"_L1; - case PA_SAMPLE_S16LE: return "Signed 16 Bit PCM, little endian (PC)."_L1; - case PA_SAMPLE_S16BE: return "Signed 16 Bit PCM, big endian."_L1; - case PA_SAMPLE_FLOAT32LE: return "32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0"_L1; - case PA_SAMPLE_FLOAT32BE: return "32 Bit IEEE floating point, big endian, range -1.0 to 1.0"_L1; - case PA_SAMPLE_S32LE: return "Signed 32 Bit PCM, little endian (PC)."_L1; - case PA_SAMPLE_S32BE: return "Signed 32 Bit PCM, big endian."_L1; - case PA_SAMPLE_S24LE: return "Signed 24 Bit PCM packed, little endian (PC)."_L1; - case PA_SAMPLE_S24BE: return "Signed 24 Bit PCM packed, big endian."_L1; - case PA_SAMPLE_S24_32LE: return "Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC)."_L1; - case PA_SAMPLE_S24_32BE: return "Signed 24 Bit PCM in LSB of 32 Bit words, big endian."_L1; - case PA_SAMPLE_MAX: return "Upper limit of valid sample types."_L1; - case PA_SAMPLE_INVALID: return "Invalid sample format"_L1; - } - - return u"Invalid value: %0"_s.arg(int(format)); -} - -static inline QString stateToQString(pa_context_state_t state) -{ - using namespace Qt::StringLiterals; - switch (state) - { - case PA_CONTEXT_UNCONNECTED: return "Unconnected"_L1; - case PA_CONTEXT_CONNECTING: return "Connecting"_L1; - case PA_CONTEXT_AUTHORIZING: return "Authorizing"_L1; - case PA_CONTEXT_SETTING_NAME: return "Setting Name"_L1; - case PA_CONTEXT_READY: return "Ready"_L1; - case PA_CONTEXT_FAILED: return "Failed"_L1; - case PA_CONTEXT_TERMINATED: return "Terminated"_L1; - } +} // namespace QPulseAudioInternal - return u"Unknown state: %0"_s.arg(int(state)); -} -} +QDebug operator<<(QDebug, pa_stream_state_t); +QDebug operator<<(QDebug, pa_sample_format); +QDebug operator<<(QDebug, pa_context_state_t); QT_END_NAMESPACE |