summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtem Dyomin <artem.dyomin@qt.io>2023-09-11 15:44:52 +0200
committerArtem Dyomin <artem.dyomin@qt.io>2023-09-18 14:03:50 +0000
commit834e008f4772462c536726b80cb3e4e19b851b53 (patch)
treebbc5beba2d5a3db6d9cd41e958d68fa9ff091ce0
parenta6f31d24318e1437b92185ca1ef4c1d1b0376d0c (diff)
Improve and prettify logic and usage of QAudioStateMachine
* both audio state + error now are handled atomically together. * internal synchronization is not need for current purposes; it has been addressed. * use acq/rel and relaxed memory order depending on the situation instead of the default non-optimal sequential order. * add new unit tests and remove some of old ones that are not relevant o the refactoring. Pick-to: 6.5 6.6 Change-Id: I644d8e67c933c0d98556a00d4a614fce879e37c2 Reviewed-by: Jøger Hansegård <joger.hansegard@qt.io> Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
-rw-r--r--src/multimedia/audio/qaudiostatemachine.cpp163
-rw-r--r--src/multimedia/audio/qaudiostatemachine_p.h55
-rw-r--r--src/multimedia/audio/qaudiostatemachineutils_p.h61
-rw-r--r--src/multimedia/darwin/qdarwinaudiosink.mm38
-rw-r--r--src/multimedia/darwin/qdarwinaudiosink_p.h4
-rw-r--r--src/multimedia/pulseaudio/qpulseaudiosink.cpp17
-rw-r--r--tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp256
7 files changed, 277 insertions, 317 deletions
diff --git a/src/multimedia/audio/qaudiostatemachine.cpp b/src/multimedia/audio/qaudiostatemachine.cpp
index 83716efd2..2d42200e6 100644
--- a/src/multimedia/audio/qaudiostatemachine.cpp
+++ b/src/multimedia/audio/qaudiostatemachine.cpp
@@ -11,37 +11,7 @@ QT_BEGIN_NAMESPACE
using Notifier = QAudioStateMachine::Notifier;
using namespace AudioStateMachineUtils;
-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(QAudioStateChangeNotifier &notifier) : m_notifier(&notifier)
{
}
@@ -49,98 +19,70 @@ QAudioStateMachine::~QAudioStateMachine() = default;
QAudio::State QAudioStateMachine::state() const
{
- return toAudioState(m_state);
+ return toAudioState(m_state.load(std::memory_order_acquire));
}
QAudio::Error QAudioStateMachine::error() const
{
- return m_error;
+ return toAudioError(m_state.load(std::memory_order_acquire));
}
-Notifier QAudioStateMachine::changeState(std::pair<RawState, uint32_t> prevStatesSet,
- RawState newState, QAudio::Error error, bool shouldDrain)
+template <typename StatesChecker, typename NewState>
+Notifier QAudioStateMachine::changeState(const StatesChecker &checker, const NewState &newState)
{
- 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 constexpr (std::is_same_v<RawState, NewState>)
+ return changeState(checker, [newState](RawState) { return newState; });
+ else {
+ RawState prevState = m_state.load(std::memory_order_relaxed);
+ const auto exchanged = multipleCompareExchange(m_state, prevState, checker, newState);
if (Q_LIKELY(exchanged))
- return { this, newState, prevState, error };
+ return { this, newState(prevState), prevState };
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);
- }
}
Notifier QAudioStateMachine::stop(QAudio::Error error, bool shouldDrain, bool forceUpdateError)
{
- auto result = changeState(
- makeStatesSet(QAudio::ActiveState, QAudio::IdleState, QAudio::SuspendedState),
- QAudio::StoppedState, error, shouldDrain);
+ auto statesChecker =
+ makeStatesChecker(QAudio::ActiveState, QAudio::IdleState, QAudio::SuspendedState,
+ forceUpdateError ? QAudio::StoppedState : QAudio::ActiveState);
- if (!result && forceUpdateError)
- setError(error);
+ const auto state = toRawState(QAudio::StoppedState, error);
+ auto getNewState = [&](RawState prevState) {
+ const bool shouldAddFlag = shouldDrain && toAudioState(prevState) == QAudio::ActiveState;
+ return shouldAddFlag ? addDrainingFlag(state) : state;
+ };
- return result;
+ return changeState(statesChecker, getNewState);
}
Notifier QAudioStateMachine::start(bool active)
{
- return changeState(makeStatesSet(QAudio::StoppedState),
- active ? QAudio::ActiveState : QAudio::IdleState);
+ return changeState(makeStatesChecker(QAudio::StoppedState),
+ toRawState(active ? QAudio::ActiveState : QAudio::IdleState));
}
-void QAudioStateMachine::waitForDrained(std::chrono::milliseconds timeout)
+bool QAudioStateMachine::isActiveOrIdle() const
{
- if (m_sychronizer)
- m_sychronizer->waitForDrained(m_state, timeout);
+ const auto state = this->state();
+ return state == QAudio::ActiveState || state == QAudio::IdleState;
}
-void QAudioStateMachine::onDrained()
+bool QAudioStateMachine::onDrained()
{
- if (m_sychronizer)
- m_sychronizer->changeState([this]() { m_state &= ~DrainingFlag; });
+ return changeState(isDrainingState, removeDrainingFlag);
}
bool QAudioStateMachine::isDraining() const
{
- return isDrainingState(m_state);
-}
-
-bool QAudioStateMachine::isActiveOrIdle() const {
- const auto state = this->state();
- return state == QAudio::ActiveState || state == QAudio::IdleState;
+ return isDrainingState(m_state.load(std::memory_order_acquire));
}
std::pair<bool, bool> QAudioStateMachine::getDrainedAndStopped() const
{
- const RawState state = m_state;
+ const auto state = m_state.load(std::memory_order_acquire);
return { !isDrainingState(state), toAudioState(state) == QAudio::StoppedState };
}
@@ -149,11 +91,11 @@ Notifier 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);
+ auto result = changeState(makeStatesChecker(QAudio::ActiveState, QAudio::IdleState),
+ toRawState(QAudio::SuspendedState, error));
if (result)
- m_suspendedInState = result.prevState();
+ m_suspendedInState = result.prevAudioState();
return result;
}
@@ -163,53 +105,46 @@ Notifier 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);
+ return changeState(makeStatesChecker(QAudio::SuspendedState),
+ toRawState(m_suspendedInState, error));
}
Notifier QAudioStateMachine::activateFromIdle()
{
- return changeState(makeStatesSet(QAudio::IdleState), QAudio::ActiveState);
+ return changeState(makeStatesChecker(QAudio::IdleState), toRawState(QAudio::ActiveState));
}
Notifier QAudioStateMachine::updateActiveOrIdle(bool isActive, QAudio::Error error)
{
const auto state = isActive ? QAudio::ActiveState : QAudio::IdleState;
- return changeState(makeStatesSet(QAudio::ActiveState, QAudio::IdleState), state, error);
+ return changeState(makeStatesChecker(QAudio::ActiveState, QAudio::IdleState),
+ toRawState(state, error));
}
-void QAudioStateMachine::setError(QAudio::Error error)
+Notifier QAudioStateMachine::setError(QAudio::Error error)
{
- if (m_error.exchange(error) != error && m_notifier)
- emit m_notifier->errorChanged(error);
+ auto fixState = [error](RawState prevState) { return setStateError(prevState, error); };
+ return changeState([](RawState) { return true; }, fixState);
}
Notifier QAudioStateMachine::forceSetState(QAudio::State state, QAudio::Error error)
{
- return changeState(makeStatesSet(QAudio::ActiveState, QAudio::IdleState, QAudio::SuspendedState,
- QAudio::StoppedState),
- state, error);
+ return changeState([](RawState) { return true; }, toRawState(state, error));
}
-void QAudioStateMachine::reset(RawState state, RawState prevState, QAudio::Error error)
+void QAudioStateMachine::reset(RawState state, RawState prevState)
{
- Q_ASSERT(!isWaitingState(state));
-
- if (!m_sychronizer && m_state != state)
- return;
-
- const auto isErrorChanged = 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));
+ const auto audioState = toAudioState(state);
+ const auto audioError = toAudioError(state);
+
+ if (toAudioState(prevState) != audioState && notifier)
+ emit notifier->stateChanged(audioState);
// check the notifier in case the object was deleted in
- if (isErrorChanged && notifier)
- emit notifier->errorChanged(error);
+ if (toAudioError(prevState) != audioError && notifier)
+ emit notifier->errorChanged(audioError);
}
QT_END_NAMESPACE
diff --git a/src/multimedia/audio/qaudiostatemachine_p.h b/src/multimedia/audio/qaudiostatemachine_p.h
index b3207230b..385453020 100644
--- a/src/multimedia/audio/qaudiostatemachine_p.h
+++ b/src/multimedia/audio/qaudiostatemachine_p.h
@@ -14,12 +14,11 @@
//
// We mean it.
//
+
#include "qaudiostatemachineutils_p.h"
-#include <qmutex.h>
-#include <qwaitcondition.h>
+
#include <qpointer.h>
#include <atomic>
-#include <chrono>
QT_BEGIN_NAMESPACE
@@ -45,7 +44,7 @@ public:
void reset()
{
if (auto stateMachine = std::exchange(m_stateMachine, nullptr))
- stateMachine->reset(m_state, m_prevState, m_error);
+ stateMachine->reset(m_state, m_prevState);
}
~Notifier() { reset(); }
@@ -54,29 +53,24 @@ public:
Notifier(Notifier &&other) noexcept
: m_stateMachine(std::exchange(other.m_stateMachine, nullptr)),
m_state(other.m_state),
- m_prevState(other.m_prevState),
- m_error(other.m_error)
+ m_prevState(other.m_prevState)
{
}
operator bool() const { return m_stateMachine != nullptr; }
- void setError(QAudio::Error error) { m_error = error; }
+ QAudio::State prevAudioState() const { return AudioStateMachineUtils::toAudioState(m_prevState); }
- // 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 "notifier = updateActiveOrIdle(isActive)"
- // void setState(QAudio::State state) { ... }
+ QAudio::State audioState() const { return AudioStateMachineUtils::toAudioState(m_state); }
- bool isStateChanged() const { return m_state != m_prevState; }
+ bool isDraining() const { return AudioStateMachineUtils::isDrainingState(m_state); }
- QAudio::State prevState() const { return QAudio::State(m_prevState); }
+ bool isStateChanged() const { return prevAudioState() != audioState(); }
private:
Notifier(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)
+ RawState prevState = QAudio::StoppedState)
+ : m_stateMachine(stateMachine), m_state(state), m_prevState(prevState)
{
}
@@ -84,12 +78,11 @@ public:
QAudioStateMachine *m_stateMachine;
RawState m_state;
const RawState m_prevState;
- QAudio::Error m_error;
friend class QAudioStateMachine;
};
- QAudioStateMachine(QAudioStateChangeNotifier &notifier, bool synchronize = true);
+ QAudioStateMachine(QAudioStateChangeNotifier &notifier);
~QAudioStateMachine();
@@ -97,20 +90,18 @@ public:
QAudio::Error error() const;
- bool isDraining() const;
-
bool isActiveOrIdle() const;
+ bool isDraining() 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();
+ // Stopped[draining] -> Stopped
+ bool onDrained();
// Active/Idle/Suspended -> Stopped
+ // or Active -> Stopped[draining] for shouldDrain = true
Notifier stop(QAudio::Error error = QAudio::NoError, bool shouldDrain = false,
bool forceUpdateError = false);
@@ -139,22 +130,18 @@ public:
Notifier forceSetState(QAudio::State state, QAudio::Error error = QAudio::NoError);
// force set the error
- void setError(QAudio::Error error);
+ Notifier setError(QAudio::Error error);
private:
- Notifier changeState(std::pair<RawState, uint32_t> prevStatesSet, RawState state,
- QAudio::Error error = QAudio::NoError, bool shouldDrain = false);
+ template <typename StatesChecker, typename NewState>
+ Notifier changeState(const StatesChecker &statesChecker, const NewState &newState);
- void reset(RawState state, RawState prevState, QAudio::Error error);
+ void reset(RawState state, RawState prevState);
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;
+ QAudio::State m_suspendedInState = QAudio::SuspendedState;
};
QT_END_NAMESPACE
diff --git a/src/multimedia/audio/qaudiostatemachineutils_p.h b/src/multimedia/audio/qaudiostatemachineutils_p.h
index a1c5ef579..f80517f77 100644
--- a/src/multimedia/audio/qaudiostatemachineutils_p.h
+++ b/src/multimedia/audio/qaudiostatemachineutils_p.h
@@ -20,47 +20,72 @@
QT_BEGIN_NAMESPACE
namespace AudioStateMachineUtils {
+
using RawState = int;
-constexpr RawState DrainingFlag = 1 << 16;
-constexpr RawState InProgressFlag = 1 << 17;
-constexpr RawState WaitingFlags = DrainingFlag | InProgressFlag;
-constexpr bool isWaitingState(RawState state)
-{
- return (state & WaitingFlags) != 0;
-}
+constexpr uint32_t AudioStateBitsCount = 8;
+constexpr RawState AudioStateMask = 0xFF;
+constexpr RawState AudioErrorMask = 0xFF00;
+constexpr RawState DrainingFlag = 0x10000;
+
+static_assert(!(AudioStateMask & DrainingFlag) && !(AudioStateMask & AudioErrorMask)
+ && !(AudioErrorMask & DrainingFlag),
+ "Invalid masks");
constexpr bool isDrainingState(RawState state)
{
return (state & DrainingFlag) != 0;
}
-constexpr RawState fromWaitingState(RawState state)
+constexpr RawState addDrainingFlag(RawState state)
{
- return state & ~WaitingFlags;
+ return state | DrainingFlag;
+}
+
+constexpr RawState removeDrainingFlag(RawState state)
+{
+ return state & ~DrainingFlag;
}
constexpr QAudio::State toAudioState(RawState state)
{
- return QAudio::State(fromWaitingState(state));
+ return QAudio::State(state & AudioStateMask);
+}
+
+constexpr QAudio::Error toAudioError(RawState state)
+{
+ return QAudio::Error((state & AudioErrorMask) >> AudioStateBitsCount);
+}
+
+constexpr RawState toRawState(QAudio::State state, QAudio::Error error = QAudio::NoError)
+{
+ return state | (error << AudioStateBitsCount);
+}
+
+constexpr RawState setStateError(RawState state, QAudio::Error error)
+{
+ return (error << AudioStateBitsCount) | (state & ~AudioErrorMask);
}
template <typename... States>
-constexpr std::pair<RawState, uint32_t> makeStatesSet(QAudio::State first, States... others)
+constexpr auto makeStatesChecker(States... states)
{
- return { first, ((1 << first) | ... | (1 << others)) };
+ return [=](RawState state) {
+ state &= (AudioStateMask | DrainingFlag);
+ return (... || (state == states));
+ };
}
// 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)
+template <typename T, typename Predicate, typename NewValueGetter>
+bool multipleCompareExchange(std::atomic<T> &target, T &prevValue, Predicate predicate,
+ NewValueGetter newValueGetter)
{
- Q_ASSERT(predicate(prevValue));
- do {
- if (target.compare_exchange_strong(prevValue, newValue))
+ while (predicate(prevValue))
+ if (target.compare_exchange_strong(prevValue, newValueGetter(prevValue),
+ std::memory_order_acq_rel))
return true;
- } while (predicate(prevValue));
return false;
}
diff --git a/src/multimedia/darwin/qdarwinaudiosink.mm b/src/multimedia/darwin/qdarwinaudiosink.mm
index e2d128483..9242bb2f0 100644
--- a/src/multimedia/darwin/qdarwinaudiosink.mm
+++ b/src/multimedia/darwin/qdarwinaudiosink.mm
@@ -272,17 +272,23 @@ void QDarwinAudioSink::stop()
m_audioBuffer->setFillingEnabled(false);
if (auto notifier = m_stateMachine.stop(QAudio::NoError, true)) {
- if (notifier.prevState() == QAudio::ActiveState) {
- m_stateMachine.waitForDrained(std::chrono::milliseconds(500));
+ Q_ASSERT((notifier.prevAudioState() == QAudio::ActiveState) == notifier.isDraining());
+ if (notifier.isDraining() && !m_drainSemaphore.tryAcquire(1, 500)) {
+ const bool wasDraining = m_stateMachine.onDrained();
- if (m_stateMachine.isDraining())
- qWarning() << "Failed wait for sink drained";
+ qWarning() << "Failed wait for getting sink drained; was draining:" << wasDraining;
+
+ // handle a rare corner case when the audio thread managed to release the semaphore in
+ // the time window between tryAcquire and onDrained
+ if (!wasDraining)
+ m_drainSemaphore.acquire();
}
}
}
void QDarwinAudioSink::reset()
{
+ onAudioDeviceDrained();
m_stateMachine.stopOrUpdateError();
}
@@ -392,8 +398,7 @@ OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionF
d->m_totalFrames += framesRead;
#if defined(Q_OS_MACOS)
- // If playback is already stopped.
- if (!drained) {
+ if (stopped) {
qreal oldVolume = d->m_cachedVolume;
// Decrease volume smoothly.
d->setVolume(d->m_volume / 2);
@@ -413,12 +418,8 @@ OSStatus QDarwinAudioSink::renderCallback(void *inRefCon, AudioUnitRenderActionF
}
else {
ioData->mBuffers[0].mDataByteSize = 0;
- if (framesRead == 0) {
- if (!drained)
- d->onAudioDeviceDrained();
- else
- d->onAudioDeviceIdle();
- }
+ if (framesRead == 0)
+ d->onAudioDeviceIdle();
else
d->onAudioDeviceError();
}
@@ -571,22 +572,23 @@ void QDarwinAudioSink::close()
void QDarwinAudioSink::onAudioDeviceIdle()
{
const bool atEnd = m_audioBuffer->deviceAtEnd();
- m_stateMachine.updateActiveOrIdle(false, atEnd ? QAudio::NoError : QAudio::UnderrunError);
+ if (!m_stateMachine.updateActiveOrIdle(false, atEnd ? QAudio::NoError : QAudio::UnderrunError))
+ onAudioDeviceDrained();
}
-void QDarwinAudioSink::onAudioDeviceError()
+void QDarwinAudioSink::onAudioDeviceDrained()
{
- m_stateMachine.stop(QAudio::IOError);
+ if (m_stateMachine.onDrained())
+ m_drainSemaphore.release();
}
-void QDarwinAudioSink::onAudioDeviceDrained()
+void QDarwinAudioSink::onAudioDeviceError()
{
- m_stateMachine.onDrained();
+ m_stateMachine.stop(QAudio::IOError);
}
void QDarwinAudioSink::updateAudioDevice()
{
-
const auto state = m_stateMachine.state();
Q_ASSERT(m_audioBuffer);
diff --git a/src/multimedia/darwin/qdarwinaudiosink_p.h b/src/multimedia/darwin/qdarwinaudiosink_p.h
index c8d0d82d9..1fddcb205 100644
--- a/src/multimedia/darwin/qdarwinaudiosink_p.h
+++ b/src/multimedia/darwin/qdarwinaudiosink_p.h
@@ -24,9 +24,8 @@
#include <CoreAudio/CoreAudioTypes.h>
#include <QtCore/QIODevice>
-#include <QtCore/QWaitCondition>
-#include <QtCore/QMutex>
#include <qdarwinaudiodevice_p.h>
+#include <qsemaphore.h>
QT_BEGIN_NAMESPACE
@@ -163,6 +162,7 @@ private:
bool m_pullMode = false;
QAudioStateMachine m_stateMachine;
+ QSemaphore m_drainSemaphore;
};
QT_END_NAMESPACE
diff --git a/src/multimedia/pulseaudio/qpulseaudiosink.cpp b/src/multimedia/pulseaudio/qpulseaudiosink.cpp
index 612f65de6..d1da36739 100644
--- a/src/multimedia/pulseaudio/qpulseaudiosink.cpp
+++ b/src/multimedia/pulseaudio/qpulseaudiosink.cpp
@@ -112,17 +112,14 @@ static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void *
}
QPulseAudioSink::QPulseAudioSink(const QByteArray &device, QObject *parent)
- : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this, false)
+ : QPlatformAudioSink(parent), m_device(device), m_stateMachine(*this)
{
}
QPulseAudioSink::~QPulseAudioSink()
{
- if (auto notifier = m_stateMachine.stop()) {
+ if (auto notifier = m_stateMachine.stop())
close();
- QSignalBlocker blocker(this);
- notifier.reset();
- }
}
QAudio::Error QPulseAudioSink::error() const
@@ -166,14 +163,13 @@ void QPulseAudioSink::start(QIODevice *device)
return;
}
- auto notifier = m_stateMachine.start();
- Q_ASSERT(notifier);
-
// ensure we only process timing infos that are up to date
gettimeofday(&lastTimingInfo, nullptr);
lastProcessedUSecs = 0;
connect(m_audioSource, &QIODevice::readyRead, this, &QPulseAudioSink::startReading);
+
+ m_stateMachine.start();
}
void QPulseAudioSink::startReading()
@@ -191,9 +187,6 @@ QIODevice *QPulseAudioSink::start()
if (!open())
return nullptr;
- auto notifier = m_stateMachine.start(false);
- Q_ASSERT(notifier);
-
m_audioSource = new PulseOutputPrivate(this);
m_audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
@@ -201,6 +194,8 @@ QIODevice *QPulseAudioSink::start()
gettimeofday(&lastTimingInfo, nullptr);
lastProcessedUSecs = 0;
+ m_stateMachine.start(false);
+
return m_audioSource;
}
diff --git a/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp b/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp
index 0b4375227..c978c8986 100644
--- a/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp
+++ b/tests/auto/unit/multimedia/qaudiostatemachine/tst_qaudiostatemachine.cpp
@@ -43,18 +43,22 @@ private slots:
void stop_doesntChangeState_whenStateIsStopped_data();
void stop_doesntChangeState_whenStateIsStopped();
+ void stopWithDraining_changesState_whenStateIsNotStopped_data();
+ void stopWithDraining_changesState_whenStateIsNotStopped();
+
+ void methods_dontChangeState_whenDraining();
+
+ void onDrained_finishesDraining();
+
+ void onDrained_getsFailed_whenDrainHasntBeenCalled_data();
+ void onDrained_getsFailed_whenDrainHasntBeenCalled();
+
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();
@@ -66,10 +70,9 @@ private slots:
void deleteNotifierInSlot_suppressesAdjacentSignal();
- void
- twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent_whenSynchronizationEnabled();
+ void twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent();
- void twoThreadsToggleStartStop_statesAreConsistent_whenSynchronizationEnabled();
+ void twoThreadsToggleStartStop_statesAreConsistent();
private:
void generateNotStoppedPrevStates()
@@ -98,9 +101,7 @@ void tst_QAudioStateMachine::constructor_setsStoppedStateWithNoError()
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()
@@ -180,18 +181,15 @@ void tst_QAudioStateMachine::stop_changesState_whenStateIsNotStopped()
auto notifier = stateMachine.stop();
QVERIFY(notifier);
- QVERIFY(!stateMachine.isDraining());
-
QCOMPARE(stateMachine.state(), QAudio::StoppedState);
- QCOMPARE(stateMachine.error(), prevError);
+ QCOMPARE(stateMachine.error(), QAudio::NoError);
QCOMPARE(stateSpy.size(), 0);
QCOMPARE(errorSpy.size(), 0);
+ QVERIFY(!notifier.isDraining());
notifier.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);
@@ -200,6 +198,7 @@ void tst_QAudioStateMachine::stop_changesState_whenStateIsNotStopped()
QCOMPARE(stateMachine.state(), QAudio::StoppedState);
QCOMPARE(stateMachine.error(), QAudio::NoError);
+ QVERIFY(!stateMachine.isDraining());
}
void tst_QAudioStateMachine::stop_doesntChangeState_whenStateIsStopped_data()
@@ -228,6 +227,111 @@ void tst_QAudioStateMachine::stop_doesntChangeState_whenStateIsStopped()
QCOMPARE(errorSpy.size(), 0);
QCOMPARE(stateMachine.state(), QAudio::StoppedState);
QCOMPARE(stateMachine.error(), error);
+ QVERIFY(!stateMachine.isDraining());
+}
+
+void tst_QAudioStateMachine::stopWithDraining_changesState_whenStateIsNotStopped_data()
+{
+ generateNotStoppedPrevStates();
+}
+
+void tst_QAudioStateMachine::stopWithDraining_changesState_whenStateIsNotStopped()
+{
+ QFETCH(QAudio::State, prevState);
+ QFETCH(QAudio::Error, prevError);
+
+ QAudioStateChangeNotifier changeNotifier;
+ QAudioStateMachine stateMachine(changeNotifier);
+
+ stateMachine.forceSetState(prevState, prevError);
+
+ QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged);
+
+ auto notifier = stateMachine.stop(QAudio::NoError, true);
+ QVERIFY(notifier);
+ QCOMPARE(notifier.isDraining(), prevState == QAudio::ActiveState);
+ notifier.reset();
+
+ QCOMPARE(stateMachine.state(), QAudio::StoppedState);
+ QCOMPARE(stateMachine.error(), QAudio::NoError);
+ QCOMPARE(stateMachine.isDraining(), prevState == QAudio::ActiveState);
+
+ QCOMPARE(stateSpy.size(), 1);
+}
+
+void tst_QAudioStateMachine::methods_dontChangeState_whenDraining()
+{
+ QAudioStateChangeNotifier changeNotifier;
+ QAudioStateMachine stateMachine(changeNotifier);
+
+ stateMachine.forceSetState(QAudio::ActiveState);
+ stateMachine.stop(QAudio::IOError, true);
+ QVERIFY(stateMachine.isDraining());
+
+ QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged);
+ QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged);
+
+ QVERIFY(!stateMachine.start());
+ QVERIFY(!stateMachine.stop());
+ QVERIFY(!stateMachine.stop(QAudio::NoError, true));
+ QVERIFY(!stateMachine.suspend());
+ QVERIFY(!stateMachine.resume());
+ QVERIFY(!stateMachine.updateActiveOrIdle(false));
+ QVERIFY(!stateMachine.updateActiveOrIdle(true));
+
+ QCOMPARE(stateMachine.state(), QAudio::StoppedState);
+ QCOMPARE(stateMachine.error(), QAudio::IOError);
+
+ QCOMPARE(stateSpy.size(), 0);
+ QCOMPARE(errorSpy.size(), 0);
+
+ QVERIFY(stateMachine.isDraining());
+}
+
+void tst_QAudioStateMachine::onDrained_finishesDraining()
+{
+ QAudioStateChangeNotifier changeNotifier;
+ QAudioStateMachine stateMachine(changeNotifier);
+
+ stateMachine.forceSetState(QAudio::ActiveState);
+ stateMachine.stop(QAudio::IOError, true);
+ QVERIFY(stateMachine.isDraining());
+
+ QSignalSpy stateSpy(&changeNotifier, &QAudioStateChangeNotifier::stateChanged);
+ QSignalSpy errorSpy(&changeNotifier, &QAudioStateChangeNotifier::errorChanged);
+
+ QVERIFY(stateMachine.onDrained());
+ QVERIFY(!stateMachine.isDraining());
+
+ QCOMPARE(stateSpy.size(), 0);
+ QCOMPARE(errorSpy.size(), 0);
+
+ QCOMPARE(stateMachine.state(), QAudio::StoppedState);
+ QCOMPARE(stateMachine.error(), QAudio::IOError);
+
+ QVERIFY(stateMachine.start());
+}
+
+void tst_QAudioStateMachine::onDrained_getsFailed_whenDrainHasntBeenCalled_data()
+{
+ generateNotStoppedPrevStates();
+ QTest::newRow("from Stopped State") << QAudio::StoppedState << QAudio::IOError;
+}
+
+void tst_QAudioStateMachine::onDrained_getsFailed_whenDrainHasntBeenCalled()
+{
+ QFETCH(QAudio::State, prevState);
+ QFETCH(QAudio::Error, prevError);
+
+ QAudioStateChangeNotifier changeNotifier;
+ QAudioStateMachine stateMachine(changeNotifier);
+
+ stateMachine.forceSetState(prevState, prevError);
+
+ QVERIFY(!stateMachine.onDrained());
+
+ QCOMPARE(stateMachine.state(), prevState);
+ QCOMPARE(stateMachine.error(), prevError);
}
void tst_QAudioStateMachine::updateActiveOrIdle_doesntChangeState_whenStateIsNotActiveOrIdle_data()
@@ -293,7 +397,7 @@ void tst_QAudioStateMachine::updateActiveOrIdle_changesState_whenStateIsActiveOr
QCOMPARE(errorSpy.size(), 0);
QCOMPARE(stateMachine.state(), expectedState);
- QCOMPARE(stateMachine.error(), prevError);
+ QCOMPARE(stateMachine.error(), error);
notifier.reset();
@@ -309,89 +413,6 @@ void tst_QAudioStateMachine::updateActiveOrIdle_changesState_whenStateIsActiveOr
QCOMPARE(stateMachine.error(), error);
}
-void tst_QAudioStateMachine::stopWithDraining_setDrainingFlagUnderTheGuard()
-{
- QAudioStateChangeNotifier changeNotifier;
- QAudioStateMachine stateMachine(changeNotifier);
-
- stateMachine.start();
-
- auto notifier = stateMachine.stop(QAudio::IOError, true);
- QVERIFY(notifier);
- QVERIFY(stateMachine.isDraining());
- QCOMPARE(stateMachine.getDrainedAndStopped(), std::make_pair(false, true));
- QCOMPARE(stateMachine.state(), QAudio::StoppedState);
-
- notifier.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 notifier = stateMachine.stop(QAudio::IOError, true);
- QVERIFY(notifier);
- 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);
-
- notifier.reset();
-
- QCOMPARE(stateSpy.size(), 1);
- QCOMPARE(errorSpy.size(), 1);
-}
-
-void tst_QAudioStateMachine::waitForDrained_waitsLimitedTime()
-{
- QAudioStateChangeNotifier changeNotifier;
- QAudioStateMachine stateMachine(changeNotifier);
-
- stateMachine.start();
-
- auto notifier = stateMachine.stop(QAudio::IOError, true);
- QVERIFY(notifier);
-
- 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");
@@ -512,8 +533,7 @@ void tst_QAudioStateMachine::deleteNotifierInSlot_suppressesAdjacentSignal()
stateMachine.stop(QAudio::IOError);
}
-void tst_QAudioStateMachine::
- twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent_whenSynchronizationEnabled()
+void tst_QAudioStateMachine::twoThreadsToggleSuspendResumeAndIdleActive_statesAreConsistent()
{
QAudioStateChangeNotifier changeNotifier;
QAudioStateMachine stateMachine(changeNotifier);
@@ -522,7 +542,7 @@ void tst_QAudioStateMachine::
QCOMPARE(stateMachine.state(), QAudio::ActiveState);
std::atomic<int> signalsCount = 0;
- int changesCount = 0; // non-atomic on purpose; it tests the guard protection
+ std::atomic<int> changesCount = 0;
connect(&changeNotifier, &QAudioStateChangeNotifier::stateChanged,
this, [&](QAudio::State) { ++signalsCount; }, Qt::DirectConnection);
@@ -534,19 +554,17 @@ void tst_QAudioStateMachine::
auto notifier = stateMachine.suspend();
QVERIFY(notifier);
QVERIFY(notifier.isStateChanged());
+ QCOMPARE(notifier.audioState(), QAudio::SuspendedState);
++changesCount;
}
- QCOMPARE(stateMachine.state(), QAudio::SuspendedState);
-
{
auto notifier = stateMachine.resume();
QVERIFY(notifier);
QVERIFY(notifier.isStateChanged());
+ QCOMPARE_NE(notifier.audioState(), QAudio::SuspendedState);
++changesCount;
}
-
- QCOMPARE_NE(stateMachine.state(), QAudio::SuspendedState);
});
auto threadIdleActive = createTestThread(counters, 1, [&]() {
@@ -554,14 +572,14 @@ void tst_QAudioStateMachine::
if (notifier.isStateChanged())
++changesCount;
- QCOMPARE(stateMachine.state(), QAudio::IdleState);
+ QCOMPARE(notifier.audioState(), QAudio::IdleState);
}
if (auto notifier = stateMachine.updateActiveOrIdle(true)) {
if (notifier.isStateChanged())
++changesCount;
- QCOMPARE(stateMachine.state(), QAudio::ActiveState);
+ QCOMPARE(notifier.audioState(), QAudio::ActiveState);
}
});
@@ -579,8 +597,7 @@ void tst_QAudioStateMachine::
QCOMPARE(signalsCount, changesCount);
}
-void tst_QAudioStateMachine::
- twoThreadsToggleStartStop_statesAreConsistent_whenSynchronizationEnabled()
+void tst_QAudioStateMachine::twoThreadsToggleStartStop_statesAreConsistent()
{
QAudioStateChangeNotifier changeNotifier;
QAudioStateMachine stateMachine(changeNotifier);
@@ -589,7 +606,7 @@ void tst_QAudioStateMachine::
QCOMPARE(stateMachine.state(), QAudio::ActiveState);
std::atomic<int> signalsCount = 0;
- int changesCount = 0; // non-atomic on purpose; it tests the guard protection
+ std::atomic<int> changesCount = 0;
connect(&changeNotifier, &QAudioStateChangeNotifier::stateChanged,
this, [&](QAudio::State) { ++signalsCount; }, Qt::DirectConnection);
@@ -598,31 +615,30 @@ void tst_QAudioStateMachine::
auto threadStartActive = createTestThread(counters, 0, [&]() {
if (auto startNotifier = stateMachine.start()) {
- QCOMPARE(startNotifier.prevState(), QAudio::StoppedState);
- QCOMPARE(stateMachine.state(), QAudio::ActiveState);
+ QCOMPARE(startNotifier.prevAudioState(), QAudio::StoppedState);
+ QCOMPARE(startNotifier.audioState(), QAudio::ActiveState);
++changesCount;
startNotifier.reset();
auto stopNotifier = stateMachine.stop();
++changesCount;
QVERIFY(stopNotifier);
- QCOMPARE(stopNotifier.prevState(), QAudio::ActiveState);
- QCOMPARE(stateMachine.state(), QAudio::StoppedState);
+ QCOMPARE(stopNotifier.prevAudioState(), QAudio::ActiveState);
}
});
auto threadStartIdle = createTestThread(counters, 1, [&]() {
if (auto startNotifier = stateMachine.start(false)) {
- QCOMPARE(startNotifier.prevState(), QAudio::StoppedState);
- QCOMPARE(stateMachine.state(), QAudio::IdleState);
+ QCOMPARE(startNotifier.prevAudioState(), QAudio::StoppedState);
+ QCOMPARE(startNotifier.audioState(), QAudio::IdleState);
++changesCount;
startNotifier.reset();
auto stopNotifier = stateMachine.stop();
++changesCount;
QVERIFY(stopNotifier);
- QCOMPARE(stateMachine.state(), QAudio::StoppedState);
- QCOMPARE(stopNotifier.prevState(), QAudio::IdleState);
+ QCOMPARE(stopNotifier.audioState(), QAudio::StoppedState);
+ QCOMPARE(stopNotifier.prevAudioState(), QAudio::IdleState);
}
});