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