summaryrefslogtreecommitdiffstats
path: root/src/multimedia/spatial/qaudioengine.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/multimedia/spatial/qaudioengine.cpp')
-rw-r--r--src/multimedia/spatial/qaudioengine.cpp623
1 files changed, 623 insertions, 0 deletions
diff --git a/src/multimedia/spatial/qaudioengine.cpp b/src/multimedia/spatial/qaudioengine.cpp
new file mode 100644
index 000000000..db3bfc80e
--- /dev/null
+++ b/src/multimedia/spatial/qaudioengine.cpp
@@ -0,0 +1,623 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Multimedia module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL-NOGPL2$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include <qaudioengine_p.h>
+#include <qspatialsound_p.h>
+#include <qambientsound.h>
+#include <qaudioroom_p.h>
+#include <qaudiolistener.h>
+#include <resonance_audio_api_extensions.h>
+#include <qambisonicdecoder_p.h>
+#include <qaudiodecoder.h>
+#include <qmediadevices.h>
+#include <qiodevice.h>
+#include <qaudiosink.h>
+#include <qdebug.h>
+#include <qelapsedtimer.h>
+
+QT_BEGIN_NAMESPACE
+
+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() {
+ QMutexLocker l(&d->mutex);
+ Q_ASSERT(!sink);
+ d->ambisonicDecoder.reset(new QAmbisonicDecoder(QAmbisonicDecoder::HighQuality, d->format));
+ sink.reset(new QAudioSink(d->device, d->format));
+ sink->setBufferSize(16384);
+ sink->start(this);
+ }
+
+ Q_INVOKABLE void stopOutput() {
+ sink->stop();
+ sink.reset();
+ d->ambisonicDecoder.reset();
+ delete this;
+ }
+
+ void setPaused(bool paused) {
+ if (paused)
+ sink->suspend();
+ else
+ sink->resume();
+ }
+
+private:
+ qint64 m_pos = 0;
+ QAudioEnginePrivate *d = nullptr;
+ std::unique_ptr<QAudioSink> sink;
+};
+
+
+QAudioOutputStream::~QAudioOutputStream()
+{
+}
+
+qint64 QAudioOutputStream::writeData(const char *, qint64)
+{
+ return 0;
+}
+
+qint64 QAudioOutputStream::readData(char *data, qint64 len)
+{
+ if (d->paused.loadRelaxed())
+ return 0;
+
+ d->updateRooms();
+
+ int nChannels = d->ambisonicDecoder ? d->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 : qAsConst(d->sources)) {
+ auto *sp = QSpatialSoundPrivate::get(source);
+ float buf[QAudioEnginePrivate::bufferSize];
+ sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 1);
+ d->api->SetInterleavedBuffer(sp->sourceId, buf, 1, QAudioEnginePrivate::bufferSize);
+ }
+ for (auto *source : qAsConst(d->stereoSources)) {
+ auto *sp = QAmbientSoundPrivate::get(source);
+ float buf[2*QAudioEnginePrivate::bufferSize];
+ sp->getBuffer(buf, QAudioEnginePrivate::bufferSize, 2);
+ d->api->SetInterleavedBuffer(sp->sourceId, buf, 2, QAudioEnginePrivate::bufferSize);
+ }
+
+ if (d->ambisonicDecoder && d->outputMode == QAudioEngine::Normal && d->format.channelCount() != 2) {
+ const float *channels[QAmbisonicDecoder::maxAmbisonicChannels];
+ int nSamples = vraudio::getAmbisonicOutput(d->api, channels, d->ambisonicDecoder->nInputChannels());
+ Q_ASSERT(d->ambisonicDecoder->nOutputChannels() <= 8);
+ d->ambisonicDecoder->processBuffer(channels, fd, nSamples);
+ } else {
+ ok = d->api->FillInterleavedOutputBuffer(2, QAudioEnginePrivate::bufferSize, fd);
+ if (!ok) {
+ 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 api;
+}
+
+void QAudioEnginePrivate::addSpatialSound(QSpatialSound *sound)
+{
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ sd->sourceId = api->CreateSoundObjectSource(vraudio::kBinauralHighQuality);
+ sources.append(sound);
+}
+
+void QAudioEnginePrivate::removeSpatialSound(QSpatialSound *sound)
+{
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ api->DestroySource(sd->sourceId);
+ sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
+ sources.removeOne(sound);
+}
+
+void QAudioEnginePrivate::addStereoSound(QAmbientSound *sound)
+{
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ sd->sourceId = api->CreateStereoSource(2);
+ stereoSources.append(sound);
+}
+
+void QAudioEnginePrivate::removeStereoSound(QAmbientSound *sound)
+{
+ QAmbientSoundPrivate *sd = QAmbientSoundPrivate::get(sound);
+
+ api->DestroySource(sd->sourceId);
+ sd->sourceId = vraudio::ResonanceAudioApi::kInvalidSourceId;
+ stereoSources.removeOne(sound);
+}
+
+void QAudioEnginePrivate::addRoom(QAudioRoom *room)
+{
+ rooms.append(room);
+}
+
+void QAudioEnginePrivate::removeRoom(QAudioRoom *room)
+{
+ rooms.removeOne(room);
+}
+
+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 it's room effects
+ for (auto *r : qAsConst(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;
+ currentRoom = room;
+
+ if (!roomDirty)
+ return;
+
+ // apply room to engine
+ if (!currentRoom) {
+ api->EnableRoomEffects(false);
+ return;
+ }
+ QAudioRoomPrivate *rp = QAudioRoomPrivate::get(room);
+ api->SetReflectionProperties(rp->reflections);
+ api->SetReverbProperties(rp->reverb);
+
+ // update room effects for all sound sources
+ for (auto *s : qAsConst(sources)) {
+ auto *sp = QSpatialSoundPrivate::get(s);
+ sp->updateRoomEffects();
+ }
+}
+
+QVector3D QAudioEnginePrivate::listenerPosition() const
+{
+ return listener ? listener->position() : QVector3D();
+}
+
+
+/*!
+ \class QAudioEngine
+ \inmodule QtMultimedia
+ \ingroup multimedia_spatialaudio
+
+ \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 amd emulate room properties and reverb settings emulating
+ different types of rooms.
+
+ Sound sources can also be occluded dampening the sound coming from those sources.
+
+*/
+
+/*!
+ Constructs a spatial audio engine with \a parent.
+
+ The engine will operate with a sample rate given by \a sampleRate. 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(QObject *parent, int sampleRate)
+ : QObject(parent)
+ , d(new QAudioEnginePrivate)
+{
+ d->sampleRate = sampleRate;
+ d->api = vraudio::CreateResonanceAudioApi(2, QAudioEnginePrivate::bufferSize, d->sampleRate);
+}
+
+/*!
+ Destroys the spatial audio engine.
+ */
+QAudioEngine::~QAudioEngine()
+{
+ stop();
+ delete d;
+}
+
+/*! \enum QAudioEngine::OutputMode
+ \value Normal Map the sounds to the loudspeaker configuration of the output device.
+ This is normally a stereo or surround speaker setup.
+ \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->api) {
+ d->api->SetStereoSpeakerMode(mode == Normal);
+ }
+ 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->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->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->format.setChannelCount(2);
+ d->format.setSampleRate(d->sampleRate);
+ d->format.setSampleFormat(QAudioFormat::Int16);
+
+ d->api->SetStereoSpeakerMode(d->outputMode == Normal);
+ d->api->SetMasterVolume(d->masterVolume);
+
+ d->outputStream.reset(new QAudioOutputStream(d));
+ d->outputStream->moveToThread(&d->audioThread);
+ d->audioThread.start();
+
+ 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->api;
+ d->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;
+}
+
+/*!
+ 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;
+}
+
+
+void QAmbientSoundPrivate::load()
+{
+ decoder.reset(new QAudioDecoder);
+ buffers.clear();
+ currentBuffer = 0;
+ bufPos = 0;
+ m_playing = false;
+ m_loading = true;
+ auto *ep = QAudioEnginePrivate::get(engine);
+ QAudioFormat f = ep->format;
+ f.setSampleFormat(QAudioFormat::Float);
+ f.setChannelConfig(nchannels == 2 ? QAudioFormat::ChannelConfigStereo : QAudioFormat::ChannelConfigMono);
+ decoder->setAudioFormat(f);
+ decoder->setSource(url);
+
+ connect(decoder.get(), &QAudioDecoder::bufferReady, this, &QAmbientSoundPrivate::bufferReady);
+ connect(decoder.get(), &QAudioDecoder::finished, this, &QAmbientSoundPrivate::finished);
+ decoder->start();
+}
+
+void QAmbientSoundPrivate::getBuffer(float *buf, int nframes, int channels)
+{
+ Q_ASSERT(channels == nchannels);
+ QMutexLocker l(&mutex);
+ if (!m_playing || currentBuffer >= buffers.size()) {
+ memset(buf, 0, nframes*sizeof(float));
+ } else {
+ int frames = nframes;
+ float *ff = buf;
+ while (frames) {
+ const QAudioBuffer &b = buffers.at(currentBuffer);
+// qDebug() << s << b.format().sampleRate() << b.format().channelCount() << b.format().sampleFormat();
+ auto *f = b.constData<float>() + bufPos*nchannels;
+ int toCopy = qMin(b.frameCount() - bufPos, frames);
+ memcpy(ff, f, toCopy*sizeof(float)*nchannels);
+ ff += toCopy*nchannels;
+ frames -= toCopy;
+ bufPos += toCopy;
+ Q_ASSERT(bufPos <= b.frameCount());
+ if (bufPos == b.frameCount()) {
+ ++currentBuffer;
+ bufPos = 0;
+ }
+ if (!m_loading) {
+ if (currentBuffer == buffers.size()) {
+ currentBuffer = 0;
+ ++m_currentLoop;
+ }
+ if (m_loops > 0 && m_currentLoop >= m_loops) {
+ m_playing = false;
+ m_currentLoop = 0;
+ }
+ }
+ }
+ Q_ASSERT(ff - buf == channels*nframes);
+ }
+}
+
+void QAmbientSoundPrivate::bufferReady()
+{
+ QMutexLocker l(&mutex);
+ auto b = decoder->read();
+// qDebug() << "read buffer" << b.format() << b.startTime() << b.duration();
+ buffers.append(b);
+ if (m_autoPlay)
+ m_playing = true;
+}
+
+void QAmbientSoundPrivate::finished()
+{
+// qDebug() << "finished";
+ m_loading = false;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qaudioengine.cpp"
+#include "qaudioengine.moc"