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_threaded.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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 <QtCore/qiodevice.h>
7#include <QtCore/qdebug.h>
8#include <QtCore/qelapsedtimer.h>
9
10#include <QtMultimedia/qaudiodecoder.h>
11#include <QtMultimedia/qmediadevices.h>
12#include <QtMultimedia/qaudiosink.h>
13#include <QtMultimedia/private/qaudio_qspan_support_p.h>
14#ifdef Q_OS_WIN
15# include <QtMultimedia/private/qwindows_wasapi_warmup_client_p.h>
16#endif
17
18#include <QtSpatialAudio/private/qambientsound_p.h>
19#include <QtSpatialAudio/private/qspatialsound_p.h>
20#include <QtSpatialAudio/private/qaudioroom_p.h>
21#include <QtSpatialAudio/private/qambisonicdecoder_p.h>
22#include <QtSpatialAudio/qambientsound.h>
23#include <QtSpatialAudio/qaudiolistener.h>
24
25#include <resonance_audio.h>
26
27#include <memory>
28
30
31// We'd like to have short buffer times, so the sound adjusts itself to changes
32// quickly, but times below 100ms seem to give stuttering on macOS.
33// It might be possible to set this value lower on other OSes.
34const int bufferTimeMs = 100;
35
36// This class lives in the audioThread, but pulls data from QAudioEnginePrivate
37// which lives in the mainThread.
39{
40public:
41 explicit QAudioOutputStream(QAudioEngineThreaded *d) : d(d) { open(QIODevice::ReadOnly); }
43
44 qint64 readData(char *data, qint64 len) override;
45
46 qint64 writeData(const char *, qint64) override;
47
48 qint64 size() const override { return 0; }
49 qint64 bytesAvailable() const override {
50 return std::numeric_limits<qint64>::max();
51 }
52 bool isSequential() const override {
53 return true;
54 }
55 bool atEnd() const override {
56 return false;
57 }
58 qint64 pos() const override {
59 return m_pos;
60 }
61
63 {
64 d->mutex.lock();
65 Q_ASSERT(!sink);
66 QAudioFormat format;
67 auto channelConfig = d->m_outputMode == QAudioEngine::Surround
68 ? d->m_device.channelConfiguration()
69 : QAudioFormat::ChannelConfigStereo;
70 if (channelConfig != QAudioFormat::ChannelConfigUnknown)
71 format.setChannelConfig(channelConfig);
72 else
73 format.setChannelCount(d->m_device.preferredFormat().channelCount());
74 format.setSampleRate(d->sampleRate());
75 format.setSampleFormat(QAudioFormat::Int16);
76 ambisonicDecoder =
77 std::make_unique<QAmbisonicDecoder>(QAmbisonicDecoder::HighQuality, format);
78 sink = std::make_unique<QAudioSink>(d->m_device, format);
79 const qsizetype bufferSize = format.bytesForDuration(bufferTimeMs * 1000);
80 sink->setBufferSize(bufferSize);
81 d->mutex.unlock();
82 // It is important to unlock the mutex before starting the sink, as the sink will
83 // call readData() in the audio thread, which will try to lock the mutex (again)
84 sink->start(this);
85
86#ifdef Q_OS_WIN
87 QtMultimediaPrivate::refreshWarmupClient();
88#endif
89 }
90
92 {
93 if (!sink)
94 return;
95 sink->stop();
96 sink.reset();
97 ambisonicDecoder.reset();
98 }
99
100 void setPaused(bool paused) {
101 if (paused)
102 sink->suspend();
103 else
104 sink->resume();
105 }
106
107private:
108 qint64 m_pos = 0;
109 QAudioEngineThreaded *d = nullptr;
110 std::unique_ptr<QAudioSink> sink;
111 std::unique_ptr<QAmbisonicDecoder> ambisonicDecoder;
112};
113
115
117{
118 return 0;
119}
120
121qint64 QAudioOutputStream::readData(char *data, const qint64 len)
122{
123 if (d->m_paused.loadRelaxed())
124 return 0;
125
126 constexpr auto framesPerBuffer = QAudioEngineThreaded::framesPerBuffer;
127 QSpan<short> outputBuffer((short *)data, len / sizeof(short));
128
129 QMutexLocker l(&d->mutex);
131
132 int nChannels = ambisonicDecoder ? ambisonicDecoder->nOutputChannels() : 2;
133 if (outputBuffer.size() < nChannels * framesPerBuffer)
134 return 0;
135
136 using QtMultimediaPrivate::drop;
137 using QtMultimediaPrivate::take;
138
139 bool ok = true;
140 while (outputBuffer.size() >= nChannels * framesPerBuffer) {
141 // Fill input buffers
142 for (auto *source : std::as_const(d->sources)) {
143 auto *sp = QSpatialSoundPrivate::get(source);
144 if (!sp)
145 continue;
146 std::array<float, framesPerBuffer> buf;
147 sp->getBuffer(buf, framesPerBuffer, 1);
148 d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf.data(), 1,
149 framesPerBuffer);
150 }
151 for (auto *source : std::as_const(d->stereoSources)) {
152 auto *sp = QAmbientSoundPrivate::get(source);
153 if (!sp)
154 continue;
155 std::array<float, 2 * framesPerBuffer> buf;
156 sp->getBuffer(buf, framesPerBuffer, 2);
157 d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf.data(), 2,
158 framesPerBuffer);
159 }
160
161 if (ambisonicDecoder && d->m_outputMode == QAudioEngine::Surround) {
162 std::array<const float *, QAmbisonicDecoder::maxAmbisonicChannels> channels;
163 std::array<const float *, 2> reverbBuffers{};
164 int nSamples = d->resonanceAudio->getAmbisonicOutput(
165 channels.data(), reverbBuffers.data(), ambisonicDecoder->nInputChannels());
166
167 if (nSamples < 0) {
168 // If we get here, it means that resonanceAudio did not actually fill the buffer.
169 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
170 // In this case we just fill the buffer with silence.
171 std::fill(outputBuffer.begin(), outputBuffer.end(), 0);
172 break;
173 }
174
175 Q_ASSERT(ambisonicDecoder->nOutputChannels() <= 8);
176 QSpan<short> currentOutput = take(outputBuffer, ambisonicDecoder->outputSize(nSamples));
177 ambisonicDecoder->processBufferWithReverb(
178 QSpan{ channels.data(), ambisonicDecoder->nInputChannels() }, reverbBuffers,
179 currentOutput);
180 outputBuffer = drop(outputBuffer, ambisonicDecoder->outputSize(nSamples));
181 } else {
182 QSpan<short> currentOutput = take(outputBuffer, nChannels * framesPerBuffer);
183 ok = d->resonanceAudio->api->FillInterleavedOutputBuffer(2, framesPerBuffer,
184 currentOutput.data());
185 if (!ok) {
186 // If we get here, it means that resonanceAudio did not actually fill the buffer.
187 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
188 // In this case we just fill the buffer with silence.
189 if (d->sources.isEmpty() && d->stereoSources.isEmpty()) {
190 std::fill(currentOutput.begin(), currentOutput.end(), 0);
191 } else {
192 // If we get here, it means that something unexpected happened, so bail.
193 qWarning() << " Reading failed!";
194 break;
195 }
196 }
197 outputBuffer = drop(outputBuffer, nChannels * framesPerBuffer);
198 }
199 }
200
201 qint64 bytesProcessed = len - outputBuffer.size_bytes();
202 m_pos += bytesProcessed;
203 return bytesProcessed;
204}
205
207{
208 audioThread.setObjectName(u"QAudioThread");
209 m_device = QMediaDevices::defaultAudioOutput();
210}
211
212QAudioEngineThreaded::~QAudioEngineThreaded()
213{
214 resonanceAudio = {};
215}
216
218{
219 if (outputStream)
220 // already started
221 return;
222
223 resonanceAudio->api->SetStereoSpeakerMode(m_outputMode != QAudioEngine::Headphone);
224 resonanceAudio->api->SetMasterVolume(masterVolume());
225
226 outputStream = std::make_unique<QAudioOutputStream>(this);
227 outputStream->moveToThread(&audioThread);
228 audioThread.start(QThread::TimeCriticalPriority);
229
230 QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::startOutput);
231}
232
234{
235 QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::stopOutput,
236 Qt::BlockingQueuedConnection);
237 outputStream.reset();
238 audioThread.exit(0);
239 audioThread.wait();
240}
241
243{
244 bool old = m_paused.fetchAndStoreRelaxed(paused);
245 if (old != paused) {
246 if (outputStream)
247 outputStream->setPaused(paused);
248 Q_Q(QAudioEngine);
249 emit q->pausedChanged();
250 }
251}
252
254{
255 return m_paused.loadRelaxed();
256}
257
258void QAudioEngineThreaded::setOutputDevice(const QAudioDevice &device)
259{
260 if (m_device == device)
261 return;
262 if (resonanceAudio->api) {
263 qWarning() << "Changing device on a running engine not implemented";
264 return;
265 }
266 m_device = device;
267 Q_Q(QAudioEngine);
268 emit q->outputDeviceChanged();
269}
270
271void QAudioEngineThreaded::setOutputMode(QAudioEngine::OutputMode mode)
272{
273 if (m_outputMode == mode)
274 return;
275 m_outputMode = mode;
276 resonanceAudio->api->SetStereoSpeakerMode(mode != QAudioEngine::Headphone);
277
278 Q_Q(QAudioEngine);
279 emit q->outputModeChanged();
280}
281
283{
284 if (m_roomEffectsEnabled == enabled)
285 return;
286 m_roomEffectsEnabled = enabled;
287 resonanceAudio->roomEffectsEnabled = enabled;
288}
289
290/*!
291 Returns true if room effects are enabled.
292 */
294{
295 return m_roomEffectsEnabled;
296}
297
298void QAudioEngineThreaded::setListenerPosition(std::optional<QVector3D> pos)
299{
300 if (listenerPosition() == pos)
301 return;
302
303 QAudioEnginePrivate::setListenerPosition(pos);
304 listenerPositionDirty = true;
305}
306
307void QAudioEngineThreaded::addSpatialSound(QSpatialSound *sound)
308{
309 QMutexLocker l(&mutex);
310 QSpatialSoundPrivate *sd = QSpatialSoundPrivate::get(sound);
311
312 sd->sourceId = resonanceAudio->api->CreateSoundObjectSource(vraudio::kBinauralHighQuality);
313 sources.append(sound);
314}
315
316void QAudioEngineThreaded::removeSpatialSound(QSpatialSound *sound)
317{
318 QMutexLocker l(&mutex);
319 QSpatialSoundPrivate *sd = QSpatialSoundPrivate::get(sound);
320
321 resonanceAudio->api->DestroySource(sd->sourceId);
322 sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
323 sources.removeOne(sound);
324}
325
326void QAudioEngineThreaded::addStereoSound(QAmbientSound *sound)
327{
328 QMutexLocker l(&mutex);
329 QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
330
331 sd->sourceId = resonanceAudio->api->CreateStereoSource(2);
332 stereoSources.append(sound);
333}
334
335void QAudioEngineThreaded::removeStereoSound(QAmbientSound *sound)
336{
337 QMutexLocker l(&mutex);
338 QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
339
340 resonanceAudio->api->DestroySource(sd->sourceId);
341 sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
342 stereoSources.removeOne(sound);
343}
344
345void QAudioEngineThreaded::addRoom(QAudioRoom *room)
346{
347 QMutexLocker l(&mutex);
348 rooms.append(room);
349}
350
351void QAudioEngineThreaded::removeRoom(QAudioRoom *room)
352{
353 QMutexLocker l(&mutex);
354 rooms.removeOne(room);
355}
356
357// This method is called from the audio thread
359{
360 if (!m_roomEffectsEnabled)
361 return;
362
363 bool needUpdate = listenerPositionDirty;
364 listenerPositionDirty = false;
365
366 bool roomDirty = false;
367 for (const auto &room : std::as_const(rooms)) {
368 auto *rd = QAudioRoomPrivate::get(room);
369 if (rd->dirty) {
370 roomDirty = true;
371 rd->update();
372 needUpdate = true;
373 }
374 }
375
376 if (!needUpdate)
377 return;
378
379 auto inferredRoom = findSmallestRoomForListener(rooms);
380 if (inferredRoom.room != m_currentRoom)
381 roomDirty = true;
382 const bool previousRoom = m_currentRoom;
383 m_currentRoom = inferredRoom.room;
384
385 if (!roomDirty)
386 return;
387
388 // apply room to engine
389 if (!m_currentRoom) {
390 resonanceAudio->api->EnableRoomEffects(false);
391 return;
392 }
393 if (!previousRoom)
394 resonanceAudio->api->EnableRoomEffects(true);
395
396 QAudioRoomPrivate *rp = QAudioRoomPrivate::get(m_currentRoom);
397 resonanceAudio->api->SetReflectionProperties(rp->reflections);
398 resonanceAudio->api->SetReverbProperties(rp->reverb);
399
400 // update room effects for all sound sources
401 for (auto *s : std::as_const(sources)) {
402 auto *sp = QSpatialSoundPrivate::get(s);
403 if (!sp)
404 continue;
405 sp->updateRoomEffects();
406 }
407}
408
409QT_END_NAMESPACE
void setOutputMode(QAudioEngine::OutputMode) override
void addRoom(QAudioRoom *room) override
void addSpatialSound(QSpatialSound *sound) override
void addStereoSound(QAmbientSound *sound) override
void setPaused(bool paused) override
void setOutputDevice(const QAudioDevice &device) override
bool isPaused() const override
void setRoomEffectsEnabled(bool) override
void removeSpatialSound(QSpatialSound *sound) override
bool roomEffectsEnabled() const override
Returns true if room effects are enabled.
void removeStereoSound(QAmbientSound *sound) override
void removeRoom(QAudioRoom *room) override
qint64 readData(char *data, qint64 len) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...
bool atEnd() const override
Returns true if the current read and write position is at the end of the device (i....
~QAudioOutputStream() override
qint64 pos() const override
For random-access devices, this function returns the position that data is written to or read from.
QAudioOutputStream(QAudioEngineThreaded *d)
bool isSequential() const override
Returns true if this device is sequential; otherwise returns false.
qint64 bytesAvailable() const override
Returns the number of bytes that are available for reading.
qint64 writeData(const char *, qint64) override
Writes up to maxSize bytes from data to the device.
qint64 size() const override
For open random-access devices, this function returns the size of the device.
QT_BEGIN_NAMESPACE const int bufferTimeMs