diff options
Diffstat (limited to 'src/multimedia/pulseaudio')
-rw-r--r-- | src/multimedia/pulseaudio/pulseaudio.json | 3 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qaudioengine_pulse.cpp | 508 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qaudioengine_pulse_p.h | 93 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiodevice.cpp | 46 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiodevice_p.h | 41 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp | 55 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h | 45 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink.cpp | 749 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink_p.h | 136 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosource.cpp | 566 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosource_p.h | 116 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulsehelpers.cpp | 284 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulsehelpers_p.h | 55 |
13 files changed, 2697 insertions, 0 deletions
diff --git a/src/multimedia/pulseaudio/pulseaudio.json b/src/multimedia/pulseaudio/pulseaudio.json new file mode 100644 index 000000000..5e0336ee8 --- /dev/null +++ b/src/multimedia/pulseaudio/pulseaudio.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "pulseaudio" ] +} diff --git a/src/multimedia/pulseaudio/qaudioengine_pulse.cpp b/src/multimedia/pulseaudio/qaudioengine_pulse.cpp new file mode 100644 index 000000000..5fac7234a --- /dev/null +++ b/src/multimedia/pulseaudio/qaudioengine_pulse.cpp @@ -0,0 +1,508 @@ +// 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() << "Failed to get server information:" << currentError(context); + return; + } + + 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); + + 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) +{ + using namespace Qt::Literals; + using namespace QPulseAudioInternal; + + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine *>(userdata); + + if (isLast < 0) { + qWarning() << "Failed to get sink information:" << currentError(context); + return; + } + + if (isLast) { + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + return; + } + + Q_ASSERT(info); + + 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); + + if (isLast) { + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + return; + } + + Q_ASSERT(info); + + 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; + + 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) +{ + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + + int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + + switch (type) { + case PA_SUBSCRIPTION_EVENT_NEW: + case PA_SUBSCRIPTION_EVENT_CHANGE: + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SERVER: { + 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: { + 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: { + PAOperationUPtr op(pa_context_get_source_info_by_index(context, index, + sourceInfoCallback, userdata)); + if (!op) + qWarning() << "PulseAudioService: failed to get source info"; + break; + } + default: + break; + } + break; + case PA_SUBSCRIPTION_EVENT_REMOVE: + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SINK: { + QWriteLocker locker(&pulseEngine->m_sinkLock); + pulseEngine->m_sinks.remove(index); + break; + } + case PA_SUBSCRIPTION_EVENT_SOURCE: { + QWriteLocker locker(&pulseEngine->m_sourceLock); + pulseEngine->m_sources.remove(index); + break; + } + default: + break; + } + break; + default: + break; + } +} + +static void contextStateCallbackInit(pa_context *context, void *userdata) +{ + Q_UNUSED(context); + + 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); +} + +static void contextStateCallback(pa_context *c, void *userdata) +{ + QPulseAudioEngine *self = reinterpret_cast<QPulseAudioEngine*>(userdata); + pa_context_state_t state = pa_context_get_state(c); + + if (Q_UNLIKELY(qLcPulseAudioEngine().isEnabled(QtDebugMsg))) + qCDebug(qLcPulseAudioEngine) << state; + + if (state == PA_CONTEXT_FAILED) + QMetaObject::invokeMethod(self, "onContextFailed", Qt::QueuedConnection); +} + +Q_GLOBAL_STATIC(QPulseAudioEngine, pulseEngine); + +QPulseAudioEngine::QPulseAudioEngine(QObject *parent) + : QObject(parent) + , m_mainLoopApi(nullptr) + , m_context(nullptr) + , m_prepared(false) +{ + prepare(); +} + +QPulseAudioEngine::~QPulseAudioEngine() +{ + if (m_prepared) + release(); +} + +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"; + return; + } + + if (pa_threaded_mainloop_start(m_mainLoop) != 0) { + qWarning() << "PulseAudioService: unable to start pulseaudio mainloop"; + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = nullptr; + return; + } + + m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop); + + lock(); + + 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"; + pa_threaded_mainloop_unlock(m_mainLoop); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = nullptr; + onContextFailed(); + return; + } + + pa_context_set_state_callback(m_context, contextStateCallbackInit, this); + + 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); + m_mainLoop = nullptr; + m_context = nullptr; + return; + } + + pa_threaded_mainloop_wait(m_mainLoop); + + while (keepGoing) { + switch (pa_context_get_state(m_context)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + qCDebug(qLcPulseAudioEngine) << "Connection established."; + keepGoing = false; + break; + + case PA_CONTEXT_TERMINATED: + qCritical("PulseAudioService: Context terminated."); + keepGoing = false; + ok = false; + break; + + case PA_CONTEXT_FAILED: + default: + qCritical() << "PulseAudioService: Connection failure:" + << currentError(m_context); + keepGoing = false; + ok = false; + } + + if (keepGoing) + pa_threaded_mainloop_wait(m_mainLoop); + } + + if (ok) { + pa_context_set_state_callback(m_context, contextStateCallback, this); + + pa_context_set_subscribe_callback(m_context, event_cb, this); + 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; + } + + unlock(); + + if (ok) { + updateDevices(); + m_prepared = true; + } else { + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = nullptr; + onContextFailed(); + } +} + +void QPulseAudioEngine::release() +{ + if (!m_prepared) + return; + + if (m_context) { + pa_context_disconnect(m_context); + pa_context_unref(m_context); + m_context = nullptr; + } + + if (m_mainLoop) { + pa_threaded_mainloop_stop(m_mainLoop); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = nullptr; + } + + m_prepared = false; +} + +void QPulseAudioEngine::updateDevices() +{ + std::lock_guard lock(*this); + + // Get default input and output devices + PAOperationUPtr operation(pa_context_get_server_info(m_context, serverInfoCallback, this)); + if (operation) { + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + } else { + qWarning() << "PulseAudioService: failed to get server info"; + } + + // Get output devices + operation.reset(pa_context_get_sink_info_list(m_context, sinkInfoCallback, this)); + if (operation) { + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + } else { + qWarning() << "PulseAudioService: failed to get sink info"; + } + + // Get input devices + operation.reset(pa_context_get_source_info_list(m_context, sourceInfoCallback, this)); + if (operation) { + while (pa_operation_get_state(operation.get()) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + } else { + qWarning() << "PulseAudioService: failed to get source info"; + } +} + +void QPulseAudioEngine::onContextFailed() +{ + // Give a chance to the connected slots to still use the Pulse main loop before releasing it. + emit contextFailed(); + + release(); + + // Try to reconnect later + QTimer::singleShot(3000, this, &QPulseAudioEngine::prepare); +} + +QPulseAudioEngine *QPulseAudioEngine::instance() +{ + return pulseEngine(); +} + +QList<QAudioDevice> QPulseAudioEngine::availableDevices(QAudioDevice::Mode mode) const +{ + if (mode == QAudioDevice::Output) { + QReadLocker locker(&m_sinkLock); + return m_sinks.values(); + } + + if (mode == QAudioDevice::Input) { + QReadLocker locker(&m_sourceLock); + return m_sources.values(); + } + + return {}; +} + +QByteArray QPulseAudioEngine::defaultDevice(QAudioDevice::Mode mode) const +{ + return (mode == QAudioDevice::Output) ? m_defaultSink : m_defaultSource; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qaudioengine_pulse_p.h b/src/multimedia/pulseaudio/qaudioengine_pulse_p.h new file mode 100644 index 000000000..2ed1fa0b1 --- /dev/null +++ b/src/multimedia/pulseaudio/qaudioengine_pulse_p.h @@ -0,0 +1,93 @@ +// 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qmap.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qreadwritelock.h> +#include <pulse/pulseaudio.h> +#include "qpulsehelpers_p.h" +#include <qaudioformat.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioEngine : public QObject +{ + Q_OBJECT + +public: + QPulseAudioEngine(QObject *parent = 0); + ~QPulseAudioEngine(); + + static QPulseAudioEngine *instance(); + pa_threaded_mainloop *mainloop() { return m_mainLoop; } + pa_context *context() { return m_context; } + + inline void lock() + { + if (m_mainLoop) + pa_threaded_mainloop_lock(m_mainLoop); + } + + inline void unlock() + { + if (m_mainLoop) + pa_threaded_mainloop_unlock(m_mainLoop); + } + + inline void wait(pa_operation *op) + { + while (m_mainLoop && pa_operation_get_state(op) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + } + + QList<QAudioDevice> availableDevices(QAudioDevice::Mode mode) const; + QByteArray defaultDevice(QAudioDevice::Mode mode) const; + +Q_SIGNALS: + void contextFailed(); + void audioInputsChanged(); + void audioOutputsChanged(); + +private Q_SLOTS: + void prepare(); + void onContextFailed(); + +private: + void updateDevices(); + void release(); + +public: + QMap<int, QAudioDevice> m_sinks; + QMap<int, QAudioDevice> m_sources; + + QByteArray m_defaultSink; + QByteArray m_defaultSource; + + mutable QReadWriteLock m_sinkLock; + mutable QReadWriteLock m_sourceLock; + mutable QReadWriteLock m_serverLock; + +private: + pa_mainloop_api *m_mainLoopApi; + pa_threaded_mainloop *m_mainLoop; + pa_context *m_context; + bool m_prepared; + }; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/pulseaudio/qpulseaudiodevice.cpp b/src/multimedia/pulseaudio/qpulseaudiodevice.cpp new file mode 100644 index 000000000..487b88f6d --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiodevice.cpp @@ -0,0 +1,46 @@ +// 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" +#include "qpulsehelpers_p.h" + +QT_BEGIN_NAMESPACE + +QPulseAudioDeviceInfo::QPulseAudioDeviceInfo(const char *device, const char *desc, bool isDef, QAudioDevice::Mode mode) + : QAudioDevicePrivate(device, mode) +{ + description = QString::fromUtf8(desc); + isDefault = isDef; + + minimumChannelCount = 1; + maximumChannelCount = PA_CHANNELS_MAX; + minimumSampleRate = 1; + maximumSampleRate = PA_RATE_MAX; + + constexpr bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian; + + const struct { + pa_sample_format pa_fmt; + QAudioFormat::SampleFormat qt_fmt; + } formatMap[] = { + { PA_SAMPLE_U8, QAudioFormat::UInt8 }, + { isBigEndian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE, QAudioFormat::Int16 }, + { isBigEndian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE, QAudioFormat::Int32 }, + { isBigEndian ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE, QAudioFormat::Float }, + }; + + for (const auto &f : formatMap) { + if (pa_sample_format_valid(f.pa_fmt) != 0) + supportedSampleFormats.append(f.qt_fmt); + } + + preferredFormat.setChannelCount(2); + preferredFormat.setSampleRate(48000); + QAudioFormat::SampleFormat f = QAudioFormat::Int16; + if (!supportedSampleFormats.contains(f)) + f = supportedSampleFormats.value(0, QAudioFormat::Unknown); + preferredFormat.setSampleFormat(f); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulseaudiodevice_p.h b/src/multimedia/pulseaudio/qpulseaudiodevice_p.h new file mode 100644 index 000000000..b44c71e0d --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiodevice_p.h @@ -0,0 +1,41 @@ +// 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qbytearray.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qlist.h> + +#include "qaudio.h" +#include "qaudiodevice.h" +#include <private/qaudiosystem_p.h> +#include <private/qaudiodevice_p.h> + +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioDeviceInfo : public QAudioDevicePrivate +{ +public: + QPulseAudioDeviceInfo(const char *device, const char *description, bool isDefault, QAudioDevice::Mode mode); + ~QPulseAudioDeviceInfo() {} +}; + +QT_END_NAMESPACE + +#endif + diff --git a/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp b/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp new file mode 100644 index 000000000..4deee6033 --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp @@ -0,0 +1,55 @@ +// 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" +#include "private/qcameradevice_p.h" + +#include "qpulseaudiosource_p.h" +#include "qpulseaudiosink_p.h" +#include "qpulseaudiodevice_p.h" +#include "qaudioengine_pulse_p.h" + +QT_BEGIN_NAMESPACE + +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() +{ + delete pulseEngine; +} + +QList<QAudioDevice> QPulseAudioMediaDevices::audioInputs() const +{ + return pulseEngine->availableDevices(QAudioDevice::Input); +} + +QList<QAudioDevice> QPulseAudioMediaDevices::audioOutputs() const +{ + return pulseEngine->availableDevices(QAudioDevice::Output); +} + +QPlatformAudioSource *QPulseAudioMediaDevices::createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QPulseAudioSource(deviceInfo.id(), parent); +} + +QPlatformAudioSink *QPulseAudioMediaDevices::createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) +{ + 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 new file mode 100644 index 000000000..094dc3907 --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h @@ -0,0 +1,45 @@ +// 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformmediadevices_p.h> +#include <qset.h> +#include <qaudio.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioEngine; + +class QPulseAudioMediaDevices : public QPlatformMediaDevices +{ +public: + QPulseAudioMediaDevices(); + ~QPulseAudioMediaDevices(); + + QList<QAudioDevice> audioInputs() const override; + QList<QAudioDevice> audioOutputs() const override; + QPlatformAudioSource *createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) override; + QPlatformAudioSink *createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) override; + +private: + QPulseAudioEngine *pulseEngine; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/pulseaudio/qpulseaudiosink.cpp b/src/multimedia/pulseaudio/qpulseaudiosink.cpp new file mode 100644 index 000000000..610677eeb --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiosink.cpp @@ -0,0 +1,749 @@ +// 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> +#include <QtCore/qmath.h> +#include <private/qaudiohelpers_p.h> + +#include "qpulseaudiosink_p.h" +#include "qaudioengine_pulse_p.h" +#include "qpulsehelpers_p.h" +#include <sys/types.h> +#include <unistd.h> +#include <mutex> // for std::lock_guard + +QT_BEGIN_NAMESPACE + +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) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + qCDebug(qLcPulseAudioOut) << "Write callback:" << length; + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void outputStreamStateCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(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() << 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; + } +} + +static void outputStreamUnderflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + qCDebug(qLcPulseAudioOut) << "Buffer underflow"; + if (userdata) + static_cast<QPulseAudioSink *>(userdata)->streamUnderflowCallback(); +} + +static void outputStreamOverflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + qCDebug(qLcPulseAudioOut) << "Buffer overflow"; +} + +static void outputStreamLatencyCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + + if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { + const pa_timing_info *info = pa_stream_get_timing_info(stream); + + qCDebug(qLcPulseAudioOut) << "Latency callback:"; + qCDebug(qLcPulseAudioOut) << "\tWrite index corrupt: " << info->write_index_corrupt; + qCDebug(qLcPulseAudioOut) << "\tWrite index: " << info->write_index; + qCDebug(qLcPulseAudioOut) << "\tRead index corrupt: " << info->read_index_corrupt; + qCDebug(qLcPulseAudioOut) << "\tRead index: " << info->read_index; + qCDebug(qLcPulseAudioOut) << "\tSink usec: " << info->sink_usec; + qCDebug(qLcPulseAudioOut) << "\tConfigured sink usec: " << info->configured_sink_usec; + } +} + +static void outputStreamSuccessCallback(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + + qCDebug(qLcPulseAudioOut) << "Stream successful:" << success; + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void outputStreamDrainComplete(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + + 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:" << static_cast<bool>(success); +} + +QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent) + : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this) +{ +} + +QPulseAudioSink::~QPulseAudioSink() +{ + if (auto notifier = m_stateMachine.stop()) + close(); +} + +QAudio::Error QPulseAudioSink::error() const +{ + return m_stateMachine.error(); +} + +QAudio::State QPulseAudioSink::state() const +{ + return m_stateMachine.state(); +} + +void QPulseAudioSink::streamUnderflowCallback() +{ + bool atEnd = m_audioSource && m_audioSource->atEnd(); + if (atEnd && m_stateMachine.state() != QAudio::StoppedState) { + qCDebug(qLcPulseAudioOut) << "Draining stream at end of buffer"; + exchangeDrainOperation(pa_stream_drain(m_stream, outputStreamDrainComplete, this)); + } + + m_stateMachine.updateActiveOrIdle( + false, (m_pullMode && atEnd) ? QAudio::NoError : QAudio::UnderrunError); +} + +void QPulseAudioSink::streamDrainedCallback() +{ + if (!exchangeDrainOperation(nullptr)) + return; +} + +void QPulseAudioSink::start(QIODevice *device) +{ + reset(); + + m_pullMode = true; + m_audioSource = device; + + if (!open()) { + m_audioSource = nullptr; + return; + } + + // ensure we only process timing infos that are up to date + gettimeofday(&lastTimingInfo, nullptr); + lastProcessedUSecs = 0; + + connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startPulling); + + m_stateMachine.start(); +} + +void QPulseAudioSink::startPulling() +{ + Q_ASSERT(m_pullMode); + if (m_tickTimer.isActive()) + return; + + m_tickTimer.start(m_pullingPeriodTime, this); +} + +void QPulseAudioSink::stopTimer() +{ + if (m_tickTimer.isActive()) + m_tickTimer.stop(); +} + +QIODevice *QPulseAudioSink::start() +{ + reset(); + + m_pullMode = false; + + if (!open()) + return nullptr; + + m_audioSource = new PulseOutputPrivate(this); + m_audioSource->open(QIODevice::WriteOnly | QIODevice::Unbuffered); + + // ensure we only process timing infos that are up to date + gettimeofday(&lastTimingInfo, nullptr); + lastProcessedUSecs = 0; + + m_stateMachine.start(false); + + return m_audioSource; +} + +bool QPulseAudioSink::open() +{ + if (m_opened) + return true; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (!pulseEngine->context() + || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + m_stateMachine.stopOrUpdateError(QAudio::FatalError); + return false; + } + + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(m_format); + Q_ASSERT(spec.channels == channel_map.channels); + + if (!pa_sample_spec_valid(&spec)) { + m_stateMachine.stopOrUpdateError(QAudio::OpenError); + return false; + } + + m_spec = spec; + m_totalTimeValue = 0; + + if (m_streamName.isNull()) + 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: " << spec.format; + qCDebug(qLcPulseAudioOut) << "\tRate: " << spec.rate; + qCDebug(qLcPulseAudioOut) << "\tChannels: " << spec.channels; + qCDebug(qLcPulseAudioOut) << "\tFrame size: " << pa_frame_size(&spec); + } + + pulseEngine->lock(); + + pa_proplist *propList = pa_proplist_new(); +#if 0 + qint64 bytesPerSecond = m_format.sampleRate() * m_format.bytesPerFrame(); + static const char *mediaRoleFromAudioRole[] = { + nullptr, // UnknownRole + "music", // MusicRole + "video", // VideoRole + "phone", // VoiceCommunicationRole + "event", // AlarmRole + "event", // NotificationRole + "phone", // RingtoneRole + "a11y", // AccessibilityRole + nullptr, // SonificationRole + "game" // GameRole + }; + + const char *r = mediaRoleFromAudioRole[m_role]; + if (r) + 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); + pa_proplist_free(propList); + + if (!m_stream) { + qCWarning(qLcPulseAudioOut) << "QAudioSink: pa_stream_new_with_proplist() failed!"; + pulseEngine->unlock(); + + m_stateMachine.stopOrUpdateError(QAudio::OpenError); + return false; + } + + pa_stream_set_state_callback(m_stream, outputStreamStateCallback, this); + pa_stream_set_write_callback(m_stream, outputStreamWriteCallback, this); + + pa_stream_set_underflow_callback(m_stream, outputStreamUnderflowCallback, this); + pa_stream_set_overflow_callback(m_stream, outputStreamOverflowCallback, this); + pa_stream_set_latency_update_callback(m_stream, outputStreamLatencyCallback, this); + + pa_buffer_attr requestedBuffer; + // 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(); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); + return false; + } + + while (pa_stream_get_state(m_stream) != PA_STREAM_READY) + pa_threaded_mainloop_wait(pulseEngine->mainloop()); + + const pa_buffer_attr *buffer = pa_stream_get_buffer_attr(m_stream); + m_bufferSize = buffer->tlength; + + 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; + PAOperationUPtr(pa_stream_set_buffer_attr(m_stream, &newBufferAttr, + streamAdjustPrebufferCallback, nullptr)); + } + + if (Q_UNLIKELY(qLcPulseAudioOut().isEnabled(QtDebugMsg))) { + qCDebug(qLcPulseAudioOut) << "Buffering info:"; + qCDebug(qLcPulseAudioOut) << "\tMax length: " << buffer->maxlength; + qCDebug(qLcPulseAudioOut) << "\tTarget length: " << buffer->tlength; + qCDebug(qLcPulseAudioOut) << "\tPre-buffering: " << buffer->prebuf; + qCDebug(qLcPulseAudioOut) << "\tMinimum request: " << buffer->minreq; + qCDebug(qLcPulseAudioOut) << "\tFragment size: " << buffer->fragsize; + } + + pulseEngine->unlock(); + + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSink::onPulseContextFailed); + + m_opened = true; + + if (m_pullMode) + startPulling(); + + m_elapsedTimeOffset = 0; + + return true; +} + +void QPulseAudioSink::close() +{ + if (!m_opened) + return; + + stopTimer(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (m_stream) { + std::lock_guard lock(*pulseEngine); + + pa_stream_set_state_callback(m_stream, nullptr, nullptr); + pa_stream_set_write_callback(m_stream, nullptr, nullptr); + pa_stream_set_underflow_callback(m_stream, nullptr, nullptr); + pa_stream_set_overflow_callback(m_stream, nullptr, nullptr); + pa_stream_set_latency_update_callback(m_stream, nullptr, nullptr); + + 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; + } + + 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; + m_audioBuffer.clear(); +} + +void QPulseAudioSink::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_tickTimer.timerId() && m_pullMode) + userFeed(); + + QPlatformAudioSink::timerEvent(event); +} + +void QPulseAudioSink::userFeed() +{ + int writableSize = bytesFree(); + + if (writableSize == 0) { + // PulseAudio server doesn't want any more data + m_stateMachine.activateFromIdle(); + return; + } + + // Write up to writableSize + const int inputSize = + std::min({ m_pullingPeriodSize, static_cast<int>(m_audioBuffer.size()), writableSize }); + + 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; + } +} + +qint64 QPulseAudioSink::write(const char *data, qint64 len) +{ + using namespace QPulseAudioInternal; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + pulseEngine->lock(); + + size_t nbytes = len; + void *dest = nullptr; + + if (pa_stream_begin_write(m_stream, &dest, &nbytes) < 0) { + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_begin_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); + return 0; + } + + len = qMin(len, qint64(nbytes)); + + if (m_volume < 1.0f) { + // Don't use PulseAudio volume, as it might affect all other streams of the same category + // or even affect the system volume if flat volumes are enabled + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, data, dest, len); + } else { + memcpy(dest, data, len); + } + + data = reinterpret_cast<char *>(dest); + + if ((pa_stream_write(m_stream, data, len, nullptr, 0, PA_SEEK_RELATIVE)) < 0) { + pulseEngine->unlock(); + qCWarning(qLcPulseAudioOut) + << "pa_stream_write error:" << currentError(pulseEngine->context()); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); + return 0; + } + + pulseEngine->unlock(); + m_totalTimeValue += len; + + m_stateMachine.updateActiveOrIdle(true); + return len; +} + +void QPulseAudioSink::stop() +{ + if (auto notifier = m_stateMachine.stop()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + std::lock_guard lock(*pulseEngine); + + if (auto prevOp = exchangeDrainOperation(nullptr)) + // cancel the draining callback that is not relevant already + pa_operation_cancel(prevOp.get()); + + PAOperationUPtr drainOp(pa_stream_drain(m_stream, outputStreamDrainComplete, nullptr)); + pulseEngine->wait(drainOp.get()); + } + + close(); + } +} + +qsizetype QPulseAudioSink::bytesFree() const +{ + if (!m_stateMachine.isActiveOrIdle()) + return 0; + + std::lock_guard lock(*QPulseAudioEngine::instance()); + return pa_stream_writable_size(m_stream); +} + +void QPulseAudioSink::setBufferSize(qsizetype value) +{ + m_userBufferSize = value; +} + +qsizetype QPulseAudioSink::bufferSize() const +{ + 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); +} + +qint64 QPulseAudioSink::processedUSecs() const +{ + const auto state = this->state(); + if (!m_stream || state == QAudio::StoppedState) + return 0; + if (state == QAudio::SuspendedState) + return lastProcessedUSecs; + + auto info = pa_stream_get_timing_info(m_stream); + if (!info) + return lastProcessedUSecs; + + // if the info changed, update our cached data, and recalculate the average latency + 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 + + // 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) { + 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 + const int latencyListMaxSize = 10; + if (latencyList.size() > latencyListMaxSize) + latencyList.pop_front(); + for (const auto l : latencyList) + averageLatency += l; + averageLatency /= latencyList.size(); + if (averageLatency < 0) + averageLatency = 0; + } + } + + 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); + + // processed data is the amount read by the server minus its latency + qint64 usecs = usecsRead - averageLatency; + + timeval tv; + gettimeofday(&tv, nullptr); + + // and now adjust for the time since the last update + qint64 timeSinceUpdate = tv - info->timestamp; + if (timeSinceUpdate > 0) + usecs += timeSinceUpdate; + + // We can never have processed more than we've written to the sink + if (usecs > usecsWritten) + usecs = usecsWritten; + + // make sure timing is monotonic + if (usecs < lastProcessedUSecs) + usecs = lastProcessedUSecs; + else + lastProcessedUSecs = usecs; + + return usecs; +} + +void QPulseAudioSink::resume() +{ + if (auto notifier = m_stateMachine.resume()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + std::lock_guard lock(*pulseEngine); + + PAOperationUPtr operation( + pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + + operation.reset(pa_stream_trigger(m_stream, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } + + if (m_pullMode) + startPulling(); + } +} + +void QPulseAudioSink::setFormat(const QAudioFormat &format) +{ + m_format = format; +} + +QAudioFormat QPulseAudioSink::format() const +{ + return m_format; +} + +void QPulseAudioSink::suspend() +{ + if (auto notifier = m_stateMachine.suspend()) { + stopTimer(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + std::lock_guard lock(*pulseEngine); + + PAOperationUPtr operation( + pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } +} + +void QPulseAudioSink::reset() +{ + if (auto notifier = m_stateMachine.stopOrUpdateError()) + close(); +} + +PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio) +{ + m_audioDevice = qobject_cast<QPulseAudioSink *>(audio); +} + +qint64 PulseOutputPrivate::readData(char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + + return 0; +} + +qint64 PulseOutputPrivate::writeData(const char *data, qint64 len) +{ + qint64 written = 0; + + 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; + } + } + + return written; +} + +void QPulseAudioSink::setVolume(qreal vol) +{ + if (qFuzzyCompare(m_volume, vol)) + return; + + m_volume = qBound(qreal(0), vol, qreal(1)); +} + +qreal QPulseAudioSink::volume() const +{ + return m_volume; +} + +void QPulseAudioSink::onPulseContextFailed() +{ + if (auto notifier = m_stateMachine.stop(QAudio::FatalError)) + close(); +} + +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 + +#include "moc_qpulseaudiosink_p.cpp" diff --git a/src/multimedia/pulseaudio/qpulseaudiosink_p.h b/src/multimedia/pulseaudio/qpulseaudiosink_p.h new file mode 100644 index 000000000..cf0b181ec --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiosink_p.h @@ -0,0 +1,136 @@ +// 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qfile.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include "qaudio.h" +#include "qaudiodevice.h" +#include "pulseaudio/qpulsehelpers_p.h" + +#include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioSink : public QPlatformAudioSink +{ + friend class PulseOutputPrivate; + Q_OBJECT + +public: + QPulseAudioSink(const QByteArray &device, QObject *parent); + ~QPulseAudioSink(); + + void start(QIODevice *device) override; + QIODevice *start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesFree() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &format) override; + QAudioFormat format() const override; + + void setVolume(qreal volume) override; + qreal volume() const override; + + void streamUnderflowCallback(); + void streamDrainedCallback(); + +protected: + void timerEvent(QTimerEvent *event) override; + +private: + void startPulling(); + void stopTimer(); + + bool open(); + void close(); + qint64 write(const char *data, qint64 len); + +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 = {}; + + mutable QList<qint64> latencyList; // last latency values + + QByteArray m_device; + QByteArray m_streamName; + QAudioFormat m_format; + QBasicTimer m_tickTimer; + + QIODevice *m_audioSource = nullptr; + pa_stream *m_stream = nullptr; + std::vector<char> m_audioBuffer; + + qint64 m_totalTimeValue = 0; + qint64 m_elapsedTimeOffset = 0; + mutable qint64 averageLatency = 0; // average latency + mutable qint64 lastProcessedUSecs = 0; + qreal m_volume = 1.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; + + QAudioStateMachine m_stateMachine; +}; + +class PulseOutputPrivate : public QIODevice +{ + friend class QPulseAudioSink; + Q_OBJECT + +public: + PulseOutputPrivate(QPulseAudioSink *audio); + virtual ~PulseOutputPrivate() {} + +protected: + qint64 readData(char *data, qint64 len) override; + qint64 writeData(const char *data, qint64 len) override; + +private: + QPulseAudioSink *m_audioDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/pulseaudio/qpulseaudiosource.cpp b/src/multimedia/pulseaudio/qpulseaudiosource.cpp new file mode 100644 index 000000000..488daa48b --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiosource.cpp @@ -0,0 +1,566 @@ +// 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> +#include <QtCore/qmath.h> +#include <private/qaudiohelpers_p.h> + +#include "qpulseaudiosource_p.h" +#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 + +const int SourcePeriodTimeMs = 50; + +static void inputStreamReadCallback(pa_stream *stream, size_t length, void *userdata) +{ + Q_UNUSED(userdata); + Q_UNUSED(length); + Q_UNUSED(stream); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void inputStreamStateCallback(pa_stream *stream, void *userdata) +{ + using namespace QPulseAudioInternal; + + Q_UNUSED(userdata); + pa_stream_state_t state = pa_stream_get_state(stream); + qCDebug(qLcPulseAudioIn) << "Stream state: " << state; + switch (state) { + case PA_STREAM_CREATING: + break; + 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); + 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; + } +} + +static void inputStreamUnderflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(userdata); + Q_UNUSED(stream); + qWarning() << "Got a buffer underflow!"; +} + +static void inputStreamOverflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + qWarning() << "Got a buffer overflow!"; +} + +static void inputStreamSuccessCallback(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + Q_UNUSED(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, 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) +{ +} + +QPulseAudioSource::~QPulseAudioSource() +{ + // TODO: Investigate draining the stream + if (auto notifier = m_stateMachine.stop()) + close(); +} + +QAudio::Error QPulseAudioSource::error() const +{ + return m_stateMachine.error(); +} + +QAudio::State QPulseAudioSource::state() const +{ + return m_stateMachine.state(); +} + +void QPulseAudioSource::setFormat(const QAudioFormat &format) +{ + if (!m_stateMachine.isActiveOrIdle()) + m_format = format; +} + +QAudioFormat QPulseAudioSource::format() const +{ + return m_format; +} + +void QPulseAudioSource::start(QIODevice *device) +{ + reset(); + + if (!open()) + return; + + m_pullMode = true; + m_audioSource = device; + + m_stateMachine.start(); +} + +QIODevice *QPulseAudioSource::start() +{ + reset(); + + if (!open()) + return nullptr; + + m_pullMode = false; + m_audioSource = new PulseInputPrivate(this); + m_audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + + m_stateMachine.start(false); + + return m_audioSource; +} + +void QPulseAudioSource::stop() +{ + if (auto notifier = m_stateMachine.stop()) + close(); +} + +bool QPulseAudioSource::open() +{ + if (m_opened) + return true; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (!pulseEngine->context() + || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + m_stateMachine.stopOrUpdateError(QAudio::FatalError); + return false; + } + + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + pa_channel_map channel_map = QPulseAudioInternal::channelMapForAudioFormat(m_format); + Q_ASSERT(spec.channels == channel_map.channels); + + if (!pa_sample_spec_valid(&spec)) { + m_stateMachine.stopOrUpdateError(QAudio::OpenError); + return false; + } + + m_spec = spec; + + //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 = + 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(); + + m_stream = pa_stream_new(pulseEngine->context(), m_streamName.constData(), &spec, &channel_map); + + pa_stream_set_state_callback(m_stream, inputStreamStateCallback, this); + pa_stream_set_read_callback(m_stream, inputStreamReadCallback, this); + + pa_stream_set_underflow_callback(m_stream, inputStreamUnderflowCallback, this); + pa_stream_set_overflow_callback(m_stream, inputStreamOverflowCallback, this); + + m_periodSize = pa_usec_to_bytes(SourcePeriodTimeMs * 1000, &spec); + + int flags = 0; + pa_buffer_attr buffer_attr; + 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 = static_cast<uint32_t>(m_bufferSize); + else + buffer_attr.fragsize = static_cast<uint32_t>(m_periodSize); + + flags |= PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING; + + 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(); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); + return false; + } + + //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()); + + 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 != static_cast<uint32_t>(-1)) + m_bufferSize = actualBufferAttr->tlength; + + pulseEngine->unlock(); + + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSource::onPulseContextFailed); + + m_opened = true; + m_timer.start(m_periodTime, this); + + m_elapsedTimeOffset = 0; + m_totalTimeValue = 0; + + return true; +} + +void QPulseAudioSource::close() +{ + if (!m_opened) + return; + + m_timer.stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (m_stream) { + std::lock_guard lock(*pulseEngine); + + pa_stream_set_state_callback(m_stream, nullptr, nullptr); + pa_stream_set_read_callback(m_stream, nullptr, nullptr); + pa_stream_set_underflow_callback(m_stream, nullptr, nullptr); + pa_stream_set_overflow_callback(m_stream, nullptr, nullptr); + + pa_stream_disconnect(m_stream); + pa_stream_unref(m_stream); + m_stream = nullptr; + } + + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, + &QPulseAudioSource::onPulseContextFailed); + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = nullptr; + } + m_opened = false; +} + +qsizetype QPulseAudioSource::bytesReady() const +{ + using namespace QPulseAudioInternal; + + if (!m_stateMachine.isActiveOrIdle()) + return 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) +{ + using namespace QPulseAudioInternal; + + Q_ASSERT(data != nullptr || len == 0); + + m_stateMachine.updateActiveOrIdle(true, QAudio::NoError); + int readBytes = 0; + + if (!m_pullMode && !m_tempBuffer.isEmpty()) { + readBytes = qMin(static_cast<int>(len), m_tempBuffer.size()); + if (readBytes) + memcpy(data, m_tempBuffer.constData(), readBytes); + m_totalTimeValue += readBytes; + + if (readBytes < m_tempBuffer.size()) { + m_tempBuffer.remove(0, readBytes); + return readBytes; + } + + m_tempBuffer.clear(); + } + + while (pa_stream_readable_size(m_stream) > 0) { + size_t readLength = 0; + + 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. + if (pa_stream_peek(m_stream, &audioBuffer, &readLength) < 0) { + qWarning() << "pa_stream_peek() failed:" << currentError(m_stream); + pulseEngine->unlock(); + return 0; + } + + qint64 actualLength = 0; + if (m_pullMode) { + QByteArray adjusted(readLength, Qt::Uninitialized); + applyVolume(audioBuffer, adjusted.data(), readLength); + actualLength = m_audioSource->write(adjusted); + + if (actualLength < qint64(readLength)) { + pulseEngine->unlock(); + m_stateMachine.updateActiveOrIdle(false, QAudio::UnderrunError); + return actualLength; + } + } else { + actualLength = qMin(static_cast<int>(len - readBytes), static_cast<int>(readLength)); + applyVolume(audioBuffer, data + readBytes, actualLength); + } + + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- wrote " << actualLength + << " to client"; + + if (actualLength < qint64(readLength)) { + 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); + QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); + } + + m_totalTimeValue += actualLength; + readBytes += actualLength; + + pa_stream_drop(m_stream); + pulseEngine->unlock(); + + if (!m_pullMode && readBytes >= len) + break; + } + + qCDebug(qLcPulseAudioIn) << "QPulseAudioSource::read -- returning after reading " << readBytes + << " bytes"; + + return readBytes; +} + +void QPulseAudioSource::applyVolume(const void *src, void *dest, int len) +{ + Q_ASSERT((src && dest) || len == 0); + if (m_volume < 1.f) + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, src, dest, len); + else if (len) + memcpy(dest, src, len); +} + +void QPulseAudioSource::resume() +{ + if (auto notifier = m_stateMachine.resume()) { + { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + std::lock_guard lock(*pulseEngine); + + PAOperationUPtr operation( + pa_stream_cork(m_stream, 0, inputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } + + m_timer.start(m_periodTime, this); + } +} + +void QPulseAudioSource::setVolume(qreal vol) +{ + if (qFuzzyCompare(m_volume, vol)) + return; + + m_volume = qBound(qreal(0), vol, qreal(1)); +} + +qreal QPulseAudioSource::volume() const +{ + return m_volume; +} + +void QPulseAudioSource::setBufferSize(qsizetype value) +{ + m_bufferSize = value; +} + +qsizetype QPulseAudioSource::bufferSize() const +{ + return m_bufferSize; +} + +qint64 QPulseAudioSource::processedUSecs() const +{ + if (!m_stream) + return 0; + 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"; + + return usecs; +} + +void QPulseAudioSource::suspend() +{ + if (auto notifier = m_stateMachine.suspend()) { + m_timer.stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + std::lock_guard lock(*pulseEngine); + + PAOperationUPtr operation(pa_stream_cork(m_stream, 1, inputStreamSuccessCallback, nullptr)); + pulseEngine->wait(operation.get()); + } +} + +void QPulseAudioSource::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timer.timerId()) + userFeed(); + + QPlatformAudioSource::timerEvent(event); +} + +void QPulseAudioSource::userFeed() +{ + 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 if (m_audioSource != nullptr) { + // emits readyRead() so user will call read() on QIODevice to get some audio data + PulseInputPrivate *a = qobject_cast<PulseInputPrivate*>(m_audioSource); + a->trigger(); + } +} + +void QPulseAudioSource::reset() +{ + if (auto notifier = m_stateMachine.stopOrUpdateError()) + close(); +} + +void QPulseAudioSource::onPulseContextFailed() +{ + if (auto notifier = m_stateMachine.stopOrUpdateError(QAudio::FatalError)) + close(); +} + +PulseInputPrivate::PulseInputPrivate(QPulseAudioSource *audio) +{ + m_audioDevice = qobject_cast<QPulseAudioSource *>(audio); +} + +qint64 PulseInputPrivate::readData(char *data, qint64 len) +{ + return m_audioDevice->read(data, len); +} + +qint64 PulseInputPrivate::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +void PulseInputPrivate::trigger() +{ + emit readyRead(); +} + +QT_END_NAMESPACE + +#include "moc_qpulseaudiosource_p.cpp" diff --git a/src/multimedia/pulseaudio/qpulseaudiosource_p.h b/src/multimedia/pulseaudio/qpulseaudiosource_p.h new file mode 100644 index 000000000..d652f81a0 --- /dev/null +++ b/src/multimedia/pulseaudio/qpulseaudiosource_p.h @@ -0,0 +1,116 @@ +// 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 +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QAUDIOINPUTPULSE_H +#define QAUDIOINPUTPULSE_H + +#include <QtCore/qfile.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include "qaudio.h" +#include "qaudiodevice.h" +#include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> + +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +class PulseInputPrivate; + +class QPulseAudioSource : public QPlatformAudioSource +{ + Q_OBJECT + +public: + QPulseAudioSource(const QByteArray &device, QObject *parent); + ~QPulseAudioSource(); + + qint64 read(char *data, qint64 len); + + void start(QIODevice *device) override; + QIODevice *start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesReady() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &format) override; + QAudioFormat format() const override; + + void setVolume(qreal volume) override; + qreal volume() const override; + + qint64 m_totalTimeValue; + QIODevice *m_audioSource; + QAudioFormat m_format; + qreal m_volume; + +protected: + void timerEvent(QTimerEvent *event) override; + +private slots: + void userFeed(); + void onPulseContextFailed(); + +private: + void applyVolume(const void *src, void *dest, int len); + + bool open(); + void close(); + + bool m_pullMode; + bool m_opened; + int m_bufferSize; + int m_periodSize; + unsigned int m_periodTime; + 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 +{ + Q_OBJECT +public: + PulseInputPrivate(QPulseAudioSource *audio); + ~PulseInputPrivate() override = default; + + qint64 readData(char *data, qint64 len) override; + qint64 writeData(const char *data, qint64 len) override; + + void trigger(); + +private: + QPulseAudioSource *m_audioDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/pulseaudio/qpulsehelpers.cpp b/src/multimedia/pulseaudio/qpulsehelpers.cpp new file mode 100644 index 000000000..bc03e133f --- /dev/null +++ b/src/multimedia/pulseaudio/qpulsehelpers.cpp @@ -0,0 +1,284 @@ +// 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 +{ +pa_sample_spec audioFormatToSampleSpec(const QAudioFormat &format) +{ + pa_sample_spec spec; + + spec.rate = format.sampleRate(); + spec.channels = format.channelCount(); + spec.format = PA_SAMPLE_INVALID; + const bool isBigEndian = QSysInfo::ByteOrder == QSysInfo::BigEndian; + + if (format.sampleFormat() == QAudioFormat::UInt8) { + spec.format = PA_SAMPLE_U8; + } else if (format.sampleFormat() == QAudioFormat::Int16) { + spec.format = isBigEndian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE; + } else if (format.sampleFormat() == QAudioFormat::Int32) { + spec.format = isBigEndian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE; + } else if (format.sampleFormat() == QAudioFormat::Float) { + spec.format = isBigEndian ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE; + } + + return spec; +} + +pa_channel_map channelMapForAudioFormat(const QAudioFormat &format) +{ + pa_channel_map map; + map.channels = 0; + + auto config = format.channelConfig(); + if (config == QAudioFormat::ChannelConfigUnknown) + config = QAudioFormat::defaultChannelConfigForChannelCount(format.channelCount()); + + if (config == QAudioFormat::ChannelConfigMono) { + map.channels = 1; + map.map[0] = PA_CHANNEL_POSITION_MONO; + } else { + if (config & QAudioFormat::channelConfig(QAudioFormat::FrontLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_LEFT; + if (config & QAudioFormat::channelConfig(QAudioFormat::FrontRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_RIGHT; + if (config & QAudioFormat::channelConfig(QAudioFormat::FrontCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::LFE)) + map.map[map.channels++] = PA_CHANNEL_POSITION_LFE; + if (config & QAudioFormat::channelConfig(QAudioFormat::BackLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_LEFT; + if (config & QAudioFormat::channelConfig(QAudioFormat::BackRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_RIGHT; + if (config & QAudioFormat::channelConfig(QAudioFormat::FrontLeftOfCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::FrontRightOfCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::BackCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_REAR_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::LFE2)) + map.map[map.channels++] = PA_CHANNEL_POSITION_LFE; + if (config & QAudioFormat::channelConfig(QAudioFormat::SideLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_LEFT; + if (config & QAudioFormat::channelConfig(QAudioFormat::SideRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_SIDE_RIGHT; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopFrontLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_FRONT_LEFT; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopFrontRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_FRONT_RIGHT; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopFrontCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_FRONT_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopBackLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_REAR_LEFT; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopBackRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_REAR_RIGHT; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopSideLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX0; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopSideRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX1; + if (config & QAudioFormat::channelConfig(QAudioFormat::TopBackCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_TOP_REAR_CENTER; + if (config & QAudioFormat::channelConfig(QAudioFormat::BottomFrontCenter)) + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX2; + if (config & QAudioFormat::channelConfig(QAudioFormat::BottomFrontLeft)) + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX3; + if (config & QAudioFormat::channelConfig(QAudioFormat::BottomFrontRight)) + map.map[map.channels++] = PA_CHANNEL_POSITION_AUX4; + } + + Q_ASSERT(qPopulationCount(config) == map.channels); + return map; +} + +QAudioFormat::ChannelConfig channelConfigFromMap(const pa_channel_map &map) +{ + quint32 config = 0; + for (int i = 0; i < map.channels; ++i) { + switch (map.map[i]) { + case PA_CHANNEL_POSITION_MONO: + case PA_CHANNEL_POSITION_FRONT_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::FrontCenter); + break; + case PA_CHANNEL_POSITION_FRONT_LEFT: + config |= QAudioFormat::channelConfig(QAudioFormat::FrontLeft); + break; + case PA_CHANNEL_POSITION_FRONT_RIGHT: + config |= QAudioFormat::channelConfig(QAudioFormat::FrontRight); + break; + case PA_CHANNEL_POSITION_REAR_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::BackCenter); + break; + case PA_CHANNEL_POSITION_REAR_LEFT: + config |= QAudioFormat::channelConfig(QAudioFormat::BackLeft); + break; + case PA_CHANNEL_POSITION_REAR_RIGHT: + config |= QAudioFormat::channelConfig(QAudioFormat::BackRight); + break; + case PA_CHANNEL_POSITION_LFE: + config |= QAudioFormat::channelConfig(QAudioFormat::LFE); + break; + case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::FrontLeftOfCenter); + break; + case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::FrontRightOfCenter); + break; + case PA_CHANNEL_POSITION_SIDE_LEFT: + config |= QAudioFormat::channelConfig(QAudioFormat::SideLeft); + break; + case PA_CHANNEL_POSITION_SIDE_RIGHT: + config |= QAudioFormat::channelConfig(QAudioFormat::SideRight); + break; + + case PA_CHANNEL_POSITION_TOP_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::TopCenter); + break; + case PA_CHANNEL_POSITION_TOP_FRONT_LEFT: + config |= QAudioFormat::channelConfig(QAudioFormat::TopFrontLeft); + break; + case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT: + config |= QAudioFormat::channelConfig(QAudioFormat::TopFrontRight); + break; + case PA_CHANNEL_POSITION_TOP_FRONT_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::TopFrontCenter); + break; + case PA_CHANNEL_POSITION_TOP_REAR_LEFT: + config |= QAudioFormat::channelConfig(QAudioFormat::TopBackLeft); + break; + case PA_CHANNEL_POSITION_TOP_REAR_RIGHT: + config |= QAudioFormat::channelConfig(QAudioFormat::TopBackRight); + break; + case PA_CHANNEL_POSITION_TOP_REAR_CENTER: + config |= QAudioFormat::channelConfig(QAudioFormat::TopBackCenter); + break; + default: + break; + } + } + return QAudioFormat::ChannelConfig(config); +} + +QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec) +{ + QAudioFormat format; + + format.setSampleRate(spec.rate); + format.setChannelCount(spec.channels); + QAudioFormat::SampleFormat sampleFormat; + switch (spec.format) { + case PA_SAMPLE_U8: + sampleFormat = QAudioFormat::UInt8; + break; + case PA_SAMPLE_S16LE: + case PA_SAMPLE_S16BE: + sampleFormat = QAudioFormat::Int16; + break; + case PA_SAMPLE_FLOAT32LE: + case PA_SAMPLE_FLOAT32BE: + sampleFormat = QAudioFormat::Float; + break; + case PA_SAMPLE_S32LE: + case PA_SAMPLE_S32BE: + sampleFormat = QAudioFormat::Int32; + break; + default: + return {}; + } + + format.setSampleFormat(sampleFormat); + 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 new file mode 100644 index 000000000..d271fde48 --- /dev/null +++ b/src/multimedia/pulseaudio/qpulsehelpers_p.h @@ -0,0 +1,55 @@ +// 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 + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qaudiodevice.h" +#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 +{ +pa_sample_spec audioFormatToSampleSpec(const QAudioFormat &format); +QAudioFormat sampleSpecToAudioFormat(const pa_sample_spec &spec); +pa_channel_map channelMapForAudioFormat(const QAudioFormat &format); +QAudioFormat::ChannelConfig channelConfigFromMap(const pa_channel_map &map); + +QUtf8StringView currentError(const pa_context *); +QUtf8StringView currentError(const pa_stream *); + +} // namespace QPulseAudioInternal + +QDebug operator<<(QDebug, pa_stream_state_t); +QDebug operator<<(QDebug, pa_sample_format); +QDebug operator<<(QDebug, pa_context_state_t); + +QT_END_NAMESPACE + +#endif |