1// Copyright (C) 2025 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#ifndef QRTAUDIOENGINE_P_H
5#define QRTAUDIOENGINE_P_H
6
7//
8// W A R N I N G
9// -------------
10//
11// This file is not part of the Qt API. It exists purely as an
12// implementation detail. This header file may change from version to
13// version without notice, or even be removed.
14//
15// We mean it.
16//
17
18#include <QtCore/qtclasshelpermacros.h>
19#include <QtCore/qtimer.h>
20#include <QtCore/qmutex.h>
21
22#include <QtMultimedia/qaudiosink.h>
23#include <QtMultimedia/qtmultimediaglobal.h>
24#include <QtMultimedia/private/qaudio_rtsan_support_p.h>
25#include <QtMultimedia/private/qaudioringbuffer_p.h>
26#include <QtMultimedia/private/qautoresetevent_p.h>
27#include <QtMultimedia/private/q_pmr_emulation_p.h>
28
29#include <cstdint>
30#include <deque>
31#include <set>
32#include <variant>
33#include <vector>
34
35QT_BEGIN_NAMESPACE
36
37namespace QtMultimediaPrivate {
38
39///////////////////////////////////////////////////////////////////////////////////////////////////
40
41// ID to uniquely identify a voice
42enum class VoiceId : uint64_t
43{
44};
45
46enum class VoicePlayResult : uint8_t
47{
48 Playing,
49 Finished,
50};
51
52///////////////////////////////////////////////////////////////////////////////////////////////////
53
54class Q_MULTIMEDIA_EXPORT QRtAudioEngineVoice
55{
56public:
57 using VoicePlayResult = QtMultimediaPrivate::VoicePlayResult;
58 using VoiceId = QtMultimediaPrivate::VoiceId;
59
60 explicit QRtAudioEngineVoice(VoiceId id) : m_voiceId{ id } { }
61 Q_DISABLE_COPY_MOVE(QRtAudioEngineVoice)
62 virtual ~QRtAudioEngineVoice() = default;
63
64 // once play() returns finished or isActive is false, the QAudioPlaybackEngine will stop the
65 // voice
66 [[nodiscard]] virtual VoicePlayResult play(QSpan<float>) noexcept QT_MM_NONBLOCKING = 0;
67 virtual bool isActive() noexcept QT_MM_NONBLOCKING = 0;
68
69 virtual const QAudioFormat &format() noexcept = 0;
70
71 VoiceId voiceId() const { return m_voiceId; }
72
73private:
74 const VoiceId m_voiceId;
75};
76
77struct QRtAudioEngineVoiceCompare : std::less<uint64_t>
78{
79 using std::less<uint64_t>::operator();
80 template <typename Lhs, typename Rhs>
81 bool operator()(const Lhs &lhs, const Rhs &rhs) const
82 {
83 return operator()(getId(lhs), getId(rhs));
84 }
85
86 static uint64_t getId(VoiceId arg) { return qToUnderlying(e: arg); }
87 static uint64_t getId(const QRtAudioEngineVoice &arg) { return getId(arg: arg.voiceId()); }
88 static uint64_t getId(const std::shared_ptr<QRtAudioEngineVoice> &arg) { return getId(arg: *arg); }
89
90 using is_transparent = std::true_type;
91};
92
93///////////////////////////////////////////////////////////////////////////////////////////////////
94
95namespace Impl {
96template <typename T>
97struct visitor_arg;
98
99template <typename R, typename Arg>
100struct visitor_arg<R(Arg)>
101{
102 using type = Arg;
103};
104
105template <typename R, typename Arg>
106struct visitor_arg<R (*)(Arg)>
107{
108 using type = Arg;
109};
110
111template <typename F>
112struct visitor_arg : visitor_arg<decltype(&F::operator())>
113{
114};
115
116template <typename C, typename R, typename Arg>
117struct visitor_arg<R (C::*)(Arg) const>
118{
119 using type = Arg;
120};
121
122template <typename C, typename R, typename Arg>
123struct visitor_arg<R (C::*)(Arg)>
124{
125 using type = Arg;
126};
127
128} // namespace Impl
129
130template <typename F>
131using visitor_arg_t = typename Impl::visitor_arg<F>::type;
132
133///////////////////////////////////////////////////////////////////////////////////////////////////
134
135// playback engine for QRtAudioEngineVoice instances
136// keeps a QAudioSink alive, but in a suspended state if no voices are playing
137class Q_MULTIMEDIA_EXPORT QRtAudioEngine final : public QObject
138{
139public:
140 using RtVoiceVisitor = std::function<void(QRtAudioEngineVoice &)>;
141 using SharedVoice = std::shared_ptr<QRtAudioEngineVoice>;
142
143 Q_OBJECT
144
145 // commands (app->rt)
146 struct PlayCommand
147 {
148 SharedVoice voice;
149 };
150
151 struct StopCommand
152 {
153 const VoiceId voiceId;
154 };
155
156 // visitors are sent back to the non-rt thread, so that they are destroyed in a safe context
157 struct VisitCommand
158 {
159 const VoiceId voiceId;
160 RtVoiceVisitor callback;
161 };
162
163 // "trivial" visitors are not sent back to the non-rt thread
164 struct VisitCommandTrivial
165 {
166 const VoiceId voiceId;
167 RtVoiceVisitor callback;
168 };
169
170 using RtCommand = std::variant<PlayCommand, StopCommand, VisitCommand, VisitCommandTrivial>;
171
172 // notifications (rt->app)
173 struct StopNotification
174 {
175 SharedVoice voice;
176 };
177
178 struct VisitReply
179 {
180 RtVoiceVisitor callback;
181 };
182
183 using Notification = std::variant<StopNotification, VisitReply>;
184
185public:
186 // we keep a pool of engines with one engine per device/format
187 static std::shared_ptr<QRtAudioEngine> getEngineFor(const QAudioDevice &, const QAudioFormat &);
188
189 QRtAudioEngine(const QAudioDevice &, const QAudioFormat &);
190 Q_DISABLE_COPY_MOVE(QRtAudioEngine)
191 ~QRtAudioEngine() override;
192
193 // play/stop/visitVoiceRT are thread-safe
194 void play(SharedVoice);
195 void stop(const SharedVoice &);
196 void stop(VoiceId);
197
198 template <typename Visitor>
199 void visitVoiceRt(VoiceId id, Visitor visitor)
200 {
201 using visitorArg = visitor_arg_t<Visitor>;
202 static_assert(std::is_reference_v<visitorArg>);
203
204 // we need to prevent that the visitor function is going to be destroyed on the real-time
205 // thread, unless:
206 // * it can be trivially destroyed
207 // * it is sufficiently small to fit into the small-buffer-optimization of std::function.
208 // we don't know what the value is, so we are conservative and assume only the size of a
209 // pointer (we could relax it with something like std::inplace_function)
210 constexpr size_t smallBufferOptimizationEstimate = 2 * sizeof(void *);
211 constexpr bool visitorIsTrivial = std::is_trivially_destructible_v<std::decay_t<Visitor>>
212 && sizeof(Visitor) <= smallBufferOptimizationEstimate;
213
214 auto wrapped = [visitor = std::move(visitor)](QRtAudioEngineVoice &voice) {
215 visitor(static_cast<visitorArg>(voice));
216 };
217 visitVoiceRt(id, RtVoiceVisitor{ wrapped }, visitorIsTrivial);
218 }
219
220 template <typename Visitor>
221 void visitVoiceRt(const SharedVoice &voice, Visitor visitor)
222 {
223 visitVoiceRt(voice->voiceId(), std::move(visitor));
224 }
225
226 static VoiceId allocateVoiceId();
227
228 std::unique_ptr<pmr::memory_resource> &rtMemoryResource() { return m_rtMemoryPool; }
229
230 // testing
231 QAudioSink &audioSink() { return m_sink; }
232 const auto &voices() const { return m_voices; }
233
234Q_SIGNALS:
235 void voiceFinished(VoiceId);
236
237private:
238 void visitVoiceRt(VoiceId, RtVoiceVisitor, bool visitorIsTrivial);
239
240 void audioCallback(QSpan<float>) noexcept QT_MM_NONBLOCKING;
241 void cleanupRetiredVoices() noexcept QT_MM_NONBLOCKING;
242
243 void runRtCommands() noexcept QT_MM_NONBLOCKING;
244 void runRtCommand(PlayCommand) noexcept QT_MM_NONBLOCKING;
245 void runRtCommand(StopCommand) noexcept QT_MM_NONBLOCKING;
246 void runRtCommand(VisitCommand) noexcept QT_MM_NONBLOCKING;
247 void runRtCommand(VisitCommandTrivial) noexcept QT_MM_NONBLOCKING;
248
249 void runNonRtNotifications();
250 void runNonRtNotification(StopNotification);
251 void runNonRtNotification(VisitReply);
252
253 QAudioSink m_sink;
254
255 QMutex m_mutex;
256
257 // Application side
258 std::set<SharedVoice, QRtAudioEngineVoiceCompare> m_voices;
259
260 // Rt memory
261 // Note: when the memory pool is exhausted, we fall back to the system allocator. Not great for
262 // real-time uses, but a simple fallback strategy
263 static constexpr size_t poolSize = 128 * 1024; // 128kb
264 std::unique_ptr<pmr::memory_resource> m_rtMemoryPool;
265
266 // Voice registry on the real-time thread:
267 // invariant: every voice in m_rtVoiceRegistry is also in m_voices
268 using VoiceRegistry = std::set<SharedVoice, QRtAudioEngineVoiceCompare,
269 pmr::polymorphic_allocator<SharedVoice>>;
270 VoiceRegistry m_rtVoiceRegistry{
271 m_rtMemoryPool.get(),
272 };
273
274 // rt/nrt communication
275 static constexpr size_t commandBuffersSize = 2048;
276 QtPrivate::QAudioRingBuffer<RtCommand> m_appToRt{ commandBuffersSize };
277 QtPrivate::QAudioRingBuffer<Notification> m_rtToApp{ commandBuffersSize };
278 std::deque<RtCommand> m_appToRtOverflowBuffer;
279 std::deque<Notification, pmr::polymorphic_allocator<Notification>> m_rtToAppOverflowBuffer{
280 m_rtMemoryPool.get(),
281 };
282 void sendAppToRtCommand(RtCommand cmd);
283 bool sendRtToAppNotification(Notification cmd);
284 void sendPendingRtCommands();
285 bool sendPendingAppNotifications();
286 QTimer m_pendingCommandsTimer;
287
288 QtPrivate::QAutoResetEvent m_notificationEvent;
289 std::vector<VoiceId> m_finishedVoices;
290};
291
292} // namespace QtMultimediaPrivate
293
294QT_END_NAMESPACE
295
296#endif // QRTAUDIOENGINE_P_H
297

source code of qtmultimedia/src/multimedia/audio/qrtaudioengine_p.h