-
-
Notifications
You must be signed in to change notification settings - Fork 77
/
Copy pathF3DAudio.c
1565 lines (1399 loc) · 46.6 KB
/
F3DAudio.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* FAudio - XAudio Reimplementation for FNA
*
* Copyright (c) 2011-2024 Ethan Lee, Luigi Auriemma, and the MonoGame Team
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com>
*
*/
#include "F3DAudio.h"
#include "FAudio_internal.h"
#include <math.h> /* ONLY USE THIS FOR isnan! */
#include <float.h> /* ONLY USE THIS FOR FLT_MIN/FLT_MAX! */
/* VS2010 doesn't define isnan (which is C99), so here it is. */
#if defined(_MSC_VER) && !defined(isnan)
#define isnan(x) _isnan(x)
#endif
/* UTILITY MACROS */
#define PARAM_CHECK_OK 1
#define PARAM_CHECK_FAIL (!PARAM_CHECK_OK)
#define ARRAY_COUNT(x) (sizeof(x) / sizeof(x[0]))
#define LERP(a, x, y) ((1.0f - a) * x + a * y)
/* PARAMETER CHECK MACROS */
#define PARAM_CHECK(cond, msg) FAudio_assert(cond && msg)
#define POINTER_CHECK(p) \
PARAM_CHECK(p != NULL, "Pointer " #p " must be != NULL")
#define FLOAT_BETWEEN_CHECK(f, a, b) \
PARAM_CHECK(f >= a, "Value" #f " is too low"); \
PARAM_CHECK(f <= b, "Value" #f " is too big")
/* Quote X3DAUDIO docs:
* "To be considered orthonormal, a pair of vectors must have a magnitude of
* 1 +- 1x10-5 and a dot product of 0 +- 1x10-5."
* VECTOR_NORMAL_CHECK verifies that vectors are normal (i.e. have norm 1 +- 1x10-5)
* VECTOR_BASE_CHECK verifies that a pair of vectors are orthogonal (i.e. their dot
* product is 0 +- 1x10-5)
*/
/* TODO: Switch to square length (to save CPU) */
#define VECTOR_NORMAL_CHECK(v) \
PARAM_CHECK( \
FAudio_fabsf(VectorLength(v) - 1.0f) <= 1e-5f, \
"Vector " #v " isn't normal" \
)
#define VECTOR_BASE_CHECK(u, v) \
PARAM_CHECK( \
FAudio_fabsf(VectorDot(u, v)) <= 1e-5f, \
"Vector u and v have non-negligible dot product" \
)
/*************************************
* F3DAudioInitialize Implementation *
*************************************/
/* F3DAUDIO_HANDLE Structure */
#define SPEAKERMASK(Instance) *((uint32_t*) &Instance[0])
#define SPEAKERCOUNT(Instance) *((uint32_t*) &Instance[4])
#define SPEAKER_LF_INDEX(Instance) *((uint32_t*) &Instance[8])
#define SPEEDOFSOUND(Instance) *((float*) &Instance[12])
#define SPEEDOFSOUNDEPSILON(Instance) *((float*) &Instance[16])
/* Export for unit tests */
F3DAUDIOAPI uint32_t F3DAudioCheckInitParams(
uint32_t SpeakerChannelMask,
float SpeedOfSound,
F3DAUDIO_HANDLE instance
) {
const uint32_t kAllowedSpeakerMasks[] =
{
SPEAKER_MONO,
SPEAKER_STEREO,
SPEAKER_2POINT1,
SPEAKER_QUAD,
SPEAKER_SURROUND,
SPEAKER_4POINT1,
SPEAKER_5POINT1,
SPEAKER_5POINT1_SURROUND,
SPEAKER_7POINT1,
SPEAKER_7POINT1_SURROUND,
};
uint8_t speakerMaskIsValid = 0;
uint32_t i;
POINTER_CHECK(instance);
for (i = 0; i < ARRAY_COUNT(kAllowedSpeakerMasks); i += 1)
{
if (SpeakerChannelMask == kAllowedSpeakerMasks[i])
{
speakerMaskIsValid = 1;
break;
}
}
/* The docs don't clearly say it, but the debug dll does check that
* we're exactly in one of the allowed speaker configurations.
* -Adrien
*/
PARAM_CHECK(
speakerMaskIsValid == 1,
"SpeakerChannelMask is invalid. Needs to be one of"
" MONO, STEREO, QUAD, 2POINT1, 4POINT1, 5POINT1, 7POINT1,"
" SURROUND, 5POINT1_SURROUND, or 7POINT1_SURROUND."
);
PARAM_CHECK(SpeedOfSound >= FLT_MIN, "SpeedOfSound needs to be >= FLT_MIN");
return PARAM_CHECK_OK;
}
void F3DAudioInitialize(
uint32_t SpeakerChannelMask,
float SpeedOfSound,
F3DAUDIO_HANDLE Instance
) {
F3DAudioInitialize8(SpeakerChannelMask, SpeedOfSound, Instance);
}
uint32_t F3DAudioInitialize8(
uint32_t SpeakerChannelMask,
float SpeedOfSound,
F3DAUDIO_HANDLE Instance
) {
union
{
float f;
uint32_t i;
} epsilonHack;
uint32_t speakerCount = 0;
if (!F3DAudioCheckInitParams(SpeakerChannelMask, SpeedOfSound, Instance))
{
return FAUDIO_E_INVALID_CALL;
}
SPEAKERMASK(Instance) = SpeakerChannelMask;
SPEEDOFSOUND(Instance) = SpeedOfSound;
/* "Convert" raw float to int... */
epsilonHack.f = SpeedOfSound;
/* ... Subtract epsilon value... */
epsilonHack.i -= 1;
/* ... Convert back to float. */
SPEEDOFSOUNDEPSILON(Instance) = epsilonHack.f;
SPEAKER_LF_INDEX(Instance) = 0xFFFFFFFF;
if (SpeakerChannelMask & SPEAKER_LOW_FREQUENCY)
{
if (SpeakerChannelMask & SPEAKER_FRONT_CENTER)
{
SPEAKER_LF_INDEX(Instance) = 3;
}
else
{
SPEAKER_LF_INDEX(Instance) = 2;
}
}
while (SpeakerChannelMask)
{
speakerCount += 1;
SpeakerChannelMask &= SpeakerChannelMask - 1;
}
SPEAKERCOUNT(Instance) = speakerCount;
return 0;
}
/************************************
* F3DAudioCalculate Implementation *
************************************/
/* VECTOR UTILITIES */
static inline F3DAUDIO_VECTOR Vec(float x, float y, float z)
{
F3DAUDIO_VECTOR res;
res.x = x;
res.y = y;
res.z = z;
return res;
}
#define VectorAdd(u, v) Vec(u.x + v.x, u.y + v.y, u.z + v.z)
#define VectorSub(u, v) Vec(u.x - v.x, u.y - v.y, u.z - v.z)
#define VectorScale(u, s) Vec(u.x * s, u.y * s, u.z * s)
#define VectorCross(u, v) Vec( \
(u.y * v.z) - (u.z * v.y), \
(u.z * v.x) - (u.x * v.z), \
(u.x * v.y) - (u.y * v.x) \
)
#define VectorLength(v) FAudio_sqrtf( \
(v.x * v.x) + (v.y * v.y) + (v.z * v.z) \
)
#define VectorDot(u, v) ((u.x * v.x) + (u.y * v.y) + (u.z * v.z))
/* This structure represent a tuple of vectors that form a left-handed basis.
* That is, all vectors are normal, orthogonal to each other, and taken in the
* order front, right, top they follow the left-hand rule.
* (https://en.wikipedia.org/wiki/Right-hand_rule)
*/
typedef struct F3DAUDIO_BASIS
{
F3DAUDIO_VECTOR front;
F3DAUDIO_VECTOR right;
F3DAUDIO_VECTOR top;
} F3DAUDIO_BASIS;
/* CHECK UTILITY FUNCTIONS */
static inline uint8_t CheckCone(F3DAUDIO_CONE *pCone)
{
if (!pCone)
{
return PARAM_CHECK_OK;
}
FLOAT_BETWEEN_CHECK(pCone->InnerAngle, 0.0f, F3DAUDIO_2PI);
FLOAT_BETWEEN_CHECK(pCone->OuterAngle, pCone->InnerAngle, F3DAUDIO_2PI);
FLOAT_BETWEEN_CHECK(pCone->InnerVolume, 0.0f, 2.0f);
FLOAT_BETWEEN_CHECK(pCone->OuterVolume, 0.0f, 2.0f);
FLOAT_BETWEEN_CHECK(pCone->InnerLPF, 0.0f, 1.0f);
FLOAT_BETWEEN_CHECK(pCone->OuterLPF, 0.0f, 1.0f);
FLOAT_BETWEEN_CHECK(pCone->InnerReverb, 0.0f, 2.0f);
FLOAT_BETWEEN_CHECK(pCone->OuterReverb, 0.0f, 2.0f);
return PARAM_CHECK_OK;
}
static inline uint8_t CheckCurve(F3DAUDIO_DISTANCE_CURVE *pCurve)
{
F3DAUDIO_DISTANCE_CURVE_POINT *points;
uint32_t i;
if (!pCurve)
{
return PARAM_CHECK_OK;
}
points = pCurve->pPoints;
POINTER_CHECK(points);
PARAM_CHECK(pCurve->PointCount >= 2, "Invalid number of points for curve");
for (i = 0; i < pCurve->PointCount; i += 1)
{
FLOAT_BETWEEN_CHECK(points[i].Distance, 0.0f, 1.0f);
}
PARAM_CHECK(
points[0].Distance == 0.0f,
"First point in the curve must be at distance 0.0f"
);
PARAM_CHECK(
points[pCurve->PointCount - 1].Distance == 1.0f,
"Last point in the curve must be at distance 1.0f"
);
for (i = 0; i < (pCurve->PointCount - 1); i += 1)
{
PARAM_CHECK(
points[i].Distance < points[i + 1].Distance,
"Curve points must be in strict ascending order"
);
}
return PARAM_CHECK_OK;
}
/* Export for unit tests */
F3DAUDIOAPI uint8_t F3DAudioCheckCalculateParams(
const F3DAUDIO_HANDLE Instance,
const F3DAUDIO_LISTENER *pListener,
const F3DAUDIO_EMITTER *pEmitter,
uint32_t Flags,
F3DAUDIO_DSP_SETTINGS *pDSPSettings
) {
uint32_t i, ChannelCount;
POINTER_CHECK(Instance);
POINTER_CHECK(pListener);
POINTER_CHECK(pEmitter);
POINTER_CHECK(pDSPSettings);
if (Flags & F3DAUDIO_CALCULATE_MATRIX)
{
POINTER_CHECK(pDSPSettings->pMatrixCoefficients);
}
if (Flags & F3DAUDIO_CALCULATE_ZEROCENTER)
{
const uint32_t isCalculateMatrix = (Flags & F3DAUDIO_CALCULATE_MATRIX);
const uint32_t hasCenter = SPEAKERMASK(Instance) & SPEAKER_FRONT_CENTER;
PARAM_CHECK(
isCalculateMatrix && hasCenter,
"F3DAUDIO_CALCULATE_ZEROCENTER is only valid for matrix"
" calculations with an output format that has a center channel"
);
}
if (Flags & F3DAUDIO_CALCULATE_REDIRECT_TO_LFE)
{
const uint32_t isCalculateMatrix = (Flags & F3DAUDIO_CALCULATE_MATRIX);
const uint32_t hasLF = SPEAKERMASK(Instance) & SPEAKER_LOW_FREQUENCY;
PARAM_CHECK(
isCalculateMatrix && hasLF,
"F3DAUDIO_CALCULATE_REDIRECT_TO_LFE is only valid for matrix"
" calculations with an output format that has a low-frequency"
" channel"
);
}
ChannelCount = SPEAKERCOUNT(Instance);
PARAM_CHECK(
pDSPSettings->DstChannelCount == ChannelCount,
"Invalid channel count, DSP settings and speaker configuration must agree"
);
PARAM_CHECK(
pDSPSettings->SrcChannelCount == pEmitter->ChannelCount,
"Invalid channel count, DSP settings and emitter must agree"
);
if (pListener->pCone)
{
PARAM_CHECK(
CheckCone(pListener->pCone) == PARAM_CHECK_OK,
"Invalid listener cone"
);
}
VECTOR_NORMAL_CHECK(pListener->OrientFront);
VECTOR_NORMAL_CHECK(pListener->OrientTop);
VECTOR_BASE_CHECK(pListener->OrientFront, pListener->OrientTop);
if (pEmitter->pCone)
{
VECTOR_NORMAL_CHECK(pEmitter->OrientFront);
PARAM_CHECK(
CheckCone(pEmitter->pCone) == PARAM_CHECK_OK,
"Invalid emitter cone"
);
}
else if (Flags & F3DAUDIO_CALCULATE_EMITTER_ANGLE)
{
VECTOR_NORMAL_CHECK(pEmitter->OrientFront);
}
if (pEmitter->ChannelCount > 1)
{
/* Only used for multi-channel emitters */
VECTOR_NORMAL_CHECK(pEmitter->OrientFront);
VECTOR_NORMAL_CHECK(pEmitter->OrientTop);
VECTOR_BASE_CHECK(pEmitter->OrientFront, pEmitter->OrientTop);
}
FLOAT_BETWEEN_CHECK(pEmitter->InnerRadius, 0.0f, FLT_MAX);
FLOAT_BETWEEN_CHECK(pEmitter->InnerRadiusAngle, 0.0f, F3DAUDIO_2PI / 4.0f);
PARAM_CHECK(
pEmitter->ChannelCount > 0,
"Invalid channel count for emitter"
);
PARAM_CHECK(
pEmitter->ChannelRadius >= 0.0f,
"Invalid channel radius for emitter"
);
if (pEmitter->ChannelCount > 1)
{
PARAM_CHECK(
pEmitter->pChannelAzimuths != NULL,
"Invalid channel azimuths for multi-channel emitter"
);
if (pEmitter->pChannelAzimuths)
{
for (i = 0; i < pEmitter->ChannelCount; i += 1)
{
float currentAzimuth = pEmitter->pChannelAzimuths[i];
FLOAT_BETWEEN_CHECK(currentAzimuth, 0.0f, F3DAUDIO_2PI);
if (currentAzimuth == F3DAUDIO_2PI)
{
PARAM_CHECK(
!(Flags & F3DAUDIO_CALCULATE_REDIRECT_TO_LFE),
"F3DAUDIO_CALCULATE_REDIRECT_TO_LFE valid only for"
" matrix calculations with emitters that have no LFE"
" channel"
);
}
}
}
}
FLOAT_BETWEEN_CHECK(pEmitter->CurveDistanceScaler, FLT_MIN, FLT_MAX);
FLOAT_BETWEEN_CHECK(pEmitter->DopplerScaler, 0.0f, FLT_MAX);
PARAM_CHECK(
CheckCurve(pEmitter->pVolumeCurve) == PARAM_CHECK_OK,
"Invalid Volume curve"
);
PARAM_CHECK(
CheckCurve(pEmitter->pLFECurve) == PARAM_CHECK_OK,
"Invalid LFE curve"
);
PARAM_CHECK(
CheckCurve(pEmitter->pLPFDirectCurve) == PARAM_CHECK_OK,
"Invalid LPFDirect curve"
);
PARAM_CHECK(
CheckCurve(pEmitter->pLPFReverbCurve) == PARAM_CHECK_OK,
"Invalid LPFReverb curve"
);
PARAM_CHECK(
CheckCurve(pEmitter->pReverbCurve) == PARAM_CHECK_OK,
"Invalid Reverb curve"
);
return PARAM_CHECK_OK;
}
/*
* MATRIX CALCULATION
*/
/* This function computes the distance either according to a curve if pCurve
* isn't NULL, or according to the inverse distance law 1/d otherwise.
*/
static inline float ComputeDistanceAttenuation(
float normalizedDistance,
F3DAUDIO_DISTANCE_CURVE *pCurve
) {
float res;
float alpha;
uint32_t n_points;
size_t i;
if (pCurve)
{
F3DAUDIO_DISTANCE_CURVE_POINT* points = pCurve->pPoints;
n_points = pCurve->PointCount;
/* By definition, the first point in the curve must be 0.0f
* -Adrien
*/
/* We advance i up until our normalizedDistance lies between the distances of
* the i_th and (i-1)_th points, or we reach the last point.
*/
for (i = 1; (i < n_points) && (normalizedDistance >= points[i].Distance); i += 1);
if (i == n_points)
{
/* We've reached the last point, so we use its value directly.
* Quote X3DAUDIO docs:
* "If an emitter moves beyond a distance of (CurveDistanceScaler × 1.0f),
* the last point on the curve is used to compute the volume output level."
*/
res = points[n_points - 1].DSPSetting;
}
else
{
/* We're between two points: the distance attenuation is the linear interpolation of the DSPSetting
* values defined by our points, according to the distance.
*/
alpha = (points[i].Distance - normalizedDistance) / (points[i].Distance - points[i - 1].Distance);
res = LERP(alpha, points[i].DSPSetting, points[i - 1].DSPSetting);
}
}
else
{
res = 1.0f;
if (normalizedDistance >= 1.0f)
{
res /= normalizedDistance;
}
}
return res;
}
static inline float ComputeConeParameter(
float distance,
float angle,
float innerAngle,
float outerAngle,
float innerParam,
float outerParam
) {
/* When computing whether a point lies inside a cone, X3DAUDIO first determines
* whether the point is close enough to the apex of the cone.
* If it is, the innerParam is used.
* The following constant is the one that is used for this distance check;
* It is an approximation, found by manual binary search.
* TODO: find the exact value of the constant via automated binary search. */
#define CONE_NULL_DISTANCE_TOLERANCE 1e-7
float halfInnerAngle, halfOuterAngle, alpha;
/* Quote X3DAudio.h:
* "Set both cone angles to 0 or X3DAUDIO_2PI for omnidirectionality using
* only the outer or inner values respectively."
*/
if (innerAngle == 0.0f && outerAngle == 0.0f)
{
return outerParam;
}
if (innerAngle == F3DAUDIO_2PI && outerAngle == F3DAUDIO_2PI)
{
return innerParam;
}
/* If we're within the inner angle, or close enough to the apex, we use
* the innerParam. */
halfInnerAngle = innerAngle / 2.0f;
if (distance <= CONE_NULL_DISTANCE_TOLERANCE || angle <= halfInnerAngle)
{
return innerParam;
}
/* If we're between the inner angle and the outer angle, we must use
* some interpolation of the innerParam and outerParam according to the
* distance between our angle and the inner and outer angles.
*/
halfOuterAngle = outerAngle / 2.0f;
if (angle <= halfOuterAngle)
{
alpha = (angle - halfInnerAngle) / (halfOuterAngle - halfInnerAngle);
/* Sooo... This is awkward. MSDN doesn't say anything, but
* X3DAudio.h says that this should be lerped. However in
* practice the behaviour of X3DAudio isn't a lerp at all. It's
* easy to see with big (InnerAngle / OuterAngle) values. If we
* want accurate emulation, we'll need to either find what
* formula they use, or use a more advanced interpolation, like
* tricubic.
*
* TODO: HIGH_ACCURACY version.
* -Adrien
*/
return LERP(alpha, innerParam, outerParam);
}
/* Otherwise, we're outside the outer angle, so we just return the outer param. */
return outerParam;
}
/* X3DAudio.h declares something like this, but the default (if emitter is NULL)
* volume curve is a *computed* inverse law, while on the other hand a curve
* leads to a piecewise linear function. So a "default curve" like this is
* pointless, not sure what X3DAudio does with it...
* -Adrien
*/
#if 0
static F3DAUDIO_DISTANCE_CURVE_POINT DefaultVolumeCurvePoints[] =
{
{ 0.0f, 1.0f },
{ 1.0f, 0.0f }
};
static F3DAUDIO_DISTANCE_CURVE DefaultVolumeCurve =
{
DefaultVolumeCurvePoints,
ARRAY_COUNT(DefaultVolumeCurvePoints)
};
#endif
/* Here we declare the azimuths of every speaker for every speaker
* configuration, ordered by increasing angle, as well as the index to which
* they map in the final matrix for their respective configuration. It had to be
* reverse engineered by looking at the data from various X3DAudioCalculate()
* matrix results for the various speaker configurations; *in particular*, the
* azimuths are different from the ones in F3DAudio.h (and X3DAudio.h) for
* SPEAKER_STEREO (which is declared has having front L and R speakers in the
* bit mask, but in fact has L and R *side* speakers). LF speakers are
* deliberately not included in the SpeakerInfo list, rather, we store the index
* into a separate field (with a -1 sentinel value if it has no LF speaker).
* -Adrien
*/
typedef struct
{
float azimuth;
uint32_t matrixIdx;
} SpeakerInfo;
typedef struct
{
uint32_t configMask;
const SpeakerInfo *speakers;
/* Not strictly necessary because it can be inferred from the
* SpeakerCount field of the F3DAUDIO_HANDLE, but makes code much
* cleaner and less error prone
*/
uint32_t numNonLFSpeakers;
int32_t LFSpeakerIdx;
} ConfigInfo;
/* It is absolutely necessary that these are stored in increasing, *positive*
* azimuth order (i.e. all angles between [0; 2PI]), as we'll do a linear
* interval search inside FindSpeakerAzimuths.
* -Adrien
*/
#define SPEAKER_AZIMUTH_CENTER 0.0f
#define SPEAKER_AZIMUTH_FRONT_RIGHT_OF_CENTER (F3DAUDIO_PI * 1.0f / 8.0f)
#define SPEAKER_AZIMUTH_FRONT_RIGHT (F3DAUDIO_PI * 1.0f / 4.0f)
#define SPEAKER_AZIMUTH_SIDE_RIGHT (F3DAUDIO_PI * 1.0f / 2.0f)
#define SPEAKER_AZIMUTH_BACK_RIGHT (F3DAUDIO_PI * 3.0f / 4.0f)
#define SPEAKER_AZIMUTH_BACK_CENTER F3DAUDIO_PI
#define SPEAKER_AZIMUTH_BACK_LEFT (F3DAUDIO_PI * 5.0f / 4.0f)
#define SPEAKER_AZIMUTH_SIDE_LEFT (F3DAUDIO_PI * 3.0f / 2.0f)
#define SPEAKER_AZIMUTH_FRONT_LEFT (F3DAUDIO_PI * 7.0f / 4.0f)
#define SPEAKER_AZIMUTH_FRONT_LEFT_OF_CENTER (F3DAUDIO_PI * 15.0f / 8.0f)
const SpeakerInfo kMonoConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 0 },
};
const SpeakerInfo kStereoConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_SIDE_RIGHT, 1 },
{ SPEAKER_AZIMUTH_SIDE_LEFT, 0 },
};
const SpeakerInfo k2Point1ConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_SIDE_RIGHT, 1 },
{ SPEAKER_AZIMUTH_SIDE_LEFT, 0 },
};
const SpeakerInfo kSurroundConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 2 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_BACK_CENTER, 3 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
const SpeakerInfo kQuadConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_BACK_RIGHT, 3 },
{ SPEAKER_AZIMUTH_BACK_LEFT, 2 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
const SpeakerInfo k4Point1ConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_BACK_RIGHT, 4 },
{ SPEAKER_AZIMUTH_BACK_LEFT, 3 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
const SpeakerInfo k5Point1ConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 2 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_BACK_RIGHT, 5 },
{ SPEAKER_AZIMUTH_BACK_LEFT, 4 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
const SpeakerInfo k7Point1ConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 2 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT_OF_CENTER, 7 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_BACK_RIGHT, 5 },
{ SPEAKER_AZIMUTH_BACK_LEFT, 4 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
{ SPEAKER_AZIMUTH_FRONT_LEFT_OF_CENTER, 6 },
};
const SpeakerInfo k5Point1SurroundConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 2 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_SIDE_RIGHT, 5 },
{ SPEAKER_AZIMUTH_SIDE_LEFT, 4 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
const SpeakerInfo k7Point1SurroundConfigSpeakers[] =
{
{ SPEAKER_AZIMUTH_CENTER, 2 },
{ SPEAKER_AZIMUTH_FRONT_RIGHT, 1 },
{ SPEAKER_AZIMUTH_SIDE_RIGHT, 7 },
{ SPEAKER_AZIMUTH_BACK_RIGHT, 5 },
{ SPEAKER_AZIMUTH_BACK_LEFT, 4 },
{ SPEAKER_AZIMUTH_SIDE_LEFT, 6 },
{ SPEAKER_AZIMUTH_FRONT_LEFT, 0 },
};
/* With that organization, the index of the LF speaker into the matrix array
* strangely looks *exactly* like the mystery field in the F3DAUDIO_HANDLE!!
* We're keeping a separate field within ConfigInfo because it makes the code
* much cleaner, though.
* -Adrien
*/
const ConfigInfo kSpeakersConfigInfo[] =
{
{ SPEAKER_MONO, kMonoConfigSpeakers, ARRAY_COUNT(kMonoConfigSpeakers), -1 },
{ SPEAKER_STEREO, kStereoConfigSpeakers, ARRAY_COUNT(kStereoConfigSpeakers), -1 },
{ SPEAKER_2POINT1, k2Point1ConfigSpeakers, ARRAY_COUNT(k2Point1ConfigSpeakers), 2 },
{ SPEAKER_SURROUND, kSurroundConfigSpeakers, ARRAY_COUNT(kSurroundConfigSpeakers), -1 },
{ SPEAKER_QUAD, kQuadConfigSpeakers, ARRAY_COUNT(kQuadConfigSpeakers), -1 },
{ SPEAKER_4POINT1, k4Point1ConfigSpeakers, ARRAY_COUNT(k4Point1ConfigSpeakers), 2 },
{ SPEAKER_5POINT1, k5Point1ConfigSpeakers, ARRAY_COUNT(k5Point1ConfigSpeakers), 3 },
{ SPEAKER_7POINT1, k7Point1ConfigSpeakers, ARRAY_COUNT(k7Point1ConfigSpeakers), 3 },
{ SPEAKER_5POINT1_SURROUND, k5Point1SurroundConfigSpeakers, ARRAY_COUNT(k5Point1SurroundConfigSpeakers), 3 },
{ SPEAKER_7POINT1_SURROUND, k7Point1SurroundConfigSpeakers, ARRAY_COUNT(k7Point1SurroundConfigSpeakers), 3 },
};
/* A simple linear search is absolutely OK for 10 elements. */
static const ConfigInfo* GetConfigInfo(uint32_t speakerConfigMask)
{
uint32_t i;
for (i = 0; i < ARRAY_COUNT(kSpeakersConfigInfo); i += 1)
{
if (kSpeakersConfigInfo[i].configMask == speakerConfigMask)
{
return &kSpeakersConfigInfo[i];
}
}
FAudio_assert(0 && "Config info not found!");
return NULL;
}
/* Given a configuration, this function finds the azimuths of the two speakers
* between which the emitter lies. All the azimuths here are relative to the
* listener's base, since that's where the speakers are defined.
*/
static inline void FindSpeakerAzimuths(
const ConfigInfo* config,
float emitterAzimuth,
uint8_t skipCenter,
const SpeakerInfo **speakerInfo
) {
uint32_t i, nexti = 0;
float a0 = 0.0f, a1 = 0.0f;
FAudio_assert(config != NULL);
/* We want to find, given an azimuth, which speakers are the closest
* ones (in terms of angle) to that azimuth.
* This is done by iterating through the list of speaker azimuths, as
* given to us by the current ConfigInfo (which stores speaker azimuths
* in increasing order of azimuth for each possible speaker configuration;
* each speaker azimuth is defined to be between 0 and 2PI by construction).
*/
for (i = 0; i < config->numNonLFSpeakers; i += 1)
{
/* a0 and a1 are the azimuths of candidate speakers */
a0 = config->speakers[i].azimuth;
nexti = (i + 1) % config->numNonLFSpeakers;
a1 = config->speakers[nexti].azimuth;
if (a0 < a1)
{
if (emitterAzimuth >= a0 && emitterAzimuth < a1)
{
break;
}
}
/* It is possible for a speaker pair to enclose the singulary at 0 == 2PI:
* consider for example the quad config, which has a front left speaker
* at 7PI/4 and a front right speaker at PI/4. In that case a0 = 7PI/4 and
* a1 = PI/4, and the way we know whether our current azimuth lies between
* that pair is by checking whether the azimuth is greather than 7PI/4 or
* whether it's less than PI/4. (By contract, currentAzimuth is always less
* than 2PI.)
*/
else
{
if (emitterAzimuth >= a0 || emitterAzimuth < a1)
{
break;
}
}
}
FAudio_assert(emitterAzimuth >= a0 || emitterAzimuth < a1);
/* skipCenter means that we don't want to use the center speaker.
* The easiest way to deal with this is to check whether either of our candidate
* speakers are the center, which always has an azimuth of 0.0. If that is the case
* we just replace it with either the previous one or the next one.
*/
if (skipCenter)
{
if (a0 == 0.0f)
{
if (i == 0)
{
i = config->numNonLFSpeakers - 1;
}
else
{
i -= 1;
}
}
else if (a1 == 0.0f)
{
nexti += 1;
if (nexti >= config->numNonLFSpeakers)
{
nexti -= config->numNonLFSpeakers;
}
}
}
speakerInfo[0] = &config->speakers[i];
speakerInfo[1] = &config->speakers[nexti];
}
/* Used to store diffusion factors */
/* See below for explanation. */
#define DIFFUSION_SPEAKERS_ALL 0
#define DIFFUSION_SPEAKERS_MATCHING 1
#define DIFFUSION_SPEAKERS_OPPOSITE 2
typedef float DiffusionSpeakerFactors[3];
/* ComputeInnerRadiusDiffusionFactors is a utility function that returns how
* energy dissipates to the speakers, given the radial distance between the
* emitter and the listener and the (optionally 0) InnerRadius distance. It
* returns 3 floats, via the diffusionFactors array, that say how much energy
* (after distance attenuation) will need to be distributed between each of the
* following cases:
*
* - SPEAKERS_ALL for all (non-LF) speakers, _INCLUDING_ the MATCHING and OPPOSITE.
* - SPEAKERS_OPPOSITE corresponds to the two speakers OPPOSITE the emitter.
* - SPEAKERS_MATCHING corresponds to the two speakers closest to the emitter.
*
* For a distance below a certain threshold (DISTANCE_EQUAL_ENERGY), all
* speakers receive equal energy.
*
* Above that, the amount that all speakers receive decreases linearly as radial
* distance increases, up until InnerRadius / 2. (If InnerRadius is null, we use
* MINIMUM_INNER_RADIUS.)
*
* At the same time, both opposite and matching speakers start to receive sound
* (in addition to the energy they receive from the aforementioned "all
* speakers" linear law) according to some unknown as of now law,
* that is currently emulated with a LERP. This is true up until InnerRadius.
*
* Above InnerRadius, only the two matching speakers receive sound.
*
* For more detail, see the "Inner Radius and Inner Radius Angle" in the
* MSDN docs for the X3DAUDIO_EMITTER structure.
* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.x3daudio.x3daudio_emitter(v=vs.85).aspx
*/
static inline void ComputeInnerRadiusDiffusionFactors(
float radialDistance,
float InnerRadius,
DiffusionSpeakerFactors diffusionFactors
) {
/* Determined experimentally; this is the midpoint value, i.e. the
* value at 0.5 for the matching speakers, used for the standard
* diffusion curve.
*
* Note: It is SUSPICIOUSLY close to 1/sqrt(2), but I haven't figured out why.
* -Adrien
*/
#define DIFFUSION_LERP_MIDPOINT_VALUE 0.707107f
/* X3DAudio always uses an InnerRadius-like behaviour (i.e. diffusing sound to more than
* a pair of speakers) even if InnerRadius is set to 0.0f.
* This constant determines the distance at which this behaviour is produced in that case. */
/* This constant was determined by manual binary search. TODO: get a more accurate version
* via an automated binary search. */
#define DIFFUSION_DISTANCE_MINIMUM_INNER_RADIUS 4e-7f
float actualInnerRadius = FAudio_max(InnerRadius, DIFFUSION_DISTANCE_MINIMUM_INNER_RADIUS);
float normalizedRadialDist;
float a, ms, os;
normalizedRadialDist = radialDistance / actualInnerRadius;
/* X3DAudio does another check for small radial distances before applying any InnerRadius-like
* behaviour. This is the constant that determines the threshold: below this distance we simply
* diffuse to all speakers equally. */
#define DIFFUSION_DISTANCE_EQUAL_ENERGY 1e-7f
if (radialDistance <= DIFFUSION_DISTANCE_EQUAL_ENERGY)
{
a = 1.0f;
ms = 0.0f;
os = 0.0f;
}
else if (normalizedRadialDist <= 0.5f)
{
/* Determined experimentally that this is indeed a linear law,
* with 100% confidence.
* -Adrien
*/
a = 1.0f - 2.0f * normalizedRadialDist;
/* Lerping here is an approximation.
* TODO: High accuracy version. Having stared at the curves long
* enough, I'm pretty sure this is a quadratic, but trying to
* polyfit with numpy didn't give nice, round polynomial
* coefficients...
* -Adrien
*/
ms = LERP(2.0f * normalizedRadialDist, 0.0f, DIFFUSION_LERP_MIDPOINT_VALUE);
os = 1.0f - a - ms;
}
else if (normalizedRadialDist <= 1.0f)
{
a = 0.0f;
/* Similarly, this is a lerp based on the midpoint value; the
* real, high-accuracy curve also looks like a quadratic.
* -Adrien
*/
ms = LERP(2.0f * (normalizedRadialDist - 0.5f), DIFFUSION_LERP_MIDPOINT_VALUE, 1.0f);
os = 1.0f - a - ms;
}
else
{
a = 0.0f;
ms = 1.0f;
os = 0.0f;
}
diffusionFactors[DIFFUSION_SPEAKERS_ALL] = a;
diffusionFactors[DIFFUSION_SPEAKERS_MATCHING] = ms;
diffusionFactors[DIFFUSION_SPEAKERS_OPPOSITE] = os;
}
/* ComputeEmitterChannelCoefficients handles the coefficients calculation for 1
* column of the matrix. It uses ComputeInnerRadiusDiffusionFactors to separate
* into three discrete cases; and for each case does the right repartition of
* the energy after attenuation to the right speakers, in particular in the
* MATCHING and OPPOSITE cases, it gives each of the two speakers found a linear
* amount of the energy, according to the angular distance between the emitter
* and the speaker azimuth.
*/
static inline void ComputeEmitterChannelCoefficients(
const ConfigInfo *curConfig,
const F3DAUDIO_BASIS *listenerBasis,
float innerRadius,
F3DAUDIO_VECTOR channelPosition,
float attenuation,
float LFEattenuation,
uint32_t flags,
uint32_t currentChannel,
uint32_t numSrcChannels,
float *pMatrixCoefficients
) {
float elevation, radialDistance;
F3DAUDIO_VECTOR projTopVec, projPlane;
uint8_t skipCenter = (flags & F3DAUDIO_CALCULATE_ZEROCENTER) ? 1 : 0;
DiffusionSpeakerFactors diffusionFactors = { 0.0f };
float x, y;
float emitterAzimuth;
float energyPerChannel;
float totalEnergy;
uint32_t nChannelsToDiffuseTo;
uint32_t iS, centerChannelIdx = -1;
const SpeakerInfo* infos[2];
float a0, a1, val;
uint32_t i0, i1;
/* We project against the listener basis' top vector to get the elevation of the
* current emitter channel position.
*/
elevation = VectorDot(listenerBasis->top, channelPosition);
/* To obtain the projection in the front-right plane of the listener's basis of the
* emitter channel position, we simply remove the projection against the top vector.
* The radial distance is then the length of the projected vector.
*/
projTopVec = VectorScale(listenerBasis->top, elevation);
projPlane = VectorSub(channelPosition, projTopVec);
radialDistance = VectorLength(projPlane);
ComputeInnerRadiusDiffusionFactors(
radialDistance,
innerRadius,
diffusionFactors
);
/* See the ComputeInnerRadiusDiffusionFactors comment above for more context. */