Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtMultimediaTestLib/private/mediabackendutils_p.h>
++ #include <QtMultimediaTestLib/private/mediafileselector_p.h>
++ #include <QtMultimediaTestLib/private/qintegrationtestbase_p.h>
++ #include <QtTest/qtest.h>
++ #include <QtMultimedia/qaudiodecoder.h>
++ #include <QtCore/qdebug.h>
++
++ #include <cmath>
++ #include <optional>
++ #include <vector>
++
++ namespace {
++
++ constexpr char TEST_FILE_NAME[] = "testdata/test.wav";
++ constexpr char TEST_UNSUPPORTED_FILE_NAME[] = "testdata/test-unsupported.avi";
++ constexpr char TEST_CORRUPTED_FILE_NAME[] = "testdata/test-corrupted.wav";
++ constexpr char TEST_INVALID_SOURCE[] = "invalid";
++ constexpr char TEST_NO_AUDIO_TRACK[] = "testdata/test-no-audio-track.mp4";
++ constexpr char TEST_24BIT_FILE_NAME[] = "testdata/test-24bit.wav";
++ constexpr char TEST_FLOAT32_FILE_NAME[] = "testdata/test-float32.wav";
++
++ constexpr int testFileSampleCount = 44094;
++ constexpr int testFileSampleRate = 44100;
++
++ constexpr qint64 testFileDurationUs = [] {
++ using namespace std::chrono;
++ using namespace std::chrono_literals;
++ auto duration = nanoseconds(1s) * testFileSampleCount / testFileSampleRate;
++ return round<microseconds>(duration).count();
++ }();
++
++ using namespace std::chrono_literals;
++
++ std::vector<qint16> decodeToInt16(const QUrl &source,
++ const std::optional<QAudioFormat> &desiredFormat = std::nullopt)
++ {
++ QAudioDecoder d;
++ d.setSource(source);
++ if (desiredFormat && desiredFormat->isValid())
++ d.setAudioFormat(*desiredFormat);
++
++ std::vector<qint16> out;
++
++ QObject::connect(&d, &QAudioDecoder::bufferReady, &d, [&] {
++ for (;;) {
++ auto buffer = d.read();
++ if (!buffer.isValid())
++ return;
++
++ const QAudioFormat fmt = buffer.format();
++ const char *data = buffer.constData<const char>();
++ const qsizetype samples = buffer.sampleCount();
++ const int bps = fmt.bytesPerSample();
++
++ for (qsizetype i = 0; i < samples; ++i) {
++ const void *samplePtr = data + i * bps;
++ float normalized = fmt.normalizedSampleValue(samplePtr);
++ long v = std::lround(normalized * 32767.0f);
++ out.push_back(static_cast<qint16>(qBound(-32768l, v, 32767l)));
++ }
++ }
++ });
++
++ d.start();
++
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ finishedSpy.wait(10s);
++
++ return out;
++ }
++
++ void compareMonoReferenceToInterleaved(QSpan<const qint16> monoRef, QSpan<const qint16> other,
++ int otherChannels, float otherNormalization = 1.0f)
++ {
++ const qsizetype frames = monoRef.size();
++ QCOMPARE(qsizetype(other.size()), frames * qsizetype(otherChannels));
++
++ for (qsizetype f = 0; f < frames; ++f) {
++ qint16 ref = monoRef[f];
++ qint16 otherLeft = other[f * otherChannels];
++ int otherNorm = int(std::lround(otherLeft * otherNormalization));
++ otherNorm = int(qBound(-32768l, long(otherNorm), 32767l));
++ int diff = std::abs(int(ref) - otherNorm);
++ if (diff > 6) { // allow difference due to normalization and/or sample format conversion
++ QString msg = QString::fromUtf8("Sample mismatch at frame %1: ref=%2 otherLeft=%3 norm=%4 diff=%5")
++ .arg(qlonglong(f))
++ .arg(ref)
++ .arg(otherLeft)
++ .arg(otherNorm)
++ .arg(diff);
++ QFAIL(qPrintable(msg));
++ }
++ }
++ }
++
++ } // namespace
++
++ /*
++ This is the backend conformance test.
++
++ Since it relies on platform media framework
++ it may be less stable.
++ */
++
++ using namespace QtMultimediaTest;
++
++ class tst_QAudioDecoderBackend : public QIntegrationTestBase
++ {
++ Q_OBJECT
++ public slots:
++ void init();
++ void cleanup();
++ void initTestCase();
++
++ private slots:
++ void testMediaFilesAreSupported();
++ void directBruteForceReading();
++ void indirectReadingByBufferReadySignal();
++ void indirectReadingByBufferAvailableSignal();
++ void stopOnBufferReady();
++ void restartOnBufferReady();
++ void restartOnFinish();
++ void fileTest();
++ void qrcFileTest();
++ void unsupportedFileTest();
++ void corruptedFileTest();
++ void invalidSource();
++ void invalidQrcSourceTest();
++ void deviceTest();
++ void play_emitsFormatError_whenMediaHasNoAudioTrack();
++ void readIntoDifferentSampleFormat();
++ void readIntoDifferentChannelCount();
++ void read24bitFile();
++ void readFloat32File();
++ void readMp3File();
++ void readOggFile();
++ void readAacFile();
++ void readFlacFile();
++
++ private:
++ QUrl testFileUrl(const QString filePath);
++ void checkNoMoreChanges(QAudioDecoder &decoder);
++ #ifdef Q_OS_ANDROID
++ QTemporaryFile *temporaryFile = nullptr;
++ #endif
++
++ MediaFileSelector m_mediaSelector;
++ MaybeUrl m_wavFile = MaybeUrl{ q23::unexpect };
++ };
++
++ void tst_QAudioDecoderBackend::init()
++ {
++ }
++
++ void tst_QAudioDecoderBackend::initTestCase()
++ {
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ qputenv("QT_FORCE_DRWAV_AUDIO_DECODER", "1");
++ #endif
++
++ if (!initIntegrationTestCase())
++ return;
++
++ QAudioDecoder d;
++ if (!d.isSupported())
++ QSKIP("Audio decoder service is not available");
++
++ m_wavFile = m_mediaSelector.select(QFINDTESTDATA(TEST_FILE_NAME));
++ }
++
++ void tst_QAudioDecoderBackend::cleanup()
++ {
++ #ifdef Q_OS_ANDROID
++ if (temporaryFile) {
++ delete temporaryFile;
++ temporaryFile = nullptr;
++ }
++ #endif
++ }
++
++ QUrl tst_QAudioDecoderBackend::testFileUrl(const QString filePath)
++ {
++ QUrl url;
++ #ifndef Q_OS_ANDROID
++ QFileInfo fileInfo(QFINDTESTDATA(filePath));
++ url = QUrl::fromLocalFile(fileInfo.absoluteFilePath());
++ #else
++ QFile file(":/" + filePath);
++ if (temporaryFile) {
++ delete temporaryFile;
++ temporaryFile = nullptr;
++ }
++ if (file.open(QIODevice::ReadOnly)) {
++ temporaryFile = QTemporaryFile::createNativeFile(file);
++ url = QUrl(temporaryFile->fileName());
++ }
++ #endif
++ return url;
++ }
++
++ void tst_QAudioDecoderBackend::checkNoMoreChanges(QAudioDecoder &decoder)
++ {
++ QSignalSpy finishedSpy(&decoder, &QAudioDecoder::finished);
++ QSignalSpy bufferReadySpy(&decoder, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferAvailableSpy(&decoder, &QAudioDecoder::bufferAvailableChanged);
++
++ QTest::qWait(50); // wait a bit to check nothing happened after finish
++
++ QCOMPARE(finishedSpy.size(), 0);
++ QCOMPARE(bufferReadySpy.size(), 0);
++ QCOMPARE(bufferAvailableSpy.size(), 0);
++ }
++
++ void tst_QAudioDecoderBackend::testMediaFilesAreSupported()
++ {
++ QCOMPARE(m_mediaSelector.dumpErrors(), "");
++ }
++
++ void tst_QAudioDecoderBackend::directBruteForceReading()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ int sampleCount = 0;
++ QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged);
++
++ decoder.setSource(*m_wavFile);
++ QVERIFY(!decoder.isDecoding());
++ QVERIFY(!decoder.bufferAvailable());
++
++ decoder.start();
++ QTRY_VERIFY(decodingSpy.size() >= 1); // Wait until decoding starts
++
++ auto waitAndCheck = [](auto &&predicate) { QVERIFY(QTest::qWaitFor(predicate)); };
++
++ auto waitForBufferAvailable = [&]() {
++ waitAndCheck([&]() { return !decoder.isDecoding() || decoder.bufferAvailable(); });
++
++ return decoder.bufferAvailable();
++ };
++
++ while (waitForBufferAvailable()) {
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++
++ sampleCount += buffer.sampleCount();
++ }
++
++ checkNoMoreChanges(decoder);
++
++ QCOMPARE(sampleCount, testFileSampleCount);
++ }
++
++ void tst_QAudioDecoderBackend::indirectReadingByBufferReadySignal()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ int sampleCount = 0;
++
++ connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() {
++ QVERIFY(decoder.bufferAvailable());
++
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++ QVERIFY(!decoder.bufferAvailable());
++
++ sampleCount += buffer.sampleCount();
++ });
++
++ QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished);
++
++ decoder.setSource(*m_wavFile);
++ QVERIFY(!decoder.isDecoding());
++ QVERIFY(!decoder.bufferAvailable());
++
++ decoder.start();
++
++ QTRY_VERIFY_WITH_TIMEOUT(decodingSpy.size() >= 1,
++ 60s); // High timeout because decoding can take a long time
++
++ QTRY_VERIFY_WITH_TIMEOUT(finishSpy.size() == 1,
++ 60s); // High timeout because decoding can take a long time
++
++ QVERIFY(!decoder.isDecoding());
++
++ checkNoMoreChanges(decoder);
++
++ QCOMPARE(sampleCount, testFileSampleCount);
++ QCOMPARE(finishSpy.size(), 1);
++ }
++
++ void tst_QAudioDecoderBackend::indirectReadingByBufferAvailableSignal() {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ int sampleCount = 0;
++
++ connect(&decoder, &QAudioDecoder::bufferAvailableChanged, this, [&](bool available) {
++ QCOMPARE(decoder.bufferAvailable(), available);
++
++ if (!available)
++ return;
++
++ while (decoder.bufferAvailable()) {
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++
++ sampleCount += buffer.sampleCount();
++ }
++ });
++
++ QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished);
++
++ decoder.setSource(*m_wavFile);
++ QVERIFY(!decoder.isDecoding());
++ QVERIFY(!decoder.bufferAvailable());
++
++ decoder.start();
++ QTRY_VERIFY(decodingSpy.size() >= 1);
++
++ QTRY_VERIFY(finishSpy.size() == 1);
++ QVERIFY(!decoder.isDecoding());
++
++ checkNoMoreChanges(decoder);
++
++ QCOMPARE(sampleCount, testFileSampleCount);
++ QCOMPARE(finishSpy.size(), 1);
++ }
++
++ void tst_QAudioDecoderBackend::stopOnBufferReady()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() {
++ decoder.read(); // run next reading
++ decoder.stop();
++ });
++
++ QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished);
++ QSignalSpy bufferReadySpy(&decoder, &QAudioDecoder::bufferReady);
++
++ decoder.setSource(*m_wavFile);
++ decoder.start();
++
++ bufferReadySpy.wait();
++ QVERIFY(!decoder.isDecoding());
++
++ checkNoMoreChanges(decoder);
++
++ QCOMPARE(bufferReadySpy.size(), 1);
++ }
++
++ void tst_QAudioDecoderBackend::restartOnBufferReady()
++ {
++ QSKIP_GSTREAMER("QTBUG-124005: failures on gstreamer");
++
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ int sampleCount = 0;
++
++ std::once_flag restartOnce;
++ connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() {
++ QVERIFY(decoder.bufferAvailable());
++
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++ QVERIFY(!decoder.bufferAvailable());
++
++ sampleCount += buffer.sampleCount();
++
++ std::call_once(restartOnce, [&]() {
++ sampleCount = 0;
++ decoder.stop();
++ decoder.start();
++ });
++ });
++
++ QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished);
++
++ decoder.setSource(*m_wavFile);
++ decoder.start();
++
++ QTRY_VERIFY2(finishSpy.size() == 2, "Wait for signals after restart and after finishing");
++ QVERIFY(!decoder.isDecoding());
++
++ checkNoMoreChanges(decoder);
++
++ QCOMPARE(sampleCount, testFileSampleCount);
++ }
++
++ void tst_QAudioDecoderBackend::restartOnFinish()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ int sampleCount = 0;
++
++ connect(&decoder, &QAudioDecoder::bufferReady, this, [&]() {
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++
++ sampleCount += buffer.sampleCount();
++ });
++
++ QSignalSpy finishSpy(&decoder, &QAudioDecoder::finished);
++
++ std::once_flag restartOnce;
++ connect(&decoder, &QAudioDecoder::finished, this, [&]() {
++ QVERIFY(!decoder.bufferAvailable());
++ QVERIFY(!decoder.isDecoding());
++
++ std::call_once(restartOnce, [&]() {
++ sampleCount = 0;
++ decoder.start();
++ });
++ });
++
++ decoder.setSource(*m_wavFile);
++ decoder.start();
++
++ QTRY_VERIFY(finishSpy.size() == 2);
++
++ QVERIFY(!decoder.isDecoding());
++
++ checkNoMoreChanges(decoder);
++ QCOMPARE(sampleCount, testFileSampleCount);
++ }
++
++ void tst_QAudioDecoderBackend::fileTest()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++ QAudioBuffer buffer;
++ quint64 duration = 0;
++ int byteCount = 0;
++ int sampleCount = 0;
++
++ QVERIFY(!d.isDecoding());
++ QVERIFY(d.bufferAvailable() == false);
++ QCOMPARE(d.source(), QUrl{});
++ QVERIFY(d.audioFormat() == QAudioFormat());
++
++ // Test local file
++
++ d.setSource(*m_wavFile);
++ QVERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.source(), *m_wavFile);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged);
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged);
++
++ d.start();
++
++ QTRY_VERIFY(!isDecodingSpy.isEmpty());
++ QTRY_VERIFY(!readySpy.isEmpty());
++ QTRY_VERIFY(!bufferChangedSpy.isEmpty());
++ QVERIFY(d.bufferAvailable());
++ QTRY_VERIFY(!durationSpy.isEmpty());
++
++ QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20);
++ if (finishedSpy.empty())
++ QVERIFY(qAbs(d.duration() - 1000) < 20);
++ else
++ QCOMPARE(d.duration(), -1);
++
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++
++ // Test file is 44.1K 16bit mono, 44094 samples
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), testFileSampleRate);
++ QCOMPARE(buffer.format().sampleFormat(), QAudioFormat::Int16);
++ QCOMPARE(buffer.byteCount(), buffer.sampleCount() * 2); // 16bit mono
++
++ // The decoder should still have no format set
++ QVERIFY(d.audioFormat() == QAudioFormat());
++ QVERIFY(errorSpy.isEmpty());
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++ byteCount += buffer.byteCount();
++
++ // Now drain the decoder
++ if (sampleCount < testFileSampleCount) {
++ QTRY_COMPARE(d.bufferAvailable(), true);
++ }
++
++ auto durationToMs = [](uint64_t dur) {
++ return std::round(dur / 1000.0);
++ };
++
++ while (d.bufferAvailable()) {
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QTRY_VERIFY(!positionSpy.isEmpty());
++ QCOMPARE(positionSpy.takeLast().at(0).toLongLong(), qint64(durationToMs(duration)));
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++ byteCount += buffer.byteCount();
++
++ if (sampleCount < testFileSampleCount) {
++ QTRY_COMPARE(d.bufferAvailable(), true);
++ }
++ }
++
++ // Make sure the duration is roughly correct (+/- 20ms)
++ QCOMPARE(sampleCount, testFileSampleCount);
++ QCOMPARE(byteCount, testFileSampleCount * 2);
++ QVERIFY(qAbs(qint64(duration) - 1000000) < 20000);
++ QVERIFY(qAbs((d.position() + (buffer.duration() / 1000)) - 1000) < 20);
++ QTRY_COMPARE(finishedSpy.size(), 1);
++ QVERIFY(!d.bufferAvailable());
++ QTRY_VERIFY(!d.isDecoding());
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QTRY_COMPARE(durationSpy.size(), 2);
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++ readySpy.clear();
++ bufferChangedSpy.clear();
++ isDecodingSpy.clear();
++ durationSpy.clear();
++ finishedSpy.clear();
++ positionSpy.clear();
++
++ #ifdef Q_OS_ANDROID
++ QSKIP("Setting a desired audio format is not yet supported on Android");
++ #endif
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav decoder does not support format conversion");
++ #endif
++ // change output audio format
++ QAudioFormat format;
++ format.setChannelCount(2);
++ format.setSampleRate(11050);
++ format.setSampleFormat(QAudioFormat::UInt8);
++
++ d.setAudioFormat(format);
++
++ // We expect 1 second still, at 11050 * 2 samples == 22k samples.
++ // (at 1 byte/sample -> 22kb)
++
++ // Make sure it stuck
++ QVERIFY(d.audioFormat() == format);
++
++ duration = 0;
++ sampleCount = 0;
++ byteCount = 0;
++
++ d.start();
++ QTRY_VERIFY(!isDecodingSpy.isEmpty());
++ QTRY_VERIFY(!readySpy.isEmpty());
++ QTRY_VERIFY(!bufferChangedSpy.isEmpty());
++ QVERIFY(d.bufferAvailable());
++ QTRY_VERIFY(!durationSpy.isEmpty());
++ QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20);
++ if (finishedSpy.empty())
++ QVERIFY(qAbs(d.duration() - 1000) < 20);
++ else
++ QCOMPARE(d.duration(), -1);
++
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++ // See if we got the right format
++ QVERIFY(buffer.format() == format);
++
++ // The decoder should still have the same format
++ QVERIFY(d.audioFormat() == format);
++
++ QVERIFY(errorSpy.isEmpty());
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++ byteCount += buffer.byteCount();
++
++ while (finishedSpy.isEmpty() || d.bufferAvailable()) {
++ if (!d.bufferAvailable()) {
++ QTest::qWait(std::chrono::milliseconds(10));
++ continue;
++ }
++
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QTRY_VERIFY(!positionSpy.isEmpty());
++ QCOMPARE(positionSpy.takeLast().at(0).toLongLong(), qlonglong(durationToMs(duration)));
++ QCOMPARE_LT(d.position() - durationToMs(duration), 20u);
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++ byteCount += buffer.byteCount();
++ }
++
++ // Resampling might end up with fewer or more samples
++ // so be a bit sloppy
++ QCOMPARE_LT(qAbs(sampleCount - 22047), 100);
++ QCOMPARE_LT(qAbs(byteCount - 22047), 100);
++ QCOMPARE_LT(qAbs(qint64(duration) - testFileDurationUs), 20000);
++ QCOMPARE_LT(qAbs((d.position() + (buffer.duration() / 1000)) - 1000), 20);
++ QVERIFY(!d.bufferAvailable());
++ QVERIFY(!d.isDecoding());
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QTRY_COMPARE(durationSpy.size(), 2);
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++ }
++
++ void tst_QAudioDecoderBackend::qrcFileTest()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder decoder;
++ if (decoder.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ const QUrl qrcUrl(QStringLiteral("qrc:/testdata/test.wav"));
++
++ int sampleCount = 0;
++ QSignalSpy decodingSpy(&decoder, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy errorSpy(&decoder, SIGNAL(error(QAudioDecoder::Error)));
++
++ decoder.setSource(qrcUrl);
++ QCOMPARE(decoder.source(), qrcUrl);
++ QVERIFY(!decoder.isDecoding());
++ QVERIFY(!decoder.bufferAvailable());
++
++ decoder.start();
++ QTRY_VERIFY(decodingSpy.size() >= 1); // Wait until decoding starts
++
++ auto waitAndCheck = [](auto &&predicate) { QVERIFY(QTest::qWaitFor(predicate)); };
++
++ auto waitForBufferAvailable = [&]() {
++ waitAndCheck([&]() { return !decoder.isDecoding() || decoder.bufferAvailable(); });
++
++ return decoder.bufferAvailable();
++ };
++
++ while (waitForBufferAvailable()) {
++ auto buffer = decoder.read();
++ QVERIFY(buffer.isValid());
++
++ sampleCount += buffer.sampleCount();
++ }
++
++ checkNoMoreChanges(decoder);
++
++ QVERIFY(errorSpy.isEmpty());
++ QCOMPARE(sampleCount, testFileSampleCount);
++ }
++
++ /*
++ The avi file has an audio stream not supported by any codec.
++ */
++ void tst_QAudioDecoderBackend::unsupportedFileTest()
++ {
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++ QAudioBuffer buffer;
++
++ QVERIFY(!d.isDecoding());
++ QVERIFY(d.bufferAvailable() == false);
++ QCOMPARE(d.source(), QUrl{});
++ QVERIFY(d.audioFormat() == QAudioFormat());
++
++ // Test local file
++ QUrl url = testFileUrl(TEST_UNSUPPORTED_FILE_NAME);
++ d.setSource(url);
++ QVERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.source(), url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged);
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged);
++
++ d.start();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.audioFormat(), QAudioFormat());
++ QCOMPARE(d.duration(), qint64(-1));
++ QCOMPARE(d.position(), qint64(-1));
++
++ // Check the error code.
++ QTRY_VERIFY(!errorSpy.isEmpty());
++
++ // Have to use qvariant_cast, toInt will return 0 because unrecognized type;
++ QAudioDecoder::Error errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0));
++ QCOMPARE(errorCode, QAudioDecoder::FormatError);
++ QCOMPARE(d.error(), QAudioDecoder::FormatError);
++
++ // Check all other spies.
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(!d.isDecoding());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ // Either reject the file directly, or set the duration to 5secs on setUrl() and back to -1 on start()
++ QVERIFY(durationSpy.isEmpty() || durationSpy.size() == 2);
++
++ errorSpy.clear();
++
++ // Try read even if the file is not supported to test robustness.
++ buffer = d.read();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!buffer.isValid());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.position(), qint64(-1));
++
++ QVERIFY(errorSpy.isEmpty());
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(!d.isDecoding());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ QVERIFY(durationSpy.isEmpty() || durationSpy.size() == 2);
++
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++ }
++
++ /*
++ The corrupted file is generated by copying a few random numbers
++ from /dev/random on a linux machine.
++ */
++ void tst_QAudioDecoderBackend::corruptedFileTest()
++ {
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++ QAudioBuffer buffer;
++
++ QVERIFY(!d.isDecoding());
++ QVERIFY(d.bufferAvailable() == false);
++ QCOMPARE(d.source(), QUrl());
++ QVERIFY(d.audioFormat() == QAudioFormat());
++
++ // Test local file
++ QUrl url = testFileUrl(TEST_CORRUPTED_FILE_NAME);
++ d.setSource(url);
++ QVERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.source(), url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged);
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged);
++
++ d.start();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.audioFormat(), QAudioFormat());
++ QCOMPARE(d.duration(), qint64(-1));
++ QCOMPARE(d.position(), qint64(-1));
++
++ // Check the error code.
++ QTRY_VERIFY(!errorSpy.isEmpty());
++
++ // Have to use qvariant_cast, toInt will return 0 because unrecognized type;
++ QAudioDecoder::Error errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0));
++ QCOMPARE(errorCode, QAudioDecoder::FormatError);
++ QCOMPARE(d.error(), QAudioDecoder::FormatError);
++
++ // Check all other spies.
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(!d.isDecoding());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ QVERIFY(durationSpy.isEmpty());
++
++ errorSpy.clear();
++
++ // Try read even if the file is corrupted to test the robustness.
++ buffer = d.read();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!buffer.isValid());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.position(), qint64(-1));
++
++ QVERIFY(errorSpy.isEmpty());
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(!d.isDecoding());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ QVERIFY(durationSpy.isEmpty());
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++ }
++
++ void tst_QAudioDecoderBackend::invalidSource()
++ {
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++ QAudioBuffer buffer;
++
++ QVERIFY(!d.isDecoding());
++ QVERIFY(d.bufferAvailable() == false);
++ QCOMPARE(d.source(), QUrl());
++ QVERIFY(d.audioFormat() == QAudioFormat());
++
++ // Test invalid file source
++ QFileInfo fileInfo(TEST_INVALID_SOURCE);
++ QUrl url = QUrl::fromLocalFile(fileInfo.absoluteFilePath());
++ d.setSource(url);
++ QVERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.source(), url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged);
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged);
++
++ d.start();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.audioFormat(), QAudioFormat());
++ QCOMPARE(d.duration(), qint64(-1));
++ QCOMPARE(d.position(), qint64(-1));
++
++ // Check the error code.
++ QTRY_VERIFY(!errorSpy.isEmpty());
++
++ // Have to use qvariant_cast, toInt will return 0 because unrecognized type;
++ QAudioDecoder::Error errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0));
++
++ if (isDarwinPlatform()) {
++ QCOMPARE(errorCode, QAudioDecoder::FormatError);
++ QCOMPARE(d.error(), QAudioDecoder::FormatError);
++ } else {
++ QCOMPARE(errorCode, QAudioDecoder::ResourceError);
++ QCOMPARE(d.error(), QAudioDecoder::ResourceError);
++ }
++
++ // Check all other spies.
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(!d.isDecoding());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ QVERIFY(durationSpy.isEmpty());
++
++ errorSpy.clear();
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++
++ QFile file;
++ file.setFileName(TEST_INVALID_SOURCE);
++ QTEST_ASSERT(!file.open(QIODevice::ReadOnly));
++ d.setSourceDevice(&file);
++
++ d.start();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QCOMPARE(d.audioFormat(), QAudioFormat());
++ QCOMPARE(d.duration(), qint64(-1));
++ QCOMPARE(d.position(), qint64(-1));
++
++ // Check the error code.
++ QTRY_VERIFY(!errorSpy.isEmpty());
++ errorCode = qvariant_cast<QAudioDecoder::Error>(errorSpy.takeLast().at(0));
++ QCOMPARE(errorCode, QAudioDecoder::ResourceError);
++ QCOMPARE(d.error(), QAudioDecoder::ResourceError);
++ QVERIFY(!d.isDecoding());
++ // Check all other spies.
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(bufferChangedSpy.isEmpty());
++ QVERIFY(finishedSpy.isEmpty());
++ QVERIFY(positionSpy.isEmpty());
++ QVERIFY(durationSpy.isEmpty());
++
++ errorSpy.clear();
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QCOMPARE(d.duration(), qint64(-1));
++ QVERIFY(!d.bufferAvailable());
++ }
++
++ void tst_QAudioDecoderBackend::invalidQrcSourceTest()
++ {
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ const QUrl url(QStringLiteral("qrc:/testdata/does-not-exist.wav"));
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++
++ d.setSource(url);
++ QCOMPARE(d.source(), url);
++ QVERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++
++ d.start();
++ QTRY_VERIFY(!errorSpy.isEmpty());
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++
++ QVERIFY(readySpy.isEmpty());
++ QVERIFY(finishedSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::deviceTest()
++ {
++ using namespace std::chrono;
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++ QAudioBuffer buffer;
++ quint64 duration = 0;
++ int sampleCount = 0;
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy bufferChangedSpy(&d, &QAudioDecoder::bufferAvailableChanged);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ QSignalSpy isDecodingSpy(&d, &QAudioDecoder::isDecodingChanged);
++ QSignalSpy durationSpy(&d, &QAudioDecoder::durationChanged);
++ QSignalSpy finishedSpy(&d, &QAudioDecoder::finished);
++ QSignalSpy positionSpy(&d, &QAudioDecoder::positionChanged);
++
++ QVERIFY(!d.isDecoding());
++ QVERIFY(d.bufferAvailable() == false);
++ QCOMPARE(d.source(), QUrl{});
++ QVERIFY(d.audioFormat() == QAudioFormat());
++ QFile file(m_wavFile->toString());
++ QVERIFY(file.open(QIODevice::ReadOnly));
++ d.setSourceDevice(&file);
++
++ QVERIFY(d.sourceDevice() == &file);
++ QVERIFY(d.source().isEmpty());
++
++ // We haven't set the format yet
++ QVERIFY(d.audioFormat() == QAudioFormat());
++
++ d.start();
++
++ QTRY_VERIFY(!isDecodingSpy.isEmpty());
++ QTRY_VERIFY(!readySpy.isEmpty());
++ QTRY_VERIFY(!bufferChangedSpy.isEmpty());
++ QVERIFY(d.bufferAvailable());
++ QTRY_VERIFY(!durationSpy.isEmpty());
++ if (finishedSpy.empty())
++ QVERIFY(qAbs(d.duration() - 1000) < 20);
++ else
++ QCOMPARE(d.duration(), -1);
++
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++
++ // Test file is 44.1K 16bit mono
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), testFileSampleRate);
++ QCOMPARE(buffer.format().sampleFormat(), QAudioFormat::Int16);
++
++ QVERIFY(errorSpy.isEmpty());
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++
++ // Now drain the decoder
++ if (sampleCount < testFileSampleCount) {
++ QTRY_COMPARE(d.bufferAvailable(), true);
++ }
++
++ while (d.bufferAvailable()) {
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QTRY_VERIFY(!positionSpy.isEmpty());
++ QCOMPARE_EQ(positionSpy.takeLast().at(0).toLongLong(),
++ round<milliseconds>(microseconds{ duration }).count());
++
++ QVERIFY(d.position() - (duration / 1000) < 20);
++
++ duration += buffer.duration();
++ sampleCount += buffer.sampleCount();
++ if (sampleCount < testFileSampleCount) {
++ QTRY_COMPARE(d.bufferAvailable(), true);
++ }
++ }
++
++ // Make sure the duration is roughly correct (+/- 20ms)
++ QCOMPARE(sampleCount, testFileSampleCount);
++ QVERIFY(qAbs(qint64(duration) - 1000000) < 20000);
++ QVERIFY(qAbs((d.position() + (buffer.duration() / 1000)) - 1000) < 20);
++ QTRY_COMPARE(finishedSpy.size(), 1);
++ QVERIFY(!d.bufferAvailable());
++ QTRY_VERIFY(!d.isDecoding());
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QTRY_COMPARE(durationSpy.size(), 2);
++ QCOMPARE(d.duration(), qint64(-1));
++ readySpy.clear();
++ bufferChangedSpy.clear();
++ isDecodingSpy.clear();
++ durationSpy.clear();
++ finishedSpy.clear();
++ positionSpy.clear();
++
++ #ifdef Q_OS_ANDROID
++ QSKIP("Setting a desired audio format is not yet supported on Android");
++ #endif
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav decoder does not support format conversion");
++ #endif
++ // Now try changing formats
++ QAudioFormat format;
++ format.setChannelCount(2);
++ format.setSampleRate(8000);
++ format.setSampleFormat(QAudioFormat::UInt8);
++
++ d.setAudioFormat(format);
++
++ // Make sure it stuck
++ QVERIFY(d.audioFormat() == format);
++
++ d.start();
++ QVERIFY(d.error() == QAudioDecoder::NoError);
++ QTRY_VERIFY(!isDecodingSpy.isEmpty());
++ QTRY_VERIFY(!readySpy.isEmpty());
++ QTRY_VERIFY(!bufferChangedSpy.isEmpty());
++ QVERIFY(d.bufferAvailable());
++ QTRY_VERIFY(!durationSpy.isEmpty());
++
++ QVERIFY(qAbs(durationSpy.front().front().value<qint64>() - 1000) < 20);
++ if (finishedSpy.empty())
++ QVERIFY(qAbs(d.duration() - 1000) < 20);
++ else
++ QCOMPARE(d.duration(), -1);
++
++ buffer = d.read();
++ QVERIFY(buffer.isValid());
++ // See if we got the right format
++ QVERIFY(buffer.format() == format);
++
++ // The decoder should still have the same format
++ QVERIFY(d.audioFormat() == format);
++
++ QVERIFY(errorSpy.isEmpty());
++
++ d.stop();
++ QTRY_VERIFY(!d.isDecoding());
++ QVERIFY(!d.bufferAvailable());
++ QTRY_COMPARE(durationSpy.size(), 2);
++ QCOMPARE(d.duration(), qint64(-1));
++ }
++
++ void tst_QAudioDecoderBackend::readIntoDifferentSampleFormat()
++ {
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ // Request float samples at same rate and channels
++ QAudioFormat format;
++ format.setChannelCount(1);
++ format.setSampleRate(testFileSampleRate);
++ format.setSampleFormat(QAudioFormat::Float);
++
++ // Decode native int16
++ std::vector<qint16> native = decodeToInt16(*m_wavFile);
++
++ // Decode requested Float format then convert to int16 for sample-wise comparison
++ std::optional<QAudioFormat> desired = format;
++ std::vector<qint16> converted = decodeToInt16(*m_wavFile, desired);
++
++ // Compare sample-wise
++ QCOMPARE(qsizetype(native.size()), qsizetype(converted.size()));
++ for (qsizetype i = 0; i < qsizetype(native.size()); ++i) {
++ int diff = std::abs(native[i] - converted[i]);
++ QVERIFY2(diff <= 1,
++ qPrintable(QString::fromUtf8("Sample mismatch at index %1: ref=%2 got=%3 diff=%4")
++ .arg(i)
++ .arg(native[i])
++ .arg(converted[i])
++ .arg(diff)));
++ }
++ }
++
++ void tst_QAudioDecoderBackend::readIntoDifferentChannelCount()
++ {
++ QSKIP_WMF("Failures with Windows media foundation backend");
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav decoder does not support channel count conversion");
++ #endif
++ CHECK_SELECTED_URL(m_wavFile);
++
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ // Request stereo output instead of mono
++ QAudioFormat format;
++ format.setChannelCount(2);
++ format.setSampleRate(testFileSampleRate);
++ format.setSampleFormat(QAudioFormat::Int16);
++
++ // Decode native int16
++ std::vector<qint16> native = decodeToInt16(*m_wavFile);
++
++ // Decode requested stereo format then convert to int16 for comparison
++ std::optional<QAudioFormat> desired = format;
++ std::vector<qint16> converted = decodeToInt16(*m_wavFile, desired);
++
++ // other should be interleaved stereo, compare left channel to native mono
++ compareMonoReferenceToInterleaved(
++ native, converted, format.channelCount(),
++ 1.41401273885f); // sqrt(2) is the expected amplification when upmixing mono to stereo
++ }
++
++ void tst_QAudioDecoderBackend::read24bitFile()
++ {
++ QSKIP_WMF("Failures with Windows media foundation backend");
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl(TEST_24BIT_FILE_NAME);
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ // decode 24bit files into 32bit integers
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ auto sf = buffer.format().sampleFormat();
++ QVERIFY(sf == QAudioFormat::Int32);
++ // ensure no error signal emitted
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::readFloat32File()
++ {
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl(TEST_FLOAT32_FILE_NAME);
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QCOMPARE(buffer.format().sampleFormat(), QAudioFormat::Float);
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), 44100);
++ // ensure no error signal emitted
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::readMp3File()
++ {
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav only supports WAV files");
++ #endif
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl("testdata/nokia-tune.mp3");
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QCOMPARE(buffer.format().channelCount(), 2);
++ QCOMPARE(buffer.format().sampleRate(), 48000);
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::readOggFile()
++ {
++ QSKIP_WMF("Failures with Windows media foundation backend");
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav only supports WAV files");
++ #endif
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl("testdata/test.ogg");
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), 44100);
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::readAacFile()
++ {
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav only supports WAV files");
++ #endif
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl("testdata/test.aac");
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), 44100);
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::readFlacFile()
++ {
++ QSKIP_WMF("Failures with Windows media foundation backend");
++ #ifdef QT_TEST_DRWAV_AUDIO_DECODER
++ QSKIP("dr_wav only supports WAV files");
++ #endif
++ QAudioDecoder d;
++ if (d.error() == QAudioDecoder::NotSupportedError)
++ QSKIP("There is no audio decoding support on this platform.");
++
++ QUrl url = testFileUrl("testdata/test.flac");
++ d.setSource(url);
++
++ QSignalSpy readySpy(&d, &QAudioDecoder::bufferReady);
++ QSignalSpy errorSpy(&d, SIGNAL(error(QAudioDecoder::Error)));
++
++ d.start();
++
++ QTRY_VERIFY(!readySpy.isEmpty());
++
++ auto buffer = d.read();
++ QVERIFY(buffer.isValid());
++ QCOMPARE(buffer.format().channelCount(), 1);
++ QCOMPARE(buffer.format().sampleRate(), 44100);
++ QVERIFY(errorSpy.isEmpty());
++ }
++
++ void tst_QAudioDecoderBackend::play_emitsFormatError_whenMediaHasNoAudioTrack()
++ {
++ QAudioDecoder decoder;
++
++ QSignalSpy errors{ &decoder, qOverload<QAudioDecoder::Error>(&QAudioDecoder::error) };
++
++ decoder.setSource(testFileUrl(TEST_NO_AUDIO_TRACK));
++ decoder.start();
++
++ QTRY_VERIFY(!errors.empty());
++
++ QCOMPARE_EQ(decoder.error(), QAudioDecoder::Error::FormatError);
++ }
++
++ QTEST_MAIN(tst_QAudioDecoderBackend)
++
++ #include "tst_qaudiodecoderbackend.moc"