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