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 <QtMultimedia/qmediadevices.h>
7#include <QtMultimedia/qaudiosink.h>
8#include <QtMultimedia/private/qaudio_qspan_support_p.h>
9#include <QtMultimedia/private/qaudiohelpers_p.h>
10#ifdef Q_OS_WIN
11# include <QtMultimedia/private/qwindows_wasapi_warmup_client_p.h>
12#endif
13#include <QtSpatialAudio/qambientsound.h>
14#include <QtSpatialAudio/qaudiolistener.h>
15#include <QtSpatialAudio/private/qaudioroom_p.h>
16#include <QtSpatialAudio/private/qambisonicdecoder_p.h>
17#include <QtSpatialAudio/private/qspatialaudiosound_p.h>
18#include <QtCore/qiodevice.h>
19#include <QtCore/qdebug.h>
20#include <QtCore/qelapsedtimer.h>
21
22#include <resonance_audio.h>
23
24#include <memory>
25#include <q20vector.h>
26
28
29// We'd like to have short buffer times, so the sound adjusts itself to changes
30// quickly, but times below 100ms seem to give stuttering on macOS.
31// It might be possible to set this value lower on other OSes.
32const int bufferTimeMs = 100;
33
34// This class lives in the audioThread, but pulls data from QAudioEnginePrivate
35// which lives in the mainThread.
37{
38public:
39 explicit QAudioOutputStream(QAudioEngineThreaded *d) : d(d) { open(QIODevice::ReadOnly); }
41
42 qint64 readData(char *data, qint64 len) override;
43
44 qint64 writeData(const char *, qint64) override;
45
46 qint64 size() const override { return 0; }
47 qint64 bytesAvailable() const override {
48 return std::numeric_limits<qint64>::max();
49 }
50 bool isSequential() const override {
51 return true;
52 }
53 bool atEnd() const override {
54 return false;
55 }
56 qint64 pos() const override {
57 return m_pos;
58 }
59
61 {
62 d->mutex.lock();
63 Q_ASSERT(!sink);
64 auto channelConfig = d->outputMode() == QAudioEngine::Surround
65 ? d->m_device.channelConfiguration()
66 : QAudioFormat::ChannelConfigStereo;
67
68 QAudioFormat format;
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
76 ambisonicDecoder = std::make_unique<QAmbisonicDecoder>(
77 QAmbisonicDecoder::AmbisonicOrder::HighQuality, format.sampleRate(),
78 format.channelCount(), format.channelConfig());
79
80 sink = std::make_unique<QAudioSink>(d->m_device, format);
81 const qsizetype bufferSize = format.bytesForDuration(bufferTimeMs * 1000);
82 sink->setBufferSize(bufferSize);
83 d->mutex.unlock();
84 // It is important to unlock the mutex before starting the sink, as the sink will
85 // call readData() in the audio thread, which will try to lock the mutex (again)
86 sink->start(this);
87
88#ifdef Q_OS_WIN
89 QtMultimediaPrivate::refreshWarmupClient();
90#endif
91 }
92
94 {
95 if (!sink)
96 return;
97 sink->stop();
98 sink.reset();
99 ambisonicDecoder.reset();
100 }
101
102 void setPaused(bool paused) {
103 if (paused)
104 sink->suspend();
105 else
106 sink->resume();
107 }
108
109private:
110 qint64 m_pos = 0;
111 QAudioEngineThreaded *d = nullptr;
112 std::unique_ptr<QAudioSink> sink;
113 std::unique_ptr<QAmbisonicDecoder> ambisonicDecoder;
114};
115
117
119{
120 return 0;
121}
122
123
124qint64 QAudioOutputStream::readData(char *data, const qint64 len)
125{
126 constexpr int framesPerBuffer = qToUnderlying(QAudioEnginePrivate::framesPerBuffer);
127 static constexpr std::array<float, 2 * framesPerBuffer> nullBuffer{};
128
129 if (d->m_paused.loadRelaxed())
130 return 0;
131
132 QSpan<short> outputBuffer((short *)data, len / sizeof(short));
133
134 QMutexLocker l(&d->mutex);
135
136 int nChannels = ambisonicDecoder ? ambisonicDecoder->nOutputChannels() : 2;
137 if (outputBuffer.size() < nChannels * framesPerBuffer)
138 return 0;
139
140 using QtMultimediaPrivate::drop;
141 using QtMultimediaPrivate::take;
142 using namespace QAudioHelperInternal;
143
144 const std::unique_ptr<vraudio::ResonanceAudioApi> &api = d->resonanceAudio->api;
145
146 bool ok = true;
147 while (outputBuffer.size() >= nChannels * framesPerBuffer) {
148
149 // Fill input buffers
150 for (auto &&[source, playbackState] : d->playbackStates) {
151 Q_ASSERT(source->nchannels <= 2);
152 if (playbackState) {
153 Q_ASSERT(playbackState->format().channelCount() <= 2);
154 std::array<float, 2 * framesPerBuffer> buf;
155
156 playbackState->getBuffer(
157 take(QSpan<float>{ buf },
158 playbackState->format().channelCount() * framesPerBuffer));
159 api->SetInterleavedBuffer(source->sourceId, buf.data(), source->nchannels,
160 framesPerBuffer);
161 } else {
162 api->SetInterleavedBuffer(source->sourceId, nullBuffer.data(), source->nchannels,
163 framesPerBuffer);
164 }
165 }
166
167 if (ambisonicDecoder && d->outputMode() == QAudioEngine::Surround) {
168 std::array<const float *, QAmbisonicDecoder::maxAmbisonicChannels> channels;
169 std::array<const float *, 2> reverbBuffers{};
170 int nFrames = d->resonanceAudio->getAmbisonicOutput(
171 channels.data(), reverbBuffers.data(), ambisonicDecoder->nInputChannels());
172
173 if (nFrames < 0) {
174 // If we get here, it means that resonanceAudio did not actually fill the buffer.
175 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
176 // In this case we just fill the buffer with silence.
177 std::fill(outputBuffer.begin(), outputBuffer.end(), 0);
178 break;
179 }
180
181 Q_ASSERT(ambisonicDecoder->nOutputChannels() <= 8);
182 int nSamples = ambisonicDecoder->outputSamples(nFrames);
183
184 constexpr size_t reverbBufferSize =
185 framesPerBuffer * QAmbisonicDecoder::maxAmbisonicChannels;
186 std::array<float, reverbBufferSize> reverbFloatBuffers;
187 QSpan<float> reverbOutputSpan = take(QSpan{ reverbFloatBuffers }, nSamples);
188 QSpan<short> currentOutput = take(outputBuffer, nSamples);
189
190 ambisonicDecoder->processBufferWithReverb(
191 QSpan{ channels.data(), ambisonicDecoder->nInputChannels() }, reverbBuffers,
192 reverbOutputSpan);
193
194 convertSampleFormat(as_bytes(reverbOutputSpan), NativeSampleFormat::float32_t,
195 as_writable_bytes(currentOutput), NativeSampleFormat::int16_t);
196 outputBuffer = drop(outputBuffer, nSamples);
197 } else {
198 QSpan<short> currentOutput = take(outputBuffer, nChannels * framesPerBuffer);
199 ok = d->resonanceAudio->api->FillInterleavedOutputBuffer(2, framesPerBuffer,
200 currentOutput.data());
201 if (!ok) {
202 // If we get here, it means that resonanceAudio did not actually fill the buffer.
203 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
204 // In this case we just fill the buffer with silence.
205 if (d->playbackStates.empty()) {
206 std::fill(currentOutput.begin(), currentOutput.end(), 0);
207 } else {
208 // If we get here, it means that something unexpected happened, so bail.
209 qWarning() << " Reading failed!";
210 break;
211 }
212 }
213 outputBuffer = drop(outputBuffer, nChannels * framesPerBuffer);
214 }
215 }
216
217 qint64 bytesProcessed = len - outputBuffer.size_bytes();
218 m_pos += bytesProcessed;
219 return bytesProcessed;
220}
221
223{
224 audioThread.setObjectName(u"QAudioThread");
225 m_device = QMediaDevices::defaultAudioOutput();
226}
227
228QAudioEngineThreaded::~QAudioEngineThreaded()
229{
230 stop();
231}
232
234{
235 if (outputStream)
236 return; // already started
237
238 outputStream = std::make_unique<QAudioOutputStream>(this);
239 outputStream->moveToThread(&audioThread);
240 audioThread.start(QThread::TimeCriticalPriority);
241
242 QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::startOutput);
243}
244
246{
247 if (!outputStream)
248 return; // already stopped
249
250 QMetaObject::invokeMethod(outputStream.get(), &QAudioOutputStream::stopOutput,
251 Qt::BlockingQueuedConnection);
252 outputStream.reset();
253 audioThread.exit(0);
254 audioThread.wait();
255}
256
258{
259 if (!outputStream)
260 return; // can't pause if not started
261
262 bool old = m_paused.fetchAndStoreRelaxed(paused);
263 if (old != paused) {
264 if (outputStream)
265 outputStream->setPaused(paused);
266 Q_Q(QAudioEngine);
267 emit q->pausedChanged();
268 }
269}
270
272{
273 return m_paused.loadRelaxed();
274}
275
276void QAudioEngineThreaded::setOutputDevice(const QAudioDevice &device)
277{
278 if (m_device == device)
279 return;
280 if (outputStream) {
281 qWarning() << "Changing device on a running engine not implemented";
282 return;
283 }
284 m_device = device;
285 Q_Q(QAudioEngine);
286 emit q->outputDeviceChanged();
287}
288
289void QAudioEngineThreaded::addSound(QSpatialAudioSoundPrivate *sound)
290{
291 std::lock_guard l(mutex);
292 playbackStates.emplace(sound, nullptr);
293}
294
295void QAudioEngineThreaded::removeSound(QSpatialAudioSoundPrivate *sound)
296{
297 std::lock_guard l(mutex);
298 playbackStates.erase(sound);
299}
300
301void QAudioEngineThreaded::setSoundPlaybackData(QSpatialAudioSoundPrivate *sound,
302 SharedPlaybackState state)
303{
304 std::lock_guard l(mutex);
305 playbackStates.insert_or_assign(sound, std::move(state));
306}
307
309{
310 std::lock_guard l(mutex);
311 for (auto [sound, key] : playbackStates)
312 sound->updateRoomEffects();
313}
314
315QT_END_NAMESPACE
std::shared_ptr< QSpatialAudioPrivate::QSpatialAudioPlaybackState > SharedPlaybackState
void setPaused(bool paused) override
void setOutputDevice(const QAudioDevice &device) override
bool isPaused() const override
void addSound(QSpatialAudioSoundPrivate *) override
void setSoundPlaybackData(QSpatialAudioSoundPrivate *, SharedPlaybackState) override
void removeSound(QSpatialAudioSoundPrivate *) 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.
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE const int bufferTimeMs