diff options
-rw-r--r-- | src/multimedia/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/multimedia/audio/qaudiostatemachine.cpp | 264 | ||||
-rw-r--r-- | src/multimedia/audio/qaudiostatemachine_p.h | 167 | ||||
-rw-r--r-- | src/multimedia/audio/qaudiosystem.cpp | 9 | ||||
-rw-r--r-- | src/multimedia/audio/qaudiosystem_p.h | 23 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosink.mm | 278 | ||||
-rw-r--r-- | src/multimedia/darwin/qdarwinaudiosink_p.h | 29 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink.cpp | 135 | ||||
-rw-r--r-- | src/multimedia/pulseaudio/qpulseaudiosink_p.h | 9 | ||||
-rw-r--r-- | tests/auto/integration/qaudiosink/BLACKLIST | 7 | ||||
-rw-r--r-- | tests/auto/integration/qaudiosink/tst_qaudiosink.cpp | 215 | ||||
-rw-r--r-- | tests/auto/unit/multimedia/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/unit/multimedia/qaudiostatemachine/CMakeLists.txt | 11 | ||||
-rw-r--r-- | tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp | 643 |
14 files changed, 1410 insertions, 382 deletions
diff --git a/src/multimedia/CMakeLists.txt b/src/multimedia/CMakeLists.txt index b66748d4e..0190c6dbe 100644 --- a/src/multimedia/CMakeLists.txt +++ b/src/multimedia/CMakeLists.txt @@ -29,6 +29,7 @@ qt_internal_add_module(Multimedia audio/qaudiosource.cpp audio/qaudiosource.h audio/qaudiosink.cpp audio/qaudiosink.h audio/qaudiosystem.cpp audio/qaudiosystem_p.h + audio/qaudiostatemachine.cpp audio/qaudiostatemachine_p.h audio/qsamplecache_p.cpp audio/qsamplecache_p.h audio/qsoundeffect.cpp audio/qsoundeffect.h audio/qwavedecoder.cpp audio/qwavedecoder.h diff --git a/src/multimedia/audio/qaudiostatemachine.cpp b/src/multimedia/audio/qaudiostatemachine.cpp new file mode 100644 index 000000000..4073abd48 --- /dev/null +++ b/src/multimedia/audio/qaudiostatemachine.cpp @@ -0,0 +1,264 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qaudiostatemachine_p.h" +#include "qaudiosystem_p.h" +#include <qpointer.h> +#include <qdebug.h> + +QT_BEGIN_NAMESPACE + +using Guard = QAudioStateMachine::StateChangeGuard; +using RawState = QAudioStateMachine::RawState; + +namespace { +constexpr RawState DrainingFlag = 1 << 16; +constexpr RawState InProgressFlag = 1 << 17; +constexpr RawState WaitingFlags = DrainingFlag | InProgressFlag; + +const auto IgnoreError = static_cast<QAudio::Error>(-1); + +inline bool isWaitingState(RawState state) +{ + return (state & WaitingFlags) != 0; +} + +inline bool isDrainingState(RawState state) +{ + return (state & DrainingFlag) != 0; +} + +inline RawState fromWaitingState(RawState state) +{ + return state & ~WaitingFlags; +} + +inline QAudio::State toAudioState(RawState state) +{ + return QAudio::State(fromWaitingState(state)); +} + +template<typename... States> +constexpr std::pair<RawState, uint32_t> makeStatesSet(QAudio::State first, States... others) +{ + return { first, ((1 << first) | ... | (1 << others)) }; +} + +// ensures compareExchange (testAndSet) operation with opportunity +// to check several states, can be considered as atomic +template<typename T, typename Predicate> +bool multipleCompareExchange(std::atomic<T> &target, T &prevValue, T newValue, Predicate predicate) +{ + Q_ASSERT(predicate(prevValue)); + do { + if (target.compare_exchange_strong(prevValue, newValue)) + return true; + } while (predicate(prevValue)); + + return false; +} + +} // namespace + +struct QAudioStateMachine::Synchronizer { + QMutex m_mutex; + QWaitCondition m_condition; + + template <typename Changer> + void changeState(Changer&& changer) { + { + QMutexLocker locker(&m_mutex); + changer(); + } + + m_condition.notify_all(); + } + + void waitForOperationFinished(std::atomic<RawState>& state) + { + QMutexLocker locker(&m_mutex); + while (isWaitingState(state)) + m_condition.wait(&m_mutex); + } + + void waitForDrained(std::atomic<RawState>& state, std::chrono::milliseconds timeout) { + QMutexLocker locker(&m_mutex); + if (isDrainingState(state)) + m_condition.wait(&m_mutex, timeout.count()); + } +}; + +QAudioStateMachine::QAudioStateMachine(QAudioStateChangeNotifier ¬ifier, bool synchronize) : + m_notifier(¬ifier), + m_sychronizer(synchronize ? std::make_unique<Synchronizer>() : nullptr) +{ +} + +QAudioStateMachine::~QAudioStateMachine() = default; + +QAudio::State QAudioStateMachine::state() const +{ + return toAudioState(m_state); +} + +QAudio::Error QAudioStateMachine::error() const +{ + return m_error; +} + +Guard QAudioStateMachine::changeState(std::pair<RawState, uint32_t> prevStatesSet, + RawState newState, QAudio::Error error, bool shouldDrain) +{ + auto checkState = [flags = prevStatesSet.second](RawState state) { + return (flags >> state) & 1; + }; + + if (!m_sychronizer) { + RawState prevState = prevStatesSet.first; + const auto exchanged = multipleCompareExchange( + m_state, prevState, newState, checkState); + + if (Q_LIKELY(exchanged)) + return { this, newState, prevState, error }; + + return {}; + } + + while (true) { + RawState prevState = prevStatesSet.first; + + const auto newWaitingState = newState | (shouldDrain ? WaitingFlags : InProgressFlag); + + const auto exchanged = multipleCompareExchange( + m_state, prevState, newWaitingState, [checkState](RawState state) { + return !isWaitingState(state) && checkState(state); + }); + + if (Q_LIKELY(exchanged)) + return { this, newState, prevState, error }; + + if (!isWaitingState(prevState)) + return {}; + + if (!checkState(fromWaitingState(prevState))) + return {}; + + m_sychronizer->waitForOperationFinished(m_state); + } +} + +Guard QAudioStateMachine::stop(QAudio::Error error, bool shouldDrain, bool forceUpdateError) +{ + auto result = changeState( + makeStatesSet(QAudio::ActiveState, QAudio::IdleState, QAudio::SuspendedState), + QAudio::StoppedState, error, shouldDrain); + + if (!result && forceUpdateError) + setError(error); + + return result; +} + +Guard QAudioStateMachine::start(bool active) +{ + return changeState(makeStatesSet(QAudio::StoppedState), + active ? QAudio::ActiveState : QAudio::IdleState); +} + +void QAudioStateMachine::waitForDrained(std::chrono::milliseconds timeout) +{ + if (m_sychronizer) + m_sychronizer->waitForDrained(m_state, timeout); +} + +void QAudioStateMachine::onDrained() +{ + if (m_sychronizer) + m_sychronizer->changeState([this]() { m_state &= ~DrainingFlag; }); +} + +bool QAudioStateMachine::isDraining() const +{ + return isDrainingState(m_state); +} + +bool QAudioStateMachine::isActiveOrIdle() const { + const auto state = this->state(); + return state == QAudio::ActiveState || state == QAudio::IdleState; +} + +std::pair<bool, bool> QAudioStateMachine::getDrainedAndStopped() const +{ + const RawState state = m_state; + return { !isDrainingState(state), toAudioState(state) == QAudio::StoppedState }; +} + +Guard QAudioStateMachine::suspend() +{ + // Due to the current documentation, we set QAudio::NoError. + // TBD: leave the previous error should be more reasonable (IgnoreError) + const auto error = QAudio::NoError; + auto result = changeState(makeStatesSet(QAudio::ActiveState, QAudio::IdleState), + QAudio::SuspendedState, error); + + if (result) + m_suspendedInState = result.prevState(); + + return result; +} + +Guard QAudioStateMachine::resume() +{ + // Due to the current documentation, we set QAudio::NoError. + // TBD: leave the previous error should be more reasonable (IgnoreError) + const auto error = QAudio::NoError; + return changeState(makeStatesSet(QAudio::SuspendedState), m_suspendedInState, error); +} + +Guard QAudioStateMachine::activateFromIdle() +{ + return changeState(makeStatesSet(QAudio::IdleState), QAudio::ActiveState); +} + +Guard QAudioStateMachine::updateActiveOrIdle(bool isActive, QAudio::Error error) +{ + const auto state = isActive ? QAudio::ActiveState : QAudio::IdleState; + return changeState(makeStatesSet(QAudio::ActiveState, QAudio::IdleState), state, error); +} + +void QAudioStateMachine::setError(QAudio::Error error) +{ + if (m_error.exchange(error) != error && m_notifier) + emit m_notifier->errorChanged(error); +} + +Guard QAudioStateMachine::forceSetState(QAudio::State state, QAudio::Error error) +{ + return changeState(makeStatesSet(QAudio::ActiveState, QAudio::IdleState, QAudio::SuspendedState, + QAudio::StoppedState), + state, error); +} + +void QAudioStateMachine::reset(RawState state, RawState prevState, QAudio::Error error) +{ + Q_ASSERT(!isWaitingState(state)); + + if (!m_sychronizer && m_state != state) + return; + + const auto isErrorChanged = error != IgnoreError && m_error.exchange(error) != error; + + if (m_sychronizer) + m_sychronizer->changeState([&](){ m_state = state; }); + + auto notifier = m_notifier; + + if (prevState != state && notifier) + emit notifier->stateChanged(toAudioState(state)); + + // check the notifier in case the object was deleted in + if (isErrorChanged && notifier) + emit notifier->errorChanged(error); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/qaudiostatemachine_p.h b/src/multimedia/audio/qaudiostatemachine_p.h new file mode 100644 index 000000000..44bef3ab0 --- /dev/null +++ b/src/multimedia/audio/qaudiostatemachine_p.h @@ -0,0 +1,167 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QAUDIOSTATEMACHINE_P_H +#define QAUDIOSTATEMACHINE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtMultimedia/qaudio.h> + +#include <qmutex.h> +#include <qwaitcondition.h> +#include <qpointer.h> +#include <atomic> +#include <chrono> + +QT_BEGIN_NAMESPACE + +class QAudioStateChangeNotifier; + +/* QAudioStateMachine provides an opportunity to + * toggle QAudio::State with QAudio::Error in + * a thread-safe manner. + * The toggling functions return a guard, + * which scope guaranties thread safety (if synchronization enabled) + * and notifies about the change via + * QAudioStateChangeNotifier::stateChanged and errorChanged. + * + * The state machine is supposed to be used mostly in + * QAudioSink and QAudioSource implementations. + */ +class Q_MULTIMEDIA_EXPORT QAudioStateMachine +{ +public: + using RawState = int; + class StateChangeGuard + { + public: + void reset() + { + if (auto stateMachine = std::exchange(m_stateMachine, nullptr)) + stateMachine->reset(m_state, m_prevState, m_error); + } + + ~StateChangeGuard() { reset(); } + + StateChangeGuard(const StateChangeGuard &) = delete; + StateChangeGuard(StateChangeGuard &&other) + : m_stateMachine(std::exchange(other.m_stateMachine, nullptr)), + m_state(other.m_state), + m_prevState(other.m_prevState), + m_error(other.m_error) + { + } + + operator bool() const { return m_stateMachine != nullptr; } + + void setError(QAudio::Error error) { m_error = error; } + + // Can be added make state changing more flexible + // but needs some investigation to ensure state change consistency + // The method is supposed to be used for sync read/write + // under "guard = updateActiveOrIdle(isActive)" + // void setState(QAudio::State state) { ... } + + bool isStateChanged() const { return m_state != m_prevState; } + + QAudio::State prevState() const { return QAudio::State(m_prevState); } + + private: + StateChangeGuard(QAudioStateMachine *stateMachine = nullptr, + RawState state = QAudio::StoppedState, + RawState prevState = QAudio::StoppedState, + QAudio::Error error = QAudio::NoError) + : m_stateMachine(stateMachine), m_state(state), m_prevState(prevState), m_error(error) + { + } + + private: + QAudioStateMachine *m_stateMachine; + RawState m_state; + const RawState m_prevState; + QAudio::Error m_error; + + friend class QAudioStateMachine; + }; + + QAudioStateMachine(QAudioStateChangeNotifier ¬ifier, bool synchronize = true); + + ~QAudioStateMachine(); + + QAudio::State state() const; + + QAudio::Error error() const; + + bool isDraining() const; + + bool isActiveOrIdle() const; + + // atomicaly checks if the state is stopped and marked as drained + std::pair<bool, bool> getDrainedAndStopped() const; + + // waits if the method stop(error, true) has bee called + void waitForDrained(std::chrono::milliseconds timeout); + + // mark as drained and wake up the method waitForDrained + void onDrained(); + + // Active/Idle/Suspended -> Stopped + StateChangeGuard stop(QAudio::Error error = QAudio::NoError, bool shouldDrain = false, + bool forceUpdateError = false); + + // Active/Idle/Suspended -> Stopped + StateChangeGuard stopOrUpdateError(QAudio::Error error = QAudio::NoError) + { + return stop(error, false, true); + } + + // Stopped -> Active/Idle + StateChangeGuard start(bool isActive = true); + + // Active/Idle -> Suspended + saves the exchanged state + StateChangeGuard suspend(); + + // Suspended -> saved state (Active/Idle) + StateChangeGuard resume(); + + // Idle -> Active + StateChangeGuard activateFromIdle(); + + // Active/Idle -> Active/Idle + updateError + StateChangeGuard updateActiveOrIdle(bool isActive, QAudio::Error error = QAudio::NoError); + + // Any -> Any; better use more strict methods + StateChangeGuard forceSetState(QAudio::State state, QAudio::Error error = QAudio::NoError); + + // force set the error + void setError(QAudio::Error error); + +private: + StateChangeGuard changeState(std::pair<RawState, uint32_t> prevStatesSet, RawState state, + QAudio::Error error = QAudio::NoError, bool shouldDrain = false); + + void reset(RawState state, RawState prevState, QAudio::Error error); + +private: + QPointer<QAudioStateChangeNotifier> m_notifier; + std::atomic<RawState> m_state = QAudio::StoppedState; + std::atomic<QAudio::Error> m_error = QAudio::NoError; + RawState m_suspendedInState = QAudio::SuspendedState; + + struct Synchronizer; + std::unique_ptr<Synchronizer> m_sychronizer; +}; + +QT_END_NAMESPACE + +#endif // QAUDIOSTATEMACHINE_P_H diff --git a/src/multimedia/audio/qaudiosystem.cpp b/src/multimedia/audio/qaudiosystem.cpp index b052b78a6..355771f6b 100644 --- a/src/multimedia/audio/qaudiosystem.cpp +++ b/src/multimedia/audio/qaudiosystem.cpp @@ -7,17 +7,16 @@ QT_BEGIN_NAMESPACE -QPlatformAudioSink::QPlatformAudioSink(QObject *parent) : QObject(parent) -{ - QPlatformMediaDevices::instance()->prepareAudio(); -} +QAudioStateChangeNotifier::QAudioStateChangeNotifier(QObject *parent) : QObject(parent) { } + +QPlatformAudioSink::QPlatformAudioSink(QObject *parent) : QAudioStateChangeNotifier(parent) { } qreal QPlatformAudioSink::volume() const { return 1.0; } -QPlatformAudioSource::QPlatformAudioSource(QObject *parent) : QObject(parent) { } +QPlatformAudioSource::QPlatformAudioSource(QObject *parent) : QAudioStateChangeNotifier(parent) { } QT_END_NAMESPACE diff --git a/src/multimedia/audio/qaudiosystem_p.h b/src/multimedia/audio/qaudiosystem_p.h index e85968b86..4a0650e80 100644 --- a/src/multimedia/audio/qaudiosystem_p.h +++ b/src/multimedia/audio/qaudiosystem_p.h @@ -28,7 +28,18 @@ QT_BEGIN_NAMESPACE class QIODevice; -class Q_MULTIMEDIA_EXPORT QPlatformAudioSink : public QObject +class Q_MULTIMEDIA_EXPORT QAudioStateChangeNotifier : public QObject +{ + Q_OBJECT +public: + QAudioStateChangeNotifier(QObject *parent = nullptr); + +signals: + void errorChanged(QAudio::Error error); + void stateChanged(QAudio::State state); +}; + +class Q_MULTIMEDIA_EXPORT QPlatformAudioSink : public QAudioStateChangeNotifier { Q_OBJECT @@ -52,13 +63,9 @@ public: virtual qreal volume() const; QElapsedTimer elapsedTime; - -Q_SIGNALS: - void errorChanged(QAudio::Error error); - void stateChanged(QAudio::State state); }; -class Q_MULTIMEDIA_EXPORT QPlatformAudioSource : public QObject +class Q_MULTIMEDIA_EXPORT QPlatformAudioSource : public QAudioStateChangeNotifier { Q_OBJECT @@ -82,10 +89,6 @@ public: virtual qreal volume() const = 0; QElapsedTimer elapsedTime; - -Q_SIGNALS: - void errorChanged(QAudio::Error error); - void stateChanged(QAudio::State state); }; QT_END_NAMESPACE diff --git a/src/multimedia/darwin/qdarwinaudiosink.mm b/src/multimedia/darwin/qdarwinaudiosink.mm index 38563a9e7..d612c78df 100644 --- a/src/multimedia/darwin/qdarwinaudiosink.mm +++ b/src/multimedia/darwin/qdarwinaudiosink.mm @@ -23,23 +23,30 @@ QT_BEGIN_NAMESPACE -QDarwinAudioSinkBuffer::QDarwinAudioSinkBuffer(int bufferSize, int maxPeriodSize, const QAudioFormat &audioFormat) - : m_deviceError(false) - , m_maxPeriodSize(maxPeriodSize) - , m_device(0) +static int audioRingBufferSize(int bufferSize, int maxPeriodSize) +{ + // TODO: review this code + return bufferSize + + (bufferSize % maxPeriodSize == 0 ? 0 : maxPeriodSize - (bufferSize % maxPeriodSize)); +} + +QDarwinAudioSinkBuffer::QDarwinAudioSinkBuffer(int bufferSize, int maxPeriodSize, + const QAudioFormat &audioFormat) + : m_deviceError(false), + m_maxPeriodSize(maxPeriodSize), + m_device(nullptr), + m_buffer( + std::make_unique<CoreAudioRingBuffer>(audioRingBufferSize(bufferSize, maxPeriodSize))) { - m_buffer = new CoreAudioRingBuffer(bufferSize + (bufferSize % maxPeriodSize == 0 ? 0 : maxPeriodSize - (bufferSize % maxPeriodSize))); m_bytesPerFrame = audioFormat.bytesPerFrame(); m_periodTime = m_maxPeriodSize / m_bytesPerFrame * 1000 / audioFormat.sampleRate(); m_fillTimer = new QTimer(this); + m_fillTimer->setTimerType(Qt::PreciseTimer); connect(m_fillTimer, SIGNAL(timeout()), SLOT(fillBuffer())); } -QDarwinAudioSinkBuffer::~QDarwinAudioSinkBuffer() -{ - delete m_buffer; -} +QDarwinAudioSinkBuffer::~QDarwinAudioSinkBuffer() = default; qint64 QDarwinAudioSinkBuffer::readFrames(char *data, qint64 maxFrames) { @@ -110,16 +117,18 @@ void QDarwinAudioSinkBuffer::reset() void QDarwinAudioSinkBuffer::setPrefetchDevice(QIODevice *device) { - if (m_device != device) { - m_device = device; - if (m_device != 0) - fillBuffer(); - } + if (std::exchange(m_device, device) != device && device) + fillBuffer(); +} + +QIODevice *QDarwinAudioSinkBuffer::prefetchDevice() const +{ + return m_device; } void QDarwinAudioSinkBuffer::startFillTimer() { - if (m_device != 0) + if (m_device) m_fillTimer->start(m_buffer->size() / 2 / m_maxPeriodSize * m_periodTime); } @@ -184,7 +193,7 @@ qint64 QDarwinAudioSinkDevice::writeData(const char *data, qint64 len) } QDarwinAudioSink::QDarwinAudioSink(const QAudioDevice &device, QObject *parent) - : QPlatformAudioSink(parent), m_audioDeviceInfo(device) + : QPlatformAudioSink(parent), m_audioDeviceInfo(device), m_stateMachine(*this) { QAudioDevice di = device; if (di.isNull()) @@ -197,7 +206,6 @@ QDarwinAudioSink::QDarwinAudioSink(const QAudioDevice &device, QObject *parent) m_device = di.id(); m_clockFrequency = CoreAudioUtils::frequency() / 1000; - m_audioThreadState.storeRelaxed(Stopped); } QDarwinAudioSink::~QDarwinAudioSink() @@ -207,107 +215,83 @@ QDarwinAudioSink::~QDarwinAudioSink() void QDarwinAudioSink::start(QIODevice *device) { - QIODevice* op = device; + reset(); if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { - m_stateCode = QAudio::StoppedState; - m_errorCode = QAudio::OpenError; + m_stateMachine.setError(QAudio::OpenError); return; } - reset(); - m_audioBuffer->reset(); - m_audioBuffer->setPrefetchDevice(op); - - if (op == 0) { - op = m_audioIO; - m_stateCode = QAudio::IdleState; + if (!device) { + m_stateMachine.setError(QAudio::IOError); + return; } - else - m_stateCode = QAudio::ActiveState; - // Start + auto guard = m_stateMachine.start(); + Q_ASSERT(guard); + + m_audioBuffer->reset(); + m_audioBuffer->setPrefetchDevice(device); + m_pullMode = true; - m_errorCode = QAudio::NoError; m_totalFrames = 0; - if (m_stateCode == QAudio::ActiveState) - audioThreadStart(); - - emit stateChanged(m_stateCode); + audioThreadStart(); } QIODevice *QDarwinAudioSink::start() { + reset(); + if (!m_audioDeviceInfo.isFormatSupported(m_audioFormat) || !open()) { - m_stateCode = QAudio::StoppedState; - m_errorCode = QAudio::OpenError; + m_stateMachine.setError(QAudio::OpenError); return m_audioIO; } - reset(); - m_audioBuffer->reset(); - m_audioBuffer->setPrefetchDevice(0); + auto guard = m_stateMachine.start(false); + Q_ASSERT(guard); - m_stateCode = QAudio::IdleState; + m_audioBuffer->reset(); + m_audioBuffer->setPrefetchDevice(nullptr); - // Start m_pullMode = false; - m_errorCode = QAudio::NoError; m_totalFrames = 0; - emit stateChanged(m_stateCode); - return m_audioIO; } void QDarwinAudioSink::stop() { - if (m_stateCode == QAudio::StoppedState) - return; + if (auto guard = m_stateMachine.stop(QAudio::NoError, true)) { + stopTimers(); + + if (guard.prevState() == QAudio::ActiveState) { + m_stateMachine.waitForDrained(std::chrono::milliseconds(500)); - audioThreadDrain(); + if (m_stateMachine.isDraining()) + qWarning() << "Failed wait for sink drained"; - m_stateCode = QAudio::StoppedState; - m_errorCode = QAudio::NoError; - emit stateChanged(m_stateCode); + audioDeviceStop(); + } + } } void QDarwinAudioSink::reset() { - if (m_stateCode == QAudio::StoppedState) - return; - - audioThreadStop(); - - m_stateCode = QAudio::StoppedState; - m_errorCode = QAudio::NoError; - emit stateChanged(m_stateCode); + if (auto guard = m_stateMachine.stopOrUpdateError()) + audioThreadStop(guard.prevState()); } void QDarwinAudioSink::suspend() { - if (m_stateCode != QAudio::ActiveState && m_stateCode != QAudio::IdleState) - return; - - audioThreadStop(); - - m_suspendedInStateCode = m_stateCode; - m_stateCode = QAudio::SuspendedState; - m_errorCode = QAudio::NoError; - emit stateChanged(m_stateCode); + if (auto guard = m_stateMachine.suspend()) + audioThreadStop(guard.prevState()); } void QDarwinAudioSink::resume() { - if (m_stateCode != QAudio::SuspendedState) - return; - - audioThreadStart(); - - m_stateCode = m_suspendedInStateCode; - m_errorCode = QAudio::NoError; - emit stateChanged(m_stateCode); + if (auto guard = m_stateMachine.resume()) + audioThreadStart(); } qsizetype QDarwinAudioSink::bytesFree() const @@ -317,7 +301,7 @@ qsizetype QDarwinAudioSink::bytesFree() const void QDarwinAudioSink::setBufferSize(qsizetype value) { - if (m_stateCode == QAudio::StoppedState) + if (state() == QAudio::StoppedState) m_internalBufferSize = value; } @@ -333,17 +317,17 @@ qint64 QDarwinAudioSink::processedUSecs() const QAudio::Error QDarwinAudioSink::error() const { - return m_errorCode; + return m_stateMachine.error(); } QAudio::State QDarwinAudioSink::state() const { - return m_stateCode; + return m_stateMachine.state(); } void QDarwinAudioSink::setFormat(const QAudioFormat &format) { - if (m_stateCode == QAudio::StoppedState) + if (state() == QAudio::StoppedState) m_audioFormat = format; } @@ -375,22 +359,10 @@ qreal QDarwinAudioSink::volume() const return m_cachedVolume; } -void QDarwinAudioSink::deviceStopped() -{ - emit stateChanged(m_stateCode); -} - void QDarwinAudioSink::inputReady() { - if (m_stateCode != QAudio::IdleState) - return; - - audioThreadStart(); - - m_stateCode = QAudio::ActiveState; - m_errorCode = QAudio::NoError; - - emit stateChanged(m_stateCode); + if (auto guard = m_stateMachine.activateFromIdle()) + audioDeviceStart(); } OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) @@ -402,15 +374,15 @@ OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionF QDarwinAudioSink* d = static_cast<QDarwinAudioSink*>(inRefCon); - const int threadState = d->m_audioThreadState.fetchAndAddAcquire(0); - if (threadState == Stopped) { + const auto [drained, stopped] = d->m_stateMachine.getDrainedAndStopped(); + + if (drained && stopped) { ioData->mBuffers[0].mDataByteSize = 0; - d->audioDeviceStop(); - } - else { + } else { const UInt32 bytesPerFrame = d->m_streamFormat.mBytesPerFrame; qint64 framesRead; + Q_ASSERT(ioData->mBuffers[0].mDataByteSize / bytesPerFrame == inNumberFrames); framesRead = d->m_audioBuffer->readFrames((char*)ioData->mBuffers[0].mData, ioData->mBuffers[0].mDataByteSize / bytesPerFrame); @@ -420,7 +392,7 @@ OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionF #if defined(Q_OS_MACOS) // If playback is already stopped. - if (threadState != Running) { + if (!drained) { qreal oldVolume = d->m_cachedVolume; // Decrease volume smoothly. d->setVolume(d->m_volume / 2); @@ -441,13 +413,13 @@ OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionF else { ioData->mBuffers[0].mDataByteSize = 0; if (framesRead == 0) { - if (threadState == Draining) - d->audioDeviceStop(); + if (!drained) + d->onAudioDeviceDrained(); else - d->audioDeviceIdle(); + d->onAudioDeviceIdle(); } else - d->audioDeviceError(); + d->onAudioDeviceError(); } } @@ -463,7 +435,7 @@ bool QDarwinAudioSink::open() CoreAudioSessionManager::instance().setActive(true); #endif - if (m_errorCode != QAudio::NoError) + if (error() != QAudio::NoError) return false; if (m_isOpen) { @@ -562,10 +534,11 @@ bool QDarwinAudioSink::open() else m_internalBufferSize -= m_internalBufferSize % m_streamFormat.mBytesPerFrame; - m_audioBuffer = new QDarwinAudioSinkBuffer(m_internalBufferSize, m_periodSizeBytes, m_audioFormat); - connect(m_audioBuffer, SIGNAL(readyRead()), SLOT(inputReady())); //Pull + m_audioBuffer = std::make_unique<QDarwinAudioSinkBuffer>(m_internalBufferSize, + m_periodSizeBytes, m_audioFormat); + connect(m_audioBuffer.get(), SIGNAL(readyRead()), SLOT(inputReady())); // Pull - m_audioIO = new QDarwinAudioSinkDevice(m_audioBuffer, this); + m_audioIO = new QDarwinAudioSinkDevice(m_audioBuffer.get(), this); //Init if (AudioUnitInitialize(m_audioUnit)) { @@ -583,98 +556,63 @@ bool QDarwinAudioSink::open() void QDarwinAudioSink::close() { if (m_audioUnit != 0) { - audioDeviceStop(); + if (auto guard = m_stateMachine.stop()) { + audioThreadStop(guard.prevState()); + QSignalBlocker blocker(this); + guard.reset(); + } AudioUnitUninitialize(m_audioUnit); AudioComponentInstanceDispose(m_audioUnit); } - delete m_audioBuffer; + m_audioBuffer.reset(); } void QDarwinAudioSink::audioThreadStart() { startTimers(); - audioDeviceStart(); + if (m_stateMachine.state() == QAudio::ActiveState) + audioDeviceStart(); } -void QDarwinAudioSink::audioThreadStop() +void QDarwinAudioSink::audioThreadStop(QAudio::State prevState) { stopTimers(); - - // It's common practice to call AudioOutputUnitStop - // from the thread where the audio output was started, - // so we don't have to rely on the stops inside renderCallback. - audioDeviceStop(); + if (prevState == QAudio::ActiveState) + audioDeviceStop(); } -void QDarwinAudioSink::audioThreadDrain() +void QDarwinAudioSink::audioDeviceStart() { - stopTimers(); - - QMutexLocker lock(&m_mutex); - - if (m_audioThreadState.testAndSetAcquire(Running, Draining)) { - constexpr int MaxDrainWaitingTime = 500; - - m_threadFinished.wait(&m_mutex, MaxDrainWaitingTime); - - if (m_audioThreadState.fetchAndStoreRelaxed(Stopped) != Stopped) { - qWarning() << "Couldn't wait for draining; force stop"; - - AudioOutputUnitStop(m_audioUnit); - } - } + AudioOutputUnitStart(m_audioUnit); } -void QDarwinAudioSink::audioDeviceStart() +void QDarwinAudioSink::audioDeviceStop() { - QMutexLocker lock(&m_mutex); - - const auto state = m_audioThreadState.loadAcquire(); - if (state == Stopped) { - m_audioThreadState.storeRelaxed(Running); - AudioOutputUnitStart(m_audioUnit); - } else { - qWarning() << "Unexpected state on audio device start:" << state; - } + AudioOutputUnitStop(m_audioUnit); } -void QDarwinAudioSink::audioDeviceStop() +void QDarwinAudioSink::onAudioDeviceIdle() { - { - QMutexLocker lock(&m_mutex); + if (auto guard = m_stateMachine.updateActiveOrIdle(false)) { + auto device = m_audioBuffer ? m_audioBuffer->prefetchDevice() : nullptr; + const bool atEnd = device && device->atEnd(); + guard.setError(atEnd ? QAudio::NoError : QAudio::UnderrunError); - AudioOutputUnitStop(m_audioUnit); - m_audioThreadState.storeRelaxed(Stopped); + if (guard.prevState() == QAudio::ActiveState) + audioDeviceStop(); } - - m_threadFinished.wakeOne(); } -void QDarwinAudioSink::audioDeviceIdle() +void QDarwinAudioSink::onAudioDeviceError() { - if (m_stateCode != QAudio::ActiveState) - return; - - m_errorCode = QAudio::UnderrunError; - m_stateCode = QAudio::IdleState; - - audioDeviceStop(); - - QMetaObject::invokeMethod(this, "deviceStopped", Qt::QueuedConnection); + if (auto guard = m_stateMachine.stop(QAudio::IOError)) + audioThreadStop(guard.prevState()); } -void QDarwinAudioSink::audioDeviceError() +void QDarwinAudioSink::onAudioDeviceDrained() { - if (m_stateCode != QAudio::ActiveState) - return; - - m_errorCode = QAudio::IOError; - m_stateCode = QAudio::StoppedState; - - audioDeviceStop(); - - QMetaObject::invokeMethod(this, "deviceStopped", Qt::QueuedConnection); + m_stateMachine.onDrained(); } void QDarwinAudioSink::startTimers() diff --git a/src/multimedia/darwin/qdarwinaudiosink_p.h b/src/multimedia/darwin/qdarwinaudiosink_p.h index 998f86e28..fedafd8af 100644 --- a/src/multimedia/darwin/qdarwinaudiosink_p.h +++ b/src/multimedia/darwin/qdarwinaudiosink_p.h @@ -15,6 +15,7 @@ // #include <private/qaudiosystem_p.h> +#include <private/qaudiostatemachine_p.h> #if defined(Q_OS_MACOS) # include <CoreAudio/CoreAudio.h> @@ -50,6 +51,8 @@ public: void setPrefetchDevice(QIODevice *device); + QIODevice *prefetchDevice() const; + void startFillTimer(); void stopFillTimer(); @@ -66,7 +69,7 @@ private: int m_periodTime; QIODevice *m_device; QTimer *m_fillTimer; - CoreAudioRingBuffer *m_buffer; + std::unique_ptr<CoreAudioRingBuffer> m_buffer; }; class QDarwinAudioSinkDevice : public QIODevice @@ -111,15 +114,10 @@ public: qreal volume() const; private slots: - void deviceStopped(); void inputReady(); private: - enum { - Running, - Draining, - Stopped - }; + enum ThreadState { Running, Draining, Stopped }; static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, @@ -131,12 +129,12 @@ private: bool open(); void close(); void audioThreadStart(); - void audioThreadStop(); - void audioThreadDrain(); + void audioThreadStop(QAudio::State prevState = QAudio::ActiveState); void audioDeviceStart(); void audioDeviceStop(); - void audioDeviceIdle(); - void audioDeviceError(); + void onAudioDeviceIdle(); + void onAudioDeviceError(); + void onAudioDeviceDrained(); void startTimers(); void stopTimers(); @@ -158,19 +156,14 @@ private: AudioUnit m_audioUnit = 0; Float64 m_clockFrequency = 0; AudioStreamBasicDescription m_streamFormat; - QDarwinAudioSinkBuffer *m_audioBuffer = nullptr; - QAtomicInt m_audioThreadState; - QWaitCondition m_threadFinished; - QMutex m_mutex; + std::unique_ptr<QDarwinAudioSinkBuffer> m_audioBuffer; qreal m_cachedVolume = 1.; #if defined(Q_OS_MACOS) qreal m_volume = 1.; #endif bool m_pullMode = false; - QAudio::Error m_errorCode = QAudio::NoError; - QAudio::State m_stateCode = QAudio::StoppedState; - QAudio::State m_suspendedInStateCode = QAudio::SuspendedState; + QAudioStateMachine m_stateMachine; }; QT_END_NAMESPACE diff --git a/src/multimedia/pulseaudio/qpulseaudiosink.cpp b/src/multimedia/pulseaudio/qpulseaudiosink.cpp index 132b9fbc1..bed3714a0 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosink.cpp +++ b/src/multimedia/pulseaudio/qpulseaudiosink.cpp @@ -12,11 +12,11 @@ #include "qpulsehelpers_p.h" #include <sys/types.h> #include <unistd.h> -#include <mutex> // for st::lock_guard +#include <mutex> // for std::lock_guard QT_BEGIN_NAMESPACE -const int SinkPeriodTimeMs = 20; +static constexpr int SinkPeriodTimeMs = 20; #define LOW_LATENCY_CATEGORY_NAME "game" @@ -111,42 +111,29 @@ static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void * qCDebug(qLcPulseAudioOut) << "Prebuffer adjusted:" << bool(success); } - QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent) - : QPlatformAudioSink(parent), - m_device(device) + : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this, false) { } QPulseAudioSink::~QPulseAudioSink() { - close(); + if (auto guard = m_stateMachine.stop()) { + close(); + QSignalBlocker blocker(this); + guard.reset(); + } QCoreApplication::processEvents(); } QAudio::Error QPulseAudioSink::error() const { - return m_errorState; -} - -void QPulseAudioSink::setStateAndError(QAudio::State state, QAudio::Error error, - bool forceEmitState) -{ - // TODO: reimplement with atomic state changings (compare_exchange) - - const auto isStateChanged = m_deviceState.exchange(state) != state; - const auto isErrorChanged = m_errorState.exchange(error) != error; - - if (isStateChanged || forceEmitState) - emit stateChanged(state); - - if (isErrorChanged) - emit errorChanged(error); + return m_stateMachine.error(); } QAudio::State QPulseAudioSink::state() const { - return m_deviceState; + return m_stateMachine.state(); } void QPulseAudioSink::streamUnderflowCallback() @@ -155,8 +142,8 @@ void QPulseAudioSink::streamUnderflowCallback() qCDebug(qLcPulseAudioOut) << "Draining stream at end of buffer"; exchangeDrainOperation(pa_stream_drain(m_stream, outputStreamDrainComplete, this)); - } else if (m_deviceState != QAudio::IdleState && !m_resuming) { - setStateAndError(QAudio::IdleState, QAudio::UnderrunError); + } else if (!m_resuming) { + m_stateMachine.updateActiveOrIdle(false, QAudio::UnderrunError); } } @@ -165,14 +152,12 @@ void QPulseAudioSink::streamDrainedCallback() if (!exchangeDrainOperation(nullptr)) return; - setStateAndError(QAudio::IdleState, QAudio::NoError); + m_stateMachine.updateActiveOrIdle(false); } void QPulseAudioSink::start(QIODevice *device) { - setStateAndError(QAudio::StoppedState, QAudio::NoError); - - close(); + reset(); m_pullMode = true; m_audioSource = device; @@ -182,12 +167,14 @@ void QPulseAudioSink::start(QIODevice *device) return; } + auto guard = m_stateMachine.start(); + Q_ASSERT(guard); + // ensure we only process timing infos that are up to date gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startReading); - setStateAndError(QAudio::ActiveState, QAudio::NoError); } void QPulseAudioSink::startReading() @@ -198,15 +185,16 @@ void QPulseAudioSink::startReading() QIODevice *QPulseAudioSink::start() { - setStateAndError(QAudio::StoppedState, QAudio::NoError); - - close(); + reset(); m_pullMode = false; if (!open()) return nullptr; + auto guard = m_stateMachine.start(false); + Q_ASSERT(guard); + m_audioSource = new PulseOutputPrivate(this); m_audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered); @@ -214,8 +202,6 @@ QIODevice *QPulseAudioSink::start() gettimeofday(&lastTimingInfo, nullptr); lastProcessedUSecs = 0; - setStateAndError(QAudio::IdleState, QAudio::NoError); - return m_audioSource; } @@ -227,7 +213,7 @@ bool QPulseAudioSink::open() QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { - setStateAndError(QAudio::StoppedState, QAudio::FatalError, true); + m_stateMachine.stopOrUpdateError(QAudio::FatalError); return false; } @@ -236,7 +222,7 @@ bool QPulseAudioSink::open() Q_ASSERT(spec.channels == channel_map.channels); if (!pa_sample_spec_valid(&spec)) { - setStateAndError(QAudio::StoppedState, QAudio::OpenError, true); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -285,7 +271,7 @@ bool QPulseAudioSink::open() qCWarning(qLcPulseAudioOut) << "QAudioSink: pa_stream_new_with_proplist() failed!"; pulseEngine->unlock(); - setStateAndError(QAudio::StoppedState, QAudio::OpenError, true); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -309,7 +295,7 @@ bool QPulseAudioSink::open() pa_stream_unref(m_stream); m_stream = nullptr; pulseEngine->unlock(); - setStateAndError(QAudio::StoppedState, QAudio::OpenError, true); + m_stateMachine.stopOrUpdateError(QAudio::OpenError); return false; } @@ -407,49 +393,52 @@ void QPulseAudioSink::timerEvent(QTimerEvent *event) void QPulseAudioSink::userFeed() { - const auto state = this->state(); - if (state == QAudio::StoppedState || state == QAudio::SuspendedState) + if (!m_stateMachine.isActiveOrIdle()) return; m_resuming = false; if (m_pullMode) { - setStateAndError(QAudio::ActiveState, QAudio::NoError); int writableSize = bytesFree(); int chunks = writableSize / m_periodSize; - if (chunks == 0) + if (chunks == 0) { + m_stateMachine.activateFromIdle(); return; + } - const int input = std::min( - m_periodSize, - static_cast<int>(m_audioBuffer.size())); // always request 1 chunk of data from user + const int input = std::min(m_periodSize, static_cast<int>(m_audioBuffer.size())); Q_ASSERT(!m_audioBuffer.empty()); int audioBytesPulled = m_audioSource->read(m_audioBuffer.data(), input); Q_ASSERT(audioBytesPulled <= input); if (audioBytesPulled > 0) { if (audioBytesPulled > input) { - qCWarning(qLcPulseAudioOut) << "Invalid audio data size provided by pull source:" - << audioBytesPulled << "should be less than" << input; + qCWarning(qLcPulseAudioOut) + << "Invalid audio data size provided by pull source:" << audioBytesPulled + << "should be less than" << input; audioBytesPulled = input; } - qint64 bytesWritten = write(m_audioBuffer.data(), audioBytesPulled); - Q_ASSERT(bytesWritten == audioBytesPulled); //unfinished write should not happen since the data provided is less than writableSize - Q_UNUSED(bytesWritten); + auto bytesWritten = write(m_audioBuffer.data(), audioBytesPulled); + if (bytesWritten != audioBytesPulled) + qWarning() << "Unfinished write should not happen since the data provided is " + "less than writableSize:" + << bytesWritten << "vs" << audioBytesPulled; if (chunks > 1) { // PulseAudio needs more data. Ask for it immediately. - QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &QPulseAudioSink::userFeed, Qt::QueuedConnection); } } else if (audioBytesPulled == 0) { m_tickTimer.stop(); const auto atEnd = m_audioSource->atEnd(); qCDebug(qLcPulseAudioOut) << "No more data available, source is done:" << atEnd; - setStateAndError(QAudio::IdleState, atEnd ? QAudio::NoError : QAudio::UnderrunError); + + m_stateMachine.updateActiveOrIdle(false, + atEnd ? QAudio::NoError : QAudio::UnderrunError); } } else { - if (state == QAudio::IdleState) - setStateAndError(state, QAudio::UnderrunError); + if (state() == QAudio::IdleState) + m_stateMachine.setError(QAudio::UnderrunError); } } @@ -466,7 +455,7 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) pulseEngine->unlock(); qCWarning(qLcPulseAudioOut) << "pa_stream_begin_write error:" << pa_strerror(pa_context_errno(pulseEngine->context())); - setStateAndError(state(), QAudio::IOError); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } @@ -486,32 +475,26 @@ qint64 QPulseAudioSink::write(const char *data, qint64 len) pulseEngine->unlock(); qCWarning(qLcPulseAudioOut) << "pa_stream_write error:" << pa_strerror(pa_context_errno(pulseEngine->context())); - setStateAndError(state(), QAudio::IOError); + m_stateMachine.updateActiveOrIdle(false, QAudio::IOError); return 0; } pulseEngine->unlock(); m_totalTimeValue += len; - setStateAndError(QAudio::ActiveState, QAudio::NoError); - + m_stateMachine.updateActiveOrIdle(true); return len; } void QPulseAudioSink::stop() { - if (m_deviceState == QAudio::StoppedState) - return; - - close(); - - setStateAndError(QAudio::StoppedState, QAudio::NoError); + if (auto guard = m_stateMachine.stop()) + close(); } qsizetype QPulseAudioSink::bytesFree() const { - const auto state = this->state(); - if (state != QAudio::ActiveState && state != QAudio::IdleState) + if (!m_stateMachine.isActiveOrIdle()) return 0; std::lock_guard lock(*QPulseAudioEngine::instance()); @@ -597,7 +580,7 @@ qint64 QPulseAudioSink::processedUSecs() const void QPulseAudioSink::resume() { - if (m_deviceState == QAudio::SuspendedState) { + if (auto guard = m_stateMachine.resume()) { m_resuming = true; { @@ -614,8 +597,6 @@ void QPulseAudioSink::resume() } m_tickTimer.start(m_periodTime, this); - - setStateAndError(m_suspendedInState, QAudio::NoError); } } @@ -631,11 +612,7 @@ QAudioFormat QPulseAudioSink::format() const void QPulseAudioSink::suspend() { - const auto state = this->state(); - if (state == QAudio::ActiveState || state == QAudio::IdleState) { - m_suspendedInState = state; - setStateAndError(QAudio::SuspendedState, QAudio::NoError); - + if (auto guard = m_stateMachine.suspend()) { m_tickTimer.stop(); QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); @@ -650,7 +627,8 @@ void QPulseAudioSink::suspend() void QPulseAudioSink::reset() { - stop(); + if (auto guard = m_stateMachine.stopOrUpdateError()) + close(); } PulseOutputPrivate::PulseOutputPrivate(QPulseAudioSink *audio) @@ -698,9 +676,8 @@ qreal QPulseAudioSink::volume() const void QPulseAudioSink::onPulseContextFailed() { - close(); - - setStateAndError(QAudio::StoppedState, QAudio::FatalError); + if (auto guard = m_stateMachine.stop(QAudio::FatalError)) + close(); } PAOperationUPtr QPulseAudioSink::exchangeDrainOperation(pa_operation *newOperation) diff --git a/src/multimedia/pulseaudio/qpulseaudiosink_p.h b/src/multimedia/pulseaudio/qpulseaudiosink_p.h index 45ca4fd94..4b9a551d7 100644 --- a/src/multimedia/pulseaudio/qpulseaudiosink_p.h +++ b/src/multimedia/pulseaudio/qpulseaudiosink_p.h @@ -27,9 +27,8 @@ #include "pulseaudio/qpulsehelpers_p.h" #include <private/qaudiosystem_p.h> - +#include <private/qaudiostatemachine_p.h> #include <pulse/pulseaudio.h> -#include <atomic> QT_BEGIN_NAMESPACE @@ -67,7 +66,6 @@ protected: void timerEvent(QTimerEvent *event) override; private: - void setStateAndError(QAudio::State state, QAudio::Error error, bool forceEmitState = false); void startReading(); bool open(); @@ -102,9 +100,6 @@ private: mutable qint64 lastProcessedUSecs = 0; qreal m_volume = 1.0; - std::atomic<QAudio::Error> m_errorState = QAudio::NoError; - std::atomic<QAudio::State> m_deviceState = QAudio::StoppedState; - QAudio::State m_suspendedInState = QAudio::SuspendedState; std::atomic<pa_operation *> m_drainOperation = nullptr; int m_periodSize = 0; int m_bufferSize = 0; @@ -112,6 +107,8 @@ private: bool m_pullMode = true; bool m_opened = false; bool m_resuming = false; + + QAudioStateMachine m_stateMachine; }; class PulseOutputPrivate : public QIODevice diff --git a/tests/auto/integration/qaudiosink/BLACKLIST b/tests/auto/integration/qaudiosink/BLACKLIST new file mode 100644 index 000000000..ec5962aff --- /dev/null +++ b/tests/auto/integration/qaudiosink/BLACKLIST @@ -0,0 +1,7 @@ +#QTBUG-113194 +[pullSuspendResume] +macos ci + +#QTBUG-113194 +[pushSuspendResume] +macos ci diff --git a/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp index 1eecb0d88..c7337f88d 100644 --- a/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp +++ b/tests/auto/integration/qaudiosink/tst_qaudiosink.cpp @@ -65,6 +65,13 @@ private: void createSineWaveData(const QAudioFormat &format, qint64 length, int sampleRate = 440); static QString dumpStateSignalSpy(const QSignalSpy &stateSignalSpy); + static qint64 wavDataSize(QIODevice &input); + + template<typename Checker> + static void pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst = false); + void generate_audiofile_testrows(); QAudioDevice audioDevice; @@ -146,6 +153,48 @@ QString tst_QAudioSink::dumpStateSignalSpy(const QSignalSpy& stateSignalSpy) { return result; } +qint64 tst_QAudioSink::wavDataSize(QIODevice &input) +{ + return input.size() - QWaveDecoder::headerLength(); +} + +template<typename Checker> +void tst_QAudioSink::pushDataToAudioSink(QAudioSink &sink, QIODevice &input, QIODevice &feed, + qint64 &allWritten, qint64 writtenLimit, Checker &&checker, + bool checkOnlyFirst) +{ + bool firstBuffer = true; + qint64 offset = 0; + QByteArray buffer; + + while ((allWritten < writtenLimit || writtenLimit < 0) && !input.atEnd() + && !QTest::currentTestFailed()) { + if (sink.bytesFree() > 0) { + if (buffer.isNull()) + buffer = input.read(sink.bytesFree()); + + const auto written = feed.write(buffer); + allWritten += written; + offset += written; + + if (offset >= buffer.size()) { + offset = 0; + buffer.clear(); + } + + if (!checkOnlyFirst || firstBuffer) + checker(); + + firstBuffer = false; + } else { + // wait a bit to ensure some the sink has consumed some data + // The delay getting might need some improvements + const auto delay = qMin(10, sink.format().durationForBytes(sink.bufferSize()) / 1000 / 2); + QTest::qWait(delay); + } + } +} + void tst_QAudioSink::generate_audiofile_testrows() { QTest::addColumn<FilePtr>("audioFile"); @@ -154,15 +203,11 @@ void tst_QAudioSink::generate_audiofile_testrows() for (int i=0; i<audioFiles.size(); i++) { QTest::newRow(QString("Audio File %1").arg(i).toUtf8().constData()) << audioFiles.at(i) << testFormats.at(i); - } } void tst_QAudioSink::initTestCase() { - if (qEnvironmentVariable("QTEST_ENVIRONMENT").toLower() == "ci") - QSKIP("SKIP on CI. To be fixed."); - // Only perform tests if audio output device exists const QList<QAudioDevice> devices = QMediaDevices::audioOutputs(); @@ -577,12 +622,11 @@ void tst_QAudioSink::pullResumeFromUnderrun() // Resume pull emit audioSource.readyRead(); - QTRY_COMPARE(stateSignal.size(), 1); - QCOMPARE(audioOutput.state(), QAudio::ActiveState); - QCOMPARE(audioOutput.error(), QAudio::NoError); - stateSignal.clear(); + QTRY_COMPARE(stateSignal.size(), 2); + QCOMPARE(stateSignal.at(0).front().value<QAudio::State>(), QAudio::ActiveState); + QCOMPARE(stateSignal.at(1).front().value<QAudio::State>(), QAudio::IdleState); - QTRY_COMPARE(stateSignal.size(), 1); + QCOMPARE(audioOutput.error(), QAudio::NoError); QCOMPARE(audioOutput.state(), QAudio::IdleState); // we played two chunks, sample rate is per second @@ -629,27 +673,23 @@ void tst_QAudioSink::push() QVERIFY2((audioOutput.processedUSecs() == qint64(0)), "processedUSecs() is not zero after start()"); qint64 written = 0; - bool firstBuffer = true; - while (written < audioFile->size() - QWaveDecoder::headerLength()) { - - if (audioOutput.bytesFree() > 0) { - auto buffer = audioFile->read(audioOutput.bytesFree()); - written += feed->write(buffer); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.size() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(dumpStateSignalSpy(stateSignal)).toUtf8().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - stateSignal.clear(); - } - } else - QTest::qWait(20); - } + auto checker = [&]() { + // Check for transition to ActiveState when data is provided + QVERIFY2((stateSignal.size() == 1), + QString("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + stateSignal.clear(); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile), checker, + true); // Wait until playback finishes QVERIFY2(audioFile->atEnd(), "didn't play to EOF"); @@ -710,28 +750,23 @@ void tst_QAudioSink::pushSuspendResume() 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; + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QString("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + qint64 written = 0; // Play half of the clip - while (written < (audioFile->size() - QWaveDecoder::headerLength()) / 2) { - - if (audioOutput.bytesFree() > 0) { - auto buffer = audioFile->read(audioOutput.bytesFree()); - written += feed->write(buffer); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.size() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(dumpStateSignalSpy(stateSignal)).toUtf8().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + stateSignal.clear(); const auto suspendedInState = audioOutput.state(); @@ -768,14 +803,14 @@ void tst_QAudioSink::pushSuspendResume() stateSignal.clear(); // Play rest of the clip - while (!audioFile->atEnd()) { - if (audioOutput.bytesFree() > 0) { - auto buffer = audioFile->read(audioOutput.bytesFree()); - written += feed->write(buffer); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after writing audio data"); - } else - QTest::qWait(20); - } + + auto restChecker = [&]() { + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after writing audio data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker); + QVERIFY(audioOutput.state() != QAudio::IdleState); stateSignal.clear(); @@ -880,28 +915,24 @@ void tst_QAudioSink::pushUnderrun() 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() - QWaveDecoder::headerLength()) / 2) { - - if (audioOutput.bytesFree() > 0) { - auto buffer = audioFile->read(audioOutput.bytesFree()); - written += feed->write(buffer); - - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.size() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(dumpStateSignalSpy(stateSignal)).toUtf8().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } + + auto firstHalfChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QString("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, wavDataSize(*audioFile) / 2, + firstHalfChecker, true); + stateSignal.clear(); // Wait for data to be played @@ -914,24 +945,20 @@ void tst_QAudioSink::pushUnderrun() 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() > 0) { - auto buffer = audioFile->read(audioOutput.bytesFree()); - written += feed->write(buffer); - if (firstBuffer) { - // Check for transition to ActiveState when data is provided - QVERIFY2((stateSignal.size() == 1), - QString("didn't emit signal after receiving data, got %1 signals instead") - .arg(dumpStateSignalSpy(stateSignal)).toUtf8().constData()); - QVERIFY2((audioOutput.state() == QAudio::ActiveState), "didn't transition to ActiveState after receiving data"); - QVERIFY2((audioOutput.error() == QAudio::NoError), "error state is not equal to QAudio::NoError after receiving data"); - firstBuffer = false; - } - } else - QTest::qWait(20); - } + auto restChecker = [&]() { + QVERIFY2((stateSignal.size() == 1), + QString("didn't emit signal after receiving data, got %1 signals instead") + .arg(dumpStateSignalSpy(stateSignal)) + .toUtf8() + .constData()); + QVERIFY2((audioOutput.state() == QAudio::ActiveState), + "didn't transition to ActiveState after receiving data"); + QVERIFY2((audioOutput.error() == QAudio::NoError), + "error state is not equal to QAudio::NoError after receiving data"); + }; + pushDataToAudioSink(audioOutput, *audioFile, *feed, written, -1, restChecker, true); + stateSignal.clear(); // Wait until playback finishes diff --git a/tests/auto/unit/multimedia/CMakeLists.txt b/tests/auto/unit/multimedia/CMakeLists.txt index d55cde39a..0aa1198a3 100644 --- a/tests/auto/unit/multimedia/CMakeLists.txt +++ b/tests/auto/unit/multimedia/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(qabstractvideobuffer) add_subdirectory(qaudiorecorder) add_subdirectory(qaudioformat) add_subdirectory(qaudionamespace) +add_subdirectory(qaudiostatemachine) add_subdirectory(qcamera) add_subdirectory(qcameradevice) add_subdirectory(qimagecapture) diff --git a/tests/auto/unit/multimedia/qaudiostatemachine/CMakeLists.txt b/tests/auto/unit/multimedia/qaudiostatemachine/CMakeLists.txt new file mode 100644 index 000000000..715091dac --- /dev/null +++ b/tests/auto/unit/multimedia/qaudiostatemachine/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qaudiostatemachine + SOURCES + tst_qaudiostatemachine.cpp + LIBRARIES + Qt::Gui + Qt::MultimediaPrivate +) + diff --git a/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp b/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp new file mode 100644 index 000000000..bc17ec821 --- /dev/null +++ b/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp @@ -0,0 +1,643 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +// TESTED_COMPONENT=src/multimedia + +#include <QtTest/QtTest> +#include <private/qaudiostatemachine_p.h> +#include <private/qaudiosystem_p.h> +#include <QThread> + +QT_USE_NAMESPACE + +template<typename F> +static std::unique_ptr<QThread> createTestThread(std::vector<std::atomic_int> &counters, + size_t index, F &&functor, + int minAttemptsCount = 2000) +{ + return std::unique_ptr<QThread>(QThread::create([=, &counters]() { + auto checkCounter = [=](int counter) { return counter < minAttemptsCount; }; + for (; !QTest::currentTestFailed() + && std::any_of(counters.begin(), counters.end(), checkCounter); + ++counters[index]) + functor(); + })); +} + +class tst_QAudioStateMachine : public QObject +{ + Q_OBJECT + +private slots: + void constructor_setsStoppedStateWithNoError(); + + void start_changesState_whenStateIsStopped_data(); + void start_changesState_whenStateIsStopped(); + + void start_doesntChangeState_whenStateIsNotStopped_data(); + void start_doesntChangeState_whenStateIsNotStopped(); + + void stop_changesState_whenStateIsNotStopped_data(); + void stop_changesState_whenStateIsNotStopped(); + + void stop_doesntChangeState_whenStateIsStopped_data(); + void stop_doesntChangeState_whenStateIsStopped(); + + void updateActiveOrIdle_doesntChangeState_whenStateIsNotActiveOrIdle_data(); + void updateActiveOrIdle_doesntChangeState_whenStateIsNotActiveOrIdle(); + + void updateActiveOrIdle_changesState_whenStateIsActiveOrIdle_data(); + void updateActiveOrIdle_changesState_whenStateIsActiveOrIdle(); + + void stopWithDraining_setDrainingFlagUnderTheGuard(); + + void onDrained_interruptsWaitingForDrained_whenCalledFromAnotherThread(); + + void waitForDrained_waitsLimitedTime(); + + void suspendAndResume_saveAndRestoreState_whenStateIsActiveOrIdle_data(); + void suspendAndResume_saveAndRestoreState_whenStateIsActiveOrIdle(); + + void suspend_doesntChangeState_whenStateIsNotActiveOrIdle_data(); + void suspend_doesntChangeState_whenStateIsNotActiveOrIdle(); + + void resume_doesntChangeState_whenStateIsNotSuspended_data(); + void resume_doesntChangeState_whenStateIsNotSuspended(); + + void deleteNotifierInSlot_suppressesAdjacentSignal(); + + void + twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent_whenSynchronizationEnabled(); + + void twoThreadsToggleStartStop_statesAreConsistent_whenSynchronizationEnabled(); + +private: + void generateNotStoppedPrevStates() + { + QTest::addColumn<QAudio::State>("prevState"); + QTest::addColumn<QAudio::Error>("prevError"); + + QTest::newRow("from IdleState") << QAudio::IdleState << QAudio::UnderrunError; + QTest::newRow("from ActiveState") << QAudio::ActiveState << QAudio::NoError; + QTest::newRow("from SuspendedState") << QAudio::SuspendedState << QAudio::NoError; + } + + void generateStoppedAndSuspendedPrevStates() + { + QTest::addColumn<QAudio::State>("prevState"); + + QTest::newRow("from StoppedState") << QAudio::StoppedState; + QTest::newRow("from SuspendedState") << QAudio::SuspendedState; + } +}; + +void tst_QAudioStateMachine::constructor_setsStoppedStateWithNoError() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + QCOMPARE(stateMachine.error(), QAudio::NoError); + QVERIFY(!stateMachine.isDraining()); + QVERIFY(!stateMachine.isActiveOrIdle()); + QCOMPARE(stateMachine.getDrainedAndStopped(), std::make_pair(true, true)); +} + +void tst_QAudioStateMachine::start_changesState_whenStateIsStopped_data() +{ + QTest::addColumn<bool>("active"); + QTest::addColumn<QAudio::State>("expectedState"); + + QTest::newRow("to active") << true << QAudio::ActiveState; + QTest::newRow("to not active") << false << QAudio::IdleState; +} + +void tst_QAudioStateMachine::start_changesState_whenStateIsStopped() +{ + QFETCH(bool, active); + QFETCH(QAudio::State, expectedState); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY(stateMachine.start(active)); + + QCOMPARE(stateSpy.size(), 1); + QCOMPARE(stateSpy.front().front().value<QAudio::State>(), expectedState); + QCOMPARE(errorSpy.size(), 0); + QCOMPARE(stateMachine.state(), expectedState); + QCOMPARE(stateMachine.error(), QAudio::NoError); +} + +void tst_QAudioStateMachine::start_doesntChangeState_whenStateIsNotStopped_data() +{ + generateNotStoppedPrevStates(); +} + +void tst_QAudioStateMachine::start_doesntChangeState_whenStateIsNotStopped() +{ + QFETCH(QAudio::State, prevState); + QFETCH(QAudio::Error, prevError); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + stateMachine.forceSetState(prevState, prevError); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY2(!stateMachine.start(), "Cannot start (active)"); + QVERIFY2(!stateMachine.start(false), "Cannot start (not active)"); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + QCOMPARE(stateMachine.state(), prevState); + QCOMPARE(stateMachine.error(), prevError); +} + +void tst_QAudioStateMachine::stop_changesState_whenStateIsNotStopped_data() +{ + generateNotStoppedPrevStates(); +} + +void tst_QAudioStateMachine::stop_changesState_whenStateIsNotStopped() +{ + QFETCH(QAudio::State, prevState); + QFETCH(QAudio::Error, prevError); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState, prevError); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + auto guard = stateMachine.stop(); + QVERIFY(guard); + + QVERIFY(!stateMachine.isDraining()); + + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + QCOMPARE(stateMachine.error(), prevError); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + guard.reset(); + + QVERIFY(!stateMachine.isDraining()); + + QCOMPARE(stateSpy.size(), 1); + QCOMPARE(stateSpy.front().front().value<QAudio::State>(), QAudio::StoppedState); + QCOMPARE(errorSpy.size(), prevError == QAudio::NoError ? 0 : 1); + if (!errorSpy.empty()) + QCOMPARE(errorSpy.front().front().value<QAudio::Error>(), QAudio::NoError); + + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + QCOMPARE(stateMachine.error(), QAudio::NoError); +} + +void tst_QAudioStateMachine::stop_doesntChangeState_whenStateIsStopped_data() +{ + QTest::addColumn<QAudio::Error>("error"); + + QTest::newRow("from NoError") << QAudio::NoError; + QTest::newRow("from IOError") << QAudio::IOError; +} + +void tst_QAudioStateMachine::stop_doesntChangeState_whenStateIsStopped() +{ + QFETCH(QAudio::Error, error); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.setError(error); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY2(!stateMachine.stop(), "should return false if already stopped"); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + QCOMPARE(stateMachine.error(), error); +} + +void tst_QAudioStateMachine::updateActiveOrIdle_doesntChangeState_whenStateIsNotActiveOrIdle_data() +{ + generateStoppedAndSuspendedPrevStates(); +} + +void tst_QAudioStateMachine::updateActiveOrIdle_doesntChangeState_whenStateIsNotActiveOrIdle() +{ + QFETCH(QAudio::State, prevState); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY(!stateMachine.updateActiveOrIdle(true)); + QVERIFY(!stateMachine.updateActiveOrIdle(false)); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); +} + +void tst_QAudioStateMachine::updateActiveOrIdle_changesState_whenStateIsActiveOrIdle_data() +{ + QTest::addColumn<QAudio::State>("prevState"); + QTest::addColumn<QAudio::Error>("prevError"); + QTest::addColumn<bool>("active"); + QTest::addColumn<QAudio::Error>("error"); + + QTest::newRow("from ActiveState+NoError -> not active+NoError") + << QAudio::ActiveState << QAudio::NoError << false << QAudio::NoError; + QTest::newRow("from Idle(UnderrunError) -> active+NoError") + << QAudio::IdleState << QAudio::UnderrunError << true << QAudio::NoError; + QTest::newRow("from Idle(UnderrunError) -> not active+UnderrunError") + << QAudio::IdleState << QAudio::UnderrunError << false << QAudio::UnderrunError; +} + +void tst_QAudioStateMachine::updateActiveOrIdle_changesState_whenStateIsActiveOrIdle() +{ + QFETCH(QAudio::State, prevState); + QFETCH(QAudio::Error, prevError); + QFETCH(bool, active); + QFETCH(QAudio::Error, error); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState, prevError); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + const auto expectedState = active ? QAudio::ActiveState : QAudio::IdleState; + + auto guard = stateMachine.updateActiveOrIdle(active, error); + QVERIFY(guard); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + QCOMPARE(stateMachine.state(), expectedState); + QCOMPARE(stateMachine.error(), prevError); + + guard.reset(); + + QCOMPARE(stateSpy.size(), expectedState == prevState ? 0 : 1); + if (!stateSpy.empty()) + QCOMPARE(stateSpy.front().front().value<QAudio::State>(), expectedState); + + QCOMPARE(errorSpy.size(), prevError == error ? 0 : 1); + if (!errorSpy.empty()) + QCOMPARE(errorSpy.front().front().value<QAudio::Error>(), error); + + QCOMPARE(stateMachine.state(), expectedState); + QCOMPARE(stateMachine.error(), error); +} + +void tst_QAudioStateMachine::stopWithDraining_setDrainingFlagUnderTheGuard() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.start(); + + auto guard = stateMachine.stop(QAudio::IOError, true); + QVERIFY(guard); + QVERIFY(stateMachine.isDraining()); + QCOMPARE(stateMachine.getDrainedAndStopped(), std::make_pair(false, true)); + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + + guard.reset(); + + QVERIFY(!stateMachine.isDraining()); + QCOMPARE(stateMachine.getDrainedAndStopped(), std::make_pair(true, true)); +} + +void tst_QAudioStateMachine::onDrained_interruptsWaitingForDrained_whenCalledFromAnotherThread() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.start(); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + auto guard = stateMachine.stop(QAudio::IOError, true); + QVERIFY(guard); + QVERIFY(stateMachine.isDraining()); + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + + std::atomic_bool threadStared = false; + auto thread = QThread::create([&]() { + threadStared = true; + QTest::qSleep(100); + stateMachine.onDrained(); + }); + + thread->start(); + + QTRY_VERIFY(threadStared); + + QElapsedTimer elapsedTimer; + elapsedTimer.start(); + stateMachine.waitForDrained(std::chrono::milliseconds(2000)); + QVERIFY(!stateMachine.isDraining()); + + QCOMPARE_LE(elapsedTimer.elapsed(), 100 + 200); + + thread->wait(); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + guard.reset(); + + QCOMPARE(stateSpy.size(), 1); + QCOMPARE(errorSpy.size(), 1); +} + +void tst_QAudioStateMachine::waitForDrained_waitsLimitedTime() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.start(); + + auto guard = stateMachine.stop(QAudio::IOError, true); + QVERIFY(guard); + + QElapsedTimer elapsedTimer; + elapsedTimer.start(); + stateMachine.waitForDrained(std::chrono::milliseconds(100)); + + QCOMPARE_LE(elapsedTimer.elapsed(), 100 + 200); + QCOMPARE_GE(elapsedTimer.elapsed(), 100); + QVERIFY(stateMachine.isDraining()); + QCOMPARE(stateMachine.getDrainedAndStopped(), std::make_pair(false, true)); +} + +void tst_QAudioStateMachine::suspendAndResume_saveAndRestoreState_whenStateIsActiveOrIdle_data() +{ + QTest::addColumn<QAudio::State>("prevState"); + QTest::addColumn<QAudio::Error>("prevError"); + + QTest::newRow("from Active+NoError") << QAudio::ActiveState << QAudio::NoError; + QTest::newRow("from Idle+UnderrunError") << QAudio::IdleState << QAudio::UnderrunError; +} + +void tst_QAudioStateMachine::suspendAndResume_saveAndRestoreState_whenStateIsActiveOrIdle() +{ + QFETCH(QAudio::State, prevState); + QFETCH(QAudio::Error, prevError); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState, prevError); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY(stateMachine.suspend()); + + QCOMPARE(stateSpy.size(), 1); + QCOMPARE(stateSpy.front().front().value<QAudio::State>(), QAudio::SuspendedState); + QCOMPARE(errorSpy.size(), prevError == QAudio::NoError ? 0 : 1); + + QCOMPARE(stateMachine.state(), QAudio::SuspendedState); + QCOMPARE(stateMachine.error(), QAudio::NoError); + + stateSpy.clear(); + errorSpy.clear(); + + QVERIFY(!stateMachine.suspend()); + QVERIFY(stateMachine.resume()); + + QCOMPARE(stateSpy.size(), 1); + QCOMPARE(stateSpy.front().front().value<QAudio::State>(), prevState); + QCOMPARE(errorSpy.size(), 0); + + QCOMPARE(stateMachine.state(), prevState); + QCOMPARE(stateMachine.error(), QAudio::NoError); +} + +void tst_QAudioStateMachine::suspend_doesntChangeState_whenStateIsNotActiveOrIdle_data() +{ + generateStoppedAndSuspendedPrevStates(); +} + +void tst_QAudioStateMachine::suspend_doesntChangeState_whenStateIsNotActiveOrIdle() +{ + QFETCH(QAudio::State, prevState); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY(!stateMachine.suspend()); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + QCOMPARE(stateMachine.state(), prevState); + QCOMPARE(stateMachine.error(), QAudio::NoError); +} + +void tst_QAudioStateMachine::resume_doesntChangeState_whenStateIsNotSuspended_data() +{ + QTest::addColumn<QAudio::State>("prevState"); + + QTest::newRow("from StoppedState") << QAudio::StoppedState; + QTest::newRow("from ActiveState") << QAudio::ActiveState; + QTest::newRow("from IdleState") << QAudio::IdleState; +} + +void tst_QAudioStateMachine::resume_doesntChangeState_whenStateIsNotSuspended() +{ + QFETCH(QAudio::State, prevState); + + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + stateMachine.forceSetState(prevState); + + QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged); + QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged); + + QVERIFY(!stateMachine.resume()); + + QCOMPARE(stateSpy.size(), 0); + QCOMPARE(errorSpy.size(), 0); + + QCOMPARE(stateMachine.state(), prevState); + QCOMPARE(stateMachine.error(), QAudio::NoError); +} + +void tst_QAudioStateMachine::deleteNotifierInSlot_suppressesAdjacentSignal() +{ + auto changeNotifier = std::make_unique<QAudioStateChangeNotifier>(); + QAudioStateMachine stateMachine(*changeNotifier); + stateMachine.start(); + + auto onSignal = [&]() { + QVERIFY2(changeNotifier, "The 2nd signal shouldn't be emitted"); + changeNotifier.reset(); + }; + + connect(changeNotifier.get(), &QAudioStateChangeNotifier::errorChanged, onSignal); + connect(changeNotifier.get(), &QAudioStateChangeNotifier::stateChanged, onSignal); + + stateMachine.stop(QAudio::IOError); +} + +void tst_QAudioStateMachine:: + twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent_whenSynchronizationEnabled() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + QVERIFY(stateMachine.start()); + QCOMPARE(stateMachine.state(), QAudio::ActiveState); + + std::atomic<int> signalsCount = 0; + int changesCount = 0; // non-atomic on purpose; it tests the guard protection + + connect(&changeNotifier, &QAudioStateChangeNotifier::stateChanged, + [&](QAudio::State) { ++signalsCount; }); + + std::vector<std::atomic_int> counters(2); + + auto threadSuspendResume = createTestThread(counters, 0, [&]() { + { + auto notifier = stateMachine.suspend(); + QVERIFY(notifier); + QVERIFY(notifier.isStateChanged()); + ++changesCount; + } + + QCOMPARE(stateMachine.state(), QAudio::SuspendedState); + + { + auto notifier = stateMachine.resume(); + QVERIFY(notifier); + QVERIFY(notifier.isStateChanged()); + ++changesCount; + } + + QCOMPARE_NE(stateMachine.state(), QAudio::SuspendedState); + }); + + auto threadIdleActive = createTestThread(counters, 1, [&]() { + if (auto notifier = stateMachine.updateActiveOrIdle(false)) { + if (notifier.isStateChanged()) + ++changesCount; + + QCOMPARE(stateMachine.state(), QAudio::IdleState); + } + + if (auto notifier = stateMachine.updateActiveOrIdle(true)) { + if (notifier.isStateChanged()) + ++changesCount; + + QCOMPARE(stateMachine.state(), QAudio::ActiveState); + } + }); + + threadSuspendResume->start(); + threadIdleActive->start(); + + threadSuspendResume->wait(); + threadIdleActive->wait(); + + if (QTest::currentTestFailed()) { + qDebug() << "counterSuspendResume:" << counters[0]; + qDebug() << "counterIdleActive:" << counters[1]; + } + + QCOMPARE(signalsCount, changesCount); +} + +void tst_QAudioStateMachine:: + twoThreadsToggleStartStop_statesAreConsistent_whenSynchronizationEnabled() +{ + QAudioStateChangeNotifier changeNotifier; + QAudioStateMachine stateMachine(changeNotifier); + + QVERIFY(stateMachine.start()); + QCOMPARE(stateMachine.state(), QAudio::ActiveState); + + std::atomic<int> signalsCount = 0; + int changesCount = 0; // non-atomic on purpose; it tests the guard protection + + connect(&changeNotifier, &QAudioStateChangeNotifier::stateChanged, + [&](QAudio::State) { ++signalsCount; }); + + std::vector<std::atomic_int> counters(2); + + auto threadStartActive = createTestThread(counters, 0, [&]() { + if (auto startNotifier = stateMachine.start()) { + QCOMPARE(startNotifier.prevState(), QAudio::StoppedState); + QCOMPARE(stateMachine.state(), QAudio::ActiveState); + ++changesCount; + startNotifier.reset(); + + auto stopNotifier = stateMachine.stop(); + ++changesCount; + QVERIFY(stopNotifier); + QCOMPARE(stopNotifier.prevState(), QAudio::ActiveState); + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + } + }); + + auto threadStartIdle = createTestThread(counters, 1, [&]() { + if (auto startNotifier = stateMachine.start(false)) { + QCOMPARE(startNotifier.prevState(), QAudio::StoppedState); + QCOMPARE(stateMachine.state(), QAudio::IdleState); + ++changesCount; + startNotifier.reset(); + + auto stopNotifier = stateMachine.stop(); + ++changesCount; + QVERIFY(stopNotifier); + QCOMPARE(stateMachine.state(), QAudio::StoppedState); + QCOMPARE(stopNotifier.prevState(), QAudio::IdleState); + } + }); + + threadStartActive->start(); + threadStartIdle->start(); + + threadStartActive->wait(); + threadStartIdle->wait(); + + if (QTest::currentTestFailed()) { + qDebug() << "counterSuspendResume:" << counters[0]; + qDebug() << "counterIdleActive:" << counters[1]; + } + + QCOMPARE(signalsCount, changesCount); +} + +QTEST_GUILESS_MAIN(tst_QAudioStateMachine) + +#include "tst_qaudiostatemachine.moc" |