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