Sweden-Number/libs/faudio/src/F3DAudio.c

1564 lines
46 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* FAudio - XAudio Reimplementation for FNA
*
* Copyright (c) 2011-2021 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: */