diff options
Diffstat (limited to 'src/multimedia/spatial/qspatialsound.cpp')
-rw-r--r-- | src/multimedia/spatial/qspatialsound.cpp | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/src/multimedia/spatial/qspatialsound.cpp b/src/multimedia/spatial/qspatialsound.cpp new file mode 100644 index 000000000..8614eaa3b --- /dev/null +++ b/src/multimedia/spatial/qspatialsound.cpp @@ -0,0 +1,616 @@ +/**************************************************************************** +** +** 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 "qaudioroom_p.h" +#include "qspatialsound_p.h" +#include "qaudiolistener.h" +#include "qaudioengine_p.h" +#include "api/resonance_audio_api.h" +#include <qaudiosink.h> +#include <qurl.h> +#include <qdebug.h> +#include <qaudiodecoder.h> + +QT_BEGIN_NAMESPACE + +/*! + \class QSpatialSound + \inmodule QtMultimedia + \ingroup multimedia_spatialaudio + + \brief A sound object in 3D space. + + QSpatialSound represents an audible object in 3D space. You can define + it's 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. + */ +QSpatialSound::QSpatialSound(QAudioEngine *engine) + : d(new QSpatialSoundPrivate(this)) +{ + setEngine(engine); +} + +/*! + Destroys the sound source. + */ +QSpatialSound::~QSpatialSound() +{ + setEngine(nullptr); +} + +/*! + \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) +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + pos *= ep->distanceScale; + d->pos = pos; + if (ep) + ep->api->SetSourcePosition(d->sourceId, pos.x(), pos.y(), pos.z()); + d->updateRoomEffects(); + emit positionChanged(); +} + +QVector3D QSpatialSound::position() const +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + return d->pos/ep->distanceScale; +} + +/*! + \property QSpatialSound::rotation + + Defines the orientation of the sound source in 3D space. + */ +void QSpatialSound::setRotation(const QQuaternion &q) +{ + d->rotation = q; + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSourceRotation(d->sourceId, q.x(), q.y(), q.z(), q.scalar()); + emit rotationChanged(); +} + +QQuaternion QSpatialSound::rotation() const +{ + 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) +{ + if (d->volume == volume) + return; + d->volume = volume; + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSourceVolume(d->sourceId, d->volume*d->wallDampening); + emit volumeChanged(); +} + +float QSpatialSound::volume() const +{ + return d->volume; +} + +/*! + \enum QSpatialSound::DistanceModel + + Defines how the volume of the sound scales with distance to the listener. + + \value DistanceModel_Logarithmic Volume decreases logarithmically with distance. + \value DistanceModel_Linear Volume decreases linearly with distance. + \value DistanceModel_ManualAttenutation 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) +{ + if (d->distanceModel == model) + return; + d->distanceModel = model; + + d->updateDistanceModel(); + emit distanceModelChanged(); +} + +void QSpatialSoundPrivate::updateDistanceModel() +{ + if (!engine || sourceId < 0) + return; + auto *ep = QAudioEnginePrivate::get(engine); + + vraudio::DistanceRolloffModel dm = vraudio::kLogarithmic; + switch (distanceModel) { + case QSpatialSound::DistanceModel_Linear: + dm = vraudio::kLinear; + break; + case QSpatialSound::DistanceModel_ManualAttenutation: + dm = vraudio::kNone; + break; + default: + break; + } + + ep->api->SetSourceDistanceModel(sourceId, dm, size, distanceCutoff); +} + +void QSpatialSoundPrivate::updateRoomEffects() +{ + if (!engine || sourceId < 0) + return; + auto *ep = QAudioEnginePrivate::get(engine); + if (!ep->currentRoom) + return; + auto *rp = QAudioRoomPrivate::get(ep->currentRoom); + + 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->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 = ep->listenerPosition() - 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; + QAudioRoom::Wall walls[3]; + 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; + float factors[3] = { 0., 0., 0. }; + 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->api->SetSourceRoomEffectsGain(sourceId, 0); + } + ep->api->SetSoundObjectOcclusionIntensity(sourceId, occlusionIntensity + wallOcclusion); + ep->api->SetSourceVolume(sourceId, volume*wallDampening); +} + +QSpatialSound::DistanceModel QSpatialSound::distanceModel() const +{ + 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) +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + size *= ep->distanceScale; + if (d->size == size) + return; + d->size = size; + + d->updateDistanceModel(); + emit sizeChanged(); +} + +float QSpatialSound::size() const +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + 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) +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + cutoff *= ep->distanceScale; + if (d->distanceCutoff == cutoff) + return; + d->distanceCutoff = cutoff; + + d->updateDistanceModel(); + emit distanceCutoffChanged(); +} + +float QSpatialSound::distanceCutoff() const +{ + auto *ep = QAudioEnginePrivate::get(d->engine); + return d->distanceCutoff/ep->distanceScale; +} + +/*! + \property QSpatialSound::manualAttenuation + + Defines a manual attenuation factor if \l distanceModel is set to + QSpatialSound::DistanceModel_ManualAttenutation. + */ +void QSpatialSound::setManualAttenuation(float attenuation) +{ + if (d->manualAttenuation == attenuation) + return; + d->manualAttenuation = attenuation; + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSourceDistanceAttenuation(d->sourceId, d->manualAttenuation); + emit manualAttenuationChanged(); +} + +float QSpatialSound::manualAttenuation() const +{ + 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) +{ + if (d->occlusionIntensity == occlusion) + return; + d->occlusionIntensity = occlusion; + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSoundObjectOcclusionIntensity(d->sourceId, d->occlusionIntensity + d->wallOcclusion); + emit occlusionIntensityChanged(); +} + +float QSpatialSound::occlusionIntensity() const +{ + 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) +{ + alpha = qBound(0., alpha, 1.); + if (alpha == d->directivity) + return; + d->directivity = alpha; + + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSoundObjectDirectivity(d->sourceId, d->directivity, d->directivityOrder); + + emit directivityChanged(); +} + +float QSpatialSound::directivity() const +{ + 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) +{ + order = qMax(order, 1.); + if (order == d->directivityOrder) + return; + d->directivityOrder = order; + + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSoundObjectDirectivity(d->sourceId, d->directivity, d->directivityOrder); + + emit directivityChanged(); +} + +float QSpatialSound::directivityOrder() const +{ + 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) +{ + gain = qBound(0., gain, 1.); + if (gain == d->nearFieldGain) + return; + d->nearFieldGain = gain; + + auto *ep = QAudioEnginePrivate::get(d->engine); + if (ep) + ep->api->SetSoundObjectNearFieldEffectGain(d->sourceId, d->nearFieldGain/9.); + + emit nearFieldGainChanged(); + +} + +float QSpatialSound::nearFieldGain() const +{ + return d->nearFieldGain; +} + +/*! + \property QSpatialSound::source + + The source file for the sound to be played. + */ +void QSpatialSound::setSource(const QUrl &url) +{ + if (d->url == url) + return; + d->url = url; + + d->load(); + emit sourceChanged(); +} + +QUrl QSpatialSound::source() const +{ + return d->url; +} + +/*! + \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 +{ + return d->m_loops.loadRelaxed(); +} + +void QSpatialSound::setLoops(int loops) +{ + int oldLoops = d->m_loops.fetchAndStoreRelaxed(loops); + if (oldLoops != loops) + 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 +{ + return d->m_autoPlay.loadRelaxed(); +} + +void QSpatialSound::setAutoPlay(bool autoPlay) +{ + bool old = d->m_autoPlay.fetchAndStoreRelaxed(autoPlay); + if (old != autoPlay) + emit autoPlayChanged(); +} + +/*! + Starts playing back the sound. Does nothing if the sound is already playing. + */ +void QSpatialSound::play() +{ + d->play(); +} + +/*! + Pauses sound playback. Calling play() will continue playback. + */ +void QSpatialSound::pause() +{ + 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() +{ + d->stop(); +} + +/*! + \internal + */ +void QSpatialSound::setEngine(QAudioEngine *engine) +{ + if (d->engine == engine) + return; + auto *ep = QAudioEnginePrivate::get(engine); + + if (ep) + ep->removeSpatialSound(this); + d->engine = engine; + + ep = QAudioEnginePrivate::get(engine); + if (ep) { + ep->addSpatialSound(this); + ep->api->SetSourcePosition(d->sourceId, d->pos.x(), d->pos.y(), d->pos.z()); + ep->api->SetSourceRotation(d->sourceId, d->rotation.x(), d->rotation.y(), d->rotation.z(), d->rotation.scalar()); + ep->api->SetSourceVolume(d->sourceId, d->volume); + ep->api->SetSoundObjectDirectivity(d->sourceId, d->directivity, d->directivityOrder); + ep->api->SetSoundObjectNearFieldEffectGain(d->sourceId, d->nearFieldGain); + d->updateDistanceModel(); + } +} + +/*! + Returns the engine associated with this listener. + */ +QAudioEngine *QSpatialSound::engine() const +{ + return d->engine; +} + +QT_END_NAMESPACE + +#include "moc_qspatialsound.cpp" |