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#include "qaudiosystem_platform_stream_support_p.h"
5
6#include <QtCore/qdebug.h>
7#include <QtMultimedia/private/qaudiohelpers_p.h>
8#include <QtMultimedia/private/qaudio_qiodevice_support_p.h>
9#include <QtMultimedia/private/qmultimedia_assume_p.h>
10
11#include <stdlib.h>
12#if __has_include(<alloca.h>)
13# include <alloca.h>
14#endif
15#if __has_include(<malloc.h>)
16# include <malloc.h>
17#endif
18
19#ifdef Q_CC_MSVC
20# define alloca _alloca
21#endif
22
23QT_BEGIN_NAMESPACE
24
25namespace QtMultimediaPrivate {
26
27using namespace std::chrono_literals;
28
29QPlatformAudioIOStream::QPlatformAudioIOStream(QAudioDevice m_audioDevice, QAudioFormat m_format,
30 std::optional<int> ringbufferSize,
31 std::optional<int32_t> hardwareBufferFrames,
32 float volume)
33 : m_audioDevice{
34 std::move(m_audioDevice),
35 },
36 m_format{
37 m_format,
38 },
39 m_hardwareBufferFrames{
40 hardwareBufferFrames,
41 },
42 m_volume{
43 volume,
44 }
45{
46 prepareRingbuffer(ringbufferSize);
47}
48
49QPlatformAudioIOStream::~QPlatformAudioIOStream()
50{
51 Q_ASSERT(m_stopRequested);
52}
53
54void QPlatformAudioIOStream::setVolume(float volume)
55{
56 m_volume.store(i: volume, m: std::memory_order_relaxed);
57}
58
59void QPlatformAudioIOStream::prepareRingbuffer(std::optional<int> ringbufferSize)
60{
61 using SampleFormat = QAudioFormat::SampleFormat;
62
63 // Warning: QAudioSink::setBufferSize is measured in *bytes* not in *frames*
64 int ringbufferElements = inferRingbufferFrames(ringbufferSize, hardwareBufferFrames: m_hardwareBufferFrames, m_format)
65 * m_format.channelCount();
66
67 switch (m_format.sampleFormat()) {
68 case SampleFormat::Float:
69 m_ringbuffer.emplace<QAudioRingBuffer<float>>(args&: ringbufferElements);
70 break;
71 case SampleFormat::Int16:
72 m_ringbuffer.emplace<QAudioRingBuffer<int16_t>>(args&: ringbufferElements);
73 break;
74 case SampleFormat::Int32:
75 m_ringbuffer.emplace<QAudioRingBuffer<int32_t>>(args&: ringbufferElements);
76 break;
77 case SampleFormat::UInt8:
78 m_ringbuffer.emplace<QAudioRingBuffer<uint8_t>>(args&: ringbufferElements);
79 break;
80
81 default:
82 qCritical() << "invalid sample format";
83 Q_UNREACHABLE_RETURN();
84 }
85}
86
87void QPlatformAudioIOStream::requestStop()
88{
89 m_stopRequested.store(i: true, m: std::memory_order_release);
90}
91
92qsizetype
93QPlatformAudioIOStream::inferRingbufferFrames(const std::optional<int> &ringbufferSize,
94 const std::optional<int32_t> &hardwareBufferFrames,
95 const QAudioFormat &format)
96{
97 int bytesPerFrame = format.bytesPerFrame();
98 QT_MM_ASSUME(bytesPerFrame > 0);
99
100 return inferRingbufferBytes(ringbufferSize, hardwareBufferFrames, format) / bytesPerFrame;
101}
102
103qsizetype
104QPlatformAudioIOStream::inferRingbufferBytes(const std::optional<int> &ringbufferSize,
105 const std::optional<int32_t> &hardwareBufferFrames,
106 const QAudioFormat &format)
107{
108 // ensure to a sane minimum ringbuffer size of twice the hw buffer size or 32 frames
109 const int minimumRingbufferFrames = hardwareBufferFrames ? *hardwareBufferFrames * 2 : 32;
110 const int minimumRingbufferBytes = format.bytesForFrames(frameCount: minimumRingbufferFrames);
111 if (ringbufferSize)
112 return ringbufferSize >= minimumRingbufferBytes ? *ringbufferSize : minimumRingbufferBytes;
113
114 using namespace std::chrono;
115 static constexpr auto defaultBufferDuration = 250ms;
116
117 return format.bytesForDuration(microseconds: microseconds(defaultBufferDuration).count());
118}
119
120int QPlatformAudioIOStream::ringbufferSizeInBytes()
121{
122 return visitRingbuffer(f: [](auto &ringbuffer) {
123 using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
124 return ringbuffer.size() * sizeof(SampleType);
125 });
126}
127
128////////////////////////////////////////////////////////////////////////////////////////////////////
129
130QPlatformAudioSinkStream::QPlatformAudioSinkStream(QAudioDevice audioDevice,
131 const QAudioFormat &format,
132 std::optional<int> ringbufferSize,
133 std::optional<int32_t> hardwareBufferFrames,
134 float volume)
135 : QPlatformAudioIOStream{
136 std::move(audioDevice), format, ringbufferSize, hardwareBufferFrames, volume,
137 }
138{
139 m_streamIdleDetectionConnection = m_streamIdleDetectionNotifier.callOnActivated(functor: [this] {
140 if (isStopRequested())
141 return;
142
143 bool sinkIsIdle = m_streamIsIdle.load();
144
145 if (sinkIsIdle) {
146 // data has been pushed to the ringbuffer, while the stream is
147 // still idle, this will change during the next audio callback
148 bool ringbufferIsEmpty = visitRingbuffer(f: [&](auto &ringbuffer) {
149 return ringbuffer.free() == ringbuffer.size();
150 });
151
152 sinkIsIdle = ringbufferIsEmpty;
153 }
154
155 updateStreamIdle(sinkIsIdle);
156 });
157}
158
159QPlatformAudioSinkStream::~QPlatformAudioSinkStream() = default;
160
161uint64_t
162QPlatformAudioSinkStream::process(QSpan<std::byte> hostBuffer, qsizetype totalNumberOfFrames,
163 std::optional<NativeSampleFormat> nativeFormat) noexcept QT_MM_NONBLOCKING
164{
165 qsizetype totalNumberOfSamples = totalNumberOfFrames * m_format.channelCount();
166
167 const float vol = volume();
168
169 int samplesConsumedFromRingbuffer = visitRingbuffer(f: [&](auto &ringbuffer) {
170 return ringbuffer.consume(totalNumberOfSamples, [&](auto ringbufferRange) {
171 if (nativeFormat) {
172 // Amount of bytes in output range differ from ringbuffer range
173 const qsizetype samplesInChunk = ringbufferRange.size();
174 const qsizetype bytesInChunk = samplesInChunk * bytesPerSample(fmt: *nativeFormat);
175
176 QSpan<std::byte> outputByteRange = take(span: hostBuffer, n: bytesInChunk);
177 hostBuffer = drop(span: hostBuffer, n: bytesInChunk);
178 convertToNative(internal: as_bytes(ringbufferRange), native: outputByteRange, volume: vol, *nativeFormat);
179 } else {
180 QSpan<std::byte> outputByteRange = take(hostBuffer, ringbufferRange.size_bytes());
181 hostBuffer = drop(hostBuffer, ringbufferRange.size_bytes());
182 QAudioHelperInternal::applyVolume(volume: vol, m_format, source: as_bytes(ringbufferRange),
183 destination: outputByteRange);
184 }
185 });
186 });
187
188 if (m_ringbufferWriterDevice) {
189 qint64 bytes = samplesConsumedFromRingbuffer * m_format.bytesPerSample();
190 m_ringbufferWriterDevice->bytesConsumedFromRingbuffer(bytes);
191 }
192
193 if (!isStopRequested()) {
194 if (notificationThresholdBytes == 0 || bytesFree() > notificationThresholdBytes)
195 m_ringbufferHasSpace.set();
196
197 bool streamIsIdle = m_streamIsIdle.load(m: std::memory_order_relaxed);
198 if (streamIsIdle && samplesConsumedFromRingbuffer) {
199 m_streamIsIdle.store(i: false);
200 m_streamIdleDetectionNotifier.set();
201 } else if (!streamIsIdle && !samplesConsumedFromRingbuffer) {
202 m_streamIsIdle.store(i: true);
203 m_streamIdleDetectionNotifier.set();
204 }
205 }
206 if (!hostBuffer.empty())
207 std::fill_n(first: hostBuffer.data(), n: hostBuffer.size(), value: std::byte{});
208
209 uint64_t consumedFrames = samplesConsumedFromRingbuffer / m_format.channelCount();
210 m_processedFrameCount += consumedFrames;
211 m_totalFrameCount += totalNumberOfFrames;
212
213 return consumedFrames;
214}
215
216quint64 QPlatformAudioSinkStream::bytesFree() const
217{
218 return visitRingbuffer(f: [](auto &ringbuffer) {
219 using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
220 return ringbuffer.free() * sizeof(SampleType);
221 });
222}
223
224std::chrono::microseconds QPlatformAudioSinkStream::processedDuration() const
225{
226 return std::chrono::microseconds{
227 m_processedFrameCount * 1'000'000 / m_format.sampleRate(),
228 };
229}
230
231void QPlatformAudioSinkStream::pullFromQIODevice()
232{
233 withPullIODeviceReentrancyGuard(f: [this] {
234 pullFromQIODeviceImpl();
235 });
236}
237
238void QPlatformAudioSinkStream::pullFromQIODeviceImpl()
239{
240 Q_ASSERT(thread()->isCurrentThread());
241 Q_ASSERT(m_device);
242 Q_ASSERT(m_pullIODeviceReentrancyGuard);
243
244 visitRingbuffer(f: [&](auto &ringbuffer) {
245 int elementsPulled = pullFromQIODeviceToRingbuffer(*m_device, ringbuffer);
246 if (elementsPulled)
247 updateStreamIdle(false);
248 });
249}
250
251void QPlatformAudioSinkStream::createQIODeviceConnections(QIODevice *device)
252{
253 // consumed from audio thread
254 m_ringbufferHasSpaceConnection = m_ringbufferHasSpace.callOnActivated(args&: device, args: [this] {
255 pullFromQIODevice();
256 });
257
258 // data has been pushed to device
259 m_iodeviceHasNewDataConnection =
260 QObject::connect(sender: device, signal: &QIODevice::readyRead, context: device, slot: [this] {
261 withPullIODeviceReentrancyGuard(f: [this] {
262 pullFromQIODeviceImpl();
263 updateStreamIdle(false);
264 });
265 });
266}
267
268void QPlatformAudioSinkStream::disconnectQIODeviceConnections()
269{
270 QObject::disconnect(m_ringbufferHasSpaceConnection);
271 QObject::disconnect(m_iodeviceHasNewDataConnection);
272}
273
274QIODevice *QPlatformAudioSinkStream::createRingbufferWriterDevice()
275{
276 m_ringbufferWriterDevice = visitRingbuffer(
277 f: [&](auto &ringbuffer) -> std::unique_ptr<QtPrivate::QIODeviceRingBufferWriterBase> {
278 using SampleType = typename std::decay_t<decltype(ringbuffer)>::ValueType;
279 return std::make_unique<QtPrivate::QIODeviceRingBufferWriter<SampleType>>(&ringbuffer);
280 });
281
282 return m_ringbufferWriterDevice.get();
283}
284
285void QPlatformAudioSinkStream::setQIODevice(QIODevice *device)
286{
287 m_device = device;
288}
289
290void QPlatformAudioSinkStream::setIdleState(bool x)
291{
292 m_streamIsIdle.store(i: x);
293}
294
295void QPlatformAudioSinkStream::stopIdleDetection()
296{
297 QObject::disconnect(m_streamIdleDetectionConnection);
298}
299
300QThread *QPlatformAudioSinkStream::thread() const
301{
302 // QPlatformAudioSinkStream is not a QObject, but still has a notion of an application thread
303 // where it lives on.
304 return m_streamIdleDetectionNotifier.thread();
305}
306
307// we limit alloca calls to 0.5MB. it's good enough for virtually all use cases (i.e. buffers
308// of 4092 frames / 32 channels) and well in the reasonable range of available stack memory on linux
309// (8MB)
310static constexpr qsizetype scratchpadBufferSizeLimit = 512 * 1024;
311static_assert(scratchpadBufferSizeLimit > 4092 * 32 * sizeof(float));
312
313void QPlatformAudioSinkStream::convertToNative(QSpan<const std::byte> internal,
314 QSpan<std::byte> native, float volume,
315 NativeSampleFormat nativeFormat) noexcept QT_MM_NONBLOCKING
316{
317 using namespace QAudioHelperInternal;
318
319 if (volume == 1.f) {
320 convertSampleFormat(source: internal, sourceFormat: toNativeSampleFormat(m_format.sampleFormat()), destination: native,
321 destinationFormat: nativeFormat);
322 return;
323 }
324
325 Q_ASSERT(internal.size() <= scratchpadBufferSizeLimit);
326 std::byte *scratchpadMemory = reinterpret_cast<std::byte *>(alloca(internal.size()));
327 QSpan scratchpadBuffer{ scratchpadMemory, internal.size() };
328
329 applyVolume(volume, m_format, source: internal, destination: scratchpadBuffer);
330 convertSampleFormat(source: scratchpadBuffer, sourceFormat: toNativeSampleFormat(m_format.sampleFormat()), destination: native,
331 destinationFormat: nativeFormat);
332}
333
334////////////////////////////////////////////////////////////////////////////////////////////////////
335
336QPlatformAudioSourceStream::QPlatformAudioSourceStream(QAudioDevice audioDevice,
337 const QAudioFormat &format,
338 std::optional<int> ringbufferSize,
339 std::optional<int32_t> hardwareBufferFrames,
340 float volume)
341 : QPlatformAudioIOStream{
342 std::move(audioDevice), format, ringbufferSize, hardwareBufferFrames, volume,
343 }
344{
345}
346
347QPlatformAudioSourceStream::~QPlatformAudioSourceStream() = default;
348
349uint64_t QPlatformAudioSourceStream::process(
350 QSpan<const std::byte> hostBuffer, qsizetype numberOfFrames,
351 std::optional<NativeSampleFormat> nativeFormat) noexcept QT_MM_NONBLOCKING
352{
353 qsizetype remainingNumberOfSamples = numberOfFrames * m_format.channelCount();
354
355 const float vol = volume();
356 using namespace QtMultimediaPrivate;
357
358 uint64_t totalSamplesWritten = visitRingbuffer(f: [&](auto &rb) {
359 using SampleType = typename std::decay_t<decltype(rb)>::ValueType;
360
361 // clang-format off
362 return rb.produceSome([&](QSpan<SampleType> ringbufferRange) {
363 if (nativeFormat) {
364 // Amount of bytes in input range differ from ringbuffer range
365 const qsizetype samplesInChunk = ringbufferRange.size();
366 const qsizetype bytesInChunk = samplesInChunk * bytesPerSample(fmt: *nativeFormat);
367
368 QSpan<const std::byte> inputByteRange = take(span: hostBuffer, n: bytesInChunk);
369 hostBuffer = drop(span: hostBuffer, n: bytesInChunk);
370 convertFromNative(native: inputByteRange, internal: as_writable_bytes(ringbufferRange), volume: vol,
371 *nativeFormat);
372 } else {
373 QSpan<const std::byte> inputByteRange =
374 take(hostBuffer, ringbufferRange.size_bytes());
375 hostBuffer = drop(hostBuffer, ringbufferRange.size_bytes());
376 QAudioHelperInternal::applyVolume(volume: vol, m_format, source: inputByteRange,
377 destination: as_writable_bytes(ringbufferRange));
378 }
379 return ringbufferRange;
380 }, remainingNumberOfSamples);
381 // clang-format on
382 });
383
384 if (totalSamplesWritten)
385 m_ringbufferHasData.set();
386
387 uint64_t framesWritten = totalSamplesWritten / m_format.channelCount();
388 m_totalNumberOfFramesPushedToRingbuffer += framesWritten;
389 return framesWritten;
390}
391
392void QPlatformAudioSourceStream::pushToIODevice()
393{
394 Q_ASSERT(thread()->isCurrentThread());
395
396 qsizetype bytesPushed = visitRingbuffer(f: [&](auto &ringbuffer) {
397 return QtPrivate::pushToQIODeviceFromRingbuffer(*m_device, ringbuffer);
398 });
399
400 if (bytesPushed)
401 Q_EMIT m_device->readyRead();
402}
403
404bool QPlatformAudioSourceStream::deviceIsRingbufferReader() const
405{
406 return m_device == m_ringbufferReaderDevice.get();
407}
408
409void QPlatformAudioSourceStream::finalizeQIODevice(ShutdownPolicy shutdownPolicy)
410{
411 switch (shutdownPolicy) {
412 case ShutdownPolicy::DiscardRingbuffer:
413 return;
414 case ShutdownPolicy::DrainRingbuffer:
415 if (!deviceIsRingbufferReader())
416 pushToIODevice();
417 return;
418
419 default:
420 Q_UNREACHABLE_RETURN();
421 }
422}
423
424void QPlatformAudioSourceStream::emptyRingbuffer()
425{
426 visitRingbuffer(f: [](auto &ringbuffer) {
427 ringbuffer.consumeAll([](auto &) {
428 });
429 });
430}
431
432QThread *QPlatformAudioSourceStream::thread() const
433{
434 // QPlatformAudioSourceStream is not a QObject, but still has a notion of an application thread
435 // where it lives on.
436 return m_ringbufferHasData.thread();
437}
438
439qsizetype QPlatformAudioSourceStream::bytesReady() const
440{
441 return visitRingbuffer(f: [](const auto &ringbuffer) {
442 return ringbuffer.used() * sizeof(typename std::decay_t<decltype(ringbuffer)>::ValueType);
443 });
444}
445
446std::chrono::microseconds QPlatformAudioSourceStream::processedDuration() const
447{
448 return std::chrono::microseconds{
449 m_format.durationForFrames(
450 frameCount: m_totalNumberOfFramesPushedToRingbuffer.load(m: std::memory_order_relaxed)),
451 };
452}
453
454void QPlatformAudioSourceStream::setQIODevice(QIODevice *device)
455{
456 m_device = device;
457}
458
459void QPlatformAudioSourceStream::createQIODeviceConnections(QIODevice *device)
460{
461 bool pushToDevice = !deviceIsRingbufferReader();
462
463 if (pushToDevice) {
464 m_ringbufferHasDataConnection = m_ringbufferHasData.callOnActivated(args&: device, args: [this] {
465 if (!isStopRequested())
466 updateStreamIdle(false);
467 pushToIODevice();
468 });
469 } else {
470 m_ringbufferHasDataConnection = m_ringbufferHasData.callOnActivated(args&: device, args: [this] {
471 if (!isStopRequested())
472 updateStreamIdle(false);
473 Q_EMIT m_device->readyRead();
474 });
475 }
476
477 m_ringbufferIsFullConnection = m_ringbufferHasData.callOnActivated(args&: device, args: [this] {
478 if (!isStopRequested())
479 updateStreamIdle(false);
480 });
481}
482
483void QPlatformAudioSourceStream::disconnectQIODeviceConnections()
484{
485 QObject::disconnect(m_ringbufferHasDataConnection);
486 QObject::disconnect(m_ringbufferIsFullConnection);
487}
488
489QIODevice *QPlatformAudioSourceStream::createRingbufferReaderDevice()
490{
491 using namespace QtPrivate;
492
493 m_ringbufferReaderDevice = visitRingbuffer(f: [&](auto &rb) -> std::unique_ptr<QIODevice> {
494 using SampleType = typename std::decay_t<decltype(rb)>::ValueType;
495 return std::make_unique<QIODeviceRingBufferReader<SampleType>>(&rb);
496 });
497
498 m_ringbufferReaderDevice->open(mode: QIODevice::ReadOnly | QIODevice::Unbuffered);
499
500 return m_ringbufferReaderDevice.get();
501}
502
503void QPlatformAudioSourceStream::convertFromNative(
504 QSpan<const std::byte> native, QSpan<std::byte> internal, float volume,
505 NativeSampleFormat nativeFormat) noexcept QT_MM_NONBLOCKING
506{
507 using namespace QAudioHelperInternal;
508 if (volume == 1.f) {
509 convertSampleFormat(source: native, sourceFormat: nativeFormat, destination: internal,
510 destinationFormat: QAudioHelperInternal::toNativeSampleFormat(m_format.sampleFormat()));
511 return;
512 }
513
514 Q_ASSERT(internal.size() <= scratchpadBufferSizeLimit);
515 std::byte *scratchpadMemory = reinterpret_cast<std::byte *>(alloca(internal.size()));
516 QSpan scratchpadBuffer{ scratchpadMemory, internal.size() };
517
518 convertSampleFormat(source: native, sourceFormat: nativeFormat, destination: scratchpadBuffer,
519 destinationFormat: QAudioHelperInternal::toNativeSampleFormat(m_format.sampleFormat()));
520
521 applyVolume(volume, m_format, source: scratchpadBuffer, destination: internal);
522}
523
524} // namespace QtMultimediaPrivate
525
526QT_END_NAMESPACE
527
528#ifdef Q_CC_MSVC
529# undef alloca
530#endif
531

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