Revision control

++ // Copyright (C) 2025 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 "qaaudiostream_p.h"
++
++ #include <chrono>
++ #include <dlfcn.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace {
++
++ using namespace std::chrono_literals;
++ constexpr std::chrono::nanoseconds stateChangeTimeout = 1s;
++ constexpr int bufferSizeInBursts = 3; // Triple buffering
++
++ static aaudio_format_t aaudioFormat(const QAudioFormat::SampleFormat sampleFormat)
++ {
++ switch (sampleFormat) {
++ case QAudioFormat::Int16:
++ case QAudioFormat::UInt8: // NOTE: Requires conversion
++ return AAUDIO_FORMAT_PCM_I16;
++ case QAudioFormat::Int32:
++ return AAUDIO_FORMAT_PCM_I32;
++ case QAudioFormat::Float:
++ return AAUDIO_FORMAT_PCM_FLOAT;
++ case QAudioFormat::Unknown:
++ case QAudioFormat::NSampleFormats:
++ qWarning() << "Sample format not supported by AAudio, needs converting";
++ return AAUDIO_FORMAT_INVALID;
++ }
++ }
++
++ void setMMapPolicy(int policy)
++ {
++ if (QNativeInterface::QAndroidApplication::sdkVersion() < 36)
++ return;
++
++ auto handle = dlopen("libaaudio.so", RTLD_NOW);
++ Q_ASSERT(handle);
++ auto guard = qScopeGuard([handle] {
++ dlclose(handle);
++ });
++
++ auto getPolicy = (int (*)(void))dlsym(handle, "AAudio_getMMapPolicy");
++ Q_ASSERT(getPolicy);
++ auto currentPolicy = getPolicy();
++ if (currentPolicy == policy)
++ return; // No need to set
++
++ auto setPolicy = (aaudio_result_t (*)(int))dlsym(handle, "AAudio_setMMapPolicy");
++ Q_ASSERT(setPolicy);
++ setPolicy(policy);
++ };
++
++ } // namespace
++
++ namespace QtAAudio {
++
++ Q_LOGGING_CATEGORY(qLcAAudioStream, "qt.multimedia.android.aaudiostream") // FIXME: Deprecated
++
++ StreamBuilder::StreamBuilder(QAudioFormat format)
++ : format{ format }
++ {
++ AAudio_createStreamBuilder(&m_builder);
++ Q_ASSERT(m_builder);
++ }
++
++ StreamBuilder::~StreamBuilder()
++ {
++ AAudioStreamBuilder_delete(m_builder);
++ }
++
++ void StreamBuilder::setupBuilder()
++ {
++ // Set device
++ AAudioStreamBuilder_setDeviceId(m_builder, deviceId);
++
++ // Set buffer capacity
++ AAudioStreamBuilder_setBufferCapacityInFrames(m_builder, bufferCapacity);
++ // TODO: Tuning buffer size at run-time?
++
++ // Set parameters from audio format
++ AAudioStreamBuilder_setSampleRate(m_builder, format.sampleRate());
++ AAudioStreamBuilder_setChannelCount(m_builder, format.channelCount());
++ AAudioStreamBuilder_setFormat(m_builder, aaudioFormat(format.sampleFormat()));
++
++ // Set callbacks
++ AAudioStreamBuilder_setDataCallback(m_builder, callback, userData);
++ AAudioStreamBuilder_setErrorCallback(m_builder, errorCallback, userData);
++
++ // Set other parameters if not default
++ StreamParameterSet defaultParams;
++ if (params.sharingMode != defaultParams.sharingMode)
++ AAudioStreamBuilder_setSharingMode(m_builder, params.sharingMode);
++ if (params.direction == AAUDIO_DIRECTION_OUTPUT) {
++ if (params.performanceMode != defaultParams.performanceMode)
++ AAudioStreamBuilder_setPerformanceMode(m_builder, params.performanceMode);
++ setMMapPolicy(2); // Ensure MMap policy is set to AUTO
++ if (params.outputUsage != defaultParams.outputUsage)
++ AAudioStreamBuilder_setUsage(m_builder, params.outputUsage);
++ if (params.outputContentType != defaultParams.outputContentType)
++ AAudioStreamBuilder_setContentType(m_builder, params.outputContentType);
++ } else {
++ AAudioStreamBuilder_setDirection(m_builder, params.direction);
++ if (params.inputPreset != defaultParams.inputPreset)
++ AAudioStreamBuilder_setInputPreset(m_builder, params.inputPreset);
++ }
++ }
++
++ Stream::Stream(StreamBuilder &builder)
++ {
++ // Request stream open
++ auto result = AAudioStreamBuilder_openStream(builder.m_builder, &m_stream);
++ if (result == AAUDIO_ERROR_INVALID_FORMAT && builder.format.sampleFormat() != QAudioFormat::Float) {
++ // Try opening with a float sample format, which is more likely to be supported
++ // NOTE: We should check sample format after opening in sink/source to adjust
++ // nativeSampleFormat
++ builder.format.setSampleFormat(QAudioFormat::Float);
++ builder.setupBuilder();
++ result = AAudioStreamBuilder_openStream(builder.m_builder, &m_stream);
++ }
++
++ if (result != AAUDIO_OK) {
++ qCWarning(qLcAAudioStream)
++ << "Opening stream failed:" << AAudio_convertResultToText(result);
++ if (isOpen())
++ close();
++ return;
++ }
++
++ qCDebug(qLcAAudioStream) << "Stream opened:" << m_stream << "for device"
++ << AAudioStream_getDeviceId(m_stream);
++
++ // Verify parameters
++ if (builder.deviceId != AAUDIO_UNSPECIFIED
++ && AAudioStream_getDeviceId(m_stream) != builder.deviceId) {
++ qCWarning(qLcAAudioStream) << "Stream device not correct";
++ if (isOpen())
++ close();
++ return;
++ }
++
++ Q_ASSERT(AAudioStream_getSampleRate(m_stream) == builder.format.sampleRate()
++ && AAudioStream_getChannelCount(m_stream) == builder.format.channelCount()
++ && AAudioStream_getFormat(m_stream) == aaudioFormat(builder.format.sampleFormat()));
++
++ StreamParameterSet defaultParams;
++ if ((builder.params.sharingMode == defaultParams.sharingMode
++ || AAudioStream_getSharingMode(m_stream) == builder.params.sharingMode)
++ && (builder.params.direction == defaultParams.direction
++ || AAudioStream_getDirection(m_stream) == builder.params.direction)
++ && (builder.params.outputUsage == defaultParams.outputUsage
++ || AAudioStream_getUsage(m_stream) == builder.params.outputUsage)
++ && (builder.params.outputContentType == defaultParams.outputContentType
++ || AAudioStream_getContentType(m_stream) == builder.params.outputContentType))
++ m_areStreamParametersRespected = true;
++
++ if (builder.params.direction == AAUDIO_DIRECTION_OUTPUT
++ && AAudioStream_getPerformanceMode(m_stream) != AAUDIO_PERFORMANCE_MODE_LOW_LATENCY)
++ qCWarning(qLcAAudioStream) << "Low latency performance mode not set";
++
++ // Set buffer size
++ auto burstSize = AAudioStream_getFramesPerBurst(m_stream);
++ AAudioStream_setBufferSizeInFrames(m_stream, burstSize * bufferSizeInBursts);
++ }
++
++ Stream::~Stream()
++ {
++ if (isOpen())
++ close();
++ }
++
++ bool Stream::start()
++ {
++ auto result = requestWithExpectedState(AAudioStream_requestStart, AAUDIO_STREAM_STATE_STARTED);
++ return result == AAUDIO_OK;
++ }
++
++ void Stream::stop()
++ {
++ // Plays pending data and stops
++ requestWithExpectedState(AAudioStream_requestStop, AAUDIO_STREAM_STATE_STOPPED);
++ }
++
++ void Stream::pause()
++ {
++ // Will freeze data flow
++ requestWithExpectedState(AAudioStream_requestPause, AAUDIO_STREAM_STATE_PAUSED);
++ }
++
++ void Stream::flush()
++ {
++ // Discards pending data
++ requestWithExpectedState(AAudioStream_requestFlush, AAUDIO_STREAM_STATE_FLUSHED);
++ }
++
++ bool Stream::isOpen() const
++ {
++ return static_cast<bool>(m_stream);
++ }
++
++ bool Stream::areStreamParametersRespected() const
++ {
++ return m_areStreamParametersRespected;
++ }
++
++ void Stream::close()
++ {
++ Q_ASSERT(isOpen());
++
++ AAudioStream_close(m_stream);
++ m_stream = nullptr;
++ }
++
++ aaudio_result_t Stream::waitForTargetState(aaudio_stream_state_t targetState)
++ {
++ Q_ASSERT(isOpen());
++
++ aaudio_result_t result = AAUDIO_OK;
++ aaudio_stream_state_t currentState = AAudioStream_getState(m_stream);
++ aaudio_stream_state_t inputState = currentState;
++
++ while (result == AAUDIO_OK && currentState != targetState) {
++ qCDebug(qLcAAudioStream) << "Waiting for state change:"
++ << AAudio_convertStreamStateToText(currentState) << " -> "
++ << AAudio_convertStreamStateToText(targetState);
++ result = AAudioStream_waitForStateChange(m_stream, inputState, &currentState,
++ stateChangeTimeout.count());
++ inputState = currentState;
++ }
++
++ return result;
++ }
++
++ template <typename Functor>
++ aaudio_result_t Stream::requestWithExpectedState(Functor &&request, aaudio_stream_state_t expected)
++ {
++ auto result = request(m_stream);
++ if (result != AAUDIO_OK) {
++ qCWarning(qLcAAudioStream) << "Request failed:" << AAudio_convertResultToText(result);
++ return result;
++ }
++
++ return waitForTargetState(expected);
++ }
++
++ } // namespace QtAAudio
++
++ QT_END_NAMESPACE