Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
References:
File last commit:
Show/Diff file:
Action:
FNA/lib/FAudio/src/F3DAudio.c
1563 lines | 46.5 KiB | text/x-c | CLexer
1563 lines | 46.5 KiB | text/x-c | CLexer
r0 | /* FAudio - XAudio Reimplementation for FNA | |||
* | ||||
* Copyright (c) 2011-2020 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. */ | ||||
/* DIFFUSION_SPEAKERS_ALL corresponds to diffusing part of the sound to all of the | ||||
* speakers, equally. The amount of sound is determined by the float value | ||||
* diffusionFactors[DIFFUSION_SPEAKERS_ALL]. */ | ||||
if (diffusionFactors[DIFFUSION_SPEAKERS_ALL] > 0.0f) | ||||
{ | ||||
nChannelsToDiffuseTo = curConfig->numNonLFSpeakers; | ||||
totalEnergy = diffusionFactors[DIFFUSION_SPEAKERS_ALL] * attenuation; | ||||
if (skipCenter) | ||||
{ | ||||
nChannelsToDiffuseTo -= 1; | ||||
FAudio_assert(curConfig->speakers[0].azimuth == SPEAKER_AZIMUTH_CENTER); | ||||
centerChannelIdx = curConfig->speakers[0].matrixIdx; | ||||
} | ||||
energyPerChannel = totalEnergy / nChannelsToDiffuseTo; | ||||
for (iS = 0; iS < curConfig->numNonLFSpeakers; iS += 1) | ||||
{ | ||||
const uint32_t curSpeakerIdx = curConfig->speakers[iS].matrixIdx; | ||||
if (skipCenter && curSpeakerIdx == centerChannelIdx) | ||||
{ | ||||
continue; | ||||
} | ||||
pMatrixCoefficients[curSpeakerIdx * numSrcChannels + currentChannel] += energyPerChannel; | ||||
} | ||||
} | ||||
/* DIFFUSION_SPEAKERS_MATCHING corresponds to sending part of the sound to the speakers closest | ||||
* (in terms of azimuth) to the current position of the emitter. The amount of sound we shoud send | ||||
* corresponds here to diffusionFactors[DIFFUSION_SPEAKERS_MATCHING]. | ||||
* We use the FindSpeakerAzimuths function to find the speakers that match. */ | ||||
if (diffusionFactors[DIFFUSION_SPEAKERS_MATCHING] > 0.0f) | ||||
{ | ||||
const float totalEnergy = diffusionFactors[DIFFUSION_SPEAKERS_MATCHING] * attenuation; | ||||
x = VectorDot(listenerBasis->front, projPlane); | ||||
y = VectorDot(listenerBasis->right, projPlane); | ||||
/* Now, a critical point: We shouldn't be sending sound to | ||||
* matching speakers when x and y are close to 0. That's the | ||||
* contract we get from ComputeInnerRadiusDiffusionFactors, | ||||
* which checks that we're not too close to the zero distance. | ||||
* This allows the atan2 calculation to give good results. | ||||
*/ | ||||
/* atan2 returns [-PI, PI], but we want [0, 2PI] */ | ||||
emitterAzimuth = FAudio_atan2f(y, x); | ||||
if (emitterAzimuth < 0.0f) | ||||
{ | ||||
emitterAzimuth += F3DAUDIO_2PI; | ||||
} | ||||
FindSpeakerAzimuths(curConfig, emitterAzimuth, skipCenter, infos); | ||||
a0 = infos[0]->azimuth; | ||||
a1 = infos[1]->azimuth; | ||||
/* The following code is necessary to handle the singularity in | ||||
* (0 == 2PI). It'll give us a nice, well ordered interval. | ||||
*/ | ||||
if (a0 > a1) | ||||
{ | ||||
if (emitterAzimuth >= a0) | ||||
{ | ||||
emitterAzimuth -= F3DAUDIO_2PI; | ||||
} | ||||
a0 -= F3DAUDIO_2PI; | ||||
} | ||||
FAudio_assert(emitterAzimuth >= a0 && emitterAzimuth <= a1); | ||||
val = (emitterAzimuth - a0) / (a1 - a0); | ||||
i0 = infos[0]->matrixIdx; | ||||
i1 = infos[1]->matrixIdx; | ||||
pMatrixCoefficients[i0 * numSrcChannels + currentChannel] += (1.0f - val) * totalEnergy; | ||||
pMatrixCoefficients[i1 * numSrcChannels + currentChannel] += ( val) * totalEnergy; | ||||
} | ||||
/* DIFFUSION_SPEAKERS_OPPOSITE corresponds to sending part of the sound to the speakers | ||||
* _opposite_ the ones that are the closest to the current emitter position. | ||||
* To find these, we simply find the ones that are closest to the current emitter's azimuth + PI | ||||
* using the FindSpeakerAzimuth function. */ | ||||
if (diffusionFactors[DIFFUSION_SPEAKERS_OPPOSITE] > 0.0f) | ||||
{ | ||||
/* This code is similar to the matching speakers code above. */ | ||||
const float totalEnergy = diffusionFactors[DIFFUSION_SPEAKERS_OPPOSITE] * attenuation; | ||||
x = VectorDot(listenerBasis->front, projPlane); | ||||
y = VectorDot(listenerBasis->right, projPlane); | ||||
/* Similarly, we expect atan2 to be well behaved here. */ | ||||
emitterAzimuth = FAudio_atan2f(y, x); | ||||
/* Opposite speakers lie at azimuth + PI */ | ||||
emitterAzimuth += F3DAUDIO_PI; | ||||
/* Normalize to [0; 2PI) range. */ | ||||
if (emitterAzimuth < 0.0f) | ||||
{ | ||||
emitterAzimuth += F3DAUDIO_2PI; | ||||
} | ||||
else if (emitterAzimuth > F3DAUDIO_2PI) | ||||
{ | ||||
emitterAzimuth -= F3DAUDIO_2PI; | ||||
} | ||||
FindSpeakerAzimuths(curConfig, emitterAzimuth, skipCenter, infos); | ||||
a0 = infos[0]->azimuth; | ||||
a1 = infos[1]->azimuth; | ||||
/* The following code is necessary to handle the singularity in | ||||
* (0 == 2PI). It'll give us a nice, well ordered interval. | ||||
*/ | ||||
if (a0 > a1) | ||||
{ | ||||
if (emitterAzimuth >= a0) | ||||
{ | ||||
emitterAzimuth -= F3DAUDIO_2PI; | ||||
} | ||||
a0 -= F3DAUDIO_2PI; | ||||
} | ||||
FAudio_assert(emitterAzimuth >= a0 && emitterAzimuth <= a1); | ||||
val = (emitterAzimuth - a0) / (a1 - a0); | ||||
i0 = infos[0]->matrixIdx; | ||||
i1 = infos[1]->matrixIdx; | ||||
pMatrixCoefficients[i0 * numSrcChannels + currentChannel] += (1.0f - val) * totalEnergy; | ||||
pMatrixCoefficients[i1 * numSrcChannels + currentChannel] += ( val) * totalEnergy; | ||||
} | ||||
if (flags & F3DAUDIO_CALCULATE_REDIRECT_TO_LFE) | ||||
{ | ||||
FAudio_assert(curConfig->LFSpeakerIdx != -1); | ||||
pMatrixCoefficients[curConfig->LFSpeakerIdx * numSrcChannels + currentChannel] += LFEattenuation / numSrcChannels; | ||||
} | ||||
} | ||||
/* Calculations consist of several orthogonal steps that compose multiplicatively: | ||||
* | ||||
* First, we compute the attenuations (volume and LFE) due to distance, which | ||||
* may involve an optional volume and/or LFE volume curve. | ||||
* | ||||
* Then, we compute those due to optional cones. | ||||
* | ||||
* We then compute how much energy is diffuse w.r.t InnerRadius. If InnerRadius | ||||
* is 0.0f, this step is computed as if it was InnerRadius was | ||||
* NON_NULL_DISTANCE_DISK_RADIUS. The way this works is, we look at the radial | ||||
* distance of the current emitter channel to the listener, with regard to the | ||||
* listener's top orientation (i.e. this distance is independant of the | ||||
* emitter's elevation!). If this distance is less than NULL_DISTANCE_RADIUS, | ||||
* energy is diffused equally between all channels. If it's greater than | ||||
* InnerRadius (or NON_NULL_DISTANCE_RADIUS, if InnerRadius is 0.0f, as | ||||
* mentioned above), the two closest speakers, by azimuth, receive all the | ||||
* energy. Between InnerRadius/2.0f and InnerRadius, the energy starts bleeding | ||||
* into the opposite speakers. Once we go below InnerRadius/2.0f, the energy | ||||
* also starts to bleed into the other (non-opposite) channels, if there are | ||||
* any. This computation is handled by the ComputeInnerRadiusDiffusionFactors | ||||
* function. (TODO: High-accuracy version of this.) | ||||
* | ||||
* Finally, if we're not in the equal diffusion case, we find out the azimuths | ||||
* of the two closest speakers (with azimuth being defined with respect to the | ||||
* listener's front orientation, in the plane normal to the listener's top | ||||
* vector), as well as the azimuths of the two opposite speakers, if necessary, | ||||
* and linearly interpolate with respect to the angular distance. In the equal | ||||
* diffusion case, each channel receives the same value. | ||||
* | ||||
* Note: in the case of multi-channel emitters, the distance attenuation is only | ||||
* compted once, but all the azimuths and InnerRadius calculations are done per | ||||
* emitter channel. | ||||
* | ||||
* TODO: Handle InnerRadiusAngle. But honestly the X3DAudio default behaviour is | ||||
* so wacky that I wonder if anybody has ever used it. | ||||
* -Adrien | ||||
*/ | ||||
static inline void CalculateMatrix( | ||||
uint32_t ChannelMask, | ||||
uint32_t Flags, | ||||
const F3DAUDIO_LISTENER *pListener, | ||||
const F3DAUDIO_EMITTER *pEmitter, | ||||
uint32_t SrcChannelCount, | ||||
uint32_t DstChannelCount, | ||||
F3DAUDIO_VECTOR emitterToListener, | ||||
float eToLDistance, | ||||
float normalizedDistance, | ||||
float* MatrixCoefficients | ||||
) { | ||||
uint32_t iEC; | ||||
float curEmAzimuth; | ||||
const ConfigInfo* curConfig = GetConfigInfo(ChannelMask); | ||||
float attenuation = ComputeDistanceAttenuation( | ||||
normalizedDistance, | ||||
pEmitter->pVolumeCurve | ||||
); | ||||
/* TODO: this could be skipped if the destination has no LFE */ | ||||
float LFEattenuation = ComputeDistanceAttenuation( | ||||
normalizedDistance, | ||||
pEmitter->pLFECurve | ||||
); | ||||
F3DAUDIO_VECTOR listenerToEmitter; | ||||
F3DAUDIO_VECTOR listenerToEmChannel; | ||||
F3DAUDIO_BASIS listenerBasis; | ||||
/* Note: For both cone calculations, the angle might be NaN or infinite | ||||
* if distance == 0... ComputeConeParameter *does* check for this | ||||
* special case. It is necessary that we still go through the | ||||
* ComputeConeParameter function, because omnidirectional cones might | ||||
* give either InnerVolume or OuterVolume. | ||||
* -Adrien | ||||
*/ | ||||
if (pListener->pCone) | ||||
{ | ||||
/* Negate the dot product because we need listenerToEmitter in | ||||
* this case | ||||
* -Adrien | ||||
*/ | ||||
const float angle = -FAudio_acosf( | ||||
VectorDot(pListener->OrientFront, emitterToListener) / | ||||
eToLDistance | ||||
); | ||||
const float listenerConeParam = ComputeConeParameter( | ||||
eToLDistance, | ||||
angle, | ||||
pListener->pCone->InnerAngle, | ||||
pListener->pCone->OuterAngle, | ||||
pListener->pCone->InnerVolume, | ||||
pListener->pCone->OuterVolume | ||||
); | ||||
attenuation *= listenerConeParam; | ||||
LFEattenuation *= listenerConeParam; | ||||
} | ||||
/* See note above. */ | ||||
if (pEmitter->pCone && pEmitter->ChannelCount == 1) | ||||
{ | ||||
const float angle = FAudio_acosf( | ||||
VectorDot(pEmitter->OrientFront, emitterToListener) / | ||||
eToLDistance | ||||
); | ||||
const float emitterConeParam = ComputeConeParameter( | ||||
eToLDistance, | ||||
angle, | ||||
pEmitter->pCone->InnerAngle, | ||||
pEmitter->pCone->OuterAngle, | ||||
pEmitter->pCone->InnerVolume, | ||||
pEmitter->pCone->OuterVolume | ||||
); | ||||
attenuation *= emitterConeParam; | ||||
} | ||||
FAudio_zero(MatrixCoefficients, sizeof(float) * SrcChannelCount * DstChannelCount); | ||||
/* In the SPEAKER_MONO case, we can skip all energy diffusion calculation. */ | ||||
if (DstChannelCount == 1) | ||||
{ | ||||
for (iEC = 0; iEC < pEmitter->ChannelCount; iEC += 1) | ||||
{ | ||||
curEmAzimuth = 0.0f; | ||||
if (pEmitter->pChannelAzimuths) | ||||
{ | ||||
curEmAzimuth = pEmitter->pChannelAzimuths[iEC]; | ||||
} | ||||
/* The MONO setup doesn't have an LFE speaker. */ | ||||
if (curEmAzimuth != F3DAUDIO_2PI) | ||||
{ | ||||
MatrixCoefficients[iEC] = attenuation; | ||||
} | ||||
} | ||||
} | ||||
else | ||||
{ | ||||
listenerToEmitter = VectorScale(emitterToListener, -1.0f); | ||||
/* Remember here that the coordinate system is Left-Handed. */ | ||||
listenerBasis.front = pListener->OrientFront; | ||||
listenerBasis.right = VectorCross(pListener->OrientTop, pListener->OrientFront); | ||||
listenerBasis.top = pListener->OrientTop; | ||||
/* Handling the mono-channel emitter case separately is easier | ||||
* than having it as a separate case of a for-loop; indeed, in | ||||
* this case, we need to ignore the non-relevant values from the | ||||
* emitter, _even if they're set_. | ||||
*/ | ||||
if (pEmitter->ChannelCount == 1) | ||||
{ | ||||
listenerToEmChannel = listenerToEmitter; | ||||
ComputeEmitterChannelCoefficients( | ||||
curConfig, | ||||
&listenerBasis, | ||||
pEmitter->InnerRadius, | ||||
listenerToEmChannel, | ||||
attenuation, | ||||
LFEattenuation, | ||||
Flags, | ||||
0 /* currentChannel */, | ||||
1 /* numSrcChannels */, | ||||
MatrixCoefficients | ||||
); | ||||
} | ||||
else /* Multi-channel emitter case. */ | ||||
{ | ||||
const F3DAUDIO_VECTOR emitterRight = VectorCross(pEmitter->OrientTop, pEmitter->OrientFront); | ||||
for (iEC = 0; iEC < pEmitter->ChannelCount; iEC += 1) | ||||
{ | ||||
const float emChAzimuth = pEmitter->pChannelAzimuths[iEC]; | ||||
/* LFEs are easy enough to deal with; we can | ||||
* just do them separately. | ||||
*/ | ||||
if (emChAzimuth == F3DAUDIO_2PI) | ||||
{ | ||||
MatrixCoefficients[curConfig->LFSpeakerIdx * pEmitter->ChannelCount + iEC] = LFEattenuation; | ||||
} | ||||
else | ||||
{ | ||||
/* First compute the emitter channel | ||||
* vector relative to the emitter base... | ||||
*/ | ||||
const F3DAUDIO_VECTOR emitterBaseToChannel = VectorAdd( | ||||
VectorScale(pEmitter->OrientFront, pEmitter->ChannelRadius * FAudio_cosf(emChAzimuth)), | ||||
VectorScale(emitterRight, pEmitter->ChannelRadius * FAudio_sinf(emChAzimuth)) | ||||
); | ||||
/* ... then translate. */ | ||||
listenerToEmChannel = VectorAdd( | ||||
listenerToEmitter, | ||||
emitterBaseToChannel | ||||
); | ||||
ComputeEmitterChannelCoefficients( | ||||
curConfig, | ||||
&listenerBasis, | ||||
pEmitter->InnerRadius, | ||||
listenerToEmChannel, | ||||
attenuation, | ||||
LFEattenuation, | ||||
Flags, | ||||
iEC, | ||||
pEmitter->ChannelCount, | ||||
MatrixCoefficients | ||||
); | ||||
} | ||||
} | ||||
} | ||||
} | ||||
/* TODO: add post check to validate values | ||||
* (sum < 1, all values > 0, no Inf / NaN.. | ||||
* Sum can be >1 when cone or curve is set to a gain! | ||||
* Perhaps under a paranoid check disabled by default. | ||||
*/ | ||||
} | ||||
/* | ||||
* OTHER CALCULATIONS | ||||
*/ | ||||
/* DopplerPitchScalar | ||||
* Adapted from algorithm published as a part of the webaudio specification: | ||||
* https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#Spatialization-doppler-shift | ||||
* -Chad | ||||
*/ | ||||
static inline void CalculateDoppler( | ||||
float SpeedOfSound, | ||||
const F3DAUDIO_LISTENER* pListener, | ||||
const F3DAUDIO_EMITTER* pEmitter, | ||||
F3DAUDIO_VECTOR emitterToListener, | ||||
float eToLDistance, | ||||
float* listenerVelocityComponent, | ||||
float* emitterVelocityComponent, | ||||
float* DopplerFactor | ||||
) { | ||||
float scaledSpeedOfSound; | ||||
*DopplerFactor = 1.0f; | ||||
/* Project... */ | ||||
if (eToLDistance != 0.0f) | ||||
{ | ||||
*listenerVelocityComponent = | ||||
VectorDot(emitterToListener, pListener->Velocity) / eToLDistance; | ||||
*emitterVelocityComponent = | ||||
VectorDot(emitterToListener, pEmitter->Velocity) / eToLDistance; | ||||
} | ||||
else | ||||
{ | ||||
*listenerVelocityComponent = 0.0f; | ||||
*emitterVelocityComponent = 0.0f; | ||||
} | ||||
if (pEmitter->DopplerScaler > 0.0f) | ||||
{ | ||||
scaledSpeedOfSound = SpeedOfSound / pEmitter->DopplerScaler; | ||||
/* Clamp... */ | ||||
*listenerVelocityComponent = FAudio_min( | ||||
*listenerVelocityComponent, | ||||
scaledSpeedOfSound | ||||
); | ||||
*emitterVelocityComponent = FAudio_min( | ||||
*emitterVelocityComponent, | ||||
scaledSpeedOfSound | ||||
); | ||||
/* ... then Multiply. */ | ||||
*DopplerFactor = ( | ||||
SpeedOfSound - pEmitter->DopplerScaler * *listenerVelocityComponent | ||||
) / ( | ||||
SpeedOfSound - pEmitter->DopplerScaler * *emitterVelocityComponent | ||||
); | ||||
if (isnan(*DopplerFactor)) /* If emitter/listener are at the same pos... */ | ||||
{ | ||||
*DopplerFactor = 1.0f; | ||||
} | ||||
/* Limit the pitch shifting to 2 octaves up and 1 octave down */ | ||||
*DopplerFactor = FAudio_clamp( | ||||
*DopplerFactor, | ||||
0.5f, | ||||
4.0f | ||||
); | ||||
} | ||||
} | ||||
void F3DAudioCalculate( | ||||
const F3DAUDIO_HANDLE Instance, | ||||
const F3DAUDIO_LISTENER *pListener, | ||||
const F3DAUDIO_EMITTER *pEmitter, | ||||
uint32_t Flags, | ||||
F3DAUDIO_DSP_SETTINGS *pDSPSettings | ||||
) { | ||||
uint32_t i; | ||||
F3DAUDIO_VECTOR emitterToListener; | ||||
float eToLDistance, normalizedDistance, dp; | ||||
#define DEFAULT_POINTS(name, x1, y1, x2, y2) \ | ||||
static F3DAUDIO_DISTANCE_CURVE_POINT name##Points[2] = \ | ||||
{ \ | ||||
{ x1, y1 }, \ | ||||
{ x2, y2 } \ | ||||
}; \ | ||||
static F3DAUDIO_DISTANCE_CURVE name##Default = \ | ||||
{ \ | ||||
(F3DAUDIO_DISTANCE_CURVE_POINT*) &name##Points[0], 2 \ | ||||
}; | ||||
DEFAULT_POINTS(lpfDirect, 0.0f, 1.0f, 1.0f, 0.75f) | ||||
DEFAULT_POINTS(lpfReverb, 0.0f, 0.75f, 1.0f, 0.75f) | ||||
DEFAULT_POINTS(reverb, 0.0f, 1.0f, 1.0f, 0.0f) | ||||
#undef DEFAULT_POINTS | ||||
/* For XACT, this calculates "Distance" */ | ||||
emitterToListener = VectorSub(pListener->Position, pEmitter->Position); | ||||
eToLDistance = VectorLength(emitterToListener); | ||||
pDSPSettings->EmitterToListenerDistance = eToLDistance; | ||||
F3DAudioCheckCalculateParams(Instance, pListener, pEmitter, Flags, pDSPSettings); | ||||
/* This is used by MATRIX, LPF, and REVERB */ | ||||
normalizedDistance = eToLDistance / pEmitter->CurveDistanceScaler; | ||||
if (Flags & F3DAUDIO_CALCULATE_MATRIX) | ||||
{ | ||||
CalculateMatrix( | ||||
SPEAKERMASK(Instance), | ||||
Flags, | ||||
pListener, | ||||
pEmitter, | ||||
pDSPSettings->SrcChannelCount, | ||||
pDSPSettings->DstChannelCount, | ||||
emitterToListener, | ||||
eToLDistance, | ||||
normalizedDistance, | ||||
pDSPSettings->pMatrixCoefficients | ||||
); | ||||
} | ||||
if (Flags & F3DAUDIO_CALCULATE_LPF_DIRECT) | ||||
{ | ||||
pDSPSettings->LPFDirectCoefficient = ComputeDistanceAttenuation( | ||||
normalizedDistance, | ||||
(pEmitter->pLPFDirectCurve != NULL) ? | ||||
pEmitter->pLPFDirectCurve : | ||||
&lpfDirectDefault | ||||
); | ||||
} | ||||
if (Flags & F3DAUDIO_CALCULATE_LPF_REVERB) | ||||
{ | ||||
pDSPSettings->LPFReverbCoefficient = ComputeDistanceAttenuation( | ||||
normalizedDistance, | ||||
(pEmitter->pLPFReverbCurve != NULL) ? | ||||
pEmitter->pLPFReverbCurve : | ||||
&lpfReverbDefault | ||||
); | ||||
} | ||||
if (Flags & F3DAUDIO_CALCULATE_REVERB) | ||||
{ | ||||
pDSPSettings->ReverbLevel = ComputeDistanceAttenuation( | ||||
normalizedDistance, | ||||
(pEmitter->pReverbCurve != NULL) ? | ||||
pEmitter->pReverbCurve : | ||||
&reverbDefault | ||||
); | ||||
} | ||||
/* For XACT, this calculates "DopplerPitchScalar" */ | ||||
if (Flags & F3DAUDIO_CALCULATE_DOPPLER) | ||||
{ | ||||
CalculateDoppler( | ||||
SPEEDOFSOUND(Instance), | ||||
pListener, | ||||
pEmitter, | ||||
emitterToListener, | ||||
eToLDistance, | ||||
&pDSPSettings->ListenerVelocityComponent, | ||||
&pDSPSettings->EmitterVelocityComponent, | ||||
&pDSPSettings->DopplerFactor | ||||
); | ||||
} | ||||
/* For XACT, this calculates "OrientationAngle" */ | ||||
if (Flags & F3DAUDIO_CALCULATE_EMITTER_ANGLE) | ||||
{ | ||||
/* Determined roughly. | ||||
* Below that distance, the emitter angle is considered to be PI/2. | ||||
*/ | ||||
#define EMITTER_ANGLE_NULL_DISTANCE 1.2e-7 | ||||
if (eToLDistance < EMITTER_ANGLE_NULL_DISTANCE) | ||||
{ | ||||
pDSPSettings->EmitterToListenerAngle = F3DAUDIO_PI / 2.0f; | ||||
} | ||||
else | ||||
{ | ||||
/* Note: pEmitter->OrientFront is normalized. */ | ||||
dp = VectorDot(emitterToListener, pEmitter->OrientFront) / eToLDistance; | ||||
pDSPSettings->EmitterToListenerAngle = FAudio_acosf(dp); | ||||
} | ||||
} | ||||
/* Unimplemented Flags */ | ||||
if ( (Flags & F3DAUDIO_CALCULATE_DELAY) && | ||||
SPEAKERMASK(Instance) == SPEAKER_STEREO ) | ||||
{ | ||||
for (i = 0; i < pDSPSettings->DstChannelCount; i += 1) | ||||
{ | ||||
pDSPSettings->pDelayTimes[i] = 0.0f; | ||||
} | ||||
FAudio_assert(0 && "DELAY not implemented!"); | ||||
} | ||||
} | ||||
/* vim: set noexpandtab shiftwidth=8 tabstop=8: */ | ||||