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