Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
++
++ #include "qaudioengine_threaded_p.h"
++
++ #include <QtSpatialAudio/qambientsound.h>
++ #include <QtSpatialAudio/qaudiolistener.h>
++ #include <QtSpatialAudio/private/qambisonicdecoder_p.h>
++ #include <QtSpatialAudio/private/qaudioroom_p.h>
++ #include <QtSpatialAudio/private/qspatialaudiosound_p.h>
++ #include <QtMultimedia/qaudiosink.h>
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtMultimedia/private/qaudio_qspan_support_p.h>
++ #include <QtMultimedia/private/qaudiohelpers_p.h>
++ #include <QtCore/q20vector.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qelapsedtimer.h>
++ #include <QtCore/qiodevice.h>
++ #ifdef Q_OS_WIN
++ # include <QtMultimedia/private/qwindows_wasapi_warmup_client_p.h>
++ #endif
++
++ #include <resonance_audio.h>
++
++ #include <memory>
++
++ QT_BEGIN_NAMESPACE
++
++ // We'd like to have short buffer times, so the sound adjusts itself to changes
++ // quickly, but times below 100ms seem to give stuttering on macOS.
++ // It might be possible to set this value lower on other OSes.
++ const int bufferTimeMs = 100;
++
++ // This class lives in the audioThread, but pulls data from QAudioEnginePrivate
++ // which lives in the mainThread.
++ class QAudioOutputStream : public QIODevice
++ {
++ public:
++ explicit QAudioOutputStream(QAudioEngineThreaded *d) : d(d) { open(QIODevice::ReadOnly); }
++ ~QAudioOutputStream() override;
++
++ qint64 readData(char *data, qint64 len) override;
++
++ qint64 writeData(const char *, qint64) override;
++
++ qint64 size() const override { return 0; }
++ qint64 bytesAvailable() const override {
++ return std::numeric_limits<qint64>::max();
++ }
++ bool isSequential() const override {
++ return true;
++ }
++ bool atEnd() const override {
++ return false;
++ }
++ qint64 pos() const override {
++ return m_pos;
++ }
++
++ void startOutput()
++ {
++ d->mutex.lock();
++ Q_ASSERT(!sink);
++ auto channelConfig = d->outputMode() == QAudioEngine::Surround
++ ? d->m_device.channelConfiguration()
++ : QAudioFormat::ChannelConfigStereo;
++
++ QAudioFormat format;
++ if (channelConfig != QAudioFormat::ChannelConfigUnknown)
++ format.setChannelConfig(channelConfig);
++ else
++ format.setChannelCount(d->m_device.preferredFormat().channelCount());
++ format.setSampleRate(d->sampleRate());
++ format.setSampleFormat(QAudioFormat::Int16);
++
++ ambisonicDecoder = std::make_unique<QAmbisonicDecoder>(
++ QAmbisonicDecoder::AmbisonicOrder::HighQuality, format.sampleRate(),
++ format.channelCount(), format.channelConfig());
++
++ sink = std::make_unique<QAudioSink>(d->m_device, format);
++ const qsizetype bufferSize = format.bytesForDuration(bufferTimeMs * 1000);
++ sink->setBufferSize(bufferSize);
++ d->mutex.unlock();
++ // It is important to unlock the mutex before starting the sink, as the sink will
++ // call readData() in the audio thread, which will try to lock the mutex (again)
++ sink->start(this);
++
++ #ifdef Q_OS_WIN
++ QtMultimediaPrivate::refreshWarmupClient();
++ #endif
++ }
++
++ void stopOutput()
++ {
++ if (!sink)
++ return;
++ sink->stop();
++ sink.reset();
++ ambisonicDecoder.reset();
++ }
++
++ void setPaused(bool paused) {
++ if (paused)
++ sink->suspend();
++ else
++ sink->resume();
++ }
++
++ private:
++ qint64 m_pos = 0;
++ QAudioEngineThreaded *d = nullptr;
++ std::unique_ptr<QAudioSink> sink;
++ std::unique_ptr<QAmbisonicDecoder> ambisonicDecoder;
++ };
++
++ QAudioOutputStream::~QAudioOutputStream() = default;
++
++ qint64 QAudioOutputStream::writeData(const char *, qint64)
++ {
++ return 0;
++ }
++
++
++ qint64 QAudioOutputStream::readData(char *data, const qint64 len)
++ {
++ constexpr int framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
++ static constexpr std::array<float, 2 * framesPerBuffer> nullBuffer{};
++
++ if (d->m_paused.loadRelaxed())
++ return 0;
++
++ QSpan<short> outputBuffer((short *)data, len / sizeof(short));
++
++ QMutexLocker l(&d->mutex);
++
++ int nChannels = ambisonicDecoder ? ambisonicDecoder->nOutputChannels() : 2;
++ if (outputBuffer.size() < nChannels * framesPerBuffer)
++ return 0;
++
++ using QtMultimediaPrivate::drop;
++ using QtMultimediaPrivate::take;
++ using namespace QAudioHelperInternal;
++
++ const std::unique_ptr<vraudio::ResonanceAudioApi> &api = d->resonanceAudio->api;
++
++ bool ok = true;
++ while (outputBuffer.size() >= nChannels * framesPerBuffer) {
++
++ // Fill input buffers
++ for (auto &&[source, playbackState] : d->playbackStates) {
++ Q_ASSERT(source->nchannels <= 2);
++ if (playbackState) {
++ Q_ASSERT(playbackState->format().channelCount() <= 2);
++ std::array<float, 2 * framesPerBuffer> buf;
++
++ playbackState->getBuffer(
++ take(QSpan<float>{ buf },
++ playbackState->format().channelCount() * framesPerBuffer));
++ api->SetInterleavedBuffer(source->sourceId, buf.data(), source->nchannels,
++ framesPerBuffer);
++ } else {
++ api->SetInterleavedBuffer(source->sourceId, nullBuffer.data(), source->nchannels,
++ framesPerBuffer);
++ }
++ }
++
++ if (ambisonicDecoder && d->outputMode() == QAudioEngine::Surround) {
++ std::array<const float *, QAmbisonicDecoder::maxAmbisonicChannels> channels;
++ std::array<const float *, 2> reverbBuffers{};
++ int nFrames = d->resonanceAudio->getAmbisonicOutput(
++ channels.data(), reverbBuffers.data(), ambisonicDecoder->nInputChannels());
++
++ if (nFrames < 0) {
++ // If we get here, it means that resonanceAudio did not actually fill the buffer.
++ // Sometimes this is expected, for example if resonanceAudio does not have any sources.
++ // In this case we just fill the buffer with silence.
++ std::fill(outputBuffer.begin(), outputBuffer.end(), 0);
++ break;
++ }
++
++ Q_ASSERT(ambisonicDecoder->nOutputChannels() <= 8);
++ int nSamples = ambisonicDecoder->outputSamples(nFrames);
++
++ constexpr size_t reverbBufferSize =
++ framesPerBuffer * QAmbisonicDecoder::maxAmbisonicChannels;
++ std::array<float, reverbBufferSize> reverbFloatBuffers;
++ QSpan<float> reverbOutputSpan = take(QSpan{ reverbFloatBuffers }, nSamples);
++ QSpan<short> currentOutput = take(outputBuffer, nSamples);
++
++ ambisonicDecoder->processBufferWithReverb(
++ QSpan{ channels.data(), ambisonicDecoder->nInputChannels() }, reverbBuffers,
++ reverbOutputSpan);
++
++ convertSampleFormat(as_bytes(reverbOutputSpan), NativeSampleFormat::float32_t,
++ as_writable_bytes(currentOutput), NativeSampleFormat::int16_t);
++ outputBuffer = drop(outputBuffer, nSamples);
++ } else {
++ QSpan<short> currentOutput = take(outputBuffer, nChannels * framesPerBuffer);
++ ok = d->resonanceAudio->api->FillInterleavedOutputBuffer(2, framesPerBuffer,
++ currentOutput.data());
++ if (!ok) {
++ // If we get here, it means that resonanceAudio did not actually fill the buffer.
++ // Sometimes this is expected, for example if resonanceAudio does not have any sources.
++ // In this case we just fill the buffer with silence.
++ if (d->playbackStates.empty()) {
++ std::fill(currentOutput.begin(), currentOutput.end(), 0);
++ } else {
++ // If we get here, it means that something unexpected happened, so bail.
++ qWarning() << " Reading failed!";
++ break;
++ }
++ }
++ outputBuffer = drop(outputBuffer, nChannels * framesPerBuffer);
++ }
++ }
++
++ qint64 bytesProcessed = len - outputBuffer.size_bytes();
++ m_pos += bytesProcessed;
++ return bytesProcessed;
++ }
++
++ QAudioEngineThreaded::QAudioEngineThreaded(int sampleRate) : QAudioEnginePrivate(sampleRate)
++ {
++ audioThread.setObjectName(u"QAudioThread");
++ m_device = QMediaDevices::defaultAudioOutput();
++ }
++
++ QAudioEngineThreaded::~QAudioEngineThreaded()
++ {
++ stop();
++ }
++
++ void QAudioEngineThreaded::start()
++ {
++ if (outputStream)
++ return; // already started
++
++ outputStream = std::make_unique<QAudioOutputStream>(this);
++ outputStream->moveToThread(&audioThread);
++ audioThread.start(QThread::TimeCriticalPriority);
++
++ QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::startOutput);
++ }
++
++ void QAudioEngineThreaded::stop()
++ {
++ if (!outputStream)
++ return; // already stopped
++
++ QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::stopOutput,
++ Qt::BlockingQueuedConnection);
++ outputStream.reset();
++ audioThread.exit(0);
++ audioThread.wait();
++ }
++
++ void QAudioEngineThreaded::setPaused(bool paused)
++ {
++ if (!outputStream)
++ return; // can't pause if not started
++
++ bool old = m_paused.fetchAndStoreRelaxed(paused);
++ if (old != paused) {
++ if (outputStream)
++ outputStream->setPaused(paused);
++ Q_Q(QAudioEngine);
++ Q_EMIT q->pausedChanged();
++ }
++ }
++
++ bool QAudioEngineThreaded::isPaused() const
++ {
++ return m_paused.loadRelaxed();
++ }
++
++ void QAudioEngineThreaded::setOutputDevice(const QAudioDevice &device)
++ {
++ if (m_device == device)
++ return;
++ if (outputStream) {
++ qWarning() << "Changing device on a running engine not implemented";
++ return;
++ }
++ m_device = device;
++ Q_Q(QAudioEngine);
++ Q_EMIT q->outputDeviceChanged();
++ }
++
++ void QAudioEngineThreaded::addSound(QSpatialAudioSoundPrivate *sound)
++ {
++ std::lock_guard l(mutex);
++ playbackStates.emplace(sound, nullptr);
++ }
++
++ void QAudioEngineThreaded::removeSound(QSpatialAudioSoundPrivate *sound)
++ {
++ std::lock_guard l(mutex);
++ playbackStates.erase(sound);
++ }
++
++ void QAudioEngineThreaded::setSoundPlaybackData(QSpatialAudioSoundPrivate *sound,
++ SharedPlaybackState state)
++ {
++ std::lock_guard l(mutex);
++ playbackStates.insert_or_assign(sound, std::move(state));
++ }
++
++ void QAudioEngineThreaded::updateRoomEffects()
++ {
++ std::lock_guard l(mutex);
++ for (auto [sound, key] : playbackStates)
++ sound->updateRoomEffects();
++ }
++
++ QT_END_NAMESPACE