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