summaryrefslogtreecommitdiffstats
path: root/src/multimedia
diff options
context:
space:
mode:
authorArtem Dyomin <artem.dyomin@qt.io>2023-04-17 13:33:52 +0200
committerArtem Dyomin <artem.dyomin@qt.io>2023-07-14 21:13:12 +0000
commitfeffdf7cfef2d2ca69906aca9a11fe46acee7fa4 (patch)
tree2805c09b9f411969f2eb7edbdafab114071b4625 /src/multimedia
parent7c1be065176be3cb8b15ce83283dbeda875e1bae (diff)
Refactor DarwinAudioSink and PulseAudioSink and run CI test
- the main change: implemented QAudioStateMachine allowing atomic state changing. The main reasons for creating the state machine: * get rid of state changing race conditions without mutex lockings and contentions. * it's good to have a common approach for audio states changings and notifications. The implementation of the state machine supports synchronization of actions after states toggling (turned off for pulse slink sinse it has own synchronization approach). Other audio sinks and sources might be refactored in the future. - remove a hack in PulseAudioSink with a callback based on QObject::destroyed connection, the hack appeared to cause a memory leak time-to-time. - the main business logic is not changed Testing: - tested manually on linux and macOS - auto tests * Linux QAudioSink tests became working locally and on CI. * macOS tests work locally, on CI only one test is blacklisted due to the issue not related to race conditions. Task-number: QTBUG-113194 Pick-to: 6.5 6.6 Change-Id: I68002c243dea874c4309b14b1fbd7b618392ab1f Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
Diffstat (limited to 'src/multimedia')
-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
9 files changed, 627 insertions, 288 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