Revision control
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtMultimediaTestLib/private/osdetection_p.h>
++ #include <QtTest/qsignalspy.h>
++ #include <QtTest/qtest.h>
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
++ #include <QtMultimedia/private/qplatformmediaintegration_p.h>
++ #include <QtMultimedia/private/qrtaudioengine_p.h>
++ #include <QtCore/qsemaphore.h>
++
++ #if __cplusplus > 201703L
++ # define QT_SET_HAS_CONTAINS
++ #endif
++
++ using namespace Qt::Literals;
++ using namespace std::chrono_literals;
++
++ // NOLINTBEGIN(readability-convert-member-functions-to-static)
++ // NOLINTBEGIN(readability-redundant-access-specifiers)
++
++ using QtMultimediaPrivate::QRtAudioEngine;
++ using QtMultimediaPrivate::QRtAudioEngineVoice;
++ using QtMultimediaPrivate::VoiceId;
++ using QtMultimediaPrivate::VoicePlayResult;
++
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ namespace {
++
++ struct NullMockVoice : public QRtAudioEngineVoice
++ {
++ NullMockVoice(QAudioFormat fmt, VoiceId id) : QRtAudioEngineVoice(id), m_format(fmt) { }
++ ~NullMockVoice() override = default;
++
++ bool isActive() noexcept Q_DECL_NONBLOCKING_FUNCTION override { return m_active; }
++ const QAudioFormat &format() noexcept override { return m_format; }
++ VoicePlayResult play(QSpan<float> buffer) noexcept Q_DECL_NONBLOCKING_FUNCTION override
++ {
++ m_sampleCount += buffer.size();
++ ranges::fill(buffer, 0);
++ return m_result;
++ }
++
++ void setActive(bool v) { m_active = v; }
++ void setVoicePlayResult(VoicePlayResult v) { m_result = v; }
++
++ // data
++ const QAudioFormat m_format;
++ bool m_active = true;
++ uint64_t m_sampleCount{};
++ VoicePlayResult m_result = VoicePlayResult::Playing;
++ };
++
++ } // namespace
++
++ static bool isPipewireBackend()
++ {
++ return QPlatformMediaIntegration::audioBackendName() == "PipeWire";
++ }
++
++ static bool isPulseAudioBackend()
++ {
++ return QPlatformMediaIntegration::audioBackendName() == "PulseAudio";
++ }
++
++ class tst_QRtAudioEngine : public QObject
++ {
++ Q_OBJECT
++ public:
++ tst_QRtAudioEngine(QObject *parent = nullptr) : QObject(parent) { }
++
++ private slots:
++ void initTestCase()
++ {
++ if (QMediaDevices::defaultAudioOutput().isNull())
++ QSKIP("No audio outputs found");
++
++ if (isMacOS || isWindows || isPipewireBackend() || isPulseAudioBackend())
++ return;
++ QSKIP("Skipping QRtAudioEngine tests on this platform: callback interface is not "
++ "supported");
++ }
++
++ void play_visit_and_stop_voice();
++ void play_and_stop_byInactive();
++ void play_and_stop_byPlaybackStatus();
++
++ void play_validateDuration();
++ void visit_overloadCommandBuffers();
++
++ private:
++ // helpers
++ static QAudioFormat getFormat()
++ {
++ QAudioDevice device = QMediaDevices::defaultAudioOutput();
++ auto format = device.preferredFormat();
++ format.setSampleFormat(QAudioFormat::Float);
++ if (format.channelCount() > 2) {
++ format.setChannelCount(2);
++ format.setChannelConfig(QAudioFormat::ChannelConfigStereo);
++ }
++ return format;
++ }
++
++ static std::shared_ptr<QRtAudioEngine> makeEngine()
++ {
++ return std::make_shared<QRtAudioEngine>(QMediaDevices::defaultAudioOutput(), getFormat());
++ }
++
++ static std::shared_ptr<NullMockVoice> makeMockVoice(QAudioFormat fmt)
++ {
++ auto voice = std::make_shared<NullMockVoice>(fmt, QRtAudioEngine::allocateVoiceId());
++ return voice;
++ }
++ };
++
++ void tst_QRtAudioEngine::play_visit_and_stop_voice()
++ {
++ std::shared_ptr<QRtAudioEngine> engine = makeEngine();
++
++ auto voice = makeMockVoice(getFormat());
++ engine->play(voice);
++ QCOMPARE(engine->voices().size(), 1);
++ #ifdef QT_SET_HAS_CONTAINS
++ QVERIFY(engine->voices().contains(voice));
++ #endif
++
++ QSemaphore sem;
++ engine->visitVoiceRt(voice, [&sem](NullMockVoice &) {
++ sem.release();
++ });
++
++ sem.acquire();
++
++ QSignalSpy finishedSpy(engine.get(), &QRtAudioEngine::voiceFinished);
++ engine->stop(voice);
++
++ finishedSpy.wait();
++ QCOMPARE(finishedSpy.front().front().value<VoiceId>(), voice->voiceId());
++ QCOMPARE(engine->voices().size(), 0);
++ }
++
++ void tst_QRtAudioEngine::play_and_stop_byInactive()
++ {
++ std::shared_ptr<QRtAudioEngine> engine = makeEngine();
++
++ auto voice = makeMockVoice(getFormat());
++ engine->play(voice);
++ QCOMPARE(engine->voices().size(), 1);
++
++ QSignalSpy finishedSpy(engine.get(), &QRtAudioEngine::voiceFinished);
++ engine->visitVoiceRt(voice, [](NullMockVoice &voice) {
++ voice.setActive(false);
++ });
++
++ finishedSpy.wait();
++ QCOMPARE(finishedSpy.front().front().value<VoiceId>(), voice->voiceId());
++ QCOMPARE(engine->voices().size(), 0);
++ }
++
++ void tst_QRtAudioEngine::play_and_stop_byPlaybackStatus()
++ {
++ std::shared_ptr<QRtAudioEngine> engine = makeEngine();
++
++ auto voice = makeMockVoice(getFormat());
++ engine->play(voice);
++ QCOMPARE(engine->voices().size(), 1);
++
++ QSignalSpy finishedSpy(engine.get(), &QRtAudioEngine::voiceFinished);
++ engine->visitVoiceRt(voice, [](NullMockVoice &voice) {
++ voice.setVoicePlayResult(VoicePlayResult::Finished);
++ });
++
++ finishedSpy.wait();
++ QCOMPARE(finishedSpy.front().front().value<VoiceId>(), voice->voiceId());
++ QCOMPARE(engine->voices().size(), 0);
++ }
++
++ void tst_QRtAudioEngine::play_validateDuration()
++ {
++ std::shared_ptr<QRtAudioEngine> engine = makeEngine();
++
++ auto voice = makeMockVoice(getFormat());
++ QSignalSpy finishedSpy(engine.get(), &QRtAudioEngine::voiceFinished);
++ engine->play(voice);
++
++ QTest::qWait(1s);
++
++ engine->stop(voice);
++ finishedSpy.wait();
++
++ auto samplesForDuration = [&](auto duration) -> uint64_t {
++ auto format = getFormat();
++ return format.framesForDuration(std::chrono::microseconds(duration).count())
++ * format.channelCount();
++ };
++
++ auto lowerBound = samplesForDuration(600ms);
++ auto upperBound = samplesForDuration(1'400ms);
++
++ QCOMPARE_LT(lowerBound, voice->m_sampleCount);
++ QCOMPARE_LT(voice->m_sampleCount, upperBound);
++ }
++
++ void tst_QRtAudioEngine::visit_overloadCommandBuffers()
++ {
++ std::shared_ptr<QRtAudioEngine> engine = makeEngine();
++
++ auto voice = makeMockVoice(getFormat());
++ QSignalSpy finishedSpy(engine.get(), &QRtAudioEngine::voiceFinished);
++ engine->play(voice);
++
++ constexpr size_t commandCount = 16384; // 8 times the command buffer size
++
++ size_t iteration = 0;
++ auto refcountedDummyObject = std::make_shared<int>(0);
++ for (size_t i = 0; i < commandCount; ++i) {
++ engine->visitVoiceRt(
++ voice,
++ [&, payload = std::array<char, 128>{}, refcountedDummyObject](NullMockVoice &) {
++ Q_UNUSED(payload);
++ iteration += 1;
++ });
++ }
++
++ QTRY_COMPARE_EQ(iteration, commandCount); // all commands should have been executed
++ QTRY_COMPARE_EQ(refcountedDummyObject.use_count(),
++ 1); // all commands should have been destroyed on the app thread
++ }
++
++ QTEST_MAIN(tst_QRtAudioEngine)
++
++ #include "tst_qrtaudioengine.moc"