Revision control
Copy as Markdown
// 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"