diff options
Diffstat (limited to 'tests/auto/integration/qaudiosink/tst_qaudiosink.cpp')
-rw-r--r-- | tests/auto/integration/qaudiosink/tst_qaudiosink.cpp | 1053 |
1 files changed, 1053 insertions, 0 deletions
diff --git a/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp new file mode 100644 index 000000000..6fdfe8221 --- /dev/null +++ b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp @@ -0,0 +1,1053 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtCore/qlocale.h> +#include <QtCore/QTemporaryDir> +#include <QtCore/QSharedPointer> +#include <QtCore/QScopedPointer> + +#include <qaudiosink.h> +#include <qaudiodevice.h> +#include <qaudioformat.h> +#include <qaudio.h> +#include <qmediadevices.h> +#include <qwavedecoder.h> + +#define AUDIO_BUFFER 192000 + +class tst_QAudioSink : public QObject +{ + Q_OBJECT +public: + tst_QAudioSink(QObject* parent=nullptr) : QObject(parent) {} + +private slots: + void initTestCase(); + void format(); + void invalidFormat_data(); + void invalidFormat(); + + void bufferSize_data(); + void bufferSize(); + + void stopWhileStopped(); + void suspendWhileStopped(); + void resumeWhileStopped(); + + void pull_data(){generate_audiofile_testrows();} + void pull(); + + void pullSuspendResume_data(){generate_audiofile_testrows();} + void pullSuspendResume(); + void pullResumeFromUnderrun(); + + void push_data(){generate_audiofile_testrows();} + void push(); + + void pushSuspendResume_data(){generate_audiofile_testrows();} + void pushSuspendResume(); + + void pushResetResume(); + + void pushUnderrun_data(){generate_audiofile_testrows();} + void pushUnderrun(); + + void volume_data(); + void volume(); + +private: + using FilePtr = QSharedPointer<QFile>; + + static QString formatToFileName(const QAudioFormat &format); + void createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate = 440); + static QString dumpStateSignalSpy(const QSignalSpy &stateSignalSpy); + + static qint64 wavDataSize(QIODevice &input); + + template<typename Checker> + static void pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst = false); + + void generate_audiofile_testrows(); + + QAudioDevice audioDevice; + QList<QAudioFormat> testFormats; + QList<FilePtr> audioFiles; + QScopedPointer<QTemporaryDir> m_temporaryDir; + + QScopedPointer<QByteArray> m_byteArray; + QScopedPointer<QBuffer> m_buffer; +}; + +QString tst_QAudioSink::formatToFileName(const QAudioFormat &format) +{ + return QStringLiteral("%1_%2_%3") + .arg(format.sampleRate()) + .arg(format.bytesPerSample()) + .arg(format.channelCount()); +} + +void tst_QAudioSink::createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate) +{ + const int channelBytes = format.bytesPerSample(); + const int sampleBytes = format.bytesPerFrame(); + + Q_ASSERT(length % sampleBytes == 0); + Q_UNUSED(sampleBytes); // suppress warning in release builds + + m_byteArray.reset(new QByteArray(length, 0)); + unsigned char *ptr = reinterpret_cast<unsigned char *>(m_byteArray->data()); + int sampleIndex = 0; + + while (length) { + const qreal x = qSin(2 * M_PI * sampleRate * qreal(sampleIndex % format.sampleRate()) / format.sampleRate()); + for (int i = 0; i < format.channelCount(); ++i) { + switch (format.sampleFormat()) { + case QAudioFormat::UInt8: { + const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255); + *reinterpret_cast<quint8 *>(ptr) = value; + break; + } + case QAudioFormat::Int16: { + qint16 value = static_cast<qint16>(x * 32767); + *reinterpret_cast<qint16 *>(ptr) = value; + break; + } + case QAudioFormat::Int32: { + quint32 value = static_cast<quint32>(x) * std::numeric_limits<qint32>::max(); + *reinterpret_cast<qint32 *>(ptr) = value; + break; + } + case QAudioFormat::Float: + *reinterpret_cast<float *>(ptr) = x; + break; + case QAudioFormat::Unknown: + case QAudioFormat::NSampleFormats: + break; + } + + ptr += channelBytes; + length -= channelBytes; + } + ++sampleIndex; + } + + m_buffer.reset(new QBuffer(m_byteArray.data(), this)); + Q_ASSERT(m_buffer->open(QIODevice::ReadOnly)); +} + +QString tst_QAudioSink::dumpStateSignalSpy(const QSignalSpy& stateSignalSpy) { + QString result = "["; + bool first = true; + for (auto& params : stateSignalSpy) + { + if (!std::exchange(first, false)) + result += ','; + result += QString::number(params.front().value<QAudio::State>()); + } + result.append(']'); + return result; +} + +qint64 tst_QAudioSink::wavDataSize(QIODevice &input) +{ + return input.size() - QWaveDecoder::headerLength(); +} + +template<typename Checker> +void tst_QAudioSink::pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst) +{ + bool firstBuffer = true; + qint64 offset = 0; + QByteArray buffer; + + while ((allWritten < writtenLimit || writtenLimit < 0) && !input.atEnd() + && !QTest::currentTestFailed()) { + if (sink.bytesFree() > 0) { + if (buffer.isNull()) + buffer = input.read(sink.bytesFree()); + + const auto written = feed.write(buffer); + allWritten += written; + offset += written; + + if (offset >= buffer.size()) { + offset = 0; + buffer.clear(); + } + + if (!checkOnlyFirst || firstBuffer) + checker(); + + firstBuffer = false; + } else { + // wait a bit to ensure some the sink has consumed some data + // The delay getting might need some improvements + const auto delay = qMin(10, sink.format().durationForBytes(sink.bufferSize()) / 1000 / 2); + QTest::qWait(delay); + } + } +} + +void tst_QAudioSink::generate_audiofile_testrows() +{ + QTest::addColumn<FilePtr>("audioFile"); + QTest::addColumn<QAudioFormat>("audioFormat"); + + for (int i=0; i<audioFiles.size(); i++) { + QTest::newRow(QStringLiteral("Audio File %1").arg(i).toUtf8().constData()) + << audioFiles.at(i) << testFormats.at(i); + } +} + +void tst_QAudioSink::initTestCase() +{ + // Only perform tests if audio output device exists + const QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); + + if (devices.size() <= 0) + QSKIP("No audio backend"); + + audioDevice = QMediaDevices::defaultAudioOutput(); + + + QAudioFormat format; + + if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) { + if (format.sampleFormat() == QAudioFormat::Int16) + testFormats.append(audioDevice.preferredFormat()); + } + + // PCM 11025 mono S16LE + format.setChannelCount(1); + format.setSampleRate(11025); + format.setSampleFormat(QAudioFormat::Int16); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 mono S16LE + format.setSampleRate(22050); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 22050 stereo S16LE + format.setChannelCount(2); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + // PCM 44100 stereo S16LE + format.setSampleRate(44100); + if (audioDevice.isFormatSupported(format)) +#ifdef Q_OS_ANDROID + // Testset crash on emulator x86 with API 23 (Android 6) for 44,1 MHz. + // It is not happen on x86 with API 24. What is more, there is no crash when + // tested sample rate is 44,999 or any other value. Seems like problem on + // emulator side. Let's turn off this frequency for API 23 + if (QNativeInterface::QAndroidApplication::sdkVersion() > __ANDROID_API_M__) +#endif + testFormats.append(format); + + // PCM 48000 stereo S16LE + format.setSampleRate(48000); + if (audioDevice.isFormatSupported(format)) + testFormats.append(format); + + QVERIFY(testFormats.size()); + + const QChar slash = QLatin1Char('/'); + QString temporaryPattern = QDir::tempPath(); + if (!temporaryPattern.endsWith(slash)) + temporaryPattern += slash; + temporaryPattern += "tst_qaudiooutputXXXXXX"; + m_temporaryDir.reset(new QTemporaryDir(temporaryPattern)); + m_temporaryDir->setAutoRemove(true); + QVERIFY(m_temporaryDir->isValid()); + + const QString temporaryAudioPath = m_temporaryDir->path() + slash; + for (const QAudioFormat &format : std::as_const(testFormats)) { + qint64 len = format.sampleRate()*format.bytesPerFrame(); // 1 second + createSineWaveData(format, len); + // Write generate sine wave data to file + const QString fileName = temporaryAudioPath + QStringLiteral("generated") + + formatToFileName(format) + QStringLiteral(".wav"); + FilePtr file(new QFile(fileName)); + QVERIFY2(file->open(QIODevice::WriteOnly), qPrintable(file->errorString())); + QWaveDecoder waveDecoder(file.data(), format); + if (waveDecoder.open(QIODevice::WriteOnly)) { + waveDecoder.write(m_byteArray->data(), len); + waveDecoder.close(); + } + file->close(); + audioFiles.append(file); + } +} + +void tst_QAudioSink::format() +{ + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QAudioFormat requested = audioDevice.preferredFormat(); + QAudioFormat actual = audioOutput.format(); + + QVERIFY2((requested.channelCount() == actual.channelCount()), + QStringLiteral("channels: requested=%1, actual=%2") + .arg(requested.channelCount()) + .arg(actual.channelCount()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleRate() == actual.sampleRate()), + QStringLiteral("sampleRate: requested=%1, actual=%2") + .arg(requested.sampleRate()) + .arg(actual.sampleRate()) + .toUtf8() + .constData()); + QVERIFY2((requested.sampleFormat() == actual.sampleFormat()), + QStringLiteral("sampleFormat: requested=%1, actual=%2") + .arg((ushort)requested.sampleFormat()) + .arg((ushort)actual.sampleFormat()) + .toUtf8() + .constData()); + QVERIFY(requested == actual); +} + +void tst_QAudioSink::invalidFormat_data() +{ + QTest::addColumn<QAudioFormat>("invalidFormat"); + + QAudioFormat format; + + QTest::newRow("Null Format") + << format; + + format = audioDevice.preferredFormat(); + format.setChannelCount(0); + QTest::newRow("Channel count 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleRate(0); + QTest::newRow("Sample rate 0") + << format; + + format = audioDevice.preferredFormat(); + format.setSampleFormat(QAudioFormat::Unknown); + QTest::newRow("Sample size 0") + << format; +} + +void tst_QAudioSink::invalidFormat() +{ + QFETCH(QAudioFormat, invalidFormat); + + QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), + "isFormatSupported() is returning true on an invalid format"); + + QAudioSink audioOutput(invalidFormat, this); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + audioOutput.start(); + // Check that error is raised + QTRY_VERIFY2((audioOutput.error() == QAudio::OpenError),"error() was not set to QAudio::OpenError after start()"); +} + +void tst_QAudioSink::bufferSize_data() +{ + QTest::addColumn<int>("bufferSize"); + QTest::newRow("Buffer size 512") << 512; + QTest::newRow("Buffer size 4096") << 4096; + QTest::newRow("Buffer size 8192") << 8192; +} + +void tst_QAudioSink::bufferSize() +{ + QFETCH(int, bufferSize); + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.error() == QAudio::NoError), + QStringLiteral("error() was not set to QAudio::NoError on creation(%1)") + .arg(bufferSize) + .toUtf8() + .constData()); + + audioOutput.setBufferSize(bufferSize); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize"); + QVERIFY2((audioOutput.bufferSize() == bufferSize), + QStringLiteral("bufferSize: requested=%1, actual=%2") + .arg(bufferSize) + .arg(audioOutput.bufferSize()) + .toUtf8() + .constData()); +} + +void tst_QAudioSink::stopWhileStopped() +{ + // Calls QAudioSink::stop() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.stop(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSink::suspendWhileStopped() +{ + // Calls QAudioSink::suspend() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.suspend(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "stop() while suspended is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after stop()"); +} + +void tst_QAudioSink::resumeWhileStopped() +{ + // Calls QAudioSink::resume() when object is already in StoppedState + // Checks that + // - No state change occurs + // - No error is raised (QAudioSink::error() returns QAudio::NoError) + + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + audioOutput.resume(); + + // Check that no state transition occurred + QVERIFY2((stateSignal.size() == 0), "resume() while stopped is emitting a signal and it shouldn't"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError after resume()"); +} + +void tst_QAudioSink::pull() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + audioOutput.start(audioFile.data()); + + // Check that QAudioSink immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + + // Wait until playback finishes + QTRY_VERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pullSuspendResume() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + audioOutput.start(audioFile.data()); + // Check that QAudioSink immediately transitions to ActiveState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after start()"); + + stateSignal.clear(); + // Wait for half of clip to play + QTest::qWait(500); + + audioOutput.suspend(); + QTest::qWait(100); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioOutput.elapsedUSecs(); + qint64 processedUs = audioOutput.processedUSecs(); + QTest::qWait(100); + QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); + QVERIFY(audioOutput.processedUSecs() == processedUs); + + audioOutput.resume(); + + // Check that QAudioSink immediately transitions to ActiveState + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after resume(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after resume()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + stateSignal.clear(); + + // Wait until playback finishes + QTRY_VERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pullResumeFromUnderrun() +{ + class AudioPullSource : public QIODevice + { + public: + qint64 readData(char *data, qint64 len) override { + qint64 read = qMin(len, available); + available -= read; + memset(data, 0, read); + return read; + } + qint64 writeData(const char *, qint64) override { return 0; } + bool isSequential() const override { return true; } + + qint64 bytesAvailable() const override { return available; } + bool atEnd() const override { return signalEnd && available == 0; } + + qint64 available = 0; + bool signalEnd = false; + }; + + constexpr int chunkSize = 128; + + QAudioFormat format; + format.setChannelCount(1); + format.setSampleFormat(QAudioFormat::UInt8); + format.setSampleRate(8000); + + AudioPullSource audioSource; + QAudioSink audioOutput(format, this); + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + audioSource.open(QIODeviceBase::ReadOnly); + audioSource.available = chunkSize; + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + audioOutput.start(&audioSource); + + QTRY_COMPARE(stateSignal.size(), 1); + QCOMPARE(audioOutput.state(), QAudio::ActiveState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + stateSignal.clear(); + + QTRY_COMPARE(stateSignal.size(), 1); + QCOMPARE(audioOutput.state(), QAudio::IdleState); + QCOMPARE(audioOutput.error(), QAudio::UnderrunError); + stateSignal.clear(); + + QTest::qWait(300); + audioSource.available = chunkSize; + audioSource.signalEnd = true; + + // Resume pull + emit audioSource.readyRead(); + + QTRY_COMPARE(stateSignal.size(), 2); + QCOMPARE(stateSignal.at(0).front().value<QAudio::State>(), QAudio::ActiveState); + QCOMPARE(stateSignal.at(1).front().value<QAudio::State>(), QAudio::IdleState); + + QCOMPARE(audioOutput.error(), QAudio::NoError); + QCOMPARE(audioOutput.state(), QAudio::IdleState); + + // we played two chunks, sample rate is per second + const int expectedUSecs = (double(chunkSize) / double(format.sampleRate())) + * 2 * 1000 * 1000; + QTRY_COMPARE(audioOutput.processedUSecs(), expectedUSecs); +} + +void tst_QAudioSink::push() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + qint64 written = 0; + + auto checker = [&]() { + // Check for transition to ActiveState when data is provided + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + stateSignal.clear(); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile), checker, + true); + + // Wait until playback finishes + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(audioOutput.state() == QAudio::IdleState); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pushSuspendResume() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + + qint64 written = 0; + // Play half of the clip + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + + stateSignal.clear(); + + const auto suspendedInState = audioOutput.state(); + audioOutput.suspend(); + + QTRY_VERIFY2( + (stateSignal.size() == 1), + QStringLiteral( + "didn't emit SuspendedState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::SuspendedState), "didn't transition to SuspendedState after suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after suspend()"); + stateSignal.clear(); + + // Check that only 'elapsed', and not 'processed' increases while suspended + qint64 elapsedUs = audioOutput.elapsedUSecs(); + qint64 processedUs = audioOutput.processedUSecs(); + QTest::qWait(100); + QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); + QVERIFY(audioOutput.processedUSecs() == processedUs); + + audioOutput.resume(); + + // Give backends running in separate threads a chance to resume + // but not too much or the rest of the file may be processed + QTest::qWait(20); + + // Check that QAudioSink immediately transitions to IdleState + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after resume(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == suspendedInState), "resume() didn't transition to state before suspend()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); + stateSignal.clear(); + + // Play rest of the clip + + auto restChecker = [&]() { + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after writing audio data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker); + + QVERIFY(audioOutput.state() != QAudio::IdleState); + stateSignal.clear(); + + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY(stateSignal.size() > 0); + QTRY_COMPARE(qvariant_cast<QAudio::State>(stateSignal.last().at(0)), QAudio::IdleState); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::pushResetResume() +{ + auto audioFile = audioFiles.at(0); + auto audioFormat = testFormats.at(0); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setBufferSize(8192); + audioOutput.setVolume(0.1f); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QPointer<QIODevice> feed = audioOutput.start(); + + QTest::qWait(20); + + auto buffer = audioFile->read(audioOutput.bytesFree()); + feed->write(buffer); + + QTest::qWait(20); + QTRY_COMPARE(audioOutput.state(), QAudio::ActiveState); + + audioOutput.reset(); + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + + const auto processedUSecs = audioOutput.processedUSecs(); + + audioOutput.resume(); + QTest::qWait(40); + + // Nothing changed if resume after reset + QCOMPARE(audioOutput.state(), QAudio::StoppedState); + QCOMPARE(audioOutput.error(), QAudio::NoError); + + QCOMPARE(audioOutput.processedUSecs(), processedUSecs); +} + +void tst_QAudioSink::pushUnderrun() +{ + QFETCH(FilePtr, audioFile); + QFETCH(QAudioFormat, audioFormat); + + QAudioSink audioOutput(audioFormat, this); + + audioOutput.setVolume(0.1f); + + QSignalSpy stateSignal(&audioOutput, &QAudioSink::stateChanged); + + // Check that we are in the default state before calling start + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "state() was not set to StoppedState before start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError before start()"); + QVERIFY2((audioOutput.elapsedUSecs() == qint64(0)),"elapsedUSecs() not zero on creation"); + + audioFile->close(); + audioFile->open(QIODevice::ReadOnly); + audioFile->seek(QWaveDecoder::headerLength()); + + QIODevice* feed = audioOutput.start(); + + // Check that QAudioSink immediately transitions to IdleState + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal on start(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after start()"); + QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after start()"); + stateSignal.clear(); + + // Check that 'elapsed' increases + QTest::qWait(40); + QVERIFY2((audioOutput.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()"); + QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); + + qint64 written = 0; + + // Play half of the clip + + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + + stateSignal.clear(); + + // Wait for data to be played + QTest::qWait(700); + + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit IdleState signal after suspend(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState, no data"); + QVERIFY2((audioOutput.error() == QAudio::UnderrunError), "error state is not equal to QAudio::UnderrunError, no data"); + stateSignal.clear(); + + // Play rest of the clip + auto restChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker, true); + + stateSignal.clear(); + + // Wait until playback finishes + QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); + QTRY_VERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit IdleState signal when at EOF, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); + stateSignal.clear(); + + QTRY_COMPARE(audioOutput.processedUSecs(), 1000000); + + audioOutput.stop(); + QTest::qWait(40); + QVERIFY2((stateSignal.size() == 1), + QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); + + QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); + QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); + + audioFile->close(); +} + +void tst_QAudioSink::volume_data() +{ + QTest::addColumn<float>("actualFloat"); + QTest::addColumn<int>("expectedInt"); + QTest::newRow("Volume 0.3") << 0.3f << 3; + QTest::newRow("Volume 0.6") << 0.6f << 6; + QTest::newRow("Volume 0.9") << 0.9f << 9; +} + +void tst_QAudioSink::volume() +{ + QFETCH(float, actualFloat); + QFETCH(int, expectedInt); + QAudioSink audioOutput(audioDevice.preferredFormat(), this); + + audioOutput.setVolume(actualFloat); + QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); + // Wait a while to see if this changes + QTest::qWait(500); + QTRY_VERIFY(qRound(audioOutput.volume()*10.0f) == expectedInt); +} + +QTEST_MAIN(tst_QAudioSink) + +#include "tst_qaudiosink.moc" |