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 | |
9 | QT_BEGIN_NAMESPACE |
10 | |
11 | using Notifier = QAudioStateMachine::Notifier; |
12 | using RawState = QAudioStateMachine::RawState; |
13 | |
14 | namespace { |
15 | constexpr RawState DrainingFlag = 1 << 16; |
16 | constexpr RawState InProgressFlag = 1 << 17; |
17 | constexpr RawState WaitingFlags = DrainingFlag | InProgressFlag; |
18 | |
19 | inline bool isWaitingState(RawState state) |
20 | { |
21 | return (state & WaitingFlags) != 0; |
22 | } |
23 | |
24 | inline bool isDrainingState(RawState state) |
25 | { |
26 | return (state & DrainingFlag) != 0; |
27 | } |
28 | |
29 | inline RawState fromWaitingState(RawState state) |
30 | { |
31 | return state & ~WaitingFlags; |
32 | } |
33 | |
34 | inline QAudio::State toAudioState(RawState state) |
35 | { |
36 | return QAudio::State(fromWaitingState(state)); |
37 | } |
38 | |
39 | template<typename... States> |
40 | constexpr 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 |
47 | template<typename T, typename Predicate> |
48 | bool 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 | |
61 | struct 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 | |
89 | QAudioStateMachine::QAudioStateMachine(QAudioStateChangeNotifier ¬ifier, bool synchronize) : |
90 | m_notifier(¬ifier), |
91 | m_sychronizer(synchronize ? std::make_unique<Synchronizer>() : nullptr) |
92 | { |
93 | } |
94 | |
95 | QAudioStateMachine::~QAudioStateMachine() = default; |
96 | |
97 | QAudio::State QAudioStateMachine::state() const |
98 | { |
99 | return toAudioState(state: m_state); |
100 | } |
101 | |
102 | QAudio::Error QAudioStateMachine::error() const |
103 | { |
104 | return m_error; |
105 | } |
106 | |
107 | Notifier 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 | |
148 | Notifier 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 | |
160 | Notifier QAudioStateMachine::start(bool active) |
161 | { |
162 | return changeState(prevStatesSet: makeStatesSet(first: QAudio::StoppedState), |
163 | newState: active ? QAudio::ActiveState : QAudio::IdleState); |
164 | } |
165 | |
166 | void QAudioStateMachine::waitForDrained(std::chrono::milliseconds timeout) |
167 | { |
168 | if (m_sychronizer) |
169 | m_sychronizer->waitForDrained(state&: m_state, timeout); |
170 | } |
171 | |
172 | void QAudioStateMachine::onDrained() |
173 | { |
174 | if (m_sychronizer) |
175 | m_sychronizer->changeState(changer: [this]() { m_state &= ~DrainingFlag; }); |
176 | } |
177 | |
178 | bool QAudioStateMachine::isDraining() const |
179 | { |
180 | return isDrainingState(state: m_state); |
181 | } |
182 | |
183 | bool QAudioStateMachine::isActiveOrIdle() const { |
184 | const auto state = this->state(); |
185 | return state == QAudio::ActiveState || state == QAudio::IdleState; |
186 | } |
187 | |
188 | std::pair<bool, bool> QAudioStateMachine::getDrainedAndStopped() const |
189 | { |
190 | const RawState state = m_state; |
191 | return { !isDrainingState(state), toAudioState(state) == QAudio::StoppedState }; |
192 | } |
193 | |
194 | Notifier 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 | |
208 | Notifier 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 | |
216 | Notifier QAudioStateMachine::activateFromIdle() |
217 | { |
218 | return changeState(prevStatesSet: makeStatesSet(first: QAudio::IdleState), newState: QAudio::ActiveState); |
219 | } |
220 | |
221 | Notifier 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 | |
227 | void QAudioStateMachine::setError(QAudio::Error error) |
228 | { |
229 | if (m_error.exchange(i: error) != error && m_notifier) |
230 | emit m_notifier->errorChanged(error); |
231 | } |
232 | |
233 | Notifier 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 | |
240 | void 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 | |
262 | QT_END_NAMESPACE |
263 | |