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