diff options
Diffstat (limited to 'src/multimedia/qnx')
-rw-r--r-- | src/multimedia/qnx/qqnxaudiodevice.cpp | 85 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudiodevice_p.h | 35 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudiosink.cpp | 516 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudiosink_p.h | 119 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudiosource.cpp | 376 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudiosource_p.h | 113 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudioutils.cpp | 148 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxaudioutils_p.h | 51 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxmediadevices.cpp | 70 | ||||
-rw-r--r-- | src/multimedia/qnx/qqnxmediadevices_p.h | 39 |
10 files changed, 1552 insertions, 0 deletions
diff --git a/src/multimedia/qnx/qqnxaudiodevice.cpp b/src/multimedia/qnx/qqnxaudiodevice.cpp new file mode 100644 index 000000000..4e1390e8b --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiodevice.cpp @@ -0,0 +1,85 @@ +// Copyright (C) 2016 Research In Motion +// Copyright (C) 2021 The Qt Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqnxaudiodevice_p.h" + +#include "qqnxaudioutils_p.h" + +#include <array> + +#include <sys/asoundlib.h> + +using namespace QnxAudioUtils; + +QT_BEGIN_NAMESPACE + +QnxAudioDeviceInfo::QnxAudioDeviceInfo(const QByteArray &deviceName, QAudioDevice::Mode mode) + : QAudioDevicePrivate(deviceName, mode) +{ + isDefault = id.contains("Preferred"); + + preferredFormat.setChannelCount(mode == QAudioDevice::Input ? 1 : 2); + + description = QString::fromUtf8(id); + + minimumChannelCount = 1; + maximumChannelCount = 2; + + const std::optional<snd_pcm_channel_info_t> info = pcmChannelInfo(id, mode); + + if (!info) + return; + + minimumSampleRate = info->min_rate; + maximumSampleRate = info->max_rate; + + constexpr std::array sampleRates { 48000, 44100, 22050, 16000, 11025, 8000 }; + + for (int rate : sampleRates) { + if (rate <= maximumSampleRate && rate >= minimumSampleRate) { + preferredFormat.setSampleRate(rate); + break; + } + } + + if (info->formats & SND_PCM_FMT_U8) { + supportedSampleFormats << QAudioFormat::UInt8; + preferredFormat.setSampleFormat(QAudioFormat::UInt8); + } + + if (info->formats & SND_PCM_FMT_S16) { + supportedSampleFormats << QAudioFormat::Int16; + preferredFormat.setSampleFormat(QAudioFormat::Int16); + } + + if (info->formats & SND_PCM_FMT_S32) + supportedSampleFormats << QAudioFormat::Int32; + + if (info->formats & SND_PCM_FMT_FLOAT) + supportedSampleFormats << QAudioFormat::Float; +} + +QnxAudioDeviceInfo::~QnxAudioDeviceInfo() +{ +} + +bool QnxAudioDeviceInfo::isFormatSupported(const QAudioFormat &format) const +{ + const HandleUniquePtr handle = openPcmDevice(id, mode); + + if (!handle) + return false; + + const std::optional<snd_pcm_channel_info_t> info = pcmChannelInfo(handle.get(), mode); + + if (!info) + return false; + + snd_pcm_channel_params_t params = formatToChannelParams(format, mode, info->max_fragment_size); + const int errorCode = snd_pcm_plugin_params(handle.get(), ¶ms); + + return errorCode == 0; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/qnx/qqnxaudiodevice_p.h b/src/multimedia/qnx/qqnxaudiodevice_p.h new file mode 100644 index 000000000..52c89c14f --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiodevice_p.h @@ -0,0 +1,35 @@ +// Copyright (C) 2016 Research In Motion +// Copyright (C) 2021 The Qt Company +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QNXAUDIODEVICE_P_H +#define QNXAUDIODEVICE_P_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/qaudiosystem_p.h" +#include <private/qaudiodevice_p.h> + +QT_BEGIN_NAMESPACE + +class QnxAudioDeviceInfo : public QAudioDevicePrivate +{ +public: + QnxAudioDeviceInfo(const QByteArray &deviceName, QAudioDevice::Mode mode); + ~QnxAudioDeviceInfo(); + + bool isFormatSupported(const QAudioFormat &format) const; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/qnx/qqnxaudiosink.cpp b/src/multimedia/qnx/qqnxaudiosink.cpp new file mode 100644 index 000000000..fa4b97ab6 --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiosink.cpp @@ -0,0 +1,516 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqnxaudiosink_p.h" + +#include <private/qaudiohelpers_p.h> +#include <sys/asoundlib.h> +#include <sys/asound_common.h> + +#include <algorithm> +#include <limits> + +#pragma GCC diagnostic ignored "-Wvla" + +QT_BEGIN_NAMESPACE + +QQnxAudioSink::QQnxAudioSink(const QAudioDevice &deviceInfo, QObject *parent) + : QPlatformAudioSink(parent) + , m_source(0) + , m_pushSource(false) + , m_timer(new QTimer(this)) + , m_error(QAudio::NoError) + , m_state(QAudio::StoppedState) + , m_suspendedInState(QAudio::SuspendedState) + , m_volume(1.0) + , m_periodSize(0) + , m_bytesWritten(0) + , m_requestedBufferSize(0) + , m_deviceInfo(deviceInfo) + , m_pcmNotifier(0) +{ + m_timer->setSingleShot(false); + m_timer->setInterval(20); + connect(m_timer, &QTimer::timeout, this, &QQnxAudioSink::pullData); + + const std::optional<snd_pcm_channel_info_t> info = QnxAudioUtils::pcmChannelInfo( + m_deviceInfo.id(), QAudioDevice::Output); + + if (info) + m_requestedBufferSize = info->max_fragment_size; +} + +QQnxAudioSink::~QQnxAudioSink() +{ + stop(); +} + +void QQnxAudioSink::start(QIODevice *source) +{ + if (m_state != QAudio::StoppedState) + stop(); + + m_source = source; + m_pushSource = false; + + if (open()) { + changeState(QAudio::ActiveState, QAudio::NoError); + m_timer->start(); + } else { + changeState(QAudio::StoppedState, QAudio::OpenError); + } +} + +QIODevice *QQnxAudioSink::start() +{ + if (m_state != QAudio::StoppedState) + stop(); + + m_source = new QnxPushIODevice(this); + m_source->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + m_pushSource = true; + + if (open()) { + changeState(QAudio::IdleState, QAudio::NoError); + } else { + changeState(QAudio::StoppedState, QAudio::OpenError); + } + + return m_source; +} + +void QQnxAudioSink::stop() +{ + if (m_state == QAudio::StoppedState) + return; + + changeState(QAudio::StoppedState, QAudio::NoError); + + close(); +} + +void QQnxAudioSink::reset() +{ + if (m_pcmHandle) +#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2) + snd_pcm_playback_drain(m_pcmHandle.get()); +#else + snd_pcm_channel_drain(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK); +#endif + stop(); +} + +void QQnxAudioSink::suspend() +{ + if (!m_pcmHandle) + return; + + snd_pcm_playback_pause(m_pcmHandle.get()); + suspendInternal(QAudio::SuspendedState); +} + +void QQnxAudioSink::resume() +{ + if (!m_pcmHandle) + return; + + snd_pcm_playback_resume(m_pcmHandle.get()); + resumeInternal(); +} + +void QQnxAudioSink::setBufferSize(qsizetype bufferSize) +{ + m_requestedBufferSize = std::clamp<qsizetype>(bufferSize, 0, std::numeric_limits<int>::max()); +} + +qsizetype QQnxAudioSink::bufferSize() const +{ + const std::optional<snd_pcm_channel_setup_t> setup = m_pcmHandle + ? QnxAudioUtils::pcmChannelSetup(m_pcmHandle.get(), QAudioDevice::Output) + : std::nullopt; + + return setup ? setup->buf.block.frag_size : m_requestedBufferSize; +} + +qsizetype QQnxAudioSink::bytesFree() const +{ + if (m_state != QAudio::ActiveState && m_state != QAudio::IdleState) + return 0; + + const std::optional<snd_pcm_channel_status_t> status = QnxAudioUtils::pcmChannelStatus( + m_pcmHandle.get(), QAudioDevice::Output); + + return status ? status->free : 0; +} + +qint64 QQnxAudioSink::processedUSecs() const +{ + return qint64(1000000) * m_format.framesForBytes(m_bytesWritten) / m_format.sampleRate(); +} + +QAudio::Error QQnxAudioSink::error() const +{ + return m_error; +} + +QAudio::State QQnxAudioSink::state() const +{ + return m_state; +} + +void QQnxAudioSink::setFormat(const QAudioFormat &format) +{ + if (m_state == QAudio::StoppedState) + m_format = format; +} + +QAudioFormat QQnxAudioSink::format() const +{ + return m_format; +} + +void QQnxAudioSink::setVolume(qreal volume) +{ + m_volume = qBound(qreal(0.0), volume, qreal(1.0)); +} + +qreal QQnxAudioSink::volume() const +{ + return m_volume; +} + +void QQnxAudioSink::updateState() +{ + const std::optional<snd_pcm_channel_status_t> status = QnxAudioUtils::pcmChannelStatus( + m_pcmHandle.get(), QAudioDevice::Output); + + if (!status) + return; + + if (state() == QAudio::ActiveState && status->underrun > 0) + changeState(QAudio::IdleState, QAudio::UnderrunError); + else if (state() == QAudio::IdleState && status->underrun == 0) + changeState(QAudio::ActiveState, QAudio::NoError); +} + +void QQnxAudioSink::pullData() +{ + if (m_state == QAudio::StoppedState + || m_state == QAudio::SuspendedState) + return; + + const int bytesAvailable = bytesFree(); + + // skip if we have less than 4ms of data + if (m_format.durationForBytes(bytesAvailable) < 4000) + return; + + const int frames = m_format.framesForBytes(bytesAvailable); + // The buffer is placed on the stack so no more than 64K or 1 frame + // whichever is larger. + const int maxFrames = qMax(m_format.framesForBytes(64 * 1024), 1); + const int bytesRequested = m_format.bytesForFrames(qMin(frames, maxFrames)); + + char buffer[bytesRequested]; + const int bytesRead = m_source->read(buffer, bytesRequested); + + // reading can take a while and stream may have been stopped + if (!m_pcmHandle) + return; + + if (bytesRead > 0) { + const qint64 bytesWritten = write(buffer, bytesRead); + + if (bytesWritten <= 0) { + close(); + changeState(QAudio::StoppedState, QAudio::FatalError); + } else if (bytesWritten != bytesRead) { + m_source->seek(m_source->pos()-(bytesRead-bytesWritten)); + } + } else { + // We're done + if (bytesRead == 0) + changeState(QAudio::IdleState, QAudio::NoError); + else + changeState(QAudio::IdleState, QAudio::IOError); + } +} + +bool QQnxAudioSink::open() +{ + if (!m_format.isValid() || m_format.sampleRate() <= 0) { + if (!m_format.isValid()) + qWarning("QQnxAudioSink: open error, invalid format."); + else + qWarning("QQnxAudioSink: open error, invalid sample rate (%d).", m_format.sampleRate()); + + return false; + } + + + m_pcmHandle = QnxAudioUtils::openPcmDevice(m_deviceInfo.id(), QAudioDevice::Output); + + if (!m_pcmHandle) + return false; + + int errorCode = 0; + + if ((errorCode = snd_pcm_nonblock_mode(m_pcmHandle.get(), 0)) < 0) { + qWarning("QQnxAudioSink: open error, couldn't set non block mode (0x%x)", -errorCode); + close(); + return false; + } + + addPcmEventFilter(); + + // Necessary so that bytesFree() which uses the "free" member of the status struct works + snd_pcm_plugin_set_disable(m_pcmHandle.get(), PLUGIN_MMAP); + + const std::optional<snd_pcm_channel_info_t> info = QnxAudioUtils::pcmChannelInfo( + m_pcmHandle.get(), QAudioDevice::Output); + + if (!info) { + close(); + return false; + } + + const int fragmentSize = std::clamp(m_requestedBufferSize, + info->min_fragment_size, info->max_fragment_size); + + snd_pcm_channel_params_t params = QnxAudioUtils::formatToChannelParams(m_format, + QAudioDevice::Output, fragmentSize); + + if ((errorCode = snd_pcm_plugin_params(m_pcmHandle.get(), ¶ms)) < 0) { + qWarning("QQnxAudioSink: open error, couldn't set channel params (0x%x)", -errorCode); + close(); + return false; + } + + if ((errorCode = snd_pcm_plugin_prepare(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK)) < 0) { + qWarning("QQnxAudioSink: open error, couldn't prepare channel (0x%x)", -errorCode); + close(); + return false; + } + + const std::optional<snd_pcm_channel_setup_t> setup = QnxAudioUtils::pcmChannelSetup( + m_pcmHandle.get(), QAudioDevice::Output); + + if (!setup) { + close(); + return false; + } + + m_periodSize = qMin(2048, setup->buf.block.frag_size); + m_bytesWritten = 0; + + createPcmNotifiers(); + + return true; +} + +void QQnxAudioSink::close() +{ + if (!m_pushSource) + m_timer->stop(); + + destroyPcmNotifiers(); + + if (m_pcmHandle) { +#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2) + snd_pcm_plugin_flush(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK); +#else + snd_pcm_plugin_drop(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK); +#endif + m_pcmHandle = nullptr; + } + + if (m_pushSource) { + delete m_source; + m_source = 0; + } +} + +void QQnxAudioSink::changeState(QAudio::State state, QAudio::Error error) +{ + if (m_state != state) { + m_state = state; + emit stateChanged(state); + } + + if (m_error != error) { + m_error = error; + emit errorChanged(error); + } +} + +qint64 QQnxAudioSink::pushData(const char *data, qint64 len) +{ + const QAudio::State s = state(); + + if (s == QAudio::StoppedState || s == QAudio::SuspendedState) + return 0; + + if (s == QAudio::IdleState) + changeState(QAudio::ActiveState, QAudio::NoError); + + qint64 totalWritten = 0; + + int retry = 0; + + constexpr int maxRetries = 10; + + while (totalWritten < len) { + const int bytesWritten = write(data + totalWritten, len - totalWritten); + + if (bytesWritten <= 0) { + ++retry; + + if (retry >= maxRetries) { + close(); + changeState(QAudio::StoppedState, QAudio::FatalError); + + return totalWritten; + } else { + continue; + } + } + + retry = 0; + + totalWritten += bytesWritten; + } + + return totalWritten; +} + +qint64 QQnxAudioSink::write(const char *data, qint64 len) +{ + if (!m_pcmHandle) + return 0; + + // Make sure we're writing (N * frame) worth of bytes + const int size = m_format.bytesForFrames(qBound(qint64(0), qint64(bytesFree()), len) / m_format.bytesPerFrame()); + + if (size == 0) + return 0; + + int written = 0; + + if (m_volume < 1.0f) { + char out[size]; + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, data, out, size); + written = snd_pcm_plugin_write(m_pcmHandle.get(), out, size); + } else { + written = snd_pcm_plugin_write(m_pcmHandle.get(), data, size); + } + + if (written > 0) { + m_bytesWritten += written; + return written; + } + + return 0; +} + +void QQnxAudioSink::suspendInternal(QAudio::State suspendState) +{ + if (!m_pushSource) + m_timer->stop(); + + m_suspendedInState = m_state; + changeState(suspendState, QAudio::NoError); +} + +void QQnxAudioSink::resumeInternal() +{ + changeState(m_suspendedInState, QAudio::NoError); + + m_timer->start(); +} + +QAudio::State suspendState(const snd_pcm_event_t &event) +{ + Q_ASSERT(event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS); + Q_ASSERT(event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED); + return QAudio::SuspendedState; +} + +void QQnxAudioSink::addPcmEventFilter() +{ + /* Enable PCM events */ + snd_pcm_filter_t filter; + memset(&filter, 0, sizeof(filter)); + filter.enable = (1<<SND_PCM_EVENT_AUDIOMGMT_STATUS) | + (1<<SND_PCM_EVENT_AUDIOMGMT_MUTE) | + (1<<SND_PCM_EVENT_OUTPUTCLASS) | + (1<<SND_PCM_EVENT_UNDERRUN); + snd_pcm_set_filter(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK, &filter); +} + +void QQnxAudioSink::createPcmNotifiers() +{ + // QSocketNotifier::Read for poll based event dispatcher. Exception for + // select based event dispatcher. + m_pcmNotifier = new QSocketNotifier(snd_pcm_file_descriptor(m_pcmHandle.get(), + SND_PCM_CHANNEL_PLAYBACK), + QSocketNotifier::Read, this); + connect(m_pcmNotifier, &QSocketNotifier::activated, + this, &QQnxAudioSink::pcmNotifierActivated); +} + +void QQnxAudioSink::destroyPcmNotifiers() +{ + if (m_pcmNotifier) { + delete m_pcmNotifier; + m_pcmNotifier = 0; + } +} + +void QQnxAudioSink::pcmNotifierActivated(int socket) +{ + Q_UNUSED(socket); + + snd_pcm_event_t pcm_event; + memset(&pcm_event, 0, sizeof(pcm_event)); + while (snd_pcm_channel_read_event(m_pcmHandle.get(), SND_PCM_CHANNEL_PLAYBACK, &pcm_event) == 0) { + if (pcm_event.type == SND_PCM_EVENT_AUDIOMGMT_STATUS) { + if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_SUSPENDED) + suspendInternal(suspendState(pcm_event)); + else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_RUNNING) + resumeInternal(); + else if (pcm_event.data.audiomgmt_status.new_status == SND_PCM_STATUS_PAUSED) + suspendInternal(QAudio::SuspendedState); + } else if (pcm_event.type == SND_PCM_EVENT_UNDERRUN) { + updateState(); + } + } +} + +QnxPushIODevice::QnxPushIODevice(QQnxAudioSink *output) + : QIODevice(output), + m_output(output) +{ +} + +QnxPushIODevice::~QnxPushIODevice() +{ +} + +qint64 QnxPushIODevice::readData(char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +qint64 QnxPushIODevice::writeData(const char *data, qint64 len) +{ + return m_output->pushData(data, len); +} + +bool QnxPushIODevice::isSequential() const +{ + return true; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/qnx/qqnxaudiosink_p.h b/src/multimedia/qnx/qqnxaudiosink_p.h new file mode 100644 index 000000000..1275121b3 --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiosink_p.h @@ -0,0 +1,119 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QNXAUDIOOUTPUT_H +#define QNXAUDIOOUTPUT_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/qaudiosystem_p.h" + +#include "qqnxaudioutils_p.h" + +#include <QElapsedTimer> +#include <QTimer> +#include <QIODevice> +#include <QSocketNotifier> + +#include <sys/asoundlib.h> +#include <sys/neutrino.h> + +QT_BEGIN_NAMESPACE + +class QnxPushIODevice; + +class QQnxAudioSink : public QPlatformAudioSink +{ + Q_OBJECT + +public: + explicit QQnxAudioSink(const QAudioDevice &deviceInfo, QObject *parent); + ~QQnxAudioSink(); + + void start(QIODevice *source) override; + QIODevice *start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesFree() const override; + void setBufferSize(qsizetype) 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 pushData(const char *data, qint64 len); + +private slots: + void pullData(); + void pcmNotifierActivated(int socket); + +private: + bool open(); + void close(); + void changeState(QAudio::State state, QAudio::Error error); + + void addPcmEventFilter(); + void createPcmNotifiers(); + void destroyPcmNotifiers(); + + void suspendInternal(QAudio::State suspendState); + void resumeInternal(); + + void updateState(); + + qint64 write(const char *data, qint64 len); + + QIODevice *m_source; + bool m_pushSource; + QTimer *m_timer; + + QAudio::Error m_error; + QAudio::State m_state; + QAudio::State m_suspendedInState; + QAudioFormat m_format; + qreal m_volume; + int m_periodSize; + + QnxAudioUtils::HandleUniquePtr m_pcmHandle; + qint64 m_bytesWritten; + + int m_requestedBufferSize; + + QAudioDevice m_deviceInfo; + + QSocketNotifier *m_pcmNotifier; +}; + +class QnxPushIODevice : public QIODevice +{ + Q_OBJECT +public: + explicit QnxPushIODevice(QQnxAudioSink *output); + ~QnxPushIODevice(); + + qint64 readData(char *data, qint64 len); + qint64 writeData(const char *data, qint64 len); + + bool isSequential() const override; + +private: + QQnxAudioSink *m_output; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/qnx/qqnxaudiosource.cpp b/src/multimedia/qnx/qqnxaudiosource.cpp new file mode 100644 index 000000000..becbaa0c0 --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiosource.cpp @@ -0,0 +1,376 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqnxaudiosource_p.h" + +#include <private/qaudiohelpers_p.h> + +#include <QDebug> + +QT_BEGIN_NAMESPACE + +QQnxAudioSource::QQnxAudioSource(const QAudioDevice &deviceInfo, QObject *parent) + : QPlatformAudioSource(parent) + , m_audioSource(0) + , m_pcmNotifier(0) + , m_error(QAudio::NoError) + , m_state(QAudio::StoppedState) + , m_bytesRead(0) + , m_elapsedTimeOffset(0) + , m_totalTimeValue(0) + , m_volume(qreal(1.0f)) + , m_bytesAvailable(0) + , m_bufferSize(0) + , m_periodSize(0) + , m_deviceInfo(deviceInfo) + , m_pullMode(true) +{ +} + +QQnxAudioSource::~QQnxAudioSource() +{ + close(); +} + +void QQnxAudioSource::start(QIODevice *device) +{ + if (m_state != QAudio::StoppedState) + close(); + + if (!m_pullMode && m_audioSource) + delete m_audioSource; + + m_pullMode = true; + m_audioSource = device; + + if (open()) + changeState(QAudio::ActiveState, QAudio::NoError); + else + changeState(QAudio::StoppedState, QAudio::OpenError); +} + +QIODevice *QQnxAudioSource::start() +{ + if (m_state != QAudio::StoppedState) + close(); + + if (!m_pullMode && m_audioSource) + delete m_audioSource; + + m_pullMode = false; + m_audioSource = new InputPrivate(this); + m_audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + + if (open()) { + changeState(QAudio::IdleState, QAudio::NoError); + } else { + delete m_audioSource; + m_audioSource = 0; + + changeState(QAudio::StoppedState, QAudio::OpenError); + } + + return m_audioSource; +} + +void QQnxAudioSource::stop() +{ + if (m_state == QAudio::StoppedState) + return; + + changeState(QAudio::StoppedState, QAudio::NoError); + close(); +} + +void QQnxAudioSource::reset() +{ + stop(); + m_bytesAvailable = 0; +} + +void QQnxAudioSource::suspend() +{ + if (m_state == QAudio::StoppedState) + return; + + snd_pcm_capture_pause(m_pcmHandle.get()); + + if (m_pcmNotifier) + m_pcmNotifier->setEnabled(false); + + changeState(QAudio::SuspendedState, QAudio::NoError); +} + +void QQnxAudioSource::resume() +{ + if (m_state == QAudio::StoppedState) + return; + + snd_pcm_capture_resume(m_pcmHandle.get()); + + if (m_pcmNotifier) + m_pcmNotifier->setEnabled(true); + + if (m_pullMode) + changeState(QAudio::ActiveState, QAudio::NoError); + else + changeState(QAudio::IdleState, QAudio::NoError); +} + +qsizetype QQnxAudioSource::bytesReady() const +{ + return qMax(m_bytesAvailable, 0); +} + +void QQnxAudioSource::setBufferSize(qsizetype bufferSize) +{ + m_bufferSize = bufferSize; +} + +qsizetype QQnxAudioSource::bufferSize() const +{ + return m_bufferSize; +} + +qint64 QQnxAudioSource::processedUSecs() const +{ + return qint64(1000000) * m_format.framesForBytes(m_bytesRead) / m_format.sampleRate(); +} + +QAudio::Error QQnxAudioSource::error() const +{ + return m_error; +} + +QAudio::State QQnxAudioSource::state() const +{ + return m_state; +} + +void QQnxAudioSource::setFormat(const QAudioFormat &format) +{ + if (m_state == QAudio::StoppedState) + m_format = format; +} + +QAudioFormat QQnxAudioSource::format() const +{ + return m_format; +} + +void QQnxAudioSource::setVolume(qreal volume) +{ + m_volume = qBound(qreal(0.0), volume, qreal(1.0)); +} + +qreal QQnxAudioSource::volume() const +{ + return m_volume; +} + +void QQnxAudioSource::userFeed() +{ + if (m_state == QAudio::StoppedState || m_state == QAudio::SuspendedState) + return; + + deviceReady(); +} + +bool QQnxAudioSource::deviceReady() +{ + if (m_pullMode) { + // reads some audio data and writes it to QIODevice + read(0, 0); + } else { + m_bytesAvailable = m_periodSize; + + // emits readyRead() so user will call read() on QIODevice to get some audio data + if (m_audioSource != 0) { + InputPrivate *input = qobject_cast<InputPrivate*>(m_audioSource); + input->trigger(); + } + } + + if (m_state != QAudio::ActiveState) + return true; + + return true; +} + +bool QQnxAudioSource::open() +{ + if (!m_format.isValid() || m_format.sampleRate() <= 0) { + if (!m_format.isValid()) + qWarning("QQnxAudioSource: open error, invalid format."); + else + qWarning("QQnxAudioSource: open error, invalid sample rate (%d).", m_format.sampleRate()); + + return false; + } + + m_pcmHandle = QnxAudioUtils::openPcmDevice(m_deviceInfo.id(), QAudioDevice::Input); + + if (!m_pcmHandle) + return false; + + int errorCode = 0; + + // Necessary so that bytesFree() which uses the "free" member of the status struct works + snd_pcm_plugin_set_disable(m_pcmHandle.get(), PLUGIN_MMAP); + + const std::optional<snd_pcm_channel_info_t> info = QnxAudioUtils::pcmChannelInfo( + m_pcmHandle.get(), QAudioDevice::Input); + + if (!info) { + close(); + return false; + } + + snd_pcm_channel_params_t params = QnxAudioUtils::formatToChannelParams(m_format, + QAudioDevice::Input, info->max_fragment_size); + + if ((errorCode = snd_pcm_plugin_params(m_pcmHandle.get(), ¶ms)) < 0) { + qWarning("QQnxAudioSource: open error, couldn't set channel params (0x%x)", -errorCode); + close(); + return false; + } + + if ((errorCode = snd_pcm_plugin_prepare(m_pcmHandle.get(), SND_PCM_CHANNEL_CAPTURE)) < 0) { + qWarning("QQnxAudioSource: open error, couldn't prepare channel (0x%x)", -errorCode); + close(); + return false; + } + + const std::optional<snd_pcm_channel_setup_t> setup = QnxAudioUtils::pcmChannelSetup( + m_pcmHandle.get(), QAudioDevice::Input); + + m_periodSize = qMin(2048, setup->buf.block.frag_size); + + m_elapsedTimeOffset = 0; + m_totalTimeValue = 0; + m_bytesRead = 0; + + m_pcmNotifier = new QSocketNotifier(snd_pcm_file_descriptor(m_pcmHandle.get(), SND_PCM_CHANNEL_CAPTURE), + QSocketNotifier::Read, this); + connect(m_pcmNotifier, SIGNAL(activated(QSocketDescriptor)), this, SLOT(userFeed())); + + return true; +} + +void QQnxAudioSource::close() +{ + if (m_pcmHandle) { +#if SND_PCM_VERSION < SND_PROTOCOL_VERSION('P',3,0,2) + snd_pcm_plugin_flush(m_pcmHandle.get(), SND_PCM_CHANNEL_CAPTURE); +#else + snd_pcm_plugin_drop(m_pcmHandle.get(), SND_PCM_CHANNEL_CAPTURE); +#endif + m_pcmHandle = nullptr; + } + + if (m_pcmNotifier) { + delete m_pcmNotifier; + m_pcmNotifier = 0; + } + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = 0; + } +} + +qint64 QQnxAudioSource::read(char *data, qint64 len) +{ + if (!m_pullMode && m_bytesAvailable == 0) + return 0; + + int errorCode = 0; + QByteArray tempBuffer(m_periodSize, 0); + + const int actualRead = snd_pcm_plugin_read(m_pcmHandle.get(), tempBuffer.data(), m_periodSize); + if (actualRead < 1) { + snd_pcm_channel_status_t status; + memset(&status, 0, sizeof(status)); + status.channel = SND_PCM_CHANNEL_CAPTURE; + if ((errorCode = snd_pcm_plugin_status(m_pcmHandle.get(), &status)) < 0) { + qWarning("QQnxAudioSource: read error, couldn't get plugin status (0x%x)", -errorCode); + close(); + changeState(QAudio::StoppedState, QAudio::FatalError); + return -1; + } + + if (status.status == SND_PCM_STATUS_READY + || status.status == SND_PCM_STATUS_OVERRUN) { + if ((errorCode = snd_pcm_plugin_prepare(m_pcmHandle.get(), SND_PCM_CHANNEL_CAPTURE)) < 0) { + qWarning("QQnxAudioSource: read error, couldn't prepare plugin (0x%x)", -errorCode); + close(); + changeState(QAudio::StoppedState, QAudio::FatalError); + return -1; + } + } + } else { + changeState(QAudio::ActiveState, QAudio::NoError); + } + + if (m_volume < 1.0f) + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, tempBuffer.data(), tempBuffer.data(), actualRead); + + m_bytesRead += actualRead; + + if (m_pullMode) { + m_audioSource->write(tempBuffer.data(), actualRead); + } else { + memcpy(data, tempBuffer.data(), qMin(static_cast<qint64>(actualRead), len)); + } + + m_bytesAvailable = 0; + + return actualRead; +} + +void QQnxAudioSource::changeState(QAudio::State state, QAudio::Error error) +{ + if (m_state != state) { + m_state = state; + emit stateChanged(state); + } + + if (m_error != error) { + m_error = error; + emit errorChanged(error); + } +} + +InputPrivate::InputPrivate(QQnxAudioSource *audio) + : m_audioDevice(audio) +{ +} + +qint64 InputPrivate::readData(char *data, qint64 len) +{ + return m_audioDevice->read(data, len); +} + +qint64 InputPrivate::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +qint64 InputPrivate::bytesAvailable() const +{ + return m_audioDevice->m_bytesAvailable + QIODevice::bytesAvailable(); +} + +bool InputPrivate::isSequential() const +{ + return true; +} + +void InputPrivate::trigger() +{ + emit readyRead(); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/qnx/qqnxaudiosource_p.h b/src/multimedia/qnx/qqnxaudiosource_p.h new file mode 100644 index 000000000..6b9cf61ac --- /dev/null +++ b/src/multimedia/qnx/qqnxaudiosource_p.h @@ -0,0 +1,113 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QNXAUDIOINPUT_H +#define QNXAUDIOINPUT_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/qaudiosystem_p.h" + +#include "qqnxaudioutils_p.h" + +#include <QSocketNotifier> +#include <QIODevice> +#include <QElapsedTimer> +#include <QTimer> + +#include <sys/asoundlib.h> + +QT_BEGIN_NAMESPACE + +class QQnxAudioSource : public QPlatformAudioSource +{ + Q_OBJECT + +public: + explicit QQnxAudioSource(const QAudioDevice &deviceInfo, QObject *parent); + ~QQnxAudioSource(); + + void start(QIODevice*) override; + QIODevice* start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + qsizetype bytesReady() const override; + void setBufferSize(qsizetype ) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat&) override; + QAudioFormat format() const override; + void setVolume(qreal) override; + qreal volume() const override; + +private slots: + void userFeed(); + bool deviceReady(); + +private: + friend class InputPrivate; + + bool open(); + void close(); + qint64 read(char *data, qint64 len); + void changeState(QAudio::State state, QAudio::Error error); + + QAudioFormat m_format; + + QIODevice *m_audioSource; + QnxAudioUtils::HandleUniquePtr m_pcmHandle; + QSocketNotifier *m_pcmNotifier; + + QAudio::Error m_error; + QAudio::State m_state; + + qint64 m_bytesRead; + qint64 m_elapsedTimeOffset; + qint64 m_totalTimeValue; + + qreal m_volume; + + int m_bytesAvailable; + int m_bufferSize; + int m_periodSize; + + QAudioDevice m_deviceInfo; + + bool m_pullMode; +}; + +class InputPrivate : public QIODevice +{ + Q_OBJECT +public: + InputPrivate(QQnxAudioSource *audio); + + qint64 readData(char *data, qint64 len) override; + qint64 writeData(const char *data, qint64 len) override; + + qint64 bytesAvailable() const override; + + bool isSequential() const override; + + void trigger(); + +private: + QQnxAudioSource *m_audioDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/qnx/qqnxaudioutils.cpp b/src/multimedia/qnx/qqnxaudioutils.cpp new file mode 100644 index 000000000..ddcb4f0de --- /dev/null +++ b/src/multimedia/qnx/qqnxaudioutils.cpp @@ -0,0 +1,148 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqnxaudioutils_p.h" + +QT_BEGIN_NAMESPACE + +namespace QnxAudioUtils +{ + +snd_pcm_channel_params_t formatToChannelParams(const QAudioFormat &format, QAudioDevice::Mode mode, int fragmentSize) +{ + snd_pcm_channel_params_t params; + memset(¶ms, 0, sizeof(params)); + params.channel = (mode == QAudioDevice::Output) ? SND_PCM_CHANNEL_PLAYBACK : SND_PCM_CHANNEL_CAPTURE; + params.mode = SND_PCM_MODE_BLOCK; + params.start_mode = SND_PCM_START_DATA; + params.stop_mode = SND_PCM_STOP_ROLLOVER; + params.buf.block.frag_size = fragmentSize; + params.buf.block.frags_min = 1; + params.buf.block.frags_max = 1; + strcpy(params.sw_mixer_subchn_name, "QAudio Channel"); + + params.format.interleave = 1; + params.format.rate = format.sampleRate(); + params.format.voices = format.channelCount(); + + switch (format.sampleFormat()) { + case QAudioFormat::UInt8: + params.format.format = SND_PCM_SFMT_U8; + break; + default: + // fall through + case QAudioFormat::Int16: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + params.format.format = SND_PCM_SFMT_S16_LE; +#else + params.format.format = SND_PCM_SFMT_S16_BE; +#endif + break; + case QAudioFormat::Int32: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + params.format.format = SND_PCM_SFMT_S32_LE; +#else + params.format.format = SND_PCM_SFMT_S32_BE; +#endif + break; + case QAudioFormat::Float: +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + params.format.format = SND_PCM_SFMT_FLOAT_LE; +#else + params.format.format = SND_PCM_SFMT_FLOAT_BE; +#endif + break; + } + + return params; +} + + +HandleUniquePtr openPcmDevice(const QByteArray &id, QAudioDevice::Mode mode) +{ + const int pcmMode = mode == QAudioDevice::Output + ? SND_PCM_OPEN_PLAYBACK + : SND_PCM_OPEN_CAPTURE; + + snd_pcm_t *handle; + + const int errorCode = snd_pcm_open_name(&handle, id.constData(), pcmMode); + + if (errorCode != 0) { + qWarning("Unable to open PCM device %s (0x%x)", id.constData(), -errorCode); + return {}; + } + + return HandleUniquePtr { handle }; +} + +template <typename T, typename Func> +std::optional<T> pcmChannelGetStruct(snd_pcm_t *handle, QAudioDevice::Mode mode, Func &&func) +{ + // initialize in-place to prevent an extra copy when returning + std::optional<T> t = { T{} }; + + t->channel = mode == QAudioDevice::Output + ? SND_PCM_CHANNEL_PLAYBACK + : SND_PCM_CHANNEL_CAPTURE; + + const int errorCode = func(handle, &(*t)); + + if (errorCode != 0) { + qWarning("QAudioDevice: couldn't get channel info (0x%x)", -errorCode); + return {}; + } + + return t; +} + +template <typename T, typename Func> +std::optional<T> pcmChannelGetStruct(const QByteArray &device, + QAudioDevice::Mode mode, Func &&func) +{ + const HandleUniquePtr handle = openPcmDevice(device, mode); + + if (!handle) + return {}; + + return pcmChannelGetStruct<T>(handle.get(), mode, std::forward<Func>(func)); +} + + +std::optional<snd_pcm_channel_info_t> pcmChannelInfo(snd_pcm_t *handle, QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_info_t>(handle, mode, snd_pcm_plugin_info); +} + +std::optional<snd_pcm_channel_info_t> pcmChannelInfo(const QByteArray &device, + QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_info_t>(device, mode, snd_pcm_plugin_info); +} + +std::optional<snd_pcm_channel_setup_t> pcmChannelSetup(snd_pcm_t *handle, QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_setup_t>(handle, mode, snd_pcm_plugin_setup); +} + +std::optional<snd_pcm_channel_setup_t> pcmChannelSetup(const QByteArray &device, + QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_setup_t>(device, mode, snd_pcm_plugin_setup); +} + +std::optional<snd_pcm_channel_status_t> pcmChannelStatus(snd_pcm_t *handle, QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_status_t>(handle, mode, snd_pcm_plugin_status); +} + +std::optional<snd_pcm_channel_status_t> pcmChannelStatus(const QByteArray &device, + QAudioDevice::Mode mode) +{ + return pcmChannelGetStruct<snd_pcm_channel_status_t>(device, mode, snd_pcm_plugin_status); +} + + +} // namespace QnxAudioUtils + +QT_END_NAMESPACE diff --git a/src/multimedia/qnx/qqnxaudioutils_p.h b/src/multimedia/qnx/qqnxaudioutils_p.h new file mode 100644 index 000000000..c0d2e5b1c --- /dev/null +++ b/src/multimedia/qnx/qqnxaudioutils_p.h @@ -0,0 +1,51 @@ +// Copyright (C) 2016 Research In Motion +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QNXAUDIOUTILS_H +#define QNXAUDIOUTILS_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/qaudiosystem_p.h" + +#include <memory> +#include <optional> + +#include <sys/asoundlib.h> + +QT_BEGIN_NAMESPACE + +namespace QnxAudioUtils +{ + snd_pcm_channel_params_t formatToChannelParams(const QAudioFormat &format, QAudioDevice::Mode mode, int fragmentSize); + + struct HandleDeleter + { + void operator()(snd_pcm_t *h) { if (h) snd_pcm_close(h); } + }; + + using HandleUniquePtr = std::unique_ptr<snd_pcm_t, HandleDeleter>; + HandleUniquePtr openPcmDevice(const QByteArray &id, QAudioDevice::Mode mode); + + std::optional<snd_pcm_channel_info_t> pcmChannelInfo(snd_pcm_t *handle, QAudioDevice::Mode mode); + std::optional<snd_pcm_channel_info_t> pcmChannelInfo(const QByteArray &device, QAudioDevice::Mode mode); + + std::optional<snd_pcm_channel_setup_t> pcmChannelSetup(snd_pcm_t *handle, QAudioDevice::Mode mode); + std::optional<snd_pcm_channel_setup_t> pcmChannelSetup(const QByteArray &device, QAudioDevice::Mode mode); + + std::optional<snd_pcm_channel_status_t> pcmChannelStatus(snd_pcm_t *handle, QAudioDevice::Mode mode); + std::optional<snd_pcm_channel_status_t> pcmChannelStatus(const QByteArray &device, QAudioDevice::Mode mode); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/qnx/qqnxmediadevices.cpp b/src/multimedia/qnx/qqnxmediadevices.cpp new file mode 100644 index 000000000..d9e33fcdc --- /dev/null +++ b/src/multimedia/qnx/qqnxmediadevices.cpp @@ -0,0 +1,70 @@ +// 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 "qqnxmediadevices_p.h" +#include "qmediadevices.h" +#include "private/qcameradevice_p.h" +#include "qcameradevice.h" + +#include "qqnxaudiosource_p.h" +#include "qqnxaudiosink_p.h" +#include "qqnxaudiodevice_p.h" + +#include <qdir.h> +#include <qdebug.h> + +QT_BEGIN_NAMESPACE + +static QList<QAudioDevice> enumeratePcmDevices(QAudioDevice::Mode mode) +{ + if (mode == QAudioDevice::Null) + return {}; + + QDir dir(QStringLiteral("/dev/snd")); + + dir.setFilter(QDir::Files); + dir.setSorting(QDir::Name); + + // QNX PCM devices names start with the pcm prefix and end either with the + // 'p' (playback) or 'c' (capture) suffix + + const char modeSuffix = mode == QAudioDevice::Input ? 'c' : 'p'; + + QList<QAudioDevice> devices; + + for (const QString &entry : dir.entryList()) { + if (entry.startsWith(QStringLiteral("pcm")) && entry.back() == modeSuffix) + devices << (new QnxAudioDeviceInfo(entry.toUtf8(), mode))->create(); + } + + return devices; +} + +QQnxMediaDevices::QQnxMediaDevices() + : QPlatformMediaDevices() +{ +} + +QList<QAudioDevice> QQnxMediaDevices::audioInputs() const +{ + return ::enumeratePcmDevices(QAudioDevice::Input); +} + +QList<QAudioDevice> QQnxMediaDevices::audioOutputs() const +{ + return ::enumeratePcmDevices(QAudioDevice::Output); +} + +QPlatformAudioSource *QQnxMediaDevices::createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QQnxAudioSource(deviceInfo, parent); +} + +QPlatformAudioSink *QQnxMediaDevices::createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QQnxAudioSink(deviceInfo, parent); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/qnx/qqnxmediadevices_p.h b/src/multimedia/qnx/qqnxmediadevices_p.h new file mode 100644 index 000000000..b8ccf5807 --- /dev/null +++ b/src/multimedia/qnx/qqnxmediadevices_p.h @@ -0,0 +1,39 @@ +// 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 QQNXMEDIADEVICES_H +#define QQNXMEDIADEVICES_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 <qaudio.h> +#include <qcameradevice.h> + +QT_BEGIN_NAMESPACE + +class QQnxMediaDevices : public QPlatformMediaDevices +{ +public: + QQnxMediaDevices(); + + 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; +}; + +QT_END_NAMESPACE + +#endif |