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