summaryrefslogtreecommitdiffstats
path: root/src/multimedia/audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/audio')
-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
4 files changed, 448 insertions, 15 deletions
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