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