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 "qrtaudioengine_p.h"
++
++ #include <QtMultimedia/private/qaudiosystem_p.h>
++ #include <QtMultimedia/private/qmemory_resource_tlsf_p.h>
++ #include <QtMultimedia/private/qtmm_rtsan_support_p.h>
++ #include <QtCore/q20map.h>
++ #include <QtCore/qcoreapplication.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qmutex.h>
++ #include <QtCore/qthread.h>
++
++ #include <mutex>
++
++ #ifdef Q_CC_MINGW
++ // mingw-13.1 seems to have a false positive when using std::function inside a std::variant
++ QT_WARNING_PUSH
++ QT_WARNING_DISABLE_GCC("-Wmaybe-uninitialized")
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtMultimediaPrivate {
++
++ using namespace QtPrivate;
++ using namespace std::chrono_literals;
++
++ static QtAudio::State sinkStateToEngineState(QtAudio::State state)
++ {
++ switch (state) {
++ case QtAudio::ActiveState:
++ case QtAudio::IdleState:
++ case QtAudio::SuspendedState:
++ return QtAudio::ActiveState;
++ case QtAudio::StoppedState:
++ return QtAudio::StoppedState;
++ default:
++ Q_UNREACHABLE_RETURN(QtAudio::StoppedState);
++ }
++ }
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ QRtAudioEngine::QRtAudioEngine(const QAudioDevice &device, const QAudioFormat &format,
++ std::optional<AudioEndpointRole> role,
++ std::optional<NativePeriodFrames> nativePeriodFrames)
++ : QRtAudioEngine{ MoveToApplicationThread::No, device, format, role, nativePeriodFrames }
++ {
++ }
++
++ QRtAudioEngine::QRtAudioEngine(MoveToApplicationThread moveToApplicationThread,
++ const QAudioDevice &device, const QAudioFormat &format,
++ std::optional<AudioEndpointRole> role,
++ std::optional<NativePeriodFrames> nativePeriodFrames)
++ : m_sink{
++ device,
++ format,
++ },
++ m_rtMemoryPool {
++ std::make_unique<QTlsfMemoryResource>(poolSize)
++ }
++ {
++ m_notificationEvent.callOnActivated([this] {
++ runNonRtNotifications();
++ });
++
++ if (moveToApplicationThread == MoveToApplicationThread::Yes && !QThread::isMainThread()) {
++ QThread *appThread = qApp->thread();
++ moveToThread(appThread);
++ m_sink.moveToThread(appThread);
++ m_notificationEvent.moveToThread(appThread);
++ m_pendingCommandsTimer.moveToThread(appThread);
++ }
++
++ m_pendingCommandsTimer.setInterval(10ms);
++ m_pendingCommandsTimer.setTimerType(Qt::CoarseTimer);
++ m_pendingCommandsTimer.callOnTimeout(&m_pendingCommandsTimer, [this] {
++ auto lock = std::lock_guard{ m_mutex };
++ sendPendingRtCommands();
++ if (m_appToRtOverflowBuffer.empty())
++ m_pendingCommandsTimer.stop();
++ });
++
++ QPlatformAudioSink *platformSink = QPlatformAudioSink::get(m_sink);
++
++ if (role)
++ platformSink->setRole(*role);
++ if (nativePeriodFrames)
++ platformSink->setNativePeriodFrames(*nativePeriodFrames);
++
++ m_sink.start([this](QSpan<float> outputBuffer) {
++ audioCallback(outputBuffer);
++ });
++
++ // we start suspended
++ m_sink.suspend();
++
++ QObject::connect(&m_sink, &QAudioSink::stateChanged, this, [&](QtAudio::State state) {
++ Q_EMIT stateChanged(sinkStateToEngineState(state));
++ });
++ }
++
++ QRtAudioEngine::~QRtAudioEngine()
++ {
++ m_sink.reset();
++
++ // consume the ringbuffers
++ m_appToRt.consumeAll([](auto) {
++ });
++ m_rtToApp.consumeAll([](auto) {
++ });
++ }
++
++ void QRtAudioEngine::play(SharedVoice voice)
++ {
++ auto lock = std::lock_guard{ m_mutex };
++
++ // TODO: where do we expect reampling to happen?
++ Q_ASSERT(voice->format() == m_sink.format());
++
++ if (m_voices.empty())
++ m_sink.resume();
++
++ m_voices.insert(voice);
++
++ sendAppToRtCommand(PlayCommand{
++ std::move(voice),
++ });
++ }
++
++ void QRtAudioEngine::stop(const SharedVoice &voice)
++ {
++ stop(voice->voiceId());
++ }
++
++ void QRtAudioEngine::stop(VoiceId voiceId)
++ {
++ auto lock = std::lock_guard{ m_mutex };
++ sendAppToRtCommand(StopCommand{ voiceId });
++ }
++
++ void QRtAudioEngine::visitVoiceRt(VoiceId voiceId, RtVoiceVisitor fn, bool visitorIsTrivial)
++ {
++ auto lock = std::lock_guard{ m_mutex };
++
++ if (visitorIsTrivial) {
++ sendAppToRtCommand(VisitCommandTrivial{
++ voiceId,
++ std::move(fn),
++ });
++
++ } else {
++ sendAppToRtCommand(VisitCommand{
++ voiceId,
++ std::move(fn),
++ });
++ }
++ }
++
++ VoiceId QRtAudioEngine::allocateVoiceId()
++ {
++ static std::atomic_uint64_t allocator{ 0 };
++ return VoiceId{ allocator.fetch_add(1, std::memory_order_relaxed) };
++ }
++
++ QtAudio::State QRtAudioEngine::audioState() const
++ {
++ return sinkStateToEngineState(m_sink.state());
++ }
++
++ void QRtAudioEngine::audioCallback(QSpan<float> outputBuffer) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ runRtCommands();
++ bool sendNotification = sendPendingAppNotifications();
++
++ std::fill(outputBuffer.begin(), outputBuffer.end(), 0.f);
++
++ std::vector<SharedVoice, pmr::polymorphic_allocator<SharedVoice>> finishedVoices{
++ m_rtMemoryPool.get(),
++ };
++
++ for (const SharedVoice &voice : m_rtVoiceRegistry) {
++ Q_ASSERT(voice.use_count() >= 2); // voice in both m_rtVoiceRegistry and m_voices
++
++ VoicePlayResult playResult = voice->play(outputBuffer);
++ if (playResult == VoicePlayResult::Finished)
++ finishedVoices.push_back(voice);
++ }
++
++ if (!finishedVoices.empty()) {
++ withRTSanDisabled([&] {
++ for (const SharedVoice &voice : finishedVoices) {
++ m_rtVoiceRegistry.erase(voice);
++ bool stopSent = sendRtToAppNotification(StopNotification{ voice });
++ if (stopSent)
++ sendNotification = true;
++ }
++ });
++ }
++
++ // TODO: we should probably (soft)clip the output buffer
++
++ cleanupRetiredVoices();
++ if (sendNotification)
++ m_notificationEvent.set();
++ }
++
++ void QRtAudioEngine::cleanupRetiredVoices() noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ bool notifyApp = false;
++
++ #if __cpp_lib_erase_if >= 202002L
++ using std::erase_if;
++ #else
++ auto erase_if = [](auto &c, auto &&pred) {
++ auto old_size = c.size();
++ for (auto first = c.begin(), last = c.end(); first != last;) {
++ if (pred(*first))
++ first = c.erase(first);
++ else
++ ++first;
++ }
++ return old_size - c.size();
++ };
++ #endif
++ withRTSanDisabled([&] {
++ erase_if(m_rtVoiceRegistry, [&](const SharedVoice &voice) {
++ bool voiceIsActive = voice->isActive();
++ if (!voiceIsActive)
++ notifyApp = sendRtToAppNotification(StopNotification{ voice });
++
++ return !voiceIsActive;
++ });
++ });
++
++ if (notifyApp)
++ m_notificationEvent.set();
++ }
++
++ void QRtAudioEngine::runRtCommands() noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ m_appToRt.consumeAll([&](QSpan<RtCommand> commands) {
++ for (RtCommand &cmd : commands) {
++ std::visit([&](auto cmd) {
++ runRtCommand(std::move(cmd));
++ }, std::move(cmd));
++ }
++ });
++ }
++
++ void QRtAudioEngine::runRtCommand(PlayCommand cmd) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ withRTSanDisabled([&] {
++ m_rtVoiceRegistry.insert(cmd.voice);
++ });
++ }
++
++ void QRtAudioEngine::runRtCommand(StopCommand cmd) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ auto it = m_rtVoiceRegistry.find(cmd.voiceId);
++ if (it == m_rtVoiceRegistry.end())
++ return;
++
++ SharedVoice voice = *it;
++ m_rtVoiceRegistry.erase(it);
++
++ bool emitNotify = sendRtToAppNotification(StopNotification{
++ std::move(voice),
++ });
++ if (emitNotify)
++ m_notificationEvent.set();
++ }
++
++ void QRtAudioEngine::runRtCommand(VisitCommand cmd) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ auto it = m_rtVoiceRegistry.find(cmd.voiceId);
++ if (it == m_rtVoiceRegistry.end())
++ return;
++
++ cmd.callback(**it);
++
++ // send callback back to application for destruction
++ bool emitNotify = sendRtToAppNotification(VisitReply{
++ std::move(cmd.callback),
++ });
++ if (emitNotify)
++ m_notificationEvent.set();
++ }
++
++ void QRtAudioEngine::runRtCommand(VisitCommandTrivial cmd) noexcept Q_DECL_NONBLOCKING_FUNCTION
++ {
++ auto it = m_rtVoiceRegistry.find(cmd.voiceId);
++ if (it == m_rtVoiceRegistry.end())
++ return;
++
++ cmd.callback(**it);
++ }
++
++ void QRtAudioEngine::runNonRtNotifications()
++ {
++ std::vector<VoiceId> finishedVoices;
++ {
++ auto lock = std::lock_guard{ m_mutex };
++ m_rtToApp.consumeAll([&](QSpan<Notification> notifications) {
++ for (Notification &notification : notifications) {
++ std::visit([&](auto notification) {
++ runNonRtNotification(std::move(notification));
++ }, std::move(notification));
++ }
++ });
++
++ finishedVoices = std::move(m_finishedVoices);
++ m_finishedVoices.clear();
++ }
++
++ // emit voiceFinished outside of the lock
++ for (VoiceId voiceId : finishedVoices)
++ Q_EMIT voiceFinished(voiceId);
++ }
++
++ void QRtAudioEngine::runNonRtNotification(StopNotification notification)
++ {
++ m_voices.erase(notification.voice);
++ if (m_voices.empty())
++ m_sink.suspend();
++ m_finishedVoices.push_back(notification.voice->voiceId());
++ }
++
++ void QRtAudioEngine::runNonRtNotification(VisitReply)
++ {
++ // nop (just making sure to delete on the application thread);
++ }
++
++ void QRtAudioEngine::sendAppToRtCommand(RtCommand cmd)
++ {
++ // first write all pending commands from overflow buffer
++ sendPendingRtCommands();
++
++ bool written = m_appToRt.produceOne([&] {
++ return std::move(cmd);
++ });
++
++ if (written)
++ return;
++
++ m_appToRtOverflowBuffer.emplace_back(std::move(cmd));
++
++ QMetaObject::invokeMethod(&m_pendingCommandsTimer, [this] {
++ if (!m_pendingCommandsTimer.isActive())
++ m_pendingCommandsTimer.start();
++ });
++ }
++
++ bool QRtAudioEngine::sendRtToAppNotification(Notification cmd)
++ {
++ // first write all pending commands from overflow buffer
++ bool emitNotification = sendPendingAppNotifications();
++
++ bool written = m_rtToApp.produceOne([&] {
++ return std::move(cmd);
++ });
++
++ if (written)
++ return true;
++
++ m_rtToAppOverflowBuffer.emplace_back(std::move(cmd));
++
++ return emitNotification;
++ }
++
++ void QRtAudioEngine::sendPendingRtCommands()
++ {
++ while (!m_appToRtOverflowBuffer.empty()) {
++ Q_UNLIKELY_BRANCH;
++ bool written = m_appToRt.produceOne([&] {
++ return std::move(m_appToRtOverflowBuffer.front());
++ });
++ if (!written)
++ return;
++
++ m_appToRtOverflowBuffer.pop_front();
++ }
++ }
++
++ bool QRtAudioEngine::sendPendingAppNotifications()
++ {
++ bool emitNotification = false;
++ while (!m_rtToAppOverflowBuffer.empty()) {
++ Q_UNLIKELY_BRANCH;
++
++ // first write all pending commands from overflow buffer
++ bool written = m_rtToApp.produceOne([&] {
++ return std::move(m_rtToAppOverflowBuffer.front());
++ });
++ if (!written)
++ break;
++
++ m_rtToAppOverflowBuffer.pop_front();
++ emitNotification = true;
++ }
++
++ return emitNotification;
++ }
++
++ } // namespace QtMultimediaPrivate
++
++ QT_END_NAMESPACE
++
++ #ifdef Q_CC_MINGW
++ QT_WARNING_POP
++ #endif
++
++ #include "moc_qrtaudioengine_p.cpp"