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