Просмотр исходного кода

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
Mary-nyan 3 лет назад
Родитель
Сommit
40311310d1
24 измененных файлов с 658 добавлено и 27 удалено
  1. 6 1
      Ryujinx.Audio/Renderer/Common/EffectType.cs
  2. 2 1
      Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs
  3. 2 1
      Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
  4. 173 0
      Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
  5. 7 8
      Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
  6. 8 9
      Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
  7. 26 0
      Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs
  8. 6 0
      Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs
  9. 48 0
      Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs
  10. 51 0
      Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs
  11. 7 6
      Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs
  12. 115 0
      Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
  13. 2 1
      Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
  14. 12 0
      Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
  15. 14 0
      Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
  16. 5 0
      Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
  17. 5 0
      Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
  18. 5 0
      Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
  19. 74 0
      Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
  20. 2 0
      Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs
  21. 67 0
      Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
  22. 1 0
      Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
  23. 4 0
      Ryujinx.Audio/Renderer/Server/StateUpdater.cs
  24. 16 0
      Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs

+ 6 - 1
Ryujinx.Audio/Renderer/Common/EffectType.cs

@@ -48,6 +48,11 @@ namespace Ryujinx.Audio.Renderer.Common
         /// <summary>
         /// <summary>
         /// Effect to capture mixes (via auxiliary buffers).
         /// Effect to capture mixes (via auxiliary buffers).
         /// </summary>
         /// </summary>
-        CaptureBuffer
+        CaptureBuffer,
+
+        /// <summary>
+        /// Effect applying a compressor filter (DRC).
+        /// </summary>
+        Compressor,
     }
     }
 }
 }

+ 2 - 1
Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs

@@ -14,6 +14,7 @@ namespace Ryujinx.Audio.Renderer.Common
         Reverb3d,
         Reverb3d,
         PcmFloat,
         PcmFloat,
         Limiter,
         Limiter,
-        CaptureBuffer
+        CaptureBuffer,
+        Compressor
     }
     }
 }
 }

+ 2 - 1
Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs

@@ -31,6 +31,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
         LimiterVersion1,
         LimiterVersion1,
         LimiterVersion2,
         LimiterVersion2,
         GroupedBiquadFilter,
         GroupedBiquadFilter,
-        CaptureBuffer
+        CaptureBuffer,
+        Compressor
     }
     }
 }
 }

+ 173 - 0
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]);
+                    }
+                }
+            }
+        }
+    }
+}

+ 7 - 8
Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs

@@ -90,32 +90,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
 
                         float inputCoefficient = Parameter.ReleaseCoefficient;
                         float inputCoefficient = Parameter.ReleaseCoefficient;
 
 
-                        if (sampleInputMax > state.DectectorAverage[channelIndex])
+                        if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
                         {
                             inputCoefficient = Parameter.AttackCoefficient;
                             inputCoefficient = Parameter.AttackCoefficient;
                         }
                         }
 
 
-                        state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]);
-
+                        float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
                         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;
                         float outputCoefficient = Parameter.ReleaseCoefficient;
 
 
-                        if (state.CompressionGain[channelIndex] > attenuation)
+                        if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
                         {
                             outputCoefficient = Parameter.AttackCoefficient;
                             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]];
                         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;
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
 
 

+ 8 - 9
Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs

@@ -101,32 +101,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
 
                         float inputCoefficient = Parameter.ReleaseCoefficient;
                         float inputCoefficient = Parameter.ReleaseCoefficient;
 
 
-                        if (sampleInputMax > state.DectectorAverage[channelIndex])
+                        if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
                         {
                             inputCoefficient = Parameter.AttackCoefficient;
                             inputCoefficient = Parameter.AttackCoefficient;
                         }
                         }
 
 
-                        state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]);
-
+                        float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
                         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;
                         float outputCoefficient = Parameter.ReleaseCoefficient;
 
 
-                        if (state.CompressionGain[channelIndex] > attenuation)
+                        if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
                         {
                             outputCoefficient = Parameter.AttackCoefficient;
                             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]];
                         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;
                         *((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];
                             ref LimiterStatistics statistics = ref MemoryMarshal.Cast<byte, LimiterStatistics>(ResultState.Span[0].SpecificData)[0];
 
 
                             statistics.InputMax[channelIndex] = Math.Max(statistics.InputMax[channelIndex], sampleInputMax);
                             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);
                         }
                         }
                     }
                     }
                 }
                 }

+ 26 - 0
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;
+        }
+    }
+}

+ 6 - 0
Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs

@@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Renderer.Dsp
             return (float)value / (1 << qBits);
             return (float)value / (1 << qBits);
         }
         }
 
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public static float ConvertFloat(float value, int qBits)
+        {
+            return value / (1 << qBits);
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static int ToFixed(float value, int qBits)
         public static int ToFixed(float value, int qBits)
         {
         {

+ 48 - 0
Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Reflection.Metadata;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 
 
 namespace Ryujinx.Audio.Renderer.Dsp
 namespace Ryujinx.Audio.Renderer.Dsp
@@ -46,6 +47,53 @@ namespace Ryujinx.Audio.Renderer.Dsp
             return MathF.Pow(10, x);
             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)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static float DegreesToRadians(float degrees)
         public static float DegreesToRadians(float degrees)
         {
         {

+ 51 - 0
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);
+        }
+    }
+}

+ 7 - 6
Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs

@@ -1,3 +1,4 @@
+using Ryujinx.Audio.Renderer.Dsp.Effect;
 using Ryujinx.Audio.Renderer.Parameter.Effect;
 using Ryujinx.Audio.Renderer.Parameter.Effect;
 using System;
 using System;
 
 
@@ -5,20 +6,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.State
 {
 {
     public class LimiterState
     public class LimiterState
     {
     {
-        public float[] DectectorAverage;
-        public float[] CompressionGain;
+        public ExponentialMovingAverage[] DetectorAverage;
+        public ExponentialMovingAverage[] CompressionGainAverage;
         public float[] DelayedSampleBuffer;
         public float[] DelayedSampleBuffer;
         public int[] DelayedSampleBufferPosition;
         public int[] DelayedSampleBufferPosition;
 
 
         public LimiterState(ref LimiterParameter parameter, ulong workBuffer)
         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];
             DelayedSampleBuffer = new float[parameter.ChannelCount * parameter.DelayBufferSampleCountMax];
             DelayedSampleBufferPosition = new int[parameter.ChannelCount];
             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);
             DelayedSampleBufferPosition.AsSpan().Fill(0);
             DelayedSampleBuffer.AsSpan().Fill(0.0f);
             DelayedSampleBuffer.AsSpan().Fill(0.0f);
 
 

+ 115 - 0
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);
+        }
+    }
+}

+ 2 - 1
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.
         /// <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.
         /// 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%.
         /// Additionally, the rendering limit percent was incremented to 80%.
-        /// 
+        ///
         /// </summary>
         /// </summary>
         /// <remarks>This was added in system update 6.0.0</remarks>
         /// <remarks>This was added in system update 6.0.0</remarks>
         public const int Revision5 = 5 << 24;
         public const int Revision5 = 5 << 24;
@@ -93,6 +93,7 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <summary>
         /// <summary>
         /// REV11:
         /// REV11:
         /// The "legacy" effects (Delay, Reverb and Reverb 3D) were updated to match the standard channel mapping used by the audio renderer.
         /// 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 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.
         /// 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>
         /// </summary>

+ 12 - 0
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>
         /// <summary>
         /// Generate a new <see cref="VolumeCommand"/>.
         /// Generate a new <see cref="VolumeCommand"/>.
         /// </summary>
         /// </summary>

+ 14 - 0
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)
         private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect)
         {
         {
             int nodeId = mix.NodeId;
             int nodeId = mix.NodeId;
@@ -650,6 +661,9 @@ namespace Ryujinx.Audio.Renderer.Server
                 case EffectType.CaptureBuffer:
                 case EffectType.CaptureBuffer:
                     GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
                     GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
                     break;
                     break;
+                case EffectType.Compressor:
+                    GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId);
+                    break;
                 default:
                 default:
                     throw new NotImplementedException($"Unsupported effect type {effect.Type}");
                     throw new NotImplementedException($"Unsupported effect type {effect.Type}");
             }
             }

+ 5 - 0
Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs

@@ -179,5 +179,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
         {
             return 0;
             return 0;
         }
         }
+
+        public uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
     }
 }
 }

+ 5 - 0
Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs

@@ -543,5 +543,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
         {
             return 0;
             return 0;
         }
         }
+
+        public uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
     }
 }
 }

+ 5 - 0
Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs

@@ -747,5 +747,10 @@ namespace Ryujinx.Audio.Renderer.Server
         {
         {
             return 0;
             return 0;
         }
         }
+
+        public virtual uint Estimate(CompressorCommand command)
+        {
+            return 0;
+        }
     }
     }
 }
 }

+ 74 - 0
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}");
+                }
+            }
+        }
     }
     }
 }
 }

+ 2 - 0
Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs

@@ -262,6 +262,8 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
                     return PerformanceDetailType.Limiter;
                     return PerformanceDetailType.Limiter;
                 case EffectType.CaptureBuffer:
                 case EffectType.CaptureBuffer:
                     return PerformanceDetailType.CaptureBuffer;
                     return PerformanceDetailType.CaptureBuffer;
+                case EffectType.Compressor:
+                    return PerformanceDetailType.Compressor;
                 default:
                 default:
                     throw new NotImplementedException($"{Type}");
                     throw new NotImplementedException($"{Type}");
             }
             }

+ 67 - 0
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;
+        }
+    }
+}

+ 1 - 0
Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs

@@ -35,5 +35,6 @@ namespace Ryujinx.Audio.Renderer.Server
         uint Estimate(LimiterCommandVersion2 command);
         uint Estimate(LimiterCommandVersion2 command);
         uint Estimate(GroupedBiquadFilterCommand command);
         uint Estimate(GroupedBiquadFilterCommand command);
         uint Estimate(CaptureBufferCommand command);
         uint Estimate(CaptureBufferCommand command);
+        uint Estimate(CompressorCommand command);
     }
     }
 }
 }

+ 4 - 0
Ryujinx.Audio/Renderer/Server/StateUpdater.cs

@@ -240,6 +240,10 @@ namespace Ryujinx.Audio.Renderer.Server
                 case EffectType.CaptureBuffer:
                 case EffectType.CaptureBuffer:
                     effect = new CaptureBufferEffect();
                     effect = new CaptureBufferEffect();
                     break;
                     break;
+                case EffectType.Compressor:
+                    effect = new CompressorEffect();
+                    break;
+
                 default:
                 default:
                     throw new NotImplementedException($"EffectType {parameter.Type} not implemented!");
                     throw new NotImplementedException($"EffectType {parameter.Type} not implemented!");
             }
             }

+ 16 - 0
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>());
+        }
+    }
+}
+