From a389dd59bd881cf2cff09a1f67f5c30de61123e6 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 18 Aug 2020 03:49:37 +0200 Subject: Amadeus: Final Act (#1481) * Amadeus: Final Act This is my requiem, I present to you Amadeus, a complete reimplementation of the Audio Renderer! This reimplementation is based on my reversing of every version of the audio system module that I carried for the past 10 months. This supports every revision (at the time of writing REV1 to REV8 included) and all features proposed by the Audio Renderer on real hardware. Because this component could be used outside an emulation context, and to avoid possible "inspirations" not crediting the project, I decided to license the Ryujinx.Audio.Renderer project under LGPLv3. - FE3H voices in videos and chapter intro are not present. - Games that use two audio renderer **at the same time** are probably going to have issues right now **until we rewrite the audio output interface** (Crash Team Racing is the only known game to use two renderer at the same time). - Persona 5 Scrambler now goes ingame but audio is garbage. This is caused by the fact that the game engine is syncing audio and video in a really aggressive way. This will disappears the day this game run at full speed. * Make timing more precise when sleeping on Windows Improve precision to a 1ms resolution on Windows NT based OS. This is used to avoid having totally erratic timings and unify all Windows users to the same resolution. NOTE: This is only active when emulation is running. --- .../Dsp/Command/AdpcmDataSourceCommandVersion1.cs | 93 +++++++ .../Dsp/Command/AuxiliaryBufferCommand.cs | 188 ++++++++++++++ .../Dsp/Command/BiquadFilterCommand.cs | 89 +++++++ .../Dsp/Command/CircularBufferSinkCommand.cs | 91 +++++++ .../Dsp/Command/ClearMixBufferCommand.cs | 41 +++ Ryujinx.Audio.Renderer/Dsp/Command/CommandList.cs | 124 +++++++++ Ryujinx.Audio.Renderer/Dsp/Command/CommandType.cs | 49 ++++ .../Dsp/Command/CopyMixBufferCommand.cs | 52 ++++ .../Dsp/Command/DataSourceVersion2Command.cs | 126 +++++++++ Ryujinx.Audio.Renderer/Dsp/Command/DelayCommand.cs | 272 ++++++++++++++++++++ .../Dsp/Command/DepopForMixBuffersCommand.cs | 103 ++++++++ .../Dsp/Command/DepopPrepareCommand.cs | 72 ++++++ .../Dsp/Command/DeviceSinkCommand.cs | 108 ++++++++ .../Dsp/Command/DownMixSurroundToStereoCommand.cs | 89 +++++++ Ryujinx.Audio.Renderer/Dsp/Command/ICommand.cs | 37 +++ Ryujinx.Audio.Renderer/Dsp/Command/MixCommand.cs | 125 +++++++++ .../Dsp/Command/MixRampCommand.cs | 83 ++++++ .../Dsp/Command/MixRampGroupedCommand.cs | 106 ++++++++ .../Command/PcmFloatDataSourceCommandVersion1.cs | 92 +++++++ .../Command/PcmInt16DataSourceCommandVersion1.cs | 92 +++++++ .../Dsp/Command/PerformanceCommand.cs | 64 +++++ .../Dsp/Command/Reverb3dCommand.cs | 269 +++++++++++++++++++ .../Dsp/Command/ReverbCommand.cs | 284 +++++++++++++++++++++ .../Dsp/Command/UpsampleCommand.cs | 87 +++++++ .../Dsp/Command/VolumeCommand.cs | 125 +++++++++ .../Dsp/Command/VolumeRampCommand.cs | 71 ++++++ 26 files changed, 2932 insertions(+) create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/AuxiliaryBufferCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/BiquadFilterCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/CircularBufferSinkCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/ClearMixBufferCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/CommandList.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/CommandType.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/CopyMixBufferCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DataSourceVersion2Command.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DelayCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DepopForMixBuffersCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DepopPrepareCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DeviceSinkCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/DownMixSurroundToStereoCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/ICommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/MixCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/MixRampCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/MixRampGroupedCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/PcmFloatDataSourceCommandVersion1.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/PcmInt16DataSourceCommandVersion1.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/PerformanceCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/Reverb3dCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/ReverbCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/UpsampleCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/VolumeCommand.cs create mode 100644 Ryujinx.Audio.Renderer/Dsp/Command/VolumeRampCommand.cs (limited to 'Ryujinx.Audio.Renderer/Dsp/Command') diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs b/Ryujinx.Audio.Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs new file mode 100644 index 00000000..df9ef086 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class AdpcmDataSourceCommandVersion1 : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.AdpcmDataSourceVersion1; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort OutputBufferIndex { get; } + public uint SampleRate { get; } + + public float Pitch { get; } + + public WaveBuffer[] WaveBuffers { get; } + + public Memory State { get; } + + public ulong AdpcmParameter { get; } + public ulong AdpcmParameterSize { get; } + + public DecodingBehaviour DecodingBehaviour { get; } + + public AdpcmDataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory state, ushort outputBufferIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + OutputBufferIndex = outputBufferIndex; + SampleRate = serverState.SampleRate; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; + + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref Server.Voice.WaveBuffer voiceWaveBuffer = ref serverState.WaveBuffers[i]; + + WaveBuffers[i] = voiceWaveBuffer.ToCommon(1); + } + + AdpcmParameter = serverState.DataSourceStateAddressInfo.GetReference(true); + AdpcmParameterSize = serverState.DataSourceStateAddressInfo.Size; + State = state; + DecodingBehaviour = serverState.DecodingBehaviour; + } + + public void Process(CommandList context) + { + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + DataSourceHelper.WaveBufferInformation info = new DataSourceHelper.WaveBufferInformation() + { + State = State, + SourceSampleRate = SampleRate, + SampleFormat = SampleFormat.Adpcm, + Pitch = Pitch, + DecodingBehaviour = DecodingBehaviour, + WaveBuffers = WaveBuffers, + ExtraParameter = AdpcmParameter, + ExtraParameterSize = AdpcmParameterSize, + ChannelIndex = 0, + ChannelCount = 1, + }; + + DataSourceHelper.ProcessWaveBuffers(context.MemoryManager, outputBuffer, info, context.SampleRate, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/AuxiliaryBufferCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/AuxiliaryBufferCommand.cs new file mode 100644 index 00000000..f63722c1 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/AuxiliaryBufferCommand.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Cpu; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Renderer.Dsp.State.AuxiliaryBufferHeader; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class AuxiliaryBufferCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.AuxiliaryBuffer; + + public ulong EstimatedProcessingTime { get; set; } + + public uint InputBufferIndex { get; } + public uint OutputBufferIndex { get; } + + public AuxiliaryBufferAddresses BufferInfo { get; } + + public CpuAddress InputBuffer { get; } + public CpuAddress OutputBuffer { get; } + public uint CountMax { get; } + public uint UpdateCount { get; } + public uint WriteOffset { get; } + + public bool IsEffectEnabled { get; } + + public AuxiliaryBufferCommand(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset, + ref AuxiliaryBufferAddresses sendBufferInfo, bool isEnabled, uint countMax, + CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId) + { + Enabled = true; + NodeId = nodeId; + InputBufferIndex = bufferOffset + inputBufferOffset; + OutputBufferIndex = bufferOffset + outputBufferOffset; + BufferInfo = sendBufferInfo; + InputBuffer = inputBuffer; + OutputBuffer = outputBuffer; + CountMax = countMax; + UpdateCount = updateCount; + WriteOffset = writeOffset; + IsEffectEnabled = isEnabled; + } + + private uint Read(MemoryManager memoryManager, ulong bufferAddress, uint countMax, Span outBuffer, uint count, uint readOffset, uint updateCount) + { + if (countMax == 0 || bufferAddress == 0) + { + return 0; + } + + uint targetReadOffset = readOffset + AuxiliaryBufferInfo.GetReadOffset(memoryManager, BufferInfo.ReturnBufferInfo); + + if (targetReadOffset > countMax) + { + return 0; + } + + uint remaining = count; + + uint outBufferOffset = 0; + + while (remaining != 0) + { + uint countToWrite = Math.Min(countMax - targetReadOffset, remaining); + + memoryManager.Read(bufferAddress + targetReadOffset * sizeof(int), MemoryMarshal.Cast(outBuffer.Slice((int)outBufferOffset, (int)countToWrite))); + + targetReadOffset = (targetReadOffset + countToWrite) % countMax; + remaining -= countToWrite; + outBufferOffset += countToWrite; + } + + if (updateCount != 0) + { + uint newReadOffset = (AuxiliaryBufferInfo.GetReadOffset(memoryManager, BufferInfo.ReturnBufferInfo) + updateCount) % countMax; + + AuxiliaryBufferInfo.SetReadOffset(memoryManager, BufferInfo.ReturnBufferInfo, newReadOffset); + } + + return count; + } + + private uint Write(MemoryManager memoryManager, ulong outBufferAddress, uint countMax, ReadOnlySpan buffer, uint count, uint writeOffset, uint updateCount) + { + if (countMax == 0 || outBufferAddress == 0) + { + return 0; + } + + uint targetWriteOffset = writeOffset + AuxiliaryBufferInfo.GetWriteOffset(memoryManager, BufferInfo.SendBufferInfo); + + if (targetWriteOffset > countMax) + { + return 0; + } + + uint remaining = count; + + uint inBufferOffset = 0; + + while (remaining != 0) + { + uint countToWrite = Math.Min(countMax - targetWriteOffset, remaining); + + memoryManager.Write(outBufferAddress + targetWriteOffset * sizeof(int), MemoryMarshal.Cast(buffer.Slice((int)inBufferOffset, (int)countToWrite))); + + targetWriteOffset = (targetWriteOffset + countToWrite) % countMax; + remaining -= countToWrite; + inBufferOffset += countToWrite; + } + + if (updateCount != 0) + { + uint newWriteOffset = (AuxiliaryBufferInfo.GetWriteOffset(memoryManager, BufferInfo.SendBufferInfo) + updateCount) % countMax; + + AuxiliaryBufferInfo.SetWriteOffset(memoryManager, BufferInfo.SendBufferInfo, newWriteOffset); + } + + return count; + } + + public void Process(CommandList context) + { + Span inputBuffer = context.GetBuffer((int)InputBufferIndex); + Span outputBuffer = context.GetBuffer((int)OutputBufferIndex); + + if (IsEffectEnabled) + { + Span inputBufferInt = MemoryMarshal.Cast(inputBuffer); + Span outputBufferInt = MemoryMarshal.Cast(outputBuffer); + + // Convert input data to the target format for user (int) + DataSourceHelper.ToInt(inputBufferInt, inputBuffer, outputBuffer.Length); + + // Send the input to the user + Write(context.MemoryManager, OutputBuffer, CountMax, inputBufferInt, context.SampleCount, WriteOffset, UpdateCount); + + // Convert back to float just in case it's reused + DataSourceHelper.ToFloat(inputBuffer, inputBufferInt, inputBuffer.Length); + + // Retrieve the input from user + uint readResult = Read(context.MemoryManager, InputBuffer, CountMax, outputBufferInt, context.SampleCount, WriteOffset, UpdateCount); + + // Convert the outputBuffer back to the target format of the renderer (float) + DataSourceHelper.ToFloat(outputBuffer, outputBufferInt, outputBuffer.Length); + + if (readResult != context.SampleCount) + { + outputBuffer.Slice((int)readResult, (int)context.SampleCount - (int)readResult).Fill(0); + } + } + else + { + MemoryHelper.FillWithZeros(context.MemoryManager, (long)BufferInfo.SendBufferInfo, Unsafe.SizeOf()); + MemoryHelper.FillWithZeros(context.MemoryManager, (long)BufferInfo.ReturnBufferInfo, Unsafe.SizeOf()); + + if (InputBufferIndex != OutputBufferIndex) + { + inputBuffer.CopyTo(outputBuffer); + } + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/BiquadFilterCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/BiquadFilterCommand.cs new file mode 100644 index 00000000..d00d3371 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/BiquadFilterCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class BiquadFilterCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.BiquadFilter; + + public ulong EstimatedProcessingTime { get; set; } + + public BiquadFilterParameter Parameter { get; } + public Memory BiquadFilterState { get; } + public int InputBufferIndex { get; } + public int OutputBufferIndex { get; } + public bool NeedInitialization { get; } + + public BiquadFilterCommand(int baseIndex, ref BiquadFilterParameter filter, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, bool needInitialization, int nodeId) + { + Parameter = filter; + BiquadFilterState = biquadFilterStateMemory; + InputBufferIndex = baseIndex + inputBufferOffset; + OutputBufferIndex = baseIndex + outputBufferOffset; + NeedInitialization = needInitialization; + + Enabled = true; + NodeId = nodeId; + } + + private void ProcessBiquadFilter(Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount) + { + const int fixedPointPrecisionForParameter = 14; + + 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); + + ref BiquadFilterState state = ref BiquadFilterState.Span[0]; + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i]; + float output = input * a0 + state.Z1; + + state.Z1 = input * a1 + output * b1 + state.Z2; + state.Z2 = input * a2 + output * b2; + + outputBuffer[i] = output; + } + } + + public void Process(CommandList context) + { + Span outputBuffer = context.GetBuffer(InputBufferIndex); + + if (NeedInitialization) + { + BiquadFilterState.Span[0] = new BiquadFilterState(); + } + + ProcessBiquadFilter(outputBuffer, outputBuffer, context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/CircularBufferSinkCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/CircularBufferSinkCommand.cs new file mode 100644 index 00000000..63719a75 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/CircularBufferSinkCommand.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Parameter.Sink; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class CircularBufferSinkCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.CircularBufferSink; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort[] Input { get; } + public uint InputCount { get; } + + public ulong CircularBuffer { get; } + public ulong CircularBufferSize { get; } + public ulong CurrentOffset { get; } + + public CircularBufferSinkCommand(uint bufferOffset, ref CircularBufferParameter parameter, ref AddressInfo circularBufferAddressInfo, uint currentOffset, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + Input = new ushort[RendererConstants.ChannelCountMax]; + InputCount = parameter.InputCount; + + for (int i = 0; i < InputCount; i++) + { + Input[i] = (ushort)(bufferOffset + parameter.Input[i]); + } + + CircularBuffer = circularBufferAddressInfo.GetReference(true); + CircularBufferSize = parameter.BufferSize; + CurrentOffset = currentOffset; + + Debug.Assert(CircularBuffer != 0); + } + + public void Process(CommandList context) + { + const int targetChannelCount = 2; + + ulong currentOffset = CurrentOffset; + + if (CircularBufferSize > 0) + { + for (int i = 0; i < InputCount; i++) + { + ReadOnlySpan inputBuffer = context.GetBuffer(Input[i]); + + ulong targetOffset = CircularBuffer + currentOffset; + + for (int y = 0; y < context.SampleCount; y++) + { + context.MemoryManager.Write(targetOffset + (ulong)y * targetChannelCount, PcmHelper.Saturate(inputBuffer[y])); + } + + currentOffset += context.SampleCount * targetChannelCount; + + if (currentOffset >= CircularBufferSize) + { + currentOffset = 0; + } + } + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/ClearMixBufferCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/ClearMixBufferCommand.cs new file mode 100644 index 00000000..3ec725d1 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/ClearMixBufferCommand.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class ClearMixBufferCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.ClearMixBuffer; + + public ulong EstimatedProcessingTime { get; set; } + + public ClearMixBufferCommand(int nodeId) + { + Enabled = true; + NodeId = nodeId; + } + + public void Process(CommandList context) + { + context.Buffers.Span.Fill(0); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/CommandList.cs b/Ryujinx.Audio.Renderer/Dsp/Command/CommandList.cs new file mode 100644 index 00000000..d65f6ced --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/CommandList.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Integration; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class CommandList + { + public ulong StartTime { get; private set; } + public ulong EndTime { get; private set; } + public uint SampleCount { get; } + public uint SampleRate { get; } + + public Memory Buffers { get; } + public uint BufferCount { get; } + + public List Commands { get; } + + public MemoryManager MemoryManager { get; } + + public HardwareDevice OutputDevice { get; private set; } + + public CommandList(AudioRenderSystem renderSystem) : this(renderSystem.MemoryManager, + renderSystem.GetMixBuffer(), + renderSystem.GetSampleCount(), + renderSystem.GetSampleRate(), + renderSystem.GetMixBufferCount(), + renderSystem.GetVoiceChannelCountMax()) + { + } + + public CommandList(MemoryManager memoryManager, Memory mixBuffer, uint sampleCount, uint sampleRate, uint mixBufferCount, uint voiceChannelCountMax) + { + SampleCount = sampleCount; + SampleRate = sampleRate; + BufferCount = mixBufferCount + voiceChannelCountMax; + Buffers = mixBuffer; + Commands = new List(); + MemoryManager = memoryManager; + } + + public void AddCommand(ICommand command) + { + Commands.Add(command); + } + + public void AddCommand(T command) where T : unmanaged, ICommand + { + throw new NotImplementedException(); + } + + public Memory GetBufferMemory(int index) + { + return Buffers.Slice(index * (int)SampleCount, (int)SampleCount); + } + + public Span GetBuffer(int index) + { + return Buffers.Span.Slice(index * (int)SampleCount, (int)SampleCount); + } + + public ulong GetTimeElapsedSinceDspStartedProcessing() + { + return (ulong)PerformanceCounter.ElapsedNanoseconds - StartTime; + } + + public void Process(HardwareDevice outputDevice) + { + OutputDevice = outputDevice; + + StartTime = (ulong)PerformanceCounter.ElapsedNanoseconds; + + foreach (ICommand command in Commands) + { + if (command.Enabled) + { + bool shouldMeter = command.ShouldMeter(); + + long startTime = 0; + + if (shouldMeter) + { + startTime = PerformanceCounter.ElapsedNanoseconds; + } + + command.Process(this); + + if (shouldMeter) + { + ulong effectiveElapsedTime = (ulong)(PerformanceCounter.ElapsedNanoseconds - startTime); + + if (effectiveElapsedTime > command.EstimatedProcessingTime) + { + Logger.Warning?.Print(LogClass.AudioRenderer, $"Command {command.GetType().Name} took {effectiveElapsedTime}ns (expected {command.EstimatedProcessingTime}ns)"); + } + } + } + } + + EndTime = (ulong)PerformanceCounter.ElapsedNanoseconds; + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/CommandType.cs b/Ryujinx.Audio.Renderer/Dsp/Command/CommandType.cs new file mode 100644 index 00000000..6532cd15 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/CommandType.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public enum CommandType : byte + { + Invalid, + PcmInt16DataSourceVersion1, + PcmInt16DataSourceVersion2, + PcmFloatDataSourceVersion1, + PcmFloatDataSourceVersion2, + AdpcmDataSourceVersion1, + AdpcmDataSourceVersion2, + Volume, + VolumeRamp, + BiquadFilter, + Mix, + MixRamp, + MixRampGrouped, + DepopPrepare, + DepopForMixBuffers, + Delay, + Upsample, + DownMixSurroundToStereo, + AuxiliaryBuffer, + DeviceSink, + CircularBufferSink, + Reverb, + Reverb3d, + Performance, + ClearMixBuffer, + CopyMixBuffer + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/CopyMixBufferCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/CopyMixBufferCommand.cs new file mode 100644 index 00000000..62b95215 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/CopyMixBufferCommand.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class CopyMixBufferCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.CopyMixBuffer; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public CopyMixBufferCommand(uint inputBufferIndex, uint outputBufferIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)inputBufferIndex; + OutputBufferIndex = (ushort)outputBufferIndex; + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + inputBuffer.CopyTo(outputBuffer); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DataSourceVersion2Command.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DataSourceVersion2Command.cs new file mode 100644 index 00000000..7b68ff5d --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DataSourceVersion2Command.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DataSourceVersion2Command : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType { get; } + + public ulong EstimatedProcessingTime { get; set; } + + public ushort OutputBufferIndex { get; } + public uint SampleRate { get; } + + public float Pitch { get; } + + public WaveBuffer[] WaveBuffers { get; } + + public Memory State { get; } + + public ulong ExtraParameter { get; } + public ulong ExtraParameterSize { get; } + + public uint ChannelIndex { get; } + + public uint ChannelCount { get; } + + public DecodingBehaviour DecodingBehaviour { get; } + + public SampleFormat SampleFormat { get; } + + public SampleRateConversionQuality SrcQuality { get; } + + public DataSourceVersion2Command(ref Server.Voice.VoiceState serverState, Memory state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + ChannelIndex = channelIndex; + ChannelCount = serverState.ChannelsCount; + SampleFormat = serverState.SampleFormat; + SrcQuality = serverState.SrcQuality; + CommandType = GetCommandTypeBySampleFormat(SampleFormat); + + OutputBufferIndex = outputBufferIndex; + SampleRate = serverState.SampleRate; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; + + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref Server.Voice.WaveBuffer voiceWaveBuffer = ref serverState.WaveBuffers[i]; + + WaveBuffers[i] = voiceWaveBuffer.ToCommon(2); + } + + if (SampleFormat == SampleFormat.Adpcm) + { + ExtraParameter = serverState.DataSourceStateAddressInfo.GetReference(true); + ExtraParameterSize = serverState.DataSourceStateAddressInfo.Size; + } + + State = state; + DecodingBehaviour = serverState.DecodingBehaviour; + } + + private static CommandType GetCommandTypeBySampleFormat(SampleFormat sampleFormat) + { + switch (sampleFormat) + { + case SampleFormat.Adpcm: + return CommandType.AdpcmDataSourceVersion2; + case SampleFormat.PcmInt16: + return CommandType.PcmInt16DataSourceVersion2; + case SampleFormat.PcmFloat: + return CommandType.PcmFloatDataSourceVersion2; + default: + throw new NotImplementedException($"{sampleFormat}"); + } + } + + public void Process(CommandList context) + { + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + DataSourceHelper.WaveBufferInformation info = new DataSourceHelper.WaveBufferInformation() + { + State = State, + SourceSampleRate = SampleRate, + SampleFormat = SampleFormat, + Pitch = Pitch, + DecodingBehaviour = DecodingBehaviour, + WaveBuffers = WaveBuffers, + ExtraParameter = ExtraParameter, + ExtraParameterSize = ExtraParameterSize, + ChannelIndex = (int)ChannelIndex, + ChannelCount = (int)ChannelCount, + SrcQuality = SrcQuality + }; + + DataSourceHelper.ProcessWaveBuffers(context.MemoryManager, outputBuffer, info, context.SampleRate, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DelayCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DelayCommand.cs new file mode 100644 index 00000000..1bf844d8 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DelayCommand.cs @@ -0,0 +1,272 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.Effect; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DelayCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Delay; + + public ulong EstimatedProcessingTime { get; set; } + + public DelayParameter Parameter => _parameter; + public Memory State { get; } + public ulong WorkBuffer { get; } + public ushort[] OutputBufferIndices { get; } + public ushort[] InputBufferIndices { get; } + public bool IsEffectEnabled { get; } + + private DelayParameter _parameter; + + private const int FixedPointPrecision = 14; + + public DelayCommand(uint bufferOffset, DelayParameter parameter, Memory state, bool isEnabled, ulong workBuffer, int nodeId) + { + Enabled = true; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + IsEffectEnabled = isEnabled; + + InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + } + + private void ProcessDelayMono(Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount) + { + ref DelayState state = ref State.Span[0]; + + float feedbackGain = FixedPointHelper.ToFloat(Parameter.FeedbackGain, FixedPointPrecision); + float inGain = FixedPointHelper.ToFloat(Parameter.InGain, FixedPointPrecision); + float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision); + float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); + + for (int i = 0; i < sampleCount; i++) + { + float input = inputBuffer[i] * 64; + float delayLineValue = state.DelayLines[0].Read(); + + float lowPassResult = input * inGain + delayLineValue * feedbackGain * state.LowPassBaseGain + state.LowPassZ[0] * state.LowPassFeedbackGain; + + state.LowPassZ[0] = lowPassResult; + + state.DelayLines[0].Update(lowPassResult); + + outputBuffer[i] = (input * dryGain + delayLineValue * outGain) / 64; + } + } + + private void ProcessDelayStereo(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ref DelayState state = ref State.Span[0]; + + float[] channelInput = new float[Parameter.ChannelCount]; + float[] delayLineValues = new float[Parameter.ChannelCount]; + float[] temp = new float[Parameter.ChannelCount]; + + float delayFeedbackBaseGain = state.DelayFeedbackBaseGain; + float delayFeedbackCrossGain = state.DelayFeedbackCrossGain; + float inGain = FixedPointHelper.ToFloat(Parameter.InGain, FixedPointPrecision); + float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision); + float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); + + for (int i = 0; i < sampleCount; i++) + { + for (int j = 0; j < Parameter.ChannelCount; j++) + { + channelInput[j] = inputBuffers[j].Span[i] * 64; + delayLineValues[j] = state.DelayLines[j].Read(); + } + + temp[0] = channelInput[0] * inGain + delayLineValues[1] * delayFeedbackCrossGain + delayLineValues[0] * delayFeedbackBaseGain; + temp[1] = channelInput[1] * inGain + delayLineValues[0] * delayFeedbackCrossGain + delayLineValues[1] * delayFeedbackBaseGain; + + for (int j = 0; j < Parameter.ChannelCount; j++) + { + float lowPassResult = state.LowPassFeedbackGain * state.LowPassZ[j] + temp[j] * state.LowPassBaseGain; + + state.LowPassZ[j] = lowPassResult; + state.DelayLines[j].Update(lowPassResult); + + outputBuffers[j].Span[i] = (channelInput[j] * dryGain + delayLineValues[j] * outGain) / 64; + } + } + } + + private void ProcessDelayQuadraphonic(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ref DelayState state = ref State.Span[0]; + + float[] channelInput = new float[Parameter.ChannelCount]; + float[] delayLineValues = new float[Parameter.ChannelCount]; + float[] temp = new float[Parameter.ChannelCount]; + + float delayFeedbackBaseGain = state.DelayFeedbackBaseGain; + float delayFeedbackCrossGain = state.DelayFeedbackCrossGain; + float inGain = FixedPointHelper.ToFloat(Parameter.InGain, FixedPointPrecision); + float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision); + float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); + + for (int i = 0; i < sampleCount; i++) + { + for (int j = 0; j < Parameter.ChannelCount; j++) + { + channelInput[j] = inputBuffers[j].Span[i] * 64; + delayLineValues[j] = state.DelayLines[j].Read(); + } + + temp[0] = channelInput[0] * inGain + (delayLineValues[2] + delayLineValues[1]) * delayFeedbackCrossGain + delayLineValues[0] * delayFeedbackBaseGain; + temp[1] = channelInput[1] * inGain + (delayLineValues[0] + delayLineValues[3]) * delayFeedbackCrossGain + delayLineValues[1] * delayFeedbackBaseGain; + temp[2] = channelInput[2] * inGain + (delayLineValues[3] + delayLineValues[0]) * delayFeedbackCrossGain + delayLineValues[2] * delayFeedbackBaseGain; + temp[3] = channelInput[3] * inGain + (delayLineValues[1] + delayLineValues[2]) * delayFeedbackCrossGain + delayLineValues[3] * delayFeedbackBaseGain; + + for (int j = 0; j < Parameter.ChannelCount; j++) + { + float lowPassResult = state.LowPassFeedbackGain * state.LowPassZ[j] + temp[j] * state.LowPassBaseGain; + + state.LowPassZ[j] = lowPassResult; + state.DelayLines[j].Update(lowPassResult); + + outputBuffers[j].Span[i] = (channelInput[j] * dryGain + delayLineValues[j] * outGain) / 64; + } + } + } + + private void ProcessDelaySurround(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ref DelayState state = ref State.Span[0]; + + float[] channelInput = new float[Parameter.ChannelCount]; + float[] delayLineValues = new float[Parameter.ChannelCount]; + float[] temp = new float[Parameter.ChannelCount]; + + float delayFeedbackBaseGain = state.DelayFeedbackBaseGain; + float delayFeedbackCrossGain = state.DelayFeedbackCrossGain; + float inGain = FixedPointHelper.ToFloat(Parameter.InGain, FixedPointPrecision); + float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision); + float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); + + for (int i = 0; i < sampleCount; i++) + { + for (int j = 0; j < Parameter.ChannelCount; j++) + { + channelInput[j] = inputBuffers[j].Span[i] * 64; + delayLineValues[j] = state.DelayLines[j].Read(); + } + + temp[0] = channelInput[0] * inGain + (delayLineValues[2] + delayLineValues[4]) * delayFeedbackCrossGain + delayLineValues[0] * delayFeedbackBaseGain; + temp[1] = channelInput[1] * inGain + (delayLineValues[4] + delayLineValues[3]) * delayFeedbackCrossGain + delayLineValues[1] * delayFeedbackBaseGain; + temp[2] = channelInput[2] * inGain + (delayLineValues[3] + delayLineValues[0]) * delayFeedbackCrossGain + delayLineValues[2] * delayFeedbackBaseGain; + temp[3] = channelInput[3] * inGain + (delayLineValues[1] + delayLineValues[2]) * delayFeedbackCrossGain + delayLineValues[3] * delayFeedbackBaseGain; + temp[4] = channelInput[4] * inGain + (delayLineValues[0] + delayLineValues[1]) * delayFeedbackCrossGain + delayLineValues[4] * delayFeedbackBaseGain; + temp[5] = channelInput[5] * inGain + delayLineValues[5] * delayFeedbackBaseGain; + + for (int j = 0; j < Parameter.ChannelCount; j++) + { + float lowPassResult = state.LowPassFeedbackGain * state.LowPassZ[j] + temp[j] * state.LowPassBaseGain; + + state.LowPassZ[j] = lowPassResult; + state.DelayLines[j].Update(lowPassResult); + + outputBuffers[j].Span[i] = (channelInput[j] * dryGain + delayLineValues[j] * outGain) / 64; + } + } + } + + private void ProcessDelay(CommandList context) + { + Debug.Assert(Parameter.IsChannelCountValid()); + + if (IsEffectEnabled && Parameter.IsChannelCountValid()) + { + ReadOnlyMemory[] inputBuffers = new ReadOnlyMemory[Parameter.ChannelCount]; + Memory[] outputBuffers = new Memory[Parameter.ChannelCount]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + inputBuffers[i] = context.GetBufferMemory(InputBufferIndices[i]); + outputBuffers[i] = context.GetBufferMemory(OutputBufferIndices[i]); + } + + switch (Parameter.ChannelCount) + { + case 1: + ProcessDelayMono(outputBuffers[0].Span, inputBuffers[0].Span, context.SampleCount); + break; + case 2: + ProcessDelayStereo(outputBuffers, inputBuffers, context.SampleCount); + break; + case 4: + ProcessDelayQuadraphonic(outputBuffers, inputBuffers, context.SampleCount); + break; + case 6: + ProcessDelaySurround(outputBuffers, inputBuffers, context.SampleCount); + break; + default: + throw new NotImplementedException($"{Parameter.ChannelCount}"); + } + } + else + { + for (int i = 0; i < Parameter.ChannelCount; i++) + { + if (InputBufferIndices[i] != OutputBufferIndices[i]) + { + context.GetBufferMemory(InputBufferIndices[i]).CopyTo(context.GetBufferMemory(OutputBufferIndices[i])); + } + } + } + } + + public void Process(CommandList context) + { + ref DelayState state = ref State.Span[0]; + + if (IsEffectEnabled) + { + if (Parameter.Status == UsageState.Invalid) + { + state = new DelayState(ref _parameter, WorkBuffer); + } + else if (Parameter.Status == UsageState.New) + { + state.UpdateParameter(ref _parameter); + } + } + + ProcessDelay(context); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DepopForMixBuffersCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DepopForMixBuffersCommand.cs new file mode 100644 index 00000000..5a9806b6 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DepopForMixBuffersCommand.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DepopForMixBuffersCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.DepopForMixBuffers; + + public ulong EstimatedProcessingTime { get; set; } + + public uint MixBufferOffset { get; } + + public uint MixBufferCount { get; } + + public float Decay { get; } + + public Memory DepopBuffer { get; } + + private const int FixedPointPrecisionForDecay = 15; + + public DepopForMixBuffersCommand(Memory depopBuffer, uint bufferOffset, uint mixBufferCount, int nodeId, uint sampleRate) + { + Enabled = true; + NodeId = nodeId; + MixBufferOffset = bufferOffset; + MixBufferCount = mixBufferCount; + DepopBuffer = depopBuffer; + + if (sampleRate == 48000) + { + Decay = 0.962189f; + } + else // if (sampleRate == 32000) + { + Decay = 0.943695f; + } + } + + private float ProcessDepopMix(Span buffer, float depopValue, uint sampleCount) + { + if (depopValue <= 0) + { + for (int i = 0; i < sampleCount; i++) + { + depopValue = FloatingPointHelper.MultiplyRoundDown(Decay, depopValue); + + buffer[i] -= depopValue; + } + + return -depopValue; + } + else + { + for (int i = 0; i < sampleCount; i++) + { + depopValue = FloatingPointHelper.MultiplyRoundDown(Decay, depopValue); + + buffer[i] += depopValue; + } + + return depopValue; + } + + } + + public void Process(CommandList context) + { + uint bufferCount = Math.Min(MixBufferOffset + MixBufferCount, context.BufferCount); + + for (int i = (int)MixBufferOffset; i < bufferCount; i++) + { + float depopValue = DepopBuffer.Span[i]; + if (depopValue != 0) + { + Span buffer = context.GetBuffer(i); + + DepopBuffer.Span[i] = ProcessDepopMix(buffer, depopValue, context.SampleCount); + } + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DepopPrepareCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DepopPrepareCommand.cs new file mode 100644 index 00000000..21ded53a --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DepopPrepareCommand.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DepopPrepareCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.DepopPrepare; + + public ulong EstimatedProcessingTime { get; set; } + + public uint MixBufferCount { get; } + + public ushort[] OutputBufferIndices { get; } + + public Memory State { get; } + public Memory DepopBuffer { get; } + + public DepopPrepareCommand(Memory state, Memory depopBuffer, uint mixBufferCount, uint bufferOffset, int nodeId, bool enabled) + { + Enabled = enabled; + NodeId = nodeId; + MixBufferCount = mixBufferCount; + + OutputBufferIndices = new ushort[RendererConstants.MixBufferCountMax]; + + for (int i = 0; i < RendererConstants.MixBufferCountMax; i++) + { + OutputBufferIndices[i] = (ushort)(bufferOffset + i); + } + + State = state; + DepopBuffer = depopBuffer; + } + + public void Process(CommandList context) + { + ref VoiceUpdateState state = ref State.Span[0]; + + for (int i = 0; i < MixBufferCount; i++) + { + if (state.LastSamples[i] != 0) + { + DepopBuffer.Span[OutputBufferIndices[i]] += state.LastSamples[i]; + + state.LastSamples[i] = 0; + } + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DeviceSinkCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DeviceSinkCommand.cs new file mode 100644 index 00000000..111d2a4c --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DeviceSinkCommand.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Integration; +using Ryujinx.Audio.Renderer.Server.Sink; +using System; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DeviceSinkCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.DeviceSink; + + public ulong EstimatedProcessingTime { get; set; } + + public string DeviceName { get; } + + public int SessionId { get; } + + public uint InputCount { get; } + public ushort[] InputBufferIndices { get; } + + public Memory Buffers { get; } + + public DeviceSinkCommand(uint bufferOffset, DeviceSink sink, int sessionId, Memory buffers, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + DeviceName = Encoding.ASCII.GetString(sink.Parameter.DeviceName).TrimEnd('\0'); + SessionId = sessionId; + InputCount = sink.Parameter.InputCount; + InputBufferIndices = new ushort[InputCount]; + + for (int i = 0; i < InputCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + sink.Parameter.Input[i]); + } + + if (sink.UpsamplerState != null) + { + Buffers = sink.UpsamplerState.OutputBuffer; + } + else + { + Buffers = buffers; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Span GetBuffer(int index, int sampleCount) + { + return Buffers.Span.Slice(index * sampleCount, sampleCount); + } + + public void Process(CommandList context) + { + HardwareDevice device = context.OutputDevice; + + if (device.GetSampleRate() == RendererConstants.TargetSampleRate) + { + int channelCount = (int)device.GetChannelCount(); + uint bufferCount = Math.Min(device.GetChannelCount(), InputCount); + + const int sampleCount = RendererConstants.TargetSampleCount; + + short[] outputBuffer = new short[bufferCount * sampleCount]; + + for (int i = 0; i < bufferCount; i++) + { + ReadOnlySpan inputBuffer = GetBuffer(InputBufferIndices[i], sampleCount); + + for (int j = 0; j < sampleCount; j++) + { + outputBuffer[i + j * channelCount] = PcmHelper.Saturate(inputBuffer[j]); + } + } + + device.AppendBuffer(outputBuffer, InputCount); + } + else + { + // TODO: support resampling for device only supporting something different + throw new NotImplementedException(); + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/DownMixSurroundToStereoCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/DownMixSurroundToStereoCommand.cs new file mode 100644 index 00000000..76f5c576 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/DownMixSurroundToStereoCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class DownMixSurroundToStereoCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.DownMixSurroundToStereo; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort[] InputBufferIndices { get; } + public ushort[] OutputBufferIndices { get; } + + public float[] Coefficients { get; } + + public DownMixSurroundToStereoCommand(uint bufferOffset, Span inputBufferOffset, Span outputBufferOffset, ReadOnlySpan downMixParameter, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + + for (int i = 0; i < RendererConstants.VoiceChannelCountMax; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + outputBufferOffset[i]); + } + + Coefficients = downMixParameter.ToArray(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float DownMixSurroundToStereo(ReadOnlySpan coefficients, float back, float lfe, float center, float front) + { + return FloatingPointHelper.RoundUp(coefficients[3] * back + coefficients[2] * lfe + coefficients[1] * center + coefficients[0] * front); + } + + public void Process(CommandList context) + { + ReadOnlySpan frontLeft = context.GetBuffer(InputBufferIndices[0]); + ReadOnlySpan frontRight = context.GetBuffer(InputBufferIndices[1]); + ReadOnlySpan lowFrequency = context.GetBuffer(InputBufferIndices[2]); + ReadOnlySpan frontCenter = context.GetBuffer(InputBufferIndices[3]); + ReadOnlySpan backLeft = context.GetBuffer(InputBufferIndices[4]); + ReadOnlySpan backRight = context.GetBuffer(InputBufferIndices[5]); + + Span stereoLeft = context.GetBuffer(OutputBufferIndices[0]); + Span stereoRight = context.GetBuffer(OutputBufferIndices[1]); + Span unused2 = context.GetBuffer(OutputBufferIndices[2]); + Span unused3 = context.GetBuffer(OutputBufferIndices[3]); + Span unused4 = context.GetBuffer(OutputBufferIndices[4]); + Span unused5 = context.GetBuffer(OutputBufferIndices[5]); + + for (int i = 0; i < context.SampleCount; i++) + { + stereoLeft[i] = DownMixSurroundToStereo(Coefficients, backLeft[i], lowFrequency[i], frontCenter[i], frontLeft[i]); + stereoRight[i] = DownMixSurroundToStereo(Coefficients, backRight[i], lowFrequency[i], frontCenter[i], frontRight[i]); + } + + unused2.Fill(0); + unused3.Fill(0); + unused4.Fill(0); + unused5.Fill(0); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/ICommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/ICommand.cs new file mode 100644 index 00000000..85a01123 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/ICommand.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public interface ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType { get; } + + public ulong EstimatedProcessingTime { get; } + + public void Process(CommandList context); + + public bool ShouldMeter() + { + return false; + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/MixCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/MixCommand.cs new file mode 100644 index 00000000..87b296f6 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/MixCommand.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class MixCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Mix; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public float Volume { get; } + + public MixCommand(uint inputBufferIndex, uint outputBufferIndex, int nodeId, float volume) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)inputBufferIndex; + OutputBufferIndex = (ushort)outputBufferIndex; + + Volume = volume; + } + + private void ProcessMixAvx(Span outputMix, ReadOnlySpan inputMix) + { + Vector256 volumeVec = Vector256.Create(Volume); + + ReadOnlySpan> inputVec = MemoryMarshal.Cast>(inputMix); + Span> outputVec = MemoryMarshal.Cast>(outputMix); + + int sisdStart = inputVec.Length * 8; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Avx.Add(outputVec[i], Avx.Ceiling(Avx.Multiply(inputVec[i], volumeVec))); + } + + for (int i = sisdStart; i < inputMix.Length; i++) + { + outputMix[i] += FloatingPointHelper.MultiplyRoundUp(inputMix[i], Volume); + } + } + + private void ProcessMixSse41(Span outputMix, ReadOnlySpan inputMix) + { + Vector128 volumeVec = Vector128.Create(Volume); + + ReadOnlySpan> inputVec = MemoryMarshal.Cast>(inputMix); + Span> outputVec = MemoryMarshal.Cast>(outputMix); + + int sisdStart = inputVec.Length * 4; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Sse.Add(outputVec[i], Sse41.Ceiling(Sse.Multiply(inputVec[i], volumeVec))); + } + + for (int i = sisdStart; i < inputMix.Length; i++) + { + outputMix[i] += FloatingPointHelper.MultiplyRoundUp(inputMix[i], Volume); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessMixSlowPath(Span outputMix, ReadOnlySpan inputMix) + { + for (int i = 0; i < inputMix.Length; i++) + { + outputMix[i] += FloatingPointHelper.MultiplyRoundUp(inputMix[i], Volume); + } + } + + private void ProcessMix(Span outputMix, ReadOnlySpan inputMix) + { + if (Avx.IsSupported) + { + ProcessMixAvx(outputMix, inputMix); + } + else if (Sse41.IsSupported) + { + ProcessMixSse41(outputMix, inputMix); + } + else + { + ProcessMixSlowPath(outputMix, inputMix); + } + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + ProcessMix(outputBuffer, inputBuffer); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/MixRampCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/MixRampCommand.cs new file mode 100644 index 00000000..0397dee2 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/MixRampCommand.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class MixRampCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.MixRamp; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public float Volume0 { get; } + public float Volume1 { get; } + + public Memory State { get; } + + public int LastSampleIndex { get; } + + public MixRampCommand(float volume0, float volume1, uint inputBufferIndex, uint outputBufferIndex, int lastSampleIndex, Memory state, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)inputBufferIndex; + OutputBufferIndex = (ushort)outputBufferIndex; + + Volume0 = volume0; + Volume1 = volume1; + + State = state; + LastSampleIndex = lastSampleIndex; + } + + private float ProcessMixRamp(Span outputBuffer, ReadOnlySpan inputBuffer, int sampleCount) + { + float ramp = (Volume1 - Volume0) / sampleCount; + float volume = Volume0; + float state = 0; + + for (int i = 0; i < sampleCount; i++) + { + state = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], volume); + + outputBuffer[i] += state; + volume += ramp; + } + + return state; + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + State.Span[0].LastSamples[LastSampleIndex] = ProcessMixRamp(outputBuffer, inputBuffer, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/MixRampGroupedCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/MixRampGroupedCommand.cs new file mode 100644 index 00000000..700b906c --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/MixRampGroupedCommand.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class MixRampGroupedCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.MixRampGrouped; + + public ulong EstimatedProcessingTime { get; set; } + + public uint MixBufferCount { get; } + + public ushort[] InputBufferIndices { get; } + public ushort[] OutputBufferIndices { get; } + + public float[] Volume0 { get; } + public float[] Volume1 { get; } + + public Memory State { get; } + + public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span volume0, Span volume1, Memory state, int nodeId) + { + Enabled = true; + MixBufferCount = mixBufferCount; + NodeId = nodeId; + + InputBufferIndices = new ushort[RendererConstants.MixBufferCountMax]; + OutputBufferIndices = new ushort[RendererConstants.MixBufferCountMax]; + Volume0 = new float[RendererConstants.MixBufferCountMax]; + Volume1 = new float[RendererConstants.MixBufferCountMax]; + + for (int i = 0; i < mixBufferCount; i++) + { + InputBufferIndices[i] = (ushort)inputBufferIndex; + OutputBufferIndices[i] = (ushort)(outputBufferIndex + i); + + Volume0[i] = volume0[i]; + Volume1[i] = volume1[i]; + } + + State = state; + } + + private float ProcessMixRampGrouped(Span outputBuffer, ReadOnlySpan inputBuffer, float volume0, float volume1, int sampleCount) + { + float ramp = (volume1 - volume0) / sampleCount; + float volume = volume0; + float state = 0; + + for (int i = 0; i < sampleCount; i++) + { + state = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], volume); + + outputBuffer[i] += state; + volume += ramp; + } + + return state; + } + + public void Process(CommandList context) + { + for (int i = 0; i < MixBufferCount; i++) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndices[i]); + Span outputBuffer = context.GetBuffer(OutputBufferIndices[i]); + + float volume0 = Volume0[i]; + float volume1 = Volume1[i]; + + ref VoiceUpdateState state = ref State.Span[0]; + + if (volume0 != 0 || volume1 != 0) + { + state.LastSamples[i] = ProcessMixRampGrouped(outputBuffer, inputBuffer, volume0, volume1, (int)context.SampleCount); + } + else + { + state.LastSamples[i] = 0; + } + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/PcmFloatDataSourceCommandVersion1.cs b/Ryujinx.Audio.Renderer/Dsp/Command/PcmFloatDataSourceCommandVersion1.cs new file mode 100644 index 00000000..49e6253d --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/PcmFloatDataSourceCommandVersion1.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class PcmFloatDataSourceCommandVersion1 : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.PcmFloatDataSourceVersion1; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort OutputBufferIndex { get; } + public uint SampleRate { get; } + public uint ChannelIndex { get; } + + public uint ChannelCount { get; } + + public float Pitch { get; } + + public WaveBuffer[] WaveBuffers { get; } + + public Memory State { get; } + public DecodingBehaviour DecodingBehaviour { get; } + + public PcmFloatDataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + OutputBufferIndex = (ushort)(channelIndex + outputBufferIndex); + SampleRate = serverState.SampleRate; + ChannelIndex = channelIndex; + ChannelCount = serverState.ChannelsCount; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; + + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref Server.Voice.WaveBuffer voiceWaveBuffer = ref serverState.WaveBuffers[i]; + + WaveBuffers[i] = voiceWaveBuffer.ToCommon(1); + } + + State = state; + DecodingBehaviour = serverState.DecodingBehaviour; + } + + public void Process(CommandList context) + { + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + DataSourceHelper.WaveBufferInformation info = new DataSourceHelper.WaveBufferInformation() + { + State = State, + SourceSampleRate = SampleRate, + SampleFormat = SampleFormat.PcmInt16, + Pitch = Pitch, + DecodingBehaviour = DecodingBehaviour, + WaveBuffers = WaveBuffers, + ExtraParameter = 0, + ExtraParameterSize = 0, + ChannelIndex = (int)ChannelIndex, + ChannelCount = (int)ChannelCount, + }; + + DataSourceHelper.ProcessWaveBuffers(context.MemoryManager, outputBuffer, info, context.SampleRate, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/PcmInt16DataSourceCommandVersion1.cs b/Ryujinx.Audio.Renderer/Dsp/Command/PcmInt16DataSourceCommandVersion1.cs new file mode 100644 index 00000000..56f85a90 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/PcmInt16DataSourceCommandVersion1.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Common; +using System; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class PcmInt16DataSourceCommandVersion1 : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.PcmInt16DataSourceVersion1; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort OutputBufferIndex { get; } + public uint SampleRate { get; } + public uint ChannelIndex { get; } + + public uint ChannelCount { get; } + + public float Pitch { get; } + + public WaveBuffer[] WaveBuffers { get; } + + public Memory State { get; } + public DecodingBehaviour DecodingBehaviour { get; } + + public PcmInt16DataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + OutputBufferIndex = (ushort)(channelIndex + outputBufferIndex); + SampleRate = serverState.SampleRate; + ChannelIndex = channelIndex; + ChannelCount = serverState.ChannelsCount; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; + + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref Server.Voice.WaveBuffer voiceWaveBuffer = ref serverState.WaveBuffers[i]; + + WaveBuffers[i] = voiceWaveBuffer.ToCommon(1); + } + + State = state; + DecodingBehaviour = serverState.DecodingBehaviour; + } + + public void Process(CommandList context) + { + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + DataSourceHelper.WaveBufferInformation info = new DataSourceHelper.WaveBufferInformation() + { + State = State, + SourceSampleRate = SampleRate, + SampleFormat = SampleFormat.PcmInt16, + Pitch = Pitch, + DecodingBehaviour = DecodingBehaviour, + WaveBuffers = WaveBuffers, + ExtraParameter = 0, + ExtraParameterSize = 0, + ChannelIndex = (int)ChannelIndex, + ChannelCount = (int)ChannelCount, + }; + + DataSourceHelper.ProcessWaveBuffers(context.MemoryManager, outputBuffer, info, context.SampleRate, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/PerformanceCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/PerformanceCommand.cs new file mode 100644 index 00000000..199667e0 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/PerformanceCommand.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Server.Performance; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class PerformanceCommand : ICommand + { + public enum Type + { + Invalid, + Start, + End + } + + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Performance; + + public ulong EstimatedProcessingTime { get; set; } + + public PerformanceEntryAddresses PerformanceEntryAddresses { get; } + + public Type PerformanceType { get; set; } + + public PerformanceCommand(ref PerformanceEntryAddresses performanceEntryAddresses, Type performanceType, int nodeId) + { + Enabled = true; + PerformanceEntryAddresses = performanceEntryAddresses; + PerformanceType = performanceType; + NodeId = nodeId; + } + + public void Process(CommandList context) + { + if (PerformanceType == Type.Start) + { + PerformanceEntryAddresses.SetStartTime(context.GetTimeElapsedSinceDspStartedProcessing()); + } + else if (PerformanceType == Type.End) + { + PerformanceEntryAddresses.SetProcessingTime(context.GetTimeElapsedSinceDspStartedProcessing()); + PerformanceEntryAddresses.IncrementEntryCount(); + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/Reverb3dCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/Reverb3dCommand.cs new file mode 100644 index 00000000..eeb7322a --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/Reverb3dCommand.cs @@ -0,0 +1,269 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.Effect; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class Reverb3dCommand : ICommand + { + private static readonly int[] OutputEarlyIndicesTableMono = new int[20] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableMono = new int[20] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + private static readonly int[] TargetOutputFeedbackIndicesTableMono = new int[1] { 0 }; + + private static readonly int[] OutputEarlyIndicesTableStereo = new int[20] { 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableStereo = new int[20] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + private static readonly int[] TargetOutputFeedbackIndicesTableStereo = new int[2] { 0, 1 }; + + private static readonly int[] OutputEarlyIndicesTableQuadraphonic = new int[20] { 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 3, 3, 3 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableQuadraphonic = new int[20] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + private static readonly int[] TargetOutputFeedbackIndicesTableQuadraphonic = new int[4] { 0, 1, 2, 3 }; + + private static readonly int[] OutputEarlyIndicesTableSurround = new int[40] { 4, 5, 0, 5, 0, 5, 1, 5, 1, 5, 1, 5, 1, 5, 2, 5, 2, 5, 2, 5, 1, 5, 1, 5, 1, 5, 0, 5, 0, 5, 0, 5, 0, 5, 3, 5, 3, 5, 3, 5 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableSurround = new int[40] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19 }; + private static readonly int[] TargetOutputFeedbackIndicesTableSurround = new int[6] { 0, 1, 2, 3, -1, 3 }; + + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Reverb3d; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public Reverb3dParameter Parameter => _parameter; + public Memory State { get; } + public ulong WorkBuffer { get; } + public ushort[] OutputBufferIndices { get; } + public ushort[] InputBufferIndices { get; } + + public bool IsEffectEnabled { get; } + + private Reverb3dParameter _parameter; + + public Reverb3dCommand(uint bufferOffset, Reverb3dParameter parameter, Memory state, bool isEnabled, ulong workBuffer, int nodeId) + { + Enabled = true; + IsEffectEnabled = isEnabled; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + } + + private void ProcessReverb3dMono(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + const int delayLineSampleIndexOffset = -1; + + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableMono, TargetEarlyDelayLineIndicesTableMono, TargetOutputFeedbackIndicesTableMono, delayLineSampleIndexOffset); + } + + private void ProcessReverb3dStereo(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + const int delayLineSampleIndexOffset = 1; + + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableStereo, TargetEarlyDelayLineIndicesTableStereo, TargetOutputFeedbackIndicesTableStereo, delayLineSampleIndexOffset); + } + + private void ProcessReverb3dQuadraphonic(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + const int delayLineSampleIndexOffset = 1; + + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableQuadraphonic, TargetEarlyDelayLineIndicesTableQuadraphonic, TargetOutputFeedbackIndicesTableQuadraphonic, delayLineSampleIndexOffset); + } + + private void ProcessReverb3dSurround(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + const int delayLineSampleIndexOffset = 1; + + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableSurround, TargetEarlyDelayLineIndicesTableSurround, TargetOutputFeedbackIndicesTableSurround, delayLineSampleIndexOffset); + } + + private void ProcessReverb3dGeneric(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable, int delayLineSampleIndexOffset) + { + ref Reverb3dState state = ref State.Span[0]; + + bool isMono = Parameter.ChannelCount == 1; + bool isSurround = Parameter.ChannelCount == 6; + + float[] outputValues = new float[RendererConstants.ChannelCountMax]; + float[] channelInput = new float[Parameter.ChannelCount]; + float[] feedbackValues = new float[4]; + float[] feedbackOutputValues = new float[4]; + float[] values = new float[4]; + + for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) + { + outputValues.AsSpan().Fill(0); + + float tapOut = state.PreDelayLine.TapUnsafe(state.ReflectionDelayTime, delayLineSampleIndexOffset); + + for (int i = 0; i < targetEarlyDelayLineIndicesTable.Length; i++) + { + int earlyDelayIndex = targetEarlyDelayLineIndicesTable[i]; + int outputIndex = outputEarlyIndicesTable[i]; + + float tempTapOut = state.PreDelayLine.TapUnsafe(state.EarlyDelayTime[earlyDelayIndex], delayLineSampleIndexOffset); + + outputValues[outputIndex] += tempTapOut * state.EarlyGain[earlyDelayIndex]; + } + + float targetPreDelayValue = 0; + + for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++) + { + channelInput[channelIndex] = inputBuffers[channelIndex].Span[sampleIndex]; + targetPreDelayValue += channelInput[channelIndex]; + } + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + outputValues[i] *= state.EarlyReflectionsGain; + } + + state.PreviousPreDelayValue = (targetPreDelayValue * state.TargetPreDelayGain) + (state.PreviousPreDelayValue * state.PreviousPreDelayGain); + + state.PreDelayLine.Update(state.PreviousPreDelayValue); + + for (int i = 0; i < state.FdnDelayLines.Length; i++) + { + float fdnValue = state.FdnDelayLines[i].Read(); + + float feedbackOutputValue = fdnValue * state.DecayDirectFdnGain[i] + state.PreviousFeedbackOutputDecayed[i]; + + state.PreviousFeedbackOutputDecayed[i] = (fdnValue * state.DecayCurrentFdnGain[i]) + (feedbackOutputValue * state.DecayCurrentOutputGain[i]); + + feedbackOutputValues[i] = feedbackOutputValue; + } + + feedbackValues[0] = feedbackOutputValues[2] + feedbackOutputValues[1]; + feedbackValues[1] = -feedbackOutputValues[0] - feedbackOutputValues[3]; + feedbackValues[2] = feedbackOutputValues[0] - feedbackOutputValues[3]; + feedbackValues[3] = feedbackOutputValues[1] - feedbackOutputValues[2]; + + for (int i = 0; i < state.DecayDelays1.Length; i++) + { + float temp = state.DecayDelays1[i].Update(tapOut * state.LateReverbGain + feedbackValues[i]); + + values[i] = state.DecayDelays2[i].Update(temp); + + state.FdnDelayLines[i].Update(values[i]); + } + + for (int channelIndex = 0; channelIndex < targetOutputFeedbackIndicesTable.Length; channelIndex++) + { + int targetOutputFeedbackIndex = targetOutputFeedbackIndicesTable[channelIndex]; + + if (targetOutputFeedbackIndex >= 0) + { + outputBuffers[channelIndex].Span[sampleIndex] = (outputValues[channelIndex] + values[targetOutputFeedbackIndex] + channelInput[channelIndex] * state.DryGain); + } + } + + if (isMono) + { + outputBuffers[0].Span[sampleIndex] += values[1]; + } + + if (isSurround) + { + outputBuffers[4].Span[sampleIndex] += (outputValues[4] + state.BackLeftDelayLine.Update((values[2] - values[3]) * 0.5f) + channelInput[4] * state.DryGain); + } + } + } + + public void ProcessReverb3d(CommandList context) + { + Debug.Assert(Parameter.IsChannelCountValid()); + + if (IsEffectEnabled && Parameter.IsChannelCountValid()) + { + ReadOnlyMemory[] inputBuffers = new ReadOnlyMemory[Parameter.ChannelCount]; + Memory[] outputBuffers = new Memory[Parameter.ChannelCount]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + inputBuffers[i] = context.GetBufferMemory(InputBufferIndices[i]); + outputBuffers[i] = context.GetBufferMemory(OutputBufferIndices[i]); + } + + switch (Parameter.ChannelCount) + { + case 1: + ProcessReverb3dMono(outputBuffers, inputBuffers, context.SampleCount); + break; + case 2: + ProcessReverb3dStereo(outputBuffers, inputBuffers, context.SampleCount); + break; + case 4: + ProcessReverb3dQuadraphonic(outputBuffers, inputBuffers, context.SampleCount); + break; + case 6: + ProcessReverb3dSurround(outputBuffers, inputBuffers, context.SampleCount); + break; + default: + throw new NotImplementedException($"{Parameter.ChannelCount}"); + } + } + else + { + for (int i = 0; i < Parameter.ChannelCount; i++) + { + if (InputBufferIndices[i] != OutputBufferIndices[i]) + { + context.GetBufferMemory(InputBufferIndices[i]).CopyTo(context.GetBufferMemory(OutputBufferIndices[i])); + } + } + } + } + + public void Process(CommandList context) + { + ref Reverb3dState state = ref State.Span[0]; + + if (IsEffectEnabled) + { + if (Parameter.ParameterStatus == UsageState.Invalid) + { + state = new Reverb3dState(ref _parameter, WorkBuffer); + } + else if (Parameter.ParameterStatus == UsageState.New) + { + state.UpdateParameter(ref _parameter); + } + } + + ProcessReverb3d(context); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/ReverbCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/ReverbCommand.cs new file mode 100644 index 00000000..3935234e --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/ReverbCommand.cs @@ -0,0 +1,284 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class ReverbCommand : ICommand + { + private static readonly int[] OutputEarlyIndicesTableMono = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableMono = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + private static readonly int[] OutputIndicesTableMono = new int[4] { 0, 0, 0, 0 }; + private static readonly int[] TargetOutputFeedbackIndicesTableMono = new int[4] { 0, 1, 2, 3 }; + + private static readonly int[] OutputEarlyIndicesTableStereo = new int[10] { 0, 0, 1, 1, 0, 1, 0, 0, 1, 1 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableStereo = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + private static readonly int[] OutputIndicesTableStereo = new int[4] { 0, 0, 1, 1 }; + private static readonly int[] TargetOutputFeedbackIndicesTableStereo = new int[4] { 2, 0, 3, 1 }; + + private static readonly int[] OutputEarlyIndicesTableQuadraphonic = new int[10] { 0, 0, 1, 1, 0, 1, 2, 2, 3, 3 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableQuadraphonic = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + private static readonly int[] OutputIndicesTableQuadraphonic = new int[4] { 0, 1, 2, 3 }; + private static readonly int[] TargetOutputFeedbackIndicesTableQuadraphonic = new int[4] { 0, 1, 2, 3 }; + + private static readonly int[] OutputEarlyIndicesTableSurround = new int[20] { 0, 5, 0, 5, 1, 5, 1, 5, 4, 5, 4, 5, 2, 5, 2, 5, 3, 5, 3, 5 }; + private static readonly int[] TargetEarlyDelayLineIndicesTableSurround = new int[20] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9 }; + private static readonly int[] OutputIndicesTableSurround = new int[RendererConstants.ChannelCountMax] { 0, 1, 2, 3, 4, 5 }; + private static readonly int[] TargetOutputFeedbackIndicesTableSurround = new int[RendererConstants.ChannelCountMax] { 0, 1, 2, 3, -1, 3 }; + + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Reverb; + + public ulong EstimatedProcessingTime { get; set; } + + public ReverbParameter Parameter => _parameter; + public Memory State { get; } + public ulong WorkBuffer { get; } + public ushort[] OutputBufferIndices { get; } + public ushort[] InputBufferIndices { get; } + public bool IsLongSizePreDelaySupported { get; } + + public bool IsEffectEnabled { get; } + + private ReverbParameter _parameter; + + private const int FixedPointPrecision = 14; + + public ReverbCommand(uint bufferOffset, ReverbParameter parameter, Memory state, bool isEnabled, ulong workBuffer, int nodeId, bool isLongSizePreDelaySupported) + { + Enabled = true; + IsEffectEnabled = isEnabled; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + + IsLongSizePreDelaySupported = isLongSizePreDelaySupported; + } + + private void ProcessReverbMono(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableMono, + TargetEarlyDelayLineIndicesTableMono, + TargetOutputFeedbackIndicesTableMono, + OutputIndicesTableMono); + } + + private void ProcessReverbStereo(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableStereo, + TargetEarlyDelayLineIndicesTableStereo, + TargetOutputFeedbackIndicesTableStereo, + OutputIndicesTableStereo); + } + + private void ProcessReverbQuadraphonic(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableQuadraphonic, + TargetEarlyDelayLineIndicesTableQuadraphonic, + TargetOutputFeedbackIndicesTableQuadraphonic, + OutputIndicesTableQuadraphonic); + } + + private void ProcessReverbSurround(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableSurround, + TargetEarlyDelayLineIndicesTableSurround, + TargetOutputFeedbackIndicesTableSurround, + OutputIndicesTableSurround); + } + + private void ProcessReverbGeneric(Memory[] outputBuffers, ReadOnlyMemory[] inputBuffers, uint sampleCount, ReadOnlySpan outputEarlyIndicesTable, ReadOnlySpan targetEarlyDelayLineIndicesTable, ReadOnlySpan targetOutputFeedbackIndicesTable, ReadOnlySpan outputIndicesTable) + { + ref ReverbState state = ref State.Span[0]; + + bool isSurround = Parameter.ChannelCount == 6; + + float reverbGain = FixedPointHelper.ToFloat(Parameter.ReverbGain, FixedPointPrecision); + float lateGain = FixedPointHelper.ToFloat(Parameter.LateGain, FixedPointPrecision); + float outGain = FixedPointHelper.ToFloat(Parameter.OutGain, FixedPointPrecision); + float dryGain = FixedPointHelper.ToFloat(Parameter.DryGain, FixedPointPrecision); + + float[] outputValues = new float[RendererConstants.ChannelCountMax]; + float[] feedbackValues = new float[4]; + float[] feedbackOutputValues = new float[4]; + float[] channelInput = new float[Parameter.ChannelCount]; + + for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) + { + outputValues.AsSpan().Fill(0); + + for (int i = 0; i < targetEarlyDelayLineIndicesTable.Length; i++) + { + int earlyDelayIndex = targetEarlyDelayLineIndicesTable[i]; + int outputIndex = outputEarlyIndicesTable[i]; + + float tapOutput = state.PreDelayLine.TapUnsafe(state.EarlyDelayTime[earlyDelayIndex], 0); + + outputValues[outputIndex] += tapOutput * state.EarlyGain[earlyDelayIndex]; + } + + if (isSurround) + { + outputValues[5] *= 0.2f; + } + + float targetPreDelayValue = 0; + + for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++) + { + channelInput[channelIndex] = inputBuffers[channelIndex].Span[sampleIndex] * 64; + targetPreDelayValue += channelInput[channelIndex] * reverbGain; + } + + state.PreDelayLine.Update(targetPreDelayValue); + + float lateValue = state.PreDelayLine.Tap(state.PreDelayLineDelayTime) * lateGain; + + for (int i = 0; i < state.FdnDelayLines.Length; i++) + { + feedbackOutputValues[i] = state.FdnDelayLines[i].Read() * state.HighFrequencyDecayDirectGain[i] + state.PreviousFeedbackOutput[i] * state.HighFrequencyDecayPreviousGain[i]; + state.PreviousFeedbackOutput[i] = feedbackOutputValues[i]; + } + + feedbackValues[0] = feedbackOutputValues[2] + feedbackOutputValues[1]; + feedbackValues[1] = -feedbackOutputValues[0] - feedbackOutputValues[3]; + feedbackValues[2] = feedbackOutputValues[0] - feedbackOutputValues[3]; + feedbackValues[3] = feedbackOutputValues[1] - feedbackOutputValues[2]; + + for (int i = 0; i < state.FdnDelayLines.Length; i++) + { + feedbackOutputValues[i] = state.DecayDelays[i].Update(feedbackValues[i] + lateValue); + state.FdnDelayLines[i].Update(feedbackOutputValues[i]); + } + + for (int i = 0; i < targetOutputFeedbackIndicesTable.Length; i++) + { + int targetOutputFeedbackIndex = targetOutputFeedbackIndicesTable[i]; + int outputIndex = outputIndicesTable[i]; + + if (targetOutputFeedbackIndex >= 0) + { + outputValues[outputIndex] += feedbackOutputValues[targetOutputFeedbackIndex]; + } + } + + if (isSurround) + { + outputValues[4] += state.BackLeftDelayLine.Update((feedbackOutputValues[2] - feedbackOutputValues[3]) * 0.5f); + } + + for (int channelIndex = 0; channelIndex < Parameter.ChannelCount; channelIndex++) + { + outputBuffers[channelIndex].Span[sampleIndex] = (outputValues[channelIndex] * outGain + channelInput[channelIndex] * dryGain) / 64; + } + } + } + + private void ProcessReverb(CommandList context) + { + Debug.Assert(Parameter.IsChannelCountValid()); + + if (IsEffectEnabled && Parameter.IsChannelCountValid()) + { + ReadOnlyMemory[] inputBuffers = new ReadOnlyMemory[Parameter.ChannelCount]; + Memory[] outputBuffers = new Memory[Parameter.ChannelCount]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + inputBuffers[i] = context.GetBufferMemory(InputBufferIndices[i]); + outputBuffers[i] = context.GetBufferMemory(OutputBufferIndices[i]); + } + + switch (Parameter.ChannelCount) + { + case 1: + ProcessReverbMono(outputBuffers, inputBuffers, context.SampleCount); + break; + case 2: + ProcessReverbStereo(outputBuffers, inputBuffers, context.SampleCount); + break; + case 4: + ProcessReverbQuadraphonic(outputBuffers, inputBuffers, context.SampleCount); + break; + case 6: + ProcessReverbSurround(outputBuffers, inputBuffers, context.SampleCount); + break; + default: + throw new NotImplementedException($"{Parameter.ChannelCount}"); + } + } + else + { + for (int i = 0; i < Parameter.ChannelCount; i++) + { + if (InputBufferIndices[i] != OutputBufferIndices[i]) + { + context.GetBufferMemory(InputBufferIndices[i]).CopyTo(context.GetBufferMemory(OutputBufferIndices[i])); + } + } + } + } + + public void Process(CommandList context) + { + ref ReverbState state = ref State.Span[0]; + + if (IsEffectEnabled) + { + if (Parameter.Status == Server.Effect.UsageState.Invalid) + { + state = new ReverbState(ref _parameter, WorkBuffer, IsLongSizePreDelaySupported); + } + else if (Parameter.Status == Server.Effect.UsageState.New) + { + state.UpdateParameter(ref _parameter); + } + } + + ProcessReverb(context); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/UpsampleCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/UpsampleCommand.cs new file mode 100644 index 00000000..0761a8da --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/UpsampleCommand.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using Ryujinx.Audio.Renderer.Server.Upsampler; +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class UpsampleCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Upsample; + + public ulong EstimatedProcessingTime { get; set; } + + public uint BufferCount { get; } + public uint InputBufferIndex { get; } + public uint InputSampleCount { get; } + public uint InputSampleRate { get; } + + public UpsamplerState UpsamplerInfo { get; } + + public Memory OutBuffer { get; } + + public UpsampleCommand(uint bufferOffset, UpsamplerState info, uint inputCount, Span inputBufferOffset, uint bufferCount, uint sampleCount, uint sampleRate, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = 0; + OutBuffer = info.OutputBuffer; + BufferCount = bufferCount; + InputSampleCount = sampleCount; + InputSampleRate = sampleRate; + info.SourceSampleCount = inputCount; + info.InputBufferIndices = new ushort[inputCount]; + + for (int i = 0; i < inputCount; i++) + { + info.InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]); + } + + UpsamplerInfo = info; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Span GetBuffer(int index, int sampleCount) + { + return UpsamplerInfo.OutputBuffer.Span.Slice(index * sampleCount, sampleCount); + } + + public void Process(CommandList context) + { + float ratio = (float)InputSampleRate / RendererConstants.TargetSampleRate; + + uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount); + + for (int i = 0; i < bufferCount; i++) + { + Span inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]); + Span outputBuffer = GetBuffer(UpsamplerInfo.InputBufferIndices[i], (int)UpsamplerInfo.SampleCount); + + float fraction = 0.0f; + + ResamplerHelper.ResampleForUpsampler(outputBuffer, inputBuffer, ratio, ref fraction, (int)(InputSampleCount / ratio)); + } + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/VolumeCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/VolumeCommand.cs new file mode 100644 index 00000000..735a0a68 --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/VolumeCommand.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class VolumeCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.Volume; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public float Volume { get; } + + public VolumeCommand(float volume, uint bufferIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)bufferIndex; + OutputBufferIndex = (ushort)bufferIndex; + + Volume = volume; + } + + private void ProcessVolumeAvx(Span outputBuffer, ReadOnlySpan inputBuffer) + { + Vector256 volumeVec = Vector256.Create(Volume); + + ReadOnlySpan> inputVec = MemoryMarshal.Cast>(inputBuffer); + Span> outputVec = MemoryMarshal.Cast>(outputBuffer); + + int sisdStart = inputVec.Length * 8; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Avx.Ceiling(Avx.Multiply(inputVec[i], volumeVec)); + } + + for (int i = sisdStart; i < inputBuffer.Length; i++) + { + outputBuffer[i] = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], Volume); + } + } + + private void ProcessVolumeSse41(Span outputBuffer, ReadOnlySpan inputBuffer) + { + Vector128 volumeVec = Vector128.Create(Volume); + + ReadOnlySpan> inputVec = MemoryMarshal.Cast>(inputBuffer); + Span> outputVec = MemoryMarshal.Cast>(outputBuffer); + + int sisdStart = inputVec.Length * 4; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Sse41.Ceiling(Sse.Multiply(inputVec[i], volumeVec)); + } + + for (int i = sisdStart; i < inputBuffer.Length; i++) + { + outputBuffer[i] = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], Volume); + } + } + + private void ProcessVolume(Span outputBuffer, ReadOnlySpan inputBuffer) + { + if (Avx.IsSupported) + { + ProcessVolumeAvx(outputBuffer, inputBuffer); + } + else if (Sse41.IsSupported) + { + ProcessVolumeSse41(outputBuffer, inputBuffer); + } + else + { + ProcessVolumeSlowPath(outputBuffer, inputBuffer); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessVolumeSlowPath(Span outputBuffer, ReadOnlySpan inputBuffer) + { + for (int i = 0; i < outputBuffer.Length; i++) + { + outputBuffer[i] = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], Volume); + } + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + ProcessVolume(outputBuffer, inputBuffer); + } + } +} diff --git a/Ryujinx.Audio.Renderer/Dsp/Command/VolumeRampCommand.cs b/Ryujinx.Audio.Renderer/Dsp/Command/VolumeRampCommand.cs new file mode 100644 index 00000000..98427a2c --- /dev/null +++ b/Ryujinx.Audio.Renderer/Dsp/Command/VolumeRampCommand.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2019-2020 Ryujinx +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Command +{ + public class VolumeRampCommand : ICommand + { + public bool Enabled { get; set; } + + public int NodeId { get; } + + public CommandType CommandType => CommandType.VolumeRamp; + + public ulong EstimatedProcessingTime { get; set; } + + public ushort InputBufferIndex { get; } + public ushort OutputBufferIndex { get; } + + public float Volume0 { get; } + public float Volume1 { get; } + + public VolumeRampCommand(float volume0, float volume1, uint bufferIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndex = (ushort)bufferIndex; + OutputBufferIndex = (ushort)bufferIndex; + + Volume0 = volume0; + Volume1 = volume1; + } + + private void ProcessVolumeRamp(Span outputBuffer, ReadOnlySpan inputBuffer, int sampleCount) + { + float ramp = (Volume1 - Volume0) / sampleCount; + + float volume = Volume0; + + for (int i = 0; i < sampleCount; i++) + { + outputBuffer[i] = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], volume); + volume += ramp; + } + } + + public void Process(CommandList context) + { + ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex); + Span outputBuffer = context.GetBuffer(OutputBufferIndex); + + ProcessVolumeRamp(outputBuffer, inputBuffer, (int)context.SampleCount); + } + } +} -- cgit v1.2.3