// 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 #include 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); // Set performance mode to low latency if mmap policy cannot be set if (params.direction == AAUDIO_DIRECTION_OUTPUT) { AAudioStreamBuilder_setPerformanceMode(m_builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); setMMapPolicy(2); // Also set MMap policy 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(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, ¤tState, stateChangeTimeout.count()); inputState = currentState; } return result; } template 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