Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qaudioengine_withplayer.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
3
5
6#include <QtMultimedia/qmediadevices.h>
7#include <QtMultimedia/private/qaudiosystem_p.h>
8#include <QtMultimedia/private/qmemory_resource_tlsf_p.h>
9#include <QtMultimedia/private/qmultimedia_ranges_p.h>
10#include <QtMultimedia/private/qrtaudioengine_p.h>
11#include <QtMultimedia/private/qtmm_pmr_p.h>
12#include <QtSpatialAudio/private/qambisonicdecoder_p.h>
13#include <QtSpatialAudio/private/qspatialaudiosound_p.h>
14
15#include <resonance_audio.h>
16
17QT_BEGIN_NAMESPACE
18
19enum class SourceId : int { };
20
22{
23public:
25
27
28 /// QRtAudioEngineVoice overrides
30 bool isActive() noexcept Q_DECL_NONBLOCKING_FUNCTION override { return true; }
31 const QAudioFormat &format() noexcept override { return m_format; }
32
35 void removeSound(SourceId);
36
37 void setPaused(bool);
38 void setOutputMode(QAudioEngine::OutputMode mode);
39
40private:
41 void processSlice(QSpan<float> hostBuffer);
42 void processSliceFillResonanceBuffers();
43 void processSliceProcessSurroundMode(QSpan<float>);
44 void processSliceProcessWithoutReverb(QSpan<float>);
45
46 constexpr static auto framesPerBuffer = QAudioEnginePrivate::framesPerBuffer;
47 QAudioEngine::OutputMode m_outputMode;
48 const std::shared_ptr<vraudio::ResonanceAudio> m_resonanceAudio;
49 const QAudioFormat m_format;
50 QAmbisonicDecoder m_ambisonicDecoder;
51 bool m_paused = false;
52
53 struct SoundState
54 {
55 SharedPlaybackState playbackState;
56 int numberOfChannels;
57 };
58
59 static constexpr size_t poolSize =
61 * 512 // space for 512 sounds
62 + sizeof(float) * qToUnderlying(framesPerBuffer) * 32 // leftover buffer for 32 channels
63 + sizeof(float) * qToUnderlying(framesPerBuffer) * 32 // temporary slice buffer
64 + 16384; // some extra space for internal fragmentation and allocator metadata
65 QtMultimediaPrivate::QTlsfMemoryResource m_rtMemoryPool{ poolSize };
66
67 QtMultimediaPrivate::pmr::map<SourceId, SoundState> m_sounds{ &m_rtMemoryPool };
68 QtMultimediaPrivate::pmr::vector<float> m_leftoverBuffer{ &m_rtMemoryPool };
69};
70
84
85QtMultimediaPrivate::QRtAudioEngineVoice::VoicePlayResult
86QResonanceAudioPlayer::play(QSpan<float> outputBuffer) noexcept Q_DECL_NONBLOCKING_FUNCTION
87{
88 using namespace QtMultimediaPrivate;
89
90 if (m_paused)
91 return VoicePlayResult::Playing;
92
93 const size_t samplesPerSlice = m_format.channelCount() * qToUnderlying(framesPerBuffer);
94
95 // resonance-audio renders slices with fill (not additive) semantics, so every slice is
96 // rendered into a zero-initialized scratch buffer first and mixed into outputBuffer
97 // afterwards
98 while (!outputBuffer.empty()) {
99 if (m_leftoverBuffer.empty() && size_t(outputBuffer.size()) >= samplesPerSlice) {
100 // fast path: the whole slice is consumed immediately, so we can use a stack-allocated
101 // scratch buffer instead of carrying it via m_leftoverBuffer
102 withTemporaryBuffer(samplesPerSlice * sizeof(float), [&](QSpan<std::byte> raw) {
103 QSpan<float> scratch{
104 reinterpret_cast<float *>(raw.data()),
105 qsizetype(samplesPerSlice),
106 };
107 std::fill(scratch.begin(), scratch.end(), 0.0f);
108 processSlice(scratch);
109 for (size_t i = 0; i != samplesPerSlice; ++i)
110 outputBuffer[i] += scratch[i];
111 });
112 outputBuffer = drop(outputBuffer, samplesPerSlice);
113 continue;
114 }
115
116 if (m_leftoverBuffer.empty()) {
117 m_leftoverBuffer.assign(samplesPerSlice, 0.0f);
118 processSlice(QSpan<float>(m_leftoverBuffer));
119 }
120
121 const auto samplesToMix = std::min<qsizetype>(m_leftoverBuffer.size(), outputBuffer.size());
122 for (qsizetype i = 0; i != samplesToMix; ++i)
123 outputBuffer[i] += m_leftoverBuffer[i];
124
125 m_leftoverBuffer.erase(m_leftoverBuffer.begin(), m_leftoverBuffer.begin() + samplesToMix);
126 outputBuffer = drop(outputBuffer, samplesToMix);
127 }
128
129 return VoicePlayResult::Playing;
130}
131
132void QResonanceAudioPlayer::addSound(SourceId id, int numberOfChannels, SharedPlaybackState state)
133{
134 auto [_, inserted] = m_sounds.insert_or_assign(id,
135 SoundState{
136 std::move(state),
137 numberOfChannels,
138 });
139 Q_ASSERT(inserted);
140}
141
142void QResonanceAudioPlayer::setSoundPlaybackData(SourceId id, SharedPlaybackState state)
143{
144 auto it = m_sounds.find(id);
145 Q_ASSERT(it != m_sounds.end());
146 it->second.playbackState = std::move(state);
147}
148
149void QResonanceAudioPlayer::removeSound(SourceId sound)
150{
151 m_sounds.erase(sound);
152}
153
154void QResonanceAudioPlayer::setPaused(bool paused)
155{
156 m_paused = paused;
157}
158
159void QResonanceAudioPlayer::setOutputMode(QAudioEngine::OutputMode mode)
160{
161 m_outputMode = mode;
162}
163
164void QResonanceAudioPlayer::processSlice(QSpan<float> hostBuffer)
165{
166 using QtMultimediaPrivate::drop;
167 constexpr auto framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
168 const qsizetype samplesPerSlice = m_format.channelCount() * framesPerBuffer;
169
170 Q_ASSERT(hostBuffer.size() == samplesPerSlice);
171
172 // fill host API
173 processSliceFillResonanceBuffers();
174
175 // and process
176 switch (m_outputMode) {
177 case QAudioEngine::Surround:
178 processSliceProcessSurroundMode(hostBuffer);
179 break;
180 default:
181 processSliceProcessWithoutReverb(hostBuffer);
182 break;
183 }
184}
185
186
187void QResonanceAudioPlayer::processSliceFillResonanceBuffers()
188{
189 constexpr auto framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
190 static constexpr std::array<float, 2 * framesPerBuffer> nullBuffer{};
191 using QtMultimediaPrivate::take;
192 vraudio::ResonanceAudioApi *api = m_resonanceAudio->api.get();
193
194 for (auto &&[sourceId, sourceState] : m_sounds) {
195 int numberOfChannels = sourceState.numberOfChannels;
196 SharedPlaybackState &playbackState = sourceState.playbackState;
197 if (playbackState) {
198 Q_ASSERT(playbackState->format().channelCount() <= 2);
199 std::array<float, 2 * framesPerBuffer> buf;
200
201 playbackState->getBuffer(take(
202 QSpan<float>{ buf }, playbackState->format().channelCount() * framesPerBuffer));
203 api->SetInterleavedBuffer(qToUnderlying(sourceId), buf.data(), numberOfChannels,
204 framesPerBuffer);
205 } else {
206 api->SetInterleavedBuffer(qToUnderlying(sourceId), nullBuffer.data(), numberOfChannels,
207 framesPerBuffer);
208 }
209 }
210}
211
212void QResonanceAudioPlayer::processSliceProcessSurroundMode(QSpan<float> outputBuffer)
213{
214 constexpr auto framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
215 using QtMultimediaPrivate::take;
216 namespace ranges = QtMultimediaPrivate::ranges;
217 Q_ASSERT(outputBuffer.size() == m_format.channelCount() * framesPerBuffer);
218
219 std::array<const float *, QAmbisonicDecoder::maxAmbisonicChannels> channels;
220 std::array<const float *, 2> reverbBuffers{};
221 int nFrames = m_resonanceAudio->getAmbisonicOutput(channels.data(), reverbBuffers.data(),
222 m_ambisonicDecoder.nInputChannels());
223
224 if (nFrames < 0) {
225 // If we get here, it means that resonanceAudio did not actually fill the buffer.
226 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
227 // In this case we just fill the buffer with silence.
228 ranges::fill(outputBuffer, 0);
229 return;
230 }
231 auto nSamples = m_ambisonicDecoder.outputSamples(nFrames);
232
233 Q_ASSERT(m_ambisonicDecoder.nOutputChannels() <= 8);
234 QSpan currentOutput = take(outputBuffer, nSamples);
235 m_ambisonicDecoder.processBufferWithReverb(
236 QSpan{ channels.data(), m_ambisonicDecoder.nInputChannels() }, reverbBuffers,
237 currentOutput);
238}
239
240void QResonanceAudioPlayer::processSliceProcessWithoutReverb(QSpan<float> outputBuffer)
241{
242 constexpr auto framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
243
244 Q_ASSERT(outputBuffer.size() == m_format.channelCount() * framesPerBuffer);
245 namespace ranges = QtMultimediaPrivate::ranges;
246
247 bool ok = m_resonanceAudio->api->FillInterleavedOutputBuffer(
248 m_format.channelCount(), framesPerBuffer, outputBuffer.data());
249 if (!ok) {
250 // If we get here, it means that resonanceAudio did not actually fill the buffer.
251 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
252 // In this case we just fill the buffer with silence.
253 if (m_sounds.empty()) {
254 ranges::fill(outputBuffer, 0.f);
255 } else {
256 // If we get here, it means that something unexpected happened, so bail.
257 qWarning() << " processSliceProcessWithoutReverb failure!";
258 return;
259 }
260 }
261}
262
263///////////////////////////////////////////////////////////////////////////////////////////////////
264
265QAudioEngineWithPlayer::QAudioEngineWithPlayer(int sampleRate) : QAudioEnginePrivate(sampleRate)
266{
267}
268
269QAudioEngineWithPlayer::~QAudioEngineWithPlayer()
270{
271 stop();
272 m_playbackEngine = nullptr;
273}
274
275void QAudioEngineWithPlayer::start()
276{
277 QAudioDevice device = m_device;
278 if (device.isNull())
279 device = QMediaDevices::defaultAudioOutput();
280
281 if (device.isNull()) {
282 qWarning() << "QAudioEngine::start() failed: No audio output device found.";
283 return;
284 }
285
286 QAudioFormat format;
287 format.setSampleFormat(QAudioFormat::Float);
288 format.setSampleRate(sampleRate());
289 format.setChannelCount(std::min(2, device.maximumChannelCount()));
290
291 m_playbackEngine = std::make_shared<QRtAudioEngine>(device, format, std::nullopt,
292 QAudioEnginePrivate::framesPerBuffer);
293 m_format = format;
294
295 auto voice = std::make_shared<QResonanceAudioPlayer>(this, QRtAudioEngine::allocateVoiceId());
296
297 for (auto &[sound, state] : playbackStates)
298 voice->addSound(SourceId{ sound->sourceId }, sound->nchannels, state);
299
300 m_engineVoice = voice;
301 m_playbackEngine->play(voice);
302}
303
304void QAudioEngineWithPlayer::stop()
305{
306 if (!m_engineVoice)
307 return;
308 m_playbackEngine->stop(m_engineVoice);
309 m_engineVoice = {};
310}
311
312void QAudioEngineWithPlayer::setPaused(bool paused)
313{
314 if (paused == m_paused)
315 return;
316 m_paused = paused;
317 if (!m_engineVoice)
318 return;
319
320 // CHECK: can we pause the stream? it seems a bit problematic, as it would prevent us from sending
321 // rt-visitors to update the state of the player.
322 m_playbackEngine->visitVoiceRt(m_engineVoice->voiceId(),
323 [paused](QResonanceAudioPlayer &player) {
324 player.setPaused(paused);
325 });
326}
327
328bool QAudioEngineWithPlayer::isPaused() const
329{
330 return m_paused;
331}
332
333void QAudioEngineWithPlayer::setOutputDevice(const QAudioDevice &device)
334{
335 if (m_device == device)
336 return;
337 if (m_engineVoice) {
338 qWarning() << "Changing device on a running engine not implemented";
339 return;
340 }
341 m_device = device;
342 Q_Q(QAudioEngine);
343 emit q->outputDeviceChanged();
344}
345
346QAudioDevice QAudioEngineWithPlayer::outputDevice() const
347{
348 return m_device;
349}
350
351void QAudioEngineWithPlayer::addSound(QSpatialAudioSoundPrivate *sound)
352{
353 playbackStates.emplace(sound, nullptr);
354
355 if (!m_engineVoice)
356 return;
357
358 m_playbackEngine->visitVoiceRt(
359 m_engineVoice->voiceId(),
360 [id = SourceId{ sound->sourceId },
361 numberOfChannels = sound->nchannels](QResonanceAudioPlayer &player) mutable {
362 player.addSound(id, numberOfChannels);
363 });
364}
365
366void QAudioEngineWithPlayer::removeSound(QSpatialAudioSoundPrivate *sound)
367{
368 SharedPlaybackState oldState = std::move(playbackStates[sound]);
369 playbackStates.erase(sound);
370 if (!m_engineVoice)
371 return;
372
373 // pass oldState to the lambda to ensure that it is not freed on the real-time thread
374 m_playbackEngine->visitVoiceRt(m_engineVoice->voiceId(),
375 [id = SourceId{ sound->sourceId },
376 oldState = std::move(oldState)](QResonanceAudioPlayer &player) {
377 player.removeSound(id);
378 });
379}
380
381void QAudioEngineWithPlayer::setSoundPlaybackData(QSpatialAudioSoundPrivate *sound,
382 SharedPlaybackState state)
383{
384 auto it = playbackStates.find(sound);
385 Q_ASSERT(it != playbackStates.end());
386
387 SharedPlaybackState oldState = std::exchange(it->second, state);
388
389 if (!m_engineVoice)
390 return;
391
392 // pass oldState to the lambda to ensure that it is not freed on the real-time thread
393 m_playbackEngine->visitVoiceRt(
394 m_engineVoice->voiceId(),
395 [id = SourceId{ sound->sourceId }, oldState = std::move(oldState),
396 state = std::move(state)](QResonanceAudioPlayer &player) mutable {
397 player.setSoundPlaybackData(id, std::move(state));
398 });
399}
400
401void QAudioEngineWithPlayer::setOutputMode(QAudioEngine::OutputMode mode)
402{
403 if (outputMode() == mode)
404 return;
405 QAudioEnginePrivate::setOutputMode(mode);
406 if (!m_engineVoice)
407 return;
408
409 m_playbackEngine->visitVoiceRt(m_engineVoice->voiceId(), [mode](QResonanceAudioPlayer &player) {
410 player.setOutputMode(mode);
411 });
412}
413
414void QAudioEngineWithPlayer::updateRoomEffects()
415{
416 for (auto [sound, key] : playbackStates)
417 sound->updateRoomEffects();
418}
419
420QT_END_NAMESPACE
QResonanceAudioPlayer(QAudioEngineWithPlayer *player, VoiceId id)
VoicePlayResult play(QSpan< float >) noexcept Q_DECL_NONBLOCKING_FUNCTION override
QRtAudioEngineVoice overrides.
bool isActive() noexcept Q_DECL_NONBLOCKING_FUNCTION override
void setOutputMode(QAudioEngine::OutputMode mode)
void setSoundPlaybackData(SourceId, SharedPlaybackState)