Revision control

++ // 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 "qandroidaudiosink_p.h"
++
++ #include "qandroidaudioutil_p.h"
++
++ #include <QtMultimedia/private/qaudiohelpers_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtAAudio {
++
++ Q_STATIC_LOGGING_CATEGORY(qLcAndroidAudioSink, "qt.multimedia.android.audiosink")
++
++ QAndroidAudioSinkStream::QAndroidAudioSinkStream(QAudioDevice device, const QAudioFormat &format,
++ std::optional<qsizetype> ringbufferSize,
++ QAndroidAudioSink *parent, float volume,
++ std::optional<NativePeriodFrames> nativePeriodFrames,
++ AudioEndpointRole role)
++ : QtMultimediaPrivate::QPlatformAudioSinkStream(std::move(device), format, ringbufferSize,
++ nativePeriodFrames, volume),
++ m_parent(parent),
++ m_role(role)
++ {
++ QtAAudio::StreamBuilder builder(format);
++
++ qCDebug(qLcAndroidAudioSink) << "Creating sink for device id:" << m_audioDevice.id()
++ << ", description:" << m_audioDevice.description();
++
++ builder.deviceId = m_audioDevice.id().toInt();
++
++ // Set buffer parameters
++ builder.bufferCapacity = m_nativePeriodFrames
++ ? qToUnderlying(*m_nativePeriodFrames)
++ : 1024;
++
++ // NOTE: AAudio doesn't support UINT8, so convert to INT16 if that's requested
++ if (format.sampleFormat() == QAudioFormat::UInt8) {
++ m_hostFormat = format;
++ m_hostFormat->setSampleFormat(QAudioFormat::Int16);
++ }
++
++ // Set builder parameters for audio sink
++ builder.params.sharingMode = AAUDIO_SHARING_MODE_SHARED;
++ builder.params.direction = AAUDIO_DIRECTION_OUTPUT;
++ switch (m_role) {
++ case QtMultimediaPrivate::AudioEndpointRole::SoundEffect:
++ builder.params.outputUsage = AAUDIO_USAGE_GAME;
++ builder.params.outputContentType = AAUDIO_CONTENT_TYPE_SONIFICATION;
++ break;
++ case QtMultimediaPrivate::AudioEndpointRole::Accessibility:
++ builder.params.outputUsage = AAUDIO_USAGE_ASSISTANCE_ACCESSIBILITY;
++ builder.params.outputContentType = AAUDIO_CONTENT_TYPE_SPEECH;
++ break;
++ case QtMultimediaPrivate::AudioEndpointRole::MediaPlayback:
++ case QtMultimediaPrivate::AudioEndpointRole::Other:
++ builder.params.outputUsage = AAUDIO_USAGE_MEDIA;
++ builder.params.outputContentType = AAUDIO_CONTENT_TYPE_MUSIC;
++ break;
++ }
++
++ if (QAndroidAudioUtil::supportsLowLatency() && !QAndroidAudioUtil::isBluetoothDevice(m_audioDevice))
++ builder.params.performanceMode = AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
++
++ builder.userData = this;
++ builder.callback = [](AAudioStream *, void *userData, void *audioData,
++ int32_t numFrames) -> int {
++ auto *stream = reinterpret_cast<QAndroidAudioSinkStream *>(userData);
++ Q_ASSERT(stream);
++ QSpan<std::byte> audioSpan = stream->getHostSpan(audioData, numFrames);
++ return stream->m_audioCallback ? stream->processCallback(audioSpan)
++ : stream->processRingbuffer(audioSpan, numFrames);
++ };
++ builder.errorCallback = [](AAudioStream *, void *userData, aaudio_result_t error) -> void {
++ auto *stream = reinterpret_cast<QAndroidAudioSinkStream *>(userData);
++ Q_ASSERT(stream);
++ stream->handleError(error);
++ };
++
++ builder.setupBuilder();
++ m_stream = std::make_unique<QtAAudio::Stream>(builder);
++ if (builder.format.sampleFormat() != format.sampleFormat()) {
++ // Original sample format unsupported, so doing sample format conversion
++ Q_ASSERT(builder.format.sampleFormat() == QAudioFormat::Float);
++ m_hostFormat = builder.format;
++ }
++ }
++
++ bool QAndroidAudioSinkStream::open()
++ {
++ if (!m_stream->isOpen()) {
++ qCWarning(qLcAndroidAudioSink) << "Stream null";
++ requestStop();
++ return false;
++ }
++
++ if (!m_stream->areStreamParametersRespected())
++ qCWarning(qLcAndroidAudioSink) << "Stream parameters not correct";
++
++ return true;
++ }
++
++ bool QAndroidAudioSinkStream::start(QIODevice *device)
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ setQIODevice(device);
++ pullFromQIODevice();
++ createQIODeviceConnections(device);
++
++ // TODO: Fill host ringbuffer before starting
++ if (!m_stream->start()) {
++ requestStop();
++ return false;
++ }
++
++ return true;
++ }
++
++ QIODevice *QAndroidAudioSinkStream::start()
++ {
++ auto *writer = createRingbufferWriterDevice();
++ return start(writer) ? writer : nullptr;
++ }
++
++ bool QAndroidAudioSinkStream::start(AudioCallback cb)
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ m_audioCallback = std::move(cb);
++
++ if (!m_stream->start()) {
++ requestStop();
++ return false;
++ }
++
++ return true;
++ }
++
++ void QAndroidAudioSinkStream::suspend()
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ m_stream->stop();
++ }
++
++ void QAndroidAudioSinkStream::resume()
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ m_stream->start();
++ }
++
++ void QAndroidAudioSinkStream::stop(ShutdownPolicy policy)
++ {
++ requestStop();
++ disconnectQIODeviceConnections();
++
++ switch (policy) {
++ case ShutdownPolicy::DrainRingbuffer:
++ stop();
++ break;
++ case ShutdownPolicy::DiscardRingbuffer:
++ reset();
++ break;
++ default:
++ Q_UNREACHABLE_RETURN();
++ }
++ }
++
++ void QAndroidAudioSinkStream::stop()
++ {
++ if (isIdle() || m_audioCallback)
++ return reset();
++
++ stopIdleDetection();
++ connectIdleHandler([this] {
++ Q_ASSERT(thread()->isCurrentThread());
++ if (!isIdle()) // Only handle <not idle> -> <idle> transitions
++ return;
++
++ // We have written everything we want to write, synchronous stop on application thread
++ m_stream->stop();
++ m_self = nullptr; // might delete the instance
++ });
++
++ m_parent = nullptr;
++
++ // Take ownership of self to avoid deletion until AAudio stream is stopped
++ m_self = shared_from_this();
++ }
++
++ void QAndroidAudioSinkStream::reset()
++ {
++ Q_ASSERT(thread()->isCurrentThread());
++ m_stream->stop();
++ }
++
++ void QAndroidAudioSinkStream::updateStreamIdle(bool arg)
++ {
++ if (m_parent)
++ m_parent->updateStreamIdle(arg);
++ }
++
++ QSpan<std::byte>
++ QAndroidAudioSinkStream::getHostSpan(void *audioData,
++ int numFrames) const noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ qsizetype byteAmount = m_hostFormat ? m_hostFormat->bytesForFrames(numFrames)
++ : m_format.bytesForFrames(numFrames);
++ return QSpan{ reinterpret_cast<std::byte *>(audioData), byteAmount };
++ }
++
++ aaudio_data_callback_result_t
++ QAndroidAudioSinkStream::processRingbuffer(QSpan<std::byte> audioSpan,
++ int numFrames) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ auto consumedFrames = m_hostFormat
++ ? QPlatformAudioSinkStream::process(
++ audioSpan, numFrames,
++ QAudioHelperInternal::toNativeSampleFormat(m_hostFormat->sampleFormat()))
++ : QPlatformAudioSinkStream::process(audioSpan, numFrames);
++ if (consumedFrames != static_cast<uint64_t>(numFrames) && isStopRequested())
++ return AAUDIO_CALLBACK_RESULT_STOP;
++
++ return AAUDIO_CALLBACK_RESULT_CONTINUE;
++ }
++
++ aaudio_data_callback_result_t
++ QAndroidAudioSinkStream::processCallback(QSpan<std::byte> audioSpan) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ if (isStopRequested())
++ return AAUDIO_CALLBACK_RESULT_STOP;
++
++ if (m_hostFormat)
++ QtMultimediaPrivate::runAudioCallback(*m_audioCallback, audioSpan, m_format, volume(),
++ *m_hostFormat);
++ else
++ QtMultimediaPrivate::runAudioCallback(*m_audioCallback, audioSpan, m_format, volume());
++
++ return AAUDIO_CALLBACK_RESULT_CONTINUE;
++ }
++
++ void QAndroidAudioSinkStream::handleError(aaudio_result_t)
++ {
++ // Handle as IO error which closes the stream
++ // TODO: Check for underruns
++ requestStop();
++ invokeOnAppThread([this] {
++ // clang-format off
++ handleIOError(m_parent);
++ // clang-format on
++ });
++ }
++
++ QAndroidAudioSink::QAndroidAudioSink(QAudioDevice device, const QAudioFormat &fmt, QObject *parent)
++ : BaseClass(std::move(device), fmt, parent)
++ {
++ }
++
++ QAndroidAudioSink::~QAndroidAudioSink()
++ = default;
++
++ } // namespace QtAAudio
++
++ QT_END_NAMESPACE