Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtMultimediaTestLib/private/qmessagespy_p.h>
#include <QtSpatialAudio/qambientsound.h>
#include <QtSpatialAudio/qaudioengine.h>
#include <QtSpatialAudio/qaudiolistener.h>
#include <QtSpatialAudio/qaudioroom.h>
#include <QtSpatialAudio/qspatialsound.h>
#include <QtSpatialAudio/private/qtspatialaudioglobal_p.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
#include <QtTest/qtesteventloop.h>
#include <QtMultimedia/qaudio.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtCore/qglobal.h>
#include <memory>
using namespace Qt::Literals;
using namespace std::chrono_literals;
using QtMultimediaPrivate::QLoggingCategoryEnabler;
using QtMultimediaPrivate::QMessageSpy;
class tst_QSpatialAudio : public QObject
{
Q_OBJECT
public:
tst_QSpatialAudio(QObject* parent=nullptr) : QObject(parent) {}
private slots:
void init();
void cleanup();
void initTestCase();
void test_QSpatialSound_basicProperties();
void test_QSpatialSound_rotation_and_directivityOrder_semantics();
void test_engine_positionDistanceScale();
void test_QSpatialSound_signalsEmittedOnChange();
void testEngineIntegration();
void testSetSource_autoPlay();
void testQAmbientSound_basic();
void testQAudioRoom_basic();
void testQAudioListener_basic();
void testSwitchAudioDevice();
void test_nullEngine_behaviour();
void testPlaybackControls();
void testFileLoading_replaceDuringPlayback();
void testFileLoading_loadInvalidFile();
void testAutoPlayTransitions();
void test_destroyRoomDuringPlayback();
void test_destroyListenerDuringPlayback();
void test_destroySpatialDuringPlayback();
void test_destroyAmbientDuringPlayback();
private:
std::unique_ptr<QSpatialSound> sound;
std::unique_ptr<QAudioEngine> engine;
std::optional<QLoggingCategoryEnabler> logEnabler;
QUrl url;
QUrl url2;
};
void tst_QSpatialAudio::init()
{
engine = std::make_unique<QAudioEngine>();
sound = std::make_unique<QSpatialSound>(engine.get());
logEnabler.emplace(qLcSpatialAudioEngine());
}
void tst_QSpatialAudio::cleanup()
{
sound.reset();
engine.reset();
logEnabler.reset();
}
void tst_QSpatialAudio::initTestCase()
{
// locate test audio file but do not fail tests if not present; playback tests will skip
QString testFileName = QStringLiteral("test.wav");
QString fullPath = QFINDTESTDATA(testFileName);
if (!fullPath.isEmpty())
url = QUrl::fromLocalFile(fullPath);
QString testFileName2 = QStringLiteral("test2.wav");
QString fullPath2 = QFINDTESTDATA(testFileName2);
if (!fullPath2.isEmpty())
url2 = QUrl::fromLocalFile(fullPath2);
}
void tst_QSpatialAudio::test_QSpatialSound_basicProperties()
{
// verify defaults and simple setters/getters
QCOMPARE(sound->loops(), 1);
QCOMPARE(sound->autoPlay(), true);
QSignalSpy loopsSpy(sound.get(), &QSpatialSound::loopsChanged);
sound->setLoops(QSpatialSound::Infinite);
QCOMPARE(sound->loops(), -1);
QCOMPARE(loopsSpy.size(), 1);
sound->setLoops(1);
QCOMPARE(loopsSpy.size(), 2);
QSignalSpy autoPlaySpy(sound.get(), &QSpatialSound::autoPlayChanged);
sound->setAutoPlay(false);
QCOMPARE(sound->autoPlay(), false);
QCOMPARE(autoPlaySpy.size(), 1);
sound->setAutoPlay(true);
QCOMPARE(autoPlaySpy.size(), 2);
QSignalSpy volumeSpy(sound.get(), &QSpatialSound::volumeChanged);
sound->setVolume(0.5f);
QCOMPARE(sound->volume(), 0.5f);
QCOMPARE(volumeSpy.size(), 1);
QSignalSpy directivitySpy(sound.get(), &QSpatialSound::directivityChanged);
sound->setDirectivity(0.2f);
QCOMPARE(sound->directivity(), 0.2f);
QCOMPARE(directivitySpy.size(), 1);
QSignalSpy directivityOrderSpy(sound.get(), &QSpatialSound::directivityOrderChanged);
sound->setDirectivityOrder(2.0f);
QCOMPARE(sound->directivityOrder(), 2.0f);
QCOMPARE(directivityOrderSpy.size(), 1);
QSignalSpy nfSpy(sound.get(), &QSpatialSound::nearFieldGainChanged);
sound->setNearFieldGain(0.3f);
QCOMPARE(sound->nearFieldGain(), 0.3f);
QCOMPARE(nfSpy.size(), 1);
QSignalSpy manualSpy(sound.get(), &QSpatialSound::manualAttenuationChanged);
sound->setManualAttenuation(0.7f);
QCOMPARE(sound->manualAttenuation(), 0.7f);
QCOMPARE(manualSpy.size(), 1);
}
void tst_QSpatialAudio::test_engine_positionDistanceScale()
{
// choose meter scale
engine->setDistanceScale(QAudioEngine::DistanceScaleMeter);
QVector3D pos(1.0f, 2.0f, 3.0f);
QSignalSpy spy(sound.get(), &QSpatialSound::positionChanged);
sound->setPosition(pos);
QCOMPARE(sound->position(), pos);
QCOMPARE(spy.size(), 1);
const int before = spy.size();
sound->setPosition(pos);
QCOMPARE(spy.size(), before);
}
void tst_QSpatialAudio::test_QSpatialSound_signalsEmittedOnChange()
{
QSignalSpy vspy(sound.get(), &QSpatialSound::volumeChanged);
sound->setVolume(0.25f);
QCOMPARE(vspy.size(), 1);
sound->setVolume(0.25f);
QCOMPARE(vspy.size(), 1);
QSignalSpy dspy(sound.get(), &QSpatialSound::distanceModelChanged);
sound->setDistanceModel(QSpatialSound::DistanceModel::Linear);
QCOMPARE(dspy.size(), 1);
sound->setDistanceModel(QSpatialSound::DistanceModel::Linear);
QCOMPARE(dspy.size(), 1);
QSignalSpy rotationSpy(sound.get(), &QSpatialSound::rotationChanged);
QQuaternion q(1, 1, 0, 0);
sound->setRotation(q);
QCOMPARE(rotationSpy.size(), 1);
sound->setRotation(q);
QCOMPARE(rotationSpy.size(), 1);
// Verify other property signals follow the expected "emit on change" semantics.
QSignalSpy loopsSpy(sound.get(), &QSpatialSound::loopsChanged);
sound->setLoops(QSpatialSound::Infinite);
QCOMPARE(loopsSpy.size(), 1);
sound->setLoops(QSpatialSound::Infinite);
QCOMPARE(loopsSpy.size(), 1);
QSignalSpy autoPlaySpy(sound.get(), &QSpatialSound::autoPlayChanged);
sound->setAutoPlay(false);
QCOMPARE(autoPlaySpy.size(), 1);
sound->setAutoPlay(false);
QCOMPARE(autoPlaySpy.size(), 1);
// size and distanceCutoff use the engine's distanceScale; ensure an
// engine is present before exercising those properties.
sound = {};
engine = std::make_unique<QAudioEngine>();
sound = std::make_unique<QSpatialSound>(engine.get());
QSignalSpy sizeSpy(sound.get(), &QSpatialSound::sizeChanged);
sound->setSize(2.5f);
QCOMPARE(sizeSpy.size(), 1);
sound->setSize(2.5f);
QCOMPARE(sizeSpy.size(), 1);
QSignalSpy cutoffSpy(sound.get(), &QSpatialSound::distanceCutoffChanged);
sound->setDistanceCutoff(3.5f);
QCOMPARE(cutoffSpy.size(), 1);
sound->setDistanceCutoff(3.5f);
QCOMPARE(cutoffSpy.size(), 1);
QSignalSpy manualSpy(sound.get(), &QSpatialSound::manualAttenuationChanged);
sound->setManualAttenuation(0.6f);
QCOMPARE(manualSpy.size(), 1);
sound->setManualAttenuation(0.6f);
QCOMPARE(manualSpy.size(), 1);
QSignalSpy occSpy(sound.get(), &QSpatialSound::occlusionIntensityChanged);
sound->setOcclusionIntensity(0.2f);
QCOMPARE(occSpy.size(), 1);
sound->setOcclusionIntensity(0.2f);
QCOMPARE(occSpy.size(), 1);
QSignalSpy dirSpy(sound.get(), &QSpatialSound::directivityChanged);
sound->setDirectivity(0.3f);
QCOMPARE(dirSpy.size(), 1);
sound->setDirectivity(0.3f);
QCOMPARE(dirSpy.size(), 1);
QSignalSpy dirOrderSpy(sound.get(), &QSpatialSound::directivityOrderChanged);
sound->setDirectivityOrder(2.0f);
QCOMPARE(dirOrderSpy.size(), 1);
sound->setDirectivityOrder(2.0f);
QCOMPARE(dirOrderSpy.size(), 1);
QSignalSpy nfSpy(sound.get(), &QSpatialSound::nearFieldGainChanged);
sound->setNearFieldGain(0.5f);
QCOMPARE(nfSpy.size(), 1);
sound->setNearFieldGain(0.5f);
QCOMPARE(nfSpy.size(), 1);
}
void tst_QSpatialAudio::test_QSpatialSound_rotation_and_directivityOrder_semantics()
{
// rotation semantics: should emit only on change
QSignalSpy rotationSpy(sound.get(), &QSpatialSound::rotationChanged);
QQuaternion q(1, 1, 0, 0);
sound->setRotation(q);
QVERIFY(rotationSpy.size() >= 1);
const int rotBefore = rotationSpy.size();
sound->setRotation(q);
QCOMPARE(rotationSpy.size(), rotBefore);
QSignalSpy ordSpy(sound.get(), &QSpatialSound::directivityOrderChanged);
const float orderVal = 3.0f;
const int ordBefore = ordSpy.size();
sound->setDirectivityOrder(orderVal);
const int ordAfter = ordSpy.size();
QCOMPARE(ordAfter - ordBefore, 1);
sound->setDirectivityOrder(orderVal);
QCOMPARE(ordAfter, ordSpy.size());
}
void tst_QSpatialAudio::testEngineIntegration()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
// basic engine lifecycle
engine->start();
QAudioEngine eng;
eng.start();
eng.stop();
}
void tst_QSpatialAudio::testSetSource_autoPlay()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
sound->setAutoPlay(false);
QSignalSpy srcSpy(sound.get(), &QSpatialSound::sourceChanged);
sound->setSource(url);
QCOMPARE(srcSpy.size(), 1);
// autoPlay is false, so playback state shouldn't start automatically
// We only check that calling play doesn't crash and can be invoked
sound->play();
QTest::qWait(1000); // allow 1 second of playback
sound->stop();
}
void tst_QSpatialAudio::testQAmbientSound_basic()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
auto ambient = std::make_unique<QAmbientSound>(engine.get());
QSignalSpy srcSpy(ambient.get(), &QAmbientSound::sourceChanged);
QSignalSpy volSpy(ambient.get(), &QAmbientSound::volumeChanged);
ambient->setAutoPlay(false);
QCOMPARE(ambient->autoPlay(), false);
ambient->setVolume(0.4f);
QCOMPARE(ambient->volume(), 0.4f);
QCOMPARE(volSpy.size(), 1);
if (!url.isEmpty()) {
ambient->setSource(url);
QCOMPARE(srcSpy.size(), 1);
}
ambient->play();
QTest::qWait(1000);
ambient->stop();
}
void tst_QSpatialAudio::testQAudioRoom_basic()
{
// exercise QAudioRoom properties and signals
auto room = std::make_unique<QAudioRoom>(engine.get());
// Position
QSignalSpy posSpy(room.get(), &QAudioRoom::positionChanged);
QVector3D newPos(1.0f, 2.0f, 3.0f);
room->setPosition(newPos);
QCOMPARE(room->position(), newPos);
QCOMPARE(posSpy.size(), 1);
room->setPosition(newPos);
QCOMPARE(posSpy.size(), 1);
// Dimensions
QSignalSpy dimSpy(room.get(), &QAudioRoom::dimensionsChanged);
QVector3D dims(10.0f, 11.0f, 12.0f);
room->setDimensions(dims);
QCOMPARE(room->dimensions(), dims);
QCOMPARE(dimSpy.size(), 1);
room->setDimensions(dims);
QCOMPARE(dimSpy.size(), 1);
// Rotation
QSignalSpy rotSpy(room.get(), &QAudioRoom::rotationChanged);
QQuaternion rot(1.0f, 1.0f, 0.0f, 0.0f);
room->setRotation(rot);
QCOMPARE(room->rotation(), rot);
QCOMPARE(rotSpy.size(), 1);
room->setRotation(rot);
QCOMPARE(rotSpy.size(), 1);
// Wall material + wallsChanged
QSignalSpy wallsSpy(room.get(), &QAudioRoom::wallsChanged);
room->setWallMaterial(QAudioRoom::LeftWall, QAudioRoom::BrickPainted);
QCOMPARE(room->wallMaterial(QAudioRoom::LeftWall), QAudioRoom::BrickPainted);
QCOMPARE(wallsSpy.size(), 1);
room->setWallMaterial(QAudioRoom::LeftWall, QAudioRoom::BrickPainted);
QCOMPARE(wallsSpy.size(), 1);
// Reflection / Reverb properties
QSignalSpy reflSpy(room.get(), &QAudioRoom::reflectionGainChanged);
QSignalSpy revGainSpy(room.get(), &QAudioRoom::reverbGainChanged);
QSignalSpy revTimeSpy(room.get(), &QAudioRoom::reverbTimeChanged);
QSignalSpy revBrightSpy(room.get(), &QAudioRoom::reverbBrightnessChanged);
room->setReflectionGain(0.25f);
QCOMPARE(room->reflectionGain(), 0.25f);
QCOMPARE(reflSpy.size(), 1);
room->setReflectionGain(0.25f);
QCOMPARE(reflSpy.size(), 1);
room->setReverbGain(0.4f);
QCOMPARE(room->reverbGain(), 0.4f);
QCOMPARE(revGainSpy.size(), 1);
room->setReverbGain(0.4f);
QCOMPARE(revGainSpy.size(), 1);
room->setReverbTime(1.2f);
QCOMPARE(room->reverbTime(), 1.2f);
QCOMPARE(revTimeSpy.size(), 1);
room->setReverbTime(1.2f);
QCOMPARE(revTimeSpy.size(), 1);
room->setReverbBrightness(0.75f);
QCOMPARE(room->reverbBrightness(), 0.75f);
QCOMPARE(revBrightSpy.size(), 1);
room->setReverbBrightness(0.75f);
QCOMPARE(revBrightSpy.size(), 1);
}
void tst_QSpatialAudio::testQAudioListener_basic()
{
QAudioEngine eng;
QAudioListener listener(&eng);
listener.setPosition({0,0,0});
QCOMPARE(listener.position(), QVector3D(0,0,0));
}
void tst_QSpatialAudio::testSwitchAudioDevice()
{
// Skip if less than 2 audio devices are available
QList outputs = QMediaDevices::audioOutputs();
if (outputs.size() < 2)
QSKIP("Needs at least 2 audio outputs");
// Get a non-default device
QAudioDevice nonDefaultDevice = [&] {
if (!outputs[0].isDefault())
return outputs[0];
return outputs[1];
}();
QAudioDevice anotherDevice = [&] {
return outputs[(outputs.indexOf(nonDefaultDevice) + 1) % outputs.size()];
}();
// Test 1: Switch device when engine is not running (should succeed)
engine->setOutputDevice(nonDefaultDevice);
QCOMPARE(engine->outputDevice(), nonDefaultDevice);
// Test 2: Switch device when engine is running (should warn)
QAudioDevice originalDevice = engine->outputDevice();
engine->start();
QMessageSpy warnSpy;
auto expectWarn =
warnSpy.expect(QtWarningMsg, "Changing device on a running engine not implemented");
engine->setOutputDevice(anotherDevice);
QVERIFY(expectWarn.wait());
QCOMPARE(engine->outputDevice(), originalDevice);
engine->stop();
}
void tst_QSpatialAudio::test_nullEngine_behaviour()
{
// Ensure constructing objects with a nullptr engine doesn't crash
QTest::ignoreMessage(QtWarningMsg, "Cannot create QSpatialSound without a valid QAudioEngine");
QSpatialSound nullSound(nullptr);
QVector3D pos(1, 2, 3);
nullSound.setPosition(pos);
QCOMPARE(nullSound.position(), QVector3D{});
nullSound.setSize(2.5f);
QCOMPARE(nullSound.size(), 0.f);
nullSound.setDistanceCutoff(3.5f);
QCOMPARE(nullSound.distanceCutoff(), 0.f);
nullSound.setRotation(QQuaternion(1, 0, 0, 0));
QVERIFY(!nullSound.rotation().isNull());
nullSound.setManualAttenuation(0.4f);
QCOMPARE(nullSound.manualAttenuation(), 0.f);
nullSound.setOcclusionIntensity(0.2f);
QCOMPARE(nullSound.occlusionIntensity(), 0.f);
nullSound.setDirectivity(0.1f);
QCOMPARE(nullSound.directivity(), 0.f);
nullSound.setNearFieldGain(0.5f);
QCOMPARE(nullSound.nearFieldGain(), 0.f);
// play/pause/stop should not crash
nullSound.play();
nullSound.pause();
nullSound.stop();
// AmbientSound with nullptr engine
QTest::ignoreMessage(QtWarningMsg, "Cannot create QAmbientSound without a valid QAudioEngine");
QAmbientSound nullAmbient(nullptr);
nullAmbient.setVolume(0.3f);
QCOMPARE(nullAmbient.volume(), 0.3f);
nullAmbient.play();
nullAmbient.pause();
nullAmbient.stop();
// QAudioListener with nullptr engine
QTest::ignoreMessage(QtWarningMsg, "Cannot create QAudioListener without a valid QAudioEngine");
QAudioListener nullListener(nullptr);
nullListener.setPosition({0,0,0});
QCOMPARE(nullListener.position(), QVector3D(0,0,0));
nullListener.setRotation(QQuaternion(1,0,0,0));
QCOMPARE(nullListener.engine(), nullptr);
// QAudioRoom with nullptr engine - mirror expectations for null-engine behaviour
QTest::ignoreMessage(QtWarningMsg, "Cannot create QAudioRoom without a valid QAudioEngine");
QAudioRoom nullRoom(nullptr);
nullRoom.setPosition(pos);
QCOMPARE(nullRoom.position(), QVector3D{});
nullRoom.setDimensions(QVector3D(10.0f, 11.0f, 12.0f));
QCOMPARE(nullRoom.dimensions(), QVector3D{});
nullRoom.setRotation(QQuaternion(1.0f, 0.0f, 0.0f, 0.0f));
nullRoom.setWallMaterial(QAudioRoom::LeftWall, QAudioRoom::BrickPainted);
QCOMPARE(nullRoom.wallMaterial(QAudioRoom::LeftWall), QAudioRoom::BrickPainted);
}
void tst_QSpatialAudio::testPlaybackControls()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
engine->start();
sound->setAutoPlay(false);
QMessageSpy playSpy(qLcSpatialAudioEngine());
auto expectLoading = playSpy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoaded = playSpy.expect(QtDebugMsg, "Sound loaded");
sound->setSource(url);
QVERIFY(expectLoading.wait());
QVERIFY(expectLoaded.wait());
auto expectPlay = playSpy.expect(QtDebugMsg, "Playing sound");
sound->play();
QVERIFY(expectPlay.wait());
auto expectPause = playSpy.expect(QtDebugMsg, "Pausing sound");
sound->pause();
QVERIFY(expectPause.wait());
auto expectPlay2 = playSpy.expect(QtDebugMsg, "Playing sound");
sound->play();
QVERIFY(expectPlay2.wait());
auto expectStop = playSpy.expect(QtDebugMsg, "Stopping sound");
sound->stop();
QVERIFY(expectStop.wait());
}
void tst_QSpatialAudio::testFileLoading_replaceDuringPlayback()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
engine->start();
sound->setAutoPlay(false);
QMessageSpy spy(qLcSpatialAudioEngine());
auto expectLoad1 = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: QUrl\\(.*test\\.wav.*\\)"));
auto expectLoaded1 = spy.expect(QtDebugMsg, "Sound loaded");
sound->setSource(url);
QVERIFY(expectLoad1.wait());
QVERIFY(expectLoaded1.wait());
auto expectPlay = spy.expect(QtDebugMsg, "Playing sound");
sound->play();
QVERIFY(expectPlay.wait());
// Replace the sound mid-playback
auto expectLoad2 = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: QUrl\\(.*test2\\.wav.*\\)"));
auto expectLoaded2 = spy.expect(QtDebugMsg, "Sound loaded");
auto expectPlaybackStarted = spy.expect(QtDebugMsg, "Sound playback started: playing");
sound->setSource(url2);
QVERIFY(expectLoad2.wait());
QVERIFY(expectLoaded2.wait());
QVERIFY(expectPlaybackStarted.wait());
auto expectStop = spy.expect(QtDebugMsg, "Stopping sound");
sound->stop();
QVERIFY(expectStop.wait());
}
void tst_QSpatialAudio::testFileLoading_loadInvalidFile()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
engine->start();
QMessageSpy spy(qLcSpatialAudioEngine());
auto expectLoadInvalid = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectWarnInvalid = spy.expect(QtWarningMsg, "QAmbientSound: cannot load file");
QVERIFY(expectLoadInvalid.wait());
QVERIFY(expectWarnInvalid.wait());
}
void tst_QSpatialAudio::testAutoPlayTransitions()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
sound->setAutoPlay(true);
QMessageSpy spy(qLcSpatialAudioEngine());
auto expectLoadAuto = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoadedAuto = spy.expect(QtDebugMsg, "Sound loaded");
auto expectPlaybackAuto = spy.expect(QtDebugMsg, "Sound playback started: playing");
sound->setSource(url);
QVERIFY(expectLoadAuto.wait());
QVERIFY(expectLoadedAuto.wait());
QVERIFY(expectPlaybackAuto.wait());
}
void tst_QSpatialAudio::test_destroyRoomDuringPlayback()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
if (url.isEmpty())
QSKIP("No test audio file available");
auto listener = std::make_unique<QAudioListener>(engine.get());
listener->setPosition(QVector3D(0, 0, 0));
auto room = std::make_unique<QAudioRoom>(engine.get());
room->setDimensions(QVector3D(5, 5, 5));
engine->start();
QMessageSpy spy(qLcSpatialAudioEngine());
// Play a spatial sound and destroy a QAudioRoom during playback
auto spatial = std::make_unique<QSpatialSound>(engine.get());
spatial->setAutoPlay(false);
auto expectLoad =
spy.expect(QtDebugMsg, QRegularExpression("Loading sound: QUrl\\(.*test\\.wav.*\\)"));
auto expectLoaded = spy.expect(QtDebugMsg, "Sound loaded");
spatial->setSource(url);
QVERIFY(expectLoad.wait());
QVERIFY(expectLoaded.wait());
auto expectPlaying = spy.expect(QtDebugMsg, "Playing sound");
spatial->play();
QVERIFY(expectPlaying.wait());
room.reset(); // destroy while playback active
auto expectStopping = spy.expect(QtDebugMsg, "Stopping sound");
spatial->stop();
QVERIFY(expectStopping.wait());
spatial.reset();
engine->stop();
}
void tst_QSpatialAudio::test_destroyListenerDuringPlayback()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
if (url.isEmpty())
QSKIP("No test audio file available");
auto listener = std::make_unique<QAudioListener>(engine.get());
listener->setPosition(QVector3D(0, 0, 0));
auto room = std::make_unique<QAudioRoom>(engine.get());
room->setDimensions(QVector3D(5, 5, 5));
engine->start();
// Use QMessageSpy to observe engine log messages instead of ignoring them.
QMessageSpy spy(qLcSpatialAudioEngine());
auto spatial = std::make_unique<QSpatialSound>(engine.get());
spatial->setAutoPlay(false);
spatial->setLoops(-1);
auto expectLoad = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoaded = spy.expect(QtDebugMsg, "Sound loaded");
spatial->setSource(url);
QVERIFY(expectLoad.wait());
QVERIFY(expectLoaded.wait());
auto expectPlaying = spy.expect(QtDebugMsg, "Playing sound");
spatial->play();
QVERIFY(expectPlaying.wait());
listener.reset(); // destroy while playback active
auto expectStopping = spy.expect(QtDebugMsg, "Stopping sound");
spatial->stop();
QVERIFY(expectStopping.wait());
spatial.reset();
engine->stop();
}
void tst_QSpatialAudio::test_destroySpatialDuringPlayback()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
if (url.isEmpty())
QSKIP("No test audio file available");
auto listener = std::make_unique<QAudioListener>(engine.get());
listener->setPosition(QVector3D(0, 0, 0));
auto room = std::make_unique<QAudioRoom>(engine.get());
room->setDimensions(QVector3D(5, 5, 5));
engine->start();
// Use QMessageSpy to observe engine log messages instead of ignoring them.
QMessageSpy spy(qLcSpatialAudioEngine());
auto spatial = std::make_unique<QSpatialSound>(engine.get());
spatial->setAutoPlay(false);
spatial->setLoops(-1);
auto expectLoad = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoaded = spy.expect(QtDebugMsg, "Sound loaded");
spatial->setSource(url);
QVERIFY(expectLoad.wait());
QVERIFY(expectLoaded.wait());
auto expectPlaying = spy.expect(QtDebugMsg, "Playing sound");
spatial->play();
QVERIFY(expectPlaying.wait());
// Destroy the spatial sound while playback is active
auto expectStopping = spy.expect(QtDebugMsg, "Stopping sound");
spatial.reset();
QVERIFY(expectStopping.wait());
// Ensure engine still usable: create a new sound and play briefly
auto s2 = std::make_unique<QSpatialSound>(engine.get());
s2->setAutoPlay(false);
auto expectLoad2 = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoaded2 = spy.expect(QtDebugMsg, "Sound loaded");
s2->setSource(url);
QVERIFY(expectLoad2.wait());
QVERIFY(expectLoaded2.wait());
auto expectPlay2 = spy.expect(QtDebugMsg, "Playing sound");
s2->play();
QVERIFY(expectPlay2.wait());
auto expectStop2 = spy.expect(QtDebugMsg, "Stopping sound");
s2->stop();
QVERIFY(expectStop2.wait());
s2.reset();
engine->stop();
}
void tst_QSpatialAudio::test_destroyAmbientDuringPlayback()
{
QList outputs = QMediaDevices::audioOutputs();
if (outputs.isEmpty())
QSKIP("No audio outputs available");
if (url.isEmpty())
QSKIP("No test audio file available");
auto listener = std::make_unique<QAudioListener>(engine.get());
listener->setPosition(QVector3D(0, 0, 0));
auto room = std::make_unique<QAudioRoom>(engine.get());
room->setDimensions(QVector3D(5, 5, 5));
engine->start();
// Use QMessageSpy to observe engine log messages instead of ignoring them.
QMessageSpy spy(qLcSpatialAudioEngine());
auto ambient = std::make_unique<QAmbientSound>(engine.get());
ambient->setAutoPlay(false);
ambient->setLoops(-1);
auto expectLoad = spy.expect(QtDebugMsg, QRegularExpression("Loading sound: .*"));
auto expectLoaded = spy.expect(QtDebugMsg, "Sound loaded");
ambient->setSource(url);
QVERIFY(expectLoad.wait());
QVERIFY(expectLoaded.wait());
auto expectPlaying = spy.expect(QtDebugMsg, "Playing sound");
ambient->play();
QVERIFY(expectPlaying.wait());
auto expectStopping = spy.expect(QtDebugMsg, "Stopping sound");
ambient.reset(); // destroy while playback active
QVERIFY(expectStopping.wait());
engine->stop();
}
QTEST_MAIN(tst_QSpatialAudio)
#include "tst_qspatialaudio.moc"