From 40311310d1a6d2fde2ee9f04bfa1f21ced7cbee2 Mon Sep 17 00:00:00 2001 From: Mary-nyan Date: Tue, 6 Dec 2022 15:04:25 +0100 Subject: [PATCH] amadeus: Add missing compressor effect from REV11 (#4010) * amadeus: Add missing compressor effect from REV11 This was in my reversing notes but seems I completely forgot to implement it Also took the opportunity to simplify the Limiter effect a bit. * Remove some outdated comment * Address gdkchan's comments --- Ryujinx.Audio/Renderer/Common/EffectType.cs | 7 +- .../Renderer/Common/PerformanceDetailType.cs | 3 +- .../Renderer/Dsp/Command/CommandType.cs | 3 +- .../Renderer/Dsp/Command/CompressorCommand.cs | 173 ++++++++++++++++++ .../Dsp/Command/LimiterCommandVersion1.cs | 15 +- .../Dsp/Command/LimiterCommandVersion2.cs | 17 +- .../Dsp/Effect/ExponentialMovingAverage.cs | 26 +++ .../Renderer/Dsp/FixedPointHelper.cs | 6 + .../Renderer/Dsp/FloatingPointHelper.cs | 48 +++++ .../Renderer/Dsp/State/CompressorState.cs | 51 ++++++ .../Renderer/Dsp/State/LimiterState.cs | 13 +- .../Parameter/Effect/CompressorParameter.cs | 115 ++++++++++++ .../Renderer/Server/BehaviourContext.cs | 3 +- .../Renderer/Server/CommandBuffer.cs | 12 ++ .../Renderer/Server/CommandGenerator.cs | 14 ++ .../CommandProcessingTimeEstimatorVersion1.cs | 5 + .../CommandProcessingTimeEstimatorVersion2.cs | 5 + .../CommandProcessingTimeEstimatorVersion3.cs | 5 + .../CommandProcessingTimeEstimatorVersion5.cs | 74 ++++++++ .../Renderer/Server/Effect/BaseEffect.cs | 2 + .../Server/Effect/CompressorEffect.cs | 67 +++++++ .../Server/ICommandProcessingTimeEstimator.cs | 1 + Ryujinx.Audio/Renderer/Server/StateUpdater.cs | 4 + .../Effect/CompressorParameterTests.cs | 16 ++ 24 files changed, 658 insertions(+), 27 deletions(-) create mode 100644 Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs create mode 100644 Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs create mode 100644 Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs create mode 100644 Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs create mode 100644 Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs create mode 100644 Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs diff --git a/Ryujinx.Audio/Renderer/Common/EffectType.cs b/Ryujinx.Audio/Renderer/Common/EffectType.cs index 2c50b9eb0d..7128db4ce1 100644 --- a/Ryujinx.Audio/Renderer/Common/EffectType.cs +++ b/Ryujinx.Audio/Renderer/Common/EffectType.cs @@ -48,6 +48,11 @@ namespace Ryujinx.Audio.Renderer.Common /// /// Effect to capture mixes (via auxiliary buffers). /// - CaptureBuffer + CaptureBuffer, + + /// + /// Effect applying a compressor filter (DRC). + /// + Compressor, } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs index 8467ed8d73..805d55183a 100644 --- a/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs +++ b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs @@ -14,6 +14,7 @@ namespace Ryujinx.Audio.Renderer.Common Reverb3d, PcmFloat, Limiter, - CaptureBuffer + CaptureBuffer, + Compressor } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs index dfe7f8862f..9ce181b179 100644 --- a/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs @@ -31,6 +31,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command LimiterVersion1, LimiterVersion2, GroupedBiquadFilter, - CaptureBuffer + CaptureBuffer, + Compressor } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs b/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs new file mode 100644 index 0000000000..8c34429356 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CompressorCommand.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using Ryujinx.Audio.Renderer.Dsp.Effect; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter.Effect; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class CompressorCommand : ICommand + { + private const int FixedPointPrecision = 15; + + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Compressor; + + public uint EstimatedProcessingTime { get; set; } + + public CompressorParameter Parameter => _parameter; + public Memory State { get; } + public ushort[] OutputBufferIndices { get; } + public ushort[] InputBufferIndices { get; } + public bool IsEffectEnabled { get; } + + private CompressorParameter _parameter; + + public CompressorCommand(uint bufferOffset, CompressorParameter parameter, Memory 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 inputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; + Span outputBuffers = stackalloc IntPtr[Parameter.ChannelCount]; + Span channelInput = stackalloc float[Parameter.ChannelCount]; + ExponentialMovingAverage inputMovingAverage = state.InputMovingAverage; + float unknown4 = state.Unknown4; + ExponentialMovingAverage compressionGainAverage = state.CompressionGainAverage; + float previousCompressionEmaAlpha = state.PreviousCompressionEmaAlpha; + + for (int i = 0; i < _parameter.ChannelCount; i++) + { + inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]); + outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]); + } + + for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++) + { + for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++) + { + channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex); + } + + float newMean = inputMovingAverage.Update(FloatingPointHelper.MeanSquare(channelInput), _parameter.InputGain); + float y = FloatingPointHelper.Log10(newMean) * 10.0f; + float z = 0.0f; + + bool unknown10OutOfRange = false; + + if (newMean < 1.0e-10f) + { + z = 1.0f; + + unknown10OutOfRange = state.Unknown10 < -100.0f; + } + + if (y >= state.Unknown10 || unknown10OutOfRange) + { + float tmpGain; + + if (y >= state.Unknown14) + { + tmpGain = ((1.0f / Parameter.Ratio) - 1.0f) * (y - Parameter.Threshold); + } + else + { + tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction); + } + + z = FloatingPointHelper.DecibelToLinearExtended(tmpGain); + } + + float unknown4New = z; + float compressionEmaAlpha; + + if ((unknown4 - z) <= 0.08f) + { + compressionEmaAlpha = Parameter.ReleaseCoefficient; + + if ((unknown4 - z) >= -0.08f) + { + if (MathF.Abs(compressionGainAverage.Read() - z) >= 0.001f) + { + unknown4New = unknown4; + } + + compressionEmaAlpha = previousCompressionEmaAlpha; + } + } + else + { + compressionEmaAlpha = Parameter.AttackCoefficient; + } + + float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha); + + for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++) + { + *((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain; + } + + unknown4 = unknown4New; + previousCompressionEmaAlpha = compressionEmaAlpha; + } + + state.InputMovingAverage = inputMovingAverage; + state.Unknown4 = unknown4; + state.CompressionGainAverage = compressionGainAverage; + state.PreviousCompressionEmaAlpha = previousCompressionEmaAlpha; + } + else + { + for (int i = 0; i < Parameter.ChannelCount; i++) + { + if (InputBufferIndices[i] != OutputBufferIndices[i]) + { + context.CopyBuffer(OutputBufferIndices[i], InputBufferIndices[i]); + } + } + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs index 9cfef736e3..a464ad704c 100644 --- a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs +++ b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion1.cs @@ -90,32 +90,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command float inputCoefficient = Parameter.ReleaseCoefficient; - if (sampleInputMax > state.DectectorAverage[channelIndex]) + if (sampleInputMax > state.DetectorAverage[channelIndex].Read()) { inputCoefficient = Parameter.AttackCoefficient; } - state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]); - + float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient); float attenuation = 1.0f; - if (state.DectectorAverage[channelIndex] > Parameter.Threshold) + if (detectorValue > Parameter.Threshold) { - attenuation = Parameter.Threshold / state.DectectorAverage[channelIndex]; + attenuation = Parameter.Threshold / detectorValue; } float outputCoefficient = Parameter.ReleaseCoefficient; - if (state.CompressionGain[channelIndex] > attenuation) + if (state.CompressionGainAverage[channelIndex].Read() > attenuation) { outputCoefficient = Parameter.AttackCoefficient; } - state.CompressionGain[channelIndex] += outputCoefficient * (attenuation - state.CompressionGain[channelIndex]); + float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient); ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]]; - float outputSample = delayedSample * state.CompressionGain[channelIndex] * Parameter.OutputGain; + float outputSample = delayedSample * compressionGain * Parameter.OutputGain; *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue; diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs index 46c95e4f9d..950de97b89 100644 --- a/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs +++ b/Ryujinx.Audio/Renderer/Dsp/Command/LimiterCommandVersion2.cs @@ -101,32 +101,31 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command float inputCoefficient = Parameter.ReleaseCoefficient; - if (sampleInputMax > state.DectectorAverage[channelIndex]) + if (sampleInputMax > state.DetectorAverage[channelIndex].Read()) { inputCoefficient = Parameter.AttackCoefficient; } - state.DectectorAverage[channelIndex] += inputCoefficient * (sampleInputMax - state.DectectorAverage[channelIndex]); - + float detectorValue = state.DetectorAverage[channelIndex].Update(sampleInputMax, inputCoefficient); float attenuation = 1.0f; - if (state.DectectorAverage[channelIndex] > Parameter.Threshold) + if (detectorValue > Parameter.Threshold) { - attenuation = Parameter.Threshold / state.DectectorAverage[channelIndex]; + attenuation = Parameter.Threshold / detectorValue; } float outputCoefficient = Parameter.ReleaseCoefficient; - if (state.CompressionGain[channelIndex] > attenuation) + if (state.CompressionGainAverage[channelIndex].Read() > attenuation) { outputCoefficient = Parameter.AttackCoefficient; } - state.CompressionGain[channelIndex] += outputCoefficient * (attenuation - state.CompressionGain[channelIndex]); + float compressionGain = state.CompressionGainAverage[channelIndex].Update(attenuation, outputCoefficient); ref float delayedSample = ref state.DelayedSampleBuffer[channelIndex * Parameter.DelayBufferSampleCountMax + state.DelayedSampleBufferPosition[channelIndex]]; - float outputSample = delayedSample * state.CompressionGain[channelIndex] * Parameter.OutputGain; + float outputSample = delayedSample * compressionGain * Parameter.OutputGain; *((float*)outputBuffers[channelIndex] + sampleIndex) = outputSample * short.MaxValue; @@ -144,7 +143,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command ref LimiterStatistics statistics = ref MemoryMarshal.Cast(ResultState.Span[0].SpecificData)[0]; statistics.InputMax[channelIndex] = Math.Max(statistics.InputMax[channelIndex], sampleInputMax); - statistics.CompressionGainMin[channelIndex] = Math.Min(statistics.CompressionGainMin[channelIndex], state.CompressionGain[channelIndex]); + statistics.CompressionGainMin[channelIndex] = Math.Min(statistics.CompressionGainMin[channelIndex], compressionGain); } } } diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs new file mode 100644 index 0000000000..78e46bf962 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Effect/ExponentialMovingAverage.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp.Effect +{ + public struct ExponentialMovingAverage + { + private float _mean; + + public ExponentialMovingAverage(float mean) + { + _mean = mean; + } + + public float Read() + { + return _mean; + } + + public float Update(float value, float alpha) + { + _mean += alpha * (value - _mean); + + return _mean; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs index 0d0ff2ae63..280e47c0c4 100644 --- a/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs +++ b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs @@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Renderer.Dsp return (float)value / (1 << qBits); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ConvertFloat(float value, int qBits) + { + return value / (1 << qBits); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ToFixed(float value, int qBits) { diff --git a/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs index 226def46a6..6645e20a32 100644 --- a/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs +++ b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; namespace Ryujinx.Audio.Renderer.Dsp @@ -46,6 +47,53 @@ namespace Ryujinx.Audio.Renderer.Dsp return MathF.Pow(10, x); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Log10(float x) + { + // NOTE: Nintendo uses an approximation of log10, we don't. + // As such, we support the same ranges as Nintendo to avoid unexpected behaviours. + return MathF.Pow(10, MathF.Max(x, 1.0e-10f)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MeanSquare(ReadOnlySpan inputs) + { + float res = 0.0f; + + foreach (float input in inputs) + { + res += (input * input); + } + + res /= inputs.Length; + + return res; + } + + /// + /// Map decibel to linear. + /// + /// The decibel value to convert + /// Converted linear value/returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DecibelToLinear(float db) + { + return MathF.Pow(10.0f, db / 20.0f); + } + + /// + /// Map decibel to linear in [0, 2] range. + /// + /// The decibel value to convert + /// Converted linear value in [0, 2] range + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DecibelToLinearExtended(float db) + { + float tmp = MathF.Log2(DecibelToLinear(db)); + + return MathF.Truncate(tmp) + MathF.Pow(2.0f, tmp - MathF.Truncate(tmp)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float DegreesToRadians(float degrees) { diff --git a/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs b/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs new file mode 100644 index 0000000000..76aff80725 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/CompressorState.cs @@ -0,0 +1,51 @@ +using Ryujinx.Audio.Renderer.Dsp.Effect; +using Ryujinx.Audio.Renderer.Parameter.Effect; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + public class CompressorState + { + public ExponentialMovingAverage InputMovingAverage; + public float Unknown4; + public ExponentialMovingAverage CompressionGainAverage; + public float CompressorGainReduction; + public float Unknown10; + public float Unknown14; + public float PreviousCompressionEmaAlpha; + public float MakeupGain; + public float OutputGain; + + public CompressorState(ref CompressorParameter parameter) + { + InputMovingAverage = new ExponentialMovingAverage(0.0f); + Unknown4 = 1.0f; + CompressionGainAverage = new ExponentialMovingAverage(1.0f); + + UpdateParameter(ref parameter); + } + + public void UpdateParameter(ref CompressorParameter parameter) + { + float threshold = parameter.Threshold; + float ratio = 1.0f / parameter.Ratio; + float attackCoefficient = parameter.AttackCoefficient; + float makeupGain; + + if (parameter.MakeupGainEnabled) + { + makeupGain = (threshold * 0.5f * (ratio - 1.0f)) - 3.0f; + } + else + { + makeupGain = 0.0f; + } + + PreviousCompressionEmaAlpha = attackCoefficient; + MakeupGain = makeupGain; + CompressorGainReduction = (1.0f - ratio) / Constants.ChannelCountMax; + Unknown10 = threshold - 1.5f; + Unknown14 = threshold + 1.5f; + OutputGain = FloatingPointHelper.DecibelToLinearExtended(parameter.OutputGain + makeupGain); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs b/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs index 92ed13ffff..0560757c98 100644 --- a/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs +++ b/Ryujinx.Audio/Renderer/Dsp/State/LimiterState.cs @@ -1,3 +1,4 @@ +using Ryujinx.Audio.Renderer.Dsp.Effect; using Ryujinx.Audio.Renderer.Parameter.Effect; using System; @@ -5,20 +6,20 @@ namespace Ryujinx.Audio.Renderer.Dsp.State { public class LimiterState { - public float[] DectectorAverage; - public float[] CompressionGain; + public ExponentialMovingAverage[] DetectorAverage; + public ExponentialMovingAverage[] CompressionGainAverage; public float[] DelayedSampleBuffer; public int[] DelayedSampleBufferPosition; public LimiterState(ref LimiterParameter parameter, ulong workBuffer) { - DectectorAverage = new float[parameter.ChannelCount]; - CompressionGain = new float[parameter.ChannelCount]; + DetectorAverage = new ExponentialMovingAverage[parameter.ChannelCount]; + CompressionGainAverage = new ExponentialMovingAverage[parameter.ChannelCount]; DelayedSampleBuffer = new float[parameter.ChannelCount * parameter.DelayBufferSampleCountMax]; DelayedSampleBufferPosition = new int[parameter.ChannelCount]; - DectectorAverage.AsSpan().Fill(0.0f); - CompressionGain.AsSpan().Fill(1.0f); + DetectorAverage.AsSpan().Fill(new ExponentialMovingAverage(0.0f)); + CompressionGainAverage.AsSpan().Fill(new ExponentialMovingAverage(1.0f)); DelayedSampleBufferPosition.AsSpan().Fill(0); DelayedSampleBuffer.AsSpan().Fill(0.0f); diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs new file mode 100644 index 0000000000..0be376088d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/CompressorParameter.cs @@ -0,0 +1,115 @@ +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// + /// for . + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CompressorParameter + { + /// + /// The input channel indices that will be used by the . + /// + public Array6 Input; + + /// + /// The output channel indices that will be used by the . + /// + public Array6 Output; + + /// + /// The maximum number of channels supported. + /// + public ushort ChannelCountMax; + + /// + /// The total channel count used. + /// + public ushort ChannelCount; + + /// + /// The target sample rate. + /// + /// This is in kHz. + public int SampleRate; + + /// + /// The threshold. + /// + public float Threshold; + + /// + /// The compressor ratio. + /// + public float Ratio; + + /// + /// The attack time. + /// This is in microseconds. + /// + public int AttackTime; + + /// + /// The release time. + /// This is in microseconds. + /// + public int ReleaseTime; + + /// + /// The input gain. + /// + public float InputGain; + + /// + /// The attack coefficient. + /// + public float AttackCoefficient; + + /// + /// The release coefficient. + /// + public float ReleaseCoefficient; + + /// + /// The output gain. + /// + public float OutputGain; + + /// + /// The current usage status of the effect on the client side. + /// + public UsageState Status; + + /// + /// Indicate if the makeup gain should be used. + /// + [MarshalAs(UnmanagedType.I1)] + public bool MakeupGainEnabled; + + /// + /// Reserved/padding. + /// + private Array2 _reserved; + + /// + /// Check if the is valid. + /// + /// Returns true if the is valid. + public bool IsChannelCountValid() + { + return EffectInParameterVersion1.IsChannelCountValid(ChannelCount); + } + + /// + /// Check if the is valid. + /// + /// Returns true if the is valid. + public bool IsChannelCountMaxValid() + { + return EffectInParameterVersion1.IsChannelCountValid(ChannelCountMax); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs index adf5294ea0..821947a987 100644 --- a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs +++ b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs @@ -44,7 +44,7 @@ namespace Ryujinx.Audio.Renderer.Server /// was added to supply the count of update done sent to the DSP. /// A new version of the command estimator was added to address timing changes caused by the voice changes. /// Additionally, the rendering limit percent was incremented to 80%. - /// + /// /// /// This was added in system update 6.0.0 public const int Revision5 = 5 << 24; @@ -93,6 +93,7 @@ namespace Ryujinx.Audio.Renderer.Server /// /// REV11: /// The "legacy" effects (Delay, Reverb and Reverb 3D) were updated to match the standard channel mapping used by the audio renderer. + /// A new effect was added: Compressor. This effect is effectively implemented with a DRC. /// A new version of the command estimator was added to address timing changes caused by the legacy effects changes. /// A voice drop parameter was added in 15.0.0: This allows an application to amplify or attenuate the estimated time of DSP commands. /// diff --git a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs index e0741cc6e3..905cb2054d 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs @@ -469,6 +469,18 @@ namespace Ryujinx.Audio.Renderer.Server } } + public void GenerateCompressorEffect(uint bufferOffset, CompressorParameter parameter, Memory state, bool isEnabled, int nodeId) + { + if (parameter.IsChannelCountValid()) + { + CompressorCommand command = new CompressorCommand(bufferOffset, parameter, state, isEnabled, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + /// /// Generate a new . /// diff --git a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs index 87e5c77f98..afc1e39b72 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs @@ -606,6 +606,17 @@ namespace Ryujinx.Audio.Renderer.Server } } + private void GenerateCompressorEffect(uint bufferOffset, CompressorEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.Compressor); + + _commandBuffer.GenerateCompressorEffect(bufferOffset, + effect.Parameter, + effect.State, + effect.IsEnabled, + nodeId); + } + private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect) { int nodeId = mix.NodeId; @@ -650,6 +661,9 @@ namespace Ryujinx.Audio.Renderer.Server case EffectType.CaptureBuffer: GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId); break; + case EffectType.Compressor: + GenerateCompressorEffect(mix.BufferOffset, (CompressorEffect)effect, nodeId); + break; default: throw new NotImplementedException($"Unsupported effect type {effect.Type}"); } diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs index 32c52dc43d..63dc9ca96d 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs @@ -179,5 +179,10 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public uint Estimate(CompressorCommand command) + { + return 0; + } } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs index 15800c9939..7ee491cd61 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs @@ -543,5 +543,10 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public uint Estimate(CompressorCommand command) + { + return 0; + } } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs index 6c6e2828c6..b79ca13694 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs @@ -747,5 +747,10 @@ namespace Ryujinx.Audio.Renderer.Server { return 0; } + + public virtual uint Estimate(CompressorCommand command) + { + return 0; + } } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs index 961d92aa6b..2ed7e6a5b0 100644 --- a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs @@ -232,5 +232,79 @@ namespace Ryujinx.Audio.Renderer.Server } } } + + public override uint Estimate(CompressorCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return 34431; + case 2: + return 44253; + case 4: + return 63827; + case 6: + return 83361; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)630.12f; + case 2: + return (uint)638.27f; + case 4: + return (uint)705.86f; + case 6: + return (uint)782.02f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return 51095; + case 2: + return 65693; + case 4: + return 95383; + case 6: + return 124510; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)840.14f; + case 2: + return (uint)826.1f; + case 4: + return (uint)901.88f; + case 6: + return (uint)965.29f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs index 35314aca51..825b3bf76c 100644 --- a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs +++ b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs @@ -262,6 +262,8 @@ namespace Ryujinx.Audio.Renderer.Server.Effect return PerformanceDetailType.Limiter; case EffectType.CaptureBuffer: return PerformanceDetailType.CaptureBuffer; + case EffectType.Compressor: + return PerformanceDetailType.Compressor; default: throw new NotImplementedException($"{Type}"); } diff --git a/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs new file mode 100644 index 0000000000..f4e5ae829c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/CompressorEffect.cs @@ -0,0 +1,67 @@ +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// + /// Server state for a compressor effect. + /// + public class CompressorEffect : BaseEffect + { + /// + /// The compressor parameter. + /// + public CompressorParameter Parameter; + + /// + /// The compressor state. + /// + public Memory State { get; } + + /// + /// Create a new . + /// + 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(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs index e365a86c04..4872ddb3a1 100644 --- a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs +++ b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs @@ -35,5 +35,6 @@ namespace Ryujinx.Audio.Renderer.Server uint Estimate(LimiterCommandVersion2 command); uint Estimate(GroupedBiquadFilterCommand command); uint Estimate(CaptureBufferCommand command); + uint Estimate(CompressorCommand command); } } \ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs index 0c2cfa7eae..0446cd8c6e 100644 --- a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs +++ b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs @@ -240,6 +240,10 @@ namespace Ryujinx.Audio.Renderer.Server case EffectType.CaptureBuffer: effect = new CaptureBufferEffect(); break; + case EffectType.Compressor: + effect = new CompressorEffect(); + break; + default: throw new NotImplementedException($"EffectType {parameter.Type} not implemented!"); } diff --git a/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs b/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs new file mode 100644 index 0000000000..24b834fcbf --- /dev/null +++ b/Ryujinx.Tests/Audio/Renderer/Parameter/Effect/CompressorParameterTests.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Tests.Audio.Renderer.Parameter.Effect +{ + class CompressorParameterTests + { + [Test] + public void EnsureTypeSize() + { + Assert.AreEqual(0x38, Unsafe.SizeOf()); + } + } +} +