Forráskód Böngészése

Update audio renderer to REV13: Add support for compressor statistics and volume reset (#7372)

* Update audio renderer to REV13: Add support for compressor statistics and volume reset

* XML docs

* Disable stats reset

* Wrong comment

* Fix more XML docs

* PR feedback
gdkchan 1 éve
szülő
commit
a2c0035013
20 módosított fájl, 352 hozzáadás és 88 törlés
  1. 35 10
      src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs
  2. 23 23
      src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs
  3. 24 24
      src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs
  4. 9 2
      src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs
  5. 38 0
      src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs
  6. 5 0
      src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
  7. 8 1
      src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
  8. 8 1
      src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
  9. 18 1
      src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
  10. 11 2
      src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
  11. 14 2
      src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
  12. 42 14
      src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
  13. 13 0
      src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs
  14. 8 1
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
  15. 4 3
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
  16. 4 2
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
  17. 4 2
      src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
  18. 1 0
      src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs
  19. 42 0
      src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs
  20. 41 0
      src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs

+ 35 - 10
src/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs

@@ -1,9 +1,11 @@
 using Ryujinx.Audio.Renderer.Dsp.Effect;
 using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
 using Ryujinx.Audio.Renderer.Parameter.Effect;
 using Ryujinx.Audio.Renderer.Server.Effect;
 using System;
 using System.Diagnostics;
+using System.Runtime.InteropServices;
 
 namespace Ryujinx.Audio.Renderer.Dsp.Command
 {
@@ -21,18 +23,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         public CompressorParameter Parameter => _parameter;
         public Memory<CompressorState> State { get; }
+        public Memory<EffectResultState> ResultState { 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)
+        public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, Memory<EffectResultState> resultState, bool isEnabled, int nodeId)
         {
             Enabled = true;
             NodeId = nodeId;
             _parameter = parameter;
             State = state;
+            ResultState = resultState;
 
             IsEffectEnabled = isEnabled;
 
@@ -71,9 +75,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
             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];
+                if (!ResultState.IsEmpty && _parameter.StatisticsReset)
+                {
+                    ref CompressorStatistics statistics = ref MemoryMarshal.Cast<byte, CompressorStatistics>(ResultState.Span[0].SpecificData)[0];
+
+                    statistics.Reset(_parameter.ChannelCount);
+                }
+
+                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;
@@ -92,7 +103,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
                         channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex);
                     }
 
-                    float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain);
+                    float mean = FloatingPointHelper.MeanSquare(channelInput);
+                    float newMean = inputMovingAverage.Update(mean, _parameter.InputGain);
                     float y = FloatingPointHelper.Log10(newMean) * 10.0f;
                     float z = 1.0f;
 
@@ -111,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                         if (y >= state.Unknown14)
                         {
-                            tmpGain = ((1.0f / Parameter.Ratio) - 1.0f) * (y - Parameter.Threshold);
+                            tmpGain = ((1.0f / _parameter.Ratio) - 1.0f) * (y - _parameter.Threshold);
                         }
                         else
                         {
@@ -126,7 +138,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                     if ((unknown4 - z) <= 0.08f)
                     {
-                        compressionEmaAlpha = Parameter.ReleaseCoefficient;
+                        compressionEmaAlpha = _parameter.ReleaseCoefficient;
 
                         if ((unknown4 - z) >= -0.08f)
                         {
@@ -140,18 +152,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
                     }
                     else
                     {
-                        compressionEmaAlpha = Parameter.AttackCoefficient;
+                        compressionEmaAlpha = _parameter.AttackCoefficient;
                     }
 
                     float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha);
 
-                    for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+                    for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                     {
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain;
                     }
 
                     unknown4 = unknown4New;
                     previousCompressionEmaAlpha = compressionEmaAlpha;
+
+                    if (!ResultState.IsEmpty)
+                    {
+                        ref CompressorStatistics statistics = ref MemoryMarshal.Cast<byte, CompressorStatistics>(ResultState.Span[0].SpecificData)[0];
+
+                        statistics.MinimumGain = MathF.Min(statistics.MinimumGain, compressionGain * state.OutputGain);
+                        statistics.MaximumMean = MathF.Max(statistics.MaximumMean, mean);
+
+                        for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
+                        {
+                            statistics.LastSamples[channelIndex] = MathF.Abs(channelInput[channelIndex] * (1f / 32768f));
+                        }
+                    }
                 }
 
                 state.InputMovingAverage = inputMovingAverage;
@@ -161,7 +186,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             }
             else
             {
-                for (int i = 0; i < Parameter.ChannelCount; i++)
+                for (int i = 0; i < _parameter.ChannelCount; i++)
                 {
                     if (InputBufferIndices[i] != OutputBufferIndices[i])
                     {

+ 23 - 23
src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs

@@ -38,10 +38,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
             OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
 
-            for (int i = 0; i < Parameter.ChannelCount; i++)
+            for (int i = 0; i < _parameter.ChannelCount; i++)
             {
-                InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
-                OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+                InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+                OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
             }
         }
 
@@ -51,11 +51,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
             if (IsEffectEnabled)
             {
-                if (Parameter.Status == UsageState.Invalid)
+                if (_parameter.Status == UsageState.Invalid)
                 {
                     state = new LimiterState(ref _parameter, WorkBuffer);
                 }
-                else if (Parameter.Status == UsageState.New)
+                else if (_parameter.Status == UsageState.New)
                 {
                     LimiterState.UpdateParameter(ref _parameter);
                 }
@@ -66,56 +66,56 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
         {
-            Debug.Assert(Parameter.IsChannelCountValid());
+            Debug.Assert(_parameter.IsChannelCountValid());
 
-            if (IsEffectEnabled && Parameter.IsChannelCountValid())
+            if (IsEffectEnabled && _parameter.IsChannelCountValid())
             {
-                Span<IntPtr> inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
-                Span<IntPtr> outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+                Span<IntPtr> inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+                Span<IntPtr> outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
 
-                for (int i = 0; i < Parameter.ChannelCount; i++)
+                for (int i = 0; i < _parameter.ChannelCount; i++)
                 {
                     inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
                     outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
                 }
 
-                for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+                for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                 {
                     for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
                     {
                         float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
 
-                        float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+                        float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
 
                         float sampleInputMax = Math.Abs(inputSample);
 
-                        float inputCoefficient = Parameter.ReleaseCoefficient;
+                        float inputCoefficient = _parameter.ReleaseCoefficient;
 
                         if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
-                            inputCoefficient = Parameter.AttackCoefficient;
+                            inputCoefficient = _parameter.AttackCoefficient;
                         }
 
                         float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
 
-                        if (detectorValue > Parameter.Threshold)
+                        if (detectorValue > _parameter.Threshold)
                         {
-                            attenuation = Parameter.Threshold / detectorValue;
+                            attenuation = _parameter.Threshold / detectorValue;
                         }
 
-                        float outputCoefficient = Parameter.ReleaseCoefficient;
+                        float outputCoefficient = _parameter.ReleaseCoefficient;
 
                         if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
-                            outputCoefficient = Parameter.AttackCoefficient;
+                            outputCoefficient = _parameter.AttackCoefficient;
                         }
 
                         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 * compressionGain * Parameter.OutputGain;
+                        float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
 
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
 
@@ -123,16 +123,16 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                         state.DelayedSampleBufferPosition[channelIndex]++;
 
-                        while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+                        while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
                         {
-                            state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+                            state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
                         }
                     }
                 }
             }
             else
             {
-                for (int i = 0; i < Parameter.ChannelCount; i++)
+                for (int i = 0; i < _parameter.ChannelCount; i++)
                 {
                     if (InputBufferIndices[i] != OutputBufferIndices[i])
                     {

+ 24 - 24
src/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs

@@ -49,10 +49,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
             OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
 
-            for (int i = 0; i < Parameter.ChannelCount; i++)
+            for (int i = 0; i < _parameter.ChannelCount; i++)
             {
-                InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]);
-                OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]);
+                InputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Input[i]);
+                OutputBufferIndices[i] = (ushort)(bufferOffset + _parameter.Output[i]);
             }
         }
 
@@ -62,11 +62,11 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
             if (IsEffectEnabled)
             {
-                if (Parameter.Status == UsageState.Invalid)
+                if (_parameter.Status == UsageState.Invalid)
                 {
                     state = new LimiterState(ref _parameter, WorkBuffer);
                 }
-                else if (Parameter.Status == UsageState.New)
+                else if (_parameter.Status == UsageState.New)
                 {
                     LimiterState.UpdateParameter(ref _parameter);
                 }
@@ -77,63 +77,63 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
         private unsafe void ProcessLimiter(CommandList context, ref LimiterState state)
         {
-            Debug.Assert(Parameter.IsChannelCountValid());
+            Debug.Assert(_parameter.IsChannelCountValid());
 
-            if (IsEffectEnabled && Parameter.IsChannelCountValid())
+            if (IsEffectEnabled && _parameter.IsChannelCountValid())
             {
-                if (!ResultState.IsEmpty && Parameter.StatisticsReset)
+                if (!ResultState.IsEmpty && _parameter.StatisticsReset)
                 {
                     ref LimiterStatistics statistics = ref MemoryMarshal.Cast<byte, LimiterStatistics>(ResultState.Span[0].SpecificData)[0];
 
                     statistics.Reset();
                 }
 
-                Span<IntPtr> inputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
-                Span<IntPtr> outputBuffers = stackalloc IntPtr[Parameter.ChannelCount];
+                Span<IntPtr> inputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
+                Span<IntPtr> outputBuffers = stackalloc IntPtr[_parameter.ChannelCount];
 
-                for (int i = 0; i < Parameter.ChannelCount; i++)
+                for (int i = 0; i < _parameter.ChannelCount; i++)
                 {
                     inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
                     outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
                 }
 
-                for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++)
+                for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                 {
                     for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
                     {
                         float rawInputSample = *((float*)inputBuffers[channelIndex] + sampleIndex);
 
-                        float inputSample = (rawInputSample / short.MaxValue) * Parameter.InputGain;
+                        float inputSample = (rawInputSample / short.MaxValue) * _parameter.InputGain;
 
                         float sampleInputMax = Math.Abs(inputSample);
 
-                        float inputCoefficient = Parameter.ReleaseCoefficient;
+                        float inputCoefficient = _parameter.ReleaseCoefficient;
 
                         if (sampleInputMax > state.DetectorAverage[channelIndex].Read())
                         {
-                            inputCoefficient = Parameter.AttackCoefficient;
+                            inputCoefficient = _parameter.AttackCoefficient;
                         }
 
                         float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient);
                         float attenuation = 1.0f;
 
-                        if (detectorValue > Parameter.Threshold)
+                        if (detectorValue > _parameter.Threshold)
                         {
-                            attenuation = Parameter.Threshold / detectorValue;
+                            attenuation = _parameter.Threshold / detectorValue;
                         }
 
-                        float outputCoefficient = Parameter.ReleaseCoefficient;
+                        float outputCoefficient = _parameter.ReleaseCoefficient;
 
                         if (state.CompressionGainAverage[channelIndex].Read() > attenuation)
                         {
-                            outputCoefficient = Parameter.AttackCoefficient;
+                            outputCoefficient = _parameter.AttackCoefficient;
                         }
 
                         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 * compressionGain * Parameter.OutputGain;
+                        float outputSample = delayedSample * compressionGain * _parameter.OutputGain;
 
                         *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue;
 
@@ -141,9 +141,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
 
                         state.DelayedSampleBufferPosition[channelIndex]++;
 
-                        while (state.DelayedSampleBufferPosition[channelIndex] >= Parameter.DelayBufferSampleCountMin)
+                        while (state.DelayedSampleBufferPosition[channelIndex] >= _parameter.DelayBufferSampleCountMin)
                         {
-                            state.DelayedSampleBufferPosition[channelIndex] -= Parameter.DelayBufferSampleCountMin;
+                            state.DelayedSampleBufferPosition[channelIndex] -= _parameter.DelayBufferSampleCountMin;
                         }
 
                         if (!ResultState.IsEmpty)
@@ -158,7 +158,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
             }
             else
             {
-                for (int i = 0; i < Parameter.ChannelCount; i++)
+                for (int i = 0; i < _parameter.ChannelCount; i++)
                 {
                     if (InputBufferIndices[i] != OutputBufferIndices[i])
                     {

+ 9 - 2
src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs

@@ -90,9 +90,16 @@ namespace Ryujinx.Audio.Renderer.Parameter.Effect
         public bool MakeupGainEnabled;
 
         /// <summary>
-        /// Reserved/padding.
+        /// Indicate if the compressor effect should output statistics.
         /// </summary>
-        private Array2<byte> _reserved;
+        [MarshalAs(UnmanagedType.I1)]
+        public bool StatisticsEnabled;
+
+        /// <summary>
+        /// Indicate to the DSP that the user did a statistics reset.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool StatisticsReset;
 
         /// <summary>
         /// Check if the <see cref="ChannelCount"/> is valid.

+ 38 - 0
src/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorStatistics.cs

@@ -0,0 +1,38 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter.Effect
+{
+    /// <summary>
+    /// Effect result state for <seealso cref="Common.EffectType.Compressor"/>.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack = 1)]
+    public struct CompressorStatistics
+    {
+        /// <summary>
+        /// Maximum input mean value since last reset.
+        /// </summary>
+        public float MaximumMean;
+
+        /// <summary>
+        /// Minimum output gain since last reset.
+        /// </summary>
+        public float MinimumGain;
+
+        /// <summary>
+        /// Last processed input sample, per channel.
+        /// </summary>
+        public Array6<float> LastSamples;
+
+        /// <summary>
+        /// Reset the statistics.
+        /// </summary>
+        /// <param name="channelCount">Number of channels to reset.</param>
+        public void Reset(ushort channelCount)
+        {
+            MaximumMean = 0.0f;
+            MinimumGain = 1.0f;
+            LastSamples.AsSpan()[..channelCount].Clear();
+        }
+    }
+}

+ 5 - 0
src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs

@@ -28,6 +28,11 @@ namespace Ryujinx.Audio.Renderer.Parameter
         /// </summary>
         bool IsUsed { get; }
 
+        /// <summary>
+        /// Set to true to force resetting the previous mix volumes.
+        /// </summary>
+        bool ResetPrevVolume { get; }
+
         /// <summary>
         /// Mix buffer volumes.
         /// </summary>

+ 8 - 1
src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs

@@ -37,10 +37,16 @@ namespace Ryujinx.Audio.Renderer.Parameter
         [MarshalAs(UnmanagedType.I1)]
         public bool IsUsed;
 
+        /// <summary>
+        /// Set to true to force resetting the previous mix volumes.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool ResetPrevVolume;
+
         /// <summary>
         /// Reserved/padding.
         /// </summary>
-        private unsafe fixed byte _reserved[3];
+        private unsafe fixed byte _reserved[2];
 
         [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
         private struct MixArray { }
@@ -58,6 +64,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
         readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => default;
 
         readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+        readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
 
         /// <summary>
         /// The expected constant of any input header.

+ 8 - 1
src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs

@@ -42,10 +42,16 @@ namespace Ryujinx.Audio.Renderer.Parameter
         [MarshalAs(UnmanagedType.I1)]
         public bool IsUsed;
 
+        /// <summary>
+        /// Set to true to force resetting the previous mix volumes.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool ResetPrevVolume;
+
         /// <summary>
         /// Reserved/padding.
         /// </summary>
-        private unsafe fixed byte _reserved[11];
+        private unsafe fixed byte _reserved[10];
 
         [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
         private struct MixArray { }
@@ -63,6 +69,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
         readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
 
         readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+        readonly bool ISplitterDestinationInParameter.ResetPrevVolume => ResetPrevVolume;
 
         /// <summary>
         /// The expected constant of any input header.

+ 18 - 1
src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs

@@ -108,10 +108,18 @@ namespace Ryujinx.Audio.Renderer.Server
         /// <remarks>This was added in system update 17.0.0</remarks>
         public const int Revision12 = 12 << 24;
 
+        /// <summary>
+        /// REV13:
+        /// The compressor effect can now output statistics.
+        /// Splitter destinations now explicitly reset the previous mix volume, instead of doing so on first use.
+        /// </summary>
+        /// <remarks>This was added in system update 18.0.0</remarks>
+        public const int Revision13 = 13 << 24;
+
         /// <summary>
         /// Last revision supported by the implementation.
         /// </summary>
-        public const int LastRevision = Revision12;
+        public const int LastRevision = Revision13;
 
         /// <summary>
         /// Target revision magic supported by the implementation.
@@ -384,6 +392,15 @@ namespace Ryujinx.Audio.Renderer.Server
             return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
         }
 
+        /// <summary>
+        /// Check if the audio renderer should support explicit previous mix volume reset on splitter.
+        /// </summary>
+        /// <returns>True if the audio renderer support explicit previous mix volume reset on splitter</returns>
+        public bool IsSplitterPrevVolumeResetSupported()
+        {
+            return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision13);
+        }
+
         /// <summary>
         /// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>.
         /// </summary>

+ 11 - 2
src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs

@@ -583,11 +583,20 @@ namespace Ryujinx.Audio.Renderer.Server
             }
         }
 
-        public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, bool isEnabled, int nodeId)
+        /// <summary>
+        /// Generate a new <see cref="CompressorCommand"/>.
+        /// </summary>
+        /// <param name="bufferOffset">The target buffer offset.</param>
+        /// <param name="parameter">The compressor parameter.</param>
+        /// <param name="state">The compressor state.</param>
+        /// <param name="effectResultState">The DSP effect result state.</param>
+        /// <param name="isEnabled">Set to true if the effect should be active.</param>
+        /// <param name="nodeId">The node id associated to this command.</param>
+        public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, Memory<EffectResultState> effectResultState, bool isEnabled, int nodeId)
         {
             if (parameter.IsChannelCountValid())
             {
-                CompressorCommand command = new(bufferOffset, parameter, state, isEnabled, nodeId);
+                CompressorCommand command = new(bufferOffset, parameter, state, effectResultState, isEnabled, nodeId);
 
                 command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
 

+ 14 - 2
src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs

@@ -735,14 +735,26 @@ namespace Ryujinx.Audio.Renderer.Server
             }
         }
 
-        private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId)
+        private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId, int effectId)
         {
             Debug.Assert(effect.Type == EffectType.Compressor);
 
+            Memory<EffectResultState> dspResultState;
+
+            if (effect.Parameter.StatisticsEnabled)
+            {
+                dspResultState = _effectContext.GetDspStateMemory(effectId);
+            }
+            else
+            {
+                dspResultState = Memory<EffectResultState>.Empty;
+            }
+
             _commandBuffer.GenerateCompressorEffect(
                 bufferOffset,
                 effect.Parameter,
                 effect.State,
+                dspResultState,
                 effect.IsEnabled,
                 nodeId);
         }
@@ -795,7 +807,7 @@ namespace Ryujinx.Audio.Renderer.Server
                     GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
                     break;
                 case EffectType.Compressor:
-                    GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId);
+                    GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId, effectId);
                     break;
                 default:
                     throw new NotImplementedException($"Unsupported effect type {effect.Type}");

+ 42 - 14
src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs

@@ -169,14 +169,28 @@ namespace Ryujinx.Audio.Renderer.Server
             {
                 if (command.Enabled)
                 {
-                    return command.Parameter.ChannelCount switch
+                    if (command.Parameter.StatisticsEnabled)
                     {
-                        1 => 34431,
-                        2 => 44253,
-                        4 => 63827,
-                        6 => 83361,
-                        _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
-                    };
+                        return command.Parameter.ChannelCount switch
+                        {
+                            1 => 22100,
+                            2 => 33211,
+                            4 => 41587,
+                            6 => 58819,
+                            _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+                        };
+                    }
+                    else
+                    {
+                        return command.Parameter.ChannelCount switch
+                        {
+                            1 => 19052,
+                            2 => 29852,
+                            4 => 37904,
+                            6 => 55020,
+                            _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+                        };
+                    }
                 }
 
                 return command.Parameter.ChannelCount switch
@@ -191,14 +205,28 @@ namespace Ryujinx.Audio.Renderer.Server
 
             if (command.Enabled)
             {
-                return command.Parameter.ChannelCount switch
+                if (command.Parameter.StatisticsEnabled)
                 {
-                    1 => 51095,
-                    2 => 65693,
-                    4 => 95383,
-                    6 => 124510,
-                    _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
-                };
+                    return command.Parameter.ChannelCount switch
+                    {
+                        1 => 32518,
+                        2 => 49102,
+                        4 => 61685,
+                        6 => 87250,
+                        _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+                    };
+                }
+                else
+                {
+                    return command.Parameter.ChannelCount switch
+                    {
+                        1 => 27963,
+                        2 => 44016,
+                        4 => 56183,
+                        6 => 81862,
+                        _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
+                    };
+                }
             }
 
             return command.Parameter.ChannelCount switch

+ 13 - 0
src/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs

@@ -62,6 +62,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
             UpdateUsageStateForCommandGeneration();
 
             Parameter.Status = UsageState.Enabled;
+            Parameter.StatisticsReset = false;
+        }
+
+        public override void InitializeResultState(ref EffectResultState state)
+        {
+            ref CompressorStatistics statistics = ref MemoryMarshal.Cast<byte, CompressorStatistics>(state.SpecificData)[0];
+
+            statistics.Reset(Parameter.ChannelCount);
+        }
+
+        public override void UpdateResultState(ref EffectResultState destState, ref EffectResultState srcState)
+        {
+            destState = srcState;
         }
     }
 }

+ 8 - 1
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs

@@ -51,6 +51,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// </summary>
         public bool IsBugFixed { get; private set; }
 
+        /// <summary>
+        /// If set to true, the previous mix volume is explicitly resetted using the input parameter, instead of implicitly on first use.
+        /// </summary>
+        public bool IsSplitterPrevVolumeResetSupported { get; private set; }
+
         /// <summary>
         /// Initialize <see cref="SplitterContext"/>.
         /// </summary>
@@ -139,6 +144,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
                 }
             }
 
+            IsSplitterPrevVolumeResetSupported = behaviourContext.IsSplitterPrevVolumeResetSupported();
+
             SplitterState.InitializeSplitters(splitters.Span);
 
             Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed());
@@ -277,7 +284,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
                 {
                     SplitterDestination destination = GetDestination(parameter.Id);
 
-                    destination.Update(parameter);
+                    destination.Update(parameter, IsSplitterPrevVolumeResetSupported);
                 }
 
                 return true;

+ 4 - 3
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs

@@ -184,15 +184,16 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// Update the splitter destination data from user parameter.
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
-        public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
+        /// <param name="isPrevVolumeResetSupported">Indicates that the audio renderer revision in use supports explicitly resetting the volume.</param>
+        public void Update<T>(in T parameter, bool isPrevVolumeResetSupported) where T : ISplitterDestinationInParameter
         {
             if (Unsafe.IsNullRef(ref _v2))
             {
-                _v1.Update(parameter);
+                _v1.Update(parameter, isPrevVolumeResetSupported);
             }
             else
             {
-                _v2.Update(parameter);
+                _v2.Update(parameter, isPrevVolumeResetSupported);
             }
         }
 

+ 4 - 2
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs

@@ -93,7 +93,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// Update the <see cref="SplitterDestinationVersion1"/> from user parameter.
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
-        public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
+        /// <param name="isPrevVolumeResetSupported">Indicates that the audio renderer revision in use supports explicitly resetting the volume.</param>
+        public void Update<T>(in T parameter, bool isPrevVolumeResetSupported) where T : ISplitterDestinationInParameter
         {
             Debug.Assert(Id == parameter.Id);
 
@@ -103,7 +104,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
 
                 parameter.MixBufferVolume.CopyTo(MixBufferVolume);
 
-                if (!IsUsed && parameter.IsUsed)
+                bool resetPrevVolume = isPrevVolumeResetSupported ? parameter.ResetPrevVolume : !IsUsed && parameter.IsUsed;
+                if (resetPrevVolume)
                 {
                     MixBufferVolume.CopyTo(PreviousMixBufferVolume);
 

+ 4 - 2
src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs

@@ -98,7 +98,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
         /// Update the <see cref="SplitterDestinationVersion2"/> from user parameter.
         /// </summary>
         /// <param name="parameter">The user parameter.</param>
-        public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
+        /// <param name="isPrevVolumeResetSupported">Indicates that the audio renderer revision in use supports explicitly resetting the volume.</param>
+        public void Update<T>(in T parameter, bool isPrevVolumeResetSupported) where T : ISplitterDestinationInParameter
         {
             Debug.Assert(Id == parameter.Id);
 
@@ -110,7 +111,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
 
                 _biquadFilters = parameter.BiquadFilters;
 
-                if (!IsUsed && parameter.IsUsed)
+                bool resetPrevVolume = isPrevVolumeResetSupported ? parameter.ResetPrevVolume : !IsUsed && parameter.IsUsed;
+                if (resetPrevVolume)
                 {
                     MixBufferVolume.CopyTo(PreviousMixBufferVolume);
 

+ 1 - 0
src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs

@@ -8,5 +8,6 @@ namespace Ryujinx.Horizon.Sdk.Audio
 
         public static Result DeviceNotFound => new(ModuleId, 1);
         public static Result UnsupportedRevision => new(ModuleId, 2);
+        public static Result NotImplemented => new(ModuleId, 513);
     }
 }

+ 42 - 0
src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs

@@ -233,6 +233,48 @@ namespace Ryujinx.Horizon.Sdk.Audio.Detail
             return Result.Success;
         }
 
+        [CmifCommand(15)] // 17.0.0+
+        public Result AcquireAudioOutputDeviceNotification([CopyHandle] out int eventHandle, ulong deviceId)
+        {
+            eventHandle = 0;
+
+            return AudioResult.NotImplemented;
+        }
+
+        [CmifCommand(16)] // 17.0.0+
+        public Result ReleaseAudioOutputDeviceNotification(ulong deviceId)
+        {
+            return AudioResult.NotImplemented;
+        }
+
+        [CmifCommand(17)] // 17.0.0+
+        public Result AcquireAudioInputDeviceNotification([CopyHandle] out int eventHandle, ulong deviceId)
+        {
+            eventHandle = 0;
+
+            return AudioResult.NotImplemented;
+        }
+
+        [CmifCommand(18)] // 17.0.0+
+        public Result ReleaseAudioInputDeviceNotification(ulong deviceId)
+        {
+            return AudioResult.NotImplemented;
+        }
+
+        [CmifCommand(19)] // 18.0.0+
+        public Result SetAudioDeviceOutputVolumeAutoTuneEnabled(bool enabled)
+        {
+            return AudioResult.NotImplemented;
+        }
+
+        [CmifCommand(20)] // 18.0.0+
+        public Result IsAudioDeviceOutputVolumeAutoTuneEnabled(out bool enabled)
+        {
+            enabled = false;
+
+            return AudioResult.NotImplemented;
+        }
+
         protected virtual void Dispose(bool disposing)
         {
             if (disposing)

+ 41 - 0
src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs

@@ -55,6 +55,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -83,6 +84,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -111,6 +113,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -139,6 +142,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -167,6 +171,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -195,6 +200,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -223,6 +229,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -251,6 +258,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -279,6 +287,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -307,6 +316,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -335,6 +345,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -363,6 +374,36 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
             Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
             Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported());
             Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsFalse(behaviourContext.IsSplitterPrevVolumeResetSupported());
+
+            Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
+            Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
+            Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
+        }
+
+        [Test]
+        public void TestRevision13()
+        {
+            BehaviourContext behaviourContext = new();
+
+            behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision13);
+
+            Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed());
+            Assert.IsTrue(behaviourContext.IsSplitterSupported());
+            Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported());
+            Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported());
+            Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported());
+            Assert.IsTrue(behaviourContext.IsSplitterBugFixed());
+            Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported());
+            Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported());
+            Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed());
+            Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
+            Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
+            Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
+            Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
+            Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported());
+            Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+            Assert.IsTrue(behaviourContext.IsSplitterPrevVolumeResetSupported());
 
             Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
             Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());