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

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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