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: [PATCH] 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/Common/EffectType.cs   |   7 +-
 .../Renderer/Common/PerformanceDetailType.cs  |   3 +-
 .../Renderer/Dsp/Command/CommandType.cs       |   3 +-
 .../Renderer/Dsp/Command/CompressorCommand.cs | 173 ++++++++++++++++++
 .../Dsp/Command/LimiterCommandVersion1.cs     |  15 +-
 .../Dsp/Command/LimiterCommandVersion2.cs     |  17 +-
 .../Dsp/Effect/ExponentialMovingAverage.cs    |  26 +++
 .../Renderer/Dsp/FixedPointHelper.cs          |   6 +
 .../Renderer/Dsp/FloatingPointHelper.cs       |  48 +++++
 .../Renderer/Dsp/State/CompressorState.cs     |  51 ++++++
 .../Renderer/Dsp/State/LimiterState.cs        |  13 +-
 .../Parameter/Effect/CompressorParameter.cs   | 115 ++++++++++++
 .../Renderer/Server/BehaviourContext.cs       |   3 +-
 .../Renderer/Server/CommandBuffer.cs          |  12 ++
 .../Renderer/Server/CommandGenerator.cs       |  14 ++
 .../CommandProcessingTimeEstimatorVersion1.cs |   5 +
 .../CommandProcessingTimeEstimatorVersion2.cs |   5 +
 .../CommandProcessingTimeEstimatorVersion3.cs |   5 +
 .../CommandProcessingTimeEstimatorVersion5.cs |  74 ++++++++
 .../Renderer/Server/Effect/BaseEffect.cs      |   2 +
 .../Server/Effect/CompressorEffect.cs         |  67 +++++++
 .../Server/ICommandProcessingTimeEstimator.cs |   1 +
 Ryujinx.Audio/Renderer/Server/StateUpdater.cs |   4 +
 .../Effect/CompressorParameterTests.cs        |  16 ++
 24 files changed, 658 insertions(+), 27 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
 create mode 100644 Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
 create mode 100644 Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
 create mode 100644 Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs

diff --git a/Ryujinx.Audio/Renderer/Common/EffectType.cs b/Ryujinx.Audio/Renderer/Common/EffectType.cs
index 2c50b9eb0d..7128db4ce1 100644
--- a/Ryujinx.Audio/Renderer/Common/EffectType.cs
+++ b/Ryujinx.Audio/Renderer/Common/EffectType.cs
@@ -48,6 +48,11 @@ namespace Ryujinx.Audio.Renderer.Common
         /// <summary>
         /// Effect to capture mixes (via auxiliary buffers).
         /// </summary>
-        CaptureBuffer
+        CaptureBuffer,
+
+        /// <summary>
+        /// Effect applying a compressor filter (DRC).
+        /// </summary>
+        Compressor,
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs
index 8467ed8d73..805d55183a 100644
--- a/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs
+++ b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs
@@ -14,6 +14,7 @@ namespace Ryujinx.Audio.Renderer.Common
         Reverb3d,
         PcmFloat,
         Limiter,
-        CaptureBuffer
+        CaptureBuffer,
+        Compressor
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
index dfe7f8862f..9ce181b179 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 0000000000..8c34429356
--- /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 9cfef736e3..a464ad704c 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 46c95e4f9d..950de97b89 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 0000000000..78e46bf962
--- /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 0d0ff2ae63..280e47c0c4 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 226def46a6..6645e20a32 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 0000000000..76aff80725
--- /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 92ed13ffff..0560757c98 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);
 
diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
new file mode 100644
index 0000000000..0be376088d
--- /dev/null
+++ b/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
@@ -0,0 +1,115 @@
+using Ryujinx.Audio.Renderer.Server.Effect;
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter.Effect
+{
+    /// <summary>
+    /// <see cref="IEffectInParameter.SpecificData"/> for <see cref="Common.EffectType.Compressor"/>.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack = 1)]
+    public struct CompressorParameter
+    {
+        /// <summary>
+        /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>.
+        /// </summary>
+        public Array6<byte> Input;
+
+        /// <summary>
+        /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>.
+        /// </summary>
+        public Array6<byte> Output;
+
+        /// <summary>
+        /// The maximum number of channels supported.
+        /// </summary>
+        public ushort ChannelCountMax;
+
+        /// <summary>
+        /// The total channel count used.
+        /// </summary>
+        public ushort ChannelCount;
+
+        /// <summary>
+        /// The target sample rate.
+        /// </summary>
+        /// <remarks>This is in kHz.</remarks>
+        public int SampleRate;
+
+        /// <summary>
+        /// The threshold.
+        /// </summary>
+        public float Threshold;
+
+        /// <summary>
+        /// The compressor ratio.
+        /// </summary>
+        public float Ratio;
+
+        /// <summary>
+        /// The attack time.
+        /// <remarks>This is in microseconds.</remarks>
+        /// </summary>
+        public int AttackTime;
+
+        /// <summary>
+        /// The release time.
+        /// <remarks>This is in microseconds.</remarks>
+        /// </summary>
+        public int ReleaseTime;
+
+        /// <summary>
+        /// The input gain.
+        /// </summary>
+        public float InputGain;
+
+        /// <summary>
+        /// The attack coefficient.
+        /// </summary>
+        public float AttackCoefficient;
+
+        /// <summary>
+        /// The release coefficient.
+        /// </summary>
+        public float ReleaseCoefficient;
+
+        /// <summary>
+        /// The output gain.
+        /// </summary>
+        public float OutputGain;
+
+        /// <summary>
+        /// The current usage status of the effect on the client side.
+        /// </summary>
+        public UsageState Status;
+
+        /// <summary>
+        /// Indicate if the makeup gain should be used.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool MakeupGainEnabled;
+
+        /// <summary>
+        /// Reserved/padding.
+        /// </summary>
+        private Array2<byte> _reserved;
+
+        /// <summary>
+        /// Check if the <see cref="ChannelCount"/> is valid.
+        /// </summary>
+        /// <returns>Returns true if the <see cref="ChannelCount"/> is valid.</returns>
+        public bool IsChannelCountValid()
+        {
+            return EffectInParameterVersion1.IsChannelCountValid(ChannelCount);
+        }
+
+        /// <summary>
+        /// Check if the <see cref="ChannelCountMax"/> is valid.
+        /// </summary>
+        /// <returns>Returns true if the <see cref="ChannelCountMax"/> is valid.</returns>
+        public bool IsChannelCountMaxValid()
+        {
+            return EffectInParameterVersion1.IsChannelCountValid(ChannelCountMax);
+        }
+    }
+}
diff --git a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
index adf5294ea0..821947a987 100644
--- a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
+++ b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
@@ -44,7 +44,7 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP.
         /// A new version of the command estimator was added to address timing changes caused by the voice changes.
         /// Additionally, the rendering limit percent was incremented to 80%.
-        /// 
+        ///
         /// </summary>
         /// <remarks>This was added in system update 6.0.0</remarks>
         public const int Revision5 = 5 << 24;
@@ -93,6 +93,7 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <summary>
         /// REV11:
         /// The "legacy" effects (Delay, Reverb and Reverb 3D) were updated to match the standard channel mapping used by the audio renderer.
+        /// A new effect was added: Compressor. This effect is effectively implemented with a DRC.
         /// A new version of the command estimator was added to address timing changes caused by the legacy effects changes.
         /// A voice drop parameter was added in 15.0.0: This allows an application to amplify or attenuate the estimated time of DSP commands.
         /// </summary>
diff --git a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
index e0741cc6e3..905cb2054d 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
@@ -469,6 +469,18 @@ namespace Ryujinx.Audio.Renderer.Server
             }
         }
 
+        public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, bool isEnabled, int nodeId)
+        {
+            if (parameter.IsChannelCountValid())
+            {
+                CompressorCommand command = new CompressorCommand(bufferOffset, parameter, state, isEnabled, nodeId);
+
+                command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
+
+                AddCommand(command);
+            }
+        }
+
         /// <summary>
         /// Generate a new <see cref="VolumeCommand"/>.
         /// </summary>
diff --git a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
index 87e5c77f98..afc1e39b72 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
@@ -606,6 +606,17 @@ namespace Ryujinx.Audio.Renderer.Server
             }
         }
 
+        private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId)
+        {
+            Debug.Assert(effect.Type == EffectType.Compressor);
+
+            _commandBuffer.GenerateCompressorEffect(bufferOffset,
+                                                    effect.Parameter,
+                                                    effect.State,
+                                                    effect.IsEnabled,
+                                                    nodeId);
+        }
+
         private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect)
         {
             int nodeId = mix.NodeId;
@@ -650,6 +661,9 @@ namespace Ryujinx.Audio.Renderer.Server
                 case EffectType.CaptureBuffer:
                     GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
                     break;
+                case EffectType.Compressor:
+                    GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId);
+                    break;
                 default:
                     throw new NotImplementedException($"Unsupported effect type {effect.Type}");
             }
diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
index 32c52dc43d..63dc9ca96d 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
@@ -179,5 +179,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
             return 0;
         }
+
+        public uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
index 15800c9939..7ee491cd61 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
@@ -543,5 +543,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
             return 0;
         }
+
+        public uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
index 6c6e2828c6..b79ca13694 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
@@ -747,5 +747,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
             return 0;
         }
+
+        public virtual uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
index 961d92aa6b..2ed7e6a5b0 100644
--- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
+++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
@@ -232,5 +232,79 @@ namespace Ryujinx.Audio.Renderer.Server
                 }
             }
         }
+
+        public override uint Estimate(CompressorCommand command)
+        {
+            Debug.Assert(_sampleCount == 160 || _sampleCount == 240);
+
+            if (_sampleCount == 160)
+            {
+                if (command.Enabled)
+                {
+                    switch (command.Parameter.ChannelCount)
+                    {
+                        case 1:
+                            return 34431;
+                        case 2:
+                            return 44253;
+                        case 4:
+                            return 63827;
+                        case 6:
+                            return 83361;
+                        default:
+                            throw new NotImplementedException($"{command.Parameter.ChannelCount}");
+                    }
+                }
+                else
+                {
+                    switch (command.Parameter.ChannelCount)
+                    {
+                        case 1:
+                            return (uint)630.12f;
+                        case 2:
+                            return (uint)638.27f;
+                        case 4:
+                            return (uint)705.86f;
+                        case 6:
+                            return (uint)782.02f;
+                        default:
+                            throw new NotImplementedException($"{command.Parameter.ChannelCount}");
+                    }
+                }
+            }
+
+            if (command.Enabled)
+            {
+                switch (command.Parameter.ChannelCount)
+                {
+                    case 1:
+                        return 51095;
+                    case 2:
+                        return 65693;
+                    case 4:
+                        return 95383;
+                    case 6:
+                        return 124510;
+                    default:
+                        throw new NotImplementedException($"{command.Parameter.ChannelCount}");
+                }
+            }
+            else
+            {
+                switch (command.Parameter.ChannelCount)
+                {
+                    case 1:
+                        return (uint)840.14f;
+                    case 2:
+                        return (uint)826.1f;
+                    case 4:
+                        return (uint)901.88f;
+                    case 6:
+                        return (uint)965.29f;
+                    default:
+                        throw new NotImplementedException($"{command.Parameter.ChannelCount}");
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
index 35314aca51..825b3bf76c 100644
--- a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
+++ b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
@@ -262,6 +262,8 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
                     return PerformanceDetailType.Limiter;
                 case EffectType.CaptureBuffer:
                     return PerformanceDetailType.CaptureBuffer;
+                case EffectType.Compressor:
+                    return PerformanceDetailType.Compressor;
                 default:
                     throw new NotImplementedException($"{Type}");
             }
diff --git a/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
new file mode 100644
index 0000000000..f4e5ae829c
--- /dev/null
+++ b/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
@@ -0,0 +1,67 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter.Effect;
+using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Audio.Renderer.Server.MemoryPool;
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Server.Effect
+{
+    /// <summary>
+    /// Server state for a compressor effect.
+    /// </summary>
+    public class CompressorEffect : BaseEffect
+    {
+        /// <summary>
+        /// The compressor parameter.
+        /// </summary>
+        public CompressorParameter Parameter;
+
+        /// <summary>
+        /// The compressor state.
+        /// </summary>
+        public Memory<CompressorState> State { get; }
+
+        /// <summary>
+        /// Create a new <see cref="CompressorEffect"/>.
+        /// </summary>
+        public CompressorEffect()
+        {
+            State = new CompressorState[1];
+        }
+
+        public override EffectType TargetEffectType => EffectType.Compressor;
+
+        public override ulong GetWorkBuffer(int index)
+        {
+            return GetSingleBuffer();
+        }
+
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
+        {
+            // Nintendo doesn't do anything here but we still require updateErrorInfo to be initialised.
+            updateErrorInfo = new BehaviourParameter.ErrorInfo();
+        }
+
+        public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
+        {
+            Debug.Assert(IsTypeValid(ref parameter));
+
+            UpdateParameterBase(ref parameter);
+
+            Parameter = MemoryMarshal.Cast<byte, CompressorParameter>(parameter.SpecificData)[0];
+            IsEnabled = parameter.IsEnabled;
+
+            updateErrorInfo = new BehaviourParameter.ErrorInfo();
+        }
+
+        public override void UpdateForCommandGeneration()
+        {
+            UpdateUsageStateForCommandGeneration();
+
+            Parameter.Status = UsageState.Enabled;
+        }
+    }
+}
diff --git a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
index e365a86c04..4872ddb3a1 100644
--- a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
+++ b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
@@ -35,5 +35,6 @@ namespace Ryujinx.Audio.Renderer.Server
         uint Estimate(LimiterCommandVersion2 command);
         uint Estimate(GroupedBiquadFilterCommand command);
         uint Estimate(CaptureBufferCommand command);
+        uint Estimate(CompressorCommand command);
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
index 0c2cfa7eae..0446cd8c6e 100644
--- a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
+++ b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs
@@ -240,6 +240,10 @@ namespace Ryujinx.Audio.Renderer.Server
                 case EffectType.CaptureBuffer:
                     effect = new CaptureBufferEffect();
                     break;
+                case EffectType.Compressor:
+                    effect = new CompressorEffect();
+                    break;
+
                 default:
                     throw new NotImplementedException($"EffectType {parameter.Type} not implemented!");
             }
diff --git a/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs b/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs
new file mode 100644
index 0000000000..24b834fcbf
--- /dev/null
+++ b/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs
@@ -0,0 +1,16 @@
+using NUnit.Framework;
+using Ryujinx.Audio.Renderer.Parameter.Effect;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Tests.Audio.Renderer.Parameter.Effect
+{
+    class CompressorParameterTests
+    {
+        [Test]
+        public void EnsureTypeSize()
+        {
+            Assert.AreEqual(0x38, Unsafe.SizeOf<CompressorParameter>());
+        }
+    }
+}
+