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
125static constexpr std::array<float, 2 * QAudioEngineThreaded::framesPerBuffer> nullBuffer{};
126
127qint64 QAudioOutputStream::readData(char *data, const qint64 len)
128{
129 if (d->m_paused.loadRelaxed())
130 return 0;
131
132 constexpr auto framesPerBuffer = QAudioEngineThreaded::framesPerBuffer;
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.
static constexpr std::array< float, 2 *QAudioEngineThreaded::framesPerBuffer > nullBuffer
QT_BEGIN_NAMESPACE const int bufferTimeMs