From 40311310d1a6d2fde2ee9f04bfa1f21ced7cbee2 Mon Sep 17 00:00:00 2001
From: Mary-nyan <mary@mary.zone>
Date: Tue, 6 Dec 2022 15:04:25 +0100
Subject: amadeus: Add missing compressor effect from REV11 (#4010)

* amadeus: Add missing compressor effect from REV11

This was in my reversing notes but seems I completely forgot to
implement it

Also took the opportunity to simplify the Limiter effect a bit.

* Remove some outdated comment

* Address gdkchan's comments
---
 Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs  |   3 +-
 .../Renderer/Dsp/Command/CompressorCommand.cs      | 173 +++++++++++++++++++++
 .../Renderer/Dsp/Command/LimiterCommandVersion1.cs |  15 +-
 .../Renderer/Dsp/Command/LimiterCommandVersion2.cs |  17 +-
 .../Dsp/Effect/ExponentialMovingAverage.cs         |  26 ++++
 Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs     |   6 +
 Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs  |  48 ++++++
 .../Renderer/Dsp/State/CompressorState.cs          |  51 ++++++
 Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs   |  13 +-
 9 files changed, 328 insertions(+), 24 deletions(-)
 create mode 100644 Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
 create mode 100644 Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs
 create mode 100644 Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs

(limited to 'Ryujinx.Audio/Renderer/Dsp')

diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
index dfe7f886..9ce181b1 100644
--- a/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
@@ -31,6 +31,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
         LimiterVersion1,
         LimiterVersion2,
         GroupedBiquadFilter,
-        CaptureBuffer
+        CaptureBuffer,
+        Compressor
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs b/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
new file mode 100644
index 00000000..8c344293
--- /dev/null
+++ b/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Diagnostics;
+using Ryujinx.Audio.Renderer.Dsp.Effect;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter.Effect;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+    public class CompressorCommand : ICommand
+    {
+        private const int FixedPointPrecision = 15;
+
+        public bool Enabled { get; set; }
+
+        public int NodeId { get; }
+
+        public CommandType CommandType => CommandType.Compressor;
+
+        public uint EstimatedProcessingTime { get; set; }
+
+        public CompressorParameter Parameter => _parameter;
+        public Memory<CompressorState> State { get; }
+        public ushort[] OutputBufferIndices { get; }
+        public ushort[] InputBufferIndices { get; }
+        public bool IsEffectEnabled { get; }
+
+        private CompressorParameter _parameter;
+
+        public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, bool isEnabled, int nodeId)
+        {
+            Enabled = true;
+            NodeId = nodeId;
+            _parameter = parameter;
+            State = state;
+
+            IsEffectEnabled = isEnabled;
+
+            InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
+            OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
+
+            for (int i = 0; i < _parameter.ChannelCount; i++)
+            {
+                InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+                OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
+            }
+        }
+
+        public void Process(CommandList context)
+        {
+            ref CompressorState state = ref State.Span[0];
+
+            if (IsEffectEnabled)
+            {
+                if (_parameter.Status == Server.Effect.UsageState.Invalid)
+                {
+                    state = new CompressorState(ref _parameter);
+                }
+                else if (_parameter.Status == Server.Effect.UsageState.New)
+                {
+                    state.UpdateParameter(ref _parameter);
+                }
+            }
+
+            ProcessCompressor(context, ref state);
+        }
+
+        private unsafe void ProcessCompressor(CommandList context, ref CompressorState state)
+        {
+            Debug.Assert(_parameter.IsChannelCountValid());
+
+            if (IsEffectEnabled && _parameter.IsChannelCountValid())
+            {
+                Span<IntPtr> inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+                Span<IntPtr> outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+                Span<float> channelInput = stackalloc float[Parameter.ChannelCount];
+                ExponentialMovingAverage inputMovingAverage = state.InputMovingAverage;
+                float unknown4 = state.Unknown4;
+                ExponentialMovingAverage compressionGainAverage = state.CompressionGainAverage;
+                float previousCompressionEmaAlpha = state.PreviousCompressionEmaAlpha;
+
+                for (int i = 0; i < _parameter.ChannelCount; i++)
+                {
+                    inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
+                    outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
+                }
+
+                for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
+                {
+                    for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
+                    {
+                        channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex);
+                    }
+
+                    float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
+                    float y = FloatingPointHelper.Log10(newMean) * 10.0f;
+                    float z = 0.0f;
+
+                    bool unknown10OutOfRange = false;
+
+                    if (newMean < 1.0e-10f)
+                    {
+                        z = 1.0f;
+
+                        unknown10OutOfRange = state.Unknown10 < -100.0f;
+                    }
+
+                    if (y >= state.Unknown10 || unknown10OutOfRange)
+                    {
+                        float tmpGain;
+
+                        if (y >= state.Unknown14)
+                        {
+                            tmpGain = ((1.0f / Parameter.Ratio) - 1.0f) * (y - Parameter.Threshold);
+                        }
+                        else
+                        {
+                            tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction);
+                        }
+
+                        z = FloatingPointHelper.DecibelToLinearExtended(tmpGain);
+                    }
+
+                    float unknown4New = z;
+                    float compressionEmaAlpha;
+
+                    if ((unknown4 - z) <= 0.08f)
+                    {
+                        compressionEmaAlpha = Parameter.ReleaseCoefficient;
+
+                        if ((unknown4 - z) >= -0.08f)
+                        {
+                            if (MathF.Abs(compressionGainAverage.Read() - z) >= 0.001f)
+                            {
+                                unknown4New = unknown4;
+                            }
+
+                            compressionEmaAlpha = previousCompressionEmaAlpha;
+                        }
+                    }
+                    else
+                    {
+                        compressionEmaAlpha = Parameter.AttackCoefficient;
+                    }
+
+                    float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha);
+
+                    for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+                    {
+                        *((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain;
+                    }
+
+                    unknown4 = unknown4New;
+                    previousCompressionEmaAlpha = compressionEmaAlpha;
+                }
+
+                state.InputMovingAverage = inputMovingAverage;
+                state.Unknown4 = unknown4;
+                state.CompressionGainAverage = compressionGainAverage;
+                state.PreviousCompressionEmaAlpha = previousCompressionEmaAlpha;
+            }
+            else
+            {
+                for (int i = 0; i < Parameter.ChannelCount; i++)
+                {
+                    if (InputBufferIndices[i] != OutputBufferIndices[i])
+                    {
+                        context.CopyBuffer(OutputBufferIndices[i], InputBufferIndices[i]);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
index 9cfef736..a464ad70 100644
--- a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
@@ -90,32 +90,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                         float inputCoefficient = Parameter.ReleaseCoefficient;
 
-                        if (sampleInputMax > state.DectectorAverage[channelIndex])
+                        if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
                             inputCoefficient = Parameter.AttackCoefficient;
                         }
 
-                        state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]);
-
+                        float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
 
-                        if (state.DectectorAverage[channelIndex] > Parameter.Threshold)
+                        if (detectorValue > Parameter.Threshold)
                         {
-                            attenuation = Parameter.Threshold / state.DectectorAverage[channelIndex];
+                            attenuation = Parameter.Threshold / detectorValue;
                         }
 
                         float outputCoefficient = Parameter.ReleaseCoefficient;
 
-                        if (state.CompressionGain[channelIndex] > attenuation)
+                        if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
                             outputCoefficient = Parameter.AttackCoefficient;
                         }
 
-                        state.CompressionGain[channelIndex] += outputCoefficient * (attenuation - state.CompressionGain[channelIndex]);
+                        float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
 
                         ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
 
-                        float outputSample = delayedSample * state.CompressionGain[channelIndex] * Parameter.OutputGain;
+                        float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
 
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
 
diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
index 46c95e4f..950de97b 100644
--- a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
@@ -101,32 +101,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                         float inputCoefficient = Parameter.ReleaseCoefficient;
 
-                        if (sampleInputMax > state.DectectorAverage[channelIndex])
+                        if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
                             inputCoefficient = Parameter.AttackCoefficient;
                         }
 
-                        state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]);
-
+                        float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
 
-                        if (state.DectectorAverage[channelIndex] > Parameter.Threshold)
+                        if (detectorValue > Parameter.Threshold)
                         {
-                            attenuation = Parameter.Threshold / state.DectectorAverage[channelIndex];
+                            attenuation = Parameter.Threshold / detectorValue;
                         }
 
                         float outputCoefficient = Parameter.ReleaseCoefficient;
 
-                        if (state.CompressionGain[channelIndex] > attenuation)
+                        if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
                             outputCoefficient = Parameter.AttackCoefficient;
                         }
 
-                        state.CompressionGain[channelIndex] += outputCoefficient * (attenuation - state.CompressionGain[channelIndex]);
+                        float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient);
 
                         ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]];
 
-                        float outputSample = delayedSample * state.CompressionGain[channelIndex] * Parameter.OutputGain;
+                        float outputSample = delayedSample * compressionGain * Parameter.OutputGain;
 
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
 
@@ -144,7 +143,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
                             ref LimiterStatistics statistics = ref MemoryMarshal.Cast<byte, LimiterStatistics>(ResultState.Span[0].SpecificData)[0];
 
                             statistics.InputMax[channelIndex] = Math.Max(statistics.InputMax[channelIndex], sampleInputMax);
-                            statistics.CompressionGainMin[channelIndex] = Math.Min(statistics.CompressionGainMin[channelIndex], state.CompressionGain[channelIndex]);
+                            statistics.CompressionGainMin[channelIndex] = Math.Min(statistics.CompressionGainMin[channelIndex], compressionGain);
                         }
                     }
                 }
diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs
new file mode 100644
index 00000000..78e46bf9
--- /dev/null
+++ b/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs
@@ -0,0 +1,26 @@
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Effect
+{
+    public struct ExponentialMovingAverage
+    {
+        private float _mean;
+
+        public ExponentialMovingAverage(float mean)
+        {
+            _mean = mean;
+        }
+
+        public float Read()
+        {
+            return _mean;
+        }
+
+        public float Update(float value, float alpha)
+        {
+            _mean += alpha * (value - _mean);
+
+            return _mean;
+        }
+    }
+}
diff --git a/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs
index 0d0ff2ae..280e47c0 100644
--- a/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs
@@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Renderer.Dsp
             return (float)value / (1 << qBits);
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float ConvertFloat(float value, int qBits)
+        {
+            return value / (1 << qBits);
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static int ToFixed(float value, int qBits)
         {
diff --git a/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
index 226def46..6645e20a 100644
--- a/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Reflection.Metadata;
 using System.Runtime.CompilerServices;
 
 namespace Ryujinx.Audio.Renderer.Dsp
@@ -46,6 +47,53 @@ namespace Ryujinx.Audio.Renderer.Dsp
             return MathF.Pow(10, x);
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float Log10(float x)
+        {
+            // NOTE: Nintendo uses an approximation of log10, we don't.
+            // As such, we support the same ranges as Nintendo to avoid unexpected behaviours.
+            return MathF.Pow(10, MathF.Max(x, 1.0e-10f));
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float MeanSquare(ReadOnlySpan<float> inputs)
+        {
+            float res = 0.0f;
+
+            foreach (float input in inputs)
+            {
+                res += (input * input);
+            }
+
+            res /= inputs.Length;
+
+            return res;
+        }
+
+        /// <summary>
+        /// Map decibel to linear.
+        /// </summary>
+        /// <param name="db">The decibel value to convert</param>
+        /// <returns>Converted linear value/returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float DecibelToLinear(float db)
+        {
+            return MathF.Pow(10.0f, db / 20.0f);
+        }
+
+        /// <summary>
+        /// Map decibel to linear in [0, 2] range.
+        /// </summary>
+        /// <param name="db">The decibel value to convert</param>
+        /// <returns>Converted linear value in [0, 2] range</returns>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float DecibelToLinearExtended(float db)
+        {
+            float tmp = MathF.Log2(DecibelToLinear(db));
+
+            return MathF.Truncate(tmp) + MathF.Pow(2.0f, tmp - MathF.Truncate(tmp));
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static float DegreesToRadians(float degrees)
         {
diff --git a/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs b/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
new file mode 100644
index 00000000..76aff807
--- /dev/null
+++ b/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
@@ -0,0 +1,51 @@
+using Ryujinx.Audio.Renderer.Dsp.Effect;
+using Ryujinx.Audio.Renderer.Parameter.Effect;
+
+namespace Ryujinx.Audio.Renderer.Dsp.State
+{
+    public class CompressorState
+    {
+        public ExponentialMovingAverage InputMovingAverage;
+        public float Unknown4;
+        public ExponentialMovingAverage CompressionGainAverage;
+        public float CompressorGainReduction;
+        public float Unknown10;
+        public float Unknown14;
+        public float PreviousCompressionEmaAlpha;
+        public float MakeupGain;
+        public float OutputGain;
+
+        public CompressorState(ref CompressorParameter parameter)
+        {
+            InputMovingAverage = new ExponentialMovingAverage(0.0f);
+            Unknown4 = 1.0f;
+            CompressionGainAverage = new ExponentialMovingAverage(1.0f);
+
+            UpdateParameter(ref parameter);
+        }
+
+        public void UpdateParameter(ref CompressorParameter parameter)
+        {
+            float threshold = parameter.Threshold;
+            float ratio = 1.0f / parameter.Ratio;
+            float attackCoefficient = parameter.AttackCoefficient;
+            float makeupGain;
+
+            if (parameter.MakeupGainEnabled)
+            {
+                makeupGain = (threshold * 0.5f * (ratio - 1.0f)) - 3.0f;
+            }
+            else
+            {
+                makeupGain = 0.0f;
+            }
+
+            PreviousCompressionEmaAlpha = attackCoefficient;
+            MakeupGain = makeupGain;
+            CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax;
+            Unknown10 = threshold - 1.5f;
+            Unknown14 = threshold + 1.5f;
+            OutputGain = FloatingPointHelper.DecibelToLinearExtended(parameter.OutputGain + makeupGain);
+        }
+    }
+}
diff --git a/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs b/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
index 92ed13ff..0560757c 100644
--- a/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
+++ b/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Audio.Renderer.Dsp.Effect;
 using Ryujinx.Audio.Renderer.Parameter.Effect;
 using System;
 
@@ -5,20 +6,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
 {
     public class LimiterState
     {
-        public float[] DectectorAverage;
-        public float[] CompressionGain;
+        public ExponentialMovingAverage[] DetectorAverage;
+        public ExponentialMovingAverage[] CompressionGainAverage;
         public float[] DelayedSampleBuffer;
         public int[] DelayedSampleBufferPosition;
 
         public LimiterState(ref LimiterParameter parameter, ulong workBuffer)
         {
-            DectectorAverage = new float[parameter.ChannelCount];
-            CompressionGain = new float[parameter.ChannelCount];
+            DetectorAverage = new ExponentialMovingAverage[parameter.ChannelCount];
+            CompressionGainAverage = new ExponentialMovingAverage[parameter.ChannelCount];
             DelayedSampleBuffer = new float[parameter.ChannelCount * parameter.DelayBufferSampleCountMax];
             DelayedSampleBufferPosition = new int[parameter.ChannelCount];
 
-            DectectorAverage.AsSpan().Fill(0.0f);
-            CompressionGain.AsSpan().Fill(1.0f);
+            DetectorAverage.AsSpan().Fill(new ExponentialMovingAverage(0.0f));
+            CompressionGainAverage.AsSpan().Fill(new ExponentialMovingAverage(1.0f));
             DelayedSampleBufferPosition.AsSpan().Fill(0);
             DelayedSampleBuffer.AsSpan().Fill(0.0f);
 
-- 
cgit v1.2.3-70-g09d2