1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qaudiostatemachine_p.h"
5#include "qaudiosystem_p.h"
6#include <qpointer.h>
7#include <qdebug.h>
8
9QT_BEGIN_NAMESPACE
10
11using Notifier = QAudioStateMachine::Notifier;
12using RawState = QAudioStateMachine::RawState;
13
14namespace {
15constexpr RawState DrainingFlag = 1 << 16;
16constexpr RawState InProgressFlag = 1 << 17;
17constexpr RawState WaitingFlags = DrainingFlag | InProgressFlag;
18
19inline bool isWaitingState(RawState state)
20{
21 return (state & WaitingFlags) != 0;
22}
23
24inline bool isDrainingState(RawState state)
25{
26 return (state & DrainingFlag) != 0;
27}
28
29inline RawState fromWaitingState(RawState state)
30{
31 return state & ~WaitingFlags;
32}
33
34inline QAudio::State toAudioState(RawState state)
35{
36 return QAudio::State(fromWaitingState(state));
37}
38
39template<typename... States>
40constexpr std::pair<RawState, uint32_t> makeStatesSet(QAudio::State first, States... others)
41{
42 return { first, ((1 << first) | ... | (1 << others)) };
43}
44
45// ensures compareExchange (testAndSet) operation with opportunity
46// to check several states, can be considered as atomic
47template<typename T, typename Predicate>
48bool multipleCompareExchange(std::atomic<T> &target, T &prevValue, T newValue, Predicate predicate)
49{
50 Q_ASSERT(predicate(prevValue));
51 do {
52 if (target.compare_exchange_strong(prevValue, newValue))
53 return true;
54 } while (predicate(prevValue));
55
56 return false;
57}
58
59} // namespace
60
61struct QAudioStateMachine::Synchronizer {
62 QMutex m_mutex;
63 QWaitCondition m_condition;
64
65 template <typename Changer>
66 void changeState(Changer&& changer) {
67 {
68 QMutexLocker locker(&m_mutex);
69 changer();
70 }
71
72 m_condition.notify_all();
73 }
74
75 void waitForOperationFinished(std::atomic<RawState>& state)
76 {
77 QMutexLocker locker(&m_mutex);
78 while (isWaitingState(state))
79 m_condition.wait(lockedMutex: &m_mutex);
80 }
81
82 void waitForDrained(std::atomic<RawState>& state, std::chrono::milliseconds timeout) {
83 QMutexLocker locker(&m_mutex);
84 if (isDrainingState(state))
85 m_condition.wait(lockedMutex: &m_mutex, time: timeout.count());
86 }
87};
88
89QAudioStateMachine::QAudioStateMachine(QAudioStateChangeNotifier &notifier, bool synchronize) :
90 m_notifier(&notifier),
91 m_sychronizer(synchronize ? std::make_unique<Synchronizer>() : nullptr)
92{
93}
94
95QAudioStateMachine::~QAudioStateMachine() = default;
96
97QAudio::State QAudioStateMachine::state() const
98{
99 return toAudioState(state: m_state);
100}
101
102QAudio::Error QAudioStateMachine::error() const
103{
104 return m_error;
105}
106
107Notifier QAudioStateMachine::changeState(std::pair<RawState, uint32_t> prevStatesSet,
108 RawState newState, QAudio::Error error, bool shouldDrain)
109{
110 auto checkState = [flags = prevStatesSet.second](RawState state) {
111 return (flags >> state) & 1;
112 };
113
114 if (!m_sychronizer) {
115 RawState prevState = prevStatesSet.first;
116 const auto exchanged = multipleCompareExchange(
117 target&: m_state, prevValue&: prevState, newValue: newState, predicate: checkState);
118
119 if (Q_LIKELY(exchanged))
120 return { this, newState, prevState, error };
121
122 return {};
123 }
124
125 while (true) {
126 RawState prevState = prevStatesSet.first;
127
128 const auto newWaitingState = newState | (shouldDrain ? WaitingFlags : InProgressFlag);
129
130 const auto exchanged = multipleCompareExchange(
131 target&: m_state, prevValue&: prevState, newValue: newWaitingState, predicate: [checkState](RawState state) {
132 return !isWaitingState(state) && checkState(state);
133 });
134
135 if (Q_LIKELY(exchanged))
136 return { this, newState, prevState, error };
137
138 if (!isWaitingState(state: prevState))
139 return {};
140
141 if (!checkState(fromWaitingState(state: prevState)))
142 return {};
143
144 m_sychronizer->waitForOperationFinished(state&: m_state);
145 }
146}
147
148Notifier QAudioStateMachine::stop(QAudio::Error error, bool shouldDrain, bool forceUpdateError)
149{
150 auto result = changeState(
151 prevStatesSet: makeStatesSet(first: QAudio::ActiveState, others: QAudio::IdleState, others: QAudio::SuspendedState),
152 newState: QAudio::StoppedState, error, shouldDrain);
153
154 if (!result && forceUpdateError)
155 setError(error);
156
157 return result;
158}
159
160Notifier QAudioStateMachine::start(bool active)
161{
162 return changeState(prevStatesSet: makeStatesSet(first: QAudio::StoppedState),
163 newState: active ? QAudio::ActiveState : QAudio::IdleState);
164}
165
166void QAudioStateMachine::waitForDrained(std::chrono::milliseconds timeout)
167{
168 if (m_sychronizer)
169 m_sychronizer->waitForDrained(state&: m_state, timeout);
170}
171
172void QAudioStateMachine::onDrained()
173{
174 if (m_sychronizer)
175 m_sychronizer->changeState(changer: [this]() { m_state &= ~DrainingFlag; });
176}
177
178bool QAudioStateMachine::isDraining() const
179{
180 return isDrainingState(state: m_state);
181}
182
183bool QAudioStateMachine::isActiveOrIdle() const {
184 const auto state = this->state();
185 return state == QAudio::ActiveState || state == QAudio::IdleState;
186}
187
188std::pair<bool, bool> QAudioStateMachine::getDrainedAndStopped() const
189{
190 const RawState state = m_state;
191 return { !isDrainingState(state), toAudioState(state) == QAudio::StoppedState };
192}
193
194Notifier QAudioStateMachine::suspend()
195{
196 // Due to the current documentation, we set QAudio::NoError.
197 // TBD: leave the previous error should be more reasonable (IgnoreError)
198 const auto error = QAudio::NoError;
199 auto result = changeState(prevStatesSet: makeStatesSet(first: QAudio::ActiveState, others: QAudio::IdleState),
200 newState: QAudio::SuspendedState, error);
201
202 if (result)
203 m_suspendedInState = result.prevState();
204
205 return result;
206}
207
208Notifier QAudioStateMachine::resume()
209{
210 // Due to the current documentation, we set QAudio::NoError.
211 // TBD: leave the previous error should be more reasonable (IgnoreError)
212 const auto error = QAudio::NoError;
213 return changeState(prevStatesSet: makeStatesSet(first: QAudio::SuspendedState), newState: m_suspendedInState, error);
214}
215
216Notifier QAudioStateMachine::activateFromIdle()
217{
218 return changeState(prevStatesSet: makeStatesSet(first: QAudio::IdleState), newState: QAudio::ActiveState);
219}
220
221Notifier QAudioStateMachine::updateActiveOrIdle(bool isActive, QAudio::Error error)
222{
223 const auto state = isActive ? QAudio::ActiveState : QAudio::IdleState;
224 return changeState(prevStatesSet: makeStatesSet(first: QAudio::ActiveState, others: QAudio::IdleState), newState: state, error);
225}
226
227void QAudioStateMachine::setError(QAudio::Error error)
228{
229 if (m_error.exchange(i: error) != error && m_notifier)
230 emit m_notifier->errorChanged(error);
231}
232
233Notifier QAudioStateMachine::forceSetState(QAudio::State state, QAudio::Error error)
234{
235 return changeState(prevStatesSet: makeStatesSet(first: QAudio::ActiveState, others: QAudio::IdleState, others: QAudio::SuspendedState,
236 others: QAudio::StoppedState),
237 newState: state, error);
238}
239
240void QAudioStateMachine::reset(RawState state, RawState prevState, QAudio::Error error)
241{
242 Q_ASSERT(!isWaitingState(state));
243
244 if (!m_sychronizer && m_state != state)
245 return;
246
247 const auto isErrorChanged = m_error.exchange(i: error) != error;
248
249 if (m_sychronizer)
250 m_sychronizer->changeState(changer: [&](){ m_state = state; });
251
252 auto notifier = m_notifier;
253
254 if (prevState != state && notifier)
255 emit notifier->stateChanged(state: toAudioState(state));
256
257 // check the notifier in case the object was deleted in
258 if (isErrorChanged && notifier)
259 emit notifier->errorChanged(error);
260}
261
262QT_END_NAMESPACE
263

source code of qtmultimedia/src/multimedia/audio/qaudiostatemachine.cpp