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 QAUDIOSYSTEM_PLATFORM_STREAM_SUPPORT_P_H
5#define QAUDIOSYSTEM_PLATFORM_STREAM_SUPPORT_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 <QtMultimedia/qtmultimediaglobal.h>
19#include <QtMultimedia/qaudioformat.h>
20#include <QtMultimedia/qaudiodevice.h>
21#include <QtMultimedia/private/qautoresetevent_p.h>
22#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
23#include <QtMultimedia/private/qaudio_rtsan_support_p.h>
24#include <QtMultimedia/private/qaudiosystem_p.h>
25#include <QtMultimedia/private/qaudiohelpers_p.h>
26#include <QtMultimedia/private/qaudioringbuffer_p.h>
27#include <QtCore/qscopedvaluerollback.h>
28#include <QtCore/qthread.h>
29
30#include <optional>
31#include <variant>
32
33QT_BEGIN_NAMESPACE
34
35namespace QtPrivate {
36class QIODeviceRingBufferWriterBase;
37}
38
39namespace QtMultimediaPrivate {
40
41class QPlatformAudioIOStream
42{
43 template <typename T>
44 using QAudioRingBuffer = QtPrivate::QAudioRingBuffer<T>;
45
46 using Ringbuffer = std::variant<QAudioRingBuffer<float>, QAudioRingBuffer<int32_t>,
47 QAudioRingBuffer<int16_t>, QAudioRingBuffer<uint8_t>>;
48
49public:
50 static qsizetype inferRingbufferFrames(const std::optional<int> &ringbufferSize,
51 const std::optional<int32_t> &hardwareBufferFrames,
52 const QAudioFormat &);
53 static qsizetype inferRingbufferBytes(const std::optional<int> &ringbufferSize,
54 const std::optional<int32_t> &hardwareBufferFrames,
55 const QAudioFormat &);
56
57protected:
58 using NativeSampleFormat = QAudioHelperInternal::NativeSampleFormat;
59 using QAutoResetEvent = QtPrivate::QAutoResetEvent;
60
61 QPlatformAudioIOStream(QAudioDevice m_audioDevice, QAudioFormat m_format,
62 std::optional<int> ringbufferSize,
63 std::optional<int32_t> hardwareBufferFrames, float volume);
64 ~QPlatformAudioIOStream();
65 Q_DISABLE_COPY_MOVE(QPlatformAudioIOStream)
66
67 void setVolume(float);
68 float volume() const { return m_volume.load(m: std::memory_order_relaxed); }
69
70 template <typename Functor>
71 auto visitRingbuffer(Functor &&f)
72 {
73 return std::visit(f, m_ringbuffer);
74 }
75
76 template <typename Functor>
77 auto visitRingbuffer(Functor &&f) const
78 {
79 return std::visit(f, m_ringbuffer);
80 }
81
82 void prepareRingbuffer(std::optional<int> ringbufferSize);
83 int ringbufferSizeInBytes();
84
85 // stop requests
86 void requestStop();
87 bool isStopRequested(std::memory_order memory_order = std::memory_order_relaxed) const
88 {
89 return m_stopRequested.load(m: memory_order);
90 }
91
92 // members
93 const QAudioDevice m_audioDevice;
94 const QAudioFormat m_format;
95 const std::optional<int32_t> m_hardwareBufferFrames;
96
97private:
98 std::atomic<float> m_volume{
99 1.f,
100 };
101
102 Ringbuffer m_ringbuffer{
103 std::in_place_type_t<QAudioRingBuffer<float>>{},
104 0,
105 };
106
107 // stop requests
108 std::atomic<bool> m_stopRequested = false;
109
110public:
111 enum class ShutdownPolicy : uint8_t
112 {
113 DrainRingbuffer,
114 DiscardRingbuffer,
115 };
116};
117
118////////////////////////////////////////////////////////////////////////////////////////////////////
119
120class QPlatformAudioSinkStream : protected QPlatformAudioIOStream
121{
122public:
123 using QPlatformAudioIOStream::ShutdownPolicy;
124 using AudioCallback = QPlatformAudioSink::AudioCallback;
125
126protected:
127 QPlatformAudioSinkStream(QAudioDevice, const QAudioFormat &, std::optional<int> ringbufferSize,
128 std::optional<int32_t> hardwareBufferFrames, float volume);
129 ~QPlatformAudioSinkStream();
130 Q_DISABLE_COPY_MOVE(QPlatformAudioSinkStream)
131
132 uint64_t process(QSpan<std::byte> hostBuffer, qsizetype totalNumberOfFrames,
133 std::optional<NativeSampleFormat> = {}) noexcept QT_MM_NONBLOCKING;
134
135 // ringbuffer / stats
136 quint64 bytesFree() const;
137 std::chrono::microseconds processedDuration() const;
138
139 // downstream delegates
140 virtual void updateStreamIdle(bool) = 0;
141
142 // iodevice
143 QIODevice *createRingbufferWriterDevice();
144 void setQIODevice(QIODevice *device);
145 void createQIODeviceConnections(QIODevice *device);
146 void disconnectQIODeviceConnections();
147 void pullFromQIODevice();
148
149 // LATER: do we want to relax notifying the app thread?
150 static constexpr int notificationThresholdBytes = 0;
151
152 // idle detection
153 void setIdleState(bool);
154 bool isIdle(std::memory_order order = std::memory_order_relaxed) const
155 {
156 return m_streamIsIdle.load(m: order);
157 }
158 void stopIdleDetection();
159
160 template <typename Functor>
161 auto connectIdleHandler(Functor &&f)
162 {
163 return m_streamIdleDetectionNotifier.callOnActivated(std::forward<Functor>(f));
164 }
165
166 template <typename ParentType>
167 void handleIOError(ParentType *parent)
168 {
169 if (parent) {
170 Q_ASSERT(thread()->isCurrentThread());
171 parent->setError(QAudio::IOError);
172 parent->updateStreamState(QtAudio::State::StoppedState);
173
174 parent->m_stream = {};
175 }
176 }
177
178 QThread *thread() const;
179
180 template <typename Functor>
181 void invokeOnAppThread(Functor &&f)
182 {
183 // note: this is not a QObject, so we use the first QObject member of the stream as context
184 QMetaObject::invokeMethod(&m_streamIdleDetectionNotifier, std::forward<Functor>(f));
185 }
186
187private:
188 // qiodevice
189 QIODevice *m_device = nullptr;
190
191 // idle detection
192 std::atomic<bool> m_streamIsIdle = false;
193 QAutoResetEvent m_streamIdleDetectionNotifier;
194 QMetaObject::Connection m_streamIdleDetectionConnection;
195
196 // ringbuffer events
197 QAutoResetEvent m_ringbufferHasSpace;
198 QMetaObject::Connection m_ringbufferHasSpaceConnection;
199 QMetaObject::Connection m_iodeviceHasNewDataConnection;
200
201 std::unique_ptr<QtPrivate::QIODeviceRingBufferWriterBase> m_ringbufferWriterDevice;
202
203 // stats
204 std::atomic_int64_t m_totalFrameCount{};
205 std::atomic_int64_t m_processedFrameCount{};
206
207 void convertToNative(QSpan<const std::byte> internal, QSpan<std::byte> native, float volume,
208 NativeSampleFormat) noexcept QT_MM_NONBLOCKING;
209
210 // pullFromQIODeviceToRingbuffer is not reentrant. however we might end up in situations where a
211 // QIODevice emits readReady from within QIODevice::readData. We protect against this using a
212 // reentrancy guard and queue invocations if we detect a reentrant call
213 template <typename Functor>
214 void withPullIODeviceReentrancyGuard(Functor f)
215 {
216 if (!m_pullIODeviceReentrancyGuard) {
217 QScopedValueRollback<bool> guard{
218 m_pullIODeviceReentrancyGuard,
219 true,
220 };
221 f();
222 } else {
223 QMetaObject::invokeMethod(&m_streamIdleDetectionNotifier,
224 [this, f = std::move(f)]() mutable {
225 withPullIODeviceReentrancyGuard(std::move(f));
226 }, Qt::QueuedConnection);
227 }
228 }
229 bool m_pullIODeviceReentrancyGuard = false;
230
231 void pullFromQIODeviceImpl();
232};
233
234////////////////////////////////////////////////////////////////////////////////////////////////////
235
236class QPlatformAudioSourceStream : protected QPlatformAudioIOStream
237{
238public:
239 using QPlatformAudioIOStream::ShutdownPolicy;
240 using AudioCallback = QPlatformAudioSource::AudioCallback;
241
242protected:
243 QPlatformAudioSourceStream(QAudioDevice, const QAudioFormat &,
244 std::optional<int> ringbufferSize,
245 std::optional<int32_t> hardwareBufferFrames, float volume);
246 ~QPlatformAudioSourceStream();
247
248 Q_DISABLE_COPY_MOVE(QPlatformAudioSourceStream)
249
250 uint64_t process(QSpan<const std::byte> hostBuffer, qsizetype numberOfFrames,
251 std::optional<NativeSampleFormat> = {}) noexcept QT_MM_NONBLOCKING;
252
253 // ringbuffer / stats
254 qsizetype bytesReady() const;
255 std::chrono::microseconds processedDuration() const;
256
257 // iodevice
258 void setQIODevice(QIODevice *device);
259 void createQIODeviceConnections(QIODevice *device);
260 void disconnectQIODeviceConnections();
261 QIODevice *createRingbufferReaderDevice();
262 void pushToIODevice();
263 bool deviceIsRingbufferReader() const;
264 void finalizeQIODevice(ShutdownPolicy);
265 void emptyRingbuffer();
266
267 // downstream delegates
268 virtual void updateStreamIdle(bool) = 0;
269
270 template <typename ParentType>
271 void handleIOError(ParentType *parent)
272 {
273 if (parent) {
274 Q_ASSERT(thread()->isCurrentThread());
275 parent->setError(QAudio::IOError);
276 parent->updateStreamState(QtAudio::State::StoppedState);
277
278 if (deviceIsRingbufferReader())
279 // we own the qiodevice, so let's keep it alive to allow users to drain the
280 // ringbuffer
281 parent->m_retiredStream = std::move(parent->m_stream);
282 else
283 parent->m_stream = {};
284 }
285 }
286
287 QThread *thread() const;
288
289 template <typename Functor>
290 void invokeOnAppThread(Functor &&f)
291 {
292 // note: this is not a QObject, so we use the first QObject member of the stream as context
293 QMetaObject::invokeMethod(&m_ringbufferHasData, std::forward<Functor>(f));
294 }
295
296private:
297 // qiodevice
298 QIODevice *m_device = nullptr;
299 std::unique_ptr<QIODevice> m_ringbufferReaderDevice;
300
301 // ringbuffer events
302 QAutoResetEvent m_ringbufferHasData;
303 QAutoResetEvent m_ringbufferIsFull;
304
305 QMetaObject::Connection m_ringbufferHasDataConnection;
306 QMetaObject::Connection m_ringbufferIsFullConnection;
307
308 // stats
309 std::atomic_uint64_t m_totalNumberOfFramesPushedToRingbuffer{};
310
311 void convertFromNative(QSpan<const std::byte> native, QSpan<std::byte> internal, float volume,
312 NativeSampleFormat) noexcept QT_MM_NONBLOCKING;
313};
314
315} // namespace QtMultimediaPrivate
316
317QT_END_NAMESPACE
318
319#endif // QAUDIOSYSTEM_PLATFORM_STREAM_SUPPORT_P_H
320

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