summaryrefslogtreecommitdiffstats
path: root/src/multimedia/pulseaudio
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/pulseaudio')
-rw-r--r--src/multimedia/pulseaudio/pulseaudio.json3
-rw-r--r--src/multimedia/pulseaudio/qaudioengine_pulse.cpp508
-rw-r--r--src/multimedia/pulseaudio/qaudioengine_pulse_p.h93
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiodevice.cpp46
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiodevice_p.h41
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiomediadevices.cpp55
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiomediadevices_p.h45
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosink.cpp749
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosink_p.h136
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosource.cpp566
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosource_p.h116
-rw-r--r--src/multimedia/pulseaudio/qpulsehelpers.cpp284
-rw-r--r--src/multimedia/pulseaudio/qpulsehelpers_p.h55
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