summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/multimedia/CMakeLists.txt1
-rw-r--r--src/multimedia/audio/qaudiostatemachine.cpp264
-rw-r--r--src/multimedia/audio/qaudiostatemachine_p.h167
-rw-r--r--src/multimedia/audio/qaudiosystem.cpp9
-rw-r--r--src/multimedia/audio/qaudiosystem_p.h23
-rw-r--r--src/multimedia/darwin/qdarwinaudiosink.mm278
-rw-r--r--src/multimedia/darwin/qdarwinaudiosink_p.h29
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosink.cpp135
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosink_p.h9
-rw-r--r--tests/auto/integration/qaudiosink/BLACKLIST7
-rw-r--r--tests/auto/integration/qaudiosink/tst_qaudiosink.cpp215
-rw-r--r--tests/auto/unit/multimedia/CMakeLists.txt1
-rw-r--r--tests/auto/unit/multimedia/qaudiostatemachine/CMakeLists.txt11
-rw-r--r--tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp643
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 &notifier, bool synchronize) :
+ m_notifier(&notifier),
+ 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 &notifier, 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"