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