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.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
3#include <qaudioengine_p.h>
4#include <qambientsound_p.h>
5#include <qspatialsound_p.h>
6#include <qambientsound.h>
7#include <qaudioroom_p.h>
8#include <qaudiolistener.h>
9#include <resonance_audio.h>
10#include <qambisonicdecoder_p.h>
11#include <qaudiodecoder.h>
12#include <qmediadevices.h>
13#include <qiodevice.h>
14#include <qaudiosink.h>
15#include <qdebug.h>
16#include <qelapsedtimer.h>
17
18#include <QFile>
19
21
22// We'd like to have short buffer times, so the sound adjusts itself to changes
23// quickly, but times below 100ms seem to give stuttering on macOS.
24// It might be possible to set this value lower on other OSes.
25const int bufferTimeMs = 100;
26
27// This class lives in the audioThread, but pulls data from QAudioEnginePrivate
28// which lives in the mainThread.
30{
32public:
39
40 qint64 readData(char *data, qint64 len) override;
41
42 qint64 writeData(const char *, qint64) override;
43
44 qint64 size() const override { return 0; }
45 qint64 bytesAvailable() const override {
46 return std::numeric_limits<qint64>::max();
47 }
48 bool isSequential() const override {
49 return true;
50 }
51 bool atEnd() const override {
52 return false;
53 }
54 qint64 pos() const override {
55 return m_pos;
56 }
57
59 d->mutex.lock();
60 Q_ASSERT(!sink);
61 QAudioFormat format;
62 auto channelConfig = d->outputMode == QAudioEngine::Surround ?
63 d->device.channelConfiguration() : QAudioFormat::ChannelConfigStereo;
64 if (channelConfig != QAudioFormat::ChannelConfigUnknown)
65 format.setChannelConfig(channelConfig);
66 else
67 format.setChannelCount(d->device.preferredFormat().channelCount());
70 ambisonicDecoder.reset(new QAmbisonicDecoder(QAmbisonicDecoder::HighQuality, format));
71 sink.reset(new QAudioSink(d->device, format));
72 const qsizetype bufferSize = format.bytesForDuration(bufferTimeMs * 1000);
73 sink->setBufferSize(bufferSize);
74 d->mutex.unlock();
75 // It is important to unlock the mutex before starting the sink, as the sink will
76 // call readData() in the audio thread, which will try to lock the mutex (again)
77 sink->start(this);
78 }
79
81 if (!sink)
82 return;
83 sink->stop();
84 sink.reset();
85 ambisonicDecoder.reset();
86 }
87
89 stopOutput();
90 startOutput();
91 }
92
93 void setPaused(bool paused) {
94 if (paused)
95 sink->suspend();
96 else
97 sink->resume();
98 }
99
100private:
101 qint64 m_pos = 0;
102 QAudioEnginePrivate *d = nullptr;
103 std::unique_ptr<QAudioSink> sink;
104 std::unique_ptr<QAmbisonicDecoder> ambisonicDecoder;
105};
106
107
108QAudioOutputStream::~QAudioOutputStream()
109{
110}
111
113{
114 return 0;
115}
116
117qint64 QAudioOutputStream::readData(char *data, qint64 len)
118{
119 if (d->paused.loadRelaxed())
120 return 0;
121
122 QMutexLocker l(&d->mutex);
124
125 int nChannels = ambisonicDecoder ? ambisonicDecoder->nOutputChannels() : 2;
126 if (len < nChannels*int(sizeof(float))*QAudioEnginePrivate::bufferSize)
127 return 0;
128
129 short *fd = (short *)data;
130 qint64 frames = len / nChannels / sizeof(short);
131 bool ok = true;
132 while (frames >= qint64(QAudioEnginePrivate::bufferSize)) {
133 // Fill input buffers
134 for (auto *source : std::as_const(d->sources)) {
135 auto *sp = QSpatialSoundPrivate::get(source);
136 if (!sp)
137 continue;
138 float buf[QAudioEnginePrivate::bufferSize];
139 sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 1);
140 d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf, 1, QAudioEnginePrivate::bufferSize);
141 }
142 for (auto *source : std::as_const(d->stereoSources)) {
143 auto *sp = QAmbientSoundPrivate::get(source);
144 if (!sp)
145 continue;
146 float buf[2*QAudioEnginePrivate::bufferSize];
147 sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 2);
148 d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf, 2, QAudioEnginePrivate::bufferSize);
149 }
150
151 if (ambisonicDecoder && d->outputMode == QAudioEngine::Surround) {
152 const float *channels[QAmbisonicDecoder::maxAmbisonicChannels];
153 const float *reverbBuffers[2];
154 int nSamples = d->resonanceAudio->getAmbisonicOutput(channels, reverbBuffers, ambisonicDecoder->nInputChannels());
155 Q_ASSERT(ambisonicDecoder->nOutputChannels() <= 8);
156 ambisonicDecoder->processBufferWithReverb(channels, reverbBuffers, fd, nSamples);
157 } else {
158 ok = d->resonanceAudio->api->FillInterleavedOutputBuffer(2, QAudioEnginePrivate::bufferSize, fd);
159 if (!ok) {
160 // If we get here, it means that resonanceAudio did not actually fill the buffer.
161 // Sometimes this is expected, for example if resonanceAudio does not have any sources.
162 // In this case we just fill the buffer with silence.
163 if (d->sources.isEmpty() && d->stereoSources.isEmpty()) {
164 memset(fd, 0, nChannels * QAudioEnginePrivate::bufferSize * sizeof(short));
165 } else {
166 // If we get here, it means that something unexpected happened, so bail.
167 qWarning() << " Reading failed!";
168 break;
169 }
170 }
171 }
172 fd += nChannels*QAudioEnginePrivate::bufferSize;
174 }
175 const int bytesProcessed = ((char *)fd - data);
176 m_pos += bytesProcessed;
177 return bytesProcessed;
178}
179
180
182{
183 audioThread.setObjectName(u"QAudioThread");
184 device = QMediaDevices::defaultAudioOutput();
185}
186
191
192void QAudioEnginePrivate::addSpatialSound(QSpatialSound *sound)
193{
194 QMutexLocker l(&mutex);
196
197 sd->sourceId = resonanceAudio->api->CreateSoundObjectSource(vraudio::kBinauralHighQuality);
198 sources.append(sound);
199}
200
201void QAudioEnginePrivate::removeSpatialSound(QSpatialSound *sound)
202{
203 QMutexLocker l(&mutex);
205
206 resonanceAudio->api->DestroySource(sd->sourceId);
207 sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
208 sources.removeOne(sound);
209}
210
211void QAudioEnginePrivate::addStereoSound(QAmbientSound *sound)
212{
213 QMutexLocker l(&mutex);
215
216 sd->sourceId = resonanceAudio->api->CreateStereoSource(2);
217 stereoSources.append(sound);
218}
219
220void QAudioEnginePrivate::removeStereoSound(QAmbientSound *sound)
221{
222 QMutexLocker l(&mutex);
224
225 resonanceAudio->api->DestroySource(sd->sourceId);
226 sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
227 stereoSources.removeOne(sound);
228}
229
230void QAudioEnginePrivate::addRoom(QAudioRoom *room)
231{
232 QMutexLocker l(&mutex);
233 rooms.append(room);
234}
235
236void QAudioEnginePrivate::removeRoom(QAudioRoom *room)
237{
238 QMutexLocker l(&mutex);
239 rooms.removeOne(room);
240}
241
242// This method is called from the audio thread
244{
246 return;
247
248 bool needUpdate = listenerPositionDirty;
249 listenerPositionDirty = false;
250
251 bool roomDirty = false;
252 for (const auto &room : std::as_const(rooms)) {
253 auto *rd = QAudioRoomPrivate::get(room);
254 if (rd->dirty) {
255 roomDirty = true;
256 rd->update();
257 needUpdate = true;
258 }
259 }
260
261 if (!needUpdate)
262 return;
263
264 QVector3D listenerPos = listenerPosition();
265 float roomVolume = float(qInf());
266 QAudioRoom *room = nullptr;
267 // Find the smallest room that contains the listener and apply its room effects
268 for (auto *r : std::as_const(rooms)) {
269 QVector3D dim2 = r->dimensions()/2.;
270 float vol = dim2.x()*dim2.y()*dim2.z();
271 if (vol > roomVolume)
272 continue;
273 QVector3D dist = r->position() - listenerPos;
274 // transform into room coordinates
275 dist = r->rotation().rotatedVector(dist);
276 if (qAbs(dist.x()) <= dim2.x() &&
277 qAbs(dist.y()) <= dim2.y() &&
278 qAbs(dist.z()) <= dim2.z()) {
279 room = r;
280 roomVolume = vol;
281 }
282 }
283 if (room != currentRoom)
284 roomDirty = true;
285 const bool previousRoom = currentRoom;
286 currentRoom = room;
287
288 if (!roomDirty)
289 return;
290
291 // apply room to engine
292 if (!currentRoom) {
293 resonanceAudio->api->EnableRoomEffects(false);
294 return;
295 }
296 if (!previousRoom)
297 resonanceAudio->api->EnableRoomEffects(true);
298
299 QAudioRoomPrivate *rp = QAudioRoomPrivate::get(room);
300 resonanceAudio->api->SetReflectionProperties(rp->reflections);
301 resonanceAudio->api->SetReverbProperties(rp->reverb);
302
303 // update room effects for all sound sources
304 for (auto *s : std::as_const(sources)) {
305 auto *sp = QSpatialSoundPrivate::get(s);
306 if (!sp)
307 continue;
308 sp->updateRoomEffects();
309 }
310}
311
313{
314 return listener ? listener->position() : QVector3D();
315}
316
317
318/*!
319 \class QAudioEngine
320 \inmodule QtSpatialAudio
321 \ingroup spatialaudio
322 \ingroup multimedia_audio
323
324 \brief QAudioEngine manages a three dimensional sound field.
325
326 You can use an instance of QAudioEngine to manage a sound field in
327 three dimensions. A sound field is defined by several QSpatialSound
328 objects that define a sound at a specified location in 3D space. You can also
329 add stereo overlays using QAmbientSound.
330
331 You can use QAudioListener to define the position of the person listening
332 to the sound field relative to the sound sources. Sound sources will be less audible
333 if the listener is further away from source. They will also get mapped to the corresponding
334 loudspeakers depending on the direction between listener and source.
335
336 QAudioEngine offers two output modes. The first mode renders the sound field to a set of
337 speakers, either a stereo speaker pair or a surround configuration. The second mode provides
338 an immersive 3D sound experience when using headphones.
339
340 Perception of sound localization is driven mainly by two factors. The first factor is timing
341 differences of the sound waves between left and right ear. The second factor comes from various
342 ways how sounds coming from different direcations create different types of reflections from our
343 ears and heads. See https://en.wikipedia.org/wiki/Sound_localization for more details.
344
345 The spatial audio engine emulates those timing differences and reflections through
346 Head related transfer functions (HRTF, see
347 https://en.wikipedia.org/wiki/Head-related_transfer_function). The functions used emulates those
348 effects for an average persons ears and head. It provides a good and immersive 3D sound localization
349 experience for most persons when using headphones.
350
351 The engine is rather versatile allowing you to define room properties and reverb settings to emulate
352 different types of rooms.
353
354 Sound sources can also be occluded dampening the sound coming from those sources.
355
356 The audio engine uses a coordinate system that is in centimeters by default. The axes are aligned with the
357 typical coordinate system used in 3D. Positive x points to the right, positive y points up and positive z points
358 backwards.
359
360*/
361
362/*!
363 \fn QAudioEngine::QAudioEngine()
364 \fn QAudioEngine::QAudioEngine(QObject *parent)
365 \fn QAudioEngine::QAudioEngine(int sampleRate, QObject *parent = nullptr)
366
367 Constructs a spatial audio engine with \a parent, if any.
368
369 The engine will operate with a sample rate given by \a sampleRate. The
370 default sample rate, if none is provided, is 44100 (44.1kHz).
371
372 Sound content that is not provided at that sample rate will automatically
373 get resampled to \a sampleRate when being processed by the engine. The
374 default sample rate is fine in most cases, but you can define a different
375 rate if most of your sound files are sampled with a different rate, and
376 avoid some CPU overhead for resampling.
377 */
378QAudioEngine::QAudioEngine(int sampleRate, QObject *parent)
379 : QObject(parent)
380 , d(new QAudioEnginePrivate)
381{
382 d->sampleRate = sampleRate;
383 d->resonanceAudio = new vraudio::ResonanceAudio(2, QAudioEnginePrivate::bufferSize, d->sampleRate);
384}
385
386/*!
387 Destroys the spatial audio engine.
388 */
389QAudioEngine::~QAudioEngine()
390{
391 stop();
392 delete d;
393}
394
395/*! \enum QAudioEngine::OutputMode
396 \value Surround Map the sounds to the loudspeaker configuration of the output device.
397 This is normally a stereo or surround speaker setup.
398 \value Stereo Map the sounds to the stereo loudspeaker configuration of the output device.
399 This will ignore any additional speakers and only use the left and right channels
400 to create a stero rendering of the sound field.
401 \value Headphone Use Headphone spatialization to create a 3D audio effect when listening
402 to the sound field through headphones
403*/
404
405/*!
406 \property QAudioEngine::outputMode
407
408 Sets or retrieves the current output mode of the engine.
409
410 \sa QAudioEngine::OutputMode
411 */
412void QAudioEngine::setOutputMode(OutputMode mode)
413{
414 if (d->outputMode == mode)
415 return;
416 d->outputMode = mode;
417 if (d->resonanceAudio->api)
418 d->resonanceAudio->api->SetStereoSpeakerMode(mode != Headphone);
419
420 QMetaObject::invokeMethod(d->outputStream.get(), "restartOutput", Qt::BlockingQueuedConnection);
421
422 emit outputModeChanged();
423}
424
425QAudioEngine::OutputMode QAudioEngine::outputMode() const
426{
427 return d->outputMode;
428}
429
430/*!
431 Returns the sample rate the engine has been configured with.
432 */
433int QAudioEngine::sampleRate() const
434{
435 return d->sampleRate;
436}
437
438/*!
439 \property QAudioEngine::outputDevice
440
441 Sets or returns the device that is being used for playing the sound field.
442 */
443void QAudioEngine::setOutputDevice(const QAudioDevice &device)
444{
445 if (d->device == device)
446 return;
447 if (d->resonanceAudio->api) {
448 qWarning() << "Changing device on a running engine not implemented";
449 return;
450 }
451 d->device = device;
452 emit outputDeviceChanged();
453}
454
455QAudioDevice QAudioEngine::outputDevice() const
456{
457 return d->device;
458}
459
460/*!
461 \property QAudioEngine::masterVolume
462
463 Sets or returns volume being used to render the sound field.
464 */
465void QAudioEngine::setMasterVolume(float volume)
466{
467 if (d->masterVolume == volume)
468 return;
469 d->masterVolume = volume;
470 d->resonanceAudio->api->SetMasterVolume(volume);
471 emit masterVolumeChanged();
472}
473
474float QAudioEngine::masterVolume() const
475{
476 return d->masterVolume;
477}
478
479/*!
480 Starts the engine.
481 */
482void QAudioEngine::start()
483{
484 if (d->outputStream)
485 // already started
486 return;
487
488 d->resonanceAudio->api->SetStereoSpeakerMode(d->outputMode != Headphone);
489 d->resonanceAudio->api->SetMasterVolume(d->masterVolume);
490
491 d->outputStream.reset(new QAudioOutputStream(d));
492 d->outputStream->moveToThread(&d->audioThread);
493 d->audioThread.start(QThread::TimeCriticalPriority);
494
495 QMetaObject::invokeMethod(d->outputStream.get(), "startOutput");
496}
497
498/*!
499 Stops the engine.
500 */
501void QAudioEngine::stop()
502{
503 QMetaObject::invokeMethod(d->outputStream.get(), "stopOutput", Qt::BlockingQueuedConnection);
504 d->outputStream.reset();
505 d->audioThread.exit(0);
506 d->audioThread.wait();
507 delete d->resonanceAudio->api;
508 d->resonanceAudio->api = nullptr;
509}
510
511/*!
512 \property QAudioEngine::paused
513
514 Pauses the spatial audio engine.
515 */
516void QAudioEngine::setPaused(bool paused)
517{
518 bool old = d->paused.fetchAndStoreRelaxed(paused);
519 if (old != paused) {
520 if (d->outputStream)
521 d->outputStream->setPaused(paused);
522 emit pausedChanged();
523 }
524}
525
526bool QAudioEngine::paused() const
527{
528 return d->paused.loadRelaxed();
529}
530
531/*!
532 Enables room effects such as echos and reverb.
533
534 Enables room effects if \a enabled is true.
535 Room effects will only apply if you create one or more \l QAudioRoom objects
536 and the listener is inside at least one of the rooms. If the listener is inside
537 multiple rooms, the room with the smallest volume will be used.
538 */
539void QAudioEngine::setRoomEffectsEnabled(bool enabled)
540{
541 if (d->roomEffectsEnabled == enabled)
542 return;
543 d->roomEffectsEnabled = enabled;
544 d->resonanceAudio->roomEffectsEnabled = enabled;
545}
546
547/*!
548 Returns true if room effects are enabled.
549 */
550bool QAudioEngine::roomEffectsEnabled() const
551{
552 return d->roomEffectsEnabled;
553}
554
555/*!
556 \property QAudioEngine::distanceScale
557
558 Defines the scale of the coordinate system being used by the spatial audio engine.
559 By default, all units are in centimeters, in line with the default units being
560 used by Qt Quick 3D.
561
562 Set the distance scale to QAudioEngine::DistanceScaleMeter to get units in meters.
563*/
564void QAudioEngine::setDistanceScale(float scale)
565{
566 // multiply with 100, to get the conversion to meters that resonance audio uses
567 scale /= 100.f;
568 if (scale <= 0.0f) {
569 qWarning() << "QAudioEngine: Invalid distance scale.";
570 return;
571 }
572 if (scale == d->distanceScale)
573 return;
574 d->distanceScale = scale;
575 emit distanceScaleChanged();
576}
577
578float QAudioEngine::distanceScale() const
579{
580 return d->distanceScale*100.f;
581}
582
583/*!
584 \fn void QAudioEngine::pause()
585
586 Pauses playback.
587*/
588/*!
589 \fn void QAudioEngine::resume()
590
591 Resumes playback.
592*/
593/*!
594 \variable QAudioEngine::DistanceScaleCentimeter
595 \internal
596*/
597/*!
598 \variable QAudioEngine::DistanceScaleMeter
599 \internal
600*/
601
602QT_END_NAMESPACE
603
604#include "moc_qaudioengine.cpp"
605#include "qaudioengine.moc"
static constexpr int maxAmbisonicChannels
vraudio::ResonanceAudio * resonanceAudio
void addRoom(QAudioRoom *room)
void addStereoSound(QAmbientSound *sound)
QAudioRoom * currentRoom
QVector3D listenerPosition() const
void removeRoom(QAudioRoom *room)
void addSpatialSound(QSpatialSound *sound)
void removeStereoSound(QAmbientSound *sound)
void removeSpatialSound(QSpatialSound *sound)
QAudioListener * listener
static constexpr int bufferSize
The QAudioFormat class stores audio stream parameter information.
Q_MULTIMEDIA_EXPORT void setChannelConfig(ChannelConfig config) noexcept
Sets the channel configuration to config.
constexpr void setChannelCount(int channelCount) noexcept
Sets the channel count to channels.
constexpr void setSampleFormat(SampleFormat f) noexcept
Sets the sample format to format.
constexpr void setSampleRate(int sampleRate) noexcept
Sets the sample rate to samplerate in Hertz.
void setPaused(bool paused)
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...
Q_INVOKABLE void restartOutput()
bool atEnd() const override
Returns true if the current read and write position is at the end of the device (i....
Q_INVOKABLE void stopOutput()
qint64 pos() const override
For random-access devices, this function returns the position that data is written to or read from.
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.
Q_INVOKABLE void startOutput()
qint64 size() const override
For open random-access devices, this function returns the size of the device.
\inmodule QtCore
Definition qmutex.h:332
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE const int bufferTimeMs