Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtMultimediaTestLib/private/mediabackendutils_p.h>
++ #include <QtMultimediaTestLib/private/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"