| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the test suite of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| //TESTED_COMPONENT=src/multimedia |
| |
| #include <QtTest/QtTest> |
| #include <QtCore/qlocale.h> |
| #include <QtCore/QTemporaryDir> |
| #include <QtCore/QSharedPointer> |
| #include <QtCore/QScopedPointer> |
| |
| #include <qaudiooutput.h> |
| #include <qaudiodeviceinfo.h> |
| #include <qaudioformat.h> |
| #include <qaudio.h> |
| |
| #include "wavheader.h" |
| |
| #define AUDIO_BUFFER 192000 |
| |
| #ifndef QTRY_VERIFY2 |
| #define QTRY_VERIFY2(__expr,__msg) \ |
| do { \ |
| const int __step = 50; \ |
| const int __timeout = 5000; \ |
| if (!(__expr)) { \ |
| QTest::qWait(0); \ |
| } \ |
| for (int __i = 0; __i < __timeout && !(__expr); __i+=__step) { \ |
| QTest::qWait(__step); \ |
| } \ |
| QVERIFY2(__expr,__msg); \ |
| } while (0) |
| #endif |
| |
| class tst_QAudioOutput : public QObject |
| { |
| Q_OBJECT |
| public: |
| tst_QAudioOutput(QObject* parent=0) : QObject(parent) {} |
| |
| private slots: |
| void initTestCase(); |
| |
| void format(); |
| void invalidFormat_data(); |
| void invalidFormat(); |
| |
| void bufferSize_data(); |
| void bufferSize(); |
| |
| void notifyInterval_data(); |
| void notifyInterval(); |
| |
| void disableNotifyInterval(); |
| |
| void stopWhileStopped(); |
| void suspendWhileStopped(); |
| void resumeWhileStopped(); |
| |
| void pull_data(){generate_audiofile_testrows();} |
| void pull(); |
| |
| void pullSuspendResume_data(){generate_audiofile_testrows();} |
| void pullSuspendResume(); |
| |
| void push_data(){generate_audiofile_testrows();} |
| void push(); |
| |
| void pushSuspendResume_data(){generate_audiofile_testrows();} |
| void pushSuspendResume(); |
| |
| void pushUnderrun_data(){generate_audiofile_testrows();} |
| void pushUnderrun(); |
| |
| void volume_data(); |
| void volume(); |
| |
| private: |
| typedef QSharedPointer<QFile> FilePtr; |
| |
| QString formatToFileName(const QAudioFormat &format); |
| void createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate = 440); |
| |
| void generate_audiofile_testrows(); |
| |
| QAudioDeviceInfo audioDevice; |
| QList<QAudioFormat> testFormats; |
| QList<FilePtr> audioFiles; |
| QScopedPointer<QTemporaryDir> m_temporaryDir; |
| |
| QScopedPointer<QByteArray> m_byteArray; |
| QScopedPointer<QBuffer> m_buffer; |
| }; |
| |
| QString tst_QAudioOutput::formatToFileName(const QAudioFormat &format) |
| { |
| const QString formatEndian = (format.byteOrder() == QAudioFormat::LittleEndian) |
| ? QString("LE") : QString("BE"); |
| |
| const QString formatSigned = (format.sampleType() == QAudioFormat::SignedInt) |
| ? QString("signed") : QString("unsigned"); |
| |
| return QString("%1_%2_%3_%4_%5") |
| .arg(format.sampleRate()) |
| .arg(format.sampleSize()) |
| .arg(formatSigned) |
| .arg(formatEndian) |
| .arg(format.channelCount()); |
| } |
| |
| void tst_QAudioOutput::createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate) |
| { |
| const int channelBytes = format.sampleSize() / 8; |
| const int sampleBytes = format.channelCount() * channelBytes; |
| |
| 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) { |
| if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::UnSignedInt) { |
| const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255); |
| *reinterpret_cast<quint8*>(ptr) = value; |
| } else if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::SignedInt) { |
| const qint8 value = static_cast<qint8>(x * 127); |
| *reinterpret_cast<quint8*>(ptr) = value; |
| } else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::UnSignedInt) { |
| quint16 value = static_cast<quint16>((1.0 + x) / 2 * 65535); |
| if (format.byteOrder() == QAudioFormat::LittleEndian) |
| qToLittleEndian<quint16>(value, ptr); |
| else |
| qToBigEndian<quint16>(value, ptr); |
| } else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt) { |
| qint16 value = static_cast<qint16>(x * 32767); |
| if (format.byteOrder() == QAudioFormat::LittleEndian) |
| qToLittleEndian<qint16>(value, ptr); |
| else |
| qToBigEndian<qint16>(value, ptr); |
| } |
| |
| ptr += channelBytes; |
| length -= channelBytes; |
| } |
| ++sampleIndex; |
| } |
| |
| m_buffer.reset(new QBuffer(m_byteArray.data(), this)); |
| Q_ASSERT(m_buffer->open(QIODevice::ReadOnly)); |
| } |
| |
| void tst_QAudioOutput::generate_audiofile_testrows() |
| { |
| QTest::addColumn<FilePtr>("audioFile"); |
| QTest::addColumn<QAudioFormat>("audioFormat"); |
| |
| for (int i=0; i<audioFiles.count(); i++) { |
| QTest::newRow(QString("Audio File %1").arg(i).toLocal8Bit().constData()) |
| << audioFiles.at(i) << testFormats.at(i); |
| |
| } |
| } |
| |
| void tst_QAudioOutput::initTestCase() |
| { |
| qRegisterMetaType<QAudioFormat>(); |
| |
| // Only perform tests if audio output device exists |
| const QList<QAudioDeviceInfo> devices = |
| QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); |
| |
| if (devices.size() <= 0) |
| QSKIP("No audio backend"); |
| |
| audioDevice = QAudioDeviceInfo::defaultOutputDevice(); |
| |
| |
| QAudioFormat format; |
| |
| format.setCodec("audio/pcm"); |
| |
| if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) |
| testFormats.append(audioDevice.preferredFormat()); |
| |
| // PCM 8000 mono S8 |
| format.setSampleRate(8000); |
| format.setSampleSize(8); |
| format.setSampleType(QAudioFormat::SignedInt); |
| format.setByteOrder(QAudioFormat::LittleEndian); |
| format.setChannelCount(1); |
| if (audioDevice.isFormatSupported(format)) |
| testFormats.append(format); |
| |
| // PCM 11025 mono S16LE |
| format.setSampleRate(11025); |
| format.setSampleSize(16); |
| 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)) |
| 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 : qAsConst(testFormats)) { |
| qint64 len = (format.sampleRate()*format.channelCount()*(format.sampleSize()/8)*2); // 2 seconds |
| 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())); |
| WavHeader wavHeader(format, len); |
| wavHeader.write(*file.data()); |
| file->write(m_byteArray->data(), len); |
| file->close(); |
| audioFiles.append(file); |
| } |
| } |
| |
| void tst_QAudioOutput::format() |
| { |
| QAudioOutput audioOutput(audioDevice.preferredFormat(), this); |
| |
| QAudioFormat requested = audioDevice.preferredFormat(); |
| QAudioFormat actual = audioOutput.format(); |
| |
| QVERIFY2((requested.channelCount() == actual.channelCount()), |
| QString("channels: requested=%1, actual=%2").arg(requested.channelCount()).arg(actual.channelCount()).toLocal8Bit().constData()); |
| QVERIFY2((requested.sampleRate() == actual.sampleRate()), |
| QString("sampleRate: requested=%1, actual=%2").arg(requested.sampleRate()).arg(actual.sampleRate()).toLocal8Bit().constData()); |
| QVERIFY2((requested.sampleSize() == actual.sampleSize()), |
| QString("sampleSize: requested=%1, actual=%2").arg(requested.sampleSize()).arg(actual.sampleSize()).toLocal8Bit().constData()); |
| QVERIFY2((requested.codec() == actual.codec()), |
| QString("codec: requested=%1, actual=%2").arg(requested.codec()).arg(actual.codec()).toLocal8Bit().constData()); |
| QVERIFY2((requested.byteOrder() == actual.byteOrder()), |
| QString("byteOrder: requested=%1, actual=%2").arg(requested.byteOrder()).arg(actual.byteOrder()).toLocal8Bit().constData()); |
| QVERIFY2((requested.sampleType() == actual.sampleType()), |
| QString("sampleType: requested=%1, actual=%2").arg(requested.sampleType()).arg(actual.sampleType()).toLocal8Bit().constData()); |
| } |
| |
| void tst_QAudioOutput::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.setSampleSize(0); |
| QTest::newRow("Sample size 0") |
| << format; |
| } |
| |
| void tst_QAudioOutput::invalidFormat() |
| { |
| QFETCH(QAudioFormat, invalidFormat); |
| |
| QVERIFY2(!audioDevice.isFormatSupported(invalidFormat), |
| "isFormatSupported() is returning true on an invalid format"); |
| |
| QAudioOutput 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_QAudioOutput::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_QAudioOutput::bufferSize() |
| { |
| QFETCH(int, bufferSize); |
| QAudioOutput audioOutput(audioDevice.preferredFormat(), this); |
| |
| QVERIFY2((audioOutput.error() == QAudio::NoError), QString("error() was not set to QAudio::NoError on creation(%1)").arg(bufferSize).toLocal8Bit().constData()); |
| |
| audioOutput.setBufferSize(bufferSize); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setBufferSize"); |
| QVERIFY2((audioOutput.bufferSize() == bufferSize), |
| QString("bufferSize: requested=%1, actual=%2").arg(bufferSize).arg(audioOutput.bufferSize()).toLocal8Bit().constData()); |
| } |
| |
| void tst_QAudioOutput::notifyInterval_data() |
| { |
| QTest::addColumn<int>("interval"); |
| QTest::newRow("Notify interval 50") << 50; |
| QTest::newRow("Notify interval 100") << 100; |
| QTest::newRow("Notify interval 250") << 250; |
| QTest::newRow("Notify interval 1000") << 1000; |
| } |
| |
| void tst_QAudioOutput::notifyInterval() |
| { |
| QFETCH(int, interval); |
| QAudioOutput audioOutput(audioDevice.preferredFormat(), this); |
| |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); |
| |
| audioOutput.setNotifyInterval(interval); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), QString("error() is not QAudio::NoError after setNotifyInterval(%1)").arg(interval).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.notifyInterval() == interval), |
| QString("notifyInterval: requested=%1, actual=%2").arg(interval).arg(audioOutput.notifyInterval()).toLocal8Bit().constData()); |
| } |
| |
| void tst_QAudioOutput::disableNotifyInterval() |
| { |
| // Sets an invalid notification interval (QAudioOutput::setNotifyInterval(0)) |
| // Checks that |
| // - No error is raised (QAudioOutput::error() returns QAudio::NoError) |
| // - if <= 0, set to zero and disable notify signal |
| |
| QAudioOutput audioOutput(audioDevice.preferredFormat(), this); |
| |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() was not set to QAudio::NoError on creation"); |
| |
| audioOutput.setNotifyInterval(0); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(0)"); |
| QVERIFY2((audioOutput.notifyInterval() == 0), |
| "notifyInterval() is not zero after setNotifyInterval(0)"); |
| |
| audioOutput.setNotifyInterval(-1); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after setNotifyInterval(-1)"); |
| QVERIFY2((audioOutput.notifyInterval() == 0), |
| "notifyInterval() is not zero after setNotifyInterval(-1)"); |
| |
| //start and run to check if notify() is emitted |
| if (audioFiles.size() > 0) { |
| QAudioOutput audioOutputCheck(testFormats.at(0), this); |
| audioOutputCheck.setNotifyInterval(0); |
| audioOutputCheck.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutputCheck, SIGNAL(notify())); |
| QFile *audioFile = audioFiles.at(0).data(); |
| audioFile->open(QIODevice::ReadOnly); |
| audioOutputCheck.start(audioFile); |
| QTest::qWait(3000); // 3 seconds should be plenty |
| audioOutputCheck.stop(); |
| QVERIFY2((notifySignal.count() == 0), |
| QString("didn't disable notify interval: shouldn't have got any but got %1").arg(notifySignal.count()).toLocal8Bit().constData()); |
| audioFile->close(); |
| } |
| } |
| |
| void tst_QAudioOutput::stopWhileStopped() |
| { |
| // Calls QAudioOutput::stop() when object is already in StoppedState |
| // Checks that |
| // - No state change occurs |
| // - No error is raised (QAudioOutput::error() returns QAudio::NoError) |
| |
| QAudioOutput 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, SIGNAL(stateChanged(QAudio::State))); |
| audioOutput.stop(); |
| |
| // Check that no state transition occurred |
| QVERIFY2((stateSignal.count() == 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_QAudioOutput::suspendWhileStopped() |
| { |
| // Calls QAudioOutput::suspend() when object is already in StoppedState |
| // Checks that |
| // - No state change occurs |
| // - No error is raised (QAudioOutput::error() returns QAudio::NoError) |
| |
| QAudioOutput 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, SIGNAL(stateChanged(QAudio::State))); |
| audioOutput.suspend(); |
| |
| // Check that no state transition occurred |
| QVERIFY2((stateSignal.count() == 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_QAudioOutput::resumeWhileStopped() |
| { |
| // Calls QAudioOutput::resume() when object is already in StoppedState |
| // Checks that |
| // - No state change occurs |
| // - No error is raised (QAudioOutput::error() returns QAudio::NoError) |
| |
| QAudioOutput 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, SIGNAL(stateChanged(QAudio::State))); |
| audioOutput.resume(); |
| |
| // Check that no state transition occurred |
| QVERIFY2((stateSignal.count() == 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_QAudioOutput::pull() |
| { |
| QFETCH(FilePtr, audioFile); |
| QFETCH(QAudioFormat, audioFormat); |
| |
| QAudioOutput audioOutput(audioFormat, this); |
| |
| audioOutput.setNotifyInterval(100); |
| audioOutput.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); |
| QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); |
| |
| // 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(WavHeader::headerLength()); |
| |
| audioOutput.start(audioFile.data()); |
| |
| // Check that QAudioOutput immediately transitions to ActiveState |
| QTRY_VERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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()"); |
| QVERIFY(audioOutput.periodSize() > 0); |
| 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.count() > 0); |
| QCOMPARE(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(); |
| |
| qint64 processedUs = audioOutput.processedUSecs(); |
| |
| audioOutput.stop(); |
| QTest::qWait(40); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); |
| |
| QVERIFY2((processedUs == 2000000), |
| QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); |
| QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); |
| QVERIFY2(notifySignal.count() > 0, "not emitting notify() signal"); |
| |
| audioFile->close(); |
| } |
| |
| void tst_QAudioOutput::pullSuspendResume() |
| { |
| QFETCH(FilePtr, audioFile); |
| QFETCH(QAudioFormat, audioFormat); |
| QAudioOutput audioOutput(audioFormat, this); |
| |
| audioOutput.setNotifyInterval(100); |
| audioOutput.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); |
| QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); |
| |
| // 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(WavHeader::headerLength()); |
| |
| audioOutput.start(audioFile.data()); |
| // Check that QAudioOutput immediately transitions to ActiveState |
| QTRY_VERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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()"); |
| QVERIFY(audioOutput.periodSize() > 0); |
| stateSignal.clear(); |
| |
| // Wait for half of clip to play |
| QTest::qWait(1000); |
| |
| audioOutput.suspend(); |
| |
| // Give backends running in separate threads a chance to suspend. |
| QTest::qWait(100); |
| |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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(1000); |
| QVERIFY(audioOutput.elapsedUSecs() > elapsedUs); |
| QVERIFY(audioOutput.processedUSecs() == processedUs); |
| |
| audioOutput.resume(); |
| |
| // Check that QAudioOutput immediately transitions to ActiveState |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after resume(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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 |
| QTest::qWait(3000); // 3 seconds should be plenty |
| |
| QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); |
| QVERIFY(stateSignal.count() > 0); |
| QCOMPARE(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(); |
| |
| processedUs = audioOutput.processedUSecs(); |
| |
| audioOutput.stop(); |
| QTest::qWait(40); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); |
| |
| QVERIFY2((processedUs == 2000000), |
| QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); |
| 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_QAudioOutput::push() |
| { |
| QFETCH(FilePtr, audioFile); |
| QFETCH(QAudioFormat, audioFormat); |
| |
| QAudioOutput audioOutput(audioFormat, this); |
| |
| audioOutput.setNotifyInterval(100); |
| audioOutput.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); |
| QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); |
| |
| // 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(WavHeader::headerLength()); |
| |
| QIODevice* feed = audioOutput.start(); |
| |
| // Check that QAudioOutput immediately transitions to IdleState |
| QTRY_VERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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()"); |
| QVERIFY(audioOutput.periodSize() > 0); |
| 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; |
| bool firstBuffer = true; |
| QByteArray buffer(AUDIO_BUFFER, 0); |
| |
| while (written < audioFile->size()-WavHeader::headerLength()) { |
| |
| if (audioOutput.bytesFree() >= audioOutput.periodSize()) { |
| qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); |
| written += feed->write(buffer.constData(), len); |
| |
| if (firstBuffer) { |
| // Check for transition to ActiveState when data is provided |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after receiving data, got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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"); |
| firstBuffer = false; |
| stateSignal.clear(); |
| } |
| } else |
| QTest::qWait(20); |
| } |
| |
| // Wait until playback finishes |
| QTest::qWait(3000); // 3 seconds should be plenty |
| |
| QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); |
| QVERIFY(stateSignal.count() > 0); |
| QCOMPARE(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(); |
| |
| qint64 processedUs = audioOutput.processedUSecs(); |
| |
| audioOutput.stop(); |
| QTest::qWait(40); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); |
| |
| QVERIFY2((processedUs == 2000000), |
| QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error() is not QAudio::NoError after stop()"); |
| QVERIFY2((audioOutput.elapsedUSecs() == (qint64)0), "elapsedUSecs() not equal to zero in StoppedState"); |
| QVERIFY2(notifySignal.count() > 0, "not emitting notify signal"); |
| |
| audioFile->close(); |
| } |
| |
| void tst_QAudioOutput::pushSuspendResume() |
| { |
| QFETCH(FilePtr, audioFile); |
| QFETCH(QAudioFormat, audioFormat); |
| |
| QAudioOutput audioOutput(audioFormat, this); |
| |
| audioOutput.setNotifyInterval(100); |
| audioOutput.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); |
| QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); |
| |
| // 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(WavHeader::headerLength()); |
| |
| QIODevice* feed = audioOutput.start(); |
| |
| // Check that QAudioOutput immediately transitions to IdleState |
| QTRY_VERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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()"); |
| QVERIFY(audioOutput.periodSize() > 0); |
| 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; |
| bool firstBuffer = true; |
| QByteArray buffer(AUDIO_BUFFER, 0); |
| |
| // Play half of the clip |
| while (written < (audioFile->size()-WavHeader::headerLength())/2) { |
| |
| if (audioOutput.bytesFree() >= audioOutput.periodSize()) { |
| qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); |
| written += feed->write(buffer.constData(), len); |
| |
| if (firstBuffer) { |
| // Check for transition to ActiveState when data is provided |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after receiving data, got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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"); |
| firstBuffer = false; |
| } |
| } else |
| QTest::qWait(20); |
| } |
| stateSignal.clear(); |
| |
| audioOutput.suspend(); |
| |
| // Give backends running in separate threads a chance to suspend. |
| QTest::qWait(100); |
| |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit SuspendedState signal after suspend(), got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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(1000); |
| 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 QAudioOutput immediately transitions to IdleState |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after resume(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transition to IdleState after resume()"); |
| QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after resume()"); |
| stateSignal.clear(); |
| |
| // Play rest of the clip |
| while (!audioFile->atEnd()) { |
| if (audioOutput.bytesFree() >= audioOutput.periodSize()) { |
| qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); |
| written += feed->write(buffer.constData(), len); |
| QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after writing audio data"); |
| } else |
| QTest::qWait(20); |
| } |
| stateSignal.clear(); |
| |
| // Wait until playback finishes |
| QTest::qWait(1000); // 1 seconds should be plenty |
| |
| QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); |
| QVERIFY(stateSignal.count() > 0); |
| QCOMPARE(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(); |
| |
| processedUs = audioOutput.processedUSecs(); |
| |
| audioOutput.stop(); |
| QTest::qWait(40); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); |
| |
| QVERIFY2((processedUs == 2000000), |
| QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); |
| 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_QAudioOutput::pushUnderrun() |
| { |
| QFETCH(FilePtr, audioFile); |
| QFETCH(QAudioFormat, audioFormat); |
| |
| QAudioOutput audioOutput(audioFormat, this); |
| |
| audioOutput.setNotifyInterval(100); |
| audioOutput.setVolume(0.1f); |
| |
| QSignalSpy notifySignal(&audioOutput, SIGNAL(notify())); |
| QSignalSpy stateSignal(&audioOutput, SIGNAL(stateChanged(QAudio::State))); |
| |
| // 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(WavHeader::headerLength()); |
| |
| QIODevice* feed = audioOutput.start(); |
| |
| // Check that QAudioOutput immediately transitions to IdleState |
| QTRY_VERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal on start(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().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()"); |
| QVERIFY(audioOutput.periodSize() > 0); |
| 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; |
| bool firstBuffer = true; |
| QByteArray buffer(AUDIO_BUFFER, 0); |
| |
| // Play half of the clip |
| while (written < (audioFile->size()-WavHeader::headerLength())/2) { |
| |
| if (audioOutput.bytesFree() >= audioOutput.periodSize()) { |
| qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); |
| written += feed->write(buffer.constData(), len); |
| |
| if (firstBuffer) { |
| // Check for transition to ActiveState when data is provided |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after receiving data, got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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"); |
| firstBuffer = false; |
| } |
| } else |
| QTest::qWait(20); |
| } |
| stateSignal.clear(); |
| |
| // Wait for data to be played |
| QTest::qWait(1000); |
| |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit IdleState signal after suspend(), got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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(); |
| |
| firstBuffer = true; |
| // Play rest of the clip |
| while (!audioFile->atEnd()) { |
| if (audioOutput.bytesFree() >= audioOutput.periodSize()) { |
| qint64 len = audioFile->read(buffer.data(),audioOutput.periodSize()); |
| written += feed->write(buffer.constData(), len); |
| if (firstBuffer) { |
| // Check for transition to ActiveState when data is provided |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit signal after receiving data, got %1 signals instead") |
| .arg(stateSignal.count()).toLocal8Bit().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"); |
| firstBuffer = false; |
| } |
| } else |
| QTest::qWait(20); |
| } |
| stateSignal.clear(); |
| |
| // Wait until playback finishes |
| QTest::qWait(1000); // 1 seconds should be plenty |
| |
| QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit IdleState signal when at EOF, got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::IdleState), "didn't transitions to IdleState when at EOF"); |
| stateSignal.clear(); |
| |
| qint64 processedUs = audioOutput.processedUSecs(); |
| |
| audioOutput.stop(); |
| QTest::qWait(40); |
| QVERIFY2((stateSignal.count() == 1), |
| QString("didn't emit StoppedState signal after stop(), got %1 signals instead").arg(stateSignal.count()).toLocal8Bit().constData()); |
| QVERIFY2((audioOutput.state() == QAudio::StoppedState), "didn't transitions to StoppedState after stop()"); |
| |
| QVERIFY2((processedUs == 2000000), |
| QString("processedUSecs() doesn't equal file duration in us (%1)").arg(processedUs).toLocal8Bit().constData()); |
| 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_QAudioOutput::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_QAudioOutput::volume() |
| { |
| QFETCH(float, actualFloat); |
| QFETCH(int, expectedInt); |
| QAudioOutput 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_QAudioOutput) |
| |
| #include "tst_qaudiooutput.moc" |