Revision control
++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-3.0-only
++
++ #include "qspatialsound.h"
++
++ #include <QtSpatialAudio/qaudiolistener.h>
++ #include <QtSpatialAudio/private/qaudioengine_p.h>
++ #include <QtSpatialAudio/private/qaudioroom_p.h>
++ #include <QtSpatialAudio/private/qspatialaudiosound_p.h>
++ #include <QtMultimedia/qaudiosink.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qurl.h>
++
++ #include <array>
++
++ QT_BEGIN_NAMESPACE
++
++
++ namespace {
++
++ static int addSpatialSound(QAudioEngine *engine)
++ {
++ auto *ep = QAudioEnginePrivate::get(engine);
++ if (!ep)
++ return -1;
++ return ep->resonanceAudio->api->CreateSoundObjectSource(vraudio::kBinauralHighQuality);
++ }
++
++ } // namespace
++
++ class QSpatialSoundPrivate final : public QSpatialAudioSoundPrivate
++ {
++ Q_DECLARE_PUBLIC(QSpatialSound)
++
++ public:
++ explicit QSpatialSoundPrivate(QAudioEngine *engine);
++ ~QSpatialSoundPrivate();
++
++ static QSpatialSoundPrivate *get(QSpatialSound *soundSource)
++ {
++ return soundSource ? soundSource->d_func() : nullptr;
++ }
++
++ void applyVolume() override;
++
++ QVector3D pos;
++ QVector3D unscaledPosition;
++ QQuaternion rotation;
++ QSpatialSound::DistanceModel distanceModel = QSpatialSound::DistanceModel::Logarithmic;
++ float size = .1f;
++ float distanceCutoff = 50.f;
++ float manualAttenuation = 0.f;
++ float occlusionIntensity = 0.f;
++ float directivity = 0.f;
++ float directivityOrder = 1.f;
++ float nearFieldGain = 0.f;
++ float wallDampening = 1.f;
++ float wallOcclusion = 0.f;
++
++ void updateDistanceModel();
++ void updateRoomEffects() override;
++ };
++
++ QSpatialSoundPrivate::QSpatialSoundPrivate(QAudioEngine *engine)
++ : QSpatialAudioSoundPrivate{
++ engine,
++ 1,
++ addSpatialSound(engine),
++ }
++ {
++ withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
++ api->SetSourcePosition(sourceId, pos.x(), pos.y(), pos.z());
++ api->SetSourceRotation(sourceId, rotation.x(), rotation.y(), rotation.z(),
++ rotation.scalar());
++ api->SetSoundObjectDirectivity(sourceId, directivity, directivityOrder);
++ api->SetSoundObjectNearFieldEffectGain(sourceId, nearFieldGain);
++ applyVolume();
++ updateDistanceModel();
++ });
++ }
++
++ QSpatialSoundPrivate::~QSpatialSoundPrivate() = default;
++
++ void QSpatialSoundPrivate::applyVolume()
++ {
++ withResonanceApi([&](vraudio::ResonanceAudioApi *api) {
++ api->SetSourceVolume(sourceId, volume() * wallDampening);
++ });
++ }
++
++ void QSpatialSoundPrivate::updateDistanceModel()
++ {
++ if (!engine || sourceId < 0)
++ return;
++ auto *ep = QAudioEnginePrivate::get(engine);
++ Q_ASSERT(ep);
++
++ vraudio::DistanceRolloffModel dm = vraudio::kLogarithmic;
++ switch (distanceModel) {
++ case QSpatialSound::DistanceModel::Linear:
++ dm = vraudio::kLinear;
++ break;
++ case QSpatialSound::DistanceModel::ManualAttenuation:
++ dm = vraudio::kNone;
++ break;
++ default:
++ break;
++ }
++
++ ep->resonanceAudio->api->SetSourceDistanceModel(sourceId, dm, size, distanceCutoff);
++ }
++
++ void QSpatialSoundPrivate::updateRoomEffects()
++ {
++ if (!engine || sourceId < 0)
++ return;
++ auto *ep = QAudioEnginePrivate::get(engine);
++ Q_ASSERT(ep);
++
++ if (!ep->currentRoom())
++ return;
++ auto *rp = QAudioRoomPrivate::get(ep->currentRoom());
++ if (!rp)
++ return;
++
++ auto listenerPos = ep->listenerPosition();
++ if (!listenerPos)
++ return;
++
++ QVector3D roomDim2 = ep->currentRoom()->dimensions() / 2.;
++ QVector3D roomPos = ep->currentRoom()->position();
++ QQuaternion roomRot = ep->currentRoom()->rotation();
++ QVector3D dist = pos - roomPos;
++ // transform into room coordinates
++ dist = roomRot.rotatedVector(dist);
++ if (qAbs(dist.x()) <= roomDim2.x() &&
++ qAbs(dist.y()) <= roomDim2.y() &&
++ qAbs(dist.z()) <= roomDim2.z()) {
++ // Source is inside room, apply
++ ep->resonanceAudio->api->SetSourceRoomEffectsGain(sourceId, 1);
++ wallDampening = 1.;
++ wallOcclusion = 0.;
++ } else {
++ // ### calculate room occlusion and dampening
++ // This is a bit of heuristics on top of the heuristic dampening/occlusion numbers for walls
++ //
++ // We basically cast a ray from the listener through the walls. If walls have different characteristics
++ // and we get close to a corner, we try to use some averaging to avoid abrupt changes
++ auto relativeListenerPos = *listenerPos - roomPos;
++ relativeListenerPos = roomRot.rotatedVector(relativeListenerPos);
++
++ auto direction = dist.normalized();
++ enum {
++ X, Y, Z
++ };
++ // Very rough approximation, use the size of the source plus twice the size of our head.
++ // One could probably improve upon this.
++ const float transitionDistance = size + 0.4;
++ std::array<QAudioRoom::Wall, 3> walls;
++ walls[X] = direction.x() > 0 ? QAudioRoom::RightWall : QAudioRoom::LeftWall;
++ walls[Y] = direction.y() > 0 ? QAudioRoom::FrontWall : QAudioRoom::BackWall;
++ walls[Z] = direction.z() > 0 ? QAudioRoom::Ceiling : QAudioRoom::Floor;
++ std::array<float, 3> factors = {};
++ bool foundWall = false;
++ if (direction.x() != 0) {
++ float sign = direction.x() > 0 ? 1.f : -1.f;
++ float dx = sign * roomDim2.x() - relativeListenerPos.x();
++ QVector3D intersection = relativeListenerPos + direction*dx/direction.x();
++ float dy = roomDim2.y() - qAbs(intersection.y());
++ float dz = roomDim2.z() - qAbs(intersection.z());
++ if (dy > 0 && dz > 0) {
++ // qDebug() << "Hit with wall X" << walls[0] << dy << dz;
++ // Ray is hitting this wall
++ factors[Y] = qMax(0.f, 1.f/3.f - dy/transitionDistance);
++ factors[Z] = qMax(0.f, 1.f/3.f - dz/transitionDistance);
++ factors[X] = 1.f - factors[Y] - factors[Z];
++ foundWall = true;
++ }
++ }
++ if (!foundWall && direction.y() != 0) {
++ float sign = direction.y() > 0 ? 1.f : -1.f;
++ float dy = sign * roomDim2.y() - relativeListenerPos.y();
++ QVector3D intersection = relativeListenerPos + direction*dy/direction.y();
++ float dx = roomDim2.x() - qAbs(intersection.x());
++ float dz = roomDim2.z() - qAbs(intersection.z());
++ if (dx > 0 && dz > 0) {
++ // Ray is hitting this wall
++ // qDebug() << "Hit with wall Y" << walls[1] << dx << dy;
++ factors[X] = qMax(0.f, 1.f/3.f - dx/transitionDistance);
++ factors[Z] = qMax(0.f, 1.f/3.f - dz/transitionDistance);
++ factors[Y] = 1.f - factors[X] - factors[Z];
++ foundWall = true;
++ }
++ }
++ if (!foundWall) {
++ Q_ASSERT(direction.z() != 0);
++ float sign = direction.z() > 0 ? 1.f : -1.f;
++ float dz = sign * roomDim2.z() - relativeListenerPos.z();
++ QVector3D intersection = relativeListenerPos + direction*dz/direction.z();
++ float dx = roomDim2.x() - qAbs(intersection.x());
++ float dy = roomDim2.y() - qAbs(intersection.y());
++ if (dx > 0 && dy > 0) {
++ // Ray is hitting this wall
++ // qDebug() << "Hit with wall Z" << walls[2];
++ factors[X] = qMax(0.f, 1.f/3.f - dx/transitionDistance);
++ factors[Y] = qMax(0.f, 1.f/3.f - dy/transitionDistance);
++ factors[Z] = 1.f - factors[X] - factors[Y];
++ foundWall = true;
++ }
++ }
++ wallDampening = 0;
++ wallOcclusion = 0;
++ for (int i = 0; i < 3; ++i) {
++ wallDampening += factors[i]*rp->wallDampening(walls[i]);
++ wallOcclusion += factors[i]*rp->wallOcclusion(walls[i]);
++ }
++
++ // qDebug() << "intersection with wall" << walls[0] << walls[1] << walls[2] << factors[0] << factors[1] << factors[2] << wallDampening << wallOcclusion;
++ ep->resonanceAudio->api->SetSourceRoomEffectsGain(sourceId, 0);
++ }
++ ep->resonanceAudio->api->SetSoundObjectOcclusionIntensity(sourceId, occlusionIntensity + wallOcclusion);
++ ep->resonanceAudio->api->SetSourceVolume(sourceId, volume() * wallDampening);
++ }
++
++
++ /*!
++ \class QSpatialSound
++ \inmodule QtSpatialAudio
++ \ingroup spatialaudio
++ \ingroup multimedia_audio
++
++ \brief A sound object in 3D space.
++
++ QSpatialSound represents an audible object in 3D space. You can define
++ its position and orientation in space, set the sound it is playing and define a
++ volume for the object.
++
++ The object can have different attenuation behavior, emit sound mainly in one direction
++ or spherically, and behave as if occluded by some other object.
++ */
++
++ /*!
++ Creates a spatial sound source for \a engine. The object can be placed in
++ 3D space and will be louder the closer to the listener it is.
++
++ \note Must be called with a valid QAudioEngine
++ */
++ QSpatialSound::QSpatialSound(QAudioEngine *engine) : QObject(*new QSpatialSoundPrivate(engine))
++ {
++ if (!engine)
++ qWarning() << "Cannot create QSpatialSound without a valid QAudioEngine";
++ }
++
++ /*!
++ Destroys the sound source.
++ */
++ QSpatialSound::~QSpatialSound()
++ {
++ Q_D(QSpatialSound);
++ if (d->state() != QSpatialAudioSoundPrivate::State::Stopped)
++ d->stop();
++ }
++
++ /*!
++ \property QSpatialSound::position
++
++ Defines the position of the sound source in 3D space. Units are in centimeters
++ by default.
++
++ \sa QAudioEngine::distanceScale
++ */
++ void QSpatialSound::setPosition(QVector3D pos)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ if (pos == d->unscaledPosition)
++ return;
++
++ d->unscaledPosition = pos;
++ pos *= ep->distanceScale();
++ d->pos = pos;
++ ep->resonanceAudio->api->SetSourcePosition(d->sourceId, pos.x(), pos.y(), pos.z());
++ Q_EMIT positionChanged();
++ }
++
++ QVector3D QSpatialSound::position() const
++ {
++ Q_D(const QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return {};
++
++ return d->pos / ep->distanceScale();
++ }
++
++ /*!
++ \property QSpatialSound::rotation
++
++ Defines the orientation of the sound source in 3D space.
++ */
++ void QSpatialSound::setRotation(const QQuaternion &q)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ if (d->rotation == q)
++ return;
++
++ d->rotation = q;
++ ep->resonanceAudio->api->SetSourceRotation(d->sourceId, q.x(), q.y(), q.z(), q.scalar());
++ Q_EMIT rotationChanged();
++ }
++
++ QQuaternion QSpatialSound::rotation() const
++ {
++ Q_D(const QSpatialSound);
++ return d->rotation;
++ }
++
++ /*!
++ \property QSpatialSound::volume
++
++ Defines the volume of the sound.
++
++ Values between 0 and 1 will attenuate the sound, while values above 1
++ provide an additional gain boost.
++ */
++ void QSpatialSound::setVolume(float volume)
++ {
++ Q_D(QSpatialSound);
++ if (volume != d->volume()) {
++ d->setVolume(volume);
++ Q_EMIT volumeChanged();
++ }
++ }
++
++ float QSpatialSound::volume() const
++ {
++ Q_D(const QSpatialSound);
++ return d->volume();
++ }
++
++ /*!
++ \enum QSpatialSound::DistanceModel
++
++ Defines how the volume of the sound scales with distance to the listener.
++
++ \value Logarithmic Volume decreases logarithmically with distance.
++ \value Linear Volume decreases linearly with distance.
++ \value ManualAttenuation Attenuation is defined manually using the
++ \l manualAttenuation property.
++ */
++
++ /*!
++ \property QSpatialSound::distanceModel
++
++ Defines distance model for this sound source. The volume starts scaling down
++ from \l size to \l distanceCutoff. The volume is constant for distances smaller
++ than size and zero for distances larger than the cutoff distance.
++
++ \sa QSpatialSound::DistanceModel
++ */
++ void QSpatialSound::setDistanceModel(DistanceModel model)
++ {
++ Q_D(QSpatialSound);
++
++ if (d->distanceModel == model)
++ return;
++ d->distanceModel = model;
++
++ d->updateDistanceModel();
++ Q_EMIT distanceModelChanged();
++ }
++
++ QSpatialSound::DistanceModel QSpatialSound::distanceModel() const
++ {
++ Q_D(const QSpatialSound);
++ return d->distanceModel;
++ }
++
++ /*!
++ \property QSpatialSound::size
++
++ Defines the size of the sound source. If the listener is closer to the sound
++ object than the size, volume will stay constant. The size is also used to for
++ occlusion calculations, where large sources can be partially occluded by a wall.
++ */
++ void QSpatialSound::setSize(float size)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ size *= ep->distanceScale();
++ if (d->size == size)
++ return;
++ d->size = size;
++
++ d->updateDistanceModel();
++ Q_EMIT sizeChanged();
++ }
++
++ float QSpatialSound::size() const
++ {
++ Q_D(const QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return {};
++
++ return d->size / ep->distanceScale();
++ }
++
++ /*!
++ \property QSpatialSound::distanceCutoff
++
++ Defines a distance beyond which sound coming from the source will cutoff.
++ If the listener is further away from the sound object than the cutoff
++ distance it won't be audible anymore.
++ */
++ void QSpatialSound::setDistanceCutoff(float cutoff)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ cutoff *= ep->distanceScale();
++ if (d->distanceCutoff == cutoff)
++ return;
++ d->distanceCutoff = cutoff;
++
++ d->updateDistanceModel();
++ Q_EMIT distanceCutoffChanged();
++ }
++
++ float QSpatialSound::distanceCutoff() const
++ {
++ Q_D(const QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return {};
++
++ return d->distanceCutoff / ep->distanceScale();
++ }
++
++ /*!
++ \property QSpatialSound::manualAttenuation
++
++ Defines a manual attenuation factor if \l distanceModel is set to
++ QSpatialSound::DistanceModel::ManualAttenuation.
++ */
++ void QSpatialSound::setManualAttenuation(float attenuation)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ if (d->manualAttenuation == attenuation)
++ return;
++ d->manualAttenuation = attenuation;
++ ep->resonanceAudio->api->SetSourceDistanceAttenuation(d->sourceId, d->manualAttenuation);
++ Q_EMIT manualAttenuationChanged();
++ }
++
++ float QSpatialSound::manualAttenuation() const
++ {
++ Q_D(const QSpatialSound);
++ return d->manualAttenuation;
++ }
++
++ /*!
++ \property QSpatialSound::occlusionIntensity
++
++ Defines how much the object is occluded. 0 implies the object is
++ not occluded at all, 1 implies the sound source is fully occluded by
++ another object.
++
++ A fully occluded object will still be audible, but especially higher
++ frequencies will be dampened. In addition, the object will still
++ participate in generating reverb and reflections in the room.
++
++ Values larger than 1 are possible to further dampen the direct
++ sound coming from the source.
++
++ The default is 0.
++ */
++ void QSpatialSound::setOcclusionIntensity(float occlusion)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ if (d->occlusionIntensity == occlusion)
++ return;
++ d->occlusionIntensity = occlusion;
++ ep->resonanceAudio->api->SetSoundObjectOcclusionIntensity(d->sourceId, d->occlusionIntensity + d->wallOcclusion);
++ Q_EMIT occlusionIntensityChanged();
++ }
++
++ float QSpatialSound::occlusionIntensity() const
++ {
++ Q_D(const QSpatialSound);
++
++ return d->occlusionIntensity;
++ }
++
++ /*!
++ \property QSpatialSound::directivity
++
++ Defines the directivity of the sound source. A value of 0 implies that the sound is
++ emitted equally in all directions, while a value of 1 implies that the source mainly
++ emits sound in the forward direction.
++
++ Valid values are between 0 and 1, the default is 0.
++ */
++ void QSpatialSound::setDirectivity(float alpha)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ alpha = qBound(0., alpha, 1.);
++ if (alpha == d->directivity)
++ return;
++ d->directivity = alpha;
++
++ ep->resonanceAudio->api->SetSoundObjectDirectivity(d->sourceId, d->directivity, d->directivityOrder);
++
++ Q_EMIT directivityChanged();
++ }
++
++ float QSpatialSound::directivity() const
++ {
++ Q_D(const QSpatialSound);
++ return d->directivity;
++ }
++
++ /*!
++ \property QSpatialSound::directivityOrder
++
++ Defines the order of the directivity of the sound source. A higher order
++ implies a sharper localization of the sound cone.
++
++ The minimum value and default for this property is 1.
++ */
++ void QSpatialSound::setDirectivityOrder(float order)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ order = qMax(order, 1.);
++ if (order == d->directivityOrder)
++ return;
++ d->directivityOrder = order;
++
++ ep->resonanceAudio->api->SetSoundObjectDirectivity(d->sourceId, d->directivity, d->directivityOrder);
++
++ Q_EMIT directivityOrderChanged();
++ }
++
++ float QSpatialSound::directivityOrder() const
++ {
++ Q_D(const QSpatialSound);
++
++ return d->directivityOrder;
++ }
++
++ /*!
++ \property QSpatialSound::nearFieldGain
++
++ Defines the near field gain for the sound source. Valid values are between 0 and 1.
++ A near field gain of 1 will raise the volume of the sound signal by approx 20 dB for
++ distances very close to the listener.
++ */
++ void QSpatialSound::setNearFieldGain(float gain)
++ {
++ Q_D(QSpatialSound);
++ auto *ep = QAudioEnginePrivate::get(d->engine);
++ if (!ep)
++ return;
++
++ gain = qBound(0., gain, 1.);
++ if (gain == d->nearFieldGain)
++ return;
++ d->nearFieldGain = gain;
++
++ ep->resonanceAudio->api->SetSoundObjectNearFieldEffectGain(d->sourceId, d->nearFieldGain*9.f);
++
++ Q_EMIT nearFieldGainChanged();
++
++ }
++
++ float QSpatialSound::nearFieldGain() const
++ {
++ Q_D(const QSpatialSound);
++
++ return d->nearFieldGain;
++ }
++
++ /*!
++ \property QSpatialSound::source
++
++ The source file for the sound to be played.
++ */
++ void QSpatialSound::setSource(const QUrl &url)
++ {
++ Q_D(QSpatialSound);
++
++ if (d->url() == url)
++ return;
++ d->loadUrl(url);
++
++ Q_EMIT sourceChanged();
++ }
++
++ QUrl QSpatialSound::source() const
++ {
++ Q_D(const QSpatialSound);
++
++ return d->url();
++ }
++
++ /*!
++ \enum QSpatialSound::Loops
++
++ Lets you control the sound playback loop using the following values:
++
++ \value Infinite Playback infinitely
++ \value Once Playback once
++ */
++ /*!
++ \property QSpatialSound::loops
++
++ Determines how many times the sound is played before the player stops.
++ Set to QSpatialSound::Infinite to play the current sound in a loop forever.
++
++ The default value is \c 1.
++ */
++ int QSpatialSound::loops() const
++ {
++ Q_D(const QSpatialSound);
++ return d->loops();
++ }
++
++ void QSpatialSound::setLoops(int loops)
++ {
++ Q_D(QSpatialSound);
++ if (loops != d->loops()) {
++ d->setLoops(loops);
++ Q_EMIT loopsChanged();
++ }
++ }
++
++ /*!
++ \property QSpatialSound::autoPlay
++
++ Determines whether the sound should automatically start playing when a source
++ gets specified.
++
++ The default value is \c true.
++ */
++ bool QSpatialSound::autoPlay() const
++ {
++ Q_D(const QSpatialSound);
++ return d->autoPlay();
++ }
++
++ void QSpatialSound::setAutoPlay(bool autoPlay)
++ {
++ Q_D(QSpatialSound);
++ if (autoPlay != d->autoPlay()) {
++ d->setAutoPlay(autoPlay);
++ Q_EMIT autoPlayChanged();
++ }
++ }
++
++ /*!
++ Starts playing back the sound. Does nothing if the sound is already playing.
++ */
++ void QSpatialSound::play()
++ {
++ Q_D(QSpatialSound);
++
++ d->play();
++ }
++
++ /*!
++ Pauses sound playback. Calling play() will continue playback.
++ */
++ void QSpatialSound::pause()
++ {
++ Q_D(QSpatialSound);
++
++ d->pause();
++ }
++
++ /*!
++ Stops sound playback and resets the current position and current loop count to 0.
++ Calling play() will start playback at the beginning of the sound file.
++ */
++ void QSpatialSound::stop()
++ {
++ Q_D(QSpatialSound);
++
++ d->stop();
++ }
++
++ /*!
++ Returns the engine associated with this listener.
++ */
++ QAudioEngine *QSpatialSound::engine() const
++ {
++ Q_D(const QSpatialSound);
++
++ return d->engine;
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qspatialsound.cpp"