Revision control
Copy as Markdown
// 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/osdetection_p.h>
#ifdef Q_OS_MACOS
# include <QtMultimediaTestLib/private/qcoreaudioaggregatedevice_p.h>
#endif
#include <QtMultimediaTestLib/private/qmockiodevice_p.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/qaudio.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtMultimedia/qaudioformat.h>
#include <QtMultimedia/qaudiosource.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/qwavedecoder.h>
#include <QtMultimedia/private/qaudiosystem_p.h>
#include <QtCore/qbuffer.h>
#include <QtCore/qsemaphore.h>
#include <QtCore/qtemporarydir.h>
#include <memory>
QT_WARNING_DISABLE_DEPRECATED; // Tests use QWaveDecoder
#define RANGE_ERR 0.5
namespace {
template<typename T> inline bool qTolerantCompare(T value, T expected)
{
return qAbs(value - expected) < (RANGE_ERR * expected);
}
bool isPulseAudioBackend()
{
return QPlatformMediaIntegration::audioBackendName() == "PulseAudio";
}
} // namespace
using AudioSourceInitializer = bool (*)(QAudioSource &);
class tst_QAudioSource : public QObject
{
Q_OBJECT
public:
tst_QAudioSource(QObject* parent=nullptr) : QObject(parent) {}
private slots:
void initTestCase();
void format();
void invalidFormat_data();
void invalidFormat();
void nullFormat();
void bufferSize();
void bufferSize_getValidDefault();
void bufferSize_setAfterStart();
void bufferSize_updatedAfterStart();
void stopWhileStopped();
void stopWhileSuspended();
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 reset_data(){generate_audiofile_testrows();}
void reset();
void volume_data(){generate_audiofile_testrows();}
void volume();
void nativePeriodFrameCount_getReturnsMinusOneWhenUnset();
void nativePeriodFrameCount_setValidValues();
void nativePeriodFrameCount_getReturnsSet();
void nativePeriodFrameCount_setInvalidValues();
void nativePeriodFrameCount_rejectWhenNotStopped();
void nativePeriodFrameCount_setMinusOneUnsets();
void stop_finishesPushMode_whenInvokedUponReadyReadSignal();
void stop_stopsAudioSource_whenInvokedUponFirstStateChange_data();
void stop_stopsAudioSource_whenInvokedUponFirstStateChange();
void stateChanged_stringBasedConnect();
void start_withSamplingRate_data();
void start_withSamplingRate();
void callbackAPI();
void callbackAPI_startFailsWithWrongType();
void callbackAPI_startWithMoveOnlyFunctor();
#ifdef Q_OS_MACOS
void coreaudio_resetAfterDeviceDisconnect_doesNotUseAfterFree(); // QTBUG-151333
#endif
private:
using FilePtr = std::shared_ptr<QFile>;
QString formatToFileName(const QAudioFormat &format);
void generate_audiofile_testrows();
QAudioDevice audioDevice;
QList<QAudioFormat> testFormats;
QList<FilePtr> audioFiles;
std::unique_ptr<QTemporaryDir> m_temporaryDir;
std::unique_ptr<QByteArray> m_byteArray;
std::unique_ptr<QBuffer> m_buffer;
};
void tst_QAudioSource::generate_audiofile_testrows()
{
QTest::addColumn<FilePtr>("audioFile");
QTest::addColumn<QAudioFormat>("audioFormat");
for (int i=0; i<audioFiles.size(); i++) {
QTest::newRow(QStringLiteral("%1").arg(i).toUtf8().constData())
<< audioFiles.at(i) << testFormats.at(i);
}
}
QString tst_QAudioSource::formatToFileName(const QAudioFormat &format)
{
return QStringLiteral("%1_%2_%3")
.arg(format.sampleRate())
.arg(format.bytesPerSample())
.arg(format.channelCount());
}
void tst_QAudioSource::initTestCase()
{
if (isMacOS && isCI())
QSKIP("QAudioSource requires microphone permissions");
// Only perform tests if audio input device exists
const QList<QAudioDevice> devices = QMediaDevices::audioInputs();
if (devices.size() <= 0)
QSKIP("No audio inputs found");
audioDevice = QMediaDevices::defaultAudioInput();
QAudioFormat format;
format.setChannelCount(1);
if (audioDevice.isFormatSupported(audioDevice.preferredFormat())) {
if (format.sampleFormat() == QAudioFormat::Int16)
testFormats.append(audioDevice.preferredFormat());
}
format.setSampleFormat(QAudioFormat::Int16);
for (int channels : { 1, 2 }) {
format.setChannelCount(channels);
for (int rate : { 44100, 48000 }) {
format.setSampleRate(rate);
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_qaudioinputXXXXXX";
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)) {
const QString fileName = temporaryAudioPath + formatToFileName(format) + QStringLiteral(".wav");
audioFiles.append(std::make_shared<QFile>(fileName));
}
}
void tst_QAudioSource::format()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QAudioFormat requested = audioDevice.preferredFormat();
QAudioFormat actual = audioSource.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());
QCOMPARE(actual, requested);
}
void tst_QAudioSource::invalidFormat_data()
{
QTest::addColumn<QAudioFormat>("invalidFormat");
QAudioFormat 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_QAudioSource::invalidFormat()
{
QFETCH(QAudioFormat, invalidFormat);
QVERIFY2(!audioDevice.isFormatSupported(invalidFormat),
"isFormatSupported() is returning true on an invalid format");
QAudioSource audioSource(invalidFormat, this);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QTest::ignoreMessage(QtWarningMsg, "QAudioSource::start: QAudioFormat not valid");
audioSource.start();
// Check that error is raised
QTRY_VERIFY2((audioSource.error() == QAudio::OpenError),
"error() was not set to QAudio::OpenError after start()");
}
void tst_QAudioSource::nullFormat()
{
QAudioDevice audioDevice = QMediaDevices::defaultAudioInput();
if (audioDevice.isNull())
QSKIP("No audio inputs found");
{
QAudioSource audioSource;
QCOMPARE(audioSource.format(), audioDevice.preferredFormat());
}
{
QAudioSource audioSource(audioDevice);
QCOMPARE(audioSource.format(), audioDevice.preferredFormat());
}
}
void tst_QAudioSource::bufferSize()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError on creation");
audioSource.setBufferSize(512);
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after setBufferSize(512)");
QVERIFY2((audioSource.bufferSize() == 512),
QStringLiteral("bufferSize: requested=512, actual=%2")
.arg(audioSource.bufferSize())
.toUtf8()
.constData());
audioSource.setBufferSize(4096);
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after setBufferSize(4096)");
QVERIFY2((audioSource.bufferSize() == 4096),
QStringLiteral("bufferSize: requested=4096, actual=%2")
.arg(audioSource.bufferSize())
.toUtf8()
.constData());
audioSource.setBufferSize(8192);
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after setBufferSize(8192)");
QVERIFY2((audioSource.bufferSize() == 8192),
QStringLiteral("bufferSize: requested=8192, actual=%2")
.arg(audioSource.bufferSize())
.toUtf8()
.constData());
}
void tst_QAudioSource::bufferSize_getValidDefault()
{
#if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN))
QSKIP("bufferSize validation only fully implemented on Mac and Windows");
#endif
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QCOMPARE_GE(audioSource.bufferSize(),
audioDevice.preferredFormat().bytesForDuration(250'000)); // 250ms
}
void tst_QAudioSource::bufferSize_setAfterStart()
{
#if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN))
QSKIP("bufferSize validation only fully implemented on Mac and Windows");
#endif
QAudioSource audioSource(audioDevice.preferredFormat(), this);
audioSource.start();
QCOMPARE_GE(audioSource.bufferSize(), audioDevice.preferredFormat().bytesForFrames(32));
}
void tst_QAudioSource::bufferSize_updatedAfterStart()
{
#if !(defined(Q_OS_MACOS) || defined(Q_OS_WIN))
QSKIP("bufferSize validation only fully implemented on Mac and Windows");
#endif
QAudioSource audioSource(audioDevice.preferredFormat(), this);
audioSource.setBufferSize(1); // small enough force a size increase
audioSource.start();
QCOMPARE_GE(audioSource.bufferSize(), audioDevice.preferredFormat().bytesForFrames(32));
}
void tst_QAudioSource::stopWhileStopped()
{
// Calls QAudioSource::stop() when object is already in StoppedState
// Checks that
// - No state change occurs
// - No error is raised (QAudioSource::error() returns QAudio::NoError)
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
audioSource.stop();
// Check that no state transition occurred
QVERIFY2((stateSignal.size() == 0), "stop() while stopped is emitting a signal and it shouldn't");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError after stop()");
}
void tst_QAudioSource::stopWhileSuspended()
{
using namespace std::chrono_literals;
QAudioSource audioSource(audioDevice.preferredFormat(), this);
audioSource.start();
QTest::qWait(10ms); // give WASAPI worker thread a bit of time to arrive at WaitForSingleObject
audioSource.suspend();
QTRY_COMPARE_EQ(audioSource.state(), QAudio::SuspendedState);
QTest::qWait(10ms); // give WASAPI worker thread a bit of time to arrive at WaitForSingleObject
audioSource.stop();
QTRY_COMPARE_EQ(audioSource.state(), QAudio::StoppedState);
}
void tst_QAudioSource::suspendWhileStopped()
{
// Calls QAudioSource::suspend() when object is already in StoppedState
// Checks that
// - No state change occurs
// - No error is raised (QAudioSource::error() returns QAudio::NoError)
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
audioSource.suspend();
// Check that no state transition occurred
QVERIFY2((stateSignal.size() == 0), "stop() while suspended is emitting a signal and it shouldn't");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError after stop()");
}
void tst_QAudioSource::resumeWhileStopped()
{
// Calls QAudioSource::resume() when object is already in StoppedState
// Checks that
// - No state change occurs
// - No error is raised (QAudioSource::error() returns QAudio::NoError)
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
audioSource.resume();
// Check that no state transition occurred
QVERIFY2((stateSignal.size() == 0), "resume() while stopped is emitting a signal and it shouldn't");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError after resume()");
}
void tst_QAudioSource::pull()
{
QFETCH(FilePtr, audioFile);
QFETCH(QAudioFormat, audioFormat);
QAudioSource audioSource(audioFormat, this);
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
audioFile->close();
QTEST_ASSERT(audioFile->open(QIODevice::WriteOnly));
QWaveDecoder waveDecoder(audioFile.get(), audioFormat);
if (!waveDecoder.open(QIODevice::WriteOnly)) {
waveDecoder.close();
audioFile->close();
QSKIP("Audio format not supported for writing to WAV file.");
}
QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength());
audioSource.start(audioFile.get());
// Check that QAudioSource immediately transitions to ActiveState or IdleState
QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on start()");
QVERIFY2((audioSource.state() == QAudio::ActiveState
|| audioSource.state() == QAudio::IdleState),
"didn't transition to ActiveState or IdleState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
// Check that 'elapsed' increases
QTRY_COMPARE_GT(audioSource.elapsedUSecs(), 0);
QTRY_COMPARE_GT(audioSource.processedUSecs(), 0);
// Allow some recording to happen
QTest::qWait(3000); // 3 seconds should be plenty
stateSignal.clear();
if (!isPulseAudioBackend()) {
// QTBUG-138000 ... pulseaudio has a rather odd timing behaviour
qint64 processedUs = audioSource.processedUSecs();
QVERIFY2(
qTolerantCompare(processedUs, 3000000LL),
QStringLiteral(
"processedUSecs() doesn't fall in acceptable range, should be 3000000 (%1)")
.arg(processedUs)
.toUtf8()
.constData());
}
audioSource.stop();
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after stop()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after stop()");
QVERIFY2((audioSource.elapsedUSecs() == (qint64)0),
"elapsedUSecs() not equal to zero in StoppedState");
//QWaveHeader::writeDataLength(*audioFile, audioFile->pos() - WavHeader::headerLength());
//waveDecoder.writeDataLength();
waveDecoder.close();
audioFile->close();
}
void tst_QAudioSource::pullSuspendResume()
{
QFETCH(FilePtr, audioFile);
QFETCH(QAudioFormat, audioFormat);
QAudioSource audioSource(audioFormat, this);
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
audioFile->close();
QTEST_ASSERT(audioFile->open(QIODevice::WriteOnly));
QWaveDecoder waveDecoder(audioFile.get(), audioFormat);
if (!waveDecoder.open(QIODevice::WriteOnly)) {
waveDecoder.close();
audioFile->close();
QSKIP("Audio format not supported for writing to WAV file.");
}
QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength());
audioSource.start(audioFile.get());
// Check that QAudioSource immediately transitions to ActiveState or IdleState
QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on start()");
QVERIFY2((audioSource.state() == QAudio::ActiveState
|| audioSource.state() == QAudio::IdleState),
"didn't transition to ActiveState or IdleState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
// Check that 'elapsed' increases
QTRY_VERIFY2((audioSource.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()");
QTRY_VERIFY2((audioSource.processedUSecs() > 0), "elapsedUSecs() is still zero after start()");
// Allow some recording to happen
QTest::qWait(3000); // 3 seconds should be plenty
QVERIFY2((audioSource.state() == QAudio::ActiveState),
"didn't transition to ActiveState after some recording");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after some recording");
stateSignal.clear();
audioSource.suspend();
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral(
"didn't emit SuspendedState signal after suspend(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::SuspendedState),
"didn't transitions to SuspendedState after stop()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after stop()");
stateSignal.clear();
// Check that only 'elapsed', and not 'processed' increases while suspended
if (!isPulseAudioBackend()) {
// QTBUG-138000 ... pulseaudio has a rather odd timing behaviour
qint64 elapsedUs = audioSource.elapsedUSecs();
qint64 processedUs = audioSource.processedUSecs();
QVERIFY2(
qTolerantCompare(processedUs, 3000000LL),
QStringLiteral(
"processedUSecs() doesn't fall in acceptable range, should be 3000000 (%1)")
.arg(processedUs)
.toUtf8()
.constData());
QTRY_COMPARE_GT(audioSource.elapsedUSecs(), elapsedUs);
QCOMPARE(audioSource.processedUSecs(), processedUs);
}
audioSource.resume();
// Check that QAudioSource immediately transitions to ActiveState
QTRY_VERIFY2((stateSignal.size() == 1),
QStringLiteral("didn't emit signal after resume(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::ActiveState),
"didn't transition to ActiveState after resume()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after resume()");
stateSignal.clear();
audioSource.stop();
QTest::qWait(40);
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after stop()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after stop()");
QVERIFY2((audioSource.elapsedUSecs() == (qint64)0),
"elapsedUSecs() not equal to zero in StoppedState");
//WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength());
//waveDecoder.writeDataLength();
waveDecoder.close();
audioFile->close();
}
void tst_QAudioSource::push()
{
QFETCH(FilePtr, audioFile);
QFETCH(QAudioFormat, audioFormat);
QAudioSource audioSource(audioFormat, this);
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
audioFile->close();
QTEST_ASSERT(audioFile->open(QIODevice::WriteOnly));
QWaveDecoder waveDecoder(audioFile.get(), audioFormat);
if (!waveDecoder.open(QIODevice::WriteOnly)) {
waveDecoder.close();
audioFile->close();
QSKIP("Audio format not supported for writing to WAV file.");
}
QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength());
// Set a large buffer to avoid underruns during QTest::qWaits
audioSource.setBufferSize(audioFormat.bytesForDuration(100000));
QIODevice *feed = audioSource.start();
// Check that QAudioSource immediately transitions to IdleState
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()");
QVERIFY2((audioSource.state() == QAudio::IdleState),
"didn't transition to IdleState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
// Check that 'elapsed' increases
QTest::qWait(40);
QVERIFY2((audioSource.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()");
qint64 totalBytesRead = 0;
bool firstBuffer = true;
qint64 len = audioFormat.sampleRate()*audioFormat.bytesPerFrame()/2; // .5 seconds
while (totalBytesRead < len) {
QTRY_VERIFY_WITH_TIMEOUT(audioSource.bytesAvailable() > 0, 1000);
QByteArray buffer = feed->readAll();
audioFile->write(buffer);
totalBytesRead += buffer.size();
if (firstBuffer && buffer.size()) {
// Check for transition to ActiveState when data is provided
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit ActiveState signal on data");
QVERIFY2((audioSource.state() == QAudio::ActiveState),
"didn't transition to ActiveState after data");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
firstBuffer = false;
}
}
stateSignal.clear();
qint64 processedUs = audioSource.processedUSecs();
audioSource.stop();
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after stop()");
QVERIFY2(qTolerantCompare(processedUs, 500000LL),
QStringLiteral(
"processedUSecs() doesn't fall in acceptable range, should be 500000 (%1)")
.arg(processedUs)
.toUtf8()
.constData());
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after stop()");
QVERIFY2((audioSource.elapsedUSecs() == (qint64)0),
"elapsedUSecs() not equal to zero in StoppedState");
//WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength());
//waveDecoder.writeDataLength();
waveDecoder.close();
audioFile->close();
}
void tst_QAudioSource::pushSuspendResume()
{
QFETCH(FilePtr, audioFile);
QFETCH(QAudioFormat, audioFormat);
QAudioSource audioSource(audioFormat, this);
audioSource.setBufferSize(audioFormat.bytesForDuration(100000));
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
audioFile->close();
QTEST_ASSERT(audioFile->open(QIODevice::WriteOnly));
QWaveDecoder waveDecoder(audioFile.get(), audioFormat);
if (!waveDecoder.open(QIODevice::WriteOnly)) {
waveDecoder.close();
audioFile->close();
QSKIP("Audio format not supported for writing to WAV file.");
}
QCOMPARE(waveDecoder.size(), QWaveDecoder::headerLength());
QIODevice *feed = audioSource.start();
// Check that QAudioSource immediately transitions to IdleState
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()");
QVERIFY2((audioSource.state() == QAudio::IdleState),
"didn't transition to IdleState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
// Check that 'elapsed' increases
QTRY_VERIFY2((audioSource.elapsedUSecs() > 0), "elapsedUSecs() is still zero after start()");
qint64 totalBytesRead = 0;
bool firstBuffer = true;
qint64 len = audioFormat.sampleRate() * audioFormat.bytesPerFrame() / 2; // .5 seconds
while (totalBytesRead < len) {
QTRY_VERIFY_WITH_TIMEOUT(audioSource.bytesAvailable() > 0, 1000);
auto buffer = feed->readAll();
audioFile->write(buffer);
totalBytesRead += buffer.size();
if (firstBuffer && buffer.size()) {
// Check for transition to ActiveState when data is provided
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit ActiveState signal on data");
QVERIFY2((audioSource.state() == QAudio::ActiveState),
"didn't transition to ActiveState after data");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
firstBuffer = false;
}
}
stateSignal.clear();
audioSource.suspend();
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral(
"didn't emit SuspendedState signal after suspend(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::SuspendedState),
"didn't transitions to SuspendedState after stop()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() is not QAudio::NoError after stop()");
stateSignal.clear();
// Check that only 'elapsed', and not 'processed' increases while suspended
qint64 elapsedUs = audioSource.elapsedUSecs();
qint64 processedUs = audioSource.processedUSecs();
QTRY_COMPARE_GT(audioSource.elapsedUSecs(), elapsedUs);
QCOMPARE(audioSource.processedUSecs(), processedUs);
// Drain any data, in case we run out of space when resuming
while (feed->readAll().size() > 0)
;
QCOMPARE(audioSource.bytesAvailable(), 0);
audioSource.resume();
// Check that QAudioSource immediately transitions to Active or IdleState
QTRY_VERIFY2((stateSignal.size() > 0),"didn't emit signals on resume()");
QVERIFY2((audioSource.state() == QAudio::ActiveState
|| audioSource.state() == QAudio::IdleState),
"didn't transition to ActiveState or IdleState after resume()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after resume()");
stateSignal.clear();
// Read another seconds worth
totalBytesRead = 0;
firstBuffer = true;
while (totalBytesRead < len && audioSource.state() != QAudio::StoppedState) {
QTRY_VERIFY(audioSource.bytesAvailable() > 0);
auto buffer = feed->readAll();
audioFile->write(buffer);
totalBytesRead += buffer.size();
}
stateSignal.clear();
processedUs = audioSource.processedUSecs();
audioSource.stop();
QTRY_VERIFY2(
(stateSignal.size() == 1),
QStringLiteral("didn't emit StoppedState signal after stop(), got %1 signals instead")
.arg(stateSignal.size())
.toUtf8()
.constData());
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after stop()");
QVERIFY2(qTolerantCompare(processedUs, 1000000LL),
QStringLiteral(
"processedUSecs() doesn't fall in acceptable range, should be 2040000 (%1)")
.arg(processedUs)
.toUtf8()
.constData());
QVERIFY2((audioSource.elapsedUSecs() == (qint64)0),
"elapsedUSecs() not equal to zero in StoppedState");
//WavHeader::writeDataLength(*audioFile,audioFile->pos()-WavHeader::headerLength());
//waveDecoder.writeDataLength();
waveDecoder.close();
audioFile->close();
}
void tst_QAudioSource::reset()
{
QFETCH(QAudioFormat, audioFormat);
// Try both push/pull.. the vagaries of Active vs Idle are tested elsewhere
{
QAudioSource audioSource(audioFormat, this);
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
QIODevice *device = audioSource.start();
// Check that QAudioSource immediately transitions to IdleState
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit IdleState signal on start()");
QVERIFY2((audioSource.state() == QAudio::IdleState),
"didn't transition to IdleState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
QTRY_VERIFY2_WITH_TIMEOUT((audioSource.bytesAvailable() > 0),
"no bytes available after starting", 10000);
// Trigger a read
QByteArray data = device->readAll();
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
audioSource.reset();
QTRY_VERIFY2((stateSignal.size() == 1),"didn't emit StoppedState signal after reset()");
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after reset()");
QVERIFY2((audioSource.bytesAvailable() == 0), "buffer not cleared after reset()");
}
{
QAudioSource audioSource(audioFormat, this);
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
QSignalSpy stateSignal(&audioSource, &QAudioSource::stateChanged);
// Check that we are in the default state before calling start
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"state() was not set to StoppedState before start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error() was not set to QAudio::NoError before start()");
QVERIFY2((audioSource.elapsedUSecs() == qint64(0)), "elapsedUSecs() not zero on creation");
audioSource.start(&buffer);
// Check that QAudioSource immediately transitions to ActiveState
QTRY_VERIFY2((stateSignal.size() >= 1),"didn't emit state changed signal on start()");
QTRY_VERIFY2((audioSource.state() == QAudio::ActiveState),
"didn't transition to ActiveState after start()");
QVERIFY2((audioSource.error() == QAudio::NoError),
"error state is not equal to QAudio::NoError after start()");
stateSignal.clear();
audioSource.reset();
QTRY_VERIFY2((stateSignal.size() >= 1),"didn't emit StoppedState signal after reset()");
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after reset()");
QVERIFY2((audioSource.bytesAvailable() == 0), "buffer not cleared after reset()");
}
}
void tst_QAudioSource::volume()
{
QFETCH(QAudioFormat, audioFormat);
const qreal half(0.5f);
const qreal one(1.0f);
QAudioSource audioSource(audioFormat, this);
qreal volume = audioSource.volume();
audioSource.setVolume(half);
QTRY_VERIFY(qRound(audioSource.volume() * 10.0f) == 5);
audioSource.setVolume(one);
QTRY_VERIFY(qRound(audioSource.volume() * 10.0f) == 10);
audioSource.setVolume(half);
audioSource.start();
QTRY_VERIFY(qRound(audioSource.volume() * 10.0f) == 5);
audioSource.setVolume(one);
QTRY_VERIFY(qRound(audioSource.volume() * 10.0f) == 10);
audioSource.setVolume(volume);
}
void tst_QAudioSource::nativePeriodFrameCount_getReturnsMinusOneWhenUnset()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QCOMPARE(audioSource.nativePeriodFrameCount(), -1);
}
void tst_QAudioSource::nativePeriodFrameCount_setValidValues()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QCOMPARE(audioSource.state(), QAudio::StoppedState);
// Test valid power-of-2 values
const auto validValues = { 32, 64, 128, 256, 512, 1024, 2048, 4096 };
for (int value : validValues) {
audioSource.setNativePeriodFrameCount(value);
QCOMPARE(audioSource.nativePeriodFrameCount(), value);
}
}
void tst_QAudioSource::nativePeriodFrameCount_getReturnsSet()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
QCOMPARE(audioSource.nativePeriodFrameCount(), -1);
audioSource.setNativePeriodFrameCount(256);
QCOMPARE(audioSource.nativePeriodFrameCount(), 256);
audioSource.setNativePeriodFrameCount(1024);
QCOMPARE(audioSource.nativePeriodFrameCount(), 1024);
}
void tst_QAudioSource::nativePeriodFrameCount_setInvalidValues()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
// Test invalid values - these should not be set
const auto invalidValues = { 33, 31, 4093, 8192 };
for (int value : invalidValues) {
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression("QAudioSource::setNativePeriodFrameCount:.*"));
audioSource.setNativePeriodFrameCount(value);
QCOMPARE(audioSource.nativePeriodFrameCount(), -1);
}
}
void tst_QAudioSource::nativePeriodFrameCount_rejectWhenNotStopped()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
// Try to set when in active state - should be rejected
audioSource.start();
QTRY_VERIFY(audioSource.state() != QAudio::StoppedState);
QTest::ignoreMessage(QtWarningMsg,
QRegularExpression("QAudioSource::setNativePeriodFrameCount:.*can only be set when stopped.*"));
audioSource.setNativePeriodFrameCount(256);
QCOMPARE(audioSource.nativePeriodFrameCount(), -1);
audioSource.stop();
}
void tst_QAudioSource::nativePeriodFrameCount_setMinusOneUnsets()
{
QAudioSource audioSource(audioDevice.preferredFormat(), this);
audioSource.setNativePeriodFrameCount(512);
QCOMPARE(audioSource.nativePeriodFrameCount(), 512);
audioSource.setNativePeriodFrameCount(-1);
QCOMPARE(audioSource.nativePeriodFrameCount(), -1);
}
void tst_QAudioSource::stop_finishesPushMode_whenInvokedUponReadyReadSignal()
{
const auto defaultAudioInputDevice = QMediaDevices::defaultAudioInput();
QAudioFormat audioFormat;
audioFormat.setSampleFormat(QAudioFormat::Int16);
audioFormat.setSampleRate(qBound(defaultAudioInputDevice.minimumSampleRate(), 48000,
defaultAudioInputDevice.maximumSampleRate()));
audioFormat.setChannelCount(qBound(defaultAudioInputDevice.minimumChannelCount(), 2,
defaultAudioInputDevice.maximumChannelCount()));
const auto isFormatSupported = defaultAudioInputDevice.isFormatSupported(audioFormat);
QCOMPARE(isFormatSupported, true);
QAudioSource audioSource(audioFormat, this);
const auto audioInputDevice = audioSource.start();
auto isReadyReadReceived = false;
connect(audioInputDevice, &QIODevice::readyRead, this, [&]() {
audioSource.stop();
isReadyReadReceived = true;
});
const auto awaitedValue = QTest::qWaitFor([&] { return isReadyReadReceived; });
QVERIFY2(awaitedValue, "didn't receive readyRead signal");
QVERIFY2((audioSource.state() == QAudio::StoppedState),
"didn't transitions to StoppedState after close()");
}
void tst_QAudioSource::stop_stopsAudioSource_whenInvokedUponFirstStateChange_data()
{
QTest::addColumn<AudioSourceInitializer>("initializer");
AudioSourceInitializer initPullMode = [](QAudioSource &source) {
QIODevice *device = new MockIODevice(&source);
device->open(QIODevice::WriteOnly);
source.start(device);
return source.error() == QtAudio::NoError;
};
AudioSourceInitializer initPushMode = [](QAudioSource &source) {
QIODevice *device = source.start();
return device && source.error() == QtAudio::NoError;
};
QTest::newRow("pullMode") << initPullMode;
QTest::newRow("pushMode") << initPushMode;
}
void tst_QAudioSource::stop_stopsAudioSource_whenInvokedUponFirstStateChange()
{
if (isAndroid)
// Revisit after migrating to AAudio
QSKIP("'initializer(audioSource)' returned FALSE");
QFETCH(const AudioSourceInitializer, initializer);
const QAudioDevice defaultAudioInputDevice = QMediaDevices::defaultAudioInput();
QAudioFormat audioFormat;
audioFormat.setSampleFormat(QAudioFormat::Int16);
audioFormat.setSampleRate(qBound(defaultAudioInputDevice.minimumSampleRate(), 48000,
defaultAudioInputDevice.maximumSampleRate()));
audioFormat.setChannelCount(qBound(defaultAudioInputDevice.minimumChannelCount(), 2,
defaultAudioInputDevice.maximumChannelCount()));
QAudioSource audioSource(audioFormat);
auto stop = [&audioSource]() {
audioSource.stop();
QCOMPARE(audioSource.state(), QtAudio::State::StoppedState);
};
connect(&audioSource, &QAudioSource::stateChanged, this, stop, Qt::SingleShotConnection);
QVERIFY(initializer(audioSource));
QTRY_COMPARE(audioSource.state(), QtAudio::State::StoppedState);
}
void tst_QAudioSource::stateChanged_stringBasedConnect()
{
const QAudioDevice defaultAudioInputDevice = QMediaDevices::defaultAudioInput();
QAudioSource audioSource(defaultAudioInputDevice);
QSignalSpy stateSignal(&audioSource, SIGNAL(stateChanged(QAudio::State)));
audioSource.start();
QTRY_VERIFY(!stateSignal.empty());
}
void tst_QAudioSource::start_withSamplingRate_data()
{
QTest::addColumn<int>("rate");
QTest::newRow("minimum") << audioDevice.minimumSampleRate();
QTest::newRow("preferred") << audioDevice.preferredFormat().sampleRate();
QTest::newRow("maximum") << audioDevice.maximumSampleRate();
}
void tst_QAudioSource::start_withSamplingRate()
{
QFETCH(int, rate);
QAudioFormat format = audioDevice.preferredFormat();
format.setSampleRate(rate);
QAudioSource audioSource(format, this);
audioSource.start();
QTRY_COMPARE(audioSource.state(), QAudio::State::IdleState);
}
void tst_QAudioSource::callbackAPI()
{
#if QT_CONFIG(thread)
using namespace std::chrono_literals;
QAudioFormat format = audioDevice.preferredFormat();
format.setSampleFormat(QAudioFormat::SampleFormat::Float);
QAudioSource audioSource(audioDevice, format);
QPlatformAudioSource *platformSource = QPlatformAudioSource::get(audioSource);
if (!platformSource->hasCallbackAPI())
QSKIP("Callback API not supported by this backend");
QSemaphore sync;
audioSource.start([&](QSpan<const float> outputBuffer) {
QCOMPARE_GT(outputBuffer.size(), 0);
sync.release();
});
QCOMPARE(audioSource.error(), QAudio::Error::NoError);
bool callbackExecuted = sync.try_acquire_for(1s);
QVERIFY(callbackExecuted);
#endif
}
void tst_QAudioSource::callbackAPI_startFailsWithWrongType()
{
using namespace std::chrono_literals;
QAudioFormat format = audioDevice.preferredFormat();
format.setSampleFormat(QAudioFormat::SampleFormat::Float);
QAudioSource audioSource(audioDevice, format);
QPlatformAudioSource *platformSource = QPlatformAudioSource::get(audioSource);
if (!platformSource->hasCallbackAPI())
QSKIP("Callback API not supported by this backend");
audioSource.start([&](QSpan<const int32_t>) {
});
QCOMPARE(audioSource.error(), QAudio::Error::OpenError);
}
void tst_QAudioSource::callbackAPI_startWithMoveOnlyFunctor()
{
#if QT_CONFIG(thread)
using namespace std::chrono_literals;
QAudioFormat format = audioDevice.preferredFormat();
format.setSampleFormat(QAudioFormat::SampleFormat::Float);
QAudioSource audioSource(audioDevice, format);
QPlatformAudioSource *platformSource = QPlatformAudioSource::get(audioSource);
if (!platformSource->hasCallbackAPI())
QSKIP("Callback API not supported by this backend");
QSemaphore sync;
audioSource.start([&, dummy = std::make_unique<int>(1)](QSpan<const float> outputBuffer) {
QCOMPARE_GT(outputBuffer.size(), 0);
sync.release();
});
QCOMPARE(audioSource.error(), QAudio::Error::NoError);
bool callbackExecuted = sync.try_acquire_for(1s);
QVERIFY(callbackExecuted);
#endif
}
#ifdef Q_OS_MACOS
void tst_QAudioSource::coreaudio_resetAfterDeviceDisconnect_doesNotUseAfterFree()
{
using namespace std::chrono_literals;
// Same scenario as tst_QAudioSink::coreaudio_resetAfterDeviceDisconnect_doesNotUseAfterFree.
constexpr int cycles = 5;
for (int cycle = 0; cycle != cycles; ++cycle) {
const auto aggregate =
QCoreAudioAggregateDevice::create(QCoreAudioAggregateDevice::Direction::Input);
if (!aggregate)
QSKIP("Could not create a CoreAudio aggregate device");
const auto device = aggregate->waitForQtDevice(5s);
if (!device)
QSKIP("The CoreAudio aggregate device was not listed by QMediaDevices");
QAudioSource source{ *device, device->preferredFormat() };
source.start();
if (source.error() != QAudio::NoError)
QSKIP("Could not start recording from the aggregate device");
QTRY_COMPARE_GT(source.bytesAvailable(), 0);
// The disconnect handler is queued on the main thread and has not run yet
QVERIFY(aggregate->destroyAndWaitForDisconnectNotification(5s));
source.reset();
// Drain the queued handler while the source is still alive
QTest::qWait(100ms);
}
}
#endif
QTEST_MAIN(tst_QAudioSource)
#include "tst_qaudiosource.moc"