0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-12-22 23:05:46 +00:00

Update audio renderer to REV12: Add support for splitter biquad filter (#6813)

* Update audio renderer to REV12: Add support for splitter biquad filter

* Formatting

* Official names

* Update BiquadFilterState size + other fixes

* Update tests

* Update comment for version 2

* Size test for SplitterDestinationVersion2

* Should use Volume1 if no ramp
This commit is contained in:
gdkchan 2024-05-17 16:46:43 -03:00 committed by GitHub
parent 9ec8b2c01a
commit 4d84df9487
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2342 additions and 392 deletions

View file

@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
{ {
public const int Align = 0x10; public const int Align = 0x10;
public const int BiquadStateOffset = 0x0; public const int BiquadStateOffset = 0x0;
public const int BiquadStateSize = 0x10;
/// <summary> /// <summary>
/// The state of the biquad filters of this voice. /// The state of the biquad filters of this voice.

View file

@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// <param name="parameter">The biquad filter parameter</param> /// <param name="parameter">The biquad filter parameter</param>
/// <param name="state">The biquad filter state</param> /// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param> /// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to write the result</param> /// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param> /// <param name="sampleCount">The count of samples to process</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount) public static void ProcessBiquadFilter(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount)
{ {
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter); float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter); float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
@ -41,23 +46,24 @@ namespace Ryujinx.Audio.Renderer.Dsp
} }
/// <summary> /// <summary>
/// Apply multiple biquad filter. /// Apply a single biquad filter and mix the result into the output buffer.
/// </summary> /// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks> /// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param> /// <param name="parameter">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param> /// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param> /// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to write the result</param> /// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param> /// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Mix volume</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(ReadOnlySpan<BiquadFilterParameter> parameters, Span<BiquadFilterState> states, Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, uint sampleCount) public static void ProcessBiquadFilterAndMix(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume)
{ {
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
BiquadFilterParameter parameter = parameters[stageIndex];
ref BiquadFilterState state = ref states[stageIndex];
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter); float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter); float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter); float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
@ -75,9 +81,227 @@ namespace Ryujinx.Audio.Renderer.Dsp
state.State3 = state.State2; state.State3 = state.State2;
state.State2 = output; state.State2 = output;
outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
}
}
/// <summary>
/// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameter">The biquad filter parameter</param>
/// <param name="state">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Initial mix volume</param>
/// <param name="ramp">Volume increment step</param>
/// <returns>Last filtered sample value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float ProcessBiquadFilterAndMixRamp(
ref BiquadFilterParameter parameter,
ref BiquadFilterState state,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume,
float ramp)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
float mixState = 0f;
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
state.State0 = input;
state.State3 = state.State2;
state.State2 = output;
mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
outputBuffer[i] += mixState;
volume += ramp;
}
return mixState;
}
/// <summary>
/// Apply multiple biquad filter.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessBiquadFilter(
ReadOnlySpan<BiquadFilterParameter> parameters,
Span<BiquadFilterState> states,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
BiquadFilterParameter parameter = parameters[stageIndex];
ref BiquadFilterState state = ref states[stageIndex];
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
for (int i = 0; i < sampleCount; i++)
{
float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
state.State0 = input;
state.State3 = state.State2;
state.State2 = output;
outputBuffer[i] = output; outputBuffer[i] = output;
} }
} }
} }
/// <summary>
/// Apply double biquad filter and mix the result into the output buffer.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Mix volume</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ProcessDoubleBiquadFilterAndMix(
ref BiquadFilterParameter parameter0,
ref BiquadFilterParameter parameter1,
ref BiquadFilterState state0,
ref BiquadFilterState state1,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume)
{
float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
state0.State1 = state0.State0;
state0.State0 = input;
state0.State3 = state0.State2;
state0.State2 = output;
input = output;
output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
state1.State1 = state1.State0;
state1.State0 = input;
state1.State3 = state1.State2;
state1.State2 = output;
outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
}
}
/// <summary>
/// Apply double biquad filter and mix the result into the output buffer with volume ramp.
/// </summary>
/// <remarks>This is implemented with a direct form 1.</remarks>
/// <param name="parameters">The biquad filter parameter</param>
/// <param name="states">The biquad filter state</param>
/// <param name="outputBuffer">The output buffer to write the result</param>
/// <param name="inputBuffer">The input buffer to read the samples from</param>
/// <param name="sampleCount">The count of samples to process</param>
/// <param name="volume">Initial mix volume</param>
/// <param name="ramp">Volume increment step</param>
/// <returns>Last filtered sample value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float ProcessDoubleBiquadFilterAndMixRamp(
ref BiquadFilterParameter parameter0,
ref BiquadFilterParameter parameter1,
ref BiquadFilterState state0,
ref BiquadFilterState state1,
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
uint sampleCount,
float volume,
float ramp)
{
float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
float mixState = 0f;
for (int i = 0; i < sampleCount; i++)
{
float input = inputBuffer[i];
float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
state0.State1 = state0.State0;
state0.State0 = input;
state0.State3 = state0.State2;
state0.State2 = output;
input = output;
output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
state1.State1 = state1.State0;
state1.State0 = input;
state1.State3 = state1.State2;
state1.State2 = output;
mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
outputBuffer[i] += mixState;
volume += ramp;
}
return mixState;
}
} }
} }

View file

@ -0,0 +1,123 @@
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
public class BiquadFilterAndMixCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
public CommandType CommandType => CommandType.BiquadFilterAndMix;
public uint EstimatedProcessingTime { get; set; }
public ushort InputBufferIndex { get; }
public ushort OutputBufferIndex { get; }
private BiquadFilterParameter _parameter;
public Memory<BiquadFilterState> BiquadFilterState { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState { get; }
public Memory<VoiceUpdateState> State { get; }
public int LastSampleIndex { get; }
public float Volume0 { get; }
public float Volume1 { get; }
public bool NeedInitialization { get; }
public bool HasVolumeRamp { get; }
public bool IsFirstMixBuffer { get; }
public BiquadFilterAndMixCommand(
float volume0,
float volume1,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter,
Memory<BiquadFilterState> biquadFilterState,
Memory<BiquadFilterState> previousBiquadFilterState,
bool needInitialization,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
Enabled = true;
NodeId = nodeId;
InputBufferIndex = (ushort)inputBufferIndex;
OutputBufferIndex = (ushort)outputBufferIndex;
_parameter = filter;
BiquadFilterState = biquadFilterState;
PreviousBiquadFilterState = previousBiquadFilterState;
State = state;
LastSampleIndex = lastSampleIndex;
Volume0 = volume0;
Volume1 = volume1;
NeedInitialization = needInitialization;
HasVolumeRamp = hasVolumeRamp;
IsFirstMixBuffer = isFirstMixBuffer;
}
public void Process(CommandList context)
{
ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex);
Span<float> outputBuffer = context.GetBuffer(OutputBufferIndex);
if (NeedInitialization)
{
// If there is no previous state, initialize to zero.
BiquadFilterState.Span[0] = new BiquadFilterState();
}
else if (IsFirstMixBuffer)
{
// This is the first buffer, set previous state to current state.
PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0];
}
else
{
// Rewind the current state by copying back the previous state.
BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0];
}
if (HasVolumeRamp)
{
float volume = Volume0;
float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp(
ref _parameter,
ref BiquadFilterState.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
volume,
ramp);
}
else
{
BiquadFilterHelper.ProcessBiquadFilterAndMix(
ref _parameter,
ref BiquadFilterState.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
Volume1);
}
}
}
}

View file

@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
CopyMixBuffer, CopyMixBuffer,
LimiterVersion1, LimiterVersion1,
LimiterVersion2, LimiterVersion2,
GroupedBiquadFilter, MultiTapBiquadFilter,
CaptureBuffer, CaptureBuffer,
Compressor, Compressor,
BiquadFilterAndMix,
MultiTapBiquadFilterAndMix,
} }
} }

View file

@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public Memory<VoiceUpdateState> State { get; } public Memory<VoiceUpdateState> State { get; }
public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> volume0, Span<float> volume1, Memory<VoiceUpdateState> state, int nodeId) public MixRampGroupedCommand(
uint mixBufferCount,
uint inputBufferIndex,
uint outputBufferIndex,
ReadOnlySpan<float> volume0,
ReadOnlySpan<float> volume1,
Memory<VoiceUpdateState> state,
int nodeId)
{ {
Enabled = true; Enabled = true;
MixBufferCount = mixBufferCount; MixBufferCount = mixBufferCount;
@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float ProcessMixRampGrouped(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float volume0, float volume1, int sampleCount) private static float ProcessMixRampGrouped(
Span<float> outputBuffer,
ReadOnlySpan<float> inputBuffer,
float volume0,
float volume1,
int sampleCount)
{ {
float ramp = (volume1 - volume0) / sampleCount; float ramp = (volume1 - volume0) / sampleCount;
float volume = volume0; float volume = volume0;

View file

@ -0,0 +1,145 @@
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
public class MultiTapBiquadFilterAndMixCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix;
public uint EstimatedProcessingTime { get; set; }
public ushort InputBufferIndex { get; }
public ushort OutputBufferIndex { get; }
private BiquadFilterParameter _parameter0;
private BiquadFilterParameter _parameter1;
public Memory<BiquadFilterState> BiquadFilterState0 { get; }
public Memory<BiquadFilterState> BiquadFilterState1 { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState0 { get; }
public Memory<BiquadFilterState> PreviousBiquadFilterState1 { get; }
public Memory<VoiceUpdateState> State { get; }
public int LastSampleIndex { get; }
public float Volume0 { get; }
public float Volume1 { get; }
public bool NeedInitialization0 { get; }
public bool NeedInitialization1 { get; }
public bool HasVolumeRamp { get; }
public bool IsFirstMixBuffer { get; }
public MultiTapBiquadFilterAndMixCommand(
float volume0,
float volume1,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter0,
ref BiquadFilterParameter filter1,
Memory<BiquadFilterState> biquadFilterState0,
Memory<BiquadFilterState> biquadFilterState1,
Memory<BiquadFilterState> previousBiquadFilterState0,
Memory<BiquadFilterState> previousBiquadFilterState1,
bool needInitialization0,
bool needInitialization1,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
Enabled = true;
NodeId = nodeId;
InputBufferIndex = (ushort)inputBufferIndex;
OutputBufferIndex = (ushort)outputBufferIndex;
_parameter0 = filter0;
_parameter1 = filter1;
BiquadFilterState0 = biquadFilterState0;
BiquadFilterState1 = biquadFilterState1;
PreviousBiquadFilterState0 = previousBiquadFilterState0;
PreviousBiquadFilterState1 = previousBiquadFilterState1;
State = state;
LastSampleIndex = lastSampleIndex;
Volume0 = volume0;
Volume1 = volume1;
NeedInitialization0 = needInitialization0;
NeedInitialization1 = needInitialization1;
HasVolumeRamp = hasVolumeRamp;
IsFirstMixBuffer = isFirstMixBuffer;
}
private void UpdateState(Memory<BiquadFilterState> state, Memory<BiquadFilterState> previousState, bool needInitialization)
{
if (needInitialization)
{
// If there is no previous state, initialize to zero.
state.Span[0] = new BiquadFilterState();
}
else if (IsFirstMixBuffer)
{
// This is the first buffer, set previous state to current state.
previousState.Span[0] = state.Span[0];
}
else
{
// Rewind the current state by copying back the previous state.
state.Span[0] = previousState.Span[0];
}
}
public void Process(CommandList context)
{
ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex);
Span<float> outputBuffer = context.GetBuffer(OutputBufferIndex);
UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0);
UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1);
if (HasVolumeRamp)
{
float volume = Volume0;
float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp(
ref _parameter0,
ref _parameter1,
ref BiquadFilterState0.Span[0],
ref BiquadFilterState1.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
volume,
ramp);
}
else
{
BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix(
ref _parameter0,
ref _parameter1,
ref BiquadFilterState0.Span[0],
ref BiquadFilterState1.Span[0],
outputBuffer,
inputBuffer,
context.SampleCount,
Volume1);
}
}
}
}

View file

@ -4,13 +4,13 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command namespace Ryujinx.Audio.Renderer.Dsp.Command
{ {
public class GroupedBiquadFilterCommand : ICommand public class MultiTapBiquadFilterCommand : ICommand
{ {
public bool Enabled { get; set; } public bool Enabled { get; set; }
public int NodeId { get; } public int NodeId { get; }
public CommandType CommandType => CommandType.GroupedBiquadFilter; public CommandType CommandType => CommandType.MultiTapBiquadFilter;
public uint EstimatedProcessingTime { get; set; } public uint EstimatedProcessingTime { get; set; }
@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private readonly int _outputBufferIndex; private readonly int _outputBufferIndex;
private readonly bool[] _isInitialized; private readonly bool[] _isInitialized;
public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId) public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
{ {
_parameters = filters.ToArray(); _parameters = filters.ToArray();
_biquadFilterStates = biquadFilterStateMemory; _biquadFilterStates = biquadFilterStateMemory;

View file

@ -2,12 +2,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.State namespace Ryujinx.Audio.Renderer.Dsp.State
{ {
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)]
public struct BiquadFilterState public struct BiquadFilterState
{ {
public float State0; public float State0;
public float State1; public float State1;
public float State2; public float State2;
public float State3; public float State3;
public float State4;
public float State5;
public float State6;
public float State7;
} }
} }

View file

@ -0,0 +1,43 @@
using Ryujinx.Common.Memory;
using System;
namespace Ryujinx.Audio.Renderer.Parameter
{
/// <summary>
/// Generic interface for the splitter destination parameters.
/// </summary>
public interface ISplitterDestinationInParameter
{
/// <summary>
/// Target splitter destination data id.
/// </summary>
int Id { get; }
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
int DestinationId { get; }
/// <summary>
/// Biquad filter parameters.
/// </summary>
Array2<BiquadFilterParameter> BiquadFilters { get; }
/// <summary>
/// Set to true if in use.
/// </summary>
bool IsUsed { get; }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
Span<float> MixBufferVolume { get; }
/// <summary>
/// Check if the magic is valid.
/// </summary>
/// <returns>Returns true if the magic is valid.</returns>
bool IsMagicValid();
}
}

View file

@ -1,3 +1,4 @@
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -5,10 +6,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter namespace Ryujinx.Audio.Renderer.Parameter
{ {
/// <summary> /// <summary>
/// Input header for a splitter destination update. /// Input header for a splitter destination version 1 update.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SplitterDestinationInParameter public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
{ {
/// <summary> /// <summary>
/// Magic of the input header. /// Magic of the input header.
@ -41,7 +42,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// </summary> /// </summary>
private unsafe fixed byte _reserved[3]; private unsafe fixed byte _reserved[3];
[StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { } private struct MixArray { }
/// <summary> /// <summary>
@ -50,6 +51,14 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// <remarks>Used when a splitter id is specified in the mix.</remarks> /// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume); public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
readonly int ISplitterDestinationInParameter.Id => Id;
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => default;
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
/// <summary> /// <summary>
/// The expected constant of any input header. /// The expected constant of any input header.
/// </summary> /// </summary>

View file

@ -0,0 +1,81 @@
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
/// <summary>
/// Input header for a splitter destination version 2 update.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
{
/// <summary>
/// Magic of the input header.
/// </summary>
public uint Magic;
/// <summary>
/// Target splitter destination data id.
/// </summary>
public int Id;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mixBufferVolume;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Biquad filter parameters.
/// </summary>
public Array2<BiquadFilterParameter> BiquadFilters;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Reserved/padding.
/// </summary>
private unsafe fixed byte _reserved[11];
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume);
readonly int ISplitterDestinationInParameter.Id => Id;
readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
readonly Array2<BiquadFilterParameter> ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
/// <summary>
/// The expected constant of any input header.
/// </summary>
private const uint ValidMagic = 0x44444E53;
/// <summary>
/// Check if the magic is valid.
/// </summary>
/// <returns>Returns true if the magic is valid.</returns>
public readonly bool IsMagicValid()
{
return Magic == ValidMagic;
}
}
}

View file

@ -1,6 +1,7 @@
using Ryujinx.Audio.Integration; using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.Command; using Ryujinx.Audio.Renderer.Dsp.Command;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter; using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Server.Effect; using Ryujinx.Audio.Renderer.Server.Effect;
using Ryujinx.Audio.Renderer.Server.MemoryPool; using Ryujinx.Audio.Renderer.Server.MemoryPool;
@ -173,6 +174,22 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.WorkBufferTooSmall; return ResultCode.WorkBufferTooSmall;
} }
Memory<BiquadFilterState> splitterBqfStates = Memory<BiquadFilterState>.Empty;
if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
parameter.SplitterCount > 0 &&
parameter.SplitterDestinationCount > 0)
{
splitterBqfStates = workBufferAllocator.Allocate<BiquadFilterState>(parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
if (splitterBqfStates.IsEmpty)
{
return ResultCode.WorkBufferTooSmall;
}
splitterBqfStates.Span.Clear();
}
// Invalidate DSP cache on what was currently allocated with workBuffer. // Invalidate DSP cache on what was currently allocated with workBuffer.
AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset); AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset);
@ -292,7 +309,7 @@ namespace Ryujinx.Audio.Renderer.Server
state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu); state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu);
} }
if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator)) if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator, splitterBqfStates))
{ {
return ResultCode.WorkBufferTooSmall; return ResultCode.WorkBufferTooSmall;
} }
@ -775,6 +792,13 @@ namespace Ryujinx.Audio.Renderer.Server
// Splitter // Splitter
size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter); size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter);
if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
parameter.SplitterCount > 0 &&
parameter.SplitterDestinationCount > 0)
{
size = WorkBufferAllocator.GetTargetSize<BiquadFilterState>(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
}
// DSP Voice // DSP Voice
size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align); size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align);

View file

@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server
/// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP. /// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP.
/// A new version of the command estimator was added to address timing changes caused by the voice changes. /// A new version of the command estimator was added to address timing changes caused by the voice changes.
/// Additionally, the rendering limit percent was incremented to 80%. /// Additionally, the rendering limit percent was incremented to 80%.
///
/// </summary> /// </summary>
/// <remarks>This was added in system update 6.0.0</remarks> /// <remarks>This was added in system update 6.0.0</remarks>
public const int Revision5 = 5 << 24; public const int Revision5 = 5 << 24;
@ -101,10 +100,18 @@ namespace Ryujinx.Audio.Renderer.Server
/// <remarks>This was added in system update 14.0.0 but some changes were made in 15.0.0</remarks> /// <remarks>This was added in system update 14.0.0 but some changes were made in 15.0.0</remarks>
public const int Revision11 = 11 << 24; public const int Revision11 = 11 << 24;
/// <summary>
/// REV12:
/// Two new commands were added to for biquad filtering and mixing (with optinal volume ramp) on the same command.
/// Splitter destinations can now specify up to two biquad filtering parameters, used for filtering the buffer before mixing.
/// </summary>
/// <remarks>This was added in system update 17.0.0</remarks>
public const int Revision12 = 12 << 24;
/// <summary> /// <summary>
/// Last revision supported by the implementation. /// Last revision supported by the implementation.
/// </summary> /// </summary>
public const int LastRevision = Revision11; public const int LastRevision = Revision12;
/// <summary> /// <summary>
/// Target revision magic supported by the implementation. /// Target revision magic supported by the implementation.
@ -354,7 +361,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice. /// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice.
/// </summary> /// </summary>
/// <returns>True if the audio renderer should use the optimization.</returns> /// <returns>True if the audio renderer should use the optimization.</returns>
public bool IsBiquadFilterGroupedOptimizationSupported() public bool UseMultiTapBiquadFilterProcessing()
{ {
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10); return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10);
} }
@ -368,6 +375,15 @@ namespace Ryujinx.Audio.Renderer.Server
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11); return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11);
} }
/// <summary>
/// Check if the audio renderer should support biquad filter on splitter.
/// </summary>
/// <returns>True if the audio renderer support biquad filter on splitter</returns>
public bool IsBiquadFilterParameterForSplitterEnabled()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
}
/// <summary> /// <summary>
/// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>. /// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>.
/// </summary> /// </summary>

View file

@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server
} }
/// <summary> /// <summary>
/// Create a new <see cref="GroupedBiquadFilterCommand"/>. /// Create a new <see cref="MultiTapBiquadFilterCommand"/>.
/// </summary> /// </summary>
/// <param name="baseIndex">The base index of the input and output buffer.</param> /// <param name="baseIndex">The base index of the input and output buffer.</param>
/// <param name="filters">The biquad filter parameters.</param> /// <param name="filters">The biquad filter parameters.</param>
@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="outputBufferOffset">The output buffer offset.</param> /// <param name="outputBufferOffset">The output buffer offset.</param>
/// <param name="isInitialized">Set to true if the biquad filter state is initialized.</param> /// <param name="isInitialized">Set to true if the biquad filter state is initialized.</param>
/// <param name="nodeId">The node id associated to this command.</param> /// <param name="nodeId">The node id associated to this command.</param>
public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId) public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan<BiquadFilterParameter> filters, Memory<BiquadFilterState> biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan<bool> isInitialized, int nodeId)
{ {
GroupedBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId); MultiTapBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
@ -232,7 +232,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="volume">The new volume.</param> /// <param name="volume">The new volume.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="nodeId">The node id associated to this command.</param> /// <param name="nodeId">The node id associated to this command.</param>
public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> previousVolume, Span<float> volume, Memory<VoiceUpdateState> state, int nodeId) public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan<float> previousVolume, ReadOnlySpan<float> volume, Memory<VoiceUpdateState> state, int nodeId)
{ {
MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId); MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId);
@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server
AddCommand(command); AddCommand(command);
} }
/// <summary>
/// Generate a new <see cref="BiquadFilterAndMixCommand"/>.
/// </summary>
/// <param name="previousVolume">The previous volume.</param>
/// <param name="volume">The new volume.</param>
/// <param name="inputBufferIndex">The input buffer index.</param>
/// <param name="outputBufferIndex">The output buffer index.</param>
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="filter">The biquad filter parameter.</param>
/// <param name="biquadFilterState">The biquad state.</param>
/// <param name="previousBiquadFilterState">The previous biquad state.</param>
/// <param name="needInitialization">Set to true if the biquad filter state needs to be initialized.</param>
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateBiquadFilterAndMix(
float previousVolume,
float volume,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter,
Memory<BiquadFilterState> biquadFilterState,
Memory<BiquadFilterState> previousBiquadFilterState,
bool needInitialization,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
BiquadFilterAndMixCommand command = new(
previousVolume,
volume,
inputBufferIndex,
outputBufferIndex,
lastSampleIndex,
state,
ref filter,
biquadFilterState,
previousBiquadFilterState,
needInitialization,
hasVolumeRamp,
isFirstMixBuffer,
nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
AddCommand(command);
}
/// <summary>
/// Generate a new <see cref="MultiTapBiquadFilterAndMixCommand"/>.
/// </summary>
/// <param name="previousVolume">The previous volume.</param>
/// <param name="volume">The new volume.</param>
/// <param name="inputBufferIndex">The input buffer index.</param>
/// <param name="outputBufferIndex">The output buffer index.</param>
/// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param>
/// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param>
/// <param name="filter0">First biquad filter parameter.</param>
/// <param name="filter1">Second biquad filter parameter.</param>
/// <param name="biquadFilterState0">First biquad state.</param>
/// <param name="biquadFilterState1">Second biquad state.</param>
/// <param name="previousBiquadFilterState0">First previous biquad state.</param>
/// <param name="previousBiquadFilterState1">Second previous biquad state.</param>
/// <param name="needInitialization0">Set to true if the first biquad filter state needs to be initialized.</param>
/// <param name="needInitialization1">Set to true if the second biquad filter state needs to be initialized.</param>
/// <param name="hasVolumeRamp">Set to true if the mix has volume ramp, and <paramref name="previousVolume"/> should be taken into account.</param>
/// <param name="isFirstMixBuffer">Set to true if the buffer is the first mix buffer.</param>
/// <param name="nodeId">The node id associated to this command.</param>
public void GenerateMultiTapBiquadFilterAndMix(
float previousVolume,
float volume,
uint inputBufferIndex,
uint outputBufferIndex,
int lastSampleIndex,
Memory<VoiceUpdateState> state,
ref BiquadFilterParameter filter0,
ref BiquadFilterParameter filter1,
Memory<BiquadFilterState> biquadFilterState0,
Memory<BiquadFilterState> biquadFilterState1,
Memory<BiquadFilterState> previousBiquadFilterState0,
Memory<BiquadFilterState> previousBiquadFilterState1,
bool needInitialization0,
bool needInitialization1,
bool hasVolumeRamp,
bool isFirstMixBuffer,
int nodeId)
{
MultiTapBiquadFilterAndMixCommand command = new(
previousVolume,
volume,
inputBufferIndex,
outputBufferIndex,
lastSampleIndex,
state,
ref filter0,
ref filter1,
biquadFilterState0,
biquadFilterState1,
previousBiquadFilterState0,
previousBiquadFilterState1,
needInitialization0,
needInitialization1,
hasVolumeRamp,
isFirstMixBuffer,
nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
AddCommand(command);
}
/// <summary> /// <summary>
/// Generate a new <see cref="DepopForMixBuffersCommand"/>. /// Generate a new <see cref="DepopForMixBuffersCommand"/>.
/// </summary> /// </summary>
@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <param name="bufferCount">The buffer count.</param> /// <param name="bufferCount">The buffer count.</param>
/// <param name="nodeId">The node id associated to this command.</param> /// <param name="nodeId">The node id associated to this command.</param>
/// <param name="sampleRate">The target sample rate in use.</param> /// <param name="sampleRate">The target sample rate in use.</param>
public void GenerateDepopForMixBuffersCommand(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate) public void GenerateDepopForMixBuffers(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
{ {
DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate); DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate);

View file

@ -12,6 +12,7 @@ using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils; using Ryujinx.Audio.Renderer.Utils;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server namespace Ryujinx.Audio.Renderer.Server
{ {
@ -46,7 +47,8 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
ref MixState mix = ref _mixContext.GetState(voiceState.MixId); ref MixState mix = ref _mixContext.GetState(voiceState.MixId);
_commandBuffer.GenerateDepopPrepare(dspState, _commandBuffer.GenerateDepopPrepare(
dspState,
_rendererContext.DepopBuffer, _rendererContext.DepopBuffer,
mix.BufferCount, mix.BufferCount,
mix.BufferOffset, mix.BufferOffset,
@ -59,15 +61,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true) while (true)
{ {
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++); SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
if (destinationSpan.IsEmpty) if (destination.IsNull)
{ {
break; break;
} }
ref SplitterDestination destination = ref destinationSpan[0];
if (destination.IsConfigured()) if (destination.IsConfigured())
{ {
int mixId = destination.DestinationId; int mixId = destination.DestinationId;
@ -76,7 +76,8 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
ref MixState mix = ref _mixContext.GetState(mixId); ref MixState mix = ref _mixContext.GetState(mixId);
_commandBuffer.GenerateDepopPrepare(dspState, _commandBuffer.GenerateDepopPrepare(
dspState,
_rendererContext.DepopBuffer, _rendererContext.DepopBuffer,
mix.BufferCount, mix.BufferCount,
mix.BufferOffset, mix.BufferOffset,
@ -95,7 +96,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported()) if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported())
{ {
_commandBuffer.GenerateDataSourceVersion2(ref voiceState, _commandBuffer.GenerateDataSourceVersion2(
ref voiceState,
dspState, dspState,
(ushort)_rendererContext.MixBufferCount, (ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex, (ushort)channelIndex,
@ -106,21 +108,24 @@ namespace Ryujinx.Audio.Renderer.Server
switch (voiceState.SampleFormat) switch (voiceState.SampleFormat)
{ {
case SampleFormat.PcmInt16: case SampleFormat.PcmInt16:
_commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState, _commandBuffer.GeneratePcmInt16DataSourceVersion1(
ref voiceState,
dspState, dspState,
(ushort)_rendererContext.MixBufferCount, (ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex, (ushort)channelIndex,
voiceState.NodeId); voiceState.NodeId);
break; break;
case SampleFormat.PcmFloat: case SampleFormat.PcmFloat:
_commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState, _commandBuffer.GeneratePcmFloatDataSourceVersion1(
ref voiceState,
dspState, dspState,
(ushort)_rendererContext.MixBufferCount, (ushort)_rendererContext.MixBufferCount,
(ushort)channelIndex, (ushort)channelIndex,
voiceState.NodeId); voiceState.NodeId);
break; break;
case SampleFormat.Adpcm: case SampleFormat.Adpcm:
_commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState, _commandBuffer.GenerateAdpcmDataSourceVersion1(
ref voiceState,
dspState, dspState,
(ushort)_rendererContext.MixBufferCount, (ushort)_rendererContext.MixBufferCount,
voiceState.NodeId); voiceState.NodeId);
@ -134,14 +139,14 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId) private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId)
{ {
bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported(); bool supportsOptimizedPath = _rendererContext.BehaviourContext.UseMultiTapBiquadFilterProcessing();
if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable) if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable)
{ {
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)]; Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory); Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);
_commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId); _commandBuffer.GenerateMultiTapBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
} }
else else
{ {
@ -151,11 +156,11 @@ namespace Ryujinx.Audio.Renderer.Server
if (filter.Enable) if (filter.Enable)
{ {
Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)]; Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state)[..(Unsafe.SizeOf<BiquadFilterState>() * Constants.VoiceBiquadFilterCount)];
Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory); Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);
_commandBuffer.GenerateBiquadFilter(baseIndex, _commandBuffer.GenerateBiquadFilter(
baseIndex,
ref filter, ref filter,
stateMemory.Slice(i, 1), stateMemory.Slice(i, 1),
bufferOffset, bufferOffset,
@ -167,11 +172,112 @@ namespace Ryujinx.Audio.Renderer.Server
} }
} }
private void GenerateVoiceMix(Span<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId) private void GenerateVoiceMixWithSplitter(
SplitterDestination destination,
Memory<VoiceUpdateState> state,
uint bufferOffset,
uint bufferCount,
uint bufferIndex,
int nodeId)
{
ReadOnlySpan<float> mixVolumes = destination.MixBufferVolume;
ReadOnlySpan<float> previousMixVolumes = destination.PreviousMixBufferVolume;
ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
Memory<BiquadFilterState> bqfState = _splitterContext.GetBiquadFilterState(destination);
bool isFirstMixBuffer = true;
for (int i = 0; i < bufferCount; i++)
{
float previousMixVolume = previousMixVolumes[i];
float mixVolume = mixVolumes[i];
if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{
if (bqf0.Enable && bqf1.Enable)
{
_commandBuffer.GenerateMultiTapBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf0,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
bqfState.Slice(2, 1),
bqfState.Slice(3, 1),
!destination.IsBiquadFilterEnabledPrev(),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
destination.UpdateBiquadFilterEnabledPrev(1);
}
else if (bqf0.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf0,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
}
else if (bqf1.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
previousMixVolume,
mixVolume,
bufferIndex,
bufferOffset + (uint)i,
i,
state,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
true,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(1);
}
isFirstMixBuffer = false;
}
}
}
private void GenerateVoiceMix(
ReadOnlySpan<float> mixVolumes,
ReadOnlySpan<float> previousMixVolumes,
Memory<VoiceUpdateState> state,
uint bufferOffset,
uint bufferCount,
uint bufferIndex,
int nodeId)
{ {
if (bufferCount > Constants.VoiceChannelCountMax) if (bufferCount > Constants.VoiceChannelCountMax)
{ {
_commandBuffer.GenerateMixRampGrouped(bufferCount, _commandBuffer.GenerateMixRampGrouped(
bufferCount,
bufferIndex, bufferIndex,
bufferOffset, bufferOffset,
previousMixVolumes, previousMixVolumes,
@ -188,7 +294,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (mixVolume != 0.0f || previousMixVolume != 0.0f) if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{ {
_commandBuffer.GenerateMixRamp(previousMixVolume, _commandBuffer.GenerateMixRamp(
previousMixVolume,
mixVolume, mixVolume,
bufferIndex, bufferIndex,
bufferOffset + (uint)i, bufferOffset + (uint)i,
@ -271,7 +378,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
} }
_commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume, _commandBuffer.GenerateVolumeRamp(
voiceState.PreviousVolume,
voiceState.Volume, voiceState.Volume,
_rendererContext.MixBufferCount + (uint)channelIndex, _rendererContext.MixBufferCount + (uint)channelIndex,
nodeId); nodeId);
@ -291,15 +399,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true) while (true)
{ {
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId); SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
if (destinationSpan.IsEmpty) if (destination.IsNull)
{ {
break; break;
} }
ref SplitterDestination destination = ref destinationSpan[0];
destinationId += (int)channelsCount; destinationId += (int)channelsCount;
if (destination.IsConfigured()) if (destination.IsConfigured())
@ -310,13 +416,27 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
ref MixState mix = ref _mixContext.GetState(mixId); ref MixState mix = ref _mixContext.GetState(mixId);
GenerateVoiceMix(destination.MixBufferVolume, if (destination.IsBiquadFilterEnabled())
{
GenerateVoiceMixWithSplitter(
destination,
dspStateMemory,
mix.BufferOffset,
mix.BufferCount,
_rendererContext.MixBufferCount + (uint)channelIndex,
nodeId);
}
else
{
GenerateVoiceMix(
destination.MixBufferVolume,
destination.PreviousMixBufferVolume, destination.PreviousMixBufferVolume,
dspStateMemory, dspStateMemory,
mix.BufferOffset, mix.BufferOffset,
mix.BufferCount, mix.BufferCount,
_rendererContext.MixBufferCount + (uint)channelIndex, _rendererContext.MixBufferCount + (uint)channelIndex,
nodeId); nodeId);
}
destination.MarkAsNeedToUpdateInternalState(); destination.MarkAsNeedToUpdateInternalState();
} }
@ -337,7 +457,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
} }
GenerateVoiceMix(channelResource.Mix.AsSpan(), GenerateVoiceMix(
channelResource.Mix.AsSpan(),
channelResource.PreviousMix.AsSpan(), channelResource.PreviousMix.AsSpan(),
dspStateMemory, dspStateMemory,
mix.BufferOffset, mix.BufferOffset,
@ -409,7 +530,8 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
if (effect.Parameter.Volumes[i] != 0.0f) if (effect.Parameter.Volumes[i] != 0.0f)
{ {
_commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i], _commandBuffer.GenerateMix(
(uint)bufferOffset + effect.Parameter.Input[i],
(uint)bufferOffset + effect.Parameter.Output[i], (uint)bufferOffset + effect.Parameter.Output[i],
nodeId, nodeId,
effect.Parameter.Volumes[i]); effect.Parameter.Volumes[i]);
@ -447,7 +569,8 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount; updateCount = newUpdateCount;
} }
_commandBuffer.GenerateAuxEffect(bufferOffset, _commandBuffer.GenerateAuxEffect(
bufferOffset,
effect.Parameter.Input[i], effect.Parameter.Input[i],
effect.Parameter.Output[i], effect.Parameter.Output[i],
ref effect.State, ref effect.State,
@ -512,7 +635,10 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < effect.Parameter.ChannelCount; i++) for (int i = 0; i < effect.Parameter.ChannelCount; i++)
{ {
_commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1), _commandBuffer.GenerateBiquadFilter(
(int)bufferOffset,
ref parameter,
effect.State.Slice(i, 1),
effect.Parameter.Input[i], effect.Parameter.Input[i],
effect.Parameter.Output[i], effect.Parameter.Output[i],
needInitialization, needInitialization,
@ -591,7 +717,8 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount; updateCount = newUpdateCount;
} }
_commandBuffer.GenerateCaptureEffect(bufferOffset, _commandBuffer.GenerateCaptureEffect(
bufferOffset,
effect.Parameter.Input[i], effect.Parameter.Input[i],
effect.State.SendBufferInfo, effect.State.SendBufferInfo,
effect.IsEnabled, effect.IsEnabled,
@ -612,7 +739,8 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
Debug.Assert(effect.Type == EffectType.Compressor); Debug.Assert(effect.Type == EffectType.Compressor);
_commandBuffer.GenerateCompressorEffect(bufferOffset, _commandBuffer.GenerateCompressorEffect(
bufferOffset,
effect.Parameter, effect.Parameter,
effect.State, effect.State,
effect.IsEnabled, effect.IsEnabled,
@ -629,8 +757,11 @@ namespace Ryujinx.Audio.Renderer.Server
bool performanceInitialized = false; bool performanceInitialized = false;
if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(), if (_performanceManager != null && _performanceManager.GetNextEntry(
isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId)) out performanceEntry,
effect.GetPerformanceDetailType(),
isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix,
nodeId))
{ {
performanceInitialized = true; performanceInitialized = true;
@ -706,6 +837,85 @@ namespace Ryujinx.Audio.Renderer.Server
} }
} }
private void GenerateMixWithSplitter(
uint inputBufferIndex,
uint outputBufferIndex,
float volume,
SplitterDestination destination,
ref bool isFirstMixBuffer,
int nodeId)
{
ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
Memory<BiquadFilterState> bqfState = _splitterContext.GetBiquadFilterState(destination);
if (bqf0.Enable && bqf1.Enable)
{
_commandBuffer.GenerateMultiTapBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf0,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
bqfState.Slice(2, 1),
bqfState.Slice(3, 1),
!destination.IsBiquadFilterEnabledPrev(),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
destination.UpdateBiquadFilterEnabledPrev(1);
}
else if (bqf0.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf0,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(0);
}
else if (bqf1.Enable)
{
_commandBuffer.GenerateBiquadFilterAndMix(
0f,
volume,
inputBufferIndex,
outputBufferIndex,
0,
Memory<VoiceUpdateState>.Empty,
ref bqf1,
bqfState[..1],
bqfState.Slice(1, 1),
!destination.IsBiquadFilterEnabledPrev(),
false,
isFirstMixBuffer,
nodeId);
destination.UpdateBiquadFilterEnabledPrev(1);
}
isFirstMixBuffer = false;
}
private void GenerateMix(ref MixState mix) private void GenerateMix(ref MixState mix)
{ {
if (mix.HasAnyDestination()) if (mix.HasAnyDestination())
@ -722,15 +932,13 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
int destinationIndex = destinationId++; int destinationIndex = destinationId++;
Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex); SplitterDestination destination = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
if (destinationSpan.IsEmpty) if (destination.IsNull)
{ {
break; break;
} }
ref SplitterDestination destination = ref destinationSpan[0];
if (destination.IsConfigured()) if (destination.IsConfigured())
{ {
int mixId = destination.DestinationId; int mixId = destination.DestinationId;
@ -741,13 +949,28 @@ namespace Ryujinx.Audio.Renderer.Server
uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount); uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount);
bool isFirstMixBuffer = true;
for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++)
{ {
float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex); float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex);
if (volume != 0.0f) if (volume != 0.0f)
{ {
_commandBuffer.GenerateMix(inputBufferIndex, if (destination.IsBiquadFilterEnabled())
{
GenerateMixWithSplitter(
inputBufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex,
volume,
destination,
ref isFirstMixBuffer,
mix.NodeId);
}
else
{
_commandBuffer.GenerateMix(
inputBufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex, destinationMix.BufferOffset + bufferDestinationIndex,
mix.NodeId, mix.NodeId,
volume); volume);
@ -758,6 +981,7 @@ namespace Ryujinx.Audio.Renderer.Server
} }
} }
} }
}
else else
{ {
ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId); ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId);
@ -770,7 +994,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (volume != 0.0f) if (volume != 0.0f)
{ {
_commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex, _commandBuffer.GenerateMix(
mix.BufferOffset + bufferIndex,
destinationMix.BufferOffset + bufferDestinationIndex, destinationMix.BufferOffset + bufferDestinationIndex,
mix.NodeId, mix.NodeId,
volume); volume);
@ -783,7 +1008,8 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateSubMix(ref MixState subMix) private void GenerateSubMix(ref MixState subMix)
{ {
_commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, _commandBuffer.GenerateDepopForMixBuffers(
_rendererContext.DepopBuffer,
subMix.BufferOffset, subMix.BufferOffset,
subMix.BufferCount, subMix.BufferCount,
subMix.NodeId, subMix.NodeId,
@ -847,7 +1073,8 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
ref MixState finalMix = ref _mixContext.GetFinalState(); ref MixState finalMix = ref _mixContext.GetFinalState();
_commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, _commandBuffer.GenerateDepopForMixBuffers(
_rendererContext.DepopBuffer,
finalMix.BufferOffset, finalMix.BufferOffset,
finalMix.BufferCount, finalMix.BufferCount,
finalMix.NodeId, finalMix.NodeId,
@ -882,7 +1109,8 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
} }
_commandBuffer.GenerateVolume(finalMix.Volume, _commandBuffer.GenerateVolume(
finalMix.Volume,
finalMix.BufferOffset + bufferIndex, finalMix.BufferOffset + bufferIndex,
nodeId); nodeId);
@ -938,7 +1166,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (useCustomDownMixingCommand) if (useCustomDownMixingCommand)
{ {
_commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, _commandBuffer.GenerateDownMixSurroundToStereo(
finalMix.BufferOffset,
sink.Parameter.Input.AsSpan(), sink.Parameter.Input.AsSpan(),
sink.Parameter.Input.AsSpan(), sink.Parameter.Input.AsSpan(),
sink.DownMixCoefficients, sink.DownMixCoefficients,
@ -947,7 +1176,8 @@ namespace Ryujinx.Audio.Renderer.Server
// NOTE: We do the downmixing at the DSP level as it's easier that way. // NOTE: We do the downmixing at the DSP level as it's easier that way.
else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6) else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6)
{ {
_commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, _commandBuffer.GenerateDownMixSurroundToStereo(
finalMix.BufferOffset,
sink.Parameter.Input.AsSpan(), sink.Parameter.Input.AsSpan(),
sink.Parameter.Input.AsSpan(), sink.Parameter.Input.AsSpan(),
Constants.DefaultSurroundToStereoCoefficients, Constants.DefaultSurroundToStereoCoefficients,
@ -958,7 +1188,8 @@ namespace Ryujinx.Audio.Renderer.Server
if (sink.UpsamplerState != null) if (sink.UpsamplerState != null)
{ {
_commandBuffer.GenerateUpsample(finalMix.BufferOffset, _commandBuffer.GenerateUpsample(
finalMix.BufferOffset,
sink.UpsamplerState, sink.UpsamplerState,
sink.Parameter.InputCount, sink.Parameter.InputCount,
sink.Parameter.Input.AsSpan(), sink.Parameter.Input.AsSpan(),
@ -968,7 +1199,8 @@ namespace Ryujinx.Audio.Renderer.Server
Constants.InvalidNodeId); Constants.InvalidNodeId);
} }
_commandBuffer.GenerateDeviceSink(finalMix.BufferOffset, _commandBuffer.GenerateDeviceSink(
finalMix.BufferOffset,
sink, sink,
_rendererContext.SessionId, _rendererContext.SessionId,
commandList.Buffers, commandList.Buffers,

View file

@ -170,7 +170,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0; return 0;
} }
public uint Estimate(GroupedBiquadFilterCommand command) public uint Estimate(MultiTapBiquadFilterCommand command)
{ {
return 0; return 0;
} }
@ -184,5 +184,15 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
return 0; return 0;
} }
public uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
} }
} }

View file

@ -462,7 +462,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0; return 0;
} }
public uint Estimate(GroupedBiquadFilterCommand command) public uint Estimate(MultiTapBiquadFilterCommand command)
{ {
return 0; return 0;
} }
@ -476,5 +476,15 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
return 0; return 0;
} }
public uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
} }
} }

View file

@ -632,7 +632,7 @@ namespace Ryujinx.Audio.Renderer.Server
}; };
} }
public virtual uint Estimate(GroupedBiquadFilterCommand command) public virtual uint Estimate(MultiTapBiquadFilterCommand command)
{ {
return 0; return 0;
} }
@ -646,5 +646,15 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
return 0; return 0;
} }
public virtual uint Estimate(BiquadFilterAndMixCommand command)
{
return 0;
}
public virtual uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
return 0;
}
} }
} }

View file

@ -10,7 +10,7 @@ namespace Ryujinx.Audio.Renderer.Server
{ {
public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { } public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { }
public override uint Estimate(GroupedBiquadFilterCommand command) public override uint Estimate(MultiTapBiquadFilterCommand command)
{ {
Debug.Assert(SampleCount == 160 || SampleCount == 240); Debug.Assert(SampleCount == 160 || SampleCount == 240);

View file

@ -210,5 +210,53 @@ namespace Ryujinx.Audio.Renderer.Server
_ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"), _ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
}; };
} }
public override uint Estimate(BiquadFilterAndMixCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
if (command.HasVolumeRamp)
{
if (SampleCount == 160)
{
return 5204;
}
return 6683;
}
else
{
if (SampleCount == 160)
{
return 3427;
}
return 4752;
}
}
public override uint Estimate(MultiTapBiquadFilterAndMixCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
if (command.HasVolumeRamp)
{
if (SampleCount == 160)
{
return 7939;
}
return 10669;
}
else
{
if (SampleCount == 160)
{
return 6256;
}
return 8683;
}
}
} }
} }

View file

@ -33,8 +33,10 @@ namespace Ryujinx.Audio.Renderer.Server
uint Estimate(UpsampleCommand command); uint Estimate(UpsampleCommand command);
uint Estimate(LimiterCommandVersion1 command); uint Estimate(LimiterCommandVersion1 command);
uint Estimate(LimiterCommandVersion2 command); uint Estimate(LimiterCommandVersion2 command);
uint Estimate(GroupedBiquadFilterCommand command); uint Estimate(MultiTapBiquadFilterCommand command);
uint Estimate(CaptureBufferCommand command); uint Estimate(CaptureBufferCommand command);
uint Estimate(CompressorCommand command); uint Estimate(CompressorCommand command);
uint Estimate(BiquadFilterAndMixCommand command);
uint Estimate(MultiTapBiquadFilterAndMixCommand command);
} }
} }

View file

@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
for (int i = 0; i < splitter.DestinationCount; i++) for (int i = 0; i < splitter.DestinationCount; i++)
{ {
Span<SplitterDestination> destination = splitter.GetData(i); SplitterDestination destination = splitter.GetData(i);
if (!destination.IsEmpty) if (!destination.IsNull)
{ {
int destinationMixId = destination[0].DestinationId; int destinationMixId = destination.DestinationId;
if (destinationMixId != UnusedMixId) if (destinationMixId != UnusedMixId)
{ {

View file

@ -1,4 +1,5 @@
using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter; using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Utils; using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common; using Ryujinx.Common;
@ -15,15 +16,35 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public class SplitterContext public class SplitterContext
{ {
/// <summary>
/// Amount of biquad filter states per splitter destination.
/// </summary>
public const int BqfStatesPerDestination = 4;
/// <summary> /// <summary>
/// Storage for <see cref="SplitterState"/>. /// Storage for <see cref="SplitterState"/>.
/// </summary> /// </summary>
private Memory<SplitterState> _splitters; private Memory<SplitterState> _splitters;
/// <summary> /// <summary>
/// Storage for <see cref="SplitterDestination"/>. /// Storage for <see cref="SplitterDestinationVersion1"/>.
/// </summary> /// </summary>
private Memory<SplitterDestination> _splitterDestinations; private Memory<SplitterDestinationVersion1> _splitterDestinationsV1;
/// <summary>
/// Storage for <see cref="SplitterDestinationVersion2"/>.
/// </summary>
private Memory<SplitterDestinationVersion2> _splitterDestinationsV2;
/// <summary>
/// Splitter biquad filtering states.
/// </summary>
private Memory<BiquadFilterState> _splitterBqfStates;
/// <summary>
/// Version of the splitter context that is being used, currently can be 1 or 2.
/// </summary>
public int Version { get; private set; }
/// <summary> /// <summary>
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>. /// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
@ -36,12 +57,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <param name="behaviourContext">The behaviour context.</param> /// <param name="behaviourContext">The behaviour context.</param>
/// <param name="parameter">The audio renderer configuration.</param> /// <param name="parameter">The audio renderer configuration.</param>
/// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param> /// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param>
/// <param name="splitterBqfStates">Memory to store the biquad filtering state for splitters during processing.</param>
/// <returns>Return true if the initialization was successful.</returns> /// <returns>Return true if the initialization was successful.</returns>
public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator) public bool Initialize(
ref BehaviourContext behaviourContext,
ref AudioRendererConfiguration parameter,
WorkBufferAllocator workBufferAllocator,
Memory<BiquadFilterState> splitterBqfStates)
{ {
if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0) if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0)
{ {
Setup(Memory<SplitterState>.Empty, Memory<SplitterDestination>.Empty, false); Setup(Memory<SplitterState>.Empty, Memory<SplitterDestinationVersion1>.Empty, Memory<SplitterDestinationVersion2>.Empty, false);
return true; return true;
} }
@ -60,23 +86,62 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
splitter = new SplitterState(splitterId++); splitter = new SplitterState(splitterId++);
} }
Memory<SplitterDestination> splitterDestinations = workBufferAllocator.Allocate<SplitterDestination>(parameter.SplitterDestinationCount, Memory<SplitterDestinationVersion1> splitterDestinationsV1 = Memory<SplitterDestinationVersion1>.Empty;
SplitterDestination.Alignment); Memory<SplitterDestinationVersion2> splitterDestinationsV2 = Memory<SplitterDestinationVersion2>.Empty;
if (splitterDestinations.IsEmpty) if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
Version = 1;
splitterDestinationsV1 = workBufferAllocator.Allocate<SplitterDestinationVersion1>(parameter.SplitterDestinationCount,
SplitterDestinationVersion1.Alignment);
if (splitterDestinationsV1.IsEmpty)
{ {
return false; return false;
} }
int splitterDestinationId = 0; int splitterDestinationId = 0;
foreach (ref SplitterDestination data in splitterDestinations.Span) foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span)
{ {
data = new SplitterDestination(splitterDestinationId++); data = new SplitterDestinationVersion1(splitterDestinationId++);
}
}
else
{
Version = 2;
splitterDestinationsV2 = workBufferAllocator.Allocate<SplitterDestinationVersion2>(parameter.SplitterDestinationCount,
SplitterDestinationVersion2.Alignment);
if (splitterDestinationsV2.IsEmpty)
{
return false;
}
int splitterDestinationId = 0;
foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span)
{
data = new SplitterDestinationVersion2(splitterDestinationId++);
}
if (parameter.SplitterDestinationCount > 0)
{
// Official code stores it in the SplitterDestinationVersion2 struct,
// but we don't to avoid using unsafe code.
splitterBqfStates.Span.Clear();
_splitterBqfStates = splitterBqfStates;
}
else
{
_splitterBqfStates = Memory<BiquadFilterState>.Empty;
}
} }
SplitterState.InitializeSplitters(splitters.Span); SplitterState.InitializeSplitters(splitters.Span);
Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed()); Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed());
return true; return true;
} }
@ -93,7 +158,15 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (behaviourContext.IsSplitterSupported()) if (behaviourContext.IsSplitterSupported())
{ {
size = WorkBufferAllocator.GetTargetSize<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment); size = WorkBufferAllocator.GetTargetSize<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment);
size = WorkBufferAllocator.GetTargetSize<SplitterDestination>(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment);
if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion2>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment);
}
else
{
size = WorkBufferAllocator.GetTargetSize<SplitterDestinationVersion1>(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment);
}
if (behaviourContext.IsSplitterBugFixed()) if (behaviourContext.IsSplitterBugFixed())
{ {
@ -110,12 +183,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Setup the <see cref="SplitterContext"/> instance. /// Setup the <see cref="SplitterContext"/> instance.
/// </summary> /// </summary>
/// <param name="splitters">The <see cref="SplitterState"/> storage.</param> /// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
/// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param> /// <param name="splitterDestinationsV1">The <see cref="SplitterDestinationVersion1"/> storage.</param>
/// <param name="splitterDestinationsV2">The <see cref="SplitterDestinationVersion2"/> storage.</param>
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param> /// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed) private void Setup(
Memory<SplitterState> splitters,
Memory<SplitterDestinationVersion1> splitterDestinationsV1,
Memory<SplitterDestinationVersion2> splitterDestinationsV2,
bool isBugFixed)
{ {
_splitters = splitters; _splitters = splitters;
_splitterDestinations = splitterDestinations; _splitterDestinationsV1 = splitterDestinationsV1;
_splitterDestinationsV2 = splitterDestinationsV2;
IsBugFixed = isBugFixed; IsBugFixed = isBugFixed;
} }
@ -141,7 +220,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
return 0; return 0;
} }
return _splitterDestinations.Length / _splitters.Length; int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
return length / _splitters.Length;
} }
/// <summary> /// <summary>
@ -178,7 +259,39 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
} }
/// <summary> /// <summary>
/// Update one or multiple <see cref="SplitterDestination"/> from user parameters. /// Update one splitter destination data from user parameters.
/// </summary>
/// <param name="input">The raw data after the splitter header.</param>
/// <returns>True if the update was successful, false otherwise</returns>
private bool UpdateData<T>(ref SequenceReader<byte> input) where T : unmanaged, ISplitterDestinationInParameter
{
ref readonly T parameter = ref input.GetRefOrRefToCopy<T>(out _);
Debug.Assert(parameter.IsMagicValid());
if (parameter.IsMagicValid())
{
int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
if (parameter.Id >= 0 && parameter.Id < length)
{
SplitterDestination destination = GetDestination(parameter.Id);
destination.Update(parameter);
}
return true;
}
else
{
input.Rewind(Unsafe.SizeOf<T>());
return false;
}
}
/// <summary>
/// Update one or multiple splitter destination data from user parameters.
/// </summary> /// </summary>
/// <param name="inputHeader">The splitter header.</param> /// <param name="inputHeader">The splitter header.</param>
/// <param name="input">The raw data after the splitter header.</param> /// <param name="input">The raw data after the splitter header.</param>
@ -186,23 +299,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++) for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{ {
ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy<SplitterDestinationInParameter>(out _); if (Version == 1)
Debug.Assert(parameter.IsMagicValid());
if (parameter.IsMagicValid())
{ {
if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length) if (!UpdateData<SplitterDestinationInParameterVersion1>(ref input))
{ {
ref SplitterDestination destination = ref GetDestination(parameter.Id); break;
}
destination.Update(parameter); }
else if (Version == 2)
{
if (!UpdateData<SplitterDestinationInParameterVersion2>(ref input))
{
break;
} }
} }
else else
{ {
input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>()); Debug.Fail($"Invalid splitter context version {Version}.");
break;
} }
} }
} }
@ -214,7 +327,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <returns>Return true if the update was successful.</returns> /// <returns>Return true if the update was successful.</returns>
public bool Update(ref SequenceReader<byte> input) public bool Update(ref SequenceReader<byte> input)
{ {
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty) if (!UsingSplitter())
{ {
return true; return true;
} }
@ -251,45 +364,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
} }
/// <summary> /// <summary>
/// Get a reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>. /// Get a reference to the splitter destination data at the given <paramref name="id"/>.
/// </summary> /// </summary>
/// <param name="id">The index to use.</param> /// <param name="id">The index to use.</param>
/// <returns>A reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.</returns> /// <returns>A reference to the splitter destination data at the given <paramref name="id"/>.</returns>
public ref SplitterDestination GetDestination(int id) public SplitterDestination GetDestination(int id)
{ {
return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); if (_splitterDestinationsV2.IsEmpty)
{
return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length));
}
else
{
return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length));
}
} }
/// <summary> /// <summary>
/// Get a <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>. /// Get a <see cref="SplitterDestination"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
/// </summary>
/// <param name="id">The index to use.</param>
/// <returns>A <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.</returns>
public Memory<SplitterDestination> GetDestinationMemory(int id)
{
return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
}
/// <summary>
/// Get a <see cref="Span{SplitterDestination}"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>.
/// </summary> /// </summary>
/// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param> /// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param>
/// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param> /// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param>
/// <returns>A <see cref="Span{SplitterDestination}"/>.</returns> /// <returns>A <see cref="SplitterDestination"/>.</returns>
public Span<SplitterDestination> GetDestination(int id, int destinationId) public SplitterDestination GetDestination(int id, int destinationId)
{ {
ref SplitterState splitter = ref GetState(id); ref SplitterState splitter = ref GetState(id);
return splitter.GetData(destinationId); return splitter.GetData(destinationId);
} }
/// <summary>
/// Gets the biquad filter state for a given splitter destination.
/// </summary>
/// <param name="destination">The splitter destination.</param>
/// <returns>Biquad filter state for the specified destination.</returns>
public Memory<BiquadFilterState> GetBiquadFilterState(SplitterDestination destination)
{
return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination);
}
/// <summary> /// <summary>
/// Return true if the audio renderer has any splitters. /// Return true if the audio renderer has any splitters.
/// </summary> /// </summary>
/// <returns>True if the audio renderer has any splitters.</returns> /// <returns>True if the audio renderer has any splitters.</returns>
public bool UsingSplitter() public bool UsingSplitter()
{ {
return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty; return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty);
} }
/// <summary> /// <summary>

View file

@ -1,115 +1,198 @@
using Ryujinx.Audio.Renderer.Parameter; using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Utilities;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
/// <summary> /// <summary>
/// Server state for a splitter destination. /// Server state for a splitter destination.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)] public ref struct SplitterDestination
public struct SplitterDestination
{ {
public const int Alignment = 0x10; private ref SplitterDestinationVersion1 _v1;
private ref SplitterDestinationVersion2 _v2;
/// <summary> /// <summary>
/// The unique id of this <see cref="SplitterDestination"/>. /// Checks if the splitter destination data reference is null.
/// </summary> /// </summary>
public int Id; public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2);
/// <summary>
/// The splitter unique id.
/// </summary>
public int Id
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return 0;
}
else
{
return _v1.Id;
}
}
else
{
return _v2.Id;
}
}
}
/// <summary> /// <summary>
/// The mix to output the result of the splitter. /// The mix to output the result of the splitter.
/// </summary> /// </summary>
public int DestinationId; public int DestinationId
{
/// <summary> get
/// Mix buffer volumes storage. {
/// </summary> if (Unsafe.IsNullRef(ref _v2))
private MixArray _mix; {
private MixArray _previousMix; if (Unsafe.IsNullRef(ref _v1))
{
/// <summary> return 0;
/// Pointer to the next linked element. }
/// </summary> else
private unsafe SplitterDestination* _next; {
return _v1.DestinationId;
/// <summary> }
/// Set to true if in use. }
/// </summary> else
[MarshalAs(UnmanagedType.I1)] {
public bool IsUsed; return _v2.DestinationId;
}
/// <summary> }
/// Set to true if the internal state need to be updated. }
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary> /// <summary>
/// Mix buffer volumes. /// Mix buffer volumes.
/// </summary> /// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks> /// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix); public Span<float> MixBufferVolume
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return Span<float>.Empty;
}
else
{
return _v1.MixBufferVolume;
}
}
else
{
return _v2.MixBufferVolume;
}
}
}
/// <summary> /// <summary>
/// Previous mix buffer volumes. /// Previous mix buffer volumes.
/// </summary> /// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks> /// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix); public Span<float> PreviousMixBufferVolume
{
get
{
if (Unsafe.IsNullRef(ref _v2))
{
if (Unsafe.IsNullRef(ref _v1))
{
return Span<float>.Empty;
}
else
{
return _v1.PreviousMixBufferVolume;
}
}
else
{
return _v2.PreviousMixBufferVolume;
}
}
}
/// <summary> /// <summary>
/// Get the <see cref="Span{SplitterDestination}"/> of the next element or <see cref="Span{SplitterDestination}.Empty"/> if not present. /// Get the <see cref="SplitterDestination"/> of the next element or null if not present.
/// </summary> /// </summary>
public readonly Span<SplitterDestination> Next public readonly SplitterDestination Next
{ {
get get
{ {
unsafe unsafe
{ {
return _next != null ? new Span<SplitterDestination>(_next, 1) : Span<SplitterDestination>.Empty; if (Unsafe.IsNullRef(ref _v2))
}
}
}
/// <summary>
/// Create a new <see cref="SplitterDestination"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestination"/>.</param>
public SplitterDestination(int id) : this()
{ {
Id = id; if (Unsafe.IsNullRef(ref _v1))
DestinationId = Constants.UnusedMixId; {
return new SplitterDestination();
ClearVolumes(); }
else
{
return new SplitterDestination(ref _v1.Next);
}
}
else
{
return new SplitterDestination(ref _v2.Next);
}
}
}
} }
/// <summary> /// <summary>
/// Update the <see cref="SplitterDestination"/> from user parameter. /// Creates a new splitter destination wrapper for the version 1 splitter destination data.
/// </summary>
/// <param name="v1">Version 1 splitter destination data</param>
public SplitterDestination(ref SplitterDestinationVersion1 v1)
{
_v1 = ref v1;
_v2 = ref Unsafe.NullRef<SplitterDestinationVersion2>();
}
/// <summary>
/// Creates a new splitter destination wrapper for the version 2 splitter destination data.
/// </summary>
/// <param name="v2">Version 2 splitter destination data</param>
public SplitterDestination(ref SplitterDestinationVersion2 v2)
{
_v1 = ref Unsafe.NullRef<SplitterDestinationVersion1>();
_v2 = ref v2;
}
/// <summary>
/// Creates a new splitter destination wrapper for the splitter destination data.
/// </summary>
/// <param name="v1">Version 1 splitter destination data</param>
/// <param name="v2">Version 2 splitter destination data</param>
public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2)
{
_v1 = ref Unsafe.AsRef<SplitterDestinationVersion1>(v1);
_v2 = ref Unsafe.AsRef<SplitterDestinationVersion2>(v2);
}
/// <summary>
/// Update the splitter destination data from user parameter.
/// </summary> /// </summary>
/// <param name="parameter">The user parameter.</param> /// <param name="parameter">The user parameter.</param>
public void Update(SplitterDestinationInParameter parameter) public void Update<T>(in T parameter) where T : ISplitterDestinationInParameter
{ {
Debug.Assert(Id == parameter.Id); if (Unsafe.IsNullRef(ref _v2))
if (parameter.IsMagicValid() && Id == parameter.Id)
{ {
DestinationId = parameter.DestinationId; _v1.Update(parameter);
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
} }
else
IsUsed = parameter.IsUsed; {
_v2.Update(parameter);
} }
} }
@ -118,12 +201,14 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public void UpdateInternalState() public void UpdateInternalState()
{ {
if (IsUsed && NeedToUpdateInternalState) if (Unsafe.IsNullRef(ref _v2))
{ {
MixBufferVolume.CopyTo(PreviousMixBufferVolume); _v1.UpdateInternalState();
}
else
{
_v2.UpdateInternalState();
} }
NeedToUpdateInternalState = false;
} }
/// <summary> /// <summary>
@ -131,16 +216,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public void MarkAsNeedToUpdateInternalState() public void MarkAsNeedToUpdateInternalState()
{ {
NeedToUpdateInternalState = true; if (Unsafe.IsNullRef(ref _v2))
{
_v1.MarkAsNeedToUpdateInternalState();
}
else
{
_v2.MarkAsNeedToUpdateInternalState();
}
} }
/// <summary> /// <summary>
/// Return true if the <see cref="SplitterDestination"/> is used and has a destination. /// Return true if the splitter destination is used and has a destination.
/// </summary> /// </summary>
/// <returns>True if the <see cref="SplitterDestination"/> is used and has a destination.</returns> /// <returns>True if the splitter destination is used and has a destination.</returns>
public readonly bool IsConfigured() public readonly bool IsConfigured()
{ {
return IsUsed && DestinationId != Constants.UnusedMixId; return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured();
} }
/// <summary> /// <summary>
@ -150,9 +242,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <returns>The volume for the given destination.</returns> /// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex) public float GetMixVolume(int destinationIndex)
{ {
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolume(destinationIndex) : _v2.GetMixVolume(destinationIndex);
}
return MixBufferVolume[destinationIndex]; /// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex);
} }
/// <summary> /// <summary>
@ -160,22 +260,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public void ClearVolumes() public void ClearVolumes()
{ {
MixBufferVolume.Clear(); if (Unsafe.IsNullRef(ref _v2))
PreviousMixBufferVolume.Clear(); {
_v1.ClearVolumes();
}
else
{
_v2.ClearVolumes();
}
} }
/// <summary> /// <summary>
/// Link the next element to the given <see cref="SplitterDestination"/>. /// Link the next element to the given splitter destination.
/// </summary> /// </summary>
/// <param name="next">The given <see cref="SplitterDestination"/> to link.</param> /// <param name="next">The given splitter destination to link.</param>
public void Link(ref SplitterDestination next) public void Link(SplitterDestination next)
{ {
unsafe if (Unsafe.IsNullRef(ref _v2))
{ {
fixed (SplitterDestination* nextPtr = &next) Debug.Assert(!Unsafe.IsNullRef(ref next._v1));
{
_next = nextPtr; _v1.Link(ref next._v1);
} }
else
{
Debug.Assert(!Unsafe.IsNullRef(ref next._v2));
_v2.Link(ref next._v2);
} }
} }
@ -184,10 +295,74 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public void Unlink() public void Unlink()
{ {
unsafe if (Unsafe.IsNullRef(ref _v2))
{ {
_next = null; _v1.Unlink();
} }
else
{
_v2.Unlink();
}
}
/// <summary>
/// Checks if any biquad filter is enabled.
/// </summary>
/// <returns>True if any biquad filter is enabled.</returns>
public bool IsBiquadFilterEnabled()
{
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled();
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <returns>True if any biquad filter was previously enabled.</returns>
public bool IsBiquadFilterEnabledPrev()
{
return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev();
}
/// <summary>
/// Gets the biquad filter parameters.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
/// <returns>Biquad filter parameters.</returns>
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
{
Debug.Assert(!Unsafe.IsNullRef(ref _v2));
return ref _v2.GetBiquadFilterParameter(index);
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
public void UpdateBiquadFilterEnabledPrev(int index)
{
if (!Unsafe.IsNullRef(ref _v2))
{
_v2.UpdateBiquadFilterEnabledPrev(index);
}
}
/// <summary>
/// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null.
/// </summary>
/// <returns>Reference for the version 1 splitter destination data.</returns>
public ref SplitterDestinationVersion1 GetV1RefOrNull()
{
return ref _v1;
}
/// <summary>
/// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null.
/// </summary>
/// <returns>Reference for the version 2 splitter destination data.</returns>
public ref SplitterDestinationVersion2 GetV2RefOrNull()
{
return ref _v2;
} }
} }
} }

View file

@ -0,0 +1,206 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
/// <summary>
/// Server state for a splitter destination (version 1).
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
public struct SplitterDestinationVersion1
{
public const int Alignment = 0x10;
/// <summary>
/// The unique id of this <see cref="SplitterDestinationVersion1"/>.
/// </summary>
public int Id;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mix;
private MixArray _previousMix;
/// <summary>
/// Pointer to the next linked element.
/// </summary>
private unsafe SplitterDestinationVersion1* _next;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Set to true if the internal state need to be updated.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
/// <summary>
/// Previous mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
/// <summary>
/// Get the reference of the next element or null if not present.
/// </summary>
public readonly ref SplitterDestinationVersion1 Next
{
get
{
unsafe
{
return ref Unsafe.AsRef<SplitterDestinationVersion1>(_next);
}
}
}
/// <summary>
/// Create a new <see cref="SplitterDestinationVersion1"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion1"/>.</param>
public SplitterDestinationVersion1(int id) : this()
{
Id = id;
DestinationId = Constants.UnusedMixId;
ClearVolumes();
}
/// <summary>
/// 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
{
Debug.Assert(Id == parameter.Id);
if (parameter.IsMagicValid() && Id == parameter.Id)
{
DestinationId = parameter.DestinationId;
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
}
IsUsed = parameter.IsUsed;
}
}
/// <summary>
/// Update the internal state of the instance.
/// </summary>
public void UpdateInternalState()
{
if (IsUsed && NeedToUpdateInternalState)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
}
NeedToUpdateInternalState = false;
}
/// <summary>
/// Set the update internal state marker.
/// </summary>
public void MarkAsNeedToUpdateInternalState()
{
NeedToUpdateInternalState = true;
}
/// <summary>
/// Return true if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.
/// </summary>
/// <returns>True if the <see cref="SplitterDestinationVersion1"/> is used and has a destination.</returns>
public readonly bool IsConfigured()
{
return IsUsed && DestinationId != Constants.UnusedMixId;
}
/// <summary>
/// Get the volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return MixBufferVolume[destinationIndex];
}
/// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return PreviousMixBufferVolume[destinationIndex];
}
/// <summary>
/// Clear the volumes.
/// </summary>
public void ClearVolumes()
{
MixBufferVolume.Clear();
PreviousMixBufferVolume.Clear();
}
/// <summary>
/// Link the next element to the given <see cref="SplitterDestinationVersion1"/>.
/// </summary>
/// <param name="next">The given <see cref="SplitterDestinationVersion1"/> to link.</param>
public void Link(ref SplitterDestinationVersion1 next)
{
unsafe
{
fixed (SplitterDestinationVersion1* nextPtr = &next)
{
_next = nextPtr;
}
}
}
/// <summary>
/// Remove the link to the next element.
/// </summary>
public void Unlink()
{
unsafe
{
_next = null;
}
}
}
}

View file

@ -0,0 +1,250 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
/// <summary>
/// Server state for a splitter destination (version 2).
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)]
public struct SplitterDestinationVersion2
{
public const int Alignment = 0x10;
/// <summary>
/// The unique id of this <see cref="SplitterDestinationVersion2"/>.
/// </summary>
public int Id;
/// <summary>
/// The mix to output the result of the splitter.
/// </summary>
public int DestinationId;
/// <summary>
/// Mix buffer volumes storage.
/// </summary>
private MixArray _mix;
private MixArray _previousMix;
/// <summary>
/// Pointer to the next linked element.
/// </summary>
private unsafe SplitterDestinationVersion2* _next;
/// <summary>
/// Set to true if in use.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool IsUsed;
/// <summary>
/// Set to true if the internal state need to be updated.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool NeedToUpdateInternalState;
[StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
/// <summary>
/// Mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix);
/// <summary>
/// Previous mix buffer volumes.
/// </summary>
/// <remarks>Used when a splitter id is specified in the mix.</remarks>
public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix);
/// <summary>
/// Get the reference of the next element or null if not present.
/// </summary>
public readonly ref SplitterDestinationVersion2 Next
{
get
{
unsafe
{
return ref Unsafe.AsRef<SplitterDestinationVersion2>(_next);
}
}
}
private Array2<BiquadFilterParameter> _biquadFilters;
private Array2<bool> _isPreviousBiquadFilterEnabled;
/// <summary>
/// Create a new <see cref="SplitterDestinationVersion2"/>.
/// </summary>
/// <param name="id">The unique id of this <see cref="SplitterDestinationVersion2"/>.</param>
public SplitterDestinationVersion2(int id) : this()
{
Id = id;
DestinationId = Constants.UnusedMixId;
ClearVolumes();
}
/// <summary>
/// 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
{
Debug.Assert(Id == parameter.Id);
if (parameter.IsMagicValid() && Id == parameter.Id)
{
DestinationId = parameter.DestinationId;
parameter.MixBufferVolume.CopyTo(MixBufferVolume);
_biquadFilters = parameter.BiquadFilters;
if (!IsUsed && parameter.IsUsed)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
NeedToUpdateInternalState = false;
}
IsUsed = parameter.IsUsed;
}
}
/// <summary>
/// Update the internal state of the instance.
/// </summary>
public void UpdateInternalState()
{
if (IsUsed && NeedToUpdateInternalState)
{
MixBufferVolume.CopyTo(PreviousMixBufferVolume);
}
NeedToUpdateInternalState = false;
}
/// <summary>
/// Set the update internal state marker.
/// </summary>
public void MarkAsNeedToUpdateInternalState()
{
NeedToUpdateInternalState = true;
}
/// <summary>
/// Return true if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.
/// </summary>
/// <returns>True if the <see cref="SplitterDestinationVersion2"/> is used and has a destination.</returns>
public readonly bool IsConfigured()
{
return IsUsed && DestinationId != Constants.UnusedMixId;
}
/// <summary>
/// Get the volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolume(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return MixBufferVolume[destinationIndex];
}
/// <summary>
/// Get the previous volume for a given destination.
/// </summary>
/// <param name="destinationIndex">The destination index to use.</param>
/// <returns>The volume for the given destination.</returns>
public float GetMixVolumePrev(int destinationIndex)
{
Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
return PreviousMixBufferVolume[destinationIndex];
}
/// <summary>
/// Clear the volumes.
/// </summary>
public void ClearVolumes()
{
MixBufferVolume.Clear();
PreviousMixBufferVolume.Clear();
}
/// <summary>
/// Link the next element to the given <see cref="SplitterDestinationVersion2"/>.
/// </summary>
/// <param name="next">The given <see cref="SplitterDestinationVersion2"/> to link.</param>
public void Link(ref SplitterDestinationVersion2 next)
{
unsafe
{
fixed (SplitterDestinationVersion2* nextPtr = &next)
{
_next = nextPtr;
}
}
}
/// <summary>
/// Remove the link to the next element.
/// </summary>
public void Unlink()
{
unsafe
{
_next = null;
}
}
/// <summary>
/// Checks if any biquad filter is enabled.
/// </summary>
/// <returns>True if any biquad filter is enabled.</returns>
public bool IsBiquadFilterEnabled()
{
return _biquadFilters[0].Enable || _biquadFilters[1].Enable;
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <returns>True if any biquad filter was previously enabled.</returns>
public bool IsBiquadFilterEnabledPrev()
{
return _isPreviousBiquadFilterEnabled[0];
}
/// <summary>
/// Gets the biquad filter parameters.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
/// <returns>Biquad filter parameters.</returns>
public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
{
return ref _biquadFilters[index];
}
/// <summary>
/// Checks if any biquad filter was previously enabled.
/// </summary>
/// <param name="index">Biquad filter index (0 or 1).</param>
public void UpdateBiquadFilterEnabledPrev(int index)
{
_isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable;
}
}
}

View file

@ -15,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
public const int Alignment = 0x10; public const int Alignment = 0x10;
private delegate void SplitterDestinationAction(SplitterDestination destination, int index);
/// <summary> /// <summary>
/// The unique id of this <see cref="SplitterState"/>. /// The unique id of this <see cref="SplitterState"/>.
/// </summary> /// </summary>
@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public uint SampleRate; public uint SampleRate;
/// <summary> /// <summary>
/// Count of splitter destinations (<see cref="SplitterDestination"/>). /// Count of splitter destinations.
/// </summary> /// </summary>
public int DestinationCount; public int DestinationCount;
@ -37,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public bool HasNewConnection; public bool HasNewConnection;
/// <summary> /// <summary>
/// Linked list of <see cref="SplitterDestination"/>. /// Linked list of <see cref="SplitterDestinationVersion1"/>.
/// </summary> /// </summary>
private unsafe SplitterDestination* _destinationsData; private unsafe SplitterDestinationVersion1* _destinationDataV1;
/// <summary> /// <summary>
/// Span to the first element of the linked list of <see cref="SplitterDestination"/>. /// Linked list of <see cref="SplitterDestinationVersion2"/>.
/// </summary> /// </summary>
public readonly Span<SplitterDestination> Destinations private unsafe SplitterDestinationVersion2* _destinationDataV2;
/// <summary>
/// First element of the linked list of splitter destinations data.
/// </summary>
public readonly SplitterDestination Destination
{ {
get get
{ {
unsafe unsafe
{ {
return (IntPtr)_destinationsData != IntPtr.Zero ? new Span<SplitterDestination>(_destinationsData, 1) : Span<SplitterDestination>.Empty; return new SplitterDestination(_destinationDataV1, _destinationDataV2);
} }
} }
} }
@ -64,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
Id = id; Id = id;
} }
public readonly Span<SplitterDestination> GetData(int index) public readonly SplitterDestination GetData(int index)
{ {
int i = 0; int i = 0;
Span<SplitterDestination> result = Destinations; SplitterDestination result = Destination;
while (i < index) while (i < index)
{ {
if (result.IsEmpty) if (result.IsNull)
{ {
break; break;
} }
result = result[0].Next; result = result.Next;
i++; i++;
} }
@ -93,25 +100,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
} }
/// <summary> /// <summary>
/// Utility function to apply a given <see cref="SpanAction{T, TArg}"/> to all <see cref="Destinations"/>. /// Utility function to apply an action to all <see cref="Destination"/>.
/// </summary> /// </summary>
/// <param name="action">The action to execute on each elements.</param> /// <param name="action">The action to execute on each elements.</param>
private readonly void ForEachDestination(SpanAction<SplitterDestination, int> action) private readonly void ForEachDestination(SplitterDestinationAction action)
{ {
Span<SplitterDestination> temp = Destinations; SplitterDestination temp = Destination;
int i = 0; int i = 0;
while (true) while (true)
{ {
if (temp.IsEmpty) if (temp.IsNull)
{ {
break; break;
} }
Span<SplitterDestination> next = temp[0].Next; SplitterDestination next = temp.Next;
action.Invoke(temp, i++); action(temp, i++);
temp = next; temp = next;
} }
@ -142,9 +149,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
input.ReadLittleEndian(out int destinationId); input.ReadLittleEndian(out int destinationId);
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationId); SplitterDestination destination = context.GetDestination(destinationId);
SetDestination(ref destination.Span[0]); SetDestination(destination);
DestinationCount = destinationCount; DestinationCount = destinationCount;
@ -152,9 +159,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
input.ReadLittleEndian(out destinationId); input.ReadLittleEndian(out destinationId);
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationId); SplitterDestination nextDestination = context.GetDestination(destinationId);
destination.Span[0].Link(ref nextDestination.Span[0]); destination.Link(nextDestination);
destination = nextDestination; destination = nextDestination;
} }
} }
@ -174,16 +181,21 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
} }
/// <summary> /// <summary>
/// Set the head of the linked list of <see cref="Destinations"/>. /// Set the head of the linked list of <see cref="Destination"/>.
/// </summary> /// </summary>
/// <param name="newValue">A reference to a <see cref="SplitterDestination"/>.</param> /// <param name="newValue">New destination value.</param>
public void SetDestination(ref SplitterDestination newValue) public void SetDestination(SplitterDestination newValue)
{ {
unsafe unsafe
{ {
fixed (SplitterDestination* newValuePtr = &newValue) fixed (SplitterDestinationVersion1* newValuePtr = &newValue.GetV1RefOrNull())
{ {
_destinationsData = newValuePtr; _destinationDataV1 = newValuePtr;
}
fixed (SplitterDestinationVersion2* newValuePtr = &newValue.GetV2RefOrNull())
{
_destinationDataV2 = newValuePtr;
} }
} }
} }
@ -193,19 +205,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary> /// </summary>
public readonly void UpdateInternalState() public readonly void UpdateInternalState()
{ {
ForEachDestination((destination, _) => destination[0].UpdateInternalState()); ForEachDestination((destination, _) => destination.UpdateInternalState());
} }
/// <summary> /// <summary>
/// Clear all links from the <see cref="Destinations"/>. /// Clear all links from the <see cref="Destination"/>.
/// </summary> /// </summary>
public void ClearLinks() public void ClearLinks()
{ {
ForEachDestination((destination, _) => destination[0].Unlink()); ForEachDestination((destination, _) => destination.Unlink());
unsafe unsafe
{ {
_destinationsData = (SplitterDestination*)IntPtr.Zero; _destinationDataV1 = null;
_destinationDataV2 = null;
} }
} }
@ -219,7 +232,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{ {
unsafe unsafe
{ {
splitter._destinationsData = (SplitterDestination*)IntPtr.Zero; splitter._destinationDataV1 = null;
splitter._destinationDataV2 = null;
} }
splitter.DestinationCount = 0; splitter.DestinationCount = 0;

View file

@ -52,7 +52,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -78,7 +80,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -104,7 +108,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -130,7 +136,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -156,7 +164,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -182,7 +192,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -208,7 +220,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -234,7 +248,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -260,7 +276,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@ -286,11 +304,69 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()); Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported()); Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported()); Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
Assert.IsTrue(behaviourContext.IsBiquadFilterGroupedOptimizationSupported()); Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit()); Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion()); Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat()); Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
} }
[Test]
public void TestRevision11()
{
BehaviourContext behaviourContext = new();
behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision11);
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.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
}
[Test]
public void TestRevision12()
{
BehaviourContext behaviourContext = new();
behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision12);
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.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
}
} }
} }

View file

@ -9,7 +9,8 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
[Test] [Test]
public void EnsureTypeSize() public void EnsureTypeSize()
{ {
Assert.AreEqual(0xE0, Unsafe.SizeOf<SplitterDestination>()); Assert.AreEqual(0xE0, Unsafe.SizeOf<SplitterDestinationVersion1>());
Assert.AreEqual(0x110, Unsafe.SizeOf<SplitterDestinationVersion2>());
} }
} }
} }