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 "qaudioroom_p.h"
4#include "qspatialsound_p.h"
5#include "qaudiolistener.h"
6#include "qaudioengine_p.h"
7#include "resonance_audio.h"
8#include <qaudiosink.h>
9#include <qurl.h>
10#include <qdebug.h>
11#include <qaudiodecoder.h>
12
13QT_BEGIN_NAMESPACE
14
15/*!
16 \class QSpatialSound
17 \inmodule QtSpatialAudio
18 \ingroup spatialaudio
19 \ingroup multimedia_audio
20
21 \brief A sound object in 3D space.
22
23 QSpatialSound represents an audible object in 3D space. You can define
24 its position and orientation in space, set the sound it is playing and define a
25 volume for the object.
26
27 The object can have different attenuation behavior, emit sound mainly in one direction
28 or spherically, and behave as if occluded by some other object.
29 */
30
31/*!
32 Creates a spatial sound source for \a engine. The object can be placed in
33 3D space and will be louder the closer to the listener it is.
34 */
35QSpatialSound::QSpatialSound(QAudioEngine *engine)
36 : d(new QSpatialSoundPrivate(this))
37{
38 setEngine(engine);
39}
40
41/*!
42 Destroys the sound source.
43 */
44QSpatialSound::~QSpatialSound()
45{
46 setEngine(nullptr);
47}
48
49/*!
50 \property QSpatialSound::position
51
52 Defines the position of the sound source in 3D space. Units are in centimeters
53 by default.
54
55 \sa QAudioEngine::distanceScale
56 */
57void QSpatialSound::setPosition(QVector3D pos)
58{
59 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
60 pos *= ep->distanceScale;
61 d->pos = pos;
62 if (ep)
63 ep->resonanceAudio->api->SetSourcePosition(source_id: d->sourceId, x: pos.x(), y: pos.y(), z: pos.z());
64 d->updateRoomEffects();
65 emit positionChanged();
66}
67
68QVector3D QSpatialSound::position() const
69{
70 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
71 return d->pos/ep->distanceScale;
72}
73
74/*!
75 \property QSpatialSound::rotation
76
77 Defines the orientation of the sound source in 3D space.
78 */
79void QSpatialSound::setRotation(const QQuaternion &q)
80{
81 d->rotation = q;
82 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
83 if (ep)
84 ep->resonanceAudio->api->SetSourceRotation(source_id: d->sourceId, x: q.x(), y: q.y(), z: q.z(), w: q.scalar());
85 emit rotationChanged();
86}
87
88QQuaternion QSpatialSound::rotation() const
89{
90 return d->rotation;
91}
92
93/*!
94 \property QSpatialSound::volume
95
96 Defines the volume of the sound.
97
98 Values between 0 and 1 will attenuate the sound, while values above 1
99 provide an additional gain boost.
100 */
101void QSpatialSound::setVolume(float volume)
102{
103 if (d->volume == volume)
104 return;
105 d->volume = volume;
106 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
107 if (ep)
108 ep->resonanceAudio->api->SetSourceVolume(source_id: d->sourceId, volume: d->volume*d->wallDampening);
109 emit volumeChanged();
110}
111
112float QSpatialSound::volume() const
113{
114 return d->volume;
115}
116
117/*!
118 \enum QSpatialSound::DistanceModel
119
120 Defines how the volume of the sound scales with distance to the listener.
121
122 \value Logarithmic Volume decreases logarithmically with distance.
123 \value Linear Volume decreases linearly with distance.
124 \value ManualAttenuation Attenuation is defined manually using the
125 \l manualAttenuation property.
126*/
127
128/*!
129 \property QSpatialSound::distanceModel
130
131 Defines distance model for this sound source. The volume starts scaling down
132 from \l size to \l distanceCutoff. The volume is constant for distances smaller
133 than size and zero for distances larger than the cutoff distance.
134
135 \sa QSpatialSound::DistanceModel
136 */
137void QSpatialSound::setDistanceModel(DistanceModel model)
138{
139 if (d->distanceModel == model)
140 return;
141 d->distanceModel = model;
142
143 d->updateDistanceModel();
144 emit distanceModelChanged();
145}
146
147void QSpatialSoundPrivate::updateDistanceModel()
148{
149 if (!engine || sourceId < 0)
150 return;
151 auto *ep = QAudioEnginePrivate::get(engine);
152
153 vraudio::DistanceRolloffModel dm = vraudio::kLogarithmic;
154 switch (distanceModel) {
155 case QSpatialSound::DistanceModel::Linear:
156 dm = vraudio::kLinear;
157 break;
158 case QSpatialSound::DistanceModel::ManualAttenuation:
159 dm = vraudio::kNone;
160 break;
161 default:
162 break;
163 }
164
165 ep->resonanceAudio->api->SetSourceDistanceModel(source_id: sourceId, rolloff: dm, min_distance: size, max_distance: distanceCutoff);
166}
167
168void QSpatialSoundPrivate::updateRoomEffects()
169{
170 if (!engine || sourceId < 0)
171 return;
172 auto *ep = QAudioEnginePrivate::get(engine);
173 if (!ep->currentRoom)
174 return;
175 auto *rp = QAudioRoomPrivate::get(r: ep->currentRoom);
176
177 QVector3D roomDim2 = ep->currentRoom->dimensions()/2.;
178 QVector3D roomPos = ep->currentRoom->position();
179 QQuaternion roomRot = ep->currentRoom->rotation();
180 QVector3D dist = pos - roomPos;
181 // transform into room coordinates
182 dist = roomRot.rotatedVector(vector: dist);
183 if (qAbs(t: dist.x()) <= roomDim2.x() &&
184 qAbs(t: dist.y()) <= roomDim2.y() &&
185 qAbs(t: dist.z()) <= roomDim2.z()) {
186 // Source is inside room, apply
187 ep->resonanceAudio->api->SetSourceRoomEffectsGain(source_id: sourceId, room_effects_gain: 1);
188 wallDampening = 1.;
189 wallOcclusion = 0.;
190 } else {
191 // ### calculate room occlusion and dampening
192 // This is a bit of heuristics on top of the heuristic dampening/occlusion numbers for walls
193 //
194 // We basically cast a ray from the listener through the walls. If walls have different characteristics
195 // and we get close to a corner, we try to use some averaging to avoid abrupt changes
196 auto relativeListenerPos = ep->listenerPosition() - roomPos;
197 relativeListenerPos = roomRot.rotatedVector(vector: relativeListenerPos);
198
199 auto direction = dist.normalized();
200 enum {
201 X, Y, Z
202 };
203 // Very rough approximation, use the size of the source plus twice the size of our head.
204 // One could probably improve upon this.
205 const float transitionDistance = size + 0.4;
206 QAudioRoom::Wall walls[3];
207 walls[X] = direction.x() > 0 ? QAudioRoom::RightWall : QAudioRoom::LeftWall;
208 walls[Y] = direction.y() > 0 ? QAudioRoom::FrontWall : QAudioRoom::BackWall;
209 walls[Z] = direction.z() > 0 ? QAudioRoom::Ceiling : QAudioRoom::Floor;
210 float factors[3] = { 0., 0., 0. };
211 bool foundWall = false;
212 if (direction.x() != 0) {
213 float sign = direction.x() > 0 ? 1.f : -1.f;
214 float dx = sign * roomDim2.x() - relativeListenerPos.x();
215 QVector3D intersection = relativeListenerPos + direction*dx/direction.x();
216 float dy = roomDim2.y() - qAbs(t: intersection.y());
217 float dz = roomDim2.z() - qAbs(t: intersection.z());
218 if (dy > 0 && dz > 0) {
219// qDebug() << "Hit with wall X" << walls[0] << dy << dz;
220 // Ray is hitting this wall
221 factors[Y] = qMax(a: 0.f, b: 1.f/3.f - dy/transitionDistance);
222 factors[Z] = qMax(a: 0.f, b: 1.f/3.f - dz/transitionDistance);
223 factors[X] = 1.f - factors[Y] - factors[Z];
224 foundWall = true;
225 }
226 }
227 if (!foundWall && direction.y() != 0) {
228 float sign = direction.y() > 0 ? 1.f : -1.f;
229 float dy = sign * roomDim2.y() - relativeListenerPos.y();
230 QVector3D intersection = relativeListenerPos + direction*dy/direction.y();
231 float dx = roomDim2.x() - qAbs(t: intersection.x());
232 float dz = roomDim2.z() - qAbs(t: intersection.z());
233 if (dx > 0 && dz > 0) {
234 // Ray is hitting this wall
235// qDebug() << "Hit with wall Y" << walls[1] << dx << dy;
236 factors[X] = qMax(a: 0.f, b: 1.f/3.f - dx/transitionDistance);
237 factors[Z] = qMax(a: 0.f, b: 1.f/3.f - dz/transitionDistance);
238 factors[Y] = 1.f - factors[X] - factors[Z];
239 foundWall = true;
240 }
241 }
242 if (!foundWall) {
243 Q_ASSERT(direction.z() != 0);
244 float sign = direction.z() > 0 ? 1.f : -1.f;
245 float dz = sign * roomDim2.z() - relativeListenerPos.z();
246 QVector3D intersection = relativeListenerPos + direction*dz/direction.z();
247 float dx = roomDim2.x() - qAbs(t: intersection.x());
248 float dy = roomDim2.y() - qAbs(t: intersection.y());
249 if (dx > 0 && dy > 0) {
250 // Ray is hitting this wall
251// qDebug() << "Hit with wall Z" << walls[2];
252 factors[X] = qMax(a: 0.f, b: 1.f/3.f - dx/transitionDistance);
253 factors[Y] = qMax(a: 0.f, b: 1.f/3.f - dy/transitionDistance);
254 factors[Z] = 1.f - factors[X] - factors[Y];
255 foundWall = true;
256 }
257 }
258 wallDampening = 0;
259 wallOcclusion = 0;
260 for (int i = 0; i < 3; ++i) {
261 wallDampening += factors[i]*rp->wallDampening(wall: walls[i]);
262 wallOcclusion += factors[i]*rp->wallOcclusion(wall: walls[i]);
263 }
264
265// qDebug() << "intersection with wall" << walls[0] << walls[1] << walls[2] << factors[0] << factors[1] << factors[2] << wallDampening << wallOcclusion;
266 ep->resonanceAudio->api->SetSourceRoomEffectsGain(source_id: sourceId, room_effects_gain: 0);
267 }
268 ep->resonanceAudio->api->SetSoundObjectOcclusionIntensity(sound_object_source_id: sourceId, intensity: occlusionIntensity + wallOcclusion);
269 ep->resonanceAudio->api->SetSourceVolume(source_id: sourceId, volume: volume*wallDampening);
270}
271
272QSpatialSound::DistanceModel QSpatialSound::distanceModel() const
273{
274 return d->distanceModel;
275}
276
277/*!
278 \property QSpatialSound::size
279
280 Defines the size of the sound source. If the listener is closer to the sound
281 object than the size, volume will stay constant. The size is also used to for
282 occlusion calculations, where large sources can be partially occluded by a wall.
283 */
284void QSpatialSound::setSize(float size)
285{
286 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
287 size *= ep->distanceScale;
288 if (d->size == size)
289 return;
290 d->size = size;
291
292 d->updateDistanceModel();
293 emit sizeChanged();
294}
295
296float QSpatialSound::size() const
297{
298 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
299 return d->size/ep->distanceScale;
300}
301
302/*!
303 \property QSpatialSound::distanceCutoff
304
305 Defines a distance beyond which sound coming from the source will cutoff.
306 If the listener is further away from the sound object than the cutoff
307 distance it won't be audible anymore.
308 */
309void QSpatialSound::setDistanceCutoff(float cutoff)
310{
311 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
312 cutoff *= ep->distanceScale;
313 if (d->distanceCutoff == cutoff)
314 return;
315 d->distanceCutoff = cutoff;
316
317 d->updateDistanceModel();
318 emit distanceCutoffChanged();
319}
320
321float QSpatialSound::distanceCutoff() const
322{
323 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
324 return d->distanceCutoff/ep->distanceScale;
325}
326
327/*!
328 \property QSpatialSound::manualAttenuation
329
330 Defines a manual attenuation factor if \l distanceModel is set to
331 QSpatialSound::DistanceModel::ManualAttenuation.
332 */
333void QSpatialSound::setManualAttenuation(float attenuation)
334{
335 if (d->manualAttenuation == attenuation)
336 return;
337 d->manualAttenuation = attenuation;
338 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
339 if (ep)
340 ep->resonanceAudio->api->SetSourceDistanceAttenuation(source_id: d->sourceId, distance_attenuation: d->manualAttenuation);
341 emit manualAttenuationChanged();
342}
343
344float QSpatialSound::manualAttenuation() const
345{
346 return d->manualAttenuation;
347}
348
349/*!
350 \property QSpatialSound::occlusionIntensity
351
352 Defines how much the object is occluded. 0 implies the object is
353 not occluded at all, 1 implies the sound source is fully occluded by
354 another object.
355
356 A fully occluded object will still be audible, but especially higher
357 frequencies will be dampened. In addition, the object will still
358 participate in generating reverb and reflections in the room.
359
360 Values larger than 1 are possible to further dampen the direct
361 sound coming from the source.
362
363 The default is 0.
364 */
365void QSpatialSound::setOcclusionIntensity(float occlusion)
366{
367 if (d->occlusionIntensity == occlusion)
368 return;
369 d->occlusionIntensity = occlusion;
370 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
371 if (ep)
372 ep->resonanceAudio->api->SetSoundObjectOcclusionIntensity(sound_object_source_id: d->sourceId, intensity: d->occlusionIntensity + d->wallOcclusion);
373 emit occlusionIntensityChanged();
374}
375
376float QSpatialSound::occlusionIntensity() const
377{
378 return d->occlusionIntensity;
379}
380
381/*!
382 \property QSpatialSound::directivity
383
384 Defines the directivity of the sound source. A value of 0 implies that the sound is
385 emitted equally in all directions, while a value of 1 implies that the source mainly
386 emits sound in the forward direction.
387
388 Valid values are between 0 and 1, the default is 0.
389 */
390void QSpatialSound::setDirectivity(float alpha)
391{
392 alpha = qBound(min: 0., val: alpha, max: 1.);
393 if (alpha == d->directivity)
394 return;
395 d->directivity = alpha;
396
397 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
398 if (ep)
399 ep->resonanceAudio->api->SetSoundObjectDirectivity(sound_object_source_id: d->sourceId, alpha: d->directivity, order: d->directivityOrder);
400
401 emit directivityChanged();
402}
403
404float QSpatialSound::directivity() const
405{
406 return d->directivity;
407}
408
409/*!
410 \property QSpatialSound::directivityOrder
411
412 Defines the order of the directivity of the sound source. A higher order
413 implies a sharper localization of the sound cone.
414
415 The minimum value and default for this property is 1.
416 */
417void QSpatialSound::setDirectivityOrder(float order)
418{
419 order = qMax(a: order, b: 1.);
420 if (order == d->directivityOrder)
421 return;
422 d->directivityOrder = order;
423
424 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
425 if (ep)
426 ep->resonanceAudio->api->SetSoundObjectDirectivity(sound_object_source_id: d->sourceId, alpha: d->directivity, order: d->directivityOrder);
427
428 emit directivityChanged();
429}
430
431float QSpatialSound::directivityOrder() const
432{
433 return d->directivityOrder;
434}
435
436/*!
437 \property QSpatialSound::nearFieldGain
438
439 Defines the near field gain for the sound source. Valid values are between 0 and 1.
440 A near field gain of 1 will raise the volume of the sound signal by approx 20 dB for
441 distances very close to the listener.
442 */
443void QSpatialSound::setNearFieldGain(float gain)
444{
445 gain = qBound(min: 0., val: gain, max: 1.);
446 if (gain == d->nearFieldGain)
447 return;
448 d->nearFieldGain = gain;
449
450 auto *ep = QAudioEnginePrivate::get(engine: d->engine);
451 if (ep)
452 ep->resonanceAudio->api->SetSoundObjectNearFieldEffectGain(sound_object_source_id: d->sourceId, gain: d->nearFieldGain*9.f);
453
454 emit nearFieldGainChanged();
455
456}
457
458float QSpatialSound::nearFieldGain() const
459{
460 return d->nearFieldGain;
461}
462
463/*!
464 \property QSpatialSound::source
465
466 The source file for the sound to be played.
467 */
468void QSpatialSound::setSource(const QUrl &url)
469{
470 if (d->url == url)
471 return;
472 d->url = url;
473
474 d->load();
475 emit sourceChanged();
476}
477
478QUrl QSpatialSound::source() const
479{
480 return d->url;
481}
482
483/*!
484 \enum QSpatialSound::Loops
485
486 Lets you control the sound playback loop using the following values:
487
488 \value Infinite Playback infinitely
489 \value Once Playback once
490*/
491/*!
492 \property QSpatialSound::loops
493
494 Determines how many times the sound is played before the player stops.
495 Set to QSpatialSound::Infinite to play the current sound in a loop forever.
496
497 The default value is \c 1.
498 */
499int QSpatialSound::loops() const
500{
501 return d->m_loops.loadRelaxed();
502}
503
504void QSpatialSound::setLoops(int loops)
505{
506 int oldLoops = d->m_loops.fetchAndStoreRelaxed(newValue: loops);
507 if (oldLoops != loops)
508 emit loopsChanged();
509}
510
511/*!
512 \property QSpatialSound::autoPlay
513
514 Determines whether the sound should automatically start playing when a source
515 gets specified.
516
517 The default value is \c true.
518 */
519bool QSpatialSound::autoPlay() const
520{
521 return d->m_autoPlay.loadRelaxed();
522}
523
524void QSpatialSound::setAutoPlay(bool autoPlay)
525{
526 bool old = d->m_autoPlay.fetchAndStoreRelaxed(newValue: autoPlay);
527 if (old != autoPlay)
528 emit autoPlayChanged();
529}
530
531/*!
532 Starts playing back the sound. Does nothing if the sound is already playing.
533 */
534void QSpatialSound::play()
535{
536 d->play();
537}
538
539/*!
540 Pauses sound playback. Calling play() will continue playback.
541 */
542void QSpatialSound::pause()
543{
544 d->pause();
545}
546
547/*!
548 Stops sound playback and resets the current position and current loop count to 0.
549 Calling play() will start playback at the beginning of the sound file.
550 */
551void QSpatialSound::stop()
552{
553 d->stop();
554}
555
556/*!
557 \internal
558 */
559void QSpatialSound::setEngine(QAudioEngine *engine)
560{
561 if (d->engine == engine)
562 return;
563 auto *ep = QAudioEnginePrivate::get(engine);
564
565 if (ep)
566 ep->removeSpatialSound(sound: this);
567 d->engine = engine;
568
569 ep = QAudioEnginePrivate::get(engine);
570 if (ep) {
571 ep->addSpatialSound(sound: this);
572 ep->resonanceAudio->api->SetSourcePosition(source_id: d->sourceId, x: d->pos.x(), y: d->pos.y(), z: d->pos.z());
573 ep->resonanceAudio->api->SetSourceRotation(source_id: d->sourceId, x: d->rotation.x(), y: d->rotation.y(), z: d->rotation.z(), w: d->rotation.scalar());
574 ep->resonanceAudio->api->SetSourceVolume(source_id: d->sourceId, volume: d->volume);
575 ep->resonanceAudio->api->SetSoundObjectDirectivity(sound_object_source_id: d->sourceId, alpha: d->directivity, order: d->directivityOrder);
576 ep->resonanceAudio->api->SetSoundObjectNearFieldEffectGain(sound_object_source_id: d->sourceId, gain: d->nearFieldGain);
577 d->updateDistanceModel();
578 }
579}
580
581/*!
582 Returns the engine associated with this listener.
583 */
584QAudioEngine *QSpatialSound::engine() const
585{
586 return d->engine;
587}
588
589QT_END_NAMESPACE
590
591#include "moc_qspatialsound.cpp"
592

source code of qtmultimedia/src/spatialaudio/qspatialsound.cpp