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