From 03930e42e47619af1b84a11aa21b937404e290fc Mon Sep 17 00:00:00 2001
From: Mark Harmstone <mark@harmstone.com>
Date: Sun, 22 Mar 2015 18:21:02 +0000
Subject: dsound: Implement EAX late reverb.

---
 dlls/dsound/dsound_eax.h |  13 +++
 dlls/dsound/eax.c        | 256 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 268 insertions(+), 1 deletion(-)

diff --git a/dlls/dsound/dsound_eax.h b/dlls/dsound/dsound_eax.h
index c33a2ff..fac4d9b 100644
--- a/dlls/dsound/dsound_eax.h
+++ b/dlls/dsound/dsound_eax.h
@@ -154,6 +154,19 @@ typedef struct {
     DelayLine Decorrelator;
     unsigned int DecoTap[3];
 
+    struct {
+        float Gain;
+        float DensityGain;
+        float MixCoeff;
+
+        float Coeff[4];
+        DelayLine Delay[4];
+        unsigned int Offset[4];
+
+        float LpCoeff[4];
+        float LpSample[4];
+    } Late;
+
     unsigned int Offset;
 } eax_buffer_info;
 
diff --git a/dlls/dsound/eax.c b/dlls/dsound/eax.c
index d40b7d6..c9a5eb6 100644
--- a/dlls/dsound/eax.c
+++ b/dlls/dsound/eax.c
@@ -107,6 +107,8 @@ static const float LATE_LINE_LENGTH[4] =
 
 static const float LATE_LINE_MULTIPLIER = 4.0f;
 
+#define SPEEDOFSOUNDMETRESPERSEC 343.3f
+
 static float lpFilter2P(FILTER *iir, unsigned int offset, float input)
 {
     float *history = &iir->history[offset*2];
@@ -121,6 +123,11 @@ static float lpFilter2P(FILTER *iir, unsigned int offset, float input)
     return output;
 }
 
+static float lerp(float val1, float val2, float mu)
+{
+    return val1 + (val2-val1)*mu;
+}
+
 static void DelayLineIn(DelayLine *Delay, unsigned int offset, float in)
 {
     Delay->Line[offset&Delay->Mask] = in;
@@ -187,8 +194,85 @@ static void EarlyReflection(IDirectSoundBufferImpl* dsb, float in, float *out)
     out[3] = dsb->eax.Early.Gain * f[3];
 }
 
+static float LateDelayLineOut(IDirectSoundBufferImpl* dsb, unsigned int index)
+{
+    return AttenuatedDelayLineOut(&dsb->eax.Late.Delay[index],
+                                  dsb->eax.Offset - dsb->eax.Late.Offset[index],
+                                  dsb->eax.Late.Coeff[index]);
+}
+
+static float LateLowPassInOut(IDirectSoundBufferImpl* dsb, unsigned int index, float in)
+{
+    in = lerp(in, dsb->eax.Late.LpSample[index], dsb->eax.Late.LpCoeff[index]);
+    dsb->eax.Late.LpSample[index] = in;
+    return in;
+}
+
+static void LateReverb(IDirectSoundBufferImpl* dsb, const float *in, float *out)
+{
+    float d[4], f[4];
+
+    /* Obtain the decayed results of the cyclical delay lines, and add the
+     * corresponding input channels.  Then pass the results through the
+     * low-pass filters. */
+
+    /* This is where the feed-back cycles from line 0 to 1 to 3 to 2 and back
+     * to 0. */
+    d[0] = LateLowPassInOut(dsb, 2, in[2] + LateDelayLineOut(dsb, 2));
+    d[1] = LateLowPassInOut(dsb, 0, in[0] + LateDelayLineOut(dsb, 0));
+    d[2] = LateLowPassInOut(dsb, 3, in[3] + LateDelayLineOut(dsb, 3));
+    d[3] = LateLowPassInOut(dsb, 1, in[1] + LateDelayLineOut(dsb, 1));
+
+    /* Late reverb is done with a modified feed-back delay network (FDN)
+     * topology.  Four input lines are each fed through their own all-pass
+     * filter and then into the mixing matrix.  The four outputs of the
+     * mixing matrix are then cycled back to the inputs.  Each output feeds
+     * a different input to form a circlular feed cycle.
+     *
+     * The mixing matrix used is a 4D skew-symmetric rotation matrix derived
+     * using a single unitary rotational parameter:
+     *
+     *  [  d,  a,  b,  c ]          1 = a^2 + b^2 + c^2 + d^2
+     *  [ -a,  d,  c, -b ]
+     *  [ -b, -c,  d,  a ]
+     *  [ -c,  b, -a,  d ]
+     *
+     * The rotation is constructed from the effect's diffusion parameter,
+     * yielding:  1 = x^2 + 3 y^2; where a, b, and c are the coefficient y
+     * with differing signs, and d is the coefficient x.  The matrix is thus:
+     *
+     *  [  x,  y, -y,  y ]          n = sqrt(matrix_order - 1)
+     *  [ -y,  x,  y,  y ]          t = diffusion_parameter * atan(n)
+     *  [  y, -y,  x,  y ]          x = cos(t)
+     *  [ -y, -y, -y,  x ]          y = sin(t) / n
+     *
+     * To reduce the number of multiplies, the x coefficient is applied with
+     * the cyclical delay line coefficients.  Thus only the y coefficient is
+     * applied when mixing, and is modified to be:  y / x.
+     */
+    f[0] = d[0] + (dsb->eax.Late.MixCoeff * (         d[1] + -d[2] + d[3]));
+    f[1] = d[1] + (dsb->eax.Late.MixCoeff * (-d[0]         +  d[2] + d[3]));
+    f[2] = d[2] + (dsb->eax.Late.MixCoeff * ( d[0] + -d[1]         + d[3]));
+    f[3] = d[3] + (dsb->eax.Late.MixCoeff * (-d[0] + -d[1] + -d[2]       ));
+
+    /* Output the results of the matrix for all four channels, attenuated by
+     * the late reverb gain (which is attenuated by the 'x' mix coefficient). */
+    out[0] = dsb->eax.Late.Gain * f[0];
+    out[1] = dsb->eax.Late.Gain * f[1];
+    out[2] = dsb->eax.Late.Gain * f[2];
+    out[3] = dsb->eax.Late.Gain * f[3];
+
+    /* Re-feed the cyclical delay lines. */
+    DelayLineIn(&dsb->eax.Late.Delay[0], dsb->eax.Offset, f[0]);
+    DelayLineIn(&dsb->eax.Late.Delay[1], dsb->eax.Offset, f[1]);
+    DelayLineIn(&dsb->eax.Late.Delay[2], dsb->eax.Offset, f[2]);
+    DelayLineIn(&dsb->eax.Late.Delay[3], dsb->eax.Offset, f[3]);
+}
+
 static void VerbPass(IDirectSoundBufferImpl* dsb, float in, float* out)
 {
+    float feed, late[4], taps[4];
+
     /* Low-pass filter the incoming sample. */
     in = lpFilter2P(&dsb->eax.LpFilter, 0, in);
 
@@ -199,6 +283,25 @@ static void VerbPass(IDirectSoundBufferImpl* dsb, float in, float* out)
     in = DelayLineOut(&dsb->eax.Delay, dsb->eax.Offset - dsb->eax.DelayTap[0]);
     EarlyReflection(dsb, in, out);
 
+    /* Feed the decorrelator from the energy-attenuated output of the second
+     * delay tap. */
+    in = DelayLineOut(&dsb->eax.Delay, dsb->eax.Offset - dsb->eax.DelayTap[1]);
+    feed = in * dsb->eax.Late.DensityGain;
+    DelayLineIn(&dsb->eax.Decorrelator, dsb->eax.Offset, feed);
+
+    /* Calculate the late reverb from the decorrelator taps. */
+    taps[0] = feed;
+    taps[1] = DelayLineOut(&dsb->eax.Decorrelator, dsb->eax.Offset - dsb->eax.DecoTap[0]);
+    taps[2] = DelayLineOut(&dsb->eax.Decorrelator, dsb->eax.Offset - dsb->eax.DecoTap[1]);
+    taps[3] = DelayLineOut(&dsb->eax.Decorrelator, dsb->eax.Offset - dsb->eax.DecoTap[2]);
+    LateReverb(dsb, taps, late);
+
+    /* Mix early reflections and late reverb. */
+    out[0] += late[0];
+    out[1] += late[1];
+    out[2] += late[2];
+    out[3] += late[3];
+
     /* Step all delays forward one sample. */
     dsb->eax.Offset++;
 }
@@ -308,6 +411,95 @@ static float CalcI3DL2HFreq(float hfRef, unsigned int frequency)
     return cosf(M_PI*2.0f * hfRef / frequency);
 }
 
+static float CalcDensityGain(float a)
+{
+    return sqrtf(1.0f - (a * a));
+}
+
+static float CalcDampingCoeff(float hfRatio, float length, float decayTime, float decayCoeff, float cw)
+{
+    float coeff, g;
+
+    coeff = 0.0f;
+    if (hfRatio < 1.0f)
+    {
+        /* Calculate the low-pass coefficient by dividing the HF decay
+         * coefficient by the full decay coefficient. */
+        g = CalcDecayCoeff(length, decayTime * hfRatio) / decayCoeff;
+
+        /* Damping is done with a 1-pole filter, so g needs to be squared. */
+        g *= g;
+        coeff = lpCoeffCalc(g, cw);
+
+        /* Very low decay times will produce minimal output, so apply an
+         * upper bound to the coefficient. */
+        if (coeff > 0.98f) coeff = 0.98f;
+    }
+    return coeff;
+}
+
+static void UpdateLateLines(float reverbGain, float lateGain, float xMix, float density, float decayTime,
+                            float diffusion, float hfRatio, float cw, unsigned int frequency, eax_buffer_info *State)
+{
+    float length;
+    unsigned int index;
+
+    /* Calculate the late reverb gain (from the master effect gain, and late
+     * reverb gain parameters).  Since the output is tapped prior to the
+     * application of the next delay line coefficients, this gain needs to be
+     * attenuated by the 'x' mixing matrix coefficient as well.
+     */
+    State->Late.Gain = reverbGain * lateGain * xMix;
+
+    /* To compensate for changes in modal density and decay time of the late
+     * reverb signal, the input is attenuated based on the maximal energy of
+     * the outgoing signal.  This approximation is used to keep the apparent
+     * energy of the signal equal for all ranges of density and decay time.
+     *
+     * The average length of the cyclcical delay lines is used to calculate
+     * the attenuation coefficient.
+     */
+    length = (LATE_LINE_LENGTH[0] + LATE_LINE_LENGTH[1] +
+              LATE_LINE_LENGTH[2] + LATE_LINE_LENGTH[3]) / 4.0f;
+    length *= 1.0f + (density * LATE_LINE_MULTIPLIER);
+    State->Late.DensityGain = CalcDensityGain(CalcDecayCoeff(length,
+                                                             decayTime));
+
+    for(index = 0; index < 4; index++)
+    {
+        /* Calculate the length (in seconds) of each cyclical delay line. */
+        length = LATE_LINE_LENGTH[index] * (1.0f + (density * LATE_LINE_MULTIPLIER));
+
+        /* Calculate the delay offset for each cyclical delay line. */
+        State->Late.Offset[index] = fastf2u(length * frequency);
+
+        /* Calculate the gain (coefficient) for each cyclical line. */
+        State->Late.Coeff[index] = CalcDecayCoeff(length, decayTime);
+
+        /* Calculate the damping coefficient for each low-pass filter. */
+        State->Late.LpCoeff[index] = CalcDampingCoeff(hfRatio, length, decayTime,
+                             State->Late.Coeff[index], cw);
+
+        /* Attenuate the cyclical line coefficients by the mixing coefficient
+         * (x). */
+        State->Late.Coeff[index] *= xMix;
+    }
+}
+
+static void CalcMatrixCoeffs(float diffusion, float *x, float *y)
+{
+    float n, t;
+
+    /* The matrix is of order 4, so n is sqrt (4 - 1). */
+    n = sqrtf(3.0f);
+    t = diffusion * atanf(n);
+
+    /* Calculate the first mixing matrix coefficient. */
+    *x = cosf(t);
+    /* Calculate the second mixing matrix coefficient. */
+    *y = sinf(t) / n;
+}
+
 static unsigned int NextPowerOf2(unsigned int value)
 {
     if (value > 0)
@@ -370,6 +562,14 @@ static BOOL AllocLines(unsigned int frequency, IDirectSoundBufferImpl *dsb)
     totalSamples += CalcLineLength(length, totalSamples, frequency,
                                    &dsb->eax.Decorrelator);
 
+    /* The late delay lines are calculated from the lowest reverb density. */
+    for (index = 0; index < 4; index++)
+    {
+        length = LATE_LINE_LENGTH[index] * (1.0f + LATE_LINE_MULTIPLIER);
+        totalSamples += CalcLineLength(length, totalSamples, frequency,
+                                       &dsb->eax.Late.Delay[index]);
+    }
+
     if (totalSamples != dsb->eax.TotalSamples)
     {
         TRACE("New reverb buffer length: %u samples (%f sec)\n", totalSamples, totalSamples/(float)frequency);
@@ -391,6 +591,7 @@ static BOOL AllocLines(unsigned int frequency, IDirectSoundBufferImpl *dsb)
     for(index = 0; index < 4; index++)
     {
         RealizeLineOffset(dsb->eax.SampleBuffer, &dsb->eax.Early.Delay[index]);
+        RealizeLineOffset(dsb->eax.SampleBuffer, &dsb->eax.Late.Delay[index]);
     }
 
     /* Clear the sample buffer. */
@@ -400,10 +601,35 @@ static BOOL AllocLines(unsigned int frequency, IDirectSoundBufferImpl *dsb)
     return TRUE;
 }
 
+static inline float CalcDecayLength(float coeff, float decayTime)
+{
+    return log10f(coeff) * decayTime / log10f(0.001f)/*-60 dB*/;
+}
+
+static float CalcLimitedHfRatio(float hfRatio, float airAbsorptionGainHF, float decayTime)
+{
+    float limitRatio;
+
+    /* Find the attenuation due to air absorption in dB (converting delay
+     * time to meters using the speed of sound).  Then reversing the decay
+     * equation, solve for HF ratio.  The delay length is cancelled out of
+     * the equation, so it can be calculated once for all lines.
+     */
+    limitRatio = 1.0f / (CalcDecayLength(airAbsorptionGainHF, decayTime) *
+                         SPEEDOFSOUNDMETRESPERSEC);
+    /* Using the limit calculated above, apply the upper bound to the HF
+     * ratio. Also need to limit the result to a minimum of 0.1, just like the
+     * HF ratio parameter. */
+    if (limitRatio < 0.1f) limitRatio = 0.1f;
+    else if (limitRatio > hfRatio) limitRatio = hfRatio;
+
+    return limitRatio;
+}
+
 static void ReverbUpdate(IDirectSoundBufferImpl *dsb)
 {
     unsigned int index;
-    float cw;
+    float cw, hfRatio, x, y;
 
     /* avoid segfaults in mixing thread when we recalculate the line offsets */
     EnterCriticalSection(&dsb->device->mixlock);
@@ -430,6 +656,20 @@ static void ReverbUpdate(IDirectSoundBufferImpl *dsb)
                     dsb->device->eax.eax_props.flLateReverbDelay, &dsb->eax);
 
     UpdateDecorrelator(dsb->device->eax.eax_props.flDensity, dsb->device->pwfx->nSamplesPerSec, &dsb->eax);
+
+    CalcMatrixCoeffs(dsb->device->eax.eax_props.flDiffusion, &x, &y);
+    dsb->eax.Late.MixCoeff = y / x;
+
+    hfRatio = dsb->device->eax.eax_props.flDecayHFRatio;
+
+    if (dsb->device->eax.eax_props.iDecayHFLimit && dsb->device->eax.eax_props.flAirAbsorptionGainHF < 1.0f) {
+        hfRatio = CalcLimitedHfRatio(hfRatio, dsb->device->eax.eax_props.flAirAbsorptionGainHF,
+                                     dsb->device->eax.eax_props.flDecayTime);
+    }
+
+    UpdateLateLines(dsb->device->eax.eax_props.flGain, dsb->device->eax.eax_props.flLateReverbGain,
+                    x, dsb->device->eax.eax_props.flDensity, dsb->device->eax.eax_props.flDecayTime,
+                    dsb->device->eax.eax_props.flDiffusion, hfRatio, cw, dsb->device->pwfx->nSamplesPerSec, &dsb->eax);
 }
 
 static BOOL ReverbDeviceUpdate(DirectSoundDevice *dev)
@@ -474,6 +714,20 @@ void init_eax_buffer(IDirectSoundBufferImpl *dsb)
     dsb->eax.DecoTap[1] = 0;
     dsb->eax.DecoTap[2] = 0;
 
+    dsb->eax.Late.Gain = 0.0f;
+    dsb->eax.Late.DensityGain = 0.0f;
+    dsb->eax.Late.MixCoeff = 0.0f;
+    for(index = 0; index < 4; index++)
+    {
+        dsb->eax.Late.Coeff[index] = 0.0f;
+        dsb->eax.Late.Delay[index].Mask = 0;
+        dsb->eax.Late.Delay[index].Line = NULL;
+        dsb->eax.Late.Offset[index] = 0;
+
+        dsb->eax.Late.LpCoeff[index] = 0.0f;
+        dsb->eax.Late.LpSample[index] = 0.0f;
+    }
+
     dsb->eax.Offset = 0;
 
     ReverbUpdate(dsb);
-- 
2.3.3

