summaryrefslogtreecommitdiffstats
path: root/src/spatialaudio/qaudioengine.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/spatialaudio/qaudioengine.cpp')
-rw-r--r--src/spatialaudio/qaudioengine.cpp602
1 files changed, 602 insertions, 0 deletions
diff --git a/src/spatialaudio/qaudioengine.cpp b/src/spatialaudio/qaudioengine.cpp
new file mode 100644
index 000000000..82228f72f
--- /dev/null
+++ b/src/spatialaudio/qaudioengine.cpp
@@ -0,0 +1,602 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
+#include <qaudioengine_p.h>
+#include <qambientsound_p.h>
+#include <qspatialsound_p.h>
+#include <qambientsound.h>
+#include <qaudioroom_p.h>
+#include <qaudiolistener.h>
+#include <resonance_audio.h>
+#include <qambisonicdecoder_p.h>
+#include <qaudiodecoder.h>
+#include <qmediadevices.h>
+#include <qiodevice.h>
+#include <qaudiosink.h>
+#include <qdebug.h>
+#include <qelapsedtimer.h>
+
+#include <QFile>
+
+QT_BEGIN_NAMESPACE
+
+// We'd like to have short buffer times, so the sound adjusts itself to changes
+// quickly, but times below 100ms seem to give stuttering on macOS.
+// It might be possible to set this value lower on other OSes.
+const int bufferTimeMs = 100;
+
+// This class lives in the audioThread, but pulls data from QAudioEnginePrivate
+// which lives in the mainThread.
+class QAudioOutputStream : public QIODevice
+{
+ Q_OBJECT
+public:
+ explicit QAudioOutputStream(QAudioEnginePrivate *d)
+ : d(d)
+ {
+ open(QIODevice::ReadOnly);
+ }
+ ~QAudioOutputStream();
+
+ qint64 readData(char *data, qint64 len) override;
+
+ qint64 writeData(const char *, qint64) override;
+
+ qint64 size() const override { return 0; }
+ qint64 bytesAvailable() const override {
+ return std::numeric_limits<qint64>::max();
+ }
+ bool isSequential() const override {
+ return true;
+ }
+ bool atEnd() const override {
+ return false;
+ }
+ qint64 pos() const override {
+ return m_pos;
+ }
+
+ Q_INVOKABLE void startOutput() {
+ d->mutex.lock();
+ Q_ASSERT(!sink);
+ QAudioFormat format;
+ auto channelConfig = d->outputMode == QAudioEngine::Surround ?
+ d->device.channelConfiguration() : QAudioFormat::ChannelConfigStereo;
+ if (channelConfig != QAudioFormat::ChannelConfigUnknown)
+ format.setChannelConfig(channelConfig);
+ else
+ format.setChannelCount(d->device.preferredFormat().channelCount());
+ format.setSampleRate(d->sampleRate);
+ format.setSampleFormat(QAudioFormat::Int16);
+ ambisonicDecoder.reset(new QAmbisonicDecoder(QAmbisonicDecoder::HighQuality, format));
+ sink.reset(new QAudioSink(d->device, format));
+ const qsizetype bufferSize = format.bytesForDuration(bufferTimeMs * 1000);
+ sink->setBufferSize(bufferSize);
+ d->mutex.unlock();
+ // It is important to unlock the mutex before starting the sink, as the sink will
+ // call readData() in the audio thread, which will try to lock the mutex (again)
+ sink->start(this);
+ }
+
+ Q_INVOKABLE void stopOutput() {
+ sink->stop();
+ sink.reset();
+ ambisonicDecoder.reset();
+ }
+
+ Q_INVOKABLE void restartOutput() {
+ stopOutput();
+ startOutput();
+ }
+
+ void setPaused(bool paused) {
+ if (paused)
+ sink->suspend();
+ else
+ sink->resume();
+ }
+
+private:
+ qint64 m_pos = 0;
+ QAudioEnginePrivate *d = nullptr;
+ std::unique_ptr<QAudioSink> sink;
+ std::unique_ptr<QAmbisonicDecoder> ambisonicDecoder;
+};
+
+
+QAudioOutputStream::~QAudioOutputStream()
+{
+}
+
+qint64 QAudioOutputStream::writeData(const char *, qint64)
+{
+ return 0;
+}
+
+qint64 QAudioOutputStream::readData(char *data, qint64 len)
+{
+ if (d->paused.loadRelaxed())
+ return 0;
+
+ QMutexLocker l(&d->mutex);
+ d->updateRooms();
+
+ int nChannels = ambisonicDecoder ? ambisonicDecoder->nOutputChannels() : 2;
+ if (len < nChannels*int(sizeof(float))*QAudioEnginePrivate::bufferSize)
+ return 0;
+
+ short *fd = (short *)data;
+ qint64 frames = len / nChannels / sizeof(short);
+ bool ok = true;
+ while (frames >= qint64(QAudioEnginePrivate::bufferSize)) {
+ // Fill input buffers
+ for (auto *source : std::as_const(d->sources)) {
+ auto *sp = QSpatialSoundPrivate::get(source);
+ if (!sp)
+ continue;
+ float buf[QAudioEnginePrivate::bufferSize];
+ sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 1);
+ d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf, 1, QAudioEnginePrivate::bufferSize);
+ }
+ for (auto *source : std::as_const(d->stereoSources)) {
+ auto *sp = QAmbientSoundPrivate::get(source);
+ if (!sp)
+ continue;
+ float buf[2*QAudioEnginePrivate::bufferSize];
+ sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 2);
+ d->resonanceAudio->api->SetInterleavedBuffer(sp->sourceId, buf, 2, QAudioEnginePrivate::bufferSize);
+ }
+
+ if (ambisonicDecoder && d->outputMode == QAudioEngine::Surround) {
+ const float *channels[QAmbisonicDecoder::maxAmbisonicChannels];
+ const float *reverbBuffers[2];
+ int nSamples = d->resonanceAudio->getAmbisonicOutput(channels, reverbBuffers, ambisonicDecoder->nInputChannels());
+ Q_ASSERT(ambisonicDecoder->nOutputChannels() <= 8);
+ ambisonicDecoder->processBufferWithReverb(channels, reverbBuffers, fd, nSamples);
+ } else {
+ ok = d->resonanceAudio->api->FillInterleavedOutputBuffer(2, QAudioEnginePrivate::bufferSize, fd);
+ if (!ok) {
+ // If we get here, it means that resonanceAudio did not actually fill the buffer.
+ // Sometimes this is expected, for example if resonanceAudio does not have any sources.
+ // In this case we just fill the buffer with silence.
+ if (d->sources.isEmpty() && d->stereoSources.isEmpty()) {
+ memset(fd, 0, nChannels * QAudioEnginePrivate::bufferSize * sizeof(short));
+ } else {
+ // If we get here, it means that something unexpected happened, so bail.
+ qWarning() << " Reading failed!";
+ break;
+ }
+ }
+ }
+ fd += nChannels*QAudioEnginePrivate::bufferSize;
+ frames -= QAudioEnginePrivate::bufferSize;
+ }
+ const int bytesProcessed = ((char *)fd - data);
+ m_pos += bytesProcessed;
+ return bytesProcessed;
+}
+
+
+QAudioEnginePrivate::QAudioEnginePrivate()
+{
+ device = QMediaDevices::defaultAudioOutput();
+}
+
+QAudioEnginePrivate::~QAudioEnginePrivate()
+{
+ delete resonanceAudio;
+}
+
+void QAudioEnginePrivate::addSpatialSound(QSpatialSound *sound)
+{
+ QMutexLocker l(&mutex);
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ sd->sourceId = resonanceAudio->api->CreateSoundObjectSource(vraudio::kBinauralHighQuality);
+ sources.append(sound);
+}
+
+void QAudioEnginePrivate::removeSpatialSound(QSpatialSound *sound)
+{
+ QMutexLocker l(&mutex);
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ resonanceAudio->api->DestroySource(sd->sourceId);
+ sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
+ sources.removeOne(sound);
+}
+
+void QAudioEnginePrivate::addStereoSound(QAmbientSound *sound)
+{
+ QMutexLocker l(&mutex);
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ sd->sourceId = resonanceAudio->api->CreateStereoSource(2);
+ stereoSources.append(sound);
+}
+
+void QAudioEnginePrivate::removeStereoSound(QAmbientSound *sound)
+{
+ QMutexLocker l(&mutex);
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ resonanceAudio->api->DestroySource(sd->sourceId);
+ sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
+ stereoSources.removeOne(sound);
+}
+
+void QAudioEnginePrivate::addRoom(QAudioRoom *room)
+{
+ QMutexLocker l(&mutex);
+ rooms.append(room);
+}
+
+void QAudioEnginePrivate::removeRoom(QAudioRoom *room)
+{
+ QMutexLocker l(&mutex);
+ rooms.removeOne(room);
+}
+
+// This method is called from the audio thread
+void QAudioEnginePrivate::updateRooms()
+{
+ if (!roomEffectsEnabled)
+ return;
+
+ bool needUpdate = listenerPositionDirty;
+ listenerPositionDirty = false;
+
+ bool roomDirty = false;
+ for (const auto &room : rooms) {
+ auto *rd = QAudioRoomPrivate::get(room);
+ if (rd->dirty) {
+ roomDirty = true;
+ rd->update();
+ needUpdate = true;
+ }
+ }
+
+ if (!needUpdate)
+ return;
+
+ QVector3D listenerPos = listenerPosition();
+ float roomVolume = float(qInf());
+ QAudioRoom *room = nullptr;
+ // Find the smallest room that contains the listener and apply its room effects
+ for (auto *r : std::as_const(rooms)) {
+ QVector3D dim2 = r->dimensions()/2.;
+ float vol = dim2.x()*dim2.y()*dim2.z();
+ if (vol > roomVolume)
+ continue;
+ QVector3D dist = r->position() - listenerPos;
+ // transform into room coordinates
+ dist = r->rotation().rotatedVector(dist);
+ if (qAbs(dist.x()) <= dim2.x() &&
+ qAbs(dist.y()) <= dim2.y() &&
+ qAbs(dist.z()) <= dim2.z()) {
+ room = r;
+ roomVolume = vol;
+ }
+ }
+ if (room != currentRoom)
+ roomDirty = true;
+ const bool previousRoom = currentRoom;
+ currentRoom = room;
+
+ if (!roomDirty)
+ return;
+
+ // apply room to engine
+ if (!currentRoom) {
+ resonanceAudio->api->EnableRoomEffects(false);
+ return;
+ }
+ if (!previousRoom)
+ resonanceAudio->api->EnableRoomEffects(true);
+
+ QAudioRoomPrivate *rp = QAudioRoomPrivate::get(room);
+ resonanceAudio->api->SetReflectionProperties(rp->reflections);
+ resonanceAudio->api->SetReverbProperties(rp->reverb);
+
+ // update room effects for all sound sources
+ for (auto *s : std::as_const(sources)) {
+ auto *sp = QSpatialSoundPrivate::get(s);
+ if (!sp)
+ continue;
+ sp->updateRoomEffects();
+ }
+}
+
+QVector3D QAudioEnginePrivate::listenerPosition() const
+{
+ return listener ? listener->position() : QVector3D();
+}
+
+
+/*!
+ \class QAudioEngine
+ \inmodule QtSpatialAudio
+ \ingroup spatialaudio
+ \ingroup multimedia_audio
+
+ \brief QAudioEngine manages a three dimensional sound field.
+
+ You can use an instance of QAudioEngine to manage a sound field in
+ three dimensions. A sound field is defined by several QSpatialSound
+ objects that define a sound at a specified location in 3D space. You can also
+ add stereo overlays using QAmbientSound.
+
+ You can use QAudioListener to define the position of the person listening
+ to the sound field relative to the sound sources. Sound sources will be less audible
+ if the listener is further away from source. They will also get mapped to the corresponding
+ loudspeakers depending on the direction between listener and source.
+
+ QAudioEngine offers two output modes. The first mode renders the sound field to a set of
+ speakers, either a stereo speaker pair or a surround configuration. The second mode provides
+ an immersive 3D sound experience when using headphones.
+
+ Perception of sound localization is driven mainly by two factors. The first factor is timing
+ differences of the sound waves between left and right ear. The second factor comes from various
+ ways how sounds coming from different direcations create different types of reflections from our
+ ears and heads. See https://en.wikipedia.org/wiki/Sound_localization for more details.
+
+ The spatial audio engine emulates those timing differences and reflections through
+ Head related transfer functions (HRTF, see
+ https://en.wikipedia.org/wiki/Head-related_transfer_function). The functions used emulates those
+ effects for an average persons ears and head. It provides a good and immersive 3D sound localization
+ experience for most persons when using headphones.
+
+ The engine is rather versatile allowing you to define room properties and reverb settings to emulate
+ different types of rooms.
+
+ Sound sources can also be occluded dampening the sound coming from those sources.
+
+ The audio engine uses a coordinate system that is in centimeters by default. The axes are aligned with the
+ typical coordinate system used in 3D. Positive x points to the right, positive y points up and positive z points
+ backwards.
+
+*/
+
+/*!
+ \fn QAudioEngine::QAudioEngine()
+ \fn QAudioEngine::QAudioEngine(QObject *parent)
+ \fn QAudioEngine::QAudioEngine(int sampleRate, QObject *parent = nullptr)
+
+ Constructs a spatial audio engine with \a parent, if any.
+
+ The engine will operate with a sample rate given by \a sampleRate. The
+ default sample rate, if none is provided, is 44100 (44.1kHz).
+
+ Sound content that is not provided at that sample rate will automatically
+ get resampled to \a sampleRate when being processed by the engine. The
+ default sample rate is fine in most cases, but you can define a different
+ rate if most of your sound files are sampled with a different rate, and
+ avoid some CPU overhead for resampling.
+ */
+QAudioEngine::QAudioEngine(int sampleRate, QObject *parent)
+ : QObject(parent)
+ , d(new QAudioEnginePrivate)
+{
+ d->sampleRate = sampleRate;
+ d->resonanceAudio = new vraudio::ResonanceAudio(2, QAudioEnginePrivate::bufferSize, d->sampleRate);
+}
+
+/*!
+ Destroys the spatial audio engine.
+ */
+QAudioEngine::~QAudioEngine()
+{
+ stop();
+ delete d;
+}
+
+/*! \enum QAudioEngine::OutputMode
+ \value Surround Map the sounds to the loudspeaker configuration of the output device.
+ This is normally a stereo or surround speaker setup.
+ \value Stereo Map the sounds to the stereo loudspeaker configuration of the output device.
+ This will ignore any additional speakers and only use the left and right channels
+ to create a stero rendering of the sound field.
+ \value Headphone Use Headphone spatialization to create a 3D audio effect when listening
+ to the sound field through headphones
+*/
+
+/*!
+ \property QAudioEngine::outputMode
+
+ Sets or retrieves the current output mode of the engine.
+
+ \sa QAudioEngine::OutputMode
+ */
+void QAudioEngine::setOutputMode(OutputMode mode)
+{
+ if (d->outputMode == mode)
+ return;
+ d->outputMode = mode;
+ if (d->resonanceAudio->api)
+ d->resonanceAudio->api->SetStereoSpeakerMode(mode != Headphone);
+
+ QMetaObject::invokeMethod(d->outputStream.get(), "restartOutput", Qt::BlockingQueuedConnection);
+
+ emit outputModeChanged();
+}
+
+QAudioEngine::OutputMode QAudioEngine::outputMode() const
+{
+ return d->outputMode;
+}
+
+/*!
+ Returns the sample rate the engine has been configured with.
+ */
+int QAudioEngine::sampleRate() const
+{
+ return d->sampleRate;
+}
+
+/*!
+ \property QAudioEngine::outputDevice
+
+ Sets or returns the device that is being used for playing the sound field.
+ */
+void QAudioEngine::setOutputDevice(const QAudioDevice &device)
+{
+ if (d->device == device)
+ return;
+ if (d->resonanceAudio->api) {
+ qWarning() << "Changing device on a running engine not implemented";
+ return;
+ }
+ d->device = device;
+ emit outputDeviceChanged();
+}
+
+QAudioDevice QAudioEngine::outputDevice() const
+{
+ return d->device;
+}
+
+/*!
+ \property QAudioEngine::masterVolume
+
+ Sets or returns volume being used to render the sound field.
+ */
+void QAudioEngine::setMasterVolume(float volume)
+{
+ if (d->masterVolume == volume)
+ return;
+ d->masterVolume = volume;
+ d->resonanceAudio->api->SetMasterVolume(volume);
+ emit masterVolumeChanged();
+}
+
+float QAudioEngine::masterVolume() const
+{
+ return d->masterVolume;
+}
+
+/*!
+ Starts the engine.
+ */
+void QAudioEngine::start()
+{
+ if (d->outputStream)
+ // already started
+ return;
+
+ d->resonanceAudio->api->SetStereoSpeakerMode(d->outputMode != Headphone);
+ d->resonanceAudio->api->SetMasterVolume(d->masterVolume);
+
+ d->outputStream.reset(new QAudioOutputStream(d));
+ d->outputStream->moveToThread(&d->audioThread);
+ d->audioThread.start(QThread::TimeCriticalPriority);
+
+ QMetaObject::invokeMethod(d->outputStream.get(), "startOutput");
+}
+
+/*!
+ Stops the engine.
+ */
+void QAudioEngine::stop()
+{
+ QMetaObject::invokeMethod(d->outputStream.get(), "stopOutput", Qt::BlockingQueuedConnection);
+ d->outputStream.reset();
+ d->audioThread.exit(0);
+ d->audioThread.wait();
+ delete d->resonanceAudio->api;
+ d->resonanceAudio->api = nullptr;
+}
+
+/*!
+ \property QAudioEngine::paused
+
+ Pauses the spatial audio engine.
+ */
+void QAudioEngine::setPaused(bool paused)
+{
+ bool old = d->paused.fetchAndStoreRelaxed(paused);
+ if (old != paused) {
+ if (d->outputStream)
+ d->outputStream->setPaused(paused);
+ emit pausedChanged();
+ }
+}
+
+bool QAudioEngine::paused() const
+{
+ return d->paused.loadRelaxed();
+}
+
+/*!
+ Enables room effects such as echos and reverb.
+
+ Enables room effects if \a enabled is true.
+ Room effects will only apply if you create one or more \l QAudioRoom objects
+ and the listener is inside at least one of the rooms. If the listener is inside
+ multiple rooms, the room with the smallest volume will be used.
+ */
+void QAudioEngine::setRoomEffectsEnabled(bool enabled)
+{
+ if (d->roomEffectsEnabled == enabled)
+ return;
+ d->roomEffectsEnabled = enabled;
+ d->resonanceAudio->roomEffectsEnabled = enabled;
+}
+
+/*!
+ Returns true if room effects are enabled.
+ */
+bool QAudioEngine::roomEffectsEnabled() const
+{
+ return d->roomEffectsEnabled;
+}
+
+/*!
+ \property QAudioEngine::distanceScale
+
+ Defines the scale of the coordinate system being used by the spatial audio engine.
+ By default, all units are in centimeters, in line with the default units being
+ used by Qt Quick 3D.
+
+ Set the distance scale to QAudioEngine::DistanceScaleMeter to get units in meters.
+*/
+void QAudioEngine::setDistanceScale(float scale)
+{
+ // multiply with 100, to get the conversion to meters that resonance audio uses
+ scale /= 100.f;
+ if (scale <= 0.0f) {
+ qWarning() << "QAudioEngine: Invalid distance scale.";
+ return;
+ }
+ if (scale == d->distanceScale)
+ return;
+ d->distanceScale = scale;
+ emit distanceScaleChanged();
+}
+
+float QAudioEngine::distanceScale() const
+{
+ return d->distanceScale*100.f;
+}
+
+/*!
+ \fn void QAudioEngine::pause()
+
+ Pauses playback.
+*/
+/*!
+ \fn void QAudioEngine::resume()
+
+ Resumes playback.
+*/
+/*!
+ \variable QAudioEngine::DistanceScaleCentimeter
+ \internal
+*/
+/*!
+ \variable QAudioEngine::DistanceScaleMeter
+ \internal
+*/
+
+QT_END_NAMESPACE
+
+#include "moc_qaudioengine.cpp"
+#include "qaudioengine.moc"