/* 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 * */ #include "F3DAudio.h" #include "FAudio_internal.h" #include /* ONLY USE THIS FOR isnan! */ #include /* 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: */