diff options
author | Mary <me@thog.eu> | 2021-02-26 01:11:56 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-26 01:11:56 +0100 |
commit | f556c80d0230056335632b60c71f1567e177239e (patch) | |
tree | 748aa6be62b93a8e941e25dbd83f39e1dbb37035 /Ryujinx.Audio | |
parent | 1c49089ff00fc87dc4872f135dc6a0d36169a970 (diff) |
Haydn: Part 1 (#2007)
* Haydn: Part 1
Based on my reverse of audio 11.0.0.
As always, core implementation under LGPLv3 for the same reasons as for Amadeus.
This place the bases of a more flexible audio system while making audout & audin accurate.
This have the following improvements:
- Complete reimplementation of audout and audin.
- Audin currently only have a dummy backend.
- Dramatically reduce CPU usage by up to 50% in common cases (SoundIO and OpenAL).
- Audio Renderer now can output to 5.1 devices when supported.
- Audio Renderer init its backend on demand instead of keeping two up all the time.
- All backends implementation are now in their own project.
- Ryujinx.Audio.Renderer was renamed Ryujinx.Audio and was refactored because of this.
As a note, games having issues with OpenAL haven't improved and will not
because of OpenAL design (stopping when buffers finish playing causing
possible audio "pops" when buffers are very small).
* Update for latest hexkyz's edits on Switchbrew
* audren: Rollback channel configuration changes
* Address gdkchan's comments
* Fix typo in OpenAL backend driver
* Address last comments
* Fix a nit
* Address gdkchan's comments
Diffstat (limited to 'Ryujinx.Audio')
212 files changed, 24956 insertions, 4569 deletions
diff --git a/Ryujinx.Audio/AudioManager.cs b/Ryujinx.Audio/AudioManager.cs new file mode 100644 index 00000000..ab25150a --- /dev/null +++ b/Ryujinx.Audio/AudioManager.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Threading; + +namespace Ryujinx.Audio +{ + /// <summary> + /// Manage audio input and output system. + /// </summary> + public class AudioManager : IDisposable + { + /// <summary> + /// Lock used to control the waiters registration. + /// </summary> + private object _lock = new object(); + + /// <summary> + /// Events signaled when the driver played audio buffers. + /// </summary> + private ManualResetEvent[] _updateRequiredEvents; + + /// <summary> + /// Action to execute when the driver played audio buffers. + /// </summary> + private Action[] _actions; + + /// <summary> + /// The worker thread in charge of handling sessions update. + /// </summary> + private Thread _workerThread; + + /// <summary> + /// Create a new <see cref="AudioManager"/>. + /// </summary> + public AudioManager() + { + _updateRequiredEvents = new ManualResetEvent[2]; + _actions = new Action[2]; + + // Termination event. + _updateRequiredEvents[1] = new ManualResetEvent(false); + + _workerThread = new Thread(Update) + { + Name = "AudioManager.Worker" + }; + } + + /// <summary> + /// Start the <see cref="AudioManager"/>. + /// </summary> + public void Start() + { + if (_workerThread.IsAlive) + { + throw new InvalidOperationException(); + } + + _workerThread.Start(); + } + + /// <summary> + /// Initialize update handlers. + /// </summary> + /// <param name="updatedRequiredEvent ">The driver event that will get signaled by the device driver when an audio buffer finished playing/being captured</param> + /// <param name="outputCallback">The callback to call when an audio buffer finished playing</param> + /// <param name="inputCallback">The callback to call when an audio buffer was captured</param> + public void Initialize(ManualResetEvent updatedRequiredEvent, Action outputCallback, Action inputCallback) + { + lock (_lock) + { + _updateRequiredEvents[0] = updatedRequiredEvent; + _actions[0] = outputCallback; + _actions[1] = inputCallback; + } + } + + /// <summary> + /// Entrypoint of the <see cref="_workerThread"/> in charge of updating the <see cref="AudioManager"/>. + /// </summary> + private void Update() + { + while (true) + { + int index = WaitHandle.WaitAny(_updateRequiredEvents); + + // Last index is here to indicate thread termination. + if (index + 1 == _updateRequiredEvents.Length) + { + break; + } + + lock (_lock) + { + foreach (Action action in _actions) + { + action?.Invoke(); + } + + _updateRequiredEvents[0].Reset(); + } + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _updateRequiredEvents[1].Set(); + _workerThread.Join(); + + _updateRequiredEvents[1].Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Backends/Common/BackendHelper.cs b/Ryujinx.Audio/Backends/Common/BackendHelper.cs new file mode 100644 index 00000000..df81d942 --- /dev/null +++ b/Ryujinx.Audio/Backends/Common/BackendHelper.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using System; + +namespace Ryujinx.Audio.Backends.Common +{ + public static class BackendHelper + { + public static int GetSampleSize(SampleFormat format) + { + return format switch + { + SampleFormat.PcmInt8 => sizeof(byte), + SampleFormat.PcmInt16 => sizeof(ushort), + SampleFormat.PcmInt24 => 3, + SampleFormat.PcmInt32 => sizeof(int), + SampleFormat.PcmFloat => sizeof(float), + _ => throw new ArgumentException($"{format}"), + }; + } + + public static int GetSampleCount(SampleFormat format, int channelCount, int bufferSize) + { + return bufferSize / GetSampleSize(format) / channelCount; + } + } +} diff --git a/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs new file mode 100644 index 00000000..b4f5f812 --- /dev/null +++ b/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs @@ -0,0 +1,183 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common; +using System; + +namespace Ryujinx.Audio.Backends.Common +{ + /// <summary> + /// A ring buffer that grow if data written to it is too big to fit. + /// </summary> + public class DynamicRingBuffer + { + private const int RingBufferAlignment = 2048; + + private object _lock = new object(); + + private byte[] _buffer; + private int _size; + private int _headOffset; + private int _tailOffset; + + public int Length => _size; + + public DynamicRingBuffer(int initialCapacity = RingBufferAlignment) + { + _buffer = new byte[initialCapacity]; + } + + public void Clear() + { + _size = 0; + _headOffset = 0; + _tailOffset = 0; + } + + public void Clear(int size) + { + lock (_lock) + { + if (size > _size) + { + size = _size; + } + + if (size == 0) + { + return; + } + + _headOffset = (_headOffset + size) % _buffer.Length; + _size -= size; + + if (_size == 0) + { + _headOffset = 0; + _tailOffset = 0; + } + } + } + + private void SetCapacityLocked(int capacity) + { + byte[] buffer = new byte[capacity]; + + if (_size > 0) + { + if (_headOffset < _tailOffset) + { + Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size); + } + else + { + Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset); + Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset); + } + } + + _buffer = buffer; + _headOffset = 0; + _tailOffset = _size; + } + + + public void Write<T>(T[] buffer, int index, int count) + { + if (count == 0) + { + return; + } + + lock (_lock) + { + if ((_size + count) > _buffer.Length) + { + SetCapacityLocked(BitUtils.AlignUp(_size + count, RingBufferAlignment)); + } + + if (_headOffset < _tailOffset) + { + int tailLength = _buffer.Length - _tailOffset; + + if (tailLength >= count) + { + Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count); + } + else + { + Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength); + Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength); + } + } + else + { + Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count); + } + + _size += count; + _tailOffset = (_tailOffset + count) % _buffer.Length; + } + } + + public int Read<T>(T[] buffer, int index, int count) + { + lock (_lock) + { + if (count > _size) + { + count = _size; + } + + if (count == 0) + { + return 0; + } + + if (_headOffset < _tailOffset) + { + Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count); + } + else + { + int tailLength = _buffer.Length - _headOffset; + + if (tailLength >= count) + { + Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count); + } + else + { + Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength); + Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength); + } + } + + _size -= count; + _headOffset = (_headOffset + count) % _buffer.Length; + + if (_size == 0) + { + _headOffset = 0; + _tailOffset = 0; + } + + return count; + } + } + } +} diff --git a/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs b/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs new file mode 100644 index 00000000..1e000e4c --- /dev/null +++ b/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; + +namespace Ryujinx.Audio.Backends.Common +{ + public abstract class HardwareDeviceSessionOutputBase : IHardwareDeviceSession + { + public IVirtualMemoryManager MemoryManager { get; } + public SampleFormat RequestedSampleFormat { get; } + public uint RequestedSampleRate { get; } + public uint RequestedChannelCount { get; } + + public HardwareDeviceSessionOutputBase(IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) + { + MemoryManager = memoryManager; + RequestedSampleFormat = requestedSampleFormat; + RequestedSampleRate = requestedSampleRate; + RequestedChannelCount = requestedChannelCount; + } + + private byte[] GetBufferSamples(AudioBuffer buffer) + { + if (buffer.DataPointer == 0) + { + return null; + } + + byte[] data = new byte[buffer.DataSize]; + + MemoryManager.Read(buffer.DataPointer, data); + + return data; + } + + protected ulong GetSampleCount(AudioBuffer buffer) + { + return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, (int)buffer.DataSize); + } + + public abstract void Dispose(); + public abstract void PrepareToClose(); + public abstract void QueueBuffer(AudioBuffer buffer); + public abstract void SetVolume(float volume); + public abstract float GetVolume(); + public abstract void Start(); + public abstract void Stop(); + public abstract ulong GetPlayedSampleCount(); + public abstract bool WasBufferFullyConsumed(AudioBuffer buffer); + public virtual bool RegisterBuffer(AudioBuffer buffer) + { + return RegisterBuffer(buffer, GetBufferSamples(buffer)); + } + + public virtual bool RegisterBuffer(AudioBuffer buffer, byte[] samples) + { + if (samples == null) + { + return false; + } + + if (buffer.Data == null) + { + buffer.Data = samples; + } + + return true; + } + + public virtual void UnregisterBuffer(AudioBuffer buffer) { } + } +} diff --git a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs new file mode 100644 index 00000000..c0305f8a --- /dev/null +++ b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Backends.Dummy; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Threading; + +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.CompatLayer +{ + public class CompatLayerHardwareDeviceDriver : IHardwareDeviceDriver + { + private IHardwareDeviceDriver _realDriver; + + public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice) + { + _realDriver = realDevice; + } + + public void Dispose() + { + _realDriver.Dispose(); + } + + public ManualResetEvent GetUpdateRequiredEvent() + { + return _realDriver.GetUpdateRequiredEvent(); + } + + private uint SelectHardwareChannelCount(uint targetChannelCount) + { + if (_realDriver.SupportsChannelCount(targetChannelCount)) + { + return targetChannelCount; + } + + return targetChannelCount switch + { + 6 => SelectHardwareChannelCount(2), + 2 => SelectHardwareChannelCount(1), + 1 => throw new ArgumentException("No valid channel configuration found!"), + _ => throw new ArgumentException($"Invalid targetChannelCount {targetChannelCount}") + }; + } + + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) + { + if (channelCount == 0) + { + channelCount = 2; + } + + if (sampleRate == 0) + { + sampleRate = Constants.TargetSampleRate; + } + + if (!_realDriver.SupportsDirection(direction)) + { + if (direction == Direction.Input) + { + Logger.Warning?.Print(LogClass.Audio, "The selected audio backend doesn't support audio input, fallback to dummy..."); + + return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount); + } + + throw new NotImplementedException(); + } + + uint hardwareChannelCount = SelectHardwareChannelCount(channelCount); + + IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, hardwareChannelCount); + + if (hardwareChannelCount == channelCount) + { + return realSession; + } + + if (direction == Direction.Input) + { + Logger.Warning?.Print(LogClass.Audio, $"The selected audio backend doesn't support the requested audio input configuration, fallback to dummy..."); + + // TODO: We currently don't support audio input upsampling/downsampling, implement this. + realSession.Dispose(); + + return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount); + } + + // It must be a HardwareDeviceSessionOutputBase. + if (realSession is not HardwareDeviceSessionOutputBase realSessionOutputBase) + { + throw new InvalidOperationException($"Real driver session class type isn't based on {typeof(HardwareDeviceSessionOutputBase).Name}."); + } + + // If we need to do post processing before sending to the hardware device, wrap around it. + return new CompatLayerHardwareDeviceSession(realSessionOutputBase, channelCount); + } + + public bool SupportsChannelCount(uint channelCount) + { + return channelCount == 1 || channelCount == 2 || channelCount == 6; + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + // TODO: More formats. + return sampleFormat == SampleFormat.PcmInt16; + } + + public bool SupportsSampleRate(uint sampleRate) + { + // TODO: More sample rates. + return sampleRate == Constants.TargetSampleRate; + } + + public IHardwareDeviceDriver GetRealDeviceDriver() + { + return _realDriver; + } + + public bool SupportsDirection(Direction direction) + { + return direction == Direction.Input || direction == Direction.Output; + } + } +} diff --git a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs new file mode 100644 index 00000000..493d682f --- /dev/null +++ b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Backends.CompatLayer +{ + class CompatLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private HardwareDeviceSessionOutputBase _realSession; + private uint _userChannelCount; + + public CompatLayerHardwareDeviceSession(HardwareDeviceSessionOutputBase realSession, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount) + { + _realSession = realSession; + _userChannelCount = userChannelCount; + } + + public override void Dispose() + { + _realSession.Dispose(); + } + + public override ulong GetPlayedSampleCount() + { + return _realSession.GetPlayedSampleCount(); + } + + public override float GetVolume() + { + return _realSession.GetVolume(); + } + + public override void PrepareToClose() + { + _realSession.PrepareToClose(); + } + + public override void QueueBuffer(AudioBuffer buffer) + { + _realSession.QueueBuffer(buffer); + } + + public override bool RegisterBuffer(AudioBuffer buffer, byte[] samples) + { + if (RequestedSampleFormat != SampleFormat.PcmInt16) + { + throw new NotImplementedException("Downmixing formats other than PCM16 is not supported."); + } + + if (samples == null) + { + return false; + } + + short[] downmixedBufferPCM16; + + ReadOnlySpan<short> samplesPCM16 = MemoryMarshal.Cast<byte, short>(samples); + + if (_userChannelCount == 6) + { + downmixedBufferPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16); + + if (_realSession.RequestedChannelCount == 1) + { + downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(downmixedBufferPCM16); + } + } + else if (_userChannelCount == 2 && _realSession.RequestedChannelCount == 1) + { + downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16); + } + else + { + throw new NotImplementedException($"Downmixing from {_userChannelCount} to {_realSession.RequestedChannelCount} not implemented."); + } + + byte[] downmixedBuffer = MemoryMarshal.Cast<short, byte>(downmixedBufferPCM16).ToArray(); + + AudioBuffer fakeBuffer = new AudioBuffer + { + BufferTag = buffer.BufferTag, + DataPointer = buffer.DataPointer, + DataSize = (ulong)downmixedBuffer.Length + }; + + bool result = _realSession.RegisterBuffer(fakeBuffer, downmixedBuffer); + + if (result) + { + buffer.Data = fakeBuffer.Data; + buffer.DataSize = fakeBuffer.DataSize; + } + + return result; + } + + public override void SetVolume(float volume) + { + _realSession.SetVolume(volume); + } + + public override void Start() + { + _realSession.Start(); + } + + public override void Stop() + { + _realSession.Stop(); + } + + public override void UnregisterBuffer(AudioBuffer buffer) + { + _realSession.UnregisterBuffer(buffer); + } + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + return _realSession.WasBufferFullyConsumed(buffer); + } + } +} diff --git a/Ryujinx.Audio/Downmixing.cs b/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs index 03847737..ff4ba7a1 100644 --- a/Ryujinx.Audio/Downmixing.cs +++ b/Ryujinx.Audio/Backends/CompatLayer/Downmixing.cs @@ -1,8 +1,25 @@ -using System; +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Ryujinx.Audio +namespace Ryujinx.Audio.Backends.CompatLayer { public static class Downmixing { diff --git a/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs new file mode 100644 index 00000000..f24b359c --- /dev/null +++ b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using System.Threading; + +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.Dummy +{ + public class DummyHardwareDeviceDriver : IHardwareDeviceDriver + { + private ManualResetEvent _updateRequiredEvent; + + public DummyHardwareDeviceDriver() + { + _updateRequiredEvent = new ManualResetEvent(false); + } + + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount) + { + if (sampleRate == 0) + { + sampleRate = Constants.TargetSampleRate; + } + + if (channelCount == 0) + { + channelCount = 2; + } + + if (direction == Direction.Output) + { + return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount); + } + else + { + return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount); + } + } + + public ManualResetEvent GetUpdateRequiredEvent() + { + return _updateRequiredEvent; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // NOTE: The _updateRequiredEvent will be disposed somewhere else. + } + } + + public bool SupportsSampleRate(uint sampleRate) + { + return true; + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + return true; + } + + public bool SupportsDirection(Direction direction) + { + return direction == Direction.Output || direction == Direction.Input; + } + + public bool SupportsChannelCount(uint channelCount) + { + return channelCount == 1 || channelCount == 2 || channelCount == 6; + } + } +} diff --git a/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionInput.cs b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionInput.cs new file mode 100644 index 00000000..451a8564 --- /dev/null +++ b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionInput.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using System; + +namespace Ryujinx.Audio.Backends.Dummy +{ + class DummyHardwareDeviceSessionInput : IHardwareDeviceSession + { + private float _volume; + private IHardwareDeviceDriver _manager; + private IVirtualMemoryManager _memoryManager; + + public DummyHardwareDeviceSessionInput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) + { + _volume = 1.0f; + _manager = manager; + _memoryManager = memoryManager; + } + + public void Dispose() + { + // Nothing to do. + } + + public ulong GetPlayedSampleCount() + { + // Not implemented for input. + throw new NotSupportedException(); + } + + public float GetVolume() + { + return _volume; + } + + public void PrepareToClose() { } + + public void QueueBuffer(AudioBuffer buffer) + { + _memoryManager.Fill(buffer.DataPointer, buffer.DataSize, 0); + + _manager.GetUpdateRequiredEvent().Set(); + } + + public bool RegisterBuffer(AudioBuffer buffer) + { + return buffer.DataPointer != 0; + } + + public void SetVolume(float volume) + { + _volume = volume; + } + + public void Start() { } + + public void Stop() { } + + public void UnregisterBuffer(AudioBuffer buffer) { } + + public bool WasBufferFullyConsumed(AudioBuffer buffer) + { + return true; + } + } +} diff --git a/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs new file mode 100644 index 00000000..7cc19997 --- /dev/null +++ b/Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceSessionOutput.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using System.Threading; + +namespace Ryujinx.Audio.Backends.Dummy +{ + internal class DummyHardwareDeviceSessionOutput : HardwareDeviceSessionOutputBase + { + private float _volume; + private IHardwareDeviceDriver _manager; + + private ulong _playedSampleCount; + + public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount) + { + _volume = 1.0f; + _manager = manager; + } + + public override void Dispose() + { + // Nothing to do. + } + + public override ulong GetPlayedSampleCount() + { + return Interlocked.Read(ref _playedSampleCount); + } + + public override float GetVolume() + { + return _volume; + } + + public override void PrepareToClose() { } + + public override void QueueBuffer(AudioBuffer buffer) + { + Interlocked.Add(ref _playedSampleCount, GetSampleCount(buffer)); + + _manager.GetUpdateRequiredEvent().Set(); + } + + public override void SetVolume(float volume) + { + _volume = volume; + } + + public override void Start() { } + + public override void Stop() { } + + public override void UnregisterBuffer(AudioBuffer buffer) { } + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + return true; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Audio/Common/AudioBuffer.cs b/Ryujinx.Audio/Common/AudioBuffer.cs new file mode 100644 index 00000000..853896d5 --- /dev/null +++ b/Ryujinx.Audio/Common/AudioBuffer.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Represent an audio buffer that will be used by an <see cref="IHardwareDeviceSession"/>. + /// </summary> + public class AudioBuffer + { + /// <summary> + /// Unique tag of this buffer. + /// </summary> + /// <remarks>Unique per session</remarks> + public ulong BufferTag; + + /// <summary> + /// Pointer to the user samples. + /// </summary> + public ulong DataPointer; + + /// <summary> + /// Size of the user samples region. + /// </summary> + public ulong DataSize; + + /// <summary> + /// The timestamp at which the buffer was played. + /// </summary> + /// <remarks>Not used but useful for debugging</remarks> + public ulong PlayedTimestamp; + + /// <summary> + /// The user samples. + /// </summary> + public byte[] Data; + } +} diff --git a/Ryujinx.Audio/Common/AudioDeviceSession.cs b/Ryujinx.Audio/Common/AudioDeviceSession.cs new file mode 100644 index 00000000..fbdb5a75 --- /dev/null +++ b/Ryujinx.Audio/Common/AudioDeviceSession.cs @@ -0,0 +1,532 @@ +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Common; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// An audio device session. + /// </summary> + class AudioDeviceSession : IDisposable + { + /// <summary> + /// The volume of the <see cref="AudioDeviceSession"/>. + /// </summary> + private float _volume; + + /// <summary> + /// The state of the <see cref="AudioDeviceSession"/>. + /// </summary> + private AudioDeviceState _state; + + /// <summary> + /// Array of all buffers currently used or released. + /// </summary> + private AudioBuffer[] _buffers; + + /// <summary> + /// The server index inside <see cref="_buffers"/> (appended but not queued to device driver). + /// </summary> + private uint _serverBufferIndex; + + /// <summary> + /// The hardware index inside <see cref="_buffers"/> (queued to device driver). + /// </summary> + private uint _hardwareBufferIndex; + + /// <summary> + /// The released index inside <see cref="_buffers"/> (released by the device driver). + /// </summary> + private uint _releasedBufferIndex; + + /// <summary> + /// The count of buffer appended (server side). + /// </summary> + private uint _bufferAppendedCount; + + /// <summary> + /// The count of buffer registered (driver side). + /// </summary> + private uint _bufferRegisteredCount; + + /// <summary> + /// The count of buffer released (released by the driver side). + /// </summary> + private uint _bufferReleasedCount; + + /// <summary> + /// The released buffer event. + /// </summary> + private IWritableEvent _bufferEvent; + + /// <summary> + /// The session on the device driver. + /// </summary> + private IHardwareDeviceSession _hardwareDeviceSession; + + /// <summary> + /// Max number of buffers that can be registered to the device driver at a time. + /// </summary> + private uint _bufferRegisteredLimit; + + /// <summary> + /// Create a new <see cref="AudioDeviceSession"/>. + /// </summary> + /// <param name="deviceSession">The device driver session associated</param> + /// <param name="bufferEvent">The release buffer event</param> + /// <param name="bufferRegisteredLimit">The max number of buffers that can be registered to the device driver at a time</param> + public AudioDeviceSession(IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent, uint bufferRegisteredLimit = 4) + { + _bufferEvent = bufferEvent; + _hardwareDeviceSession = deviceSession; + _bufferRegisteredLimit = bufferRegisteredLimit; + + _buffers = new AudioBuffer[Constants.AudioDeviceBufferCountMax]; + _serverBufferIndex = 0; + _hardwareBufferIndex = 0; + _releasedBufferIndex = 0; + + _bufferAppendedCount = 0; + _bufferRegisteredCount = 0; + _bufferReleasedCount = 0; + _volume = 1.0f; + _state = AudioDeviceState.Stopped; + } + + /// <summary> + /// Get the released buffer event. + /// </summary> + /// <returns>The released buffer event</returns> + public IWritableEvent GetBufferEvent() + { + return _bufferEvent; + } + + /// <summary> + /// Get the state of the session. + /// </summary> + /// <returns>The state of the session</returns> + public AudioDeviceState GetState() + { + Debug.Assert(_state == AudioDeviceState.Started || _state == AudioDeviceState.Stopped); + + return _state; + } + + /// <summary> + /// Get the total buffer count (server + driver + released). + /// </summary> + /// <returns>Return the total buffer count</returns> + private uint GetTotalBufferCount() + { + uint bufferCount = _bufferAppendedCount + _bufferRegisteredCount + _bufferReleasedCount; + + Debug.Assert(bufferCount <= Constants.AudioDeviceBufferCountMax); + + return bufferCount; + } + + /// <summary> + /// Register a new <see cref="AudioBuffer"/> on the server side. + /// </summary> + /// <param name="buffer">The <see cref="AudioBuffer"/> to register</param> + /// <returns>True if the operation succeeded</returns> + private bool RegisterBuffer(AudioBuffer buffer) + { + if (GetTotalBufferCount() == Constants.AudioDeviceBufferCountMax) + { + return false; + } + + _buffers[_serverBufferIndex] = buffer; + _serverBufferIndex = (_serverBufferIndex + 1) % Constants.AudioDeviceBufferCountMax; + _bufferAppendedCount++; + + return true; + } + + /// <summary> + /// Flush server buffers to hardware. + /// </summary> + private void FlushToHardware() + { + uint bufferToFlushCount = Math.Min(Math.Min(_bufferAppendedCount, 4), _bufferRegisteredLimit - _bufferRegisteredCount); + + AudioBuffer[] buffersToFlush = new AudioBuffer[bufferToFlushCount]; + + uint hardwareBufferIndex = _hardwareBufferIndex; + + for (int i = 0; i < buffersToFlush.Length; i++) + { + buffersToFlush[i] = _buffers[_hardwareBufferIndex]; + + _bufferAppendedCount--; + _bufferRegisteredCount++; + + hardwareBufferIndex = (hardwareBufferIndex + 1) % Constants.AudioDeviceBufferCountMax; + } + + _hardwareBufferIndex = hardwareBufferIndex; + + for (int i = 0; i < buffersToFlush.Length; i++) + { + _hardwareDeviceSession.QueueBuffer(buffersToFlush[i]); + } + } + + /// <summary> + /// Get the current index of the <see cref="AudioBuffer"/> playing on the driver side. + /// </summary> + /// <param name="playingIndex">The output index of the <see cref="AudioBuffer"/> playing on the driver side</param> + /// <returns>True if any buffer is playing</returns> + private bool TryGetPlayingBufferIndex(out uint playingIndex) + { + if (_bufferRegisteredCount > 0) + { + playingIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax; + + return true; + } + + playingIndex = 0; + + return false; + } + + /// <summary> + /// Try to pop the <see cref="AudioBuffer"/> playing on the driver side. + /// </summary> + /// <param name="buffer">The output <see cref="AudioBuffer"/> playing on the driver side</param> + /// <returns>True if any buffer is playing</returns> + private bool TryPopPlayingBuffer(out AudioBuffer buffer) + { + if (_bufferRegisteredCount > 0) + { + uint bufferIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax; + + buffer = _buffers[bufferIndex]; + + _buffers[bufferIndex] = null; + + _bufferRegisteredCount--; + + return true; + } + + buffer = null; + + return false; + } + + /// <summary> + /// Try to pop a <see cref="AudioBuffer"/> released by the driver side. + /// </summary> + /// <param name="buffer">The output <see cref="AudioBuffer"/> released by the driver side</param> + /// <returns>True if any buffer has been released</returns> + public bool TryPopReleasedBuffer(out AudioBuffer buffer) + { + if (_bufferReleasedCount > 0) + { + uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax; + + buffer = _buffers[bufferIndex]; + + _buffers[bufferIndex] = null; + + _bufferReleasedCount--; + + return true; + } + + buffer = null; + + return false; + } + + /// <summary> + /// Release a <see cref="AudioBuffer"/>. + /// </summary> + /// <param name="buffer">The <see cref="AudioBuffer"/> to release</param> + private void ReleaseBuffer(AudioBuffer buffer) + { + buffer.PlayedTimestamp = (ulong)PerformanceCounter.ElapsedNanoseconds; + + _bufferRegisteredCount--; + _bufferReleasedCount++; + + _releasedBufferIndex = (_releasedBufferIndex + 1) % Constants.AudioDeviceBufferCountMax; + } + + /// <summary> + /// Update the released buffers. + /// </summary> + /// <param name="updateForStop">True if the session is currently stopping</param> + private void UpdateReleaseBuffers(bool updateForStop = false) + { + bool wasAnyBuffersReleased = false; + + while (TryGetPlayingBufferIndex(out uint playingIndex)) + { + if (!updateForStop && !_hardwareDeviceSession.WasBufferFullyConsumed(_buffers[playingIndex])) + { + break; + } + + if (updateForStop) + { + _hardwareDeviceSession.UnregisterBuffer(_buffers[playingIndex]); + } + + ReleaseBuffer(_buffers[playingIndex]); + + wasAnyBuffersReleased = true; + } + + if (wasAnyBuffersReleased) + { + _bufferEvent.Signal(); + } + } + + /// <summary> + /// Append a new <see cref="AudioBuffer"/>. + /// </summary> + /// <param name="buffer">The <see cref="AudioBuffer"/> to append</param> + /// <returns>True if the buffer was appended</returns> + public bool AppendBuffer(AudioBuffer buffer) + { + if (_hardwareDeviceSession.RegisterBuffer(buffer)) + { + if (RegisterBuffer(buffer)) + { + FlushToHardware(); + + return true; + } + + _hardwareDeviceSession.UnregisterBuffer(buffer); + } + + return false; + } + + public bool AppendUacBuffer(AudioBuffer buffer, uint handle) + { + // NOTE: On hardware, there is another RegisterBuffer method taking an handle. + // This variant of the call always return false (stubbed?) as a result this logic will never succeed. + + return false; + } + + /// <summary> + /// Start the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Start() + { + if (_state == AudioDeviceState.Started) + { + return ResultCode.OperationFailed; + } + + _hardwareDeviceSession.Start(); + + _state = AudioDeviceState.Started; + + FlushToHardware(); + + _hardwareDeviceSession.SetVolume(_volume); + + return ResultCode.Success; + } + + /// <summary> + /// Stop the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Stop() + { + if (_state == AudioDeviceState.Started) + { + _hardwareDeviceSession.Stop(); + + UpdateReleaseBuffers(true); + + _state = AudioDeviceState.Stopped; + } + + return ResultCode.Success; + } + + /// <summary> + /// Get the volume of the session. + /// </summary> + /// <returns>The volume of the session</returns> + public float GetVolume() + { + return _hardwareDeviceSession.GetVolume(); + } + + /// <summary> + /// Set the volume of the session. + /// </summary> + /// <param name="volume">The new volume to set</param> + public void SetVolume(float volume) + { + _volume = volume; + + if (_state == AudioDeviceState.Started) + { + _hardwareDeviceSession.SetVolume(volume); + } + } + + /// <summary> + /// Get the count of buffer currently in use (server + driver side). + /// </summary> + /// <returns>The count of buffer currently in use</returns> + public uint GetBufferCount() + { + return _bufferAppendedCount + _bufferRegisteredCount; + } + + /// <summary> + /// Check if a buffer is present. + /// </summary> + /// <param name="bufferTag">The unique tag of the buffer</param> + /// <returns>Return true if a buffer is present</returns> + public bool ContainsBuffer(ulong bufferTag) + { + uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax; + + for (int i = 0; i < GetTotalBufferCount(); i++) + { + if (_buffers[bufferIndex].BufferTag == bufferTag) + { + return true; + } + + bufferIndex = (bufferIndex + 1) % Constants.AudioDeviceBufferCountMax; + } + + return false; + } + + /// <summary> + /// Get the count of sample played in this session. + /// </summary> + /// <returns>The count of sample played in this session</returns> + public ulong GetPlayedSampleCount() + { + if (_state == AudioDeviceState.Stopped) + { + return 0; + } + else + { + return _hardwareDeviceSession.GetPlayedSampleCount(); + } + } + + /// <summary> + /// Flush all buffers to the initial state. + /// </summary> + /// <returns>True if any buffer was flushed</returns> + public bool FlushBuffers() + { + if (_state == AudioDeviceState.Stopped) + { + return false; + } + + uint bufferCount = GetBufferCount(); + + while (TryPopReleasedBuffer(out AudioBuffer buffer)) + { + _hardwareDeviceSession.UnregisterBuffer(buffer); + } + + while (TryPopPlayingBuffer(out AudioBuffer buffer)) + { + _hardwareDeviceSession.UnregisterBuffer(buffer); + } + + if (_bufferRegisteredCount == 0 || (_bufferReleasedCount + _bufferAppendedCount) > Constants.AudioDeviceBufferCountMax) + { + return false; + } + + _bufferReleasedCount += _bufferAppendedCount; + _releasedBufferIndex = (_releasedBufferIndex + _bufferAppendedCount) % Constants.AudioDeviceBufferCountMax; + _bufferAppendedCount = 0; + _hardwareBufferIndex = _serverBufferIndex; + + if (bufferCount > 0) + { + _bufferEvent.Signal(); + } + + return true; + } + + /// <summary> + /// Update the session. + /// </summary> + public void Update() + { + if (_state == AudioDeviceState.Started) + { + UpdateReleaseBuffers(); + FlushToHardware(); + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Tell the hardware session that we are ending. + _hardwareDeviceSession.PrepareToClose(); + + // Unregister all buffers + + while (TryPopReleasedBuffer(out AudioBuffer buffer)) + { + _hardwareDeviceSession.UnregisterBuffer(buffer); + } + + while (TryPopPlayingBuffer(out AudioBuffer buffer)) + { + _hardwareDeviceSession.UnregisterBuffer(buffer); + } + + // Finally dispose hardware session. + _hardwareDeviceSession.Dispose(); + + _bufferEvent.Signal(); + } + } + } +} diff --git a/Ryujinx.Audio/Common/AudioDeviceState.cs b/Ryujinx.Audio/Common/AudioDeviceState.cs new file mode 100644 index 00000000..77604525 --- /dev/null +++ b/Ryujinx.Audio/Common/AudioDeviceState.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Audio device state. + /// </summary> + public enum AudioDeviceState : uint + { + /// <summary> + /// The audio device is started. + /// </summary> + Started, + + /// <summary> + /// The audio device is stopped. + /// </summary> + Stopped + } +} diff --git a/Ryujinx.Audio/Common/AudioInputConfiguration.cs b/Ryujinx.Audio/Common/AudioInputConfiguration.cs new file mode 100644 index 00000000..9aa86569 --- /dev/null +++ b/Ryujinx.Audio/Common/AudioInputConfiguration.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Audio user input configuration. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AudioInputConfiguration + { + /// <summary> + /// The target sample rate of the user. + /// </summary> + /// <remarks>Only 48000Hz is considered valid, other sample rates will be refused.</remarks> + public uint SampleRate; + + /// <summary> + /// The target channel count of the user. + /// </summary> + /// <remarks>Only Stereo and Surround are considered valid, other configurations will be refused.</remarks> + /// <remarks>Not used in audin.</remarks> + public ushort ChannelCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private ushort _reserved; + } +} diff --git a/Ryujinx.Audio/Common/AudioOutputConfiguration.cs b/Ryujinx.Audio/Common/AudioOutputConfiguration.cs new file mode 100644 index 00000000..a493561f --- /dev/null +++ b/Ryujinx.Audio/Common/AudioOutputConfiguration.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Audio system output configuration. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AudioOutputConfiguration + { + /// <summary> + /// The target sample rate of the system. + /// </summary> + public uint SampleRate; + + /// <summary> + /// The target channel count of the system. + /// </summary> + public uint ChannelCount; + + /// <summary> + /// Reserved/unused + /// </summary> + public SampleFormat SampleFormat; + + /// <summary> + /// Reserved/unused. + /// </summary> + private Array3<byte> _padding; + + /// <summary> + /// The initial audio system state. + /// </summary> + public AudioDeviceState AudioOutState; + } +} diff --git a/Ryujinx.Audio/Common/AudioUserBuffer.cs b/Ryujinx.Audio/Common/AudioUserBuffer.cs new file mode 100644 index 00000000..a0614184 --- /dev/null +++ b/Ryujinx.Audio/Common/AudioUserBuffer.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Audio user buffer. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AudioUserBuffer + { + /// <summary> + /// Pointer to the next buffer (ignored). + /// </summary> + public ulong NextBuffer; + + /// <summary> + /// Pointer to the user samples. + /// </summary> + public ulong Data; + + /// <summary> + /// Capacity of the buffer (unused). + /// </summary> + public ulong Capacity; + + /// <summary> + /// Size of the user samples region. + /// </summary> + public ulong DataSize; + + /// <summary> + /// Offset in the user samples region (unused). + /// </summary> + public ulong DataOffset; + } +} diff --git a/Ryujinx.Audio/Common/SampleFormat.cs b/Ryujinx.Audio/Common/SampleFormat.cs new file mode 100644 index 00000000..bb51bdd3 --- /dev/null +++ b/Ryujinx.Audio/Common/SampleFormat.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Common +{ + /// <summary> + /// Sample format definition. + /// </summary> + public enum SampleFormat : byte + { + /// <summary> + /// Invalid sample format. + /// </summary> + Invalid = 0, + + /// <summary> + /// PCM8 sample format. (unsupported) + /// </summary> + PcmInt8 = 1, + + /// <summary> + /// PCM16 sample format. + /// </summary> + PcmInt16 = 2, + + /// <summary> + /// PCM24 sample format. (unsupported) + /// </summary> + PcmInt24 = 3, + + /// <summary> + /// PCM32 sample format. + /// </summary> + PcmInt32 = 4, + + /// <summary> + /// PCM Float sample format. + /// </summary> + PcmFloat = 5, + + /// <summary> + /// ADPCM sample format. (Also known as GC-ADPCM) + /// </summary> + Adpcm = 6 + } +} diff --git a/Ryujinx.Audio/Constants.cs b/Ryujinx.Audio/Constants.cs new file mode 100644 index 00000000..7a407fc6 --- /dev/null +++ b/Ryujinx.Audio/Constants.cs @@ -0,0 +1,192 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio +{ + /// <summary> + /// Define constants used by the audio system. + /// </summary> + public static class Constants + { + /// <summary> + /// The default device output name. + /// </summary> + public const string DefaultDeviceOutputName = "DeviceOut"; + + /// <summary> + /// The default device input name. + /// </summary> + public const string DefaultDeviceInputName = "BuiltInHeadset"; + + /// <summary> + /// The maximum number of channels supported. (6 channels for 5.1 surround) + /// </summary> + public const int ChannelCountMax = 6; + + /// <summary> + /// The maximum number of channels supported per voice. + /// </summary> + public const int VoiceChannelCountMax = ChannelCountMax; + + /// <summary> + /// The maximum count of mix buffer supported per operations (volumes, mix effect, ...) + /// </summary> + public const int MixBufferCountMax = 24; + + /// <summary> + /// The maximum count of wavebuffer per voice. + /// </summary> + public const int VoiceWaveBufferCount = 4; + + /// <summary> + /// The maximum count of biquad filter per voice. + /// </summary> + public const int VoiceBiquadFilterCount = 2; + + /// <summary> + /// The lowest priority that a voice can have. + /// </summary> + public const int VoiceLowestPriority = 0xFF; + + /// <summary> + /// The highest priority that a voice can have. + /// </summary> + /// <remarks>Voices with the highest priority will not be dropped if a voice drop needs to occur.</remarks> + public const int VoiceHighestPriority = 0; + + /// <summary> + /// Maximum <see cref="Common.BehaviourParameter.ErrorInfo"/> that can be returned by <see cref="Parameter.BehaviourErrorInfoOutStatus"/>. + /// </summary> + public const int MaxErrorInfos = 10; + + /// <summary> + /// Default alignment for buffers. + /// </summary> + public const int BufferAlignment = 0x40; + + /// <summary> + /// Alignment required for the work buffer. + /// </summary> + public const int WorkBufferAlignment = 0x1000; + + /// <summary> + /// Alignment required for every performance metrics frame. + /// </summary> + public const int PerformanceMetricsPerFramesSizeAlignment = 0x100; + + /// <summary> + /// The id of the final mix. + /// </summary> + public const int FinalMixId = 0; + + /// <summary> + /// The id defining an unused mix id. + /// </summary> + public const int UnusedMixId = int.MaxValue; + + /// <summary> + /// The id defining an unused splitter id as a signed integer. + /// </summary> + public const int UnusedSplitterIdInt = -1; + + /// <summary> + /// The id defining an unused splitter id. + /// </summary> + public const uint UnusedSplitterId = uint.MaxValue; + + /// <summary> + /// The id of invalid/unused node id. + /// </summary> + public const int InvalidNodeId = -268435456; + + /// <summary> + /// The indice considered invalid for processing order. + /// </summary> + public const int InvalidProcessingOrder = -1; + + /// <summary> + /// The maximum number of audio renderer sessions allowed to be created system wide. + /// </summary> + public const int AudioRendererSessionCountMax = 2; + + /// <summary> + /// The maximum number of audio output sessions allowed to be created system wide. + /// </summary> + public const int AudioOutSessionCountMax = 12; + + /// <summary> + /// The maximum number of audio input sessions allowed to be created system wide. + /// </summary> + public const int AudioInSessionCountMax = 4; + + /// <summary> + /// Maximum buffers supported by one audio device session. + /// </summary> + public const int AudioDeviceBufferCountMax = 32; + + /// <summary> + /// The target sample rate of the audio renderer. (48kHz) + /// </summary> + public const uint TargetSampleRate = 48000; + + /// <summary> + /// The target sample size of the audio renderer. (PCM16) + /// </summary> + public const int TargetSampleSize = sizeof(ushort); + + /// <summary> + /// The target sample count per audio renderer update. + /// </summary> + public const int TargetSampleCount = 240; + + /// <summary> + /// The size of an upsampler entry to process upsampling to <see cref="TargetSampleRate"/>. + /// </summary> + public const int UpSampleEntrySize = TargetSampleCount * VoiceChannelCountMax; + + /// <summary> + /// The target audio latency computed from <see cref="TargetSampleRate"/> and <see cref="TargetSampleCount"/>. + /// </summary> + public const int AudioProcessorMaxUpdateTimeTarget = 1000000000 / ((int)TargetSampleRate / TargetSampleCount); // 5.00 ms + + /// <summary> + /// The maximum update time of the DSP on original hardware. + /// </summary> + public const int AudioProcessorMaxUpdateTime = 5760000; // 5.76 ms + + /// <summary> + /// The maximum update time per audio renderer session. + /// </summary> + public const int AudioProcessorMaxUpdateTimePerSessions = AudioProcessorMaxUpdateTime / AudioRendererSessionCountMax; + + /// <summary> + /// Guest timer frequency used for system ticks. + /// </summary> + public const int TargetTimerFrequency = 19200000; + + /// <summary> + /// The default coefficients used for standard 5.1 surround to stereo downmixing. + /// </summary> + public static float[] DefaultSurroundToStereoCoefficients = new float[4] + { + 1.0f, + 0.707f, + 0.251f, + 0.707f, + }; + } +} diff --git a/Ryujinx.Audio/IAalOutput.cs b/Ryujinx.Audio/IAalOutput.cs deleted file mode 100644 index ddb1492c..00000000 --- a/Ryujinx.Audio/IAalOutput.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace Ryujinx.Audio -{ - public interface IAalOutput : IDisposable - { - bool SupportsChannelCount(int channels); - - private int SelectHardwareChannelCount(int targetChannelCount) - { - if (SupportsChannelCount(targetChannelCount)) - { - return targetChannelCount; - } - - return targetChannelCount switch - { - 6 => SelectHardwareChannelCount(2), - 2 => SelectHardwareChannelCount(1), - 1 => throw new ArgumentException("No valid channel configuration found!"), - _ => throw new ArgumentException($"Invalid targetChannelCount {targetChannelCount}"), - }; - } - - int OpenTrack(int sampleRate, int channels, ReleaseCallback callback) - { - return OpenHardwareTrack(sampleRate, SelectHardwareChannelCount(channels), channels, callback); - } - - int OpenHardwareTrack(int sampleRate, int hardwareChannels, int virtualChannels, ReleaseCallback callback); - - void CloseTrack(int trackId); - - bool ContainsBuffer(int trackId, long bufferTag); - - long[] GetReleasedBuffers(int trackId, int maxCount); - - void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer) where T : struct; - - void Start(int trackId); - - void Stop(int trackId); - - uint GetBufferCount(int trackId); - - ulong GetPlayedSampleCount(int trackId); - - bool FlushBuffers(int trackId); - - float GetVolume(int trackId); - - void SetVolume(int trackId, float volume); - - PlaybackState GetState(int trackId); - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Input/AudioInputManager.cs b/Ryujinx.Audio/Input/AudioInputManager.cs new file mode 100644 index 00000000..e098ae9e --- /dev/null +++ b/Ryujinx.Audio/Input/AudioInputManager.cs @@ -0,0 +1,262 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Input +{ + /// <summary> + /// The audio input manager. + /// </summary> + public class AudioInputManager : IDisposable + { + private object _lock = new object(); + + /// <summary> + /// Lock used for session allocation. + /// </summary> + private object _sessionLock = new object(); + + /// <summary> + /// The session ids allocation table. + /// </summary> + private int[] _sessionIds; + + /// <summary> + /// The device driver. + /// </summary> + private IHardwareDeviceDriver _deviceDriver; + + /// <summary> + /// The events linked to each session. + /// </summary> + private IWritableEvent[] _sessionsBufferEvents; + + /// <summary> + /// The <see cref="AudioInputSystem"/> session instances. + /// </summary> + private AudioInputSystem[] _sessions; + + /// <summary> + /// The count of active sessions. + /// </summary> + private int _activeSessionCount; + + /// <summary> + /// Create a new <see cref="AudioInputManager"/>. + /// </summary> + public AudioInputManager() + { + _sessionIds = new int[Constants.AudioInSessionCountMax]; + _sessions = new AudioInputSystem[Constants.AudioInSessionCountMax]; + _activeSessionCount = 0; + + for (int i = 0; i < _sessionIds.Length; i++) + { + _sessionIds[i] = i; + } + } + + /// <summary> + /// Initialize the <see cref="AudioInputManager"/>. + /// </summary> + /// <param name="deviceDriver">The device driver.</param> + /// <param name="sessionRegisterEvents">The events associated to each session.</param> + public void Initialize(IHardwareDeviceDriver deviceDriver, IWritableEvent[] sessionRegisterEvents) + { + _deviceDriver = deviceDriver; + _sessionsBufferEvents = sessionRegisterEvents; + } + + /// <summary> + /// Acquire a new session id. + /// </summary> + /// <returns>A new session id.</returns> + private int AcquireSessionId() + { + lock (_sessionLock) + { + int index = _activeSessionCount; + + Debug.Assert(index < _sessionIds.Length); + + int sessionId = _sessionIds[index]; + + _sessionIds[index] = -1; + + _activeSessionCount++; + + Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new input ({sessionId})"); + + return sessionId; + } + } + + /// <summary> + /// Release a given <paramref name="sessionId"/>. + /// </summary> + /// <param name="sessionId">The session id to release.</param> + private void ReleaseSessionId(int sessionId) + { + lock (_sessionLock) + { + Debug.Assert(_activeSessionCount > 0); + + int newIndex = --_activeSessionCount; + + _sessionIds[newIndex] = sessionId; + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered input ({sessionId})"); + } + + /// <summary> + /// Used to update audio input system. + /// </summary> + public void Update() + { + lock (_sessionLock) + { + foreach (AudioInputSystem input in _sessions) + { + input?.Update(); + } + } + } + + /// <summary> + /// Register a new <see cref="AudioInputSystem"/>. + /// </summary> + /// <param name="input">The <see cref="AudioInputSystem"/> to register.</param> + private void Register(AudioInputSystem input) + { + lock (_sessionLock) + { + _sessions[input.GetSessionId()] = input; + } + } + + /// <summary> + /// Unregister a new <see cref="AudioInputSystem"/>. + /// </summary> + /// <param name="input">The <see cref="AudioInputSystem"/> to unregister.</param> + internal void Unregister(AudioInputSystem input) + { + lock (_sessionLock) + { + int sessionId = input.GetSessionId(); + + _sessions[input.GetSessionId()] = null; + + ReleaseSessionId(sessionId); + } + } + + /// <summary> + /// Get the list of all audio inputs names. + /// </summary> + /// <param name="filtered">If true, filter disconnected devices</param> + /// <returns>The list of all audio inputs name</returns> + public string[] ListAudioIns(bool filtered) + { + if (filtered) + { + // TODO: Detect if the driver supports audio input + } + + return new string[] { Constants.DefaultDeviceInputName }; + } + + /// <summary> + /// Open a new <see cref="AudioInputSystem"/>. + /// </summary> + /// <param name="outputDeviceName">The output device name selected by the <see cref="AudioInputSystem"/></param> + /// <param name="outputConfiguration">The output audio configuration selected by the <see cref="AudioInputSystem"/></param> + /// <param name="obj">The new <see cref="AudioInputSystem"/></param> + /// <param name="memoryManager">The memory manager that will be used for all guest memory operations</param> + /// <param name="inputDeviceName">The input device name wanted by the user</param> + /// <param name="sampleFormat">The sample format to use</param> + /// <param name="parameter">The user configuration</param> + /// <param name="appletResourceUserId">The applet resource user id of the application</param> + /// <param name="processHandle">The process handle of the application</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode OpenAudioIn(out string outputDeviceName, + out AudioOutputConfiguration outputConfiguration, + out AudioInputSystem obj, + IVirtualMemoryManager memoryManager, + string inputDeviceName, + SampleFormat sampleFormat, + ref AudioInputConfiguration parameter, + ulong appletResourceUserId, + uint processHandle) + { + int sessionId = AcquireSessionId(); + + _sessionsBufferEvents[sessionId].Clear(); + + IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Input, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount); + + AudioInputSystem audioIn = new AudioInputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]); + + ResultCode result = audioIn.Initialize(inputDeviceName, sampleFormat, ref parameter, sessionId); + + if (result == ResultCode.Success) + { + outputDeviceName = audioIn.DeviceName; + outputConfiguration = new AudioOutputConfiguration + { + ChannelCount = audioIn.ChannelCount, + SampleFormat = audioIn.SampleFormat, + SampleRate = audioIn.SampleRate, + AudioOutState = audioIn.GetState(), + }; + + obj = audioIn; + + Register(audioIn); + } + else + { + ReleaseSessionId(sessionId); + + obj = null; + outputDeviceName = null; + outputConfiguration = default; + } + + return result; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Nothing to do here. + } + } + } +} diff --git a/Ryujinx.Audio/Input/AudioInputSystem.cs b/Ryujinx.Audio/Input/AudioInputSystem.cs new file mode 100644 index 00000000..8064a947 --- /dev/null +++ b/Ryujinx.Audio/Input/AudioInputSystem.cs @@ -0,0 +1,400 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using System; + +namespace Ryujinx.Audio.Input +{ + /// <summary> + /// Audio input system. + /// </summary> + public class AudioInputSystem : IDisposable + { + /// <summary> + /// The session id associated to the <see cref="AudioInputSystem"/>. + /// </summary> + private int _sessionId; + + /// <summary> + /// The session the <see cref="AudioInputSystem"/>. + /// </summary> + private AudioDeviceSession _session; + + /// <summary> + /// The target device name of the <see cref="AudioInputSystem"/>. + /// </summary> + public string DeviceName { get; private set; } + + /// <summary> + /// The target sample rate of the <see cref="AudioInputSystem"/>. + /// </summary> + public uint SampleRate { get; private set; } + + /// <summary> + /// The target channel count of the <see cref="AudioInputSystem"/>. + /// </summary> + public uint ChannelCount { get; private set; } + + /// <summary> + /// The target sample format of the <see cref="AudioInputSystem"/>. + /// </summary> + public SampleFormat SampleFormat { get; private set; } + + /// <summary> + /// The <see cref="AudioInputManager"/> owning this. + /// </summary> + private AudioInputManager _manager; + + /// <summary> + /// THe lock of the parent. + /// </summary> + private object _parentLock; + + /// <summary> + /// Create a new <see cref="AudioInputSystem"/>. + /// </summary> + /// <param name="manager">The manager instance</param> + /// <param name="parentLock">The lock of the manager</param> + /// <param name="deviceSession">The hardware device session</param> + /// <param name="bufferEvent">The buffer release event of the audio input</param> + public AudioInputSystem(AudioInputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent) + { + _manager = manager; + _parentLock = parentLock; + _session = new AudioDeviceSession(deviceSession, bufferEvent); + } + + /// <summary> + /// Get the default device name on the system. + /// </summary> + /// <returns>The default device name on the system.</returns> + private static string GetDeviceDefaultName() + { + return Constants.DefaultDeviceInputName; + } + + /// <summary> + /// Check if a given configuration and device name is valid on the system. + /// </summary> + /// <param name="configuration">The configuration to check.</param> + /// <param name="deviceName">The device name to check.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + private static ResultCode IsConfigurationValid(ref AudioInputConfiguration configuration, string deviceName) + { + if (deviceName.Length != 0 && !deviceName.Equals(GetDeviceDefaultName())) + { + return ResultCode.DeviceNotFound; + } + else if (configuration.SampleRate != 0 && configuration.SampleRate != Constants.TargetSampleRate) + { + return ResultCode.UnsupportedSampleRate; + } + else if (configuration.ChannelCount != 0 && configuration.ChannelCount != 1 && configuration.ChannelCount != 2 && configuration.ChannelCount != 6) + { + return ResultCode.UnsupportedChannelConfiguration; + } + + return ResultCode.Success; + } + + /// <summary> + /// Get the released buffer event. + /// </summary> + /// <returns>The released buffer event</returns> + public IWritableEvent RegisterBufferEvent() + { + lock (_parentLock) + { + return _session.GetBufferEvent(); + } + } + + /// <summary> + /// Update the <see cref="AudioInputSystem"/>. + /// </summary> + public void Update() + { + lock (_parentLock) + { + _session.Update(); + } + } + + /// <summary> + /// Get the id of this session. + /// </summary> + /// <returns>The id of this session</returns> + public int GetSessionId() + { + return _sessionId; + } + + /// <summary> + /// Initialize the <see cref="AudioInputSystem"/>. + /// </summary> + /// <param name="inputDeviceName">The input device name wanted by the user</param> + /// <param name="sampleFormat">The sample format to use</param> + /// <param name="parameter">The user configuration</param> + /// <param name="sessionId">The session id associated to this <see cref="AudioInputSystem"/></param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode Initialize(string inputDeviceName, SampleFormat sampleFormat, ref AudioInputConfiguration parameter, int sessionId) + { + _sessionId = sessionId; + + ResultCode result = IsConfigurationValid(ref parameter, inputDeviceName); + + if (result == ResultCode.Success) + { + if (inputDeviceName.Length == 0) + { + DeviceName = GetDeviceDefaultName(); + } + else + { + DeviceName = inputDeviceName; + } + + if (parameter.ChannelCount == 6) + { + ChannelCount = 6; + } + else + { + ChannelCount = 2; + } + + SampleFormat = sampleFormat; + SampleRate = Constants.TargetSampleRate; + } + + return result; + } + + /// <summary> + /// Append a new audio buffer to the audio input. + /// </summary> + /// <param name="bufferTag">The unique tag of this buffer.</param> + /// <param name="userBuffer">The buffer informations.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer) + { + lock (_parentLock) + { + AudioBuffer buffer = new AudioBuffer + { + BufferTag = bufferTag, + DataPointer = userBuffer.Data, + DataSize = userBuffer.DataSize + }; + + if (_session.AppendBuffer(buffer)) + { + return ResultCode.Success; + } + + return ResultCode.BufferRingFull; + } + } + + /// <summary> + /// Append a new audio buffer to the audio input. + /// </summary> + /// <remarks>This is broken by design, only added for completness.</remarks> + /// <param name="bufferTag">The unique tag of this buffer.</param> + /// <param name="userBuffer">The buffer informations.</param> + /// <param name="handle">Some unknown handle.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode AppendUacBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer, uint handle) + { + lock (_parentLock) + { + AudioBuffer buffer = new AudioBuffer + { + BufferTag = bufferTag, + DataPointer = userBuffer.Data, + DataSize = userBuffer.DataSize + }; + + if (_session.AppendUacBuffer(buffer, handle)) + { + return ResultCode.Success; + } + + return ResultCode.BufferRingFull; + } + } + + /// <summary> + /// Get the release buffers. + /// </summary> + /// <param name="releasedBuffers">The buffer to write the release buffers</param> + /// <param name="releasedCount">The count of released buffers</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode GetReleasedBuffers(Span<ulong> releasedBuffers, out uint releasedCount) + { + releasedCount = 0; + + // Ensure that the first entry is set to zero if no entries are returned. + if (releasedBuffers.Length > 0) + { + releasedBuffers[0] = 0; + } + + lock (_parentLock) + { + for (int i = 0; i < releasedBuffers.Length; i++) + { + if (!_session.TryPopReleasedBuffer(out AudioBuffer buffer)) + { + break; + } + + releasedBuffers[i] = buffer.BufferTag; + releasedCount++; + } + } + + return ResultCode.Success; + } + + /// <summary> + /// Get the current state of the <see cref="AudioInputSystem"/>. + /// </summary> + /// <returns>Return the curent sta\te of the <see cref="AudioInputSystem"/></returns> + public AudioDeviceState GetState() + { + lock (_parentLock) + { + return _session.GetState(); + } + } + + /// <summary> + /// Start the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Start() + { + lock (_parentLock) + { + return _session.Start(); + } + } + + /// <summary> + /// Stop the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Stop() + { + lock (_parentLock) + { + return _session.Stop(); + } + } + + /// <summary> + /// Get the volume of the session. + /// </summary> + /// <returns>The volume of the session</returns> + public float GetVolume() + { + lock (_parentLock) + { + return _session.GetVolume(); + } + } + + /// <summary> + /// Set the volume of the session. + /// </summary> + /// <param name="volume">The new volume to set</param> + public void SetVolume(float volume) + { + lock (_parentLock) + { + _session.SetVolume(volume); + } + } + + /// <summary> + /// Get the count of buffer currently in use (server + driver side). + /// </summary> + /// <returns>The count of buffer currently in use</returns> + public uint GetBufferCount() + { + lock (_parentLock) + { + return _session.GetBufferCount(); + } + } + + /// <summary> + /// Check if a buffer is present. + /// </summary> + /// <param name="bufferTag">The unique tag of the buffer</param> + /// <returns>Return true if a buffer is present</returns> + public bool ContainsBuffer(ulong bufferTag) + { + lock (_parentLock) + { + return _session.ContainsBuffer(bufferTag); + } + } + + /// <summary> + /// Get the count of sample played in this session. + /// </summary> + /// <returns>The count of sample played in this session</returns> + public ulong GetPlayedSampleCount() + { + lock (_parentLock) + { + return _session.GetPlayedSampleCount(); + } + } + + /// <summary> + /// Flush all buffers to the initial state. + /// </summary> + /// <returns>True if any buffers was flushed</returns> + public bool FlushBuffers() + { + lock (_parentLock) + { + return _session.FlushBuffers(); + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _session.Dispose(); + + _manager.Unregister(this); + } + } + } +} diff --git a/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs b/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs new file mode 100644 index 00000000..d489b008 --- /dev/null +++ b/Ryujinx.Audio/Integration/HardwareDeviceImpl.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Integration +{ + public class HardwareDeviceImpl : IHardwareDevice + { + private IHardwareDeviceSession _session; + private uint _channelCount; + private uint _sampleRate; + private uint _currentBufferTag; + + private byte[] _buffer; + + public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate) + { + _session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount); + _channelCount = channelCount; + _sampleRate = sampleRate; + _currentBufferTag = 0; + + _buffer = new byte[Constants.TargetSampleCount * channelCount * sizeof(ushort)]; + + _session.Start(); + } + + public void AppendBuffer(ReadOnlySpan<short> data, uint channelCount) + { + data.CopyTo(MemoryMarshal.Cast<byte, short>(_buffer)); + + _session.QueueBuffer(new AudioBuffer + { + DataPointer = _currentBufferTag++, + Data = _buffer, + DataSize = (ulong)_buffer.Length, + }); + + _currentBufferTag = _currentBufferTag % 4; + } + + public uint GetChannelCount() + { + return _channelCount; + } + + public uint GetSampleRate() + { + return _sampleRate; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _session.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Integration/IHardwareDevice.cs b/Ryujinx.Audio/Integration/IHardwareDevice.cs new file mode 100644 index 00000000..0f67b2c8 --- /dev/null +++ b/Ryujinx.Audio/Integration/IHardwareDevice.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Integration +{ + /// <summary> + /// Represent an hardware device used in <see cref="Renderer.Dsp.Command.DeviceSinkCommand"/> + /// </summary> + public interface IHardwareDevice : IDisposable + { + /// <summary> + /// Get the supported sample rate of this device. + /// </summary> + /// <returns>The supported sample rate of this device.</returns> + uint GetSampleRate(); + + /// <summary> + /// Get the channel count supported by this device. + /// </summary> + /// <returns>The channel count supported by this device.</returns> + uint GetChannelCount(); + + /// <summary> + /// Appends new PCM16 samples to the device. + /// </summary> + /// <param name="data">The new PCM16 samples.</param> + /// <param name="channelCount">The number of channels.</param> + void AppendBuffer(ReadOnlySpan<short> data, uint channelCount); + + /// <summary> + /// Check if the audio renderer needs to perform downmixing. + /// </summary> + /// <returns>True if downmixing is needed.</returns> + public bool NeedDownmixing() + { + uint channelCount = GetChannelCount(); + + Debug.Assert(channelCount > 0 && channelCount <= Constants.ChannelCountMax); + + return channelCount != Constants.ChannelCountMax; + } + } +} diff --git a/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs b/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs new file mode 100644 index 00000000..70738c90 --- /dev/null +++ b/Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Memory; +using System; +using System.Threading; + +namespace Ryujinx.Audio.Integration +{ + /// <summary> + /// Represent an hardware device driver used in <see cref="Output.AudioOutputSystem"/>. + /// </summary> + public interface IHardwareDeviceDriver : IDisposable + { + public enum Direction + { + Input, + Output + } + + IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount); + + ManualResetEvent GetUpdateRequiredEvent(); + + bool SupportsDirection(Direction direction); + bool SupportsSampleRate(uint sampleRate); + bool SupportsSampleFormat(SampleFormat sampleFormat); + bool SupportsChannelCount(uint channelCount); + + IHardwareDeviceDriver GetRealDeviceDriver() + { + return this; + } + } +} diff --git a/Ryujinx.Audio/Integration/IHardwareDeviceSession.cs b/Ryujinx.Audio/Integration/IHardwareDeviceSession.cs new file mode 100644 index 00000000..0e286768 --- /dev/null +++ b/Ryujinx.Audio/Integration/IHardwareDeviceSession.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using System; + +namespace Ryujinx.Audio.Integration +{ + public interface IHardwareDeviceSession : IDisposable + { + bool RegisterBuffer(AudioBuffer buffer); + + void UnregisterBuffer(AudioBuffer buffer); + + void QueueBuffer(AudioBuffer buffer); + + bool WasBufferFullyConsumed(AudioBuffer buffer); + + void SetVolume(float volume); + + float GetVolume(); + + ulong GetPlayedSampleCount(); + + void Start(); + + void Stop(); + + void PrepareToClose(); + } +} diff --git a/Ryujinx.Audio/Integration/IWritableEvent.cs b/Ryujinx.Audio/Integration/IWritableEvent.cs new file mode 100644 index 00000000..ced20dbd --- /dev/null +++ b/Ryujinx.Audio/Integration/IWritableEvent.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Integration +{ + /// <summary> + /// Represent a writable event with manual clear. + /// </summary> + public interface IWritableEvent + { + /// <summary> + /// Signal the event. + /// </summary> + void Signal(); + + /// <summary> + /// Clear the signaled state of the event. + /// </summary> + void Clear(); + } +} diff --git a/Ryujinx.Audio/LICENSE.txt b/Ryujinx.Audio/LICENSE.txt new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/Ryujinx.Audio/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Ryujinx.Audio/Native/libsoundio/MarshalExtensions.cs b/Ryujinx.Audio/Native/libsoundio/MarshalExtensions.cs deleted file mode 100644 index 5e86263e..00000000 --- a/Ryujinx.Audio/Native/libsoundio/MarshalExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public static class MarshalEx - { - public static double ReadDouble(IntPtr handle, int offset = 0) - { - return BitConverter.Int64BitsToDouble(Marshal.ReadInt64(handle, offset)); - } - - public static void WriteDouble(IntPtr handle, double value) - { - WriteDouble(handle, 0, value); - } - - public static void WriteDouble(IntPtr handle, int offset, double value) - { - Marshal.WriteInt64(handle, offset, BitConverter.DoubleToInt64Bits(value)); - } - - public static float ReadFloat(IntPtr handle, int offset = 0) - { - return BitConverter.Int32BitsToSingle(Marshal.ReadInt32(handle, offset)); - } - - public static void WriteFloat(IntPtr handle, float value) - { - WriteFloat(handle, 0, value); - } - - public static void WriteFloat(IntPtr handle, int offset, float value) - { - Marshal.WriteInt32(handle, offset, BitConverter.SingleToInt32Bits(value)); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIO.cs b/Ryujinx.Audio/Native/libsoundio/SoundIO.cs deleted file mode 100644 index c4ce1887..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIO.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public class SoundIO : IDisposable - { - Pointer<SoundIo> handle; - - public SoundIO() - { - handle = Natives.soundio_create(); - } - - internal SoundIO(Pointer<SoundIo> handle) - { - this.handle = handle; - } - - public void Dispose () - { - foreach (var h in allocated_hglobals) - { - Marshal.FreeHGlobal(h); - } - - Natives.soundio_destroy(handle); - } - - // Equality (based on handle) - - public override bool Equals(object other) - { - var d = other as SoundIO; - - return d != null && this.handle == d.handle; - } - - public override int GetHashCode() - { - return (int)(IntPtr)handle; - } - - public static bool operator == (SoundIO obj1, SoundIO obj2) - { - return obj1 is null ? obj2 is null : obj1.Equals(obj2); - } - - public static bool operator != (SoundIO obj1, SoundIO obj2) - { - return obj1 is null ? obj2 is object : !obj1.Equals(obj2); - } - - // fields - - // FIXME: this should be taken care in more centralized/decent manner... we don't want to write - // this kind of code anywhere we need string marshaling. - List<IntPtr> allocated_hglobals = new List<IntPtr>(); - - public string ApplicationName { - get { return Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle, app_name_offset)); } - set - { - unsafe - { - var existing = Marshal.ReadIntPtr(handle, app_name_offset); - if (allocated_hglobals.Contains (existing)) - { - allocated_hglobals.Remove(existing); - Marshal.FreeHGlobal(existing); - } - - var ptr = Marshal.StringToHGlobalAnsi(value); - Marshal.WriteIntPtr(handle, app_name_offset, ptr); - allocated_hglobals.Add(ptr); - } - } - } - - static readonly int app_name_offset = (int)Marshal.OffsetOf<SoundIo>("app_name"); - - public SoundIOBackend CurrentBackend - { - get { return (SoundIOBackend)Marshal.ReadInt32(handle, current_backend_offset); } - } - - static readonly int current_backend_offset = (int)Marshal.OffsetOf<SoundIo>("current_backend"); - - // emit_rtprio_warning - public Action EmitRealtimePriorityWarning - { - get { return emit_rtprio_warning; } - set - { - emit_rtprio_warning = value; - - var ptr = Marshal.GetFunctionPointerForDelegate(on_devices_change); - - Marshal.WriteIntPtr(handle, emit_rtprio_warning_offset, ptr); - } - } - - static readonly int emit_rtprio_warning_offset = (int)Marshal.OffsetOf<SoundIo>("emit_rtprio_warning"); - - Action emit_rtprio_warning; - - // jack_error_callback - public Action<string> JackErrorCallback - { - get { return jack_error_callback; } - set - { - jack_error_callback = value; - if (value == null) - { - jack_error_callback = null; - } - else - { - jack_error_callback_native = msg => jack_error_callback(msg); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(jack_error_callback_native); - Marshal.WriteIntPtr(handle, jack_error_callback_offset, ptr); - } - } - - static readonly int jack_error_callback_offset = (int)Marshal.OffsetOf<SoundIo>("jack_error_callback"); - - Action<string> jack_error_callback; - delegate void jack_error_delegate(string message); - jack_error_delegate jack_error_callback_native; - - // jack_info_callback - public Action<string> JackInfoCallback - { - get { return jack_info_callback; } - set - { - jack_info_callback = value; - if (value == null) - { - jack_info_callback = null; - } - else - { - jack_info_callback_native = msg => jack_info_callback(msg); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(jack_info_callback_native); - Marshal.WriteIntPtr(handle, jack_info_callback_offset, ptr); - } - } - - static readonly int jack_info_callback_offset = (int)Marshal.OffsetOf<SoundIo>("jack_info_callback"); - - Action<string> jack_info_callback; - delegate void jack_info_delegate(string message); - jack_info_delegate jack_info_callback_native; - - // on_backend_disconnect - public Action<int> OnBackendDisconnect - { - get { return on_backend_disconnect; } - set - { - on_backend_disconnect = value; - if (value == null) - { - on_backend_disconnect_native = null; - } - else - { - on_backend_disconnect_native = (sio, err) => on_backend_disconnect(err); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(on_backend_disconnect_native); - Marshal.WriteIntPtr(handle, on_backend_disconnect_offset, ptr); - } - } - - static readonly int on_backend_disconnect_offset = (int)Marshal.OffsetOf<SoundIo>("on_backend_disconnect"); - - Action<int> on_backend_disconnect; - delegate void on_backend_disconnect_delegate(IntPtr handle, int errorCode); - on_backend_disconnect_delegate on_backend_disconnect_native; - - // on_devices_change - public Action OnDevicesChange - { - get { return on_devices_change; } - set - { - on_devices_change = value; - if (value == null) - { - on_devices_change_native = null; - } - else - { - on_devices_change_native = sio => on_devices_change(); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(on_devices_change_native); - Marshal.WriteIntPtr(handle, on_devices_change_offset, ptr); - } - } - - static readonly int on_devices_change_offset = (int)Marshal.OffsetOf<SoundIo>("on_devices_change"); - - Action on_devices_change; - delegate void on_devices_change_delegate(IntPtr handle); - on_devices_change_delegate on_devices_change_native; - - // on_events_signal - public Action OnEventsSignal - { - get { return on_events_signal; } - set - { - on_events_signal = value; - if (value == null) - { - on_events_signal_native = null; - } - else - { - on_events_signal_native = sio => on_events_signal(); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(on_events_signal_native); - Marshal.WriteIntPtr(handle, on_events_signal_offset, ptr); - } - } - - static readonly int on_events_signal_offset = (int)Marshal.OffsetOf<SoundIo>("on_events_signal"); - - Action on_events_signal; - delegate void on_events_signal_delegate(IntPtr handle); - on_events_signal_delegate on_events_signal_native; - - - // functions - - public int BackendCount - { - get { return Natives.soundio_backend_count(handle); } - } - - public int InputDeviceCount - { - get { return Natives.soundio_input_device_count(handle); } - } - - public int OutputDeviceCount - { - get { return Natives.soundio_output_device_count(handle); } - } - - public int DefaultInputDeviceIndex - { - get { return Natives.soundio_default_input_device_index(handle); } - } - - public int DefaultOutputDeviceIndex - { - get { return Natives.soundio_default_output_device_index(handle); } - } - - public SoundIOBackend GetBackend(int index) - { - return (SoundIOBackend)Natives.soundio_get_backend(handle, index); - } - - public SoundIODevice GetInputDevice(int index) - { - return new SoundIODevice(Natives.soundio_get_input_device(handle, index)); - } - - public SoundIODevice GetOutputDevice(int index) - { - return new SoundIODevice(Natives.soundio_get_output_device(handle, index)); - } - - public void Connect() - { - var ret = (SoundIoError)Natives.soundio_connect(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void ConnectBackend(SoundIOBackend backend) - { - var ret = (SoundIoError)Natives.soundio_connect_backend(handle, (SoundIoBackend)backend); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void Disconnect() - { - Natives.soundio_disconnect(handle); - } - - public void FlushEvents() - { - Natives.soundio_flush_events(handle); - } - - public void WaitEvents() - { - Natives.soundio_wait_events(handle); - } - - public void Wakeup() - { - Natives.soundio_wakeup(handle); - } - - public void ForceDeviceScan() - { - Natives.soundio_force_device_scan(handle); - } - - public SoundIORingBuffer CreateRingBuffer(int capacity) - { - return new SoundIORingBuffer(Natives.soundio_ring_buffer_create(handle, capacity)); - } - - // static methods - - public static string VersionString - { - get { return Marshal.PtrToStringAnsi(Natives.soundio_version_string()); } - } - - public static int VersionMajor - { - get { return Natives.soundio_version_major(); } - } - - public static int VersionMinor - { - get { return Natives.soundio_version_minor(); } - } - - public static int VersionPatch - { - get { return Natives.soundio_version_patch(); } - } - - public static string GetBackendName(SoundIOBackend backend) - { - return Marshal.PtrToStringAnsi(Natives.soundio_backend_name((SoundIoBackend)backend)); - } - - public static bool HaveBackend(SoundIOBackend backend) - { - return Natives.soundio_have_backend((SoundIoBackend)backend); - } - - public static int GetBytesPerSample(SoundIOFormat format) - { - return Natives.soundio_get_bytes_per_sample((SoundIoFormat)format); - } - - public static int GetBytesPerFrame(SoundIOFormat format, int channelCount) - { - return Natives.soundio_get_bytes_per_frame((SoundIoFormat)format, channelCount); - } - - public static int GetBytesPerSecond(SoundIOFormat format, int channelCount, int sampleRate) - { - return Natives.soundio_get_bytes_per_second((SoundIoFormat)format, channelCount, sampleRate); - } - - public static string GetSoundFormatName(SoundIOFormat format) - { - return Marshal.PtrToStringAnsi(Natives.soundio_format_string((SoundIoFormat)format)); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOBackend.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOBackend.cs deleted file mode 100644 index fd105804..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOBackend.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace SoundIOSharp -{ - public enum SoundIOBackend - { - None, - Jack, - PulseAudio, - Alsa, - CoreAudio, - Wasapi, - Dummy - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelArea.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOChannelArea.cs deleted file mode 100644 index c15fb744..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelArea.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public struct SoundIOChannelArea - { - internal SoundIOChannelArea(Pointer<SoundIoChannelArea> handle) - { - this.handle = handle; - } - - Pointer<SoundIoChannelArea> handle; - - public IntPtr Pointer - { - get { return Marshal.ReadIntPtr(handle, ptr_offset); } - set { Marshal.WriteIntPtr(handle, ptr_offset, value); } - } - - static readonly int ptr_offset = (int)Marshal.OffsetOf<SoundIoChannelArea>("ptr"); - - public int Step - { - get { return Marshal.ReadInt32(handle, step_offset); } - } - - static readonly int step_offset = (int)Marshal.OffsetOf<SoundIoChannelArea>("step"); - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelAreas.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOChannelAreas.cs deleted file mode 100644 index e0f375b9..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelAreas.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public struct SoundIOChannelAreas - { - static readonly int native_size = Marshal.SizeOf<SoundIoChannelArea>(); - - internal SoundIOChannelAreas(IntPtr head, int channelCount, int frameCount) - { - this.head = head; - this.channel_count = channelCount; - this.frame_count = frameCount; - } - - IntPtr head; - int channel_count; - int frame_count; - - public bool IsEmpty - { - get { return head == IntPtr.Zero; } - } - - public SoundIOChannelArea GetArea(int channel) - { - return new SoundIOChannelArea(head + native_size * channel); - } - - public int ChannelCount => channel_count; - public int FrameCount => frame_count; - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelId.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOChannelId.cs deleted file mode 100644 index 002669dc..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelId.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace SoundIOSharp -{ - public enum SoundIOChannelId - { - Invalid, - FrontLeft, - FrontRight, - FrontCenter, - Lfe, - BackLeft, - BackRight, - FrontLeftCenter, - FrontRightCenter, - BackCenter, - SideLeft, - SideRight, - TopCenter, - TopFrontLeft, - TopFrontCenter, - TopFrontRight, - TopBackLeft, - TopBackCenter, - TopBackRight, - BackLeftCenter, - BackRightCenter, - FrontLeftWide, - FrontRightWide, - FrontLeftHigh, - FrontCenterHigh, - FrontRightHigh, - TopFrontLeftCenter, - TopFrontRightCenter, - TopSideLeft, - TopSideRight, - LeftLfe, - RightLfe, - Lfe2, - BottomCenter, - BottomLeftCenter, - BottomRightCenter, - MsMid, - MsSide, - AmbisonicW, - AmbisonicX, - AmbisonicY, - AmbisonicZ, - XyX, - XyY, - HeadphonesLeft, - HeadphonesRight, - ClickTrack, - ForeignLanguage, - HearingImpaired, - Narration, - Haptic, - DialogCentricMix, - Aux, - Aux0, - Aux1, - Aux2, - Aux3, - Aux4, - Aux5, - Aux6, - Aux7, - Aux8, - Aux9, - Aux10, - Aux11, - Aux12, - Aux13, - Aux14, - Aux15 - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelLayout.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOChannelLayout.cs deleted file mode 100644 index cff6114f..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOChannelLayout.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public struct SoundIOChannelLayout - { - public static int BuiltInCount - { - get { return Natives.soundio_channel_layout_builtin_count(); } - } - - public static SoundIOChannelLayout GetBuiltIn(int index) - { - return new SoundIOChannelLayout(Natives.soundio_channel_layout_get_builtin(index)); - } - - public static SoundIOChannelLayout GetDefault(int channelCount) - { - var handle = Natives.soundio_channel_layout_get_default(channelCount); - - return new SoundIOChannelLayout (handle); - } - - public static SoundIOChannelId ParseChannelId(string name) - { - var ptr = Marshal.StringToHGlobalAnsi(name); - - try - { - return (SoundIOChannelId)Natives.soundio_parse_channel_id(ptr, name.Length); - } - finally - { - Marshal.FreeHGlobal(ptr); - } - } - - // instance members - - internal SoundIOChannelLayout(Pointer<SoundIoChannelLayout> handle) - { - this.handle = handle; - } - - readonly Pointer<SoundIoChannelLayout> handle; - - public bool IsNull - { - get { return handle.Handle == IntPtr.Zero; } - } - - internal IntPtr Handle - { - get { return handle; } - } - - public int ChannelCount - { - get { return IsNull ? 0 : Marshal.ReadInt32((IntPtr)handle + channel_count_offset); } - } - - static readonly int channel_count_offset = (int)Marshal.OffsetOf<SoundIoChannelLayout>("channel_count"); - - public string Name - { - get { return IsNull ? null : Marshal.PtrToStringAnsi(Marshal.ReadIntPtr((IntPtr)handle + name_offset)); } - } - - static readonly int name_offset = (int)Marshal.OffsetOf<SoundIoChannelLayout>("name"); - - public IEnumerable<SoundIOChannelId> Channels - { - get - { - if (IsNull) yield break; - - for (int i = 0; i < 24; i++) - { - yield return (SoundIOChannelId)Marshal.ReadInt32((IntPtr)handle + channels_offset + sizeof(SoundIoChannelId) * i); - } - } - } - - static readonly int channels_offset = (int)Marshal.OffsetOf<SoundIoChannelLayout>("channels"); - - public override bool Equals(object other) - { - if (!(other is SoundIOChannelLayout)) return false; - - var s = (SoundIOChannelLayout) other; - - return handle == s.handle || Natives.soundio_channel_layout_equal(handle, s.handle); - } - - public override int GetHashCode() - { - return handle.GetHashCode(); - } - - public string DetectBuiltInName() - { - if (IsNull) throw new InvalidOperationException(); - - return Natives.soundio_channel_layout_detect_builtin(handle) ? Name : null; - } - - public int FindChannel(SoundIOChannelId channel) - { - if (IsNull) throw new InvalidOperationException(); - - return Natives.soundio_channel_layout_find_channel(handle, (SoundIoChannelId)channel); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIODevice.cs b/Ryujinx.Audio/Native/libsoundio/SoundIODevice.cs deleted file mode 100644 index 6e7c4964..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIODevice.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public class SoundIODevice - { - public static SoundIOChannelLayout BestMatchingChannelLayout(SoundIODevice device1, SoundIODevice device2) - { - var ptr1 = Marshal.ReadIntPtr(device1.handle, layouts_offset); - var ptr2 = Marshal.ReadIntPtr(device2.handle, layouts_offset); - - return new SoundIOChannelLayout(Natives.soundio_best_matching_channel_layout(ptr1, device1.LayoutCount, ptr2, device2.LayoutCount)); - } - - internal SoundIODevice(Pointer<SoundIoDevice> handle) - { - this.handle = handle; - } - - readonly Pointer<SoundIoDevice> handle; - - // Equality (based on handle and native func) - - public override bool Equals(object other) - { - var d = other as SoundIODevice; - - return d != null && (this.handle == d.handle || Natives.soundio_device_equal (this.handle, d.handle)); - } - - public override int GetHashCode() - { - return (int)(IntPtr)handle; - } - - public static bool operator == (SoundIODevice obj1, SoundIODevice obj2) - { - return obj1 is null ? obj2 is null : obj1.Equals(obj2); - } - - public static bool operator != (SoundIODevice obj1, SoundIODevice obj2) - { - return obj1 is null ? obj2 is object : !obj1.Equals(obj2); - } - - // fields - - public SoundIODeviceAim Aim - { - get { return (SoundIODeviceAim)Marshal.ReadInt32(handle, aim_offset); } - } - - static readonly int aim_offset = (int)Marshal.OffsetOf<SoundIoDevice>("aim"); - - public SoundIOFormat CurrentFormat - { - get { return (SoundIOFormat)Marshal.ReadInt32(handle, current_format_offset); } - } - - static readonly int current_format_offset = (int)Marshal.OffsetOf<SoundIoDevice>("current_format"); - - public SoundIOChannelLayout CurrentLayout - { - get { return new SoundIOChannelLayout((IntPtr)handle + current_layout_offset); } - } - - static readonly int current_layout_offset = (int)Marshal.OffsetOf<SoundIoDevice>("current_layout"); - - public int FormatCount - { - get { return Marshal.ReadInt32(handle, format_count_offset); } - } - - static readonly int format_count_offset = (int)Marshal.OffsetOf<SoundIoDevice>("format_count"); - - public IEnumerable<SoundIOFormat> Formats - { - get - { - var ptr = Marshal.ReadIntPtr(handle, formats_offset); - - for (int i = 0; i < FormatCount; i++) - { - yield return (SoundIOFormat)Marshal.ReadInt32(ptr, i); - } - } - } - - static readonly int formats_offset = (int)Marshal.OffsetOf<SoundIoDevice>("formats"); - - public string Id - { - get { return Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle, id_offset)); } - } - - static readonly int id_offset = (int)Marshal.OffsetOf<SoundIoDevice>("id"); - - public bool IsRaw - { - get { return Marshal.ReadInt32(handle, is_raw_offset) != 0; } - } - - static readonly int is_raw_offset = (int)Marshal.OffsetOf<SoundIoDevice>("is_raw"); - - public int LayoutCount - { - get { return Marshal.ReadInt32(handle, layout_count_offset); } - } - - static readonly int layout_count_offset = (int)Marshal.OffsetOf<SoundIoDevice>("layout_count"); - - public IEnumerable<SoundIOChannelLayout> Layouts - { - get - { - var ptr = Marshal.ReadIntPtr (handle, layouts_offset); - - for (int i = 0; i < LayoutCount; i++) - { - yield return new SoundIOChannelLayout(ptr + i * Marshal.SizeOf<SoundIoChannelLayout>()); - } - } - } - - static readonly int layouts_offset = (int)Marshal.OffsetOf<SoundIoDevice>("layouts"); - - public string Name - { - get { return Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle, name_offset)); } - } - - static readonly int name_offset = (int)Marshal.OffsetOf<SoundIoDevice>("name"); - - public int ProbeError - { - get { return Marshal.ReadInt32(handle, probe_error_offset); } - } - - static readonly int probe_error_offset = (int)Marshal.OffsetOf<SoundIoDevice>("probe_error"); - - public int ReferenceCount - { - get { return Marshal.ReadInt32(handle, ref_count_offset); } - } - - static readonly int ref_count_offset = (int)Marshal.OffsetOf<SoundIoDevice>("ref_count"); - - public int SampleRateCount - { - get { return Marshal.ReadInt32(handle, sample_rate_count_offset); } - } - - static readonly int sample_rate_count_offset = (int)Marshal.OffsetOf<SoundIoDevice>("sample_rate_count"); - - public IEnumerable<SoundIOSampleRateRange> SampleRates - { - get - { - var ptr = Marshal.ReadIntPtr(handle, sample_rates_offset); - - for (int i = 0; i < SampleRateCount; i++) - { - yield return new SoundIOSampleRateRange(Marshal.ReadInt32(ptr, i * 2), Marshal.ReadInt32(ptr, i * 2 + 1)); - } - } - } - - static readonly int sample_rates_offset = (int)Marshal.OffsetOf<SoundIoDevice>("sample_rates"); - - public double SoftwareLatencyCurrent - { - get { return MarshalEx.ReadDouble(handle, software_latency_current_offset); } - set { MarshalEx.WriteDouble(handle, software_latency_current_offset, value); } - } - - static readonly int software_latency_current_offset = (int)Marshal.OffsetOf<SoundIoDevice>("software_latency_current"); - - public double SoftwareLatencyMin - { - get { return MarshalEx.ReadDouble(handle, software_latency_min_offset); } - set { MarshalEx.WriteDouble(handle, software_latency_min_offset, value); } - } - - static readonly int software_latency_min_offset = (int)Marshal.OffsetOf<SoundIoDevice>("software_latency_min"); - - public double SoftwareLatencyMax - { - get { return MarshalEx.ReadDouble(handle, software_latency_max_offset); } - set { MarshalEx.WriteDouble(handle, software_latency_max_offset, value); } - } - - static readonly int software_latency_max_offset = (int)Marshal.OffsetOf<SoundIoDevice>("software_latency_max"); - - public SoundIO SoundIO - { - get { return new SoundIO(Marshal.ReadIntPtr(handle, soundio_offset)); } - } - - static readonly int soundio_offset = (int)Marshal.OffsetOf<SoundIoDevice>("soundio"); - - // functions - - public void AddReference() - { - Natives.soundio_device_ref(handle); - } - - public void RemoveReference() - { - Natives.soundio_device_unref(handle); - } - - public void SortDeviceChannelLayouts() - { - Natives.soundio_device_sort_channel_layouts(handle); - } - - public static readonly SoundIOFormat S16NE = BitConverter.IsLittleEndian ? SoundIOFormat.S16LE : SoundIOFormat.S16BE; - public static readonly SoundIOFormat U16NE = BitConverter.IsLittleEndian ? SoundIOFormat.U16LE : SoundIOFormat.U16BE; - public static readonly SoundIOFormat S24NE = BitConverter.IsLittleEndian ? SoundIOFormat.S24LE : SoundIOFormat.S24BE; - public static readonly SoundIOFormat U24NE = BitConverter.IsLittleEndian ? SoundIOFormat.U24LE : SoundIOFormat.U24BE; - public static readonly SoundIOFormat S32NE = BitConverter.IsLittleEndian ? SoundIOFormat.S32LE : SoundIOFormat.S32BE; - public static readonly SoundIOFormat U32NE = BitConverter.IsLittleEndian ? SoundIOFormat.U32LE : SoundIOFormat.U32BE; - public static readonly SoundIOFormat Float32NE = BitConverter.IsLittleEndian ? SoundIOFormat.Float32LE : SoundIOFormat.Float32BE; - public static readonly SoundIOFormat Float64NE = BitConverter.IsLittleEndian ? SoundIOFormat.Float64LE : SoundIOFormat.Float64BE; - public static readonly SoundIOFormat S16FE = !BitConverter.IsLittleEndian ? SoundIOFormat.S16LE : SoundIOFormat.S16BE; - public static readonly SoundIOFormat U16FE = !BitConverter.IsLittleEndian ? SoundIOFormat.U16LE : SoundIOFormat.U16BE; - public static readonly SoundIOFormat S24FE = !BitConverter.IsLittleEndian ? SoundIOFormat.S24LE : SoundIOFormat.S24BE; - public static readonly SoundIOFormat U24FE = !BitConverter.IsLittleEndian ? SoundIOFormat.U24LE : SoundIOFormat.U24BE; - public static readonly SoundIOFormat S32FE = !BitConverter.IsLittleEndian ? SoundIOFormat.S32LE : SoundIOFormat.S32BE; - public static readonly SoundIOFormat U32FE = !BitConverter.IsLittleEndian ? SoundIOFormat.U32LE : SoundIOFormat.U32BE; - public static readonly SoundIOFormat Float32FE = !BitConverter.IsLittleEndian ? SoundIOFormat.Float32LE : SoundIOFormat.Float32BE; - public static readonly SoundIOFormat Float64FE = !BitConverter.IsLittleEndian ? SoundIOFormat.Float64LE : SoundIOFormat.Float64BE; - - public bool SupportsFormat(SoundIOFormat format) - { - return Natives.soundio_device_supports_format(handle, (SoundIoFormat)format); - } - - public bool SupportsSampleRate(int sampleRate) - { - return Natives.soundio_device_supports_sample_rate(handle, sampleRate); - } - - public bool SupportsChannelCount(int channelCount) - { - return Natives.soundio_device_supports_layout(handle, SoundIOChannelLayout.GetDefault(channelCount).Handle); - } - - public int GetNearestSampleRate(int sampleRate) - { - return Natives.soundio_device_nearest_sample_rate(handle, sampleRate); - } - - public SoundIOInStream CreateInStream() - { - return new SoundIOInStream(Natives.soundio_instream_create(handle)); - } - - public SoundIOOutStream CreateOutStream() - { - return new SoundIOOutStream(Natives.soundio_outstream_create(handle)); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIODeviceAim.cs b/Ryujinx.Audio/Native/libsoundio/SoundIODeviceAim.cs deleted file mode 100644 index 1e596127..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIODeviceAim.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SoundIOSharp -{ - public enum SoundIODeviceAim // soundio.h (228, 6) - { - Input, - Output - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOException.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOException.cs deleted file mode 100644 index 537b6cde..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOException.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public class SoundIOException : Exception - { - internal SoundIOException(SoundIoError errorCode) : base (Marshal.PtrToStringAnsi(Natives.soundio_strerror((int) errorCode))) { } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOFormat.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOFormat.cs deleted file mode 100644 index df1b71c6..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOFormat.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace SoundIOSharp -{ - public enum SoundIOFormat - { - Invalid, - S8, - U8, - S16LE, - S16BE, - U16LE, - U16BE, - S24LE, - S24BE, - U24LE, - U24BE, - S32LE, - S32BE, - U32LE, - U32BE, - Float32LE, - Float32BE, - Float64LE, - Float64BE - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOInStream.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOInStream.cs deleted file mode 100644 index df97d653..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOInStream.cs +++ /dev/null @@ -1,293 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public class SoundIOInStream : IDisposable - { - internal SoundIOInStream(Pointer<SoundIoInStream> handle) - { - this.handle = handle; - } - - Pointer<SoundIoInStream> handle; - - public void Dispose() - { - Natives.soundio_instream_destroy(handle); - } - - // Equality (based on handle) - - public override bool Equals(object other) - { - var d = other as SoundIOInStream; - - return d != null && (this.handle == d.handle); - } - - public override int GetHashCode() - { - return (int)(IntPtr)handle; - } - - public static bool operator == (SoundIOInStream obj1, SoundIOInStream obj2) - { - return obj1 is null ? obj2 is null : obj1.Equals(obj2); - } - - public static bool operator != (SoundIOInStream obj1, SoundIOInStream obj2) - { - return obj1 is null ? obj2 is object : !obj1.Equals(obj2); - } - - // fields - - public SoundIODevice Device - { - get { return new SoundIODevice(Marshal.ReadIntPtr(handle, device_offset)); } - } - - static readonly int device_offset = (int)Marshal.OffsetOf<SoundIoInStream>("device"); - - public SoundIOFormat Format - { - get { return (SoundIOFormat)Marshal.ReadInt32(handle, format_offset); } - set { Marshal.WriteInt32(handle, format_offset, (int) value); } - } - - static readonly int format_offset = (int)Marshal.OffsetOf<SoundIoInStream>("format"); - - public int SampleRate - { - get { return Marshal.ReadInt32(handle, sample_rate_offset); } - set { Marshal.WriteInt32(handle, sample_rate_offset, value); } - } - - static readonly int sample_rate_offset = (int)Marshal.OffsetOf<SoundIoInStream>("sample_rate"); - - public SoundIOChannelLayout Layout - { - get { return new SoundIOChannelLayout ((IntPtr) handle + layout_offset); } - set - { - unsafe - { - Buffer.MemoryCopy((void*)((IntPtr)handle + layout_offset), (void*)value.Handle, Marshal.SizeOf<SoundIoChannelLayout>(), Marshal.SizeOf<SoundIoChannelLayout>()); - } - } - } - - static readonly int layout_offset = (int)Marshal.OffsetOf<SoundIoInStream>("layout"); - - public double SoftwareLatency - { - get { return MarshalEx.ReadDouble(handle, software_latency_offset); } - set { MarshalEx.WriteDouble(handle, software_latency_offset, value); } - } - - static readonly int software_latency_offset = (int)Marshal.OffsetOf<SoundIoInStream>("software_latency"); - - // error_callback - public Action ErrorCallback - { - get { return error_callback; } - set - { - error_callback = value; - error_callback_native = _ => error_callback(); - - var ptr = Marshal.GetFunctionPointerForDelegate(error_callback_native); - - Marshal.WriteIntPtr(handle, error_callback_offset, ptr); - } - } - - static readonly int error_callback_offset = (int)Marshal.OffsetOf<SoundIoInStream>("error_callback"); - - Action error_callback; - delegate void error_callback_delegate(IntPtr handle); - error_callback_delegate error_callback_native; - - // read_callback - public Action<int,int> ReadCallback - { - get { return read_callback; } - set - { - read_callback = value; - read_callback_native = (_, minFrameCount, maxFrameCount) => read_callback(minFrameCount, maxFrameCount); - - var ptr = Marshal.GetFunctionPointerForDelegate(read_callback_native); - - Marshal.WriteIntPtr(handle, read_callback_offset, ptr); - } - } - - static readonly int read_callback_offset = (int)Marshal.OffsetOf<SoundIoInStream>("read_callback"); - - Action<int, int> read_callback; - delegate void read_callback_delegate(IntPtr handle, int min, int max); - read_callback_delegate read_callback_native; - - // overflow_callback - public Action OverflowCallback - { - get { return overflow_callback; } - set - { - overflow_callback = value; - overflow_callback_native = _ => overflow_callback(); - - var ptr = Marshal.GetFunctionPointerForDelegate(overflow_callback_native); - - Marshal.WriteIntPtr(handle, overflow_callback_offset, ptr); - } - } - static readonly int overflow_callback_offset = (int)Marshal.OffsetOf<SoundIoInStream>("overflow_callback"); - - Action overflow_callback; - delegate void overflow_callback_delegate(IntPtr handle); - overflow_callback_delegate overflow_callback_native; - - // FIXME: this should be taken care in more centralized/decent manner... we don't want to write - // this kind of code anywhere we need string marshaling. - List<IntPtr> allocated_hglobals = new List<IntPtr>(); - - public string Name - { - get { return Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle, name_offset)); } - set - { - unsafe - { - var existing = Marshal.ReadIntPtr(handle, name_offset); - if (allocated_hglobals.Contains(existing)) - { - allocated_hglobals.Remove(existing); - Marshal.FreeHGlobal(existing); - } - - var ptr = Marshal.StringToHGlobalAnsi(value); - Marshal.WriteIntPtr(handle, name_offset, ptr); - allocated_hglobals.Add(ptr); - } - } - } - - static readonly int name_offset = (int)Marshal.OffsetOf<SoundIoInStream>("name"); - - public bool NonTerminalHint - { - get { return Marshal.ReadInt32(handle, non_terminal_hint_offset) != 0; } - } - - static readonly int non_terminal_hint_offset = (int)Marshal.OffsetOf<SoundIoInStream>("non_terminal_hint"); - - public int BytesPerFrame - { - get { return Marshal.ReadInt32(handle, bytes_per_frame_offset); } - } - - static readonly int bytes_per_frame_offset = (int)Marshal.OffsetOf<SoundIoInStream>("bytes_per_frame"); - - public int BytesPerSample - { - get { return Marshal.ReadInt32(handle, bytes_per_sample_offset); } - } - - static readonly int bytes_per_sample_offset = (int)Marshal.OffsetOf<SoundIoInStream>("bytes_per_sample"); - - public string LayoutErrorMessage - { - get - { - var code = (SoundIoError)Marshal.ReadInt32(handle, layout_error_offset); - - return code == SoundIoError.SoundIoErrorNone ? null : Marshal.PtrToStringAnsi(Natives.soundio_strerror((int)code)); - } - } - - static readonly int layout_error_offset = (int)Marshal.OffsetOf<SoundIoInStream>("layout_error"); - - // functions - - public void Open() - { - var ret = (SoundIoError)Natives.soundio_instream_open(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void Start() - { - var ret = (SoundIoError)Natives.soundio_instream_start(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public SoundIOChannelAreas BeginRead(ref int frameCount) - { - IntPtr ptrs = default; - int nativeFrameCount = frameCount; - - unsafe - { - var frameCountPtr = &nativeFrameCount; - var ptrptr = &ptrs; - var ret = (SoundIoError)Natives.soundio_instream_begin_read(handle, (IntPtr)ptrptr, (IntPtr)frameCountPtr); - - frameCount = *frameCountPtr; - - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - - return new SoundIOChannelAreas(ptrs, Layout.ChannelCount, frameCount); - } - } - - public void EndRead() - { - var ret = (SoundIoError)Natives.soundio_instream_end_read(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void Pause(bool pause) - { - var ret = (SoundIoError)Natives.soundio_instream_pause(handle, pause); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public double GetLatency() - { - unsafe - { - double* dptr = null; - IntPtr p = new IntPtr(dptr); - - var ret = (SoundIoError)Natives.soundio_instream_get_latency(handle, p); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - - dptr = (double*)p; - - return *dptr; - } - } - } -} diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOOutStream.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOOutStream.cs deleted file mode 100644 index 432ca42b..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOOutStream.cs +++ /dev/null @@ -1,331 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace SoundIOSharp -{ - public class SoundIOOutStream : IDisposable - { - internal SoundIOOutStream (Pointer<SoundIoOutStream> handle) - { - this.handle = handle; - } - - Pointer<SoundIoOutStream> handle; - - public void Dispose () - { - Natives.soundio_outstream_destroy (handle); - } - // Equality (based on handle) - - public override bool Equals (object other) - { - var d = other as SoundIOOutStream; - - return d != null && (this.handle == d.handle); - } - - public override int GetHashCode () - { - return (int)(IntPtr)handle; - } - - public static bool operator == (SoundIOOutStream obj1, SoundIOOutStream obj2) - { - return obj1 is null ? obj2 is null : obj1.Equals(obj2); - } - - public static bool operator != (SoundIOOutStream obj1, SoundIOOutStream obj2) - { - return obj1 is null ? obj2 is object : !obj1.Equals(obj2); - } - - // fields - - public SoundIODevice Device - { - get { return new SoundIODevice(Marshal.ReadIntPtr(handle, device_offset)); } - } - - static readonly int device_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("device"); - - public SoundIOFormat Format - { - get { return (SoundIOFormat) Marshal.ReadInt32(handle, format_offset); } - set { Marshal.WriteInt32(handle, format_offset, (int) value); } - } - - static readonly int format_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("format"); - - public int SampleRate - { - get { return Marshal.ReadInt32(handle, sample_rate_offset); } - set { Marshal.WriteInt32(handle, sample_rate_offset, value); } - } - - static readonly int sample_rate_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("sample_rate"); - - public SoundIOChannelLayout Layout - { - get { unsafe { return new SoundIOChannelLayout((IntPtr) (void*)((IntPtr)handle + layout_offset)); } } - set - { - unsafe - { - Buffer.MemoryCopy((void*)value.Handle, (void*)((IntPtr)handle + layout_offset), Marshal.SizeOf<SoundIoChannelLayout>(), Marshal.SizeOf<SoundIoChannelLayout>()); - } - } - } - static readonly int layout_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("layout"); - - public double SoftwareLatency - { - get { return MarshalEx.ReadDouble (handle, software_latency_offset); } - set { MarshalEx.WriteDouble (handle, software_latency_offset, value); } - } - - static readonly int software_latency_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("software_latency"); - - public float Volume - { - get { return MarshalEx.ReadFloat(handle, volume_offset); } - set { MarshalEx.WriteFloat(handle, volume_offset, value); } - } - - static readonly int volume_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("volume"); - - // error_callback - public Action ErrorCallback - { - get { return error_callback; } - set - { - error_callback = value; - if (value == null) - { - error_callback_native = null; - } - else - { - error_callback_native = stream => error_callback(); - } - - var ptr = Marshal.GetFunctionPointerForDelegate(error_callback_native); - Marshal.WriteIntPtr(handle, error_callback_offset, ptr); - } - } - - static readonly int error_callback_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("error_callback"); - - Action error_callback; - delegate void error_callback_delegate (IntPtr handle); - error_callback_delegate error_callback_native; - - // write_callback - public Action<int, int> WriteCallback - { - get { return write_callback; } - set - { - write_callback = value; - if (value == null) - { - write_callback_native = null; - } - else - { - write_callback_native = (h, frame_count_min, frame_count_max) => write_callback(frame_count_min, frame_count_max); - } - - var ptr = Marshal.GetFunctionPointerForDelegate (write_callback_native); - Marshal.WriteIntPtr (handle, write_callback_offset, ptr); - } - } - - static readonly int write_callback_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("write_callback"); - - Action<int, int> write_callback; - delegate void write_callback_delegate(IntPtr handle, int min, int max); - write_callback_delegate write_callback_native; - - // underflow_callback - public Action UnderflowCallback - { - get { return underflow_callback; } - set - { - underflow_callback = value; - if (value == null) - { - underflow_callback_native = null; - } - else - { - underflow_callback_native = h => underflow_callback(); - } - - var ptr = Marshal.GetFunctionPointerForDelegate (underflow_callback_native); - Marshal.WriteIntPtr (handle, underflow_callback_offset, ptr); - } - } - - static readonly int underflow_callback_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("underflow_callback"); - - Action underflow_callback; - delegate void underflow_callback_delegate(IntPtr handle); - underflow_callback_delegate underflow_callback_native; - - // FIXME: this should be taken care in more centralized/decent manner... we don't want to write - // this kind of code anywhere we need string marshaling. - List<IntPtr> allocated_hglobals = new List<IntPtr>(); - - public string Name { - get { return Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(handle, name_offset)); } - set - { - unsafe - { - var existing = Marshal.ReadIntPtr(handle, name_offset); - if (allocated_hglobals.Contains(existing)) - { - allocated_hglobals.Remove(existing); - Marshal.FreeHGlobal(existing); - } - - var ptr = Marshal.StringToHGlobalAnsi(value); - Marshal.WriteIntPtr(handle, name_offset, ptr); - allocated_hglobals.Add(ptr); - } - } - } - - static readonly int name_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("name"); - - public bool NonTerminalHint - { - get { return Marshal.ReadInt32(handle, non_terminal_hint_offset) != 0; } - } - - static readonly int non_terminal_hint_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("non_terminal_hint"); - - public int BytesPerFrame - { - get { return Marshal.ReadInt32(handle, bytes_per_frame_offset); } - } - - static readonly int bytes_per_frame_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("bytes_per_frame"); - - public int BytesPerSample - { - get { return Marshal.ReadInt32(handle, bytes_per_sample_offset); } - } - - static readonly int bytes_per_sample_offset = (int)Marshal.OffsetOf<SoundIoOutStream>("bytes_per_sample"); - - public string LayoutErrorMessage - { - get - { - var code = (SoundIoError)Marshal.ReadInt32(handle, layout_error_offset); - - return code == SoundIoError.SoundIoErrorNone ? null : Marshal.PtrToStringAnsi(Natives.soundio_strerror((int)code)); - } - } - - static readonly int layout_error_offset = (int)Marshal.OffsetOf<SoundIoOutStream> ("layout_error"); - - // functions - - public void Open () - { - var ret = (SoundIoError)Natives.soundio_outstream_open(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void Start () - { - var ret = (SoundIoError)Natives.soundio_outstream_start(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public SoundIOChannelAreas BeginWrite(ref int frameCount) - { - IntPtr ptrs = default; - int nativeFrameCount = frameCount; - - unsafe - { - var frameCountPtr = &nativeFrameCount; - var ptrptr = &ptrs; - var ret = (SoundIoError)Natives.soundio_outstream_begin_write(handle, (IntPtr)ptrptr, (IntPtr)frameCountPtr); - - frameCount = *frameCountPtr; - - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - - return new SoundIOChannelAreas(ptrs, Layout.ChannelCount, frameCount); - } - } - - public void EndWrite () - { - var ret = (SoundIoError)Natives.soundio_outstream_end_write(handle); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public void ClearBuffer () - { - _ = Natives.soundio_outstream_clear_buffer(handle); - } - - public void Pause (bool pause) - { - var ret = (SoundIoError)Natives.soundio_outstream_pause(handle, pause); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - - public double GetLatency () - { - unsafe - { - double* dptr = null; - IntPtr p = new IntPtr(dptr); - - var ret = (SoundIoError)Natives.soundio_outstream_get_latency(handle, p); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - - dptr = (double*)p; - - return *dptr; - } - } - - public void SetVolume (double volume) - { - var ret = (SoundIoError)Natives.soundio_outstream_set_volume(handle, volume); - if (ret != SoundIoError.SoundIoErrorNone) - { - throw new SoundIOException(ret); - } - } - } -} diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIORingBuffer.cs b/Ryujinx.Audio/Native/libsoundio/SoundIORingBuffer.cs deleted file mode 100644 index 7530da72..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIORingBuffer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -namespace SoundIOSharp -{ - public class SoundIORingBuffer : IDisposable - { - internal SoundIORingBuffer(IntPtr handle) - { - this.handle = handle; - } - - IntPtr handle; - - public int Capacity - { - get { return Natives.soundio_ring_buffer_capacity(handle); } - } - - public void Clear() - { - Natives.soundio_ring_buffer_clear(handle); - } - - public void Dispose() - { - Natives.soundio_ring_buffer_destroy(handle); - } - - public int FillCount - { - get { return Natives.soundio_ring_buffer_fill_count(handle); } - } - - public int FreeCount - { - get { return Natives.soundio_ring_buffer_free_count(handle); } - } - - public IntPtr ReadPointer - { - get { return Natives.soundio_ring_buffer_read_ptr(handle); } - } - - public IntPtr WritePointer - { - get { return Natives.soundio_ring_buffer_write_ptr(handle); } - } - - public void AdvanceReadPointer(int count) - { - Natives.soundio_ring_buffer_advance_read_ptr(handle, count); - } - - public void AdvanceWritePointer(int count) - { - Natives.soundio_ring_buffer_advance_write_ptr(handle, count); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Native/libsoundio/SoundIOSampleRateRange.cs b/Ryujinx.Audio/Native/libsoundio/SoundIOSampleRateRange.cs deleted file mode 100644 index e01ec2bd..00000000 --- a/Ryujinx.Audio/Native/libsoundio/SoundIOSampleRateRange.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -namespace SoundIOSharp -{ - public struct SoundIOSampleRateRange - { - internal SoundIOSampleRateRange(int min, int max) - { - Min = min; - Max = max; - } - - public readonly int Min; - public readonly int Max; - } -} diff --git a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dll b/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dll Binary files differdeleted file mode 100644 index 48804312..00000000 --- a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dll +++ /dev/null diff --git a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dylib b/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dylib Binary files differdeleted file mode 100644 index 10171f4f..00000000 --- a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.dylib +++ /dev/null diff --git a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.so b/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.so Binary files differdeleted file mode 100644 index 87c8b506..00000000 --- a/Ryujinx.Audio/Native/libsoundio/libs/libsoundio.so +++ /dev/null diff --git a/Ryujinx.Audio/Native/libsoundio/libsoundio-interop.cs b/Ryujinx.Audio/Native/libsoundio/libsoundio-interop.cs deleted file mode 100644 index 5377582f..00000000 --- a/Ryujinx.Audio/Native/libsoundio/libsoundio-interop.cs +++ /dev/null @@ -1,643 +0,0 @@ -// This source file is generated by nclang PInvokeGenerator. -using System; -using System.Runtime.InteropServices; -using delegate0 = SoundIOSharp.Delegates.delegate0; -using delegate1 = SoundIOSharp.Delegates.delegate1; -using delegate2 = SoundIOSharp.Delegates.delegate2; -using delegate3 = SoundIOSharp.Delegates.delegate3; -using delegate4 = SoundIOSharp.Delegates.delegate4; -using delegate5 = SoundIOSharp.Delegates.delegate5; -using delegate6 = SoundIOSharp.Delegates.delegate6; -using delegate7 = SoundIOSharp.Delegates.delegate7; -using delegate8 = SoundIOSharp.Delegates.delegate8; -using delegate9 = SoundIOSharp.Delegates.delegate9; - -namespace SoundIOSharp -{ - enum SoundIoError // soundio.h (72, 6) - { - SoundIoErrorNone = 0, - SoundIoErrorNoMem = 1, - SoundIoErrorInitAudioBackend = 2, - SoundIoErrorSystemResources = 3, - SoundIoErrorOpeningDevice = 4, - SoundIoErrorNoSuchDevice = 5, - SoundIoErrorInvalid = 6, - SoundIoErrorBackendUnavailable = 7, - SoundIoErrorStreaming = 8, - SoundIoErrorIncompatibleDevice = 9, - SoundIoErrorNoSuchClient = 10, - SoundIoErrorIncompatibleBackend = 11, - SoundIoErrorBackendDisconnected = 12, - SoundIoErrorInterrupted = 13, - SoundIoErrorUnderflow = 14, - SoundIoErrorEncodingString = 15, - } - - enum SoundIoChannelId // soundio.h (106, 6) - { - SoundIoChannelIdInvalid = 0, - SoundIoChannelIdFrontLeft = 1, - SoundIoChannelIdFrontRight = 2, - SoundIoChannelIdFrontCenter = 3, - SoundIoChannelIdLfe = 4, - SoundIoChannelIdBackLeft = 5, - SoundIoChannelIdBackRight = 6, - SoundIoChannelIdFrontLeftCenter = 7, - SoundIoChannelIdFrontRightCenter = 8, - SoundIoChannelIdBackCenter = 9, - SoundIoChannelIdSideLeft = 10, - SoundIoChannelIdSideRight = 11, - SoundIoChannelIdTopCenter = 12, - SoundIoChannelIdTopFrontLeft = 13, - SoundIoChannelIdTopFrontCenter = 14, - SoundIoChannelIdTopFrontRight = 15, - SoundIoChannelIdTopBackLeft = 16, - SoundIoChannelIdTopBackCenter = 17, - SoundIoChannelIdTopBackRight = 18, - SoundIoChannelIdBackLeftCenter = 19, - SoundIoChannelIdBackRightCenter = 20, - SoundIoChannelIdFrontLeftWide = 21, - SoundIoChannelIdFrontRightWide = 22, - SoundIoChannelIdFrontLeftHigh = 23, - SoundIoChannelIdFrontCenterHigh = 24, - SoundIoChannelIdFrontRightHigh = 25, - SoundIoChannelIdTopFrontLeftCenter = 26, - SoundIoChannelIdTopFrontRightCenter = 27, - SoundIoChannelIdTopSideLeft = 28, - SoundIoChannelIdTopSideRight = 29, - SoundIoChannelIdLeftLfe = 30, - SoundIoChannelIdRightLfe = 31, - SoundIoChannelIdLfe2 = 32, - SoundIoChannelIdBottomCenter = 33, - SoundIoChannelIdBottomLeftCenter = 34, - SoundIoChannelIdBottomRightCenter = 35, - SoundIoChannelIdMsMid = 36, - SoundIoChannelIdMsSide = 37, - SoundIoChannelIdAmbisonicW = 38, - SoundIoChannelIdAmbisonicX = 39, - SoundIoChannelIdAmbisonicY = 40, - SoundIoChannelIdAmbisonicZ = 41, - SoundIoChannelIdXyX = 42, - SoundIoChannelIdXyY = 43, - SoundIoChannelIdHeadphonesLeft = 44, - SoundIoChannelIdHeadphonesRight = 45, - SoundIoChannelIdClickTrack = 46, - SoundIoChannelIdForeignLanguage = 47, - SoundIoChannelIdHearingImpaired = 48, - SoundIoChannelIdNarration = 49, - SoundIoChannelIdHaptic = 50, - SoundIoChannelIdDialogCentricMix = 51, - SoundIoChannelIdAux = 52, - SoundIoChannelIdAux0 = 53, - SoundIoChannelIdAux1 = 54, - SoundIoChannelIdAux2 = 55, - SoundIoChannelIdAux3 = 56, - SoundIoChannelIdAux4 = 57, - SoundIoChannelIdAux5 = 58, - SoundIoChannelIdAux6 = 59, - SoundIoChannelIdAux7 = 60, - SoundIoChannelIdAux8 = 61, - SoundIoChannelIdAux9 = 62, - SoundIoChannelIdAux10 = 63, - SoundIoChannelIdAux11 = 64, - SoundIoChannelIdAux12 = 65, - SoundIoChannelIdAux13 = 66, - SoundIoChannelIdAux14 = 67, - SoundIoChannelIdAux15 = 68, - } - - enum SoundIoChannelLayoutId // soundio.h (189, 6) - { - SoundIoChannelLayoutIdMono = 0, - SoundIoChannelLayoutIdStereo = 1, - SoundIoChannelLayoutId2Point1 = 2, - SoundIoChannelLayoutId3Point0 = 3, - SoundIoChannelLayoutId3Point0Back = 4, - SoundIoChannelLayoutId3Point1 = 5, - SoundIoChannelLayoutId4Point0 = 6, - SoundIoChannelLayoutIdQuad = 7, - SoundIoChannelLayoutIdQuadSide = 8, - SoundIoChannelLayoutId4Point1 = 9, - SoundIoChannelLayoutId5Point0Back = 10, - SoundIoChannelLayoutId5Point0Side = 11, - SoundIoChannelLayoutId5Point1 = 12, - SoundIoChannelLayoutId5Point1Back = 13, - SoundIoChannelLayoutId6Point0Side = 14, - SoundIoChannelLayoutId6Point0Front = 15, - SoundIoChannelLayoutIdHexagonal = 16, - SoundIoChannelLayoutId6Point1 = 17, - SoundIoChannelLayoutId6Point1Back = 18, - SoundIoChannelLayoutId6Point1Front = 19, - SoundIoChannelLayoutId7Point0 = 20, - SoundIoChannelLayoutId7Point0Front = 21, - SoundIoChannelLayoutId7Point1 = 22, - SoundIoChannelLayoutId7Point1Wide = 23, - SoundIoChannelLayoutId7Point1WideBack = 24, - SoundIoChannelLayoutIdOctagonal = 25, - } - - enum SoundIoBackend // soundio.h (218, 6) - { - SoundIoBackendNone = 0, - SoundIoBackendJack = 1, - SoundIoBackendPulseAudio = 2, - SoundIoBackendAlsa = 3, - SoundIoBackendCoreAudio = 4, - SoundIoBackendWasapi = 5, - SoundIoBackendDummy = 6, - } - - enum SoundIoDeviceAim // soundio.h (228, 6) - { - SoundIoDeviceAimInput = 0, - SoundIoDeviceAimOutput = 1, - } - - enum SoundIoFormat // soundio.h (235, 6) - { - SoundIoFormatInvalid = 0, - SoundIoFormatS8 = 1, - SoundIoFormatU8 = 2, - SoundIoFormatS16LE = 3, - SoundIoFormatS16BE = 4, - SoundIoFormatU16LE = 5, - SoundIoFormatU16BE = 6, - SoundIoFormatS24LE = 7, - SoundIoFormatS24BE = 8, - SoundIoFormatU24LE = 9, - SoundIoFormatU24BE = 10, - SoundIoFormatS32LE = 11, - SoundIoFormatS32BE = 12, - SoundIoFormatU32LE = 13, - SoundIoFormatU32BE = 14, - SoundIoFormatFloat32LE = 15, - SoundIoFormatFloat32BE = 16, - SoundIoFormatFloat64LE = 17, - SoundIoFormatFloat64BE = 18, - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoChannelLayout // soundio.h (306, 8) - { - [CTypeDetails("Pointer<byte>")] public System.IntPtr @name; - public int @channel_count; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] - [CTypeDetails("ConstArrayOf<SoundIoChannelId>")] public SoundIoChannelId[] @channels; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoSampleRateRange // soundio.h (313, 8) - { - public int @min; - public int @max; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoChannelArea // soundio.h (319, 8) - { - [CTypeDetails("Pointer<byte>")] public System.IntPtr @ptr; - public int @step; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIo // soundio.h (328, 8) - { - [CTypeDetails("Pointer<void>")] public System.IntPtr @userdata; - [CTypeDetails("Pointer<void (SoundIo *)>")] public delegate0 @on_devices_change; - [CTypeDetails("Pointer<void (SoundIo *, int)>")] public delegate1 @on_backend_disconnect; - [CTypeDetails("Pointer<void (SoundIo *)>")] public Delegates.delegate0 @on_events_signal; - public SoundIoBackend @current_backend; - [CTypeDetails("Pointer<byte>")] public System.IntPtr @app_name; - [CTypeDetails("Pointer<void ()>")] public delegate2 @emit_rtprio_warning; - [CTypeDetails("Pointer<void (const char *)>")] public delegate3 @jack_info_callback; - [CTypeDetails("Pointer<void (const char *)>")] public Delegates.delegate3 @jack_error_callback; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoDevice // soundio.h (387, 8) - { - [CTypeDetails("Pointer<SoundIo>")] public System.IntPtr @soundio; - [CTypeDetails("Pointer<byte>")] public System.IntPtr @id; - [CTypeDetails("Pointer<byte>")] public System.IntPtr @name; - public SoundIoDeviceAim @aim; - [CTypeDetails("Pointer<SoundIoChannelLayout>")] public System.IntPtr @layouts; - public int @layout_count; - public SoundIoChannelLayout @current_layout; - [CTypeDetails("Pointer<SoundIoFormat>")] public System.IntPtr @formats; - public int @format_count; - public SoundIoFormat @current_format; - [CTypeDetails("Pointer<SoundIoSampleRateRange>")] public System.IntPtr @sample_rates; - public int @sample_rate_count; - public int @sample_rate_current; - public double @software_latency_min; - public double @software_latency_max; - public double @software_latency_current; - public bool @is_raw; - public int @ref_count; - public int @probe_error; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoOutStream // soundio.h (497, 8) - { - [CTypeDetails("Pointer<SoundIoDevice>")] public System.IntPtr @device; - public SoundIoFormat @format; - public int @sample_rate; - public SoundIoChannelLayout @layout; - public double @software_latency; - public float @volume; - [CTypeDetails("Pointer<void>")] public System.IntPtr @userdata; - [CTypeDetails("Pointer<void (SoundIoOutStream *, int, int)>")] public delegate4 @write_callback; - [CTypeDetails("Pointer<void (SoundIoOutStream *)>")] public delegate5 @underflow_callback; - [CTypeDetails("Pointer<void (SoundIoOutStream *, int)>")] public delegate6 @error_callback; - [CTypeDetails("Pointer<byte>")] public System.IntPtr @name; - public bool @non_terminal_hint; - public int @bytes_per_frame; - public int @bytes_per_sample; - public int @layout_error; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoInStream // soundio.h (600, 8) - { - [CTypeDetails("Pointer<SoundIoDevice>")] public System.IntPtr @device; - public SoundIoFormat @format; - public int @sample_rate; - public SoundIoChannelLayout @layout; - public double @software_latency; - [CTypeDetails("Pointer<void>")] public System.IntPtr @userdata; - [CTypeDetails("Pointer<void (SoundIoInStream *, int, int)>")] public delegate7 @read_callback; - [CTypeDetails("Pointer<void (SoundIoInStream *)>")] public delegate8 @overflow_callback; - [CTypeDetails("Pointer<void (SoundIoInStream *, int)>")] public delegate9 @error_callback; - [CTypeDetails("Pointer<byte>")] public System.IntPtr @name; - public bool @non_terminal_hint; - public int @bytes_per_frame; - public int @bytes_per_sample; - public int @layout_error; - } - - [StructLayout(LayoutKind.Sequential)] - struct SoundIoRingBuffer // soundio.h (1170, 8) - { - } - - partial class Natives - { - const string LibraryName = "libsoundio"; - // function soundio_version_string - soundio.h (682, 28) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_version_string(); - - // function soundio_version_major - soundio.h (684, 20) - [DllImport(LibraryName)] - internal static extern int soundio_version_major(); - - // function soundio_version_minor - soundio.h (686, 20) - [DllImport(LibraryName)] - internal static extern int soundio_version_minor(); - - // function soundio_version_patch - soundio.h (688, 20) - [DllImport(LibraryName)] - internal static extern int soundio_version_patch(); - - // function soundio_create - soundio.h (694, 32) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_create(); - - // function soundio_destroy - soundio.h (695, 21) - [DllImport(LibraryName)] - internal static extern void soundio_destroy([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_connect - soundio.h (705, 20) - [DllImport(LibraryName)] - internal static extern int soundio_connect([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_connect_backend - soundio.h (717, 20) - [DllImport(LibraryName)] - internal static extern int soundio_connect_backend([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio, SoundIoBackend @backend); - - // function soundio_disconnect - soundio.h (718, 21) - [DllImport(LibraryName)] - internal static extern void soundio_disconnect([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_strerror - soundio.h (721, 28) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_strerror(int @error); - - // function soundio_backend_name - soundio.h (723, 28) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_backend_name(SoundIoBackend @backend); - - // function soundio_backend_count - soundio.h (726, 20) - [DllImport(LibraryName)] - internal static extern int soundio_backend_count([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_get_backend - soundio.h (729, 36) - [DllImport(LibraryName)] - internal static extern SoundIoBackend soundio_get_backend([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio, int @index); - - // function soundio_have_backend - soundio.h (732, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_have_backend(SoundIoBackend @backend); - - // function soundio_flush_events - soundio.h (756, 21) - [DllImport(LibraryName)] - internal static extern void soundio_flush_events([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_wait_events - soundio.h (760, 21) - [DllImport(LibraryName)] - internal static extern void soundio_wait_events([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_wakeup - soundio.h (763, 21) - [DllImport(LibraryName)] - internal static extern void soundio_wakeup([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_force_device_scan - soundio.h (780, 21) - [DllImport(LibraryName)] - internal static extern void soundio_force_device_scan([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_channel_layout_equal - soundio.h (787, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_channel_layout_equal([CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @a, [CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @b); - - // function soundio_get_channel_name - soundio.h (791, 28) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_get_channel_name(SoundIoChannelId @id); - - // function soundio_parse_channel_id - soundio.h (795, 38) - [DllImport(LibraryName)] - internal static extern SoundIoChannelId soundio_parse_channel_id([CTypeDetails("Pointer<byte>")]System.IntPtr @str, int @str_len); - - // function soundio_channel_layout_builtin_count - soundio.h (798, 20) - [DllImport(LibraryName)] - internal static extern int soundio_channel_layout_builtin_count(); - - // function soundio_channel_layout_get_builtin - soundio.h (803, 51) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_channel_layout_get_builtin(int @index); - - // function soundio_channel_layout_get_default - soundio.h (806, 51) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_channel_layout_get_default(int @channel_count); - - // function soundio_channel_layout_find_channel - soundio.h (809, 20) - [DllImport(LibraryName)] - internal static extern int soundio_channel_layout_find_channel([CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @layout, SoundIoChannelId @channel); - - // function soundio_channel_layout_detect_builtin - soundio.h (814, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_channel_layout_detect_builtin([CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @layout); - - // function soundio_best_matching_channel_layout - soundio.h (819, 51) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_best_matching_channel_layout([CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @preferred_layouts, int @preferred_layout_count, [CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @available_layouts, int @available_layout_count); - - // function soundio_sort_channel_layouts - soundio.h (824, 21) - [DllImport(LibraryName)] - internal static extern void soundio_sort_channel_layouts([CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @layouts, int @layout_count); - - // function soundio_get_bytes_per_sample - soundio.h (830, 20) - [DllImport(LibraryName)] - internal static extern int soundio_get_bytes_per_sample(SoundIoFormat @format); - - // function soundio_get_bytes_per_frame - soundio.h (833, 19) - [DllImport(LibraryName)] - internal static extern int soundio_get_bytes_per_frame(SoundIoFormat @format, int @channel_count); - - // function soundio_get_bytes_per_second - soundio.h (838, 19) - [DllImport(LibraryName)] - internal static extern int soundio_get_bytes_per_second(SoundIoFormat @format, int @channel_count, int @sample_rate); - - // function soundio_format_string - soundio.h (845, 29) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_format_string(SoundIoFormat @format); - - // function soundio_input_device_count - soundio.h (861, 20) - [DllImport(LibraryName)] - internal static extern int soundio_input_device_count([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_output_device_count - soundio.h (864, 20) - [DllImport(LibraryName)] - internal static extern int soundio_output_device_count([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_get_input_device - soundio.h (870, 38) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_get_input_device([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio, int @index); - - // function soundio_get_output_device - soundio.h (875, 38) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_get_output_device([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio, int @index); - - // function soundio_default_input_device_index - soundio.h (880, 20) - [DllImport(LibraryName)] - internal static extern int soundio_default_input_device_index([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_default_output_device_index - soundio.h (885, 20) - [DllImport(LibraryName)] - internal static extern int soundio_default_output_device_index([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio); - - // function soundio_device_ref - soundio.h (888, 21) - [DllImport(LibraryName)] - internal static extern void soundio_device_ref([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device); - - // function soundio_device_unref - soundio.h (891, 21) - [DllImport(LibraryName)] - internal static extern void soundio_device_unref([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device); - - // function soundio_device_equal - soundio.h (895, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_device_equal([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @a, [CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @b); - - // function soundio_device_sort_channel_layouts - soundio.h (900, 21) - [DllImport(LibraryName)] - internal static extern void soundio_device_sort_channel_layouts([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device); - - // function soundio_device_supports_format - soundio.h (904, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_device_supports_format([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device, SoundIoFormat @format); - - // function soundio_device_supports_layout - soundio.h (909, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_device_supports_layout([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device, [CTypeDetails("Pointer<SoundIoChannelLayout>")]System.IntPtr @layout); - - // function soundio_device_supports_sample_rate - soundio.h (914, 21) - [DllImport(LibraryName)] - internal static extern bool soundio_device_supports_sample_rate([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device, int @sample_rate); - - // function soundio_device_nearest_sample_rate - soundio.h (919, 20) - [DllImport(LibraryName)] - internal static extern int soundio_device_nearest_sample_rate([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device, int @sample_rate); - - // function soundio_outstream_create - soundio.h (929, 41) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_outstream_create([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device); - - // function soundio_outstream_destroy - soundio.h (931, 21) - [DllImport(LibraryName)] - internal static extern void soundio_outstream_destroy([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream); - - // function soundio_outstream_open - soundio.h (954, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_open([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream); - - // function soundio_outstream_start - soundio.h (965, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_start([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream); - - // function soundio_outstream_begin_write - soundio.h (997, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_begin_write([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream, [CTypeDetails("Pointer<System.IntPtr>")]System.IntPtr @areas, [CTypeDetails("Pointer<int>")]System.IntPtr @frame_count); - - // function soundio_outstream_end_write - soundio.h (1009, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_end_write([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream); - - // function soundio_outstream_clear_buffer - soundio.h (1024, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_clear_buffer([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream); - - // function soundio_outstream_pause - soundio.h (1045, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_pause([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream, bool @pause); - - // function soundio_outstream_get_latency - soundio.h (1058, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_get_latency([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream, [CTypeDetails("Pointer<double>")]System.IntPtr @out_latency); - - // function soundio_outstream_set_volume - soundio.h (1061, 20) - [DllImport(LibraryName)] - internal static extern int soundio_outstream_set_volume([CTypeDetails("Pointer<SoundIoOutStream>")]System.IntPtr @outstream, double @volume); - - // function soundio_instream_create - soundio.h (1071, 40) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_instream_create([CTypeDetails("Pointer<SoundIoDevice>")]System.IntPtr @device); - - // function soundio_instream_destroy - soundio.h (1073, 21) - [DllImport(LibraryName)] - internal static extern void soundio_instream_destroy([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream); - - // function soundio_instream_open - soundio.h (1093, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_open([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream); - - // function soundio_instream_start - soundio.h (1102, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_start([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream); - - // function soundio_instream_begin_read - soundio.h (1133, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_begin_read([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream, [CTypeDetails("Pointer<System.IntPtr>")]System.IntPtr @areas, [CTypeDetails("Pointer<int>")]System.IntPtr @frame_count); - - // function soundio_instream_end_read - soundio.h (1143, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_end_read([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream); - - // function soundio_instream_pause - soundio.h (1156, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_pause([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream, bool @pause); - - // function soundio_instream_get_latency - soundio.h (1166, 20) - [DllImport(LibraryName)] - internal static extern int soundio_instream_get_latency([CTypeDetails("Pointer<SoundIoInStream>")]System.IntPtr @instream, [CTypeDetails("Pointer<double>")]System.IntPtr @out_latency); - - // function soundio_ring_buffer_create - soundio.h (1181, 42) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_ring_buffer_create([CTypeDetails("Pointer<SoundIo>")]System.IntPtr @soundio, int @requested_capacity); - - // function soundio_ring_buffer_destroy - soundio.h (1182, 21) - [DllImport(LibraryName)] - internal static extern void soundio_ring_buffer_destroy([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_capacity - soundio.h (1186, 20) - [DllImport(LibraryName)] - internal static extern int soundio_ring_buffer_capacity([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_write_ptr - soundio.h (1189, 22) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_ring_buffer_write_ptr([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_advance_write_ptr - soundio.h (1191, 21) - [DllImport(LibraryName)] - internal static extern void soundio_ring_buffer_advance_write_ptr([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer, int @count); - - // function soundio_ring_buffer_read_ptr - soundio.h (1194, 22) - [DllImport(LibraryName)] - internal static extern System.IntPtr soundio_ring_buffer_read_ptr([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_advance_read_ptr - soundio.h (1196, 21) - [DllImport(LibraryName)] - internal static extern void soundio_ring_buffer_advance_read_ptr([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer, int @count); - - // function soundio_ring_buffer_fill_count - soundio.h (1199, 20) - [DllImport(LibraryName)] - internal static extern int soundio_ring_buffer_fill_count([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_free_count - soundio.h (1202, 20) - [DllImport(LibraryName)] - internal static extern int soundio_ring_buffer_free_count([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - // function soundio_ring_buffer_clear - soundio.h (1205, 21) - [DllImport(LibraryName)] - internal static extern void soundio_ring_buffer_clear([CTypeDetails("Pointer<SoundIoRingBuffer>")]System.IntPtr @ring_buffer); - - } - - class Delegates - { - public delegate void delegate0(System.IntPtr p0); - public delegate void delegate1(System.IntPtr p0, int p1); - public delegate void delegate2(); - public delegate void delegate3(System.IntPtr p0); - public delegate void delegate4(System.IntPtr p0, int p1, int p2); - public delegate void delegate5(System.IntPtr p0); - public delegate void delegate6(System.IntPtr p0, int p1); - public delegate void delegate7(System.IntPtr p0, int p1, int p2); - public delegate void delegate8(System.IntPtr p0); - public delegate void delegate9(System.IntPtr p0, int p1); - } - - public struct Pointer<T> - { - public IntPtr Handle; - public static implicit operator IntPtr(Pointer<T> value) { return value.Handle; } - public static implicit operator Pointer<T>(IntPtr value) { return new Pointer<T>(value); } - - public Pointer(IntPtr handle) - { - Handle = handle; - } - - public override bool Equals(object obj) - { - return obj is Pointer<T> && this == (Pointer<T>)obj; - } - - public override int GetHashCode() - { - return (int)Handle; - } - - public static bool operator ==(Pointer<T> p1, Pointer<T> p2) - { - return p1.Handle == p2.Handle; - } - - public static bool operator !=(Pointer<T> p1, Pointer<T> p2) - { - return p1.Handle != p2.Handle; - } - } - public struct ArrayOf<T> { } - public struct ConstArrayOf<T> { } - public class CTypeDetailsAttribute : Attribute - { - public CTypeDetailsAttribute(string value) - { - Value = value; - } - - public string Value { get; set; } - } - -} diff --git a/Ryujinx.Audio/Output/AudioOutputManager.cs b/Ryujinx.Audio/Output/AudioOutputManager.cs new file mode 100644 index 00000000..baa84997 --- /dev/null +++ b/Ryujinx.Audio/Output/AudioOutputManager.cs @@ -0,0 +1,256 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Output +{ + /// <summary> + /// The audio output manager. + /// </summary> + public class AudioOutputManager : IDisposable + { + private object _lock = new object(); + + /// <summary> + /// Lock used for session allocation. + /// </summary> + private object _sessionLock = new object(); + + /// <summary> + /// The session ids allocation table. + /// </summary> + private int[] _sessionIds; + + /// <summary> + /// The device driver. + /// </summary> + private IHardwareDeviceDriver _deviceDriver; + + /// <summary> + /// The events linked to each session. + /// </summary> + private IWritableEvent[] _sessionsBufferEvents; + + /// <summary> + /// The <see cref="AudioOutputSystem"/> session instances. + /// </summary> + private AudioOutputSystem[] _sessions; + + /// <summary> + /// The count of active sessions. + /// </summary> + private int _activeSessionCount; + + /// <summary> + /// Create a new <see cref="AudioOutputManager"/>. + /// </summary> + public AudioOutputManager() + { + _sessionIds = new int[Constants.AudioOutSessionCountMax]; + _sessions = new AudioOutputSystem[Constants.AudioOutSessionCountMax]; + _activeSessionCount = 0; + + for (int i = 0; i < _sessionIds.Length; i++) + { + _sessionIds[i] = i; + } + } + + /// <summary> + /// Initialize the <see cref="AudioOutputManager"/>. + /// </summary> + /// <param name="deviceDriver">The device driver.</param> + /// <param name="sessionRegisterEvents">The events associated to each session.</param> + public void Initialize(IHardwareDeviceDriver deviceDriver, IWritableEvent[] sessionRegisterEvents) + { + _deviceDriver = deviceDriver; + _sessionsBufferEvents = sessionRegisterEvents; + } + + /// <summary> + /// Acquire a new session id. + /// </summary> + /// <returns>A new session id.</returns> + private int AcquireSessionId() + { + lock (_sessionLock) + { + int index = _activeSessionCount; + + Debug.Assert(index < _sessionIds.Length); + + int sessionId = _sessionIds[index]; + + _sessionIds[index] = -1; + + _activeSessionCount++; + + Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new output ({sessionId})"); + + return sessionId; + } + } + + /// <summary> + /// Release a given <paramref name="sessionId"/>. + /// </summary> + /// <param name="sessionId">The session id to release.</param> + private void ReleaseSessionId(int sessionId) + { + lock (_sessionLock) + { + Debug.Assert(_activeSessionCount > 0); + + int newIndex = --_activeSessionCount; + + _sessionIds[newIndex] = sessionId; + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered output ({sessionId})"); + } + + /// <summary> + /// Used to update audio output system. + /// </summary> + public void Update() + { + lock (_sessionLock) + { + foreach (AudioOutputSystem output in _sessions) + { + output?.Update(); + } + } + } + + /// <summary> + /// Register a new <see cref="AudioOutputSystem"/>. + /// </summary> + /// <param name="output">The <see cref="AudioOutputSystem"/> to register.</param> + private void Register(AudioOutputSystem output) + { + lock (_sessionLock) + { + _sessions[output.GetSessionId()] = output; + } + } + + /// <summary> + /// Unregister a new <see cref="AudioOutputSystem"/>. + /// </summary> + /// <param name="output">The <see cref="AudioOutputSystem"/> to unregister.</param> + internal void Unregister(AudioOutputSystem output) + { + lock (_sessionLock) + { + int sessionId = output.GetSessionId(); + + _sessions[output.GetSessionId()] = null; + + ReleaseSessionId(sessionId); + } + } + + /// <summary> + /// Get the list of all audio outputs name. + /// </summary> + /// <returns>The list of all audio outputs name</returns> + public string[] ListAudioOuts() + { + return new string[] { Constants.DefaultDeviceOutputName }; + } + + /// <summary> + /// Open a new <see cref="AudioOutputSystem"/>. + /// </summary> + /// <param name="outputDeviceName">The output device name selected by the <see cref="AudioOutputSystem"/></param> + /// <param name="outputConfiguration">The output audio configuration selected by the <see cref="AudioOutputSystem"/></param> + /// <param name="obj">The new <see cref="AudioOutputSystem"/></param> + /// <param name="memoryManager">The memory manager that will be used for all guest memory operations</param> + /// <param name="inputDeviceName">The input device name wanted by the user</param> + /// <param name="sampleFormat">The sample format to use</param> + /// <param name="parameter">The user configuration</param> + /// <param name="appletResourceUserId">The applet resource user id of the application</param> + /// <param name="processHandle">The process handle of the application</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode OpenAudioOut(out string outputDeviceName, + out AudioOutputConfiguration outputConfiguration, + out AudioOutputSystem obj, + IVirtualMemoryManager memoryManager, + string inputDeviceName, + SampleFormat sampleFormat, + ref AudioInputConfiguration parameter, + ulong appletResourceUserId, + uint processHandle) + { + int sessionId = AcquireSessionId(); + + _sessionsBufferEvents[sessionId].Clear(); + + IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount); + + AudioOutputSystem audioOut = new AudioOutputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]); + + ResultCode result = audioOut.Initialize(inputDeviceName, sampleFormat, ref parameter, sessionId); + + if (result == ResultCode.Success) + { + outputDeviceName = audioOut.DeviceName; + outputConfiguration = new AudioOutputConfiguration + { + ChannelCount = audioOut.ChannelCount, + SampleFormat = audioOut.SampleFormat, + SampleRate = audioOut.SampleRate, + AudioOutState = audioOut.GetState(), + }; + + obj = audioOut; + + Register(audioOut); + } + else + { + ReleaseSessionId(sessionId); + + obj = null; + outputDeviceName = null; + outputConfiguration = default; + } + + return result; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Nothing to do here. + } + } + } +} diff --git a/Ryujinx.Audio/Output/AudioOutputSystem.cs b/Ryujinx.Audio/Output/AudioOutputSystem.cs new file mode 100644 index 00000000..f5db9d7a --- /dev/null +++ b/Ryujinx.Audio/Output/AudioOutputSystem.cs @@ -0,0 +1,373 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using System; + +namespace Ryujinx.Audio.Output +{ + /// <summary> + /// Audio output system. + /// </summary> + public class AudioOutputSystem : IDisposable + { + /// <summary> + /// The session id associated to the <see cref="AudioOutputSystem"/>. + /// </summary> + private int _sessionId; + + /// <summary> + /// The session the <see cref="AudioOutputSystem"/>. + /// </summary> + private AudioDeviceSession _session; + + /// <summary> + /// The target device name of the <see cref="AudioOutputSystem"/>. + /// </summary> + public string DeviceName { get; private set; } + + /// <summary> + /// The target sample rate of the <see cref="AudioOutputSystem"/>. + /// </summary> + public uint SampleRate { get; private set; } + + /// <summary> + /// The target channel count of the <see cref="AudioOutputSystem"/>. + /// </summary> + public uint ChannelCount { get; private set; } + + /// <summary> + /// The target sample format of the <see cref="AudioOutputSystem"/>. + /// </summary> + public SampleFormat SampleFormat { get; private set; } + + /// <summary> + /// The <see cref="AudioOutputManager"/> owning this. + /// </summary> + private AudioOutputManager _manager; + + /// <summary> + /// THe lock of the parent. + /// </summary> + private object _parentLock; + + /// <summary> + /// Create a new <see cref="AudioOutputSystem"/>. + /// </summary> + /// <param name="manager">The manager instance</param> + /// <param name="parentLock">The lock of the manager</param> + /// <param name="deviceSession">The hardware device session</param> + /// <param name="bufferEvent">The buffer release event of the audio output</param> + public AudioOutputSystem(AudioOutputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent) + { + _manager = manager; + _parentLock = parentLock; + _session = new AudioDeviceSession(deviceSession, bufferEvent); + } + + /// <summary> + /// Get the default device name on the system. + /// </summary> + /// <returns>The default device name on the system.</returns> + private static string GetDeviceDefaultName() + { + return Constants.DefaultDeviceOutputName; + } + + /// <summary> + /// Check if a given configuration and device name is valid on the system. + /// </summary> + /// <param name="configuration">The configuration to check.</param> + /// <param name="deviceName">The device name to check.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + private static ResultCode IsConfigurationValid(ref AudioInputConfiguration configuration, string deviceName) + { + if (deviceName.Length != 0 && !deviceName.Equals(GetDeviceDefaultName())) + { + return ResultCode.DeviceNotFound; + } + else if (configuration.SampleRate != 0 && configuration.SampleRate != Constants.TargetSampleRate) + { + return ResultCode.UnsupportedSampleRate; + } + else if (configuration.ChannelCount != 0 && configuration.ChannelCount != 1 && configuration.ChannelCount != 2 && configuration.ChannelCount != 6) + { + return ResultCode.UnsupportedChannelConfiguration; + } + + return ResultCode.Success; + } + + /// <summary> + /// Get the released buffer event. + /// </summary> + /// <returns>The released buffer event</returns> + public IWritableEvent RegisterBufferEvent() + { + lock (_parentLock) + { + return _session.GetBufferEvent(); + } + } + + /// <summary> + /// Update the <see cref="AudioOutputSystem"/>. + /// </summary> + public void Update() + { + lock (_parentLock) + { + _session.Update(); + } + } + + /// <summary> + /// Get the id of this session. + /// </summary> + /// <returns>The id of this session</returns> + public int GetSessionId() + { + return _sessionId; + } + + /// <summary> + /// Initialize the <see cref="AudioOutputSystem"/>. + /// </summary> + /// <param name="inputDeviceName">The input device name wanted by the user</param> + /// <param name="sampleFormat">The sample format to use</param> + /// <param name="parameter">The user configuration</param> + /// <param name="sessionId">The session id associated to this <see cref="AudioOutputSystem"/></param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode Initialize(string inputDeviceName, SampleFormat sampleFormat, ref AudioInputConfiguration parameter, int sessionId) + { + _sessionId = sessionId; + + ResultCode result = IsConfigurationValid(ref parameter, inputDeviceName); + + if (result == ResultCode.Success) + { + if (inputDeviceName.Length == 0) + { + DeviceName = GetDeviceDefaultName(); + } + else + { + DeviceName = inputDeviceName; + } + + if (parameter.ChannelCount == 6) + { + ChannelCount = 6; + } + else + { + ChannelCount = 2; + } + + SampleFormat = sampleFormat; + SampleRate = Constants.TargetSampleRate; + } + + return result; + } + + /// <summary> + /// Append a new audio buffer to the audio output. + /// </summary> + /// <param name="bufferTag">The unique tag of this buffer.</param> + /// <param name="userBuffer">The buffer informations.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer) + { + lock (_parentLock) + { + AudioBuffer buffer = new AudioBuffer + { + BufferTag = bufferTag, + DataPointer = userBuffer.Data, + DataSize = userBuffer.DataSize + }; + + if (_session.AppendBuffer(buffer)) + { + return ResultCode.Success; + } + + return ResultCode.BufferRingFull; + } + } + + /// <summary> + /// Get the release buffers. + /// </summary> + /// <param name="releasedBuffers">The buffer to write the release buffers</param> + /// <param name="releasedCount">The count of released buffers</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode GetReleasedBuffer(Span<ulong> releasedBuffers, out uint releasedCount) + { + releasedCount = 0; + + // Ensure that the first entry is set to zero if no entries are returned. + if (releasedBuffers.Length > 0) + { + releasedBuffers[0] = 0; + } + + lock (_parentLock) + { + for (int i = 0; i < releasedBuffers.Length; i++) + { + if (!_session.TryPopReleasedBuffer(out AudioBuffer buffer)) + { + break; + } + + releasedBuffers[i] = buffer.BufferTag; + releasedCount++; + } + } + + return ResultCode.Success; + } + + /// <summary> + /// Get the current state of the <see cref="AudioOutputSystem"/>. + /// </summary> + /// <returns>Return the curent sta\te of the <see cref="AudioOutputSystem"/></returns> + /// <returns></returns> + public AudioDeviceState GetState() + { + lock (_parentLock) + { + return _session.GetState(); + } + } + + /// <summary> + /// Start the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Start() + { + lock (_parentLock) + { + return _session.Start(); + } + } + + /// <summary> + /// Stop the audio session. + /// </summary> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns> + public ResultCode Stop() + { + lock (_parentLock) + { + return _session.Stop(); + } + } + + /// <summary> + /// Get the volume of the session. + /// </summary> + /// <returns>The volume of the session</returns> + public float GetVolume() + { + lock (_parentLock) + { + return _session.GetVolume(); + } + } + + /// <summary> + /// Set the volume of the session. + /// </summary> + /// <param name="volume">The new volume to set</param> + public void SetVolume(float volume) + { + lock (_parentLock) + { + _session.SetVolume(volume); + } + } + + /// <summary> + /// Get the count of buffer currently in use (server + driver side). + /// </summary> + /// <returns>The count of buffer currently in use</returns> + public uint GetBufferCount() + { + lock (_parentLock) + { + return _session.GetBufferCount(); + } + } + + /// <summary> + /// Check if a buffer is present. + /// </summary> + /// <param name="bufferTag">The unique tag of the buffer</param> + /// <returns>Return true if a buffer is present</returns> + public bool ContainsBuffer(ulong bufferTag) + { + lock (_parentLock) + { + return _session.ContainsBuffer(bufferTag); + } + } + + /// <summary> + /// Get the count of sample played in this session. + /// </summary> + /// <returns>The count of sample played in this session</returns> + public ulong GetPlayedSampleCount() + { + lock (_parentLock) + { + return _session.GetPlayedSampleCount(); + } + } + + /// <summary> + /// Flush all buffers to the initial state. + /// </summary> + /// <returns>True if any buffers was flushed</returns> + public bool FlushBuffers() + { + lock (_parentLock) + { + return _session.FlushBuffers(); + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _session.Dispose(); + + _manager.Unregister(this); + } + } + } +} diff --git a/Ryujinx.Audio/PlaybackState.cs b/Ryujinx.Audio/PlaybackState.cs deleted file mode 100644 index 7d862092..00000000 --- a/Ryujinx.Audio/PlaybackState.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Ryujinx.Audio -{ - /// <summary> - /// The playback state of a track - /// </summary> - public enum PlaybackState - { - /// <summary> - /// The track is currently playing - /// </summary> - Playing = 0, - /// <summary> - /// The track is currently stopped - /// </summary> - Stopped = 1 - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/ReleaseCallback.cs b/Ryujinx.Audio/ReleaseCallback.cs deleted file mode 100644 index f534e02c..00000000 --- a/Ryujinx.Audio/ReleaseCallback.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Ryujinx.Audio -{ - public delegate void ReleaseCallback(); -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Common/AuxiliaryBufferAddresses.cs b/Ryujinx.Audio/Renderer/Common/AuxiliaryBufferAddresses.cs new file mode 100644 index 00000000..5d826569 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/AuxiliaryBufferAddresses.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AuxiliaryBufferAddresses + { + public ulong SendBufferInfo; + public ulong SendBufferInfoBase; + public ulong ReturnBufferInfo; + public ulong ReturnBufferInfoBase; + } +} diff --git a/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs b/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs new file mode 100644 index 00000000..0de3d571 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/BehaviourParameter.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Represents the input parameter for <see cref="Server.BehaviourContext"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BehaviourParameter + { + /// <summary> + /// The current audio renderer revision in use. + /// </summary> + public int UserRevision; + + /// <summary> + /// Reserved/padding. + /// </summary> + private uint _padding; + + /// <summary> + /// The flags given controlling behaviour of the audio renderer + /// </summary> + /// <remarks>See <see cref="Server.BehaviourContext.UpdateFlags(ulong)"/> and <see cref="Server.BehaviourContext.IsMemoryPoolForceMappingEnabled"/>.</remarks> + public ulong Flags; + + /// <summary> + /// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ErrorInfo + { + /// <summary> + /// The error code to report. + /// </summary> + public ResultCode ErrorCode; + + /// <summary> + /// Reserved/padding. + /// </summary> + private uint _padding; + + /// <summary> + /// Extra information given with the <see cref="ResultCode"/> + /// </summary> + /// <remarks>This is usually used to report a faulting cpu address when a <see cref="Server.MemoryPool.MemoryPoolState"/> mapping fail.</remarks> + public ulong ExtraErrorInfo; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/EdgeMatrix.cs b/Ryujinx.Audio/Renderer/Common/EdgeMatrix.cs new file mode 100644 index 00000000..7423eda4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/EdgeMatrix.cs @@ -0,0 +1,167 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Represents a adjacent matrix. + /// </summary> + /// <remarks>This is used for splitter routing.</remarks> + public class EdgeMatrix + { + /// <summary> + /// Backing <see cref="BitArray"/> used for node connections. + /// </summary> + private BitArray _storage; + + /// <summary> + /// The count of nodes of the current instance. + /// </summary> + private int _nodeCount; + + /// <summary> + /// Get the required work buffer size memory needed for the <see cref="EdgeMatrix"/>. + /// </summary> + /// <param name="nodeCount">The count of nodes.</param> + /// <returns>The size required for the given <paramref name="nodeCount"/>.</returns> + public static int GetWorkBufferSize(int nodeCount) + { + int size = BitUtils.AlignUp(nodeCount * nodeCount, Constants.BufferAlignment); + + return size / Unsafe.SizeOf<byte>(); + } + + /// <summary> + /// Initializes the <see cref="EdgeMatrix"/> instance with backing memory. + /// </summary> + /// <param name="edgeMatrixWorkBuffer">The backing memory.</param> + /// <param name="nodeCount">The count of nodes.</param> + public void Initialize(Memory<byte> edgeMatrixWorkBuffer, int nodeCount) + { + Debug.Assert(edgeMatrixWorkBuffer.Length >= GetWorkBufferSize(nodeCount)); + + _storage = new BitArray(edgeMatrixWorkBuffer); + + _nodeCount = nodeCount; + + _storage.Reset(); + } + + /// <summary> + /// Test if the bit at the given index is set. + /// </summary> + /// <param name="index">A bit index.</param> + /// <returns>Returns true if the bit at the given index is set</returns> + public bool Test(int index) + { + return _storage.Test(index); + } + + /// <summary> + /// Reset all bits in the storage. + /// </summary> + public void Reset() + { + _storage.Reset(); + } + + /// <summary> + /// Reset the bit at the given index. + /// </summary> + /// <param name="index">A bit index.</param> + public void Reset(int index) + { + _storage.Reset(index); + } + + /// <summary> + /// Set the bit at the given index. + /// </summary> + /// <param name="index">A bit index.</param> + public void Set(int index) + { + _storage.Set(index); + } + + /// <summary> + /// Connect a given source to a given destination. + /// </summary> + /// <param name="source">The source index.</param> + /// <param name="destination">The destination index.</param> + public void Connect(int source, int destination) + { + Debug.Assert(source < _nodeCount); + Debug.Assert(destination < _nodeCount); + + _storage.Set(_nodeCount * source + destination); + } + + /// <summary> + /// Check if the given source is connected to the given destination. + /// </summary> + /// <param name="source">The source index.</param> + /// <param name="destination">The destination index.</param> + /// <returns>Returns true if the given source is connected to the given destination.</returns> + public bool Connected(int source, int destination) + { + Debug.Assert(source < _nodeCount); + Debug.Assert(destination < _nodeCount); + + return _storage.Test(_nodeCount * source + destination); + } + + /// <summary> + /// Disconnect a given source from a given destination. + /// </summary> + /// <param name="source">The source index.</param> + /// <param name="destination">The destination index.</param> + public void Disconnect(int source, int destination) + { + Debug.Assert(source < _nodeCount); + Debug.Assert(destination < _nodeCount); + + _storage.Reset(_nodeCount * source + destination); + } + + /// <summary> + /// Remove all edges from a given source. + /// </summary> + /// <param name="source">The source index.</param> + public void RemoveEdges(int source) + { + for (int i = 0; i < _nodeCount; i++) + { + Disconnect(source, i); + } + } + + /// <summary> + /// Get the total node count. + /// </summary> + /// <returns>The total node count.</returns> + public int GetNodeCount() + { + return _nodeCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/EffectType.cs b/Ryujinx.Audio/Renderer/Common/EffectType.cs new file mode 100644 index 00000000..082f94f8 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/EffectType.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// The type of an effect. + /// </summary> + public enum EffectType : byte + { + /// <summary> + /// Invalid effect. + /// </summary> + Invalid, + + /// <summary> + /// Effect applying additional mixing capability. + /// </summary> + BufferMix, + + /// <summary> + /// Effect applying custom user effect (via auxiliary buffers). + /// </summary> + AuxiliaryBuffer, + + /// <summary> + /// Effect applying a delay. + /// </summary> + Delay, + + /// <summary> + /// Effect applying a reverberation effect via a given preset. + /// </summary> + Reverb, + + /// <summary> + /// Effect applying a 3D reverberation effect via a given preset. + /// </summary> + Reverb3d, + + /// <summary> + /// Effect applying a biquad filter. + /// </summary> + BiquadFilter + } +} diff --git a/Ryujinx.Audio/Renderer/Common/MemoryPoolUserState.cs b/Ryujinx.Audio/Renderer/Common/MemoryPoolUserState.cs new file mode 100644 index 00000000..7f9765d9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/MemoryPoolUserState.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Represents the state of a memory pool. + /// </summary> + public enum MemoryPoolUserState : uint + { + /// <summary> + /// Invalid state. + /// </summary> + Invalid = 0, + + /// <summary> + /// The memory pool is new. (client side only) + /// </summary> + New = 1, + + /// <summary> + /// The user asked to detach the memory pool from the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + RequestDetach = 2, + + /// <summary> + /// The memory pool is detached from the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + Detached = 3, + + /// <summary> + /// The user asked to attach the memory pool to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + RequestAttach = 4, + + /// <summary> + /// The memory pool is attached to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + Attached = 5, + + /// <summary> + /// The memory pool is released. (client side only) + /// </summary> + Released = 6 + } +} diff --git a/Ryujinx.Audio/Renderer/Common/NodeIdHelper.cs b/Ryujinx.Audio/Renderer/Common/NodeIdHelper.cs new file mode 100644 index 00000000..9bf1f2d0 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/NodeIdHelper.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Helper for manipulating node ids. + /// </summary> + public static class NodeIdHelper + { + /// <summary> + /// Get the type of a node from a given node id. + /// </summary> + /// <param name="nodeId">Id of the node.</param> + /// <returns>The type of the node.</returns> + public static NodeIdType GetType(int nodeId) + { + return (NodeIdType)(nodeId >> 28); + } + + /// <summary> + /// Get the base of a node from a given node id. + /// </summary> + /// <param name="nodeId">Id of the node.</param> + /// <returns>The base of the node.</returns> + public static int GetBase(int nodeId) + { + return (nodeId >> 16) & 0xFFF; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/NodeIdType.cs b/Ryujinx.Audio/Renderer/Common/NodeIdType.cs new file mode 100644 index 00000000..4a4796a9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/NodeIdType.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// The type of a node. + /// </summary> + public enum NodeIdType : byte + { + /// <summary> + /// Invalid node id. + /// </summary> + Invalid = 0, + + /// <summary> + /// Voice related node id. (data source, biquad filter, ...) + /// </summary> + Voice = 1, + + /// <summary> + /// Mix related node id. (mix, effects, splitters, ...) + /// </summary> + Mix = 2, + + /// <summary> + /// Sink related node id. (device & circular buffer sink) + /// </summary> + Sink = 3, + + /// <summary> + /// Performance monitoring related node id (performance commands) + /// </summary> + Performance = 15 + } +} diff --git a/Ryujinx.Audio/Renderer/Common/NodeStates.cs b/Ryujinx.Audio/Renderer/Common/NodeStates.cs new file mode 100644 index 00000000..5efd7767 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/NodeStates.cs @@ -0,0 +1,246 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Common +{ + public class NodeStates + { + private class Stack + { + private Memory<int> _storage; + private int _index; + + private int _nodeCount; + + public void Reset(Memory<int> storage, int nodeCount) + { + Debug.Assert(storage.Length * sizeof(int) >= CalcBufferSize(nodeCount)); + + _storage = storage; + _index = 0; + _nodeCount = nodeCount; + } + + public int GetCurrentCount() + { + return _index; + } + + public void Push(int data) + { + Debug.Assert(_index + 1 <= _nodeCount); + + _storage.Span[_index++] = data; + } + + public int Pop() + { + Debug.Assert(_index > 0); + + return _storage.Span[--_index]; + } + + public int Top() + { + return _storage.Span[_index - 1]; + } + + public static int CalcBufferSize(int nodeCount) + { + return nodeCount * sizeof(int); + } + } + + private int _nodeCount; + private EdgeMatrix _discovered; + private EdgeMatrix _finished; + private Memory<int> _resultArray; + private Stack _stack; + private int _tsortResultIndex; + + private enum NodeState : byte + { + Unknown, + Discovered, + Finished + } + + public NodeStates() + { + _stack = new Stack(); + _discovered = new EdgeMatrix(); + _finished = new EdgeMatrix(); + } + + public static int GetWorkBufferSize(int nodeCount) + { + return Stack.CalcBufferSize(nodeCount * nodeCount) + 0xC * nodeCount + 2 * EdgeMatrix.GetWorkBufferSize(nodeCount); + } + + public void Initialize(Memory<byte> nodeStatesWorkBuffer, int nodeCount) + { + int workBufferSize = GetWorkBufferSize(nodeCount); + + Debug.Assert(nodeStatesWorkBuffer.Length >= workBufferSize); + + _nodeCount = nodeCount; + + int edgeMatrixWorkBufferSize = EdgeMatrix.GetWorkBufferSize(nodeCount); + + _discovered.Initialize(nodeStatesWorkBuffer.Slice(0, edgeMatrixWorkBufferSize), nodeCount); + _finished.Initialize(nodeStatesWorkBuffer.Slice(edgeMatrixWorkBufferSize, edgeMatrixWorkBufferSize), nodeCount); + + nodeStatesWorkBuffer = nodeStatesWorkBuffer.Slice(edgeMatrixWorkBufferSize * 2); + + _resultArray = SpanMemoryManager<int>.Cast(nodeStatesWorkBuffer.Slice(0, sizeof(int) * nodeCount)); + + nodeStatesWorkBuffer = nodeStatesWorkBuffer.Slice(sizeof(int) * nodeCount); + + Memory<int> stackWorkBuffer = SpanMemoryManager<int>.Cast(nodeStatesWorkBuffer.Slice(0, Stack.CalcBufferSize(nodeCount * nodeCount))); + + _stack.Reset(stackWorkBuffer, nodeCount * nodeCount); + } + + private void Reset() + { + _discovered.Reset(); + _finished.Reset(); + _tsortResultIndex = 0; + _resultArray.Span.Fill(-1); + } + + private NodeState GetState(int index) + { + Debug.Assert(index < _nodeCount); + + if (_discovered.Test(index)) + { + Debug.Assert(!_finished.Test(index)); + + return NodeState.Discovered; + } + else if (_finished.Test(index)) + { + Debug.Assert(!_discovered.Test(index)); + + return NodeState.Finished; + } + + return NodeState.Unknown; + } + + private void SetState(int index, NodeState state) + { + switch (state) + { + case NodeState.Unknown: + _discovered.Reset(index); + _finished.Reset(index); + break; + case NodeState.Discovered: + _discovered.Set(index); + _finished.Reset(index); + break; + case NodeState.Finished: + _finished.Set(index); + _discovered.Reset(index); + break; + } + } + + private void PushTsortResult(int index) + { + Debug.Assert(index < _nodeCount); + + _resultArray.Span[_tsortResultIndex++] = index; + } + + public ReadOnlySpan<int> GetTsortResult() + { + return _resultArray.Span.Slice(0, _tsortResultIndex); + } + + public bool Sort(EdgeMatrix edgeMatrix) + { + Reset(); + + if (_nodeCount <= 0) + { + return true; + } + + for (int i = 0; i < _nodeCount; i++) + { + if (GetState(i) == NodeState.Unknown) + { + _stack.Push(i); + } + + while (_stack.GetCurrentCount() > 0) + { + int topIndex = _stack.Top(); + + NodeState topState = GetState(topIndex); + + if (topState == NodeState.Discovered) + { + SetState(topIndex, NodeState.Finished); + PushTsortResult(topIndex); + _stack.Pop(); + } + else if (topState == NodeState.Finished) + { + _stack.Pop(); + } + else + { + if (topState == NodeState.Unknown) + { + SetState(topIndex, NodeState.Discovered); + } + + for (int j = 0; j < edgeMatrix.GetNodeCount(); j++) + { + if (edgeMatrix.Connected(topIndex, j)) + { + NodeState jState = GetState(j); + + if (jState == NodeState.Unknown) + { + _stack.Push(j); + } + // Found a loop, reset and propagate rejection. + else if (jState == NodeState.Discovered) + { + Reset(); + + return false; + } + } + } + } + } + } + + return true; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs new file mode 100644 index 00000000..a92bd0cc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/PerformanceDetailType.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + public enum PerformanceDetailType : byte + { + Unknown, + PcmInt16, + Adpcm, + VolumeRamp, + BiquadFilter, + Mix, + Delay, + Aux, + Reverb, + Reverb3d, + PcmFloat + } +} diff --git a/Ryujinx.Audio/Renderer/Common/PerformanceEntryType.cs b/Ryujinx.Audio/Renderer/Common/PerformanceEntryType.cs new file mode 100644 index 00000000..0d9a547d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/PerformanceEntryType.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + public enum PerformanceEntryType : byte + { + Invalid, + Voice, + SubMix, + FinalMix, + Sink + } +} diff --git a/Ryujinx.Audio/Renderer/Common/PlayState.cs b/Ryujinx.Audio/Renderer/Common/PlayState.cs new file mode 100644 index 00000000..7771c154 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/PlayState.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Common play state. + /// </summary> + public enum PlayState : byte + { + /// <summary> + /// The user request the voice to be started. + /// </summary> + Start, + + /// <summary> + /// The user request the voice to be stopped. + /// </summary> + Stop, + + /// <summary> + /// The user request the voice to be paused. + /// </summary> + Pause + } +} diff --git a/Ryujinx.Audio/Renderer/Common/ReverbEarlyMode.cs b/Ryujinx.Audio/Renderer/Common/ReverbEarlyMode.cs new file mode 100644 index 00000000..a0f65e5e --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/ReverbEarlyMode.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Early reverb reflection. + /// </summary> + public enum ReverbEarlyMode : uint + { + /// <summary> + /// Room early reflection. (small acoustic space, fast reflection) + /// </summary> + Room, + + /// <summary> + /// Chamber early reflection. (bigger than <see cref="Room"/>'s acoustic space, short reflection) + /// </summary> + Chamber, + + /// <summary> + /// Hall early reflection. (large acoustic space, warm reflection) + /// </summary> + Hall, + + /// <summary> + /// Cathedral early reflection. (very large acoustic space, pronounced bright reflection) + /// </summary> + Cathedral, + + /// <summary> + /// No early reflection. + /// </summary> + Disabled + } +} diff --git a/Ryujinx.Audio/Renderer/Common/ReverbLateMode.cs b/Ryujinx.Audio/Renderer/Common/ReverbLateMode.cs new file mode 100644 index 00000000..57760821 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/ReverbLateMode.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Late reverb reflection. + /// </summary> + public enum ReverbLateMode : uint + { + /// <summary> + /// Room late reflection. (small acoustic space, fast reflection) + /// </summary> + Room, + + /// <summary> + /// Hall late reflection. (large acoustic space, warm reflection) + /// </summary> + Hall, + + /// <summary> + /// Classic plate late reflection. (clean distinctive reverb) + /// </summary> + Plate, + + /// <summary> + /// Cathedral late reflection. (very large acoustic space, pronounced bright reflection) + /// </summary> + Cathedral, + + /// <summary> + /// Do not apply any delay. (max delay) + /// </summary> + NoDelay, + + /// <summary> + /// Max delay. (used for delay line limits) + /// </summary> + Limit = NoDelay + } +} diff --git a/Ryujinx.Audio/Renderer/Common/SinkType.cs b/Ryujinx.Audio/Renderer/Common/SinkType.cs new file mode 100644 index 00000000..e1a35508 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/SinkType.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// The type of a sink. + /// </summary> + public enum SinkType : byte + { + /// <summary> + /// The sink is in an invalid state. + /// </summary> + Invalid, + + /// <summary> + /// The sink is a device. + /// </summary> + Device, + + /// <summary> + /// The sink is a circular buffer. + /// </summary> + CircularBuffer + } +} diff --git a/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs b/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs new file mode 100644 index 00000000..ae516662 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/UpdateDataHeader.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>. + /// </summary> + public struct UpdateDataHeader + { + public int Revision; + public uint BehaviourSize; + public uint MemoryPoolsSize; + public uint VoicesSize; + public uint VoiceResourcesSize; + public uint EffectsSize; + public uint MixesSize; + public uint SinksSize; + public uint PerformanceBufferSize; + public uint Unknown24; + public uint RenderInfoSize; + + private unsafe fixed int _reserved[4]; + + public uint TotalSize; + + public void Initialize(int revision) + { + Revision = revision; + + TotalSize = (uint)Unsafe.SizeOf<UpdateDataHeader>(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs b/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs new file mode 100644 index 00000000..6cebad5e --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// Represent the update state of a voice. + /// </summary> + /// <remarks>This is shared between the server and audio processor.</remarks> + [StructLayout(LayoutKind.Sequential, Pack = Align)] + public struct VoiceUpdateState + { + public const int Align = 0x10; + public const int BiquadStateOffset = 0x0; + public const int BiquadStateSize = 0x10; + + /// <summary> + /// The state of the biquad filters of this voice. + /// </summary> + public Array2<BiquadFilterState> BiquadFilterState; + + /// <summary> + /// The total amount of samples that was played. + /// </summary> + /// <remarks>This is reset to 0 when a <see cref="WaveBuffer"/> finishes playing and <see cref="WaveBuffer.IsEndOfStream"/> is set.</remarks> + /// <remarks>This is reset to 0 when looping while <see cref="Parameter.VoiceInParameter.DecodingBehaviour.PlayedSampleCountResetWhenLooping"/> is set.</remarks> + public ulong PlayedSampleCount; + + /// <summary> + /// The current sample offset in the <see cref="WaveBuffer"/> pointed by <see cref="WaveBufferIndex"/>. + /// </summary> + public int Offset; + + /// <summary> + /// The current index of the <see cref="WaveBuffer"/> in use. + /// </summary> + public uint WaveBufferIndex; + + private WaveBufferValidArray _isWaveBufferValid; + + /// <summary> + /// The total amount of <see cref="WaveBuffer"/> consumed. + /// </summary> + public uint WaveBufferConsumed; + + /// <summary> + /// Pitch used for Sample Rate Conversion. + /// </summary> + public Array8<short> Pitch; + + public float Fraction; + + /// <summary> + /// The ADPCM loop context when <see cref="SampleFormat.Adpcm"/> is in use. + /// </summary> + public AdpcmLoopContext LoopContext; + + /// <summary> + /// The last samples after a mix ramp. + /// </summary> + /// <remarks>This is used for depop (to perform voice drop).</remarks> + public Array24<float> LastSamples; + + /// <summary> + /// The current count of loop performed. + /// </summary> + public int LoopCount; + + [StructLayout(LayoutKind.Sequential, Size = 1 * Constants.VoiceWaveBufferCount, Pack = 1)] + private struct WaveBufferValidArray { } + + /// <summary> + /// Contains information of <see cref="WaveBuffer"/> validity. + /// </summary> + public Span<bool> IsWaveBufferValid => SpanHelpers.AsSpan<WaveBufferValidArray, bool>(ref _isWaveBufferValid); + + /// <summary> + /// Mark the current <see cref="WaveBuffer"/> as played and switch to the next one. + /// </summary> + /// <param name="waveBuffer">The current <see cref="WaveBuffer"/></param> + /// <param name="waveBufferIndex">The wavebuffer index.</param> + /// <param name="waveBufferConsumed">The amount of wavebuffers consumed.</param> + /// <param name="playedSampleCount">The total count of sample played.</param> + public void MarkEndOfBufferWaveBufferProcessing(ref WaveBuffer waveBuffer, ref int waveBufferIndex, ref uint waveBufferConsumed, ref ulong playedSampleCount) + { + IsWaveBufferValid[waveBufferIndex++] = false; + LoopCount = 0; + waveBufferConsumed++; + + if (waveBufferIndex >= Constants.VoiceWaveBufferCount) + { + waveBufferIndex = 0; + } + + if (waveBuffer.IsEndOfStream) + { + playedSampleCount = 0; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Common/WaveBuffer.cs b/Ryujinx.Audio/Renderer/Common/WaveBuffer.cs new file mode 100644 index 00000000..53906c0c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/WaveBuffer.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +using DspAddr = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Common +{ + /// <summary> + /// A wavebuffer used for data source commands. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct WaveBuffer + { + /// <summary> + /// The DSP address of the sample data of the wavebuffer. + /// </summary> + public DspAddr Buffer; + + /// <summary> + /// The DSP address of the context of the wavebuffer. + /// </summary> + /// <remarks>Only used by <see cref="SampleFormat.Adpcm"/>.</remarks> + public DspAddr Context; + + /// <summary> + /// The size of the sample buffer data. + /// </summary> + public uint BufferSize; + + /// <summary> + /// The size of the context buffer. + /// </summary> + public uint ContextSize; + + /// <summary> + /// First sample to play on the wavebuffer. + /// </summary> + public uint StartSampleOffset; + + /// <summary> + /// Last sample to play on the wavebuffer. + /// </summary> + public uint EndSampleOffset; + + /// <summary> + /// First sample to play when looping the wavebuffer. + /// </summary> + /// <remarks> + /// If <see cref="LoopStartSampleOffset"/> or <see cref="LoopEndSampleOffset"/> is equal to zero,, it will default to <see cref="StartSampleOffset"/> and <see cref="EndSampleOffset"/>. + /// </remarks> + public uint LoopStartSampleOffset; + + /// <summary> + /// Last sample to play when looping the wavebuffer. + /// </summary> + /// <remarks> + /// If <see cref="LoopStartSampleOffset"/> or <see cref="LoopEndSampleOffset"/> is equal to zero, it will default to <see cref="StartSampleOffset"/> and <see cref="EndSampleOffset"/>. + /// </remarks> + public uint LoopEndSampleOffset; + + /// <summary> + /// The max loop count. + /// </summary> + public int LoopCount; + + /// <summary> + /// Set to true if the wavebuffer is looping. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool Looping; + + /// <summary> + /// Set to true if the wavebuffer is the end of stream. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsEndOfStream; + + /// <summary> + /// Padding/Reserved. + /// </summary> + private ushort _padding; + } +} diff --git a/Ryujinx.Audio/Renderer/Common/WorkBufferAllocator.cs b/Ryujinx.Audio/Renderer/Common/WorkBufferAllocator.cs new file mode 100644 index 00000000..2aff4eb0 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Common/WorkBufferAllocator.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Common +{ + public class WorkBufferAllocator + { + public Memory<byte> BackingMemory { get; } + + public ulong Offset { get; private set; } + + public WorkBufferAllocator(Memory<byte> backingMemory) + { + BackingMemory = backingMemory; + } + + public Memory<byte> Allocate(ulong size, int align) + { + Debug.Assert(align != 0); + + if (size != 0) + { + ulong alignedOffset = BitUtils.AlignUp(Offset, align); + + if (alignedOffset + size <= (ulong)BackingMemory.Length) + { + Memory<byte> result = BackingMemory.Slice((int)alignedOffset, (int)size); + + Offset = alignedOffset + size; + + // Clear the memory to be sure that is does not contain any garbage. + result.Span.Fill(0); + + return result; + } + } + + return Memory<byte>.Empty; + } + + public Memory<T> Allocate<T>(ulong count, int align) where T: unmanaged + { + Memory<byte> allocatedMemory = Allocate((ulong)Unsafe.SizeOf<T>() * count, align); + + if (allocatedMemory.IsEmpty) + { + return Memory<T>.Empty; + } + + return SpanMemoryManager<T>.Cast(allocatedMemory); + } + + public static ulong GetTargetSize<T>(ulong currentSize, ulong count, int align) where T: unmanaged + { + return BitUtils.AlignUp(currentSize, align) + (ulong)Unsafe.SizeOf<T>() * count; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Device/VirtualDevice.cs b/Ryujinx.Audio/Renderer/Device/VirtualDevice.cs new file mode 100644 index 00000000..29395e5c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Device/VirtualDevice.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Device +{ + /// <summary> + /// Represents a virtual device used by IAudioDevice. + /// </summary> + public class VirtualDevice + { + /// <summary> + /// All the defined virtual devices. + /// </summary> + public static readonly VirtualDevice[] Devices = new VirtualDevice[4] + { + new VirtualDevice("AudioStereoJackOutput", 2), + new VirtualDevice("AudioBuiltInSpeakerOutput", 2), + new VirtualDevice("AudioTvOutput", 6), + new VirtualDevice("AudioUsbDeviceOutput", 2), + }; + + /// <summary> + /// The name of the <see cref="VirtualDevice"/>. + /// </summary> + public string Name { get; } + + /// <summary> + /// The count of channels supported by the <see cref="VirtualDevice"/>. + /// </summary> + public uint ChannelCount { get; } + + /// <summary> + /// The system master volume of the <see cref="VirtualDevice"/>. + /// </summary> + public float MasterVolume { get; private set; } + + /// <summary> + /// Create a new <see cref="VirtualDevice"/> instance. + /// </summary> + /// <param name="name">The name of the <see cref="VirtualDevice"/>.</param> + /// <param name="channelCount">The count of channels supported by the <see cref="VirtualDevice"/>.</param> + private VirtualDevice(string name, uint channelCount) + { + Name = name; + ChannelCount = channelCount; + } + + /// <summary> + /// Update the master volume of the <see cref="VirtualDevice"/>. + /// </summary> + /// <param name="volume">The new master volume.</param> + public void UpdateMasterVolume(float volume) + { + Debug.Assert(volume >= 0.0f && volume <= 1.0f); + + MasterVolume = volume; + } + + /// <summary> + /// Check if the <see cref="VirtualDevice"/> is a usb device. + /// </summary> + /// <returns>Returns true if the <see cref="VirtualDevice"/> is a usb device.</returns> + public bool IsUsbDevice() + { + return Name.Equals("AudioUsbDeviceOutput"); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Device/VirtualDeviceSession.cs b/Ryujinx.Audio/Renderer/Device/VirtualDeviceSession.cs new file mode 100644 index 00000000..b2669418 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Device/VirtualDeviceSession.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Device +{ + /// <summary> + /// Represents a virtual device session used by IAudioDevice. + /// </summary> + public class VirtualDeviceSession + { + /// <summary> + /// The <see cref="VirtualDevice"/> associated to this session. + /// </summary> + public VirtualDevice Device { get; } + + /// <summary> + /// The user volume of this session. + /// </summary> + public float Volume { get; set; } + + /// <summary> + /// Create a new <see cref="VirtualDeviceSession"/> instance. + /// </summary> + /// <param name="virtualDevice">The <see cref="VirtualDevice"/> associated to this session.</param> + public VirtualDeviceSession(VirtualDevice virtualDevice) + { + Device = virtualDevice; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Device/VirtualDeviceSessionRegistry.cs b/Ryujinx.Audio/Renderer/Device/VirtualDeviceSessionRegistry.cs new file mode 100644 index 00000000..ed6806b2 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Device/VirtualDeviceSessionRegistry.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Collections.Generic; + +namespace Ryujinx.Audio.Renderer.Device +{ + /// <summary> + /// Represent an instance containing a registry of <see cref="VirtualDeviceSession"/>. + /// </summary> + public class VirtualDeviceSessionRegistry + { + /// <summary> + /// The session registry, used to store the sessions of a given AppletResourceId. + /// </summary> + private Dictionary<ulong, VirtualDeviceSession[]> _sessionsRegistry = new Dictionary<ulong, VirtualDeviceSession[]>(); + + /// <summary> + /// The default <see cref="VirtualDevice"/>. + /// </summary> + /// <remarks>This is used when the USB device is the default one on older revision.</remarks> + public VirtualDevice DefaultDevice => VirtualDevice.Devices[0]; + + /// <summary> + /// The current active <see cref="VirtualDevice"/>. + /// </summary> + // TODO: make this configurable + public VirtualDevice ActiveDevice = VirtualDevice.Devices[1]; + + /// <summary> + /// Get the associated <see cref="T:VirtualDeviceSession[]"/> from an AppletResourceId. + /// </summary> + /// <param name="resourceAppletId">The AppletResourceId used.</param> + /// <returns>The associated <see cref="T:VirtualDeviceSession[]"/> from an AppletResourceId.</returns> + public VirtualDeviceSession[] GetSessionByAppletResourceId(ulong resourceAppletId) + { + if (_sessionsRegistry.TryGetValue(resourceAppletId, out VirtualDeviceSession[] result)) + { + return result; + } + + result = CreateSessionsFromBehaviourContext(); + + _sessionsRegistry.Add(resourceAppletId, result); + + return result; + } + + /// <summary> + /// Create a new array of sessions for each <see cref="VirtualDevice"/>. + /// </summary> + /// <returns>A new array of sessions for each <see cref="VirtualDevice"/>.</returns> + private static VirtualDeviceSession[] CreateSessionsFromBehaviourContext() + { + VirtualDeviceSession[] virtualDeviceSession = new VirtualDeviceSession[VirtualDevice.Devices.Length]; + + for (int i = 0; i < virtualDeviceSession.Length; i++) + { + virtualDeviceSession[i] = new VirtualDeviceSession(VirtualDevice.Devices[i]); + } + + return virtualDeviceSession; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs b/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs new file mode 100644 index 00000000..f9f0811d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.State; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class AdpcmHelper + { + private const int FixedPointPrecision = 11; + private const int SamplesPerFrame = 14; + private const int NibblesPerFrame = SamplesPerFrame + 2; + private const int BytesPerFrame = 8; + private const int BitsPerFrame = BytesPerFrame * 8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetAdpcmDataSize(int sampleCount) + { + Debug.Assert(sampleCount >= 0); + + int frames = sampleCount / SamplesPerFrame; + int extraSize = 0; + + if ((sampleCount % SamplesPerFrame) != 0) + { + extraSize = (sampleCount % SamplesPerFrame) / 2 + 1 + (sampleCount % 2); + } + + return (uint)(BytesPerFrame * frames + extraSize); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetAdpcmOffsetFromSampleOffset(int sampleOffset) + { + Debug.Assert(sampleOffset >= 0); + + return GetNibblesFromSampleCount(sampleOffset) / 2; + } + + public static int NibbleToSample(int nibble) + { + int frames = nibble / NibblesPerFrame; + int extraNibbles = nibble % NibblesPerFrame; + int samples = SamplesPerFrame * frames; + + return samples + extraNibbles - 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetNibblesFromSampleCount(int sampleCount) + { + byte headerSize = 0; + + if ((sampleCount % SamplesPerFrame) != 0) + { + headerSize = 2; + } + + return sampleCount % SamplesPerFrame + NibblesPerFrame * (sampleCount / SamplesPerFrame) + headerSize; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static short Saturate(int value) + { + if (value > short.MaxValue) + value = short.MaxValue; + + if (value < short.MinValue) + value = short.MinValue; + + return (short)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Decode(Span<short> output, ReadOnlySpan<byte> input, int startSampleOffset, int endSampleOffset, int offset, int count, ReadOnlySpan<short> coefficients, ref AdpcmLoopContext loopContext) + { + if (input.IsEmpty || endSampleOffset < startSampleOffset) + { + return 0; + } + + byte predScale = (byte)loopContext.PredScale; + byte scale = (byte)(predScale & 0xF); + byte coefficientIndex = (byte)((predScale >> 4) & 0xF); + short history0 = loopContext.History0; + short history1 = loopContext.History1; + short coefficient0 = coefficients[coefficientIndex * 2 + 0]; + short coefficient1 = coefficients[coefficientIndex * 2 + 1]; + + int decodedCount = Math.Min(count, endSampleOffset - startSampleOffset - offset); + int nibbles = GetNibblesFromSampleCount(offset + startSampleOffset); + int remaining = decodedCount; + int outputBufferIndex = 0; + int inputIndex = 0; + + ReadOnlySpan<byte> targetInput; + + targetInput = input.Slice(nibbles / 2); + + while (remaining > 0) + { + int samplesCount; + + if (((uint)nibbles % NibblesPerFrame) == 0) + { + predScale = targetInput[inputIndex++]; + + scale = (byte)(predScale & 0xF); + + coefficientIndex = (byte)((predScale >> 4) & 0xF); + + coefficient0 = coefficients[coefficientIndex * 2 + 0]; + coefficient1 = coefficients[coefficientIndex * 2 + 1]; + + nibbles += 2; + + samplesCount = Math.Min(remaining, SamplesPerFrame); + } + else + { + samplesCount = 1; + } + + int scaleFixedPoint = FixedPointHelper.ToFixed(1.0f, FixedPointPrecision) << scale; + + if (samplesCount < SamplesPerFrame) + { + for (int i = 0; i < samplesCount; i++) + { + int value = targetInput[inputIndex]; + + int sample; + + if ((nibbles & 1) != 0) + { + sample = (value << 28) >> 28; + + inputIndex++; + } + else + { + sample = (value << 24) >> 28; + } + + nibbles++; + + int prediction = coefficient0 * history0 + coefficient1 * history1; + + sample = FixedPointHelper.RoundUpAndToInt(sample * scaleFixedPoint + prediction, FixedPointPrecision); + + short saturatedSample = Saturate(sample); + + history1 = history0; + history0 = saturatedSample; + + output[outputBufferIndex++] = saturatedSample; + + remaining--; + } + } + else + { + for (int i = 0; i < SamplesPerFrame / 2; i++) + { + int value = targetInput[inputIndex]; + + int sample0; + int sample1; + + sample0 = (value << 24) >> 28; + sample1 = (value << 28) >> 28; + + inputIndex++; + + int prediction0 = coefficient0 * history0 + coefficient1 * history1; + sample0 = FixedPointHelper.RoundUpAndToInt(sample0 * scaleFixedPoint + prediction0, FixedPointPrecision); + short saturatedSample0 = Saturate(sample0); + + int prediction1 = coefficient0 * saturatedSample0 + coefficient1 * history0; + sample1 = FixedPointHelper.RoundUpAndToInt(sample1 * scaleFixedPoint + prediction1, FixedPointPrecision); + short saturatedSample1 = Saturate(sample1); + + history1 = saturatedSample0; + history0 = saturatedSample1; + + output[outputBufferIndex++] = saturatedSample0; + output[outputBufferIndex++] = saturatedSample1; + } + + nibbles += SamplesPerFrame; + remaining -= SamplesPerFrame; + } + } + + loopContext.PredScale = predScale; + loopContext.History0 = history0; + loopContext.History1 = history1; + + return decodedCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs b/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs new file mode 100644 index 00000000..2c97a0c1 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/AudioProcessor.cs @@ -0,0 +1,255 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using System; +using System.Threading; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public class AudioProcessor : IDisposable + { + private const int MaxBufferedFrames = 5; + private const int TargetBufferedFrames = 3; + + private enum MailboxMessage : uint + { + Start, + Stop, + RenderStart, + RenderEnd + } + + private class RendererSession + { + public CommandList CommandList; + public int RenderingLimit; + public ulong AppletResourceId; + } + + private Mailbox<MailboxMessage> _mailbox; + private RendererSession[] _sessionCommandList; + private Thread _workerThread; + + public IHardwareDevice[] OutputDevices { get; private set; } + + private long _lastTime; + private long _playbackEnds; + private ManualResetEvent _event; + + public AudioProcessor() + { + _event = new ManualResetEvent(false); + } + + private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver) + { + // Get the real device driver (In case the compat layer is on top of it). + deviceDriver = deviceDriver.GetRealDeviceDriver(); + + if (deviceDriver.SupportsChannelCount(6)) + { + return 6; + } + else + { + // NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible. + return 2; + } + } + + public void Start(IHardwareDeviceDriver deviceDriver) + { + OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax]; + + // TODO: Before enabling this, we need up-mixing from stereo to 5.1. + // uint channelCount = GetHardwareChannelCount(deviceDriver); + uint channelCount = 2; + + for (int i = 0; i < OutputDevices.Length; i++) + { + // TODO: Don't hardcode sample rate. + OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate); + } + + _mailbox = new Mailbox<MailboxMessage>(); + _sessionCommandList = new RendererSession[Constants.AudioRendererSessionCountMax]; + _event.Reset(); + _lastTime = PerformanceCounter.ElapsedNanoseconds; + + StartThread(); + + _mailbox.SendMessage(MailboxMessage.Start); + + if (_mailbox.ReceiveResponse() != MailboxMessage.Start) + { + throw new InvalidOperationException("Audio Processor Start response was invalid!"); + } + } + + public void Stop() + { + _mailbox.SendMessage(MailboxMessage.Stop); + + if (_mailbox.ReceiveResponse() != MailboxMessage.Stop) + { + throw new InvalidOperationException("Audio Processor Stop response was invalid!"); + } + + foreach (IHardwareDevice device in OutputDevices) + { + device.Dispose(); + } + } + + public void Send(int sessionId, CommandList commands, int renderingLimit, ulong appletResourceId) + { + _sessionCommandList[sessionId] = new RendererSession + { + CommandList = commands, + RenderingLimit = renderingLimit, + AppletResourceId = appletResourceId + }; + } + + public void Signal() + { + _mailbox.SendMessage(MailboxMessage.RenderStart); + } + + public void Wait() + { + if (_mailbox.ReceiveResponse() != MailboxMessage.RenderEnd) + { + throw new InvalidOperationException("Audio Processor Wait response was invalid!"); + } + + long increment = Constants.AudioProcessorMaxUpdateTimeTarget; + + long timeNow = PerformanceCounter.ElapsedNanoseconds; + + if (timeNow > _playbackEnds) + { + // Playback has restarted. + _playbackEnds = timeNow; + } + + _playbackEnds += increment; + + // The number of frames we are behind where the timer says we should be. + long framesBehind = (timeNow - _lastTime) / increment; + + // The number of frames yet to play on the backend. + long bufferedFrames = (_playbackEnds - timeNow) / increment + framesBehind; + + // If we've entered a situation where a lot of buffers will be queued on the backend, + // Skip some audio frames so that playback can catch up. + if (bufferedFrames > MaxBufferedFrames) + { + // Skip a few frames so that we're not too far behind. (the target number of frames) + _lastTime += increment * (bufferedFrames - TargetBufferedFrames); + } + + while (timeNow < _lastTime + increment) + { + _event.WaitOne(1); + + timeNow = PerformanceCounter.ElapsedNanoseconds; + } + + _lastTime += increment; + } + + private void StartThread() + { + _workerThread = new Thread(Work) + { + Name = "AudioProcessor.Worker" + }; + + _workerThread.Start(); + } + + private void Work() + { + if (_mailbox.ReceiveMessage() != MailboxMessage.Start) + { + throw new InvalidOperationException("Audio Processor Start message was invalid!"); + } + + _mailbox.SendResponse(MailboxMessage.Start); + _mailbox.SendResponse(MailboxMessage.RenderEnd); + + Logger.Info?.Print(LogClass.AudioRenderer, "Starting audio processor"); + + while (true) + { + MailboxMessage message = _mailbox.ReceiveMessage(); + + if (message == MailboxMessage.Stop) + { + break; + } + + if (message == MailboxMessage.RenderStart) + { + long startTicks = PerformanceCounter.ElapsedNanoseconds; + + for (int i = 0; i < _sessionCommandList.Length; i++) + { + if (_sessionCommandList[i] != null) + { + _sessionCommandList[i].CommandList.Process(OutputDevices[i]); + _sessionCommandList[i] = null; + } + } + + long endTicks = PerformanceCounter.ElapsedNanoseconds; + + long elapsedTime = endTicks - startTicks; + + if (Constants.AudioProcessorMaxUpdateTime < elapsedTime) + { + Logger.Debug?.Print(LogClass.AudioRenderer, $"DSP too slow (exceeded by {elapsedTime - Constants.AudioProcessorMaxUpdateTime}ns)"); + } + + _mailbox.SendResponse(MailboxMessage.RenderEnd); + } + } + + Logger.Info?.Print(LogClass.AudioRenderer, "Stopping audio processor"); + _mailbox.SendResponse(MailboxMessage.Stop); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _event.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs b/Ryujinx.Audio/Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs new file mode 100644 index 00000000..bea6215d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/AdpcmDataSourceCommandVersion1.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +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<VoiceUpdateState> State { get; } + + public ulong AdpcmParameter { get; } + public ulong AdpcmParameterSize { get; } + + public DecodingBehaviour DecodingBehaviour { get; } + + public AdpcmDataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + OutputBufferIndex = outputBufferIndex; + SampleRate = serverState.SampleRate; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[Constants.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<float> 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..5ff3a6b4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/AuxiliaryBufferCommand.cs @@ -0,0 +1,205 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Memory; +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(IVirtualMemoryManager memoryManager, ulong bufferAddress, uint countMax, Span<int> 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<int, byte>(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(IVirtualMemoryManager memoryManager, ulong outBufferAddress, uint countMax, ReadOnlySpan<int> 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<int, byte>(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<float> inputBuffer = context.GetBuffer((int)InputBufferIndex); + Span<float> outputBuffer = context.GetBuffer((int)OutputBufferIndex); + + if (IsEffectEnabled) + { + Span<int> inputBufferInt = MemoryMarshal.Cast<float, int>(inputBuffer); + Span<int> outputBufferInt = MemoryMarshal.Cast<float, int>(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 + { + ZeroFill(context.MemoryManager, BufferInfo.SendBufferInfo, Unsafe.SizeOf<AuxiliaryBufferInfo>()); + ZeroFill(context.MemoryManager, BufferInfo.ReturnBufferInfo, Unsafe.SizeOf<AuxiliaryBufferInfo>()); + + if (InputBufferIndex != OutputBufferIndex) + { + inputBuffer.CopyTo(outputBuffer); + } + } + } + + private static void ZeroFill(IVirtualMemoryManager memoryManager, ulong address, int size) + { + ulong endAddress = address + (ulong)size; + + while (address + 7UL < endAddress) + { + memoryManager.Write(address, 0UL); + address += 8; + } + + while (address < endAddress) + { + memoryManager.Write(address, (byte)0); + address++; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs b/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs new file mode 100644 index 00000000..580eb732 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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> BiquadFilterState { get; } + public int InputBufferIndex { get; } + public int OutputBufferIndex { get; } + public bool NeedInitialization { get; } + + public BiquadFilterCommand(int baseIndex, ref BiquadFilterParameter filter, Memory<BiquadFilterState> 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<float> outputBuffer, ReadOnlySpan<float> 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<float> 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..e4c635d5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CircularBufferSinkCommand.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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[Constants.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<float> 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..ba9a0357 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/ClearMixBufferCommand.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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..fee90192 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CommandList.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Server; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +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<float> Buffers { get; } + public uint BufferCount { get; } + + public List<ICommand> Commands { get; } + + public IVirtualMemoryManager MemoryManager { get; } + + public IHardwareDevice OutputDevice { get; private set; } + + public CommandList(AudioRenderSystem renderSystem) : this(renderSystem.MemoryManager, + renderSystem.GetMixBuffer(), + renderSystem.GetSampleCount(), + renderSystem.GetSampleRate(), + renderSystem.GetMixBufferCount(), + renderSystem.GetVoiceChannelCountMax()) + { + } + + public CommandList(IVirtualMemoryManager memoryManager, Memory<float> mixBuffer, uint sampleCount, uint sampleRate, uint mixBufferCount, uint voiceChannelCountMax) + { + SampleCount = sampleCount; + SampleRate = sampleRate; + BufferCount = mixBufferCount + voiceChannelCountMax; + Buffers = mixBuffer; + Commands = new List<ICommand>(); + MemoryManager = memoryManager; + } + + public void AddCommand(ICommand command) + { + Commands.Add(command); + } + + public void AddCommand<T>(T command) where T : unmanaged, ICommand + { + throw new NotImplementedException(); + } + + public Memory<float> GetBufferMemory(int index) + { + return Buffers.Slice(index * (int)SampleCount, (int)SampleCount); + } + + public Span<float> GetBuffer(int index) + { + return Buffers.Span.Slice(index * (int)SampleCount, (int)SampleCount); + } + + public ulong GetTimeElapsedSinceDspStartedProcessing() + { + return (ulong)PerformanceCounter.ElapsedNanoseconds - StartTime; + } + + public void Process(IHardwareDevice 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..8ff1c581 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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..2dc6f04b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/CopyMixBufferCommand.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> inputBuffer = context.GetBuffer(InputBufferIndex); + Span<float> 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..cbc48be7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DataSourceVersion2Command.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +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<VoiceUpdateState> 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<VoiceUpdateState> 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 = (ushort)(channelIndex + outputBufferIndex); + SampleRate = serverState.SampleRate; + Pitch = serverState.Pitch; + + WaveBuffers = new WaveBuffer[Constants.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<float> 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..4ceebcf6 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DelayCommand.cs @@ -0,0 +1,272 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<DelayState> 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<DelayState> state, bool isEnabled, ulong workBuffer, int nodeId) + { + Enabled = true; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + IsEffectEnabled = isEnabled; + + InputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + } + + private void ProcessDelayMono(Span<float> outputBuffer, ReadOnlySpan<float> 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<float>[] outputBuffers, ReadOnlyMemory<float>[] 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<float>[] outputBuffers, ReadOnlyMemory<float>[] 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<float>[] outputBuffers, ReadOnlyMemory<float>[] 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<float>[] inputBuffers = new ReadOnlyMemory<float>[Parameter.ChannelCount]; + Memory<float>[] outputBuffers = new Memory<float>[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..c6feacfb --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DepopForMixBuffersCommand.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> DepopBuffer { get; } + + private const int FixedPointPrecisionForDecay = 15; + + public DepopForMixBuffersCommand(Memory<float> 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<float> 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<float> 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..35275229 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DepopPrepareCommand.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<VoiceUpdateState> State { get; } + public Memory<float> DepopBuffer { get; } + + public DepopPrepareCommand(Memory<VoiceUpdateState> state, Memory<float> depopBuffer, uint mixBufferCount, uint bufferOffset, int nodeId, bool enabled) + { + Enabled = enabled; + NodeId = nodeId; + MixBufferCount = mixBufferCount; + + OutputBufferIndices = new ushort[Constants.MixBufferCountMax]; + + for (int i = 0; i < Constants.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..28b4a5f1 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DeviceSinkCommand.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.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<float> Buffers { get; } + + public DeviceSinkCommand(uint bufferOffset, DeviceSink sink, int sessionId, Memory<float> 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<float> GetBuffer(int index, int sampleCount) + { + return Buffers.Span.Slice(index * sampleCount, sampleCount); + } + + public void Process(CommandList context) + { + IHardwareDevice device = context.OutputDevice; + + if (device.GetSampleRate() == Constants.TargetSampleRate) + { + int channelCount = (int)device.GetChannelCount(); + uint bufferCount = Math.Min(device.GetChannelCount(), InputCount); + + const int sampleCount = Constants.TargetSampleCount; + + short[] outputBuffer = new short[bufferCount * sampleCount]; + + for (int i = 0; i < bufferCount; i++) + { + ReadOnlySpan<float> 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..f81a2849 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/DownMixSurroundToStereoCommand.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<byte> inputBufferOffset, Span<byte> outputBufferOffset, ReadOnlySpan<float> downMixParameter, int nodeId) + { + Enabled = true; + NodeId = nodeId; + + InputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < Constants.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<float> 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<float> frontLeft = context.GetBuffer(InputBufferIndices[0]); + ReadOnlySpan<float> frontRight = context.GetBuffer(InputBufferIndices[1]); + ReadOnlySpan<float> frontCenter = context.GetBuffer(InputBufferIndices[2]); + ReadOnlySpan<float> lowFrequency = context.GetBuffer(InputBufferIndices[3]); + ReadOnlySpan<float> backLeft = context.GetBuffer(InputBufferIndices[4]); + ReadOnlySpan<float> backRight = context.GetBuffer(InputBufferIndices[5]); + + Span<float> stereoLeft = context.GetBuffer(OutputBufferIndices[0]); + Span<float> stereoRight = context.GetBuffer(OutputBufferIndices[1]); + Span<float> unused2 = context.GetBuffer(OutputBufferIndices[2]); + Span<float> unused3 = context.GetBuffer(OutputBufferIndices[3]); + Span<float> unused4 = context.GetBuffer(OutputBufferIndices[4]); + Span<float> 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..e2f00ef6 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/ICommand.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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..566fea92 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/MixCommand.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> outputMix, ReadOnlySpan<float> inputMix) + { + Vector256<float> volumeVec = Vector256.Create(Volume); + + ReadOnlySpan<Vector256<float>> inputVec = MemoryMarshal.Cast<float, Vector256<float>>(inputMix); + Span<Vector256<float>> outputVec = MemoryMarshal.Cast<float, Vector256<float>>(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<float> outputMix, ReadOnlySpan<float> inputMix) + { + Vector128<float> volumeVec = Vector128.Create(Volume); + + ReadOnlySpan<Vector128<float>> inputVec = MemoryMarshal.Cast<float, Vector128<float>>(inputMix); + Span<Vector128<float>> outputVec = MemoryMarshal.Cast<float, Vector128<float>>(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<float> outputMix, ReadOnlySpan<float> inputMix) + { + for (int i = 0; i < inputMix.Length; i++) + { + outputMix[i] += FloatingPointHelper.MultiplyRoundUp(inputMix[i], Volume); + } + } + + private void ProcessMix(Span<float> outputMix, ReadOnlySpan<float> inputMix) + { + if (Avx.IsSupported) + { + ProcessMixAvx(outputMix, inputMix); + } + else if (Sse41.IsSupported) + { + ProcessMixSse41(outputMix, inputMix); + } + else + { + ProcessMixSlowPath(outputMix, inputMix); + } + } + + public void Process(CommandList context) + { + ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex); + Span<float> 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..f34e50e4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/MixRampCommand.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<VoiceUpdateState> State { get; } + + public int LastSampleIndex { get; } + + public MixRampCommand(float volume0, float volume1, uint inputBufferIndex, uint outputBufferIndex, int lastSampleIndex, Memory<VoiceUpdateState> 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<float> outputBuffer, ReadOnlySpan<float> 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<float> inputBuffer = context.GetBuffer(InputBufferIndex); + Span<float> 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..cbd2503b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<VoiceUpdateState> State { get; } + + public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> volume0, Span<float> volume1, Memory<VoiceUpdateState> state, int nodeId) + { + Enabled = true; + MixBufferCount = mixBufferCount; + NodeId = nodeId; + + InputBufferIndices = new ushort[Constants.MixBufferCountMax]; + OutputBufferIndices = new ushort[Constants.MixBufferCountMax]; + Volume0 = new float[Constants.MixBufferCountMax]; + Volume1 = new float[Constants.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<float> outputBuffer, ReadOnlySpan<float> 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<float> inputBuffer = context.GetBuffer(InputBufferIndices[i]); + Span<float> 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..c4b9b0ff --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/PcmFloatDataSourceCommandVersion1.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +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<VoiceUpdateState> State { get; } + public DecodingBehaviour DecodingBehaviour { get; } + + public PcmFloatDataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory<VoiceUpdateState> 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[Constants.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<float> 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..3758dbe0 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/PcmInt16DataSourceCommandVersion1.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +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<VoiceUpdateState> State { get; } + public DecodingBehaviour DecodingBehaviour { get; } + + public PcmInt16DataSourceCommandVersion1(ref Server.Voice.VoiceState serverState, Memory<VoiceUpdateState> 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[Constants.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<float> 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..0cc35712 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/PerformanceCommand.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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..c501bc1c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/Reverb3dCommand.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<Reverb3dState> 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<Reverb3dState> state, bool isEnabled, ulong workBuffer, int nodeId) + { + Enabled = true; + IsEffectEnabled = isEnabled; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + InputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + } + + private void ProcessReverb3dMono(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableMono, TargetEarlyDelayLineIndicesTableMono, TargetOutputFeedbackIndicesTableMono); + } + + private void ProcessReverb3dStereo(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableStereo, TargetEarlyDelayLineIndicesTableStereo, TargetOutputFeedbackIndicesTableStereo); + } + + private void ProcessReverb3dQuadraphonic(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableQuadraphonic, TargetEarlyDelayLineIndicesTableQuadraphonic, TargetOutputFeedbackIndicesTableQuadraphonic); + } + + private void ProcessReverb3dSurround(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverb3dGeneric(outputBuffers, inputBuffers, sampleCount, OutputEarlyIndicesTableSurround, TargetEarlyDelayLineIndicesTableSurround, TargetOutputFeedbackIndicesTableSurround); + } + + private void ProcessReverb3dGeneric(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount, ReadOnlySpan<int> outputEarlyIndicesTable, ReadOnlySpan<int> targetEarlyDelayLineIndicesTable, ReadOnlySpan<int> targetOutputFeedbackIndicesTable) + { + const int delayLineSampleIndexOffset = 1; + + ref Reverb3dState state = ref State.Span[0]; + + bool isMono = Parameter.ChannelCount == 1; + bool isSurround = Parameter.ChannelCount == 6; + + float[] outputValues = new float[Constants.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<float>[] inputBuffers = new ReadOnlyMemory<float>[Parameter.ChannelCount]; + Memory<float>[] outputBuffers = new Memory<float>[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..0ed955de --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/ReverbCommand.cs @@ -0,0 +1,284 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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[Constants.ChannelCountMax] { 0, 1, 2, 3, 4, 5 }; + private static readonly int[] TargetOutputFeedbackIndicesTableSurround = new int[Constants.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<ReverbState> 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<ReverbState> state, bool isEnabled, ulong workBuffer, int nodeId, bool isLongSizePreDelaySupported) + { + Enabled = true; + IsEffectEnabled = isEnabled; + NodeId = nodeId; + _parameter = parameter; + State = state; + WorkBuffer = workBuffer; + + InputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < Parameter.ChannelCount; i++) + { + InputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Input[i]); + OutputBufferIndices[i] = (ushort)(bufferOffset + Parameter.Output[i]); + } + + IsLongSizePreDelaySupported = isLongSizePreDelaySupported; + } + + private void ProcessReverbMono(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableMono, + TargetEarlyDelayLineIndicesTableMono, + TargetOutputFeedbackIndicesTableMono, + OutputIndicesTableMono); + } + + private void ProcessReverbStereo(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableStereo, + TargetEarlyDelayLineIndicesTableStereo, + TargetOutputFeedbackIndicesTableStereo, + OutputIndicesTableStereo); + } + + private void ProcessReverbQuadraphonic(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableQuadraphonic, + TargetEarlyDelayLineIndicesTableQuadraphonic, + TargetOutputFeedbackIndicesTableQuadraphonic, + OutputIndicesTableQuadraphonic); + } + + private void ProcessReverbSurround(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount) + { + ProcessReverbGeneric(outputBuffers, + inputBuffers, + sampleCount, + OutputEarlyIndicesTableSurround, + TargetEarlyDelayLineIndicesTableSurround, + TargetOutputFeedbackIndicesTableSurround, + OutputIndicesTableSurround); + } + + private void ProcessReverbGeneric(Memory<float>[] outputBuffers, ReadOnlyMemory<float>[] inputBuffers, uint sampleCount, ReadOnlySpan<int> outputEarlyIndicesTable, ReadOnlySpan<int> targetEarlyDelayLineIndicesTable, ReadOnlySpan<int> targetOutputFeedbackIndicesTable, ReadOnlySpan<int> 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[Constants.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<float>[] inputBuffers = new ReadOnlyMemory<float>[Parameter.ChannelCount]; + Memory<float>[] outputBuffers = new Memory<float>[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..6c446f05 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/UpsampleCommand.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> OutBuffer { get; } + + public UpsampleCommand(uint bufferOffset, UpsamplerState info, uint inputCount, Span<byte> 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<float> GetBuffer(int index, int sampleCount) + { + return UpsamplerInfo.OutputBuffer.Span.Slice(index * sampleCount, sampleCount); + } + + public void Process(CommandList context) + { + float ratio = (float)InputSampleRate / Constants.TargetSampleRate; + + uint bufferCount = Math.Min(BufferCount, UpsamplerInfo.SourceSampleCount); + + for (int i = 0; i < bufferCount; i++) + { + Span<float> inputBuffer = context.GetBuffer(UpsamplerInfo.InputBufferIndices[i]); + Span<float> 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..b58ae1f8 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/VolumeCommand.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> outputBuffer, ReadOnlySpan<float> inputBuffer) + { + Vector256<float> volumeVec = Vector256.Create(Volume); + + ReadOnlySpan<Vector256<float>> inputVec = MemoryMarshal.Cast<float, Vector256<float>>(inputBuffer); + Span<Vector256<float>> outputVec = MemoryMarshal.Cast<float, Vector256<float>>(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<float> outputBuffer, ReadOnlySpan<float> inputBuffer) + { + Vector128<float> volumeVec = Vector128.Create(Volume); + + ReadOnlySpan<Vector128<float>> inputVec = MemoryMarshal.Cast<float, Vector128<float>>(inputBuffer); + Span<Vector128<float>> outputVec = MemoryMarshal.Cast<float, Vector128<float>>(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<float> outputBuffer, ReadOnlySpan<float> 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<float> outputBuffer, ReadOnlySpan<float> inputBuffer) + { + for (int i = 0; i < outputBuffer.Length; i++) + { + outputBuffer[i] = FloatingPointHelper.MultiplyRoundUp(inputBuffer[i], Volume); + } + } + + public void Process(CommandList context) + { + ReadOnlySpan<float> inputBuffer = context.GetBuffer(InputBufferIndex); + Span<float> 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..d07926c8 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Command/VolumeRampCommand.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +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<float> outputBuffer, ReadOnlySpan<float> 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<float> inputBuffer = context.GetBuffer(InputBufferIndex); + Span<float> outputBuffer = context.GetBuffer(OutputBufferIndex); + + ProcessVolumeRamp(outputBuffer, inputBuffer, (int)context.SampleCount); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/DataSourceHelper.cs b/Ryujinx.Audio/Renderer/Dsp/DataSourceHelper.cs new file mode 100644 index 00000000..c9514529 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/DataSourceHelper.cs @@ -0,0 +1,409 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class DataSourceHelper + { + private const int FixedPointPrecision = 15; + + public class WaveBufferInformation + { + public Memory<VoiceUpdateState> State; + public uint SourceSampleRate; + public SampleFormat SampleFormat; + public float Pitch; + public DecodingBehaviour DecodingBehaviour; + public WaveBuffer[] WaveBuffers; + public ulong ExtraParameter; + public ulong ExtraParameterSize; + public int ChannelIndex; + public int ChannelCount; + public SampleRateConversionQuality SrcQuality; + } + + private static int GetPitchLimitBySrcQuality(SampleRateConversionQuality quality) + { + switch (quality) + { + case SampleRateConversionQuality.Default: + case SampleRateConversionQuality.Low: + return 4; + case SampleRateConversionQuality.High: + return 8; + default: + throw new ArgumentException($"{quality}"); + } + } + + public static void ProcessWaveBuffers(IVirtualMemoryManager memoryManager, Span<float> outputBuffer, WaveBufferInformation info, uint targetSampleRate, int sampleCount) + { + const int tempBufferSize = 0x3F00; + + ref VoiceUpdateState state = ref info.State.Span[0]; + + short[] tempBuffer = ArrayPool<short>.Shared.Rent(tempBufferSize); + + float sampleRateRatio = ((float)info.SourceSampleRate / targetSampleRate * info.Pitch); + + float fraction = state.Fraction; + int waveBufferIndex = (int)state.WaveBufferIndex; + ulong playedSampleCount = state.PlayedSampleCount; + int offset = state.Offset; + uint waveBufferConsumed = state.WaveBufferConsumed; + + int pitchMaxLength = GetPitchLimitBySrcQuality(info.SrcQuality); + + int totalNeededSize = (int)MathF.Truncate(fraction + sampleRateRatio * sampleCount); + + if (totalNeededSize + pitchMaxLength <= tempBufferSize && totalNeededSize >= 0) + { + int sourceSampleCountToProcess = sampleCount; + + int maxSampleCountPerIteration = Math.Min((int)MathF.Truncate((tempBufferSize - fraction) / sampleRateRatio), sampleCount); + + bool isStarving = false; + + int i = 0; + + while (i < sourceSampleCountToProcess) + { + int tempBufferIndex = 0; + + if (!info.DecodingBehaviour.HasFlag(DecodingBehaviour.SkipPitchAndSampleRateConversion)) + { + state.Pitch.ToSpan().Slice(0, pitchMaxLength).CopyTo(tempBuffer.AsSpan()); + tempBufferIndex += pitchMaxLength; + } + + int sampleCountToProcess = Math.Min(sourceSampleCountToProcess, maxSampleCountPerIteration); + + int y = 0; + + int sampleCountToDecode = (int)MathF.Truncate(fraction + sampleRateRatio * sampleCountToProcess); + + while (y < sampleCountToDecode) + { + if (waveBufferIndex >= Constants.VoiceWaveBufferCount) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Invalid WaveBuffer index {waveBufferIndex}"); + + waveBufferIndex = 0; + playedSampleCount = 0; + } + + if (!state.IsWaveBufferValid[waveBufferIndex]) + { + isStarving = true; + break; + } + + ref WaveBuffer waveBuffer = ref info.WaveBuffers[waveBufferIndex]; + + if (offset == 0 && info.SampleFormat == SampleFormat.Adpcm && waveBuffer.Context != 0) + { + state.LoopContext = memoryManager.Read<AdpcmLoopContext>(waveBuffer.Context); + } + + Span<short> tempSpan = tempBuffer.AsSpan().Slice(tempBufferIndex + y); + + int decodedSampleCount = -1; + + int targetSampleStartOffset; + int targetSampleEndOffset; + + if (state.LoopCount > 0 && waveBuffer.LoopStartSampleOffset != 0 && waveBuffer.LoopEndSampleOffset != 0 && waveBuffer.LoopStartSampleOffset <= waveBuffer.LoopEndSampleOffset) + { + targetSampleStartOffset = (int)waveBuffer.LoopStartSampleOffset; + targetSampleEndOffset = (int)waveBuffer.LoopEndSampleOffset; + } + else + { + targetSampleStartOffset = (int)waveBuffer.StartSampleOffset; + targetSampleEndOffset = (int)waveBuffer.EndSampleOffset; + } + + int targetWaveBufferSampleCount = targetSampleEndOffset - targetSampleStartOffset; + + switch (info.SampleFormat) + { + case SampleFormat.Adpcm: + ReadOnlySpan<byte> waveBufferAdpcm = ReadOnlySpan<byte>.Empty; + + if (waveBuffer.Buffer != 0 && waveBuffer.BufferSize != 0) + { + // TODO: we are possibly copying a lot of unneeded data here, we should only take what we need. + waveBufferAdpcm = memoryManager.GetSpan(waveBuffer.Buffer, (int)waveBuffer.BufferSize); + } + + ReadOnlySpan<short> coefficients = MemoryMarshal.Cast<byte, short>(memoryManager.GetSpan(info.ExtraParameter, (int)info.ExtraParameterSize)); + decodedSampleCount = AdpcmHelper.Decode(tempSpan, waveBufferAdpcm, targetSampleStartOffset, targetSampleEndOffset, offset, sampleCountToDecode - y, coefficients, ref state.LoopContext); + break; + case SampleFormat.PcmInt16: + ReadOnlySpan<short> waveBufferPcm16 = ReadOnlySpan<short>.Empty; + + if (waveBuffer.Buffer != 0 && waveBuffer.BufferSize != 0) + { + ulong bufferOffset = waveBuffer.Buffer + PcmHelper.GetBufferOffset<short>(targetSampleStartOffset, offset, info.ChannelCount); + int bufferSize = PcmHelper.GetBufferSize<short>(targetSampleStartOffset, targetSampleEndOffset, offset, sampleCountToDecode - y) * info.ChannelCount; + + waveBufferPcm16 = MemoryMarshal.Cast<byte, short>(memoryManager.GetSpan(bufferOffset, bufferSize)); + } + + decodedSampleCount = PcmHelper.Decode(tempSpan, waveBufferPcm16, targetSampleStartOffset, targetSampleEndOffset, info.ChannelIndex, info.ChannelCount); + break; + case SampleFormat.PcmFloat: + ReadOnlySpan<float> waveBufferPcmFloat = ReadOnlySpan<float>.Empty; + + if (waveBuffer.Buffer != 0 && waveBuffer.BufferSize != 0) + { + ulong bufferOffset = waveBuffer.Buffer + PcmHelper.GetBufferOffset<float>(targetSampleStartOffset, offset, info.ChannelCount); + int bufferSize = PcmHelper.GetBufferSize<float>(targetSampleStartOffset, targetSampleEndOffset, offset, sampleCountToDecode - y) * info.ChannelCount; + + waveBufferPcmFloat = MemoryMarshal.Cast<byte, float>(memoryManager.GetSpan(bufferOffset, bufferSize)); + } + + decodedSampleCount = PcmHelper.Decode(tempSpan, waveBufferPcmFloat, targetSampleStartOffset, targetSampleEndOffset, info.ChannelIndex, info.ChannelCount); + break; + default: + Logger.Warning?.Print(LogClass.AudioRenderer, $"Unsupported sample format {info.SampleFormat}"); + break; + } + + Debug.Assert(decodedSampleCount <= sampleCountToDecode); + + if (decodedSampleCount < 0) + { + Logger.Warning?.Print(LogClass.AudioRenderer, $"Decoding failed, skipping WaveBuffer"); + + state.MarkEndOfBufferWaveBufferProcessing(ref waveBuffer, ref waveBufferIndex, ref waveBufferConsumed, ref playedSampleCount); + decodedSampleCount = 0; + } + + y += decodedSampleCount; + offset += decodedSampleCount; + playedSampleCount += (uint)decodedSampleCount; + + if (offset >= targetWaveBufferSampleCount || decodedSampleCount == 0) + { + offset = 0; + + if (waveBuffer.Looping) + { + state.LoopCount++; + + if (waveBuffer.LoopCount >= 0) + { + if (decodedSampleCount == 0 || state.LoopCount > waveBuffer.LoopCount) + { + state.MarkEndOfBufferWaveBufferProcessing(ref waveBuffer, ref waveBufferIndex, ref waveBufferConsumed, ref playedSampleCount); + } + } + + if (decodedSampleCount == 0) + { + isStarving = true; + break; + } + + if (info.DecodingBehaviour.HasFlag(DecodingBehaviour.PlayedSampleCountResetWhenLooping)) + { + playedSampleCount = 0; + } + } + else + { + state.MarkEndOfBufferWaveBufferProcessing(ref waveBuffer, ref waveBufferIndex, ref waveBufferConsumed, ref playedSampleCount); + } + } + } + + Span<float> outputSpan = outputBuffer.Slice(i); + Span<int> outputSpanInt = MemoryMarshal.Cast<float, int>(outputSpan); + + if (info.DecodingBehaviour.HasFlag(DecodingBehaviour.SkipPitchAndSampleRateConversion)) + { + for (int j = 0; j < y; j++) + { + outputBuffer[j] = tempBuffer[j]; + } + } + else + { + Span<short> tempSpan = tempBuffer.AsSpan().Slice(tempBufferIndex + y); + + tempSpan.Slice(0, sampleCountToDecode - y).Fill(0); + + ToFloat(outputBuffer, outputSpanInt, sampleCountToProcess); + + ResamplerHelper.Resample(outputBuffer, tempBuffer, sampleRateRatio, ref fraction, sampleCountToProcess, info.SrcQuality, y != sourceSampleCountToProcess || info.Pitch != 1.0f); + + tempBuffer.AsSpan().Slice(sampleCountToDecode, pitchMaxLength).CopyTo(state.Pitch.ToSpan()); + } + + i += sampleCountToProcess; + } + + Debug.Assert(sourceSampleCountToProcess == i || !isStarving); + + state.WaveBufferConsumed = waveBufferConsumed; + state.Offset = offset; + state.PlayedSampleCount = playedSampleCount; + state.WaveBufferIndex = (uint)waveBufferIndex; + state.Fraction = fraction; + } + + ArrayPool<short>.Shared.Return(tempBuffer); + } + + private static void ToFloatAvx(Span<float> output, ReadOnlySpan<int> input, int sampleCount) + { + ReadOnlySpan<Vector256<int>> inputVec = MemoryMarshal.Cast<int, Vector256<int>>(input); + Span<Vector256<float>> outputVec = MemoryMarshal.Cast<float, Vector256<float>>(output); + + int sisdStart = inputVec.Length * 8; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Avx.ConvertToVector256Single(inputVec[i]); + } + + for (int i = sisdStart; i < sampleCount; i++) + { + output[i] = input[i]; + } + } + + private static void ToFloatSse2(Span<float> output, ReadOnlySpan<int> input, int sampleCount) + { + ReadOnlySpan<Vector128<int>> inputVec = MemoryMarshal.Cast<int, Vector128<int>>(input); + Span<Vector128<float>> outputVec = MemoryMarshal.Cast<float, Vector128<float>>(output); + + int sisdStart = inputVec.Length * 4; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Sse2.ConvertToVector128Single(inputVec[i]); + } + + for (int i = sisdStart; i < sampleCount; i++) + { + output[i] = input[i]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToFloatSlow(Span<float> output, ReadOnlySpan<int> input, int sampleCount) + { + for (int i = 0; i < sampleCount; i++) + { + output[i] = input[i]; + } + } + + public static void ToFloat(Span<float> output, ReadOnlySpan<int> input, int sampleCount) + { + if (Avx.IsSupported) + { + ToFloatAvx(output, input, sampleCount); + } + else if (Sse2.IsSupported) + { + ToFloatSse2(output, input, sampleCount); + } + else + { + ToFloatSlow(output, input, sampleCount); + } + } + + public static void ToIntAvx(Span<int> output, ReadOnlySpan<float> input, int sampleCount) + { + ReadOnlySpan<Vector256<float>> inputVec = MemoryMarshal.Cast<float, Vector256<float>>(input); + Span<Vector256<int>> outputVec = MemoryMarshal.Cast<int, Vector256<int>>(output); + + int sisdStart = inputVec.Length * 8; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Avx.ConvertToVector256Int32(inputVec[i]); + } + + for (int i = sisdStart; i < sampleCount; i++) + { + output[i] = (int)input[i]; + } + } + + public static void ToIntSse2(Span<int> output, ReadOnlySpan<float> input, int sampleCount) + { + ReadOnlySpan<Vector128<float>> inputVec = MemoryMarshal.Cast<float, Vector128<float>>(input); + Span<Vector128<int>> outputVec = MemoryMarshal.Cast<int, Vector128<int>>(output); + + int sisdStart = inputVec.Length * 4; + + for (int i = 0; i < inputVec.Length; i++) + { + outputVec[i] = Avx.ConvertToVector128Int32(inputVec[i]); + } + + for (int i = sisdStart; i < sampleCount; i++) + { + output[i] = (int)input[i]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToIntSlow(Span<int> output, ReadOnlySpan<float> input, int sampleCount) + { + for (int i = 0; i < sampleCount; i++) + { + output[i] = (int)input[i]; + } + } + + public static void ToInt(Span<int> output, ReadOnlySpan<float> input, int sampleCount) + { + if (Avx.IsSupported) + { + ToIntAvx(output, input, sampleCount); + } + else if (Sse2.IsSupported) + { + ToIntSse2(output, input, sampleCount); + } + else + { + ToIntSlow(output, input, sampleCount); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/DecayDelay.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/DecayDelay.cs new file mode 100644 index 00000000..8a815c56 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Effect/DecayDelay.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Dsp.Effect +{ + public class DecayDelay : IDelayLine + { + private readonly IDelayLine _delayLine; + + public uint CurrentSampleCount => _delayLine.CurrentSampleCount; + + public uint SampleCountMax => _delayLine.SampleCountMax; + + private float _decayRate; + + public DecayDelay(IDelayLine delayLine) + { + _decayRate = 0.0f; + _delayLine = delayLine; + } + + public void SetDecayRate(float decayRate) + { + _decayRate = decayRate; + } + + public float Update(float value) + { + float delayLineValue = _delayLine.Read(); + float processedValue = value - (_decayRate * delayLineValue); + + return _delayLine.Update(processedValue) + processedValue * _decayRate; + } + + public void SetDelay(float delayTime) + { + _delayLine.SetDelay(delayTime); + } + + public float Read() + { + return _delayLine.Read(); + } + + public float TapUnsafe(uint sampleIndex, int offset) + { + return _delayLine.TapUnsafe(sampleIndex, offset); + } + + public float Tap(uint sampleIndex) + { + return _delayLine.Tap(sampleIndex); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLine.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLine.cs new file mode 100644 index 00000000..d178e699 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLine.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.Effect +{ + public class DelayLine : IDelayLine + { + private float[] _workBuffer; + private uint _sampleRate; + private uint _currentSampleIndex; + private uint _lastSampleIndex; + + public uint CurrentSampleCount { get; private set; } + public uint SampleCountMax { get; private set; } + + public DelayLine(uint sampleRate, float delayTimeMax) + { + _sampleRate = sampleRate; + SampleCountMax = IDelayLine.GetSampleCount(_sampleRate, delayTimeMax); + _workBuffer = new float[SampleCountMax]; + + SetDelay(delayTimeMax); + } + + private void ConfigureDelay(uint targetSampleCount) + { + CurrentSampleCount = Math.Min(SampleCountMax, targetSampleCount); + _currentSampleIndex = 0; + + if (CurrentSampleCount == 0) + { + _lastSampleIndex = 0; + } + else + { + _lastSampleIndex = CurrentSampleCount - 1; + } + } + + public void SetDelay(float delayTime) + { + ConfigureDelay(IDelayLine.GetSampleCount(_sampleRate, delayTime)); + } + + public float Read() + { + return _workBuffer[_currentSampleIndex]; + } + + public float Update(float value) + { + float output = Read(); + + _workBuffer[_currentSampleIndex++] = value; + + if (_currentSampleIndex >= _lastSampleIndex) + { + _currentSampleIndex = 0; + } + + return output; + } + + public float TapUnsafe(uint sampleIndex, int offset) + { + return IDelayLine.Tap(_workBuffer, (int)_currentSampleIndex, (int)sampleIndex + offset, (int)CurrentSampleCount); + } + + public float Tap(uint sampleIndex) + { + if (sampleIndex >= CurrentSampleCount) + { + sampleIndex = CurrentSampleCount - 1; + } + + return TapUnsafe(sampleIndex, -1); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLineReverb3d.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLineReverb3d.cs new file mode 100644 index 00000000..a30dcc63 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Effect/DelayLineReverb3d.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Dsp.Effect +{ + public class DelayLine3d : IDelayLine + { + private float[] _workBuffer; + private uint _sampleRate; + private uint _currentSampleIndex; + private uint _lastSampleIndex; + + public uint CurrentSampleCount { get; private set; } + public uint SampleCountMax { get; private set; } + + public DelayLine3d(uint sampleRate, float delayTimeMax) + { + _sampleRate = sampleRate; + SampleCountMax = IDelayLine.GetSampleCount(_sampleRate, delayTimeMax); + _workBuffer = new float[SampleCountMax + 1]; + + SetDelay(delayTimeMax); + } + + private void ConfigureDelay(uint targetSampleCount) + { + if (SampleCountMax >= targetSampleCount) + { + CurrentSampleCount = targetSampleCount; + _lastSampleIndex = (_currentSampleIndex + targetSampleCount) % (SampleCountMax + 1); + } + } + + public void SetDelay(float delayTime) + { + ConfigureDelay(IDelayLine.GetSampleCount(_sampleRate, delayTime)); + } + + public float Read() + { + return _workBuffer[_currentSampleIndex]; + } + + public float Update(float value) + { + Debug.Assert(!float.IsNaN(value) && !float.IsInfinity(value)); + + _workBuffer[_lastSampleIndex++] = value; + + float output = Read(); + + _currentSampleIndex++; + + if (_currentSampleIndex >= SampleCountMax) + { + _currentSampleIndex = 0; + } + + if (_lastSampleIndex >= SampleCountMax) + { + _lastSampleIndex = 0; + } + + return output; + } + + public float TapUnsafe(uint sampleIndex, int offset) + { + return IDelayLine.Tap(_workBuffer, (int)_lastSampleIndex, (int)sampleIndex + offset, (int)SampleCountMax + 1); + } + + public float Tap(uint sampleIndex) + { + return TapUnsafe(sampleIndex, -1); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/Effect/IDelayLine.cs b/Ryujinx.Audio/Renderer/Dsp/Effect/IDelayLine.cs new file mode 100644 index 00000000..ac59445e --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/Effect/IDelayLine.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp.Effect +{ + public interface IDelayLine + { + uint CurrentSampleCount { get; } + uint SampleCountMax { get; } + + void SetDelay(float delayTime); + float Read(); + float Update(float value); + + float TapUnsafe(uint sampleIndex, int offset); + float Tap(uint sampleIndex); + + public static float Tap(Span<float> workBuffer, int baseIndex, int sampleIndex, int delaySampleCount) + { + int targetIndex = baseIndex - sampleIndex; + + if (targetIndex < 0) + { + targetIndex += delaySampleCount; + } + + return workBuffer[targetIndex]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetSampleCount(uint sampleRate, float delayTime) + { + return (uint)MathF.Round(sampleRate * delayTime); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs new file mode 100644 index 00000000..0afd8926 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/FixedPointHelper.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class FixedPointHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ToInt(long value, int qBits) + { + return (int)(value >> qBits); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ToFloat(long value, int qBits) + { + return (float)value / (1 << qBits); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ToFixed(float value, int qBits) + { + return (int)(value * (1 << qBits)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundUpAndToInt(long value, int qBits) + { + int half = 1 << (qBits - 1); + + return ToInt(value + half, qBits); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs new file mode 100644 index 00000000..b143d8f9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/FloatingPointHelper.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class FloatingPointHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MultiplyRoundDown(float a, float b) + { + return RoundDown(a * b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float RoundDown(float a) + { + return MathF.Round(a, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float RoundUp(float a) + { + return MathF.Round(a); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float MultiplyRoundUp(float a, float b) + { + return RoundUp(a * b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pow10(float x) + { + // NOTE: Nintendo implementation uses Q15 and a LUT for this, we don't. + // As such, we support the same ranges as Nintendo to avoid unexpected behaviours. + if (x >= 0.0f) + { + return 1.0f; + } + else if (x <= -5.3f) + { + return 0.0f; + } + + return MathF.Pow(10, x); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DegreesToRadians(float degrees) + { + return degrees * MathF.PI / 180.0f; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Cos(float value) + { + return MathF.Cos(DegreesToRadians(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Sin(float value) + { + return MathF.Sin(DegreesToRadians(value)); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs b/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs new file mode 100644 index 00000000..12c1d475 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class PcmHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCountToDecode(int startSampleOffset, int endSampleOffset, int offset, int count) + { + return Math.Min(count, endSampleOffset - startSampleOffset - offset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetBufferOffset<T>(int startSampleOffset, int offset, int channelCount) where T : unmanaged + { + return (ulong)(Unsafe.SizeOf<T>() * channelCount * (startSampleOffset + offset)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBufferSize<T>(int startSampleOffset, int endSampleOffset, int offset, int count) where T : unmanaged + { + return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf<T>(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Decode(Span<short> output, ReadOnlySpan<short> input, int startSampleOffset, int endSampleOffset, int channelIndex, int channelCount) + { + if (input.IsEmpty || endSampleOffset < startSampleOffset) + { + return 0; + } + + int decodedCount = input.Length / channelCount; + + for (int i = 0; i < decodedCount; i++) + { + output[i] = input[i * channelCount + channelIndex]; + } + + return decodedCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Decode(Span<short> output, ReadOnlySpan<float> input, int startSampleOffset, int endSampleOffset, int channelIndex, int channelCount) + { + if (input.IsEmpty || endSampleOffset < startSampleOffset) + { + return 0; + } + + int decodedCount = input.Length / channelCount; + + for (int i = 0; i < decodedCount; i++) + { + output[i] = (short)(input[i * channelCount + channelIndex] * short.MaxValue); + } + + return decodedCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Saturate(float value) + { + if (value > short.MaxValue) + { + return short.MaxValue; + } + + if (value < short.MinValue) + { + return short.MinValue; + } + + return (short)value; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/ResamplerHelper.cs b/Ryujinx.Audio/Renderer/Dsp/ResamplerHelper.cs new file mode 100644 index 00000000..d40da412 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/ResamplerHelper.cs @@ -0,0 +1,624 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Dsp +{ + public static class ResamplerHelper + { + #region "Default Quality Lookup Tables" + private static short[] _normalCurveLut0 = new short[] + { + 6600, 19426, 6722, 3, 6479, 19424, 6845, 9, 6359, 19419, 6968, 15, 6239, 19412, 7093, 22, + 6121, 19403, 7219, 28, 6004, 19391, 7345, 34, 5888, 19377, 7472, 41, 5773, 19361, 7600, 48, + 5659, 19342, 7728, 55, 5546, 19321, 7857, 62, 5434, 19298, 7987, 69, 5323, 19273, 8118, 77, + 5213, 19245, 8249, 84, 5104, 19215, 8381, 92, 4997, 19183, 8513, 101, 4890, 19148, 8646, 109, + 4785, 19112, 8780, 118, 4681, 19073, 8914, 127, 4579, 19031, 9048, 137, 4477, 18988, 9183, 147, + 4377, 18942, 9318, 157, 4277, 18895, 9454, 168, 4179, 18845, 9590, 179, 4083, 18793, 9726, 190, + 3987, 18738, 9863, 202, 3893, 18682, 10000, 215, 3800, 18624, 10137, 228, 3709, 18563, 10274, 241, + 3618, 18500, 10411, 255, 3529, 18436, 10549, 270, 3441, 18369, 10687, 285, 3355, 18300, 10824, 300, + 3269, 18230, 10962, 317, 3186, 18157, 11100, 334, 3103, 18082, 11238, 351, 3022, 18006, 11375, 369, + 2942, 17927, 11513, 388, 2863, 17847, 11650, 408, 2785, 17765, 11788, 428, 2709, 17681, 11925, 449, + 2635, 17595, 12062, 471, 2561, 17507, 12198, 494, 2489, 17418, 12334, 517, 2418, 17327, 12470, 541, + 2348, 17234, 12606, 566, 2280, 17140, 12741, 592, 2213, 17044, 12876, 619, 2147, 16946, 13010, 647, + 2083, 16846, 13144, 675, 2020, 16745, 13277, 704, 1958, 16643, 13409, 735, 1897, 16539, 13541, 766, + 1838, 16434, 13673, 798, 1780, 16327, 13803, 832, 1723, 16218, 13933, 866, 1667, 16109, 14062, 901, + 1613, 15998, 14191, 937, 1560, 15885, 14318, 975, 1508, 15772, 14445, 1013, 1457, 15657, 14571, 1052, + 1407, 15540, 14695, 1093, 1359, 15423, 14819, 1134, 1312, 15304, 14942, 1177, 1266, 15185, 15064, 1221, + 1221, 15064, 15185, 1266, 1177, 14942, 15304, 1312, 1134, 14819, 15423, 1359, 1093, 14695, 15540, 1407, + 1052, 14571, 15657, 1457, 1013, 14445, 15772, 1508, 975, 14318, 15885, 1560, 937, 14191, 15998, 1613, + 901, 14062, 16109, 1667, 866, 13933, 16218, 1723, 832, 13803, 16327, 1780, 798, 13673, 16434, 1838, + 766, 13541, 16539, 1897, 735, 13409, 16643, 1958, 704, 13277, 16745, 2020, 675, 13144, 16846, 2083, + 647, 13010, 16946, 2147, 619, 12876, 17044, 2213, 592, 12741, 17140, 2280, 566, 12606, 17234, 2348, + 541, 12470, 17327, 2418, 517, 12334, 17418, 2489, 494, 12198, 17507, 2561, 471, 12062, 17595, 2635, + 449, 11925, 17681, 2709, 428, 11788, 17765, 2785, 408, 11650, 17847, 2863, 388, 11513, 17927, 2942, + 369, 11375, 18006, 3022, 351, 11238, 18082, 3103, 334, 11100, 18157, 3186, 317, 10962, 18230, 3269, + 300, 10824, 18300, 3355, 285, 10687, 18369, 3441, 270, 10549, 18436, 3529, 255, 10411, 18500, 3618, + 241, 10274, 18563, 3709, 228, 10137, 18624, 3800, 215, 10000, 18682, 3893, 202, 9863, 18738, 3987, + 190, 9726, 18793, 4083, 179, 9590, 18845, 4179, 168, 9454, 18895, 4277, 157, 9318, 18942, 4377, + 147, 9183, 18988, 4477, 137, 9048, 19031, 4579, 127, 8914, 19073, 4681, 118, 8780, 19112, 4785, + 109, 8646, 19148, 4890, 101, 8513, 19183, 4997, 92, 8381, 19215, 5104, 84, 8249, 19245, 5213, + 77, 8118, 19273, 5323, 69, 7987, 19298, 5434, 62, 7857, 19321, 5546, 55, 7728, 19342, 5659, + 48, 7600, 19361, 5773, 41, 7472, 19377, 5888, 34, 7345, 19391, 6004, 28, 7219, 19403, 6121, + 22, 7093, 19412, 6239, 15, 6968, 19419, 6359, 9, 6845, 19424, 6479, 3, 6722, 19426, 6600 + }; + + private static short[] _normalCurveLut1 = new short[] + { + -68, 32639, 69, -5, -200, 32630, 212, -15, -328, 32613, 359, -26, -450, 32586, 512, -36, + -568, 32551, 669, -47, -680, 32507, 832, -58, -788, 32454, 1000, -69, -891, 32393, 1174, -80, + -990, 32323, 1352, -92, -1084, 32244, 1536, -103, -1173, 32157, 1724, -115, -1258, 32061, 1919, -128, + -1338, 31956, 2118, -140, -1414, 31844, 2322, -153, -1486, 31723, 2532, -167, -1554, 31593, 2747, -180, + -1617, 31456, 2967, -194, -1676, 31310, 3192, -209, -1732, 31157, 3422, -224, -1783, 30995, 3657, -240, + -1830, 30826, 3897, -256, -1874, 30649, 4143, -272, -1914, 30464, 4393, -289, -1951, 30272, 4648, -307, + -1984, 30072, 4908, -325, -2014, 29866, 5172, -343, -2040, 29652, 5442, -362, -2063, 29431, 5716, -382, + -2083, 29203, 5994, -403, -2100, 28968, 6277, -424, -2114, 28727, 6565, -445, -2125, 28480, 6857, -468, + -2133, 28226, 7153, -490, -2139, 27966, 7453, -514, -2142, 27700, 7758, -538, -2142, 27428, 8066, -563, + -2141, 27151, 8378, -588, -2136, 26867, 8694, -614, -2130, 26579, 9013, -641, -2121, 26285, 9336, -668, + -2111, 25987, 9663, -696, -2098, 25683, 9993, -724, -2084, 25375, 10326, -753, -2067, 25063, 10662, -783, + -2049, 24746, 11000, -813, -2030, 24425, 11342, -844, -2009, 24100, 11686, -875, -1986, 23771, 12033, -907, + -1962, 23438, 12382, -939, -1937, 23103, 12733, -972, -1911, 22764, 13086, -1005, -1883, 22422, 13441, -1039, + -1855, 22077, 13798, -1072, -1825, 21729, 14156, -1107, -1795, 21380, 14516, -1141, -1764, 21027, 14877, -1176, + -1732, 20673, 15239, -1211, -1700, 20317, 15602, -1246, -1667, 19959, 15965, -1282, -1633, 19600, 16329, -1317, + -1599, 19239, 16694, -1353, -1564, 18878, 17058, -1388, -1530, 18515, 17423, -1424, -1495, 18151, 17787, -1459, + -1459, 17787, 18151, -1495, -1424, 17423, 18515, -1530, -1388, 17058, 18878, -1564, -1353, 16694, 19239, -1599, + -1317, 16329, 19600, -1633, -1282, 15965, 19959, -1667, -1246, 15602, 20317, -1700, -1211, 15239, 20673, -1732, + -1176, 14877, 21027, -1764, -1141, 14516, 21380, -1795, -1107, 14156, 21729, -1825, -1072, 13798, 22077, -1855, + -1039, 13441, 22422, -1883, -1005, 13086, 22764, -1911, -972, 12733, 23103, -1937, -939, 12382, 23438, -1962, + -907, 12033, 23771, -1986, -875, 11686, 24100, -2009, -844, 11342, 24425, -2030, -813, 11000, 24746, -2049, + -783, 10662, 25063, -2067, -753, 10326, 25375, -2084, -724, 9993, 25683, -2098, -696, 9663, 25987, -2111, + -668, 9336, 26285, -2121, -641, 9013, 26579, -2130, -614, 8694, 26867, -2136, -588, 8378, 27151, -2141, + -563, 8066, 27428, -2142, -538, 7758, 27700, -2142, -514, 7453, 27966, -2139, -490, 7153, 28226, -2133, + -468, 6857, 28480, -2125, -445, 6565, 28727, -2114, -424, 6277, 28968, -2100, -403, 5994, 29203, -2083, + -382, 5716, 29431, -2063, -362, 5442, 29652, -2040, -343, 5172, 29866, -2014, -325, 4908, 30072, -1984, + -307, 4648, 30272, -1951, -289, 4393, 30464, -1914, -272, 4143, 30649, -1874, -256, 3897, 30826, -1830, + -240, 3657, 30995, -1783, -224, 3422, 31157, -1732, -209, 3192, 31310, -1676, -194, 2967, 31456, -1617, + -180, 2747, 31593, -1554, -167, 2532, 31723, -1486, -153, 2322, 31844, -1414, -140, 2118, 31956, -1338, + -128, 1919, 32061, -1258, -115, 1724, 32157, -1173, -103, 1536, 32244, -1084, -92, 1352, 32323, -990, + -80, 1174, 32393, -891, -69, 1000, 32454, -788, -58, 832, 32507, -680, -47, 669, 32551, -568, + -36, 512, 32586, -450, -26, 359, 32613, -328, -15, 212, 32630, -200, -5, 69, 32639, -68 + }; + + private static short[] _normalCurveLut2 = new short[] + { + 3195, 26287, 3329, -32, 3064, 26281, 3467, -34, 2936, 26270, 3608, -38, 2811, 26253, 3751, -42, + 2688, 26230, 3897, -46, 2568, 26202, 4046, -50, 2451, 26169, 4199, -54, 2338, 26130, 4354, -58, + 2227, 26085, 4512, -63, 2120, 26035, 4673, -67, 2015, 25980, 4837, -72, 1912, 25919, 5004, -76, + 1813, 25852, 5174, -81, 1716, 25780, 5347, -87, 1622, 25704, 5522, -92, 1531, 25621, 5701, -98, + 1442, 25533, 5882, -103, 1357, 25440, 6066, -109, 1274, 25342, 6253, -115, 1193, 25239, 6442, -121, + 1115, 25131, 6635, -127, 1040, 25018, 6830, -133, 967, 24899, 7027, -140, 897, 24776, 7227, -146, + 829, 24648, 7430, -153, 764, 24516, 7635, -159, 701, 24379, 7842, -166, 641, 24237, 8052, -174, + 583, 24091, 8264, -181, 526, 23940, 8478, -187, 472, 23785, 8695, -194, 420, 23626, 8914, -202, + 371, 23462, 9135, -209, 324, 23295, 9358, -215, 279, 23123, 9583, -222, 236, 22948, 9809, -230, + 194, 22769, 10038, -237, 154, 22586, 10269, -243, 117, 22399, 10501, -250, 81, 22208, 10735, -258, + 47, 22015, 10970, -265, 15, 21818, 11206, -271, -16, 21618, 11444, -277, -44, 21415, 11684, -283, + -71, 21208, 11924, -290, -97, 20999, 12166, -296, -121, 20786, 12409, -302, -143, 20571, 12653, -306, + -163, 20354, 12898, -311, -183, 20134, 13143, -316, -201, 19911, 13389, -321, -218, 19686, 13635, -325, + -234, 19459, 13882, -328, -248, 19230, 14130, -332, -261, 18998, 14377, -335, -273, 18765, 14625, -337, + -284, 18531, 14873, -339, -294, 18295, 15121, -341, -302, 18057, 15369, -341, -310, 17817, 15617, -341, + -317, 17577, 15864, -340, -323, 17335, 16111, -340, -328, 17092, 16357, -338, -332, 16848, 16603, -336, + -336, 16603, 16848, -332, -338, 16357, 17092, -328, -340, 16111, 17335, -323, -340, 15864, 17577, -317, + -341, 15617, 17817, -310, -341, 15369, 18057, -302, -341, 15121, 18295, -294, -339, 14873, 18531, -284, + -337, 14625, 18765, -273, -335, 14377, 18998, -261, -332, 14130, 19230, -248, -328, 13882, 19459, -234, + -325, 13635, 19686, -218, -321, 13389, 19911, -201, -316, 13143, 20134, -183, -311, 12898, 20354, -163, + -306, 12653, 20571, -143, -302, 12409, 20786, -121, -296, 12166, 20999, -97, -290, 11924, 21208, -71, + -283, 11684, 21415, -44, -277, 11444, 21618, -16, -271, 11206, 21818, 15, -265, 10970, 22015, 47, + -258, 10735, 22208, 81, -250, 10501, 22399, 117, -243, 10269, 22586, 154, -237, 10038, 22769, 194, + -230, 9809, 22948, 236, -222, 9583, 23123, 279, -215, 9358, 23295, 324, -209, 9135, 23462, 371, + -202, 8914, 23626, 420, -194, 8695, 23785, 472, -187, 8478, 23940, 526, -181, 8264, 24091, 583, + -174, 8052, 24237, 641, -166, 7842, 24379, 701, -159, 7635, 24516, 764, -153, 7430, 24648, 829, + -146, 7227, 24776, 897, -140, 7027, 24899, 967, -133, 6830, 25018, 1040, -127, 6635, 25131, 1115, + -121, 6442, 25239, 1193, -115, 6253, 25342, 1274, -109, 6066, 25440, 1357, -103, 5882, 25533, 1442, + -98, 5701, 25621, 1531, -92, 5522, 25704, 1622, -87, 5347, 25780, 1716, -81, 5174, 25852, 1813, + -76, 5004, 25919, 1912, -72, 4837, 25980, 2015, -67, 4673, 26035, 2120, -63, 4512, 26085, 2227, + -58, 4354, 26130, 2338, -54, 4199, 26169, 2451, -50, 4046, 26202, 2568, -46, 3897, 26230, 2688, + -42, 3751, 26253, 2811, -38, 3608, 26270, 2936, -34, 3467, 26281, 3064, -32, 3329, 26287, 3195 + }; + #endregion + + #region "High Quality Lookup Tables" + private static short[] _highCurveLut0 = new short[] + { + -582, -23, 8740, 16386, 8833, 8, -590, 0, -573, -54, 8647, 16385, 8925, 40, -598, -1, + -565, -84, 8555, 16383, 9018, 72, -606, -1, -557, -113, 8462, 16379, 9110, 105, -614, -2, + -549, -142, 8370, 16375, 9203, 139, -622, -2, -541, -170, 8277, 16369, 9295, 173, -630, -3, + -533, -198, 8185, 16362, 9387, 208, -638, -4, -525, -225, 8093, 16354, 9480, 244, -646, -5, + -516, -251, 8000, 16344, 9572, 280, -654, -5, -508, -277, 7908, 16334, 9664, 317, -662, -6, + -500, -302, 7816, 16322, 9756, 355, -670, -7, -492, -327, 7724, 16310, 9847, 393, -678, -8, + -484, -351, 7632, 16296, 9939, 432, -686, -9, -476, -374, 7540, 16281, 10030, 471, -694, -10, + -468, -397, 7449, 16265, 10121, 511, -702, -11, -460, -419, 7357, 16247, 10212, 552, -709, -13, + -452, -441, 7266, 16229, 10303, 593, -717, -14, -445, -462, 7175, 16209, 10394, 635, -724, -15, + -437, -483, 7084, 16189, 10484, 678, -732, -16, -429, -503, 6994, 16167, 10574, 722, -739, -18, + -421, -523, 6903, 16144, 10664, 766, -747, -19, -414, -542, 6813, 16120, 10754, 810, -754, -21, + -406, -560, 6723, 16095, 10843, 856, -761, -22, -398, -578, 6633, 16068, 10932, 902, -768, -24, + -391, -596, 6544, 16041, 11021, 949, -775, -26, -383, -612, 6454, 16012, 11109, 996, -782, -27, + -376, -629, 6366, 15983, 11197, 1044, -789, -29, -368, -645, 6277, 15952, 11285, 1093, -796, -31, + -361, -660, 6189, 15920, 11372, 1142, -802, -33, -354, -675, 6100, 15887, 11459, 1192, -809, -35, + -347, -689, 6013, 15853, 11546, 1243, -815, -37, -339, -703, 5925, 15818, 11632, 1294, -821, -39, + -332, -717, 5838, 15782, 11718, 1346, -827, -41, -325, -730, 5751, 15745, 11803, 1399, -833, -43, + -318, -742, 5665, 15707, 11888, 1452, -839, -46, -312, -754, 5579, 15668, 11973, 1506, -845, -48, + -305, -766, 5493, 15627, 12057, 1561, -850, -50, -298, -777, 5408, 15586, 12140, 1616, -855, -53, + -291, -787, 5323, 15544, 12224, 1672, -861, -56, -285, -798, 5239, 15500, 12306, 1729, -866, -58, + -278, -807, 5155, 15456, 12388, 1786, -871, -61, -272, -817, 5071, 15410, 12470, 1844, -875, -64, + -265, -826, 4988, 15364, 12551, 1902, -880, -67, -259, -834, 4905, 15317, 12631, 1962, -884, -70, + -253, -842, 4823, 15268, 12711, 2022, -888, -73, -247, -850, 4741, 15219, 12790, 2082, -892, -76, + -241, -857, 4659, 15168, 12869, 2143, -896, -79, -235, -864, 4578, 15117, 12947, 2205, -899, -82, + -229, -870, 4498, 15065, 13025, 2267, -903, -85, -223, -876, 4417, 15012, 13102, 2331, -906, -89, + -217, -882, 4338, 14958, 13178, 2394, -909, -92, -211, -887, 4259, 14903, 13254, 2459, -911, -96, + -206, -892, 4180, 14847, 13329, 2523, -914, -100, -200, -896, 4102, 14790, 13403, 2589, -916, -103, + -195, -900, 4024, 14732, 13477, 2655, -918, -107, -190, -904, 3947, 14673, 13550, 2722, -919, -111, + -184, -908, 3871, 14614, 13622, 2789, -921, -115, -179, -911, 3795, 14553, 13693, 2857, -922, -119, + -174, -913, 3719, 14492, 13764, 2926, -923, -123, -169, -916, 3644, 14430, 13834, 2995, -923, -127, + -164, -918, 3570, 14367, 13904, 3065, -924, -132, -159, -920, 3496, 14303, 13972, 3136, -924, -136, + -154, -921, 3423, 14239, 14040, 3207, -924, -140, -150, -922, 3350, 14173, 14107, 3278, -923, -145, + -145, -923, 3278, 14107, 14173, 3350, -922, -150, -140, -924, 3207, 14040, 14239, 3423, -921, -154, + -136, -924, 3136, 13972, 14303, 3496, -920, -159, -132, -924, 3065, 13904, 14367, 3570, -918, -164, + -127, -923, 2995, 13834, 14430, 3644, -916, -169, -123, -923, 2926, 13764, 14492, 3719, -913, -174, + -119, -922, 2857, 13693, 14553, 3795, -911, -179, -115, -921, 2789, 13622, 14614, 3871, -908, -184, + -111, -919, 2722, 13550, 14673, 3947, -904, -190, -107, -918, 2655, 13477, 14732, 4024, -900, -195, + -103, -916, 2589, 13403, 14790, 4102, -896, -200, -100, -914, 2523, 13329, 14847, 4180, -892, -206, + -96, -911, 2459, 13254, 14903, 4259, -887, -211, -92, -909, 2394, 13178, 14958, 4338, -882, -217, + -89, -906, 2331, 13102, 15012, 4417, -876, -223, -85, -903, 2267, 13025, 15065, 4498, -870, -229, + -82, -899, 2205, 12947, 15117, 4578, -864, -235, -79, -896, 2143, 12869, 15168, 4659, -857, -241, + -76, -892, 2082, 12790, 15219, 4741, -850, -247, -73, -888, 2022, 12711, 15268, 4823, -842, -253, + -70, -884, 1962, 12631, 15317, 4905, -834, -259, -67, -880, 1902, 12551, 15364, 4988, -826, -265, + -64, -875, 1844, 12470, 15410, 5071, -817, -272, -61, -871, 1786, 12388, 15456, 5155, -807, -278, + -58, -866, 1729, 12306, 15500, 5239, -798, -285, -56, -861, 1672, 12224, 15544, 5323, -787, -291, + -53, -855, 1616, 12140, 15586, 5408, -777, -298, -50, -850, 1561, 12057, 15627, 5493, -766, -305, + -48, -845, 1506, 11973, 15668, 5579, -754, -312, -46, -839, 1452, 11888, 15707, 5665, -742, -318, + -43, -833, 1399, 11803, 15745, 5751, -730, -325, -41, -827, 1346, 11718, 15782, 5838, -717, -332, + -39, -821, 1294, 11632, 15818, 5925, -703, -339, -37, -815, 1243, 11546, 15853, 6013, -689, -347, + -35, -809, 1192, 11459, 15887, 6100, -675, -354, -33, -802, 1142, 11372, 15920, 6189, -660, -361, + -31, -796, 1093, 11285, 15952, 6277, -645, -368, -29, -789, 1044, 11197, 15983, 6366, -629, -376, + -27, -782, 996, 11109, 16012, 6454, -612, -383, -26, -775, 949, 11021, 16041, 6544, -596, -391, + -24, -768, 902, 10932, 16068, 6633, -578, -398, -22, -761, 856, 10843, 16095, 6723, -560, -406, + -21, -754, 810, 10754, 16120, 6813, -542, -414, -19, -747, 766, 10664, 16144, 6903, -523, -421, + -18, -739, 722, 10574, 16167, 6994, -503, -429, -16, -732, 678, 10484, 16189, 7084, -483, -437, + -15, -724, 635, 10394, 16209, 7175, -462, -445, -14, -717, 593, 10303, 16229, 7266, -441, -452, + -13, -709, 552, 10212, 16247, 7357, -419, -460, -11, -702, 511, 10121, 16265, 7449, -397, -468, + -10, -694, 471, 10030, 16281, 7540, -374, -476, -9, -686, 432, 9939, 16296, 7632, -351, -484, + -8, -678, 393, 9847, 16310, 7724, -327, -492, -7, -670, 355, 9756, 16322, 7816, -302, -500, + -6, -662, 317, 9664, 16334, 7908, -277, -508, -5, -654, 280, 9572, 16344, 8000, -251, -516, + -5, -646, 244, 9480, 16354, 8093, -225, -525, -4, -638, 208, 9387, 16362, 8185, -198, -533, + -3, -630, 173, 9295, 16369, 8277, -170, -541, -2, -622, 139, 9203, 16375, 8370, -142, -549, + -2, -614, 105, 9110, 16379, 8462, -113, -557, -1, -606, 72, 9018, 16383, 8555, -84, -565, + -1, -598, 40, 8925, 16385, 8647, -54, -573, 0, -590, 8, 8833, 16386, 8740, -23, -582, + }; + + private static short[] _highCurveLut1 = new short[] + { + -12, 47, -134, 32767, 81, -16, 2, 0, -26, 108, -345, 32760, 301, -79, 17, -1, + -40, 168, -552, 32745, 526, -144, 32, -2, -53, 226, -753, 32723, 755, -210, 47, -3, + -66, 284, -950, 32694, 989, -277, 63, -5, -78, 340, -1143, 32658, 1226, -346, 79, -6, + -90, 394, -1331, 32615, 1469, -415, 96, -8, -101, 447, -1514, 32564, 1715, -486, 113, -9, + -112, 499, -1692, 32506, 1966, -557, 130, -11, -123, 550, -1865, 32441, 2221, -630, 148, -13, + -133, 599, -2034, 32369, 2480, -703, 166, -14, -143, 646, -2198, 32290, 2743, -778, 185, -16, + -152, 693, -2357, 32204, 3010, -853, 204, -18, -162, 738, -2512, 32110, 3281, -929, 223, -20, + -170, 781, -2662, 32010, 3555, -1007, 242, -23, -178, 823, -2807, 31903, 3834, -1084, 262, -25, + -186, 864, -2947, 31789, 4116, -1163, 282, -27, -194, 903, -3082, 31668, 4403, -1242, 303, -30, + -201, 940, -3213, 31540, 4692, -1322, 323, -32, -207, 977, -3339, 31406, 4985, -1403, 344, -35, + -214, 1011, -3460, 31265, 5282, -1484, 365, -37, -220, 1045, -3577, 31117, 5582, -1566, 387, -40, + -225, 1077, -3688, 30963, 5885, -1648, 409, -43, -230, 1107, -3796, 30802, 6191, -1730, 431, -46, + -235, 1136, -3898, 30635, 6501, -1813, 453, -49, -240, 1164, -3996, 30462, 6813, -1896, 475, -52, + -244, 1190, -4089, 30282, 7128, -1980, 498, -55, -247, 1215, -4178, 30097, 7446, -2064, 520, -58, + -251, 1239, -4262, 29905, 7767, -2148, 543, -62, -254, 1261, -4342, 29707, 8091, -2231, 566, -65, + -257, 1281, -4417, 29503, 8416, -2315, 589, -69, -259, 1301, -4488, 29293, 8745, -2399, 613, -72, + -261, 1319, -4555, 29078, 9075, -2483, 636, -76, -263, 1336, -4617, 28857, 9408, -2567, 659, -80, + -265, 1351, -4674, 28631, 9743, -2651, 683, -83, -266, 1365, -4728, 28399, 10080, -2734, 706, -87, + -267, 1378, -4777, 28161, 10418, -2817, 730, -91, -267, 1389, -4822, 27919, 10759, -2899, 753, -95, + -268, 1400, -4863, 27671, 11100, -2981, 777, -99, -268, 1409, -4900, 27418, 11444, -3063, 800, -103, + -268, 1416, -4933, 27161, 11789, -3144, 824, -107, -267, 1423, -4962, 26898, 12135, -3224, 847, -112, + -267, 1428, -4987, 26631, 12482, -3303, 870, -116, -266, 1433, -5008, 26359, 12830, -3382, 893, -120, + -265, 1436, -5026, 26083, 13179, -3460, 916, -125, -264, 1438, -5039, 25802, 13529, -3537, 939, -129, + -262, 1438, -5049, 25517, 13880, -3613, 962, -133, -260, 1438, -5055, 25228, 14231, -3687, 984, -138, + -258, 1437, -5058, 24935, 14582, -3761, 1006, -142, -256, 1435, -5058, 24639, 14934, -3833, 1028, -147, + -254, 1431, -5053, 24338, 15286, -3904, 1049, -151, -252, 1427, -5046, 24034, 15638, -3974, 1071, -155, + -249, 1422, -5035, 23726, 15989, -4042, 1091, -160, -246, 1416, -5021, 23415, 16341, -4109, 1112, -164, + -243, 1408, -5004, 23101, 16691, -4174, 1132, -169, -240, 1400, -4984, 22783, 17042, -4237, 1152, -173, + -237, 1392, -4960, 22463, 17392, -4299, 1171, -178, -234, 1382, -4934, 22140, 17740, -4358, 1190, -182, + -230, 1371, -4905, 21814, 18088, -4416, 1209, -186, -227, 1360, -4873, 21485, 18435, -4472, 1226, -191, + -223, 1348, -4839, 21154, 18781, -4526, 1244, -195, -219, 1335, -4801, 20821, 19125, -4578, 1260, -199, + -215, 1321, -4761, 20486, 19468, -4627, 1277, -203, -211, 1307, -4719, 20148, 19809, -4674, 1292, -207, + -207, 1292, -4674, 19809, 20148, -4719, 1307, -211, -203, 1277, -4627, 19468, 20486, -4761, 1321, -215, + -199, 1260, -4578, 19125, 20821, -4801, 1335, -219, -195, 1244, -4526, 18781, 21154, -4839, 1348, -223, + -191, 1226, -4472, 18435, 21485, -4873, 1360, -227, -186, 1209, -4416, 18088, 21814, -4905, 1371, -230, + -182, 1190, -4358, 17740, 22140, -4934, 1382, -234, -178, 1171, -4299, 17392, 22463, -4960, 1392, -237, + -173, 1152, -4237, 17042, 22783, -4984, 1400, -240, -169, 1132, -4174, 16691, 23101, -5004, 1408, -243, + -164, 1112, -4109, 16341, 23415, -5021, 1416, -246, -160, 1091, -4042, 15989, 23726, -5035, 1422, -249, + -155, 1071, -3974, 15638, 24034, -5046, 1427, -252, -151, 1049, -3904, 15286, 24338, -5053, 1431, -254, + -147, 1028, -3833, 14934, 24639, -5058, 1435, -256, -142, 1006, -3761, 14582, 24935, -5058, 1437, -258, + -138, 984, -3687, 14231, 25228, -5055, 1438, -260, -133, 962, -3613, 13880, 25517, -5049, 1438, -262, + -129, 939, -3537, 13529, 25802, -5039, 1438, -264, -125, 916, -3460, 13179, 26083, -5026, 1436, -265, + -120, 893, -3382, 12830, 26359, -5008, 1433, -266, -116, 870, -3303, 12482, 26631, -4987, 1428, -267, + -112, 847, -3224, 12135, 26898, -4962, 1423, -267, -107, 824, -3144, 11789, 27161, -4933, 1416, -268, + -103, 800, -3063, 11444, 27418, -4900, 1409, -268, -99, 777, -2981, 11100, 27671, -4863, 1400, -268, + -95, 753, -2899, 10759, 27919, -4822, 1389, -267, -91, 730, -2817, 10418, 28161, -4777, 1378, -267, + -87, 706, -2734, 10080, 28399, -4728, 1365, -266, -83, 683, -2651, 9743, 28631, -4674, 1351, -265, + -80, 659, -2567, 9408, 28857, -4617, 1336, -263, -76, 636, -2483, 9075, 29078, -4555, 1319, -261, + -72, 613, -2399, 8745, 29293, -4488, 1301, -259, -69, 589, -2315, 8416, 29503, -4417, 1281, -257, + -65, 566, -2231, 8091, 29707, -4342, 1261, -254, -62, 543, -2148, 7767, 29905, -4262, 1239, -251, + -58, 520, -2064, 7446, 30097, -4178, 1215, -247, -55, 498, -1980, 7128, 30282, -4089, 1190, -244, + -52, 475, -1896, 6813, 30462, -3996, 1164, -240, -49, 453, -1813, 6501, 30635, -3898, 1136, -235, + -46, 431, -1730, 6191, 30802, -3796, 1107, -230, -43, 409, -1648, 5885, 30963, -3688, 1077, -225, + -40, 387, -1566, 5582, 31117, -3577, 1045, -220, -37, 365, -1484, 5282, 31265, -3460, 1011, -214, + -35, 344, -1403, 4985, 31406, -3339, 977, -207, -32, 323, -1322, 4692, 31540, -3213, 940, -201, + -30, 303, -1242, 4403, 31668, -3082, 903, -194, -27, 282, -1163, 4116, 31789, -2947, 864, -186, + -25, 262, -1084, 3834, 31903, -2807, 823, -178, -23, 242, -1007, 3555, 32010, -2662, 781, -170, + -20, 223, -929, 3281, 32110, -2512, 738, -162, -18, 204, -853, 3010, 32204, -2357, 693, -152, + -16, 185, -778, 2743, 32290, -2198, 646, -143, -14, 166, -703, 2480, 32369, -2034, 599, -133, + -13, 148, -630, 2221, 32441, -1865, 550, -123, -11, 130, -557, 1966, 32506, -1692, 499, -112, + -9, 113, -486, 1715, 32564, -1514, 447, -101, -8, 96, -415, 1469, 32615, -1331, 394, -90, + -6, 79, -346, 1226, 32658, -1143, 340, -78, -5, 63, -277, 989, 32694, -950, 284, -66, + -3, 47, -210, 755, 32723, -753, 226, -53, -2, 32, -144, 526, 32745, -552, 168, -40, + -1, 17, -79, 301, 32760, -345, 108, -26, 0, 2, -16, 81, 32767, -134, 47, -12, + }; + + private static short[] _highCurveLut2 = new short[] + { + 418, -2538, 6118, 24615, 6298, -2563, 417, 0, 420, -2512, 5939, 24611, 6479, -2588, 415, 1, + 421, -2485, 5761, 24605, 6662, -2612, 412, 2, 422, -2458, 5585, 24595, 6846, -2635, 409, 3, + 423, -2430, 5410, 24582, 7030, -2658, 406, 4, 423, -2402, 5236, 24565, 7216, -2680, 403, 5, + 423, -2373, 5064, 24546, 7403, -2701, 399, 6, 423, -2343, 4893, 24523, 7591, -2721, 395, 7, + 423, -2313, 4724, 24496, 7780, -2741, 391, 8, 422, -2283, 4556, 24467, 7970, -2759, 386, 9, + 421, -2252, 4390, 24434, 8161, -2777, 381, 11, 420, -2221, 4225, 24398, 8353, -2794, 376, 12, + 419, -2190, 4062, 24359, 8545, -2810, 370, 14, 418, -2158, 3900, 24316, 8739, -2825, 364, 15, + 416, -2126, 3740, 24271, 8933, -2839, 358, 17, 414, -2093, 3582, 24222, 9127, -2851, 351, 19, + 412, -2060, 3425, 24170, 9323, -2863, 344, 21, 410, -2027, 3270, 24115, 9519, -2874, 336, 22, + 407, -1993, 3117, 24056, 9715, -2884, 328, 24, 404, -1960, 2966, 23995, 9912, -2893, 319, 26, + 402, -1926, 2816, 23930, 10110, -2900, 311, 29, 398, -1892, 2668, 23863, 10308, -2907, 301, 31, + 395, -1858, 2522, 23792, 10506, -2912, 292, 33, 392, -1823, 2378, 23718, 10705, -2916, 282, 35, + 389, -1789, 2235, 23641, 10904, -2919, 271, 38, 385, -1754, 2095, 23561, 11103, -2920, 261, 40, + 381, -1719, 1956, 23478, 11303, -2921, 249, 43, 377, -1684, 1819, 23393, 11502, -2920, 238, 45, + 373, -1649, 1684, 23304, 11702, -2917, 225, 48, 369, -1615, 1551, 23212, 11902, -2914, 213, 51, + 365, -1580, 1420, 23118, 12102, -2909, 200, 54, 361, -1545, 1291, 23020, 12302, -2902, 186, 57, + 356, -1510, 1163, 22920, 12502, -2895, 173, 60, 352, -1475, 1038, 22817, 12702, -2885, 158, 63, + 347, -1440, 915, 22711, 12901, -2875, 143, 66, 342, -1405, 793, 22602, 13101, -2863, 128, 69, + 338, -1370, 674, 22491, 13300, -2849, 113, 73, 333, -1335, 557, 22377, 13499, -2834, 97, 76, + 328, -1301, 441, 22260, 13698, -2817, 80, 80, 323, -1266, 328, 22141, 13896, -2799, 63, 83, + 318, -1232, 217, 22019, 14094, -2779, 46, 87, 313, -1197, 107, 21894, 14291, -2758, 28, 91, + 307, -1163, 0, 21767, 14488, -2735, 9, 95, 302, -1129, -105, 21637, 14684, -2710, -9, 98, + 297, -1096, -208, 21506, 14879, -2684, -29, 102, 292, -1062, -310, 21371, 15074, -2656, -48, 106, + 286, -1029, -409, 21234, 15268, -2626, -69, 111, 281, -996, -506, 21095, 15461, -2595, -89, 115, + 276, -963, -601, 20954, 15654, -2562, -110, 119, 270, -930, -694, 20810, 15846, -2527, -132, 123, + 265, -898, -785, 20664, 16036, -2490, -154, 128, 260, -866, -874, 20516, 16226, -2452, -176, 132, + 254, -834, -961, 20366, 16415, -2411, -199, 137, 249, -803, -1046, 20213, 16602, -2369, -222, 141, + 243, -771, -1129, 20059, 16789, -2326, -246, 146, 238, -740, -1209, 19902, 16974, -2280, -270, 151, + 233, -710, -1288, 19744, 17158, -2232, -294, 156, 227, -680, -1365, 19583, 17341, -2183, -319, 160, + 222, -650, -1440, 19421, 17523, -2132, -345, 165, 217, -620, -1513, 19257, 17703, -2079, -370, 170, + 211, -591, -1583, 19091, 17882, -2023, -396, 175, 206, -562, -1652, 18923, 18059, -1966, -423, 180, + 201, -533, -1719, 18754, 18235, -1907, -450, 185, 196, -505, -1784, 18582, 18410, -1847, -477, 191, + 191, -477, -1847, 18410, 18582, -1784, -505, 196, 185, -450, -1907, 18235, 18754, -1719, -533, 201, + 180, -423, -1966, 18059, 18923, -1652, -562, 206, 175, -396, -2023, 17882, 19091, -1583, -591, 211, + 170, -370, -2079, 17703, 19257, -1513, -620, 217, 165, -345, -2132, 17523, 19421, -1440, -650, 222, + 160, -319, -2183, 17341, 19583, -1365, -680, 227, 156, -294, -2232, 17158, 19744, -1288, -710, 233, + 151, -270, -2280, 16974, 19902, -1209, -740, 238, 146, -246, -2326, 16789, 20059, -1129, -771, 243, + 141, -222, -2369, 16602, 20213, -1046, -803, 249, 137, -199, -2411, 16415, 20366, -961, -834, 254, + 132, -176, -2452, 16226, 20516, -874, -866, 260, 128, -154, -2490, 16036, 20664, -785, -898, 265, + 123, -132, -2527, 15846, 20810, -694, -930, 270, 119, -110, -2562, 15654, 20954, -601, -963, 276, + 115, -89, -2595, 15461, 21095, -506, -996, 281, 111, -69, -2626, 15268, 21234, -409, -1029, 286, + 106, -48, -2656, 15074, 21371, -310, -1062, 292, 102, -29, -2684, 14879, 21506, -208, -1096, 297, + 98, -9, -2710, 14684, 21637, -105, -1129, 302, 95, 9, -2735, 14488, 21767, 0, -1163, 307, + 91, 28, -2758, 14291, 21894, 107, -1197, 313, 87, 46, -2779, 14094, 22019, 217, -1232, 318, + 83, 63, -2799, 13896, 22141, 328, -1266, 323, 80, 80, -2817, 13698, 22260, 441, -1301, 328, + 76, 97, -2834, 13499, 22377, 557, -1335, 333, 73, 113, -2849, 13300, 22491, 674, -1370, 338, + 69, 128, -2863, 13101, 22602, 793, -1405, 342, 66, 143, -2875, 12901, 22711, 915, -1440, 347, + 63, 158, -2885, 12702, 22817, 1038, -1475, 352, 60, 173, -2895, 12502, 22920, 1163, -1510, 356, + 57, 186, -2902, 12302, 23020, 1291, -1545, 361, 54, 200, -2909, 12102, 23118, 1420, -1580, 365, + 51, 213, -2914, 11902, 23212, 1551, -1615, 369, 48, 225, -2917, 11702, 23304, 1684, -1649, 373, + 45, 238, -2920, 11502, 23393, 1819, -1684, 377, 43, 249, -2921, 11303, 23478, 1956, -1719, 381, + 40, 261, -2920, 11103, 23561, 2095, -1754, 385, 38, 271, -2919, 10904, 23641, 2235, -1789, 389, + 35, 282, -2916, 10705, 23718, 2378, -1823, 392, 33, 292, -2912, 10506, 23792, 2522, -1858, 395, + 31, 301, -2907, 10308, 23863, 2668, -1892, 398, 29, 311, -2900, 10110, 23930, 2816, -1926, 402, + 26, 319, -2893, 9912, 23995, 2966, -1960, 404, 24, 328, -2884, 9715, 24056, 3117, -1993, 407, + 22, 336, -2874, 9519, 24115, 3270, -2027, 410, 21, 344, -2863, 9323, 24170, 3425, -2060, 412, + 19, 351, -2851, 9127, 24222, 3582, -2093, 414, 17, 358, -2839, 8933, 24271, 3740, -2126, 416, + 15, 364, -2825, 8739, 24316, 3900, -2158, 418, 14, 370, -2810, 8545, 24359, 4062, -2190, 419, + 12, 376, -2794, 8353, 24398, 4225, -2221, 420, 11, 381, -2777, 8161, 24434, 4390, -2252, 421, + 9, 386, -2759, 7970, 24467, 4556, -2283, 422, 8, 391, -2741, 7780, 24496, 4724, -2313, 423, + 7, 395, -2721, 7591, 24523, 4893, -2343, 423, 6, 399, -2701, 7403, 24546, 5064, -2373, 423, + 5, 403, -2680, 7216, 24565, 5236, -2402, 423, 4, 406, -2658, 7030, 24582, 5410, -2430, 423, + 3, 409, -2635, 6846, 24595, 5585, -2458, 422, 2, 412, -2612, 6662, 24605, 5761, -2485, 421, + 1, 415, -2588, 6479, 24611, 5939, -2512, 420, 0, 417, -2563, 6298, 24615, 6118, -2538, 418, + }; + #endregion + + private static float[] _normalCurveLut0F; + private static float[] _normalCurveLut1F; + private static float[] _normalCurveLut2F; + + private static float[] _highCurveLut0F; + private static float[] _highCurveLut1F; + private static float[] _highCurveLut2F; + + static ResamplerHelper() + { + _normalCurveLut0F = _normalCurveLut0.Select(x => x / 32768f).ToArray(); + _normalCurveLut1F = _normalCurveLut1.Select(x => x / 32768f).ToArray(); + _normalCurveLut2F = _normalCurveLut2.Select(x => x / 32768f).ToArray(); + + _highCurveLut0F = _highCurveLut0.Select(x => x / 32768f).ToArray(); + _highCurveLut1F = _highCurveLut1.Select(x => x / 32768f).ToArray(); + _highCurveLut2F = _highCurveLut2.Select(x => x / 32768f).ToArray(); + } + + private const int FixedPointPrecision = 15; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Resample(Span<float> outputBuffer, ReadOnlySpan<short> inputBuffer, float ratio, ref float fraction, int sampleCount, SampleRateConversionQuality srcQuality, bool needPitch) + { + switch (srcQuality) + { + case SampleRateConversionQuality.Default: + ResampleDefaultQuality(outputBuffer, inputBuffer, ratio, ref fraction, sampleCount, needPitch); + break; + case SampleRateConversionQuality.Low: + ResampleLowQuality(outputBuffer, inputBuffer, ratio, ref fraction, sampleCount); + break; + case SampleRateConversionQuality.High: + ResampleHighQuality(outputBuffer, inputBuffer, ratio, ref fraction, sampleCount); + break; + default: + throw new NotImplementedException($"{srcQuality}"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan<float> GetDefaultParameter(float ratio) + { + if (ratio <= 1.0f) + { + return _normalCurveLut1F; + } + else if (ratio > 1.333313f) + { + return _normalCurveLut0F; + } + + return _normalCurveLut2F; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe static void ResampleDefaultQuality(Span<float> outputBuffer, ReadOnlySpan<short> inputBuffer, float ratio, ref float fraction, int sampleCount, bool needPitch) + { + ReadOnlySpan<float> parameters = GetDefaultParameter(ratio); + + int inputBufferIndex = 0, i = 0; + + // TODO: REV8 fast path (when needPitch == false the input index progression is constant + we need SIMD) + + if (Sse41.IsSupported) + { + if (ratio == 1f) + { + fixed (short* pInput = inputBuffer) + { + fixed (float* pOutput = outputBuffer, pParameters = parameters) + { + Vector128<float> parameter = Sse.LoadVector128(pParameters); + + for (; i < (sampleCount & ~3); i += 4) + { + Vector128<int> intInput0 = Sse41.ConvertToVector128Int32(pInput + (uint)i); + Vector128<int> intInput1 = Sse41.ConvertToVector128Int32(pInput + (uint)i + 1); + Vector128<int> intInput2 = Sse41.ConvertToVector128Int32(pInput + (uint)i + 2); + Vector128<int> intInput3 = Sse41.ConvertToVector128Int32(pInput + (uint)i + 3); + + Vector128<float> input0 = Sse2.ConvertToVector128Single(intInput0); + Vector128<float> input1 = Sse2.ConvertToVector128Single(intInput1); + Vector128<float> input2 = Sse2.ConvertToVector128Single(intInput2); + Vector128<float> input3 = Sse2.ConvertToVector128Single(intInput3); + + Vector128<float> mix0 = Sse.Multiply(input0, parameter); + Vector128<float> mix1 = Sse.Multiply(input1, parameter); + Vector128<float> mix2 = Sse.Multiply(input2, parameter); + Vector128<float> mix3 = Sse.Multiply(input3, parameter); + + Vector128<float> mix01 = Sse3.HorizontalAdd(mix0, mix1); + Vector128<float> mix23 = Sse3.HorizontalAdd(mix2, mix3); + + Vector128<float> mix0123 = Sse3.HorizontalAdd(mix01, mix23); + + Sse.Store(pOutput + (uint)i, Sse41.RoundToNearestInteger(mix0123)); + } + } + } + + inputBufferIndex = i; + } + else + { + fixed (short* pInput = inputBuffer) + { + fixed (float* pOutput = outputBuffer, pParameters = parameters) + { + for (; i < (sampleCount & ~3); i += 4) + { + uint baseIndex0 = (uint)(fraction * 128) * 4; + uint inputIndex0 = (uint)inputBufferIndex; + + fraction += ratio; + + uint baseIndex1 = ((uint)(fraction * 128) & 127) * 4; + uint inputIndex1 = (uint)inputBufferIndex + (uint)fraction; + + fraction += ratio; + + uint baseIndex2 = ((uint)(fraction * 128) & 127) * 4; + uint inputIndex2 = (uint)inputBufferIndex + (uint)fraction; + + fraction += ratio; + + uint baseIndex3 = ((uint)(fraction * 128) & 127) * 4; + uint inputIndex3 = (uint)inputBufferIndex + (uint)fraction; + + fraction += ratio; + inputBufferIndex += (int)fraction; + + // Only keep lower part (safe as fraction isn't supposed to be negative) + fraction -= (int)fraction; + + Vector128<float> parameter0 = Sse.LoadVector128(pParameters + baseIndex0); + Vector128<float> parameter1 = Sse.LoadVector128(pParameters + baseIndex1); + Vector128<float> parameter2 = Sse.LoadVector128(pParameters + baseIndex2); + Vector128<float> parameter3 = Sse.LoadVector128(pParameters + baseIndex3); + + Vector128<int> intInput0 = Sse41.ConvertToVector128Int32(pInput + inputIndex0); + Vector128<int> intInput1 = Sse41.ConvertToVector128Int32(pInput + inputIndex1); + Vector128<int> intInput2 = Sse41.ConvertToVector128Int32(pInput + inputIndex2); + Vector128<int> intInput3 = Sse41.ConvertToVector128Int32(pInput + inputIndex3); + + Vector128<float> input0 = Sse2.ConvertToVector128Single(intInput0); + Vector128<float> input1 = Sse2.ConvertToVector128Single(intInput1); + Vector128<float> input2 = Sse2.ConvertToVector128Single(intInput2); + Vector128<float> input3 = Sse2.ConvertToVector128Single(intInput3); + + Vector128<float> mix0 = Sse.Multiply(input0, parameter0); + Vector128<float> mix1 = Sse.Multiply(input1, parameter1); + Vector128<float> mix2 = Sse.Multiply(input2, parameter2); + Vector128<float> mix3 = Sse.Multiply(input3, parameter3); + + Vector128<float> mix01 = Sse3.HorizontalAdd(mix0, mix1); + Vector128<float> mix23 = Sse3.HorizontalAdd(mix2, mix3); + + Vector128<float> mix0123 = Sse3.HorizontalAdd(mix01, mix23); + + Sse.Store(pOutput + (uint)i, Sse41.RoundToNearestInteger(mix0123)); + } + } + } + } + } + + for (; i < sampleCount; i++) + { + int baseIndex = (int)(fraction * 128) * 4; + ReadOnlySpan<float> parameter = parameters.Slice(baseIndex, 4); + ReadOnlySpan<short> currentInput = inputBuffer.Slice(inputBufferIndex, 4); + + outputBuffer[i] = (float)Math.Round(currentInput[0] * parameter[0] + + currentInput[1] * parameter[1] + + currentInput[2] * parameter[2] + + currentInput[3] * parameter[3]); + + fraction += ratio; + inputBufferIndex += (int)fraction; + + // Only keep lower part (safe as fraction isn't supposed to be negative) + fraction -= (int)fraction; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ReadOnlySpan<float> GetHighParameter(float ratio) + { + if (ratio <= 1.0f) + { + return _highCurveLut1F; + } + else if (ratio > 1.333313f) + { + return _highCurveLut0F; + } + + return _highCurveLut2F; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ResampleHighQuality(Span<float> outputBuffer, ReadOnlySpan<short> inputBuffer, float ratio, ref float fraction, int sampleCount) + { + ReadOnlySpan<float> parameters = GetHighParameter(ratio); + + int inputBufferIndex = 0; + + // TODO: fast path + + for (int i = 0; i < sampleCount; i++) + { + int baseIndex = (int)(fraction * 128) * 8; + ReadOnlySpan<float> parameter = parameters.Slice(baseIndex, 8); + ReadOnlySpan<short> currentInput = inputBuffer.Slice(inputBufferIndex, 8); + + outputBuffer[i] = (float)Math.Round(currentInput[0] * parameter[0] + + currentInput[1] * parameter[1] + + currentInput[2] * parameter[2] + + currentInput[3] * parameter[3] + + currentInput[4] * parameter[4] + + currentInput[5] * parameter[5] + + currentInput[6] * parameter[6] + + currentInput[7] * parameter[7]); + + fraction += ratio; + inputBufferIndex += (int)MathF.Truncate(fraction); + + fraction -= (int)fraction; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResampleLowQuality(Span<float> outputBuffer, ReadOnlySpan<short> inputBuffer, float ratio, ref float fraction, int sampleCount) + { + int inputBufferIndex = 0; + + for (int i = 0; i < sampleCount; i++) + { + int outputData = inputBuffer[inputBufferIndex]; + + if (fraction > 1.0f) + { + outputData = inputBuffer[inputBufferIndex + 1]; + } + + outputBuffer[i] = outputData; + + fraction += ratio; + inputBufferIndex += (int)MathF.Truncate(fraction); + + fraction -= (int)fraction; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResampleForUpsampler(Span<float> outputBuffer, ReadOnlySpan<float> inputBuffer, float ratio, ref float fraction, int sampleCount) + { + // TODO: use a bandwidth filter to have better resampling. + int inputBufferIndex = 0; + + for (int i = 0; i < sampleCount; i++) + { + float outputData = inputBuffer[inputBufferIndex]; + + if (fraction > 1.0f) + { + outputData = inputBuffer[inputBufferIndex + 1]; + } + + outputBuffer[i] = outputData; + + fraction += ratio; + inputBufferIndex += (int)MathF.Truncate(fraction); + + fraction -= (int)fraction; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/AdpcmLoopContext.cs b/Ryujinx.Audio/Renderer/Dsp/State/AdpcmLoopContext.cs new file mode 100644 index 00000000..695b32d2 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/AdpcmLoopContext.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 6)] + public struct AdpcmLoopContext + { + public short PredScale; + public short History0; + public short History1; + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/AuxiliaryBufferHeader.cs b/Ryujinx.Audio/Renderer/Dsp/State/AuxiliaryBufferHeader.cs new file mode 100644 index 00000000..3cf24302 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/AuxiliaryBufferHeader.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x40)] + public struct AuxiliaryBufferHeader + { + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)] + public struct AuxiliaryBufferInfo + { + private const uint ReadOffsetPosition = 0x0; + private const uint WriteOffsetPosition = 0x4; + + public uint ReadOffset; + public uint WriteOffset; + private uint _reserved; + + public static uint GetReadOffset(IVirtualMemoryManager manager, ulong bufferAddress) + { + return manager.Read<uint>(bufferAddress + ReadOffsetPosition); + } + + public static uint GetWriteOffset(IVirtualMemoryManager manager, ulong bufferAddress) + { + return manager.Read<uint>(bufferAddress + WriteOffsetPosition); + } + + public static void SetReadOffset(IVirtualMemoryManager manager, ulong bufferAddress, uint value) + { + manager.Write(bufferAddress + ReadOffsetPosition, value); + } + + public static void SetWriteOffset(IVirtualMemoryManager manager, ulong bufferAddress, uint value) + { + manager.Write(bufferAddress + WriteOffsetPosition, value); + } + } + + public AuxiliaryBufferInfo BufferInfo; + public unsafe fixed uint Unknown[13]; + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs b/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs new file mode 100644 index 00000000..9677333d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + public struct BiquadFilterState + { + public float Z1; + public float Z2; + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs b/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs new file mode 100644 index 00000000..7b694fb0 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/DelayState.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Effect; +using Ryujinx.Audio.Renderer.Parameter.Effect; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + public class DelayState + { + public DelayLine[] DelayLines { get; } + public float[] LowPassZ { get; set; } + public float FeedbackGain { get; private set; } + public float DelayFeedbackBaseGain { get; private set; } + public float DelayFeedbackCrossGain { get; private set; } + public float LowPassFeedbackGain { get; private set; } + public float LowPassBaseGain { get; private set; } + + private const int FixedPointPrecision = 14; + + public DelayState(ref DelayParameter parameter, ulong workBuffer) + { + DelayLines = new DelayLine[parameter.ChannelCount]; + LowPassZ = new float[parameter.ChannelCount]; + + uint sampleRate = (uint)FixedPointHelper.ToInt(parameter.SampleRate, FixedPointPrecision) / 1000; + + for (int i = 0; i < DelayLines.Length; i++) + { + DelayLines[i] = new DelayLine(sampleRate, parameter.DelayTimeMax); + DelayLines[i].SetDelay(parameter.DelayTime); + LowPassZ[0] = 0; + } + + UpdateParameter(ref parameter); + } + + public void UpdateParameter(ref DelayParameter parameter) + { + FeedbackGain = FixedPointHelper.ToFloat(parameter.FeedbackGain, FixedPointPrecision) * 0.98f; + + float channelSpread = FixedPointHelper.ToFloat(parameter.ChannelSpread, FixedPointPrecision); + + DelayFeedbackBaseGain = (1.0f - channelSpread) * FeedbackGain; + + if (parameter.ChannelCount == 4 || parameter.ChannelCount == 6) + { + DelayFeedbackCrossGain = channelSpread * 0.5f * FeedbackGain; + } + else + { + DelayFeedbackCrossGain = channelSpread * FeedbackGain; + } + + LowPassFeedbackGain = 0.95f * FixedPointHelper.ToFloat(parameter.LowPassAmount, FixedPointPrecision); + LowPassBaseGain = 1.0f - LowPassFeedbackGain; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs b/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs new file mode 100644 index 00000000..be0f9734 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/Reverb3dState.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Effect; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + public class Reverb3dState + { + private readonly float[] FdnDelayMinTimes = new float[4] { 5.0f, 6.0f, 13.0f, 14.0f }; + private readonly float[] FdnDelayMaxTimes = new float[4] { 45.704f, 82.782f, 149.94f, 271.58f }; + private readonly float[] DecayDelayMaxTimes1 = new float[4] { 17.0f, 13.0f, 9.0f, 7.0f }; + private readonly float[] DecayDelayMaxTimes2 = new float[4] { 19.0f, 11.0f, 10.0f, 6.0f }; + private readonly float[] EarlyDelayTimes = new float[20] { 0.017136f, 0.059154f, 0.16173f, 0.39019f, 0.42526f, 0.45541f, 0.68974f, 0.74591f, 0.83384f, 0.8595f, 0.0f, 0.075024f, 0.16879f, 0.2999f, 0.33744f, 0.3719f, 0.59901f, 0.71674f, 0.81786f, 0.85166f }; + public readonly float[] EarlyGain = new float[20] { 0.67096f, 0.61027f, 1.0f, 0.35680f, 0.68361f, 0.65978f, 0.51939f, 0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.38270f, 0.72867f, 0.69794f, 0.5464f, 0.24563f, 0.45214f, 0.44042f }; + + public IDelayLine[] FdnDelayLines { get; } + public DecayDelay[] DecayDelays1 { get; } + public DecayDelay[] DecayDelays2 { get; } + public IDelayLine PreDelayLine { get; } + public IDelayLine BackLeftDelayLine { get; } + public float DryGain { get; private set; } + public uint[] EarlyDelayTime { get; private set; } + public float PreviousPreDelayValue { get; set; } + public float PreviousPreDelayGain { get; private set; } + public float TargetPreDelayGain { get; private set; } + public float EarlyReflectionsGain { get; private set; } + public float LateReverbGain { get; private set; } + public uint ReflectionDelayTime { get; private set; } + public float EchoLateReverbDecay { get; private set; } + public float[] DecayDirectFdnGain { get; private set; } + public float[] DecayCurrentFdnGain { get; private set; } + public float[] DecayCurrentOutputGain { get; private set; } + public float[] PreviousFeedbackOutputDecayed { get; private set; } + + public Reverb3dState(ref Reverb3dParameter parameter, ulong workBuffer) + { + FdnDelayLines = new IDelayLine[4]; + DecayDelays1 = new DecayDelay[4]; + DecayDelays2 = new DecayDelay[4]; + DecayDirectFdnGain = new float[4]; + DecayCurrentFdnGain = new float[4]; + DecayCurrentOutputGain = new float[4]; + PreviousFeedbackOutputDecayed = new float[4]; + + uint sampleRate = parameter.SampleRate / 1000; + + for (int i = 0; i < 4; i++) + { + FdnDelayLines[i] = new DelayLine3d(sampleRate, FdnDelayMaxTimes[i]); + DecayDelays1[i] = new DecayDelay(new DelayLine3d(sampleRate, DecayDelayMaxTimes1[i])); + DecayDelays2[i] = new DecayDelay(new DelayLine3d(sampleRate, DecayDelayMaxTimes2[i])); + } + + PreDelayLine = new DelayLine3d(sampleRate, 400); + BackLeftDelayLine = new DelayLine3d(sampleRate, 5); + + UpdateParameter(ref parameter); + } + + public void UpdateParameter(ref Reverb3dParameter parameter) + { + uint sampleRate = parameter.SampleRate / 1000; + + EarlyDelayTime = new uint[20]; + DryGain = parameter.DryGain; + PreviousFeedbackOutputDecayed.AsSpan().Fill(0); + PreviousPreDelayValue = 0; + + EarlyReflectionsGain = FloatingPointHelper.Pow10(Math.Min(parameter.RoomGain + parameter.ReflectionsGain, 5000.0f) / 2000.0f); + LateReverbGain = FloatingPointHelper.Pow10(Math.Min(parameter.RoomGain + parameter.ReverbGain, 5000.0f) / 2000.0f); + + float highFrequencyRoomGain = FloatingPointHelper.Pow10(parameter.RoomHf / 2000.0f); + + if (highFrequencyRoomGain < 1.0f) + { + float tempA = 1.0f - highFrequencyRoomGain; + float tempB = 2.0f - ((2.0f * highFrequencyRoomGain) * FloatingPointHelper.Cos(256.0f * parameter.HfReference / parameter.SampleRate)); + float tempC = MathF.Sqrt(MathF.Pow(tempB, 2) - (4.0f * (1.0f - highFrequencyRoomGain) * (1.0f - highFrequencyRoomGain))); + + PreviousPreDelayGain = (tempB - tempC) / (2.0f * tempA); + TargetPreDelayGain = 1.0f - PreviousPreDelayGain; + } + else + { + PreviousPreDelayGain = 0.0f; + TargetPreDelayGain = 1.0f; + } + + ReflectionDelayTime = IDelayLine.GetSampleCount(sampleRate, 1000.0f * (parameter.ReflectionDelay + parameter.ReverbDelayTime)); + EchoLateReverbDecay = 0.6f * parameter.Diffusion * 0.01f; + + for (int i = 0; i < FdnDelayLines.Length; i++) + { + FdnDelayLines[i].SetDelay(FdnDelayMinTimes[i] + (parameter.Density / 100 * (FdnDelayMaxTimes[i] - FdnDelayMinTimes[i]))); + + uint tempSampleCount = FdnDelayLines[i].CurrentSampleCount + DecayDelays1[i].CurrentSampleCount + DecayDelays2[i].CurrentSampleCount; + + float tempA = (-60.0f * tempSampleCount) / (parameter.DecayTime * parameter.SampleRate); + float tempB = tempA / parameter.HfDecayRatio; + float tempC = FloatingPointHelper.Cos(128.0f * 0.5f * parameter.HfReference / parameter.SampleRate) / FloatingPointHelper.Sin(128.0f * 0.5f * parameter.HfReference / parameter.SampleRate); + float tempD = FloatingPointHelper.Pow10((tempB - tempA) / 40.0f); + float tempE = FloatingPointHelper.Pow10((tempB + tempA) / 40.0f) * 0.7071f; + + DecayDirectFdnGain[i] = tempE * ((tempD * tempC) + 1.0f) / (tempC + tempD); + DecayCurrentFdnGain[i] = tempE * (1.0f - (tempD * tempC)) / (tempC + tempD); + DecayCurrentOutputGain[i] = (tempC - tempD) / (tempC + tempD); + + DecayDelays1[i].SetDecayRate(EchoLateReverbDecay); + DecayDelays2[i].SetDecayRate(EchoLateReverbDecay * -0.9f); + } + + for (int i = 0; i < EarlyDelayTime.Length; i++) + { + uint sampleCount = Math.Min(IDelayLine.GetSampleCount(sampleRate, (parameter.ReflectionDelay * 1000.0f) + (EarlyDelayTimes[i] * 1000.0f * ((parameter.ReverbDelayTime * 0.9998f) + 0.02f))), PreDelayLine.SampleCountMax); + EarlyDelayTime[i] = sampleCount; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs b/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs new file mode 100644 index 00000000..60d8e284 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Dsp/State/ReverbState.cs @@ -0,0 +1,221 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Effect; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using System; + +namespace Ryujinx.Audio.Renderer.Dsp.State +{ + public class ReverbState + { + private static readonly float[] FdnDelayTimes = new float[20] + { + // Room + 53.953247f, 79.192566f, 116.238770f, 130.615295f, + // Hall + 53.953247f, 79.192566f, 116.238770f, 170.615295f, + // Plate + 5f, 10f, 5f, 10f, + // Cathedral + 47.03f, 71f, 103f, 170f, + // Max delay (Hall is the one with the highest values so identical to Hall) + 53.953247f, 79.192566f, 116.238770f, 170.615295f, + }; + + private static readonly float[] DecayDelayTimes = new float[20] + { + // Room + 7f, 9f, 13f, 17f, + // Hall + 7f, 9f, 13f, 17f, + // Plate (no decay) + 1f, 1f, 1f, 1f, + // Cathedral + 7f, 7f, 13f, 9f, + // Max delay (Hall is the one with the highest values so identical to Hall) + 7f, 9f, 13f, 17f, + }; + + private static readonly float[] EarlyDelayTimes = new float[50] + { + // Room + 0.0f, 3.5f, 2.8f, 3.9f, 2.7f, 13.4f, 7.9f, 8.4f, 9.9f, 12.0f, + // Chamber + 0.0f, 11.8f, 5.5f, 11.2f, 10.4f, 38.1f, 22.2f, 29.6f, 21.2f, 24.8f, + // Hall + 0.0f, 41.5f, 20.5f, 41.3f, 0.0f, 29.5f, 33.8f, 45.2f, 46.8f, 0.0f, + // Cathedral + 33.1f, 43.3f, 22.8f, 37.9f, 14.9f, 35.3f, 17.9f, 34.2f, 0.0f, 43.3f, + // Disabled + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + + private static readonly float[] EarlyGainBase = new float[50] + { + // Room + 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, + // Chamber + 0.70f, 0.68f, 0.70f, 0.68f, 0.70f, 0.68f, 0.68f, 0.68f, 0.68f, 0.68f, + // Hall + 0.50f, 0.70f, 0.70f, 0.68f, 0.50f, 0.68f, 0.68f, 0.70f, 0.68f, 0.00f, + // Cathedral + 0.93f, 0.92f, 0.87f, 0.86f, 0.94f, 0.81f, 0.80f, 0.77f, 0.76f, 0.65f, + // Disabled + 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f, 0.00f + }; + + private static readonly float[] PreDelayTimes = new float[5] + { + // Room + 12.5f, + // Chamber + 40.0f, + // Hall + 50.0f, + // Cathedral + 50.0f, + // Disabled + 0.0f + }; + + public IDelayLine[] FdnDelayLines { get; } + public DecayDelay[] DecayDelays { get; } + public IDelayLine PreDelayLine { get; } + public IDelayLine BackLeftDelayLine { get; } + public uint[] EarlyDelayTime { get; } + public float[] EarlyGain { get; } + public uint PreDelayLineDelayTime { get; private set; } + + public float[] HighFrequencyDecayDirectGain { get; } + public float[] HighFrequencyDecayPreviousGain { get; } + public float[] PreviousFeedbackOutput { get; } + + public const int EarlyModeCount = 10; + + private const int FixedPointPrecision = 14; + + private ReadOnlySpan<float> GetFdnDelayTimesByLateMode(ReverbLateMode lateMode) + { + return FdnDelayTimes.AsSpan().Slice((int)lateMode * 4, 4); + } + + private ReadOnlySpan<float> GetDecayDelayTimesByLateMode(ReverbLateMode lateMode) + { + return DecayDelayTimes.AsSpan().Slice((int)lateMode * 4, 4); + } + + public ReverbState(ref ReverbParameter parameter, ulong workBuffer, bool isLongSizePreDelaySupported) + { + FdnDelayLines = new IDelayLine[4]; + DecayDelays = new DecayDelay[4]; + EarlyDelayTime = new uint[EarlyModeCount]; + EarlyGain = new float[EarlyModeCount]; + HighFrequencyDecayDirectGain = new float[4]; + HighFrequencyDecayPreviousGain = new float[4]; + PreviousFeedbackOutput = new float[4]; + + ReadOnlySpan<float> fdnDelayTimes = GetFdnDelayTimesByLateMode(ReverbLateMode.Limit); + ReadOnlySpan<float> decayDelayTimes = GetDecayDelayTimesByLateMode(ReverbLateMode.Limit); + + uint sampleRate = (uint)FixedPointHelper.ToFloat((uint)parameter.SampleRate, FixedPointPrecision); + + for (int i = 0; i < 4; i++) + { + FdnDelayLines[i] = new DelayLine(sampleRate, fdnDelayTimes[i]); + DecayDelays[i] = new DecayDelay(new DelayLine(sampleRate, decayDelayTimes[i])); + } + + float preDelayTimeMax = 150.0f; + + if (isLongSizePreDelaySupported) + { + preDelayTimeMax = 350.0f; + } + + PreDelayLine = new DelayLine(sampleRate, preDelayTimeMax); + BackLeftDelayLine = new DelayLine(sampleRate, 5.0f); + + UpdateParameter(ref parameter); + } + + public void UpdateParameter(ref ReverbParameter parameter) + { + uint sampleRate = (uint)FixedPointHelper.ToFloat((uint)parameter.SampleRate, FixedPointPrecision); + + float preDelayTimeInMilliseconds = FixedPointHelper.ToFloat(parameter.PreDelayTime, FixedPointPrecision); + float earlyGain = FixedPointHelper.ToFloat(parameter.EarlyGain, FixedPointPrecision); + float coloration = FixedPointHelper.ToFloat(parameter.Coloration, FixedPointPrecision); + float decayTime = FixedPointHelper.ToFloat(parameter.DecayTime, FixedPointPrecision); + + for (int i = 0; i < 10; i++) + { + EarlyDelayTime[i] = Math.Min(IDelayLine.GetSampleCount(sampleRate, EarlyDelayTimes[i] + preDelayTimeInMilliseconds), PreDelayLine.SampleCountMax) + 1; + EarlyGain[i] = EarlyGainBase[i] * earlyGain; + } + + if (parameter.ChannelCount == 2) + { + EarlyGain[4] = EarlyGain[4] * 0.5f; + EarlyGain[5] = EarlyGain[5] * 0.5f; + } + + PreDelayLineDelayTime = Math.Min(IDelayLine.GetSampleCount(sampleRate, PreDelayTimes[(int)parameter.EarlyMode] + preDelayTimeInMilliseconds), PreDelayLine.SampleCountMax); + + ReadOnlySpan<float> fdnDelayTimes = GetFdnDelayTimesByLateMode(parameter.LateMode); + ReadOnlySpan<float> decayDelayTimes = GetDecayDelayTimesByLateMode(parameter.LateMode); + + float highFrequencyDecayRatio = FixedPointHelper.ToFloat(parameter.HighFrequencyDecayRatio, FixedPointPrecision); + float highFrequencyUnknownValue = FloatingPointHelper.Cos(1280.0f / sampleRate); + + for (int i = 0; i < 4; i++) + { + FdnDelayLines[i].SetDelay(fdnDelayTimes[i]); + DecayDelays[i].SetDelay(decayDelayTimes[i]); + + float tempA = -3 * (DecayDelays[i].CurrentSampleCount + FdnDelayLines[i].CurrentSampleCount); + float tempB = tempA / (decayTime * sampleRate); + float tempC; + float tempD; + + if (highFrequencyDecayRatio < 0.995f) + { + float tempE = FloatingPointHelper.Pow10((((1.0f / highFrequencyDecayRatio) - 1.0f) * 2) / 100 * (tempB / 10)); + float tempF = 1.0f - tempE; + float tempG = 2.0f - (tempE * 2 * highFrequencyUnknownValue); + float tempH = MathF.Sqrt((tempG * tempG) - (tempF * tempF * 4)); + + tempC = (tempG - tempH) / (tempF * 2); + tempD = 1.0f - tempC; + } + else + { + // no high frequency decay ratio + tempC = 0.0f; + tempD = 1.0f; + } + + HighFrequencyDecayDirectGain[i] = FloatingPointHelper.Pow10(tempB / 1000) * tempD * 0.7071f; + HighFrequencyDecayPreviousGain[i] = tempC; + PreviousFeedbackOutput[i] = 0.0f; + + DecayDelays[i].SetDecayRate(0.6f * (1.0f - coloration)); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/AudioRendererConfiguration.cs b/Ryujinx.Audio/Renderer/Parameter/AudioRendererConfiguration.cs new file mode 100644 index 00000000..047aa279 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/AudioRendererConfiguration.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Audio Renderer user configuration. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AudioRendererConfiguration + { + /// <summary> + /// The target sample rate of the user. + /// </summary> + /// <remarks>Only 32000Hz and 48000Hz are considered valid, other sample rates will cause undefined behaviour.</remarks> + public uint SampleRate; + + /// <summary> + /// The target sample count per <see cref="Dsp.AudioProcessor"/> updates. + /// </summary> + public uint SampleCount; + + /// <summary> + /// The maximum mix buffer count. + /// </summary> + public uint MixBufferCount; + + /// <summary> + /// The maximum amount of sub mixes that could be used by the user. + /// </summary> + public uint SubMixBufferCount; + + /// <summary> + /// The maximum amount of voices that could be used by the user. + /// </summary> + public uint VoiceCount; + + /// <summary> + /// The maximum amount of sinks that could be used by the user. + /// </summary> + public uint SinkCount; + + /// <summary> + /// The maximum amount of effects that could be used by the user. + /// </summary> + public uint EffectCount; + + /// <summary> + /// The maximum amount of performance metric frames that could be used by the user. + /// </summary> + public uint PerformanceMetricFramesCount; + + /// <summary> + /// Set to true if the user allows the <see cref="Server.AudioRenderSystem"/> to drop voices. + /// </summary> + /// <seealso cref="Server.AudioRenderSystem.ComputeVoiceDrop(Server.CommandBuffer, long, long)"/> + [MarshalAs(UnmanagedType.I1)] + public bool VoiceDropEnabled; + + /// <summary> + /// Reserved/unused + /// </summary> + private byte _reserved; + + /// <summary> + /// The target rendering device. + /// </summary> + /// <remarks>Must be <see cref="AudioRendererRenderingDevice.Dsp"/></remarks> + public AudioRendererRenderingDevice RenderingDevice; + + /// <summary> + /// The target execution mode. + /// </summary> + /// <remarks>Must be <see cref="AudioRendererExecutionMode.Auto"/></remarks> + public AudioRendererExecutionMode ExecutionMode; + + /// <summary> + /// The maximum amount of splitters that could be used by the user. + /// </summary> + public uint SplitterCount; + + /// <summary> + /// The maximum amount of splitters destinations that could be used by the user. + /// </summary> + public uint SplitterDestinationCount; + + /// <summary> + /// The size of the external context. + /// </summary> + /// <remarks>This is a leftover of the old "codec" interface system that was present between 1.0.0 and 3.0.0. This was entirely removed from the server side with REV8.</remarks> + public uint ExternalContextSize; + + /// <summary> + /// The user audio revision + /// </summary> + /// <seealso cref="Server.BehaviourContext"/> + public int Revision; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs new file mode 100644 index 00000000..e5312f04 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/BehaviourErrorInfoOutStatus.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Output information for behaviour. + /// </summary> + /// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, ReadOnlyMemory{byte})"/> processing.</remarks> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BehaviourErrorInfoOutStatus + { + /// <summary> + /// The reported errors. + /// </summary> + public Array10<ErrorInfo> ErrorInfos; + + /// <summary> + /// The amount of error that got reported. + /// </summary> + public uint ErrorInfosCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/BiquadFilterParameter.cs b/Ryujinx.Audio/Renderer/Parameter/BiquadFilterParameter.cs new file mode 100644 index 00000000..de6b9577 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/BiquadFilterParameter.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Biquad filter parameters. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0xC, Pack = 1)] + public struct BiquadFilterParameter + { + /// <summary> + /// Set to true if the biquad filter is active. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool Enable; + + /// <summary> + /// Reserved/padding. + /// </summary> + private byte _reserved; + + /// <summary> + /// Biquad filter numerator (b0, b1, b2). + /// </summary> + public Array3<short> Numerator; + + /// <summary> + /// Biquad filter denominator (a1, a2). + /// </summary> + /// <remarks>a0 = 1</remarks> + public Array2<short> Denominator; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/AuxiliaryBufferParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/AuxiliaryBufferParameter.cs new file mode 100644 index 00000000..5a747922 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/AuxiliaryBufferParameter.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.AuxiliaryBuffer"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct AuxiliaryBufferParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/> to write data to <see cref="SendBufferInfoAddress"/>. + /// </summary> + public Array24<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/> to read data from <see cref="ReturnBufferInfoAddress"/>. + /// </summary> + public Array24<byte> Output; + + /// <summary> + /// The total channel count used. + /// </summary> + public uint ChannelCount; + + /// <summary> + /// The target sample rate. + /// </summary> + public uint SampleRate; + + /// <summary> + /// The buffer storage total size. + /// </summary> + public uint BufferStorageSize; + + /// <summary> + /// The maximum number of channels supported. + /// </summary> + /// <remarks>This is unused.</remarks> + public uint ChannelCountMax; + + /// <summary> + /// The address of the start of the region containing two <see cref="Dsp.State.AuxiliaryBufferHeader"/> followed by the data that will be written by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public ulong SendBufferInfoAddress; + + /// <summary> + /// The address of the start of the region containling data that will be written by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <remarks>This is unused.</remarks> + public ulong SendBufferStorageAddress; + + /// <summary> + /// The address of the start of the region containing two <see cref="Dsp.State.AuxiliaryBufferHeader"/> followed by the data that will be read by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public ulong ReturnBufferInfoAddress; + + /// <summary> + /// The address of the start of the region containling data that will be read by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <remarks>This is unused.</remarks> + public ulong ReturnBufferStorageAddress; + + /// <summary> + /// Size of a sample of the mix buffer. + /// </summary> + /// <remarks>This is unused.</remarks> + public uint MixBufferSampleSize; + + /// <summary> + /// The total count of sample that can be stored. + /// </summary> + /// <remarks>This is unused.</remarks> + public uint TotalSampleCount; + + /// <summary> + /// The count of sample of the mix buffer. + /// </summary> + /// <remarks>This is unused.</remarks> + public uint MixBufferSampleCount; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter.cs new file mode 100644 index 00000000..7d58d4be --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/BiquadFilterEffectParameter.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.BiquadFilter"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BiquadFilterEffectParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Output; + + /// <summary> + /// Biquad filter numerator (b0, b1, b2). + /// </summary> + public Array3<short> Numerator; + + /// <summary> + /// Biquad filter denominator (a1, a2). + /// </summary> + /// <remarks>a0 = 1</remarks> + public Array2<short> Denominator; + + /// <summary> + /// The total channel count used. + /// </summary> + public byte ChannelCount; + + /// <summary> + /// The current usage status of the effect on the client side. + /// </summary> + public UsageState Status; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/BufferMixerParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/BufferMixerParameter.cs new file mode 100644 index 00000000..b0c0c337 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/BufferMixerParameter.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.BufferMix"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BufferMixParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array24<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array24<byte> Output; + + /// <summary> + /// The output volumes of the mixes. + /// </summary> + public Array24<float> Volumes; + + /// <summary> + /// The total count of mixes used. + /// </summary> + public uint MixesCount; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/DelayParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/DelayParameter.cs new file mode 100644 index 00000000..e0dbeb7c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/DelayParameter.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.Delay"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DelayParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Output; + + /// <summary> + /// The maximum number of channels supported. + /// </summary> + public ushort ChannelCountMax; + + /// <summary> + /// The total channel count used. + /// </summary> + public ushort ChannelCount; + + /// <summary> + /// The maximum delay time in milliseconds. + /// </summary> + public uint DelayTimeMax; + + /// <summary> + /// The delay time in milliseconds. + /// </summary> + public uint DelayTime; + + /// <summary> + /// The target sample rate. (Q15) + /// </summary> + public uint SampleRate; + + /// <summary> + /// The input gain. (Q15) + /// </summary> + public uint InGain; + + /// <summary> + /// The feedback gain. (Q15) + /// </summary> + public uint FeedbackGain; + + /// <summary> + /// The output gain. (Q15) + /// </summary> + public uint OutGain; + + /// <summary> + /// The dry gain. (Q15) + /// </summary> + public uint DryGain; + + /// <summary> + /// The channel spread of the <see cref="FeedbackGain"/>. (Q15) + /// </summary> + public uint ChannelSpread; + + /// <summary> + /// The low pass amount. (Q15) + /// </summary> + public uint LowPassAmount; + + /// <summary> + /// The current usage status of the effect on the client side. + /// </summary> + public UsageState Status; + + /// <summary> + /// Check if the <see cref="ChannelCount"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCount"/> is valid.</returns> + public bool IsChannelCountValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCount); + } + + /// <summary> + /// Check if the <see cref="ChannelCountMax"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCountMax"/> is valid.</returns> + public bool IsChannelCountMaxValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCountMax); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/Reverb3dParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/Reverb3dParameter.cs new file mode 100644 index 00000000..0dbecfb8 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/Reverb3dParameter.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.Reverb3d"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Reverb3dParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Output; + + /// <summary> + /// The maximum number of channels supported. + /// </summary> + public ushort ChannelCountMax; + + /// <summary> + /// The total channel count used. + /// </summary> + public ushort ChannelCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private uint _reserved; + + /// <summary> + /// The target sample rate. + /// </summary> + /// <remarks>This is in kHz.</remarks> + public uint SampleRate; + + /// <summary> + /// Gain of the room high-frequency effect. + /// </summary> + public float RoomHf; + + /// <summary> + /// Reference high frequency. + /// </summary> + public float HfReference; + + /// <summary> + /// Reverberation decay time at low frequencies. + /// </summary> + public float DecayTime; + + /// <summary> + /// Ratio of the decay time at high frequencies to the decay time at low frequencies. + /// </summary> + public float HfDecayRatio; + + /// <summary> + /// Gain of the room effect. + /// </summary> + public float RoomGain; + + /// <summary> + /// Gain of the early reflections relative to <see cref="RoomGain"/>. + /// </summary> + public float ReflectionsGain; + + /// <summary> + /// Gain of the late reverberation relative to <see cref="RoomGain"/>. + /// </summary> + public float ReverbGain; + + /// <summary> + /// Echo density in the late reverberation decay. + /// </summary> + public float Diffusion; + + /// <summary> + /// Modal density in the late reverberation decay. + /// </summary> + public float ReflectionDelay; + + /// <summary> + /// Time limit between the early reflections and the late reverberation relative to the time of the first reflection. + /// </summary> + public float ReverbDelayTime; + + /// <summary> + /// Modal density in the late reverberation decay. + /// </summary> + public float Density; + + /// <summary> + /// The dry gain. + /// </summary> + public float DryGain; + + /// <summary> + /// The current usage status of the effect on the client side. + /// </summary> + public UsageState ParameterStatus; + + /// <summary> + /// Check if the <see cref="ChannelCount"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCount"/> is valid.</returns> + public bool IsChannelCountValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCount); + } + + /// <summary> + /// Check if the <see cref="ChannelCountMax"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCountMax"/> is valid.</returns> + public bool IsChannelCountMaxValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCountMax); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Effect/ReverbParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Effect/ReverbParameter.cs new file mode 100644 index 00000000..daa81c51 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Effect/ReverbParameter.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Effect +{ + /// <summary> + /// <see cref="EffectInParameter.SpecificData"/> for <see cref="Common.EffectType.Reverb"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ReverbParameter + { + /// <summary> + /// The input channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// The output channel indices that will be used by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public Array6<byte> Output; + + /// <summary> + /// The maximum number of channels supported. + /// </summary> + public ushort ChannelCountMax; + + /// <summary> + /// The total channel count used. + /// </summary> + public ushort ChannelCount; + + /// <summary> + /// The target sample rate. (Q15) + /// </summary> + /// <remarks>This is in kHz.</remarks> + public int SampleRate; + + /// <summary> + /// The early mode to use. + /// </summary> + public ReverbEarlyMode EarlyMode; + + /// <summary> + /// The gain to apply to the result of the early reflection. (Q15) + /// </summary> + public int EarlyGain; + + /// <summary> + /// The pre-delay time in milliseconds. (Q15) + /// </summary> + public int PreDelayTime; + + /// <summary> + /// The late mode to use. + /// </summary> + public ReverbLateMode LateMode; + + /// <summary> + /// The gain to apply to the result of the late reflection. (Q15) + /// </summary> + public int LateGain; + + /// <summary> + /// The decay time. (Q15) + /// </summary> + public int DecayTime; + + /// <summary> + /// The high frequency decay ratio. (Q15) + /// </summary> + /// <remarks>If <see cref="HighFrequencyDecayRatio"/> >= 0.995f, it is considered disabled.</remarks> + public int HighFrequencyDecayRatio; + + /// <summary> + /// The coloration of the decay. (Q15) + /// </summary> + public int Coloration; + + /// <summary> + /// The reverb gain. (Q15) + /// </summary> + public int ReverbGain; + + /// <summary> + /// The output gain. (Q15) + /// </summary> + public int OutGain; + + /// <summary> + /// The dry gain. (Q15) + /// </summary> + public int DryGain; + + /// <summary> + /// The current usage status of the effect on the client side. + /// </summary> + public UsageState Status; + + /// <summary> + /// Check if the <see cref="ChannelCount"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCount"/> is valid.</returns> + public bool IsChannelCountValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCount); + } + + /// <summary> + /// Check if the <see cref="ChannelCountMax"/> is valid. + /// </summary> + /// <returns>Returns true if the <see cref="ChannelCountMax"/> is valid.</returns> + public bool IsChannelCountMaxValid() + { + return EffectInParameter.IsChannelCountValid(ChannelCountMax); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/EffectInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/EffectInParameter.cs new file mode 100644 index 00000000..af8edeff --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/EffectInParameter.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for an effect. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct EffectInParameter + { + /// <summary> + /// Type of the effect. + /// </summary> + public EffectType Type; + + /// <summary> + /// Set to true if the effect is new. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsNew; + + /// <summary> + /// Set to true if the effect must be active. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsEnabled; + + /// <summary> + /// Reserved/padding. + /// </summary> + private byte _reserved1; + + /// <summary> + /// The target mix id of the effect. + /// </summary> + public int MixId; + + /// <summary> + /// Address of the processing workbuffer. + /// </summary> + /// <remarks>This is additional data that could be required by the effect processing.</remarks> + public ulong BufferBase; + + /// <summary> + /// Size of the processing workbuffer. + /// </summary> + /// <remarks>This is additional data that could be required by the effect processing.</remarks> + public ulong BufferSize; + + /// <summary> + /// Position of the effect while processing effects. + /// </summary> + public uint ProcessingOrder; + + /// <summary> + /// Reserved/padding. + /// </summary> + private uint _reserved2; + + /// <summary> + /// Specific data storage. + /// </summary> + private SpecificDataStruct _specificDataStart; + + [StructLayout(LayoutKind.Sequential, Size = 0xA0, Pack = 1)] + private struct SpecificDataStruct { } + + /// <summary> + /// Specific data changing depending of the <see cref="Type"/>. See also the <see cref="Effect"/> namespace. + /// </summary> + public Span<byte> SpecificData => SpanHelpers.AsSpan<SpecificDataStruct, byte>(ref _specificDataStart); + + /// <summary> + /// Check if the given channel count is valid. + /// </summary> + /// <param name="channelCount">The channel count to check</param> + /// <returns>Returns true if the channel count is valid.</returns> + public static bool IsChannelCountValid(int channelCount) + { + return channelCount == 1 || channelCount == 2 || channelCount == 4 || channelCount == 6; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/EffectOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/EffectOutStatus.cs new file mode 100644 index 00000000..c6178165 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/EffectOutStatus.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Output information for an effect. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct EffectOutStatus + { + /// <summary> + /// The state of an effect. + /// </summary> + public enum EffectState : byte + { + /// <summary> + /// The effect is enabled. + /// </summary> + Enabled = 3, + + /// <summary> + /// The effect is disabled. + /// </summary> + Disabled = 4 + } + + /// <summary> + /// Current effect state. + /// </summary> + public EffectState State; + + /// <summary> + /// Unused/Reserved. + /// </summary> + private unsafe fixed byte _reserved[15]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/MemoryPoolInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/MemoryPoolInParameter.cs new file mode 100644 index 00000000..15498cca --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/MemoryPoolInParameter.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for a memory pool. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MemoryPoolInParameter + { + /// <summary> + /// The CPU address used by the memory pool. + /// </summary> + public CpuAddress CpuAddress; + + /// <summary> + /// The size used by the memory pool. + /// </summary> + public ulong Size; + + /// <summary> + /// The target state the user wants. + /// </summary> + public MemoryPoolUserState State; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/MemoryPoolOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/MemoryPoolOutStatus.cs new file mode 100644 index 00000000..c125cb8b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/MemoryPoolOutStatus.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Output information for a memory pool. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MemoryPoolOutStatus + { + /// <summary> + /// The current server memory pool state. + /// </summary> + public MemoryPoolUserState State; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/MixInParameterDirtyOnlyUpdate.cs b/Ryujinx.Audio/Renderer/Parameter/MixInParameterDirtyOnlyUpdate.cs new file mode 100644 index 00000000..cdbd5871 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/MixInParameterDirtyOnlyUpdate.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information header for mix updates on REV7 and later + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MixInParameterDirtyOnlyUpdate + { + /// <summary> + /// Magic of the header + /// </summary> + /// <remarks>Never checked on hardware.</remarks> + public uint Magic; + + /// <summary> + /// The count of <see cref="MixParameter"/> following this header. + /// </summary> + public uint MixCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed byte _reserved[24]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/MixParameter.cs b/Ryujinx.Audio/Renderer/Parameter/MixParameter.cs new file mode 100644 index 00000000..1d7f27f4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/MixParameter.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for a mix. + /// </summary> + /// <remarks>Also used on the client side for mix tracking.</remarks> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MixParameter + { + /// <summary> + /// Base volume of the mix. + /// </summary> + public float Volume; + + /// <summary> + /// Target sample rate of the mix. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Target buffer count. + /// </summary> + public uint BufferCount; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// Set to true if it was changed. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsDirty; + + /// <summary> + /// Reserved/padding. + /// </summary> + private ushort _reserved1; + + /// <summary> + /// The id of the mix. + /// </summary> + public int MixId; + + /// <summary> + /// The effect count. (client side) + /// </summary> + public uint EffectCount; + + /// <summary> + /// The mix node id. + /// </summary> + public int NodeId; + + /// <summary> + /// Reserved/padding. + /// </summary> + private ulong _reserved2; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixVolumeArray _mixBufferVolumeArray; + + /// <summary> + /// The mix to output the result of this mix. + /// </summary> + public int DestinationMixId; + + /// <summary> + /// The splitter to output the result of this mix. + /// </summary> + public uint DestinationSplitterId; + + /// <summary> + /// Reserved/padding. + /// </summary> + private uint _reserved3; + + [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax * Constants.MixBufferCountMax, Pack = 1)] + private struct MixVolumeArray { } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when no splitter id is specified.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixVolumeArray, float>(ref _mixBufferVolumeArray); + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceInParameter.cs new file mode 100644 index 00000000..929f006b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceInParameter.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Performance +{ + /// <summary> + /// Input information for performance monitoring. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct PerformanceInParameter + { + /// <summary> + /// The target node id to monitor performance on. + /// </summary> + public int TargetNodeId; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceOutStatus.cs new file mode 100644 index 00000000..9263362c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Performance/PerformanceOutStatus.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Performance +{ + /// <summary> + /// Output information for performance monitoring. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct PerformanceOutStatus + { + /// <summary> + /// Indicates the total size output to the performance buffer. + /// </summary> + public uint HistorySize; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/RendererInfoOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/RendererInfoOutStatus.cs new file mode 100644 index 00000000..315f0968 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/RendererInfoOutStatus.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Renderer output information on REV5 and later. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct RendererInfoOutStatus + { + /// <summary> + /// The count of updates sent to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + public ulong ElapsedFrameCount; + + /// <summary> + /// Reserved/Unused. + /// </summary> + private ulong _reserved; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Sink/CircularBufferParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Sink/CircularBufferParameter.cs new file mode 100644 index 00000000..ae06481f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Sink/CircularBufferParameter.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Parameter.Sink +{ + /// <summary> + /// <see cref="SinkInParameter.SpecificData"/> for <see cref="SinkType.CircularBuffer"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CircularBufferParameter + { + /// <summary> + /// The CPU address of the user circular buffer. + /// </summary> + public CpuAddress BufferAddress; + + /// <summary> + /// The size of the user circular buffer. + /// </summary> + public uint BufferSize; + + /// <summary> + /// The total count of channels to output to the circular buffer. + /// </summary> + public uint InputCount; + + /// <summary> + /// The target sample count to output per update in the circular buffer. + /// </summary> + public uint SampleCount; + + /// <summary> + /// Last read offset on the CPU side. + /// </summary> + public uint LastReadOffset; + + /// <summary> + /// The target <see cref="SampleFormat"/>. + /// </summary> + /// <remarks>Only <see cref="SampleFormat.PcmInt16"/> is supported.</remarks> + public SampleFormat SampleFormat; + + /// <summary> + /// Reserved/padding. + /// </summary> + private unsafe fixed byte _reserved1[3]; + + /// <summary> + /// The input channels index that will be used. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// Reserved/padding. + /// </summary> + private ushort _reserved2; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/Sink/DeviceParameter.cs b/Ryujinx.Audio/Renderer/Parameter/Sink/DeviceParameter.cs new file mode 100644 index 00000000..591b4945 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/Sink/DeviceParameter.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter.Sink +{ + /// <summary> + /// <see cref="SinkInParameter.SpecificData"/> for <see cref="Common.SinkType.Device"/>. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DeviceParameter + { + /// <summary> + /// Device name storage. + /// </summary> + private DeviceNameStruct _deviceName; + + /// <summary> + /// Reserved/padding. + /// </summary> + private byte _padding; + + /// <summary> + /// The total count of channels to output to the device. + /// </summary> + public uint InputCount; + + /// <summary> + /// The input channels index that will be used. + /// </summary> + public Array6<byte> Input; + + /// <summary> + /// Reserved/padding. + /// </summary> + private byte _reserved; + + /// <summary> + /// Set to true if the user controls Surround to Stereo downmixing coefficients. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool DownMixParameterEnabled; + + /// <summary> + /// The user Surround to Stereo downmixing coefficients. + /// </summary> + public Array4<float> DownMixParameter; + + [StructLayout(LayoutKind.Sequential, Size = 0xFF, Pack = 1)] + private struct DeviceNameStruct { } + + /// <summary> + /// The output device name. + /// </summary> + public Span<byte> DeviceName => SpanHelpers.AsSpan<DeviceNameStruct, byte>(ref _deviceName); + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/SinkInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/SinkInParameter.cs new file mode 100644 index 00000000..0859ef31 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/SinkInParameter.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for a sink. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SinkInParameter + { + /// <summary> + /// Type of the sink. + /// </summary> + public SinkType Type; + + /// <summary> + /// Set to true if the sink is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// Reserved/padding. + /// </summary> + private ushort _reserved1; + + /// <summary> + /// The node id of the sink. + /// </summary> + public int NodeId; + + /// <summary> + /// Reserved/padding. + /// </summary> + private unsafe fixed ulong _reserved2[3]; + + /// <summary> + /// Specific data storage. + /// </summary> + private SpecificDataStruct _specificDataStart; + + [StructLayout(LayoutKind.Sequential, Size = 0x120, Pack = 1)] + private struct SpecificDataStruct { } + + /// <summary> + /// Specific data changing depending of the <see cref="Type"/>. See also the <see cref="Sink"/> namespace. + /// </summary> + public Span<byte> SpecificData => SpanHelpers.AsSpan<SpecificDataStruct, byte>(ref _specificDataStart); + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/SinkOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/SinkOutStatus.cs new file mode 100644 index 00000000..e0cb4942 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/SinkOutStatus.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Output information for a sink. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SinkOutStatus + { + /// <summary> + /// Last written offset if the sink type is <see cref="Common.SinkType.CircularBuffer"/>. + /// </summary> + public uint LastWrittenOffset; + + /// <summary> + /// Reserved/padding. + /// </summary> + private uint _padding; + + /// <summary> + /// Reserved/padding. + /// </summary> + private unsafe fixed ulong _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs new file mode 100644 index 00000000..77d5ddc2 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input header for a splitter destination update. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SplitterDestinationInParameter + { + /// <summary> + /// Magic of the input header. + /// </summary> + public uint Magic; + + /// <summary> + /// Target splitter destination data id. + /// </summary> + public int Id; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixArray _mixBufferVolume; + + /// <summary> + /// The mix to output the result of the splitter. + /// </summary> + public int DestinationId; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// Reserved/padding. + /// </summary> + private unsafe fixed byte _reserved[3]; + + [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when a splitter id is specified in the mix.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mixBufferVolume); + + /// <summary> + /// The expected constant of any input header. + /// </summary> + private const uint ValidMagic = 0x44444E53; + + /// <summary> + /// Check if the magic is valid. + /// </summary> + /// <returns>Returns true if the magic is valid.</returns> + public bool IsMagicValid() + { + return Magic == ValidMagic; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/SplitterInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/SplitterInParameter.cs new file mode 100644 index 00000000..a829abf9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/SplitterInParameter.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input header for a splitter state update. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SplitterInParameter + { + /// <summary> + /// Magic of the input header. + /// </summary> + public uint Magic; + + /// <summary> + /// Target splitter id. + /// </summary> + public int Id; + + /// <summary> + /// Target sample rate to use on the splitter. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Count of splitter destinations. + /// </summary> + /// <remarks>Splitter destination ids are defined right after this header.</remarks> + public int DestinationCount; + + /// <summary> + /// The expected constant of any input header. + /// </summary> + private const uint ValidMagic = 0x49444E53; + + /// <summary> + /// Check if the magic is valid. + /// </summary> + /// <returns>Returns true if the magic is valid.</returns> + public bool IsMagicValid() + { + return Magic == ValidMagic; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/SplitterInParameterHeader.cs b/Ryujinx.Audio/Renderer/Parameter/SplitterInParameterHeader.cs new file mode 100644 index 00000000..746d59b7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/SplitterInParameterHeader.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input header for splitter update. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SplitterInParameterHeader + { + /// <summary> + /// Magic of the input header. + /// </summary> + public uint Magic; + + /// <summary> + /// The count of <see cref="SplitterInParameter"/> after the header. + /// </summary> + public uint SplitterCount; + + /// <summary> + /// The count of splitter destinations after the header and splitter info. + /// </summary> + public uint SplitterDestinationCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved[5]; + + /// <summary> + /// The expected constant of any input splitter header. + /// </summary> + private const uint ValidMagic = 0x48444E53; + + /// <summary> + /// Check if the magic is valid. + /// </summary> + /// <returns>Returns true if the magic is valid.</returns> + public bool IsMagicValid() + { + return Magic == ValidMagic; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/VoiceChannelResourceInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/VoiceChannelResourceInParameter.cs new file mode 100644 index 00000000..73ad2bfd --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/VoiceChannelResourceInParameter.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for a voice channel resources. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x70, Pack = 1)] + public struct VoiceChannelResourceInParameter + { + /// <summary> + /// The id of the voice channel resource. + /// </summary> + public uint Id; + + /// <summary> + /// Mix volumes for the voice channel resource. + /// </summary> + public Array24<float> Mix; + + /// <summary> + /// Indicate if the voice channel resource is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs b/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs new file mode 100644 index 00000000..bced1538 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/VoiceInParameter.cs @@ -0,0 +1,361 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp; +using Ryujinx.Common.Memory; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Input information for a voice. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x170, Pack = 1)] + public struct VoiceInParameter + { + /// <summary> + /// Id of the voice. + /// </summary> + public int Id; + + /// <summary> + /// Node id of the voice. + /// </summary> + public int NodeId; + + /// <summary> + /// Set to true if the voice is new. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsNew; + + /// <summary> + /// Set to true if the voice is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool InUse; + + /// <summary> + /// The voice <see cref="PlayState"/> wanted by the user. + /// </summary> + public PlayState PlayState; + + /// <summary> + /// The <see cref="SampleFormat"/> of the voice. + /// </summary> + public SampleFormat SampleFormat; + + /// <summary> + /// The sample rate of the voice. + /// </summary> + public uint SampleRate; + + /// <summary> + /// The priority of the voice. + /// </summary> + public uint Priority; + + /// <summary> + /// Target sorting position of the voice. (Used to sort voices with the same <see cref="Priority"/>) + /// </summary> + public uint SortingOrder; + + /// <summary> + /// The total channel count used. + /// </summary> + public uint ChannelCount; + + /// <summary> + /// The pitch used on the voice. + /// </summary> + public float Pitch; + + /// <summary> + /// The output volume of the voice. + /// </summary> + public float Volume; + + /// <summary> + /// Biquad filters to apply to the output of the voice. + /// </summary> + public Array2<BiquadFilterParameter> BiquadFilters; + + /// <summary> + /// Total count of <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersCount; + + /// <summary> + /// Current playing <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersIndex; + + /// <summary> + /// Reserved/unused. + /// </summary> + private uint _reserved1; + + /// <summary> + /// User state address required by the data source. + /// </summary> + /// <remarks>Only used for <see cref="SampleFormat.Adpcm"/> as the address of the GC-ADPCM coefficients.</remarks> + public ulong DataSourceStateAddress; + + /// <summary> + /// User state size required by the data source. + /// </summary> + /// <remarks>Only used for <see cref="SampleFormat.Adpcm"/> as the size of the GC-ADPCM coefficients.</remarks> + public ulong DataSourceStateSize; + + /// <summary> + /// The target mix id of the voice. + /// </summary> + public int MixId; + + /// <summary> + /// The target splitter id of the voice. + /// </summary> + public uint SplitterId; + + /// <summary> + /// The wavebuffer parameters of this voice. + /// </summary> + public Array4<WaveBufferInternal> WaveBuffers; + + /// <summary> + /// The channel resource ids associated to the voice. + /// </summary> + public Array6<int> ChannelResourceIds; + + /// <summary> + /// Reset the voice drop flag during voice server update. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool ResetVoiceDropFlag; + + /// <summary> + /// Flush the amount of wavebuffer specified. This will result in the wavebuffer being skipped and marked played. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public byte FlushWaveBufferCount; + + /// <summary> + /// Reserved/unused. + /// </summary> + private ushort _reserved2; + + /// <summary> + /// Change the behaviour of the voice. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public DecodingBehaviour DecodingBehaviourFlags; + + /// <summary> + /// Change the Sample Rate Conversion (SRC) quality of the voice. + /// </summary> + /// <remarks>This was added on REV8.</remarks> + public SampleRateConversionQuality SrcQuality; + + /// <summary> + /// This was previously used for opus codec support on the Audio Renderer and was removed on REV3. + /// </summary> + public uint ExternalContext; + + /// <summary> + /// This was previously used for opus codec support on the Audio Renderer and was removed on REV3. + /// </summary> + public uint ExternalContextSize; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed uint _reserved3[2]; + + /// <summary> + /// Input information for a voice wavebuffer. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x38, Pack = 1)] + public struct WaveBufferInternal + { + /// <summary> + /// Address of the wavebuffer data. + /// </summary> + public ulong Address; + + /// <summary> + /// Size of the wavebuffer data. + /// </summary> + public ulong Size; + + /// <summary> + /// Offset of the first sample to play. + /// </summary> + public uint StartSampleOffset; + + /// <summary> + /// Offset of the last sample to play. + /// </summary> + public uint EndSampleOffset; + + /// <summary> + /// If set to true, the wavebuffer will loop when reaching <see cref="EndSampleOffset"/>. + /// </summary> + /// <remarks> + /// Starting with REV8, you can specify how many times to loop the wavebuffer (<see cref="LoopCount"/>) and where it should start and end when looping (<see cref="LoopFirstSampleOffset"/> and <see cref="LoopLastSampleOffset"/>) + /// </remarks> + [MarshalAs(UnmanagedType.I1)] + public bool ShouldLoop; + + /// <summary> + /// Indicates that this is the last wavebuffer to play of the voice. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsEndOfStream; + + /// <summary> + /// Indicates if the server should update its internal state. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool SentToServer; + + /// <summary> + /// Reserved/unused. + /// </summary> + private byte _reserved; + + /// <summary> + /// If set to anything other than 0, specifies how many times to loop the wavebuffer. + /// </summary> + /// <remarks>This was added in REV8.</remarks> + public int LoopCount; + + /// <summary> + /// Address of the context used by the sample decoder. + /// </summary> + /// <remarks>This is only currently used by <see cref="SampleFormat.Adpcm"/>.</remarks> + public ulong ContextAddress; + + /// <summary> + /// Size of the context used by the sample decoder. + /// </summary> + /// <remarks>This is only currently used by <see cref="SampleFormat.Adpcm"/>.</remarks> + public ulong ContextSize; + + /// <summary> + /// If set to anything other than 0, specifies the offset of the first sample to play when looping. + /// </summary> + /// <remarks>This was added in REV8.</remarks> + public uint LoopFirstSampleOffset; + + /// <summary> + /// If set to anything other than 0, specifies the offset of the last sample to play when looping. + /// </summary> + /// <remarks>This was added in REV8.</remarks> + public uint LoopLastSampleOffset; + + /// <summary> + /// Check if the sample offsets are in a valid range for generic PCM. + /// </summary> + /// <typeparam name="T">The PCM sample type</typeparam> + /// <returns>Returns true if the sample offset are in range of the size.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsSampleOffsetInRangeForPcm<T>() where T : unmanaged + { + uint dataTypeSize = (uint)Unsafe.SizeOf<T>(); + + return StartSampleOffset * dataTypeSize <= Size && + EndSampleOffset * dataTypeSize <= Size; + } + + /// <summary> + /// Check if the sample offsets are in a valid range for the given <see cref="SampleFormat"/>. + /// </summary> + /// <param name="format">The target <see cref="SampleFormat"/></param> + /// <returns>Returns true if the sample offset are in range of the size.</returns> + public bool IsSampleOffsetValid(SampleFormat format) + { + bool result; + + switch (format) + { + case SampleFormat.PcmInt16: + result = IsSampleOffsetInRangeForPcm<ushort>(); + break; + case SampleFormat.PcmFloat: + result = IsSampleOffsetInRangeForPcm<float>(); + break; + case SampleFormat.Adpcm: + result = AdpcmHelper.GetAdpcmDataSize((int)StartSampleOffset) <= Size && + AdpcmHelper.GetAdpcmDataSize((int)EndSampleOffset) <= Size; + break; + default: + throw new NotImplementedException($"{format} not implemented!"); + } + + return result; + } + } + + /// <summary> + /// Flag altering the behaviour of wavebuffer decoding. + /// </summary> + [Flags] + public enum DecodingBehaviour : ushort + { + /// <summary> + /// Default decoding behaviour. + /// </summary> + Default = 0, + + /// <summary> + /// Reset the played samples accumulator when looping. + /// </summary> + PlayedSampleCountResetWhenLooping = 1, + + /// <summary> + /// Skip pitch and Sample Rate Conversion (SRC). + /// </summary> + SkipPitchAndSampleRateConversion = 2 + } + + /// <summary> + /// Specify the quality to use during Sample Rate Conversion (SRC) and pitch handling. + /// </summary> + /// <remarks>This was added in REV8.</remarks> + public enum SampleRateConversionQuality : byte + { + /// <summary> + /// Resample interpolating 4 samples per output sample. + /// </summary> + Default, + + /// <summary> + /// Resample interpolating 8 samples per output sample. + /// </summary> + High, + + /// <summary> + /// Resample interpolating 1 samples per output sample. + /// </summary> + Low + } + } +} diff --git a/Ryujinx.Audio/Renderer/Parameter/VoiceOutStatus.cs b/Ryujinx.Audio/Renderer/Parameter/VoiceOutStatus.cs new file mode 100644 index 00000000..62d17a5a --- /dev/null +++ b/Ryujinx.Audio/Renderer/Parameter/VoiceOutStatus.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Parameter +{ + /// <summary> + /// Output information about a voice. + /// </summary> + /// <remarks>See <seealso cref="Server.StateUpdater.UpdateVoices(Server.Voice.VoiceContext, System.Memory{Server.MemoryPool.MemoryPoolState})"/></remarks> + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct VoiceOutStatus + { + /// <summary> + /// The total amount of samples that was played. + /// </summary> + /// <remarks>This is reset to 0 when a <see cref="Common.WaveBuffer"/> finishes playing and <see cref="Common.WaveBuffer.IsEndOfStream"/> is set.</remarks> + /// <remarks>This is reset to 0 when looping while <see cref="Parameter.VoiceInParameter.DecodingBehaviour.PlayedSampleCountResetWhenLooping"/> is set.</remarks> + public ulong PlayedSampleCount; + + /// <summary> + /// The total amount of <see cref="WaveBuffer"/> consumed. + /// </summary> + public uint PlayedWaveBuffersCount; + + /// <summary> + /// If set to true, the voice was dropped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool VoiceDropFlag; + + /// <summary> + /// Reserved/unused. + /// </summary> + private unsafe fixed byte _reserved[3]; + } +} diff --git a/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs new file mode 100644 index 00000000..787b8f9f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs @@ -0,0 +1,838 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Server.Types; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using Ryujinx.Audio.Renderer.Server.Voice; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Threading; + +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server +{ + public class AudioRenderSystem : IDisposable + { + private object _lock = new object(); + + private AudioRendererExecutionMode _executionMode; + private IWritableEvent _systemEvent; + private ManualResetEvent _terminationEvent; + private MemoryPoolState _dspMemoryPoolState; + private VoiceContext _voiceContext; + private MixContext _mixContext; + private SinkContext _sinkContext; + private SplitterContext _splitterContext; + private EffectContext _effectContext; + private PerformanceManager _performanceManager; + private UpsamplerManager _upsamplerManager; + private bool _isActive; + private BehaviourContext _behaviourContext; + private ulong _totalElapsedTicksUpdating; + private ulong _totalElapsedTicks; + private int _sessionId; + private Memory<MemoryPoolState> _memoryPools; + + private uint _sampleRate; + private uint _sampleCount; + private uint _mixBufferCount; + private uint _voiceChannelCountMax; + private uint _upsamplerCount; + private uint _memoryPoolCount; + private uint _processHandle; + private ulong _appletResourceId; + + private WritableRegion _workBufferRegion; + private MemoryHandle _workBufferMemoryPin; + + private Memory<float> _mixBuffer; + private Memory<float> _depopBuffer; + + private uint _renderingTimeLimitPercent; + private bool _voiceDropEnabled; + private uint _voiceDropCount; + private bool _isDspRunningBehind; + + private ICommandProcessingTimeEstimator _commandProcessingTimeEstimator; + + private Memory<byte> _performanceBuffer; + + public IVirtualMemoryManager MemoryManager { get; private set; } + + private ulong _elapsedFrameCount; + private ulong _renderingStartTick; + + private AudioRendererManager _manager; + + public AudioRenderSystem(AudioRendererManager manager, IWritableEvent systemEvent) + { + _manager = manager; + _terminationEvent = new ManualResetEvent(false); + _dspMemoryPoolState = MemoryPoolState.Create(MemoryPoolState.LocationType.Dsp); + _voiceContext = new VoiceContext(); + _mixContext = new MixContext(); + _sinkContext = new SinkContext(); + _splitterContext = new SplitterContext(); + _effectContext = new EffectContext(); + + _commandProcessingTimeEstimator = null; + _systemEvent = systemEvent; + _behaviourContext = new BehaviourContext(); + + _totalElapsedTicksUpdating = 0; + _sessionId = 0; + } + + public ResultCode Initialize(ref AudioRendererConfiguration parameter, uint processHandle, CpuAddress workBuffer, ulong workBufferSize, int sessionId, ulong appletResourceId, IVirtualMemoryManager memoryManager) + { + if (!BehaviourContext.CheckValidRevision(parameter.Revision)) + { + return ResultCode.OperationFailed; + } + + if (GetWorkBufferSize(ref parameter) > workBufferSize) + { + return ResultCode.WorkBufferTooSmall; + } + + Debug.Assert(parameter.RenderingDevice == AudioRendererRenderingDevice.Dsp && parameter.ExecutionMode == AudioRendererExecutionMode.Auto); + + Logger.Info?.Print(LogClass.AudioRenderer, $"Initializing with REV{BehaviourContext.GetRevisionNumber(parameter.Revision)}"); + + _behaviourContext.SetUserRevision(parameter.Revision); + + _sampleRate = parameter.SampleRate; + _sampleCount = parameter.SampleCount; + _mixBufferCount = parameter.MixBufferCount; + _voiceChannelCountMax = Constants.VoiceChannelCountMax; + _upsamplerCount = parameter.SinkCount + parameter.SubMixBufferCount; + _appletResourceId = appletResourceId; + _memoryPoolCount = parameter.EffectCount + parameter.VoiceCount * Constants.VoiceWaveBufferCount; + _executionMode = parameter.ExecutionMode; + _sessionId = sessionId; + MemoryManager = memoryManager; + + WorkBufferAllocator workBufferAllocator; + + _workBufferRegion = MemoryManager.GetWritableRegion(workBuffer, (int)workBufferSize); + _workBufferRegion.Memory.Span.Fill(0); + _workBufferMemoryPin = _workBufferRegion.Memory.Pin(); + + workBufferAllocator = new WorkBufferAllocator(_workBufferRegion.Memory); + + PoolMapper poolMapper = new PoolMapper(processHandle, false); + poolMapper.InitializeSystemPool(ref _dspMemoryPoolState, workBuffer, workBufferSize); + + _mixBuffer = workBufferAllocator.Allocate<float>(_sampleCount * (_voiceChannelCountMax + _mixBufferCount), 0x10); + + if (_mixBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + Memory<float> upSamplerWorkBuffer = workBufferAllocator.Allocate<float>(Constants.TargetSampleCount * (_voiceChannelCountMax + _mixBufferCount) * _upsamplerCount, 0x10); + + if (upSamplerWorkBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _depopBuffer = workBufferAllocator.Allocate<float>((ulong)BitUtils.AlignUp(parameter.MixBufferCount, Constants.BufferAlignment), Constants.BufferAlignment); + + if (_depopBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Invalidate DSP cache on what was currently allocated with workBuffer. + AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset); + + Debug.Assert((workBufferAllocator.Offset % Constants.BufferAlignment) == 0); + + Memory<VoiceState> voices = workBufferAllocator.Allocate<VoiceState>(parameter.VoiceCount, VoiceState.Alignment); + + if (voices.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + foreach (ref VoiceState voice in voices.Span) + { + voice.Initialize(); + } + + // A pain to handle as we can't have VoiceState*, use indices to be a bit more safe + Memory<int> sortedVoices = workBufferAllocator.Allocate<int>(parameter.VoiceCount, 0x10); + + if (sortedVoices.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Clear memory (use -1 as it's an invalid index) + sortedVoices.Span.Fill(-1); + + Memory<VoiceChannelResource> voiceChannelResources = workBufferAllocator.Allocate<VoiceChannelResource>(parameter.VoiceCount, VoiceChannelResource.Alignment); + + if (voiceChannelResources.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + for (uint id = 0; id < voiceChannelResources.Length; id++) + { + ref VoiceChannelResource voiceChannelResource = ref voiceChannelResources.Span[(int)id]; + + voiceChannelResource.Id = id; + voiceChannelResource.IsUsed = false; + } + + Memory<VoiceUpdateState> voiceUpdateStates = workBufferAllocator.Allocate<VoiceUpdateState>(parameter.VoiceCount, VoiceUpdateState.Align); + + if (voiceUpdateStates.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + uint mixesCount = parameter.SubMixBufferCount + 1; + + Memory<MixState> mixes = workBufferAllocator.Allocate<MixState>(mixesCount, MixState.Alignment); + + if (mixes.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + if (parameter.EffectCount == 0) + { + foreach (ref MixState mix in mixes.Span) + { + mix = new MixState(Memory<int>.Empty, ref _behaviourContext); + } + } + else + { + Memory<int> effectProcessingOrderArray = workBufferAllocator.Allocate<int>(parameter.EffectCount * mixesCount, 0x10); + + foreach (ref MixState mix in mixes.Span) + { + mix = new MixState(effectProcessingOrderArray.Slice(0, (int)parameter.EffectCount), ref _behaviourContext); + + effectProcessingOrderArray = effectProcessingOrderArray.Slice((int)parameter.EffectCount); + } + } + + // Initialize the final mix id + mixes.Span[0].MixId = Constants.FinalMixId; + + Memory<int> sortedMixesState = workBufferAllocator.Allocate<int>(mixesCount, 0x10); + + if (sortedMixesState.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + // Clear memory (use -1 as it's an invalid index) + sortedMixesState.Span.Fill(-1); + + Memory<byte> nodeStatesWorkBuffer = Memory<byte>.Empty; + Memory<byte> edgeMatrixWorkBuffer = Memory<byte>.Empty; + + if (_behaviourContext.IsSplitterSupported()) + { + nodeStatesWorkBuffer = workBufferAllocator.Allocate((uint)NodeStates.GetWorkBufferSize((int)mixesCount), 1); + edgeMatrixWorkBuffer = workBufferAllocator.Allocate((uint)EdgeMatrix.GetWorkBufferSize((int)mixesCount), 1); + + if (nodeStatesWorkBuffer.IsEmpty || edgeMatrixWorkBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + } + + _mixContext.Initialize(sortedMixesState, mixes, nodeStatesWorkBuffer, edgeMatrixWorkBuffer); + + _memoryPools = workBufferAllocator.Allocate<MemoryPoolState>(_memoryPoolCount, MemoryPoolState.Alignment); + + if (_memoryPools.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + foreach (ref MemoryPoolState state in _memoryPools.Span) + { + state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu); + } + + if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator)) + { + return ResultCode.WorkBufferTooSmall; + } + + _processHandle = processHandle; + + _upsamplerManager = new UpsamplerManager(upSamplerWorkBuffer, _upsamplerCount); + + _effectContext.Initialize(parameter.EffectCount); + _sinkContext.Initialize(parameter.SinkCount); + + Memory<VoiceUpdateState> voiceUpdateStatesDsp = workBufferAllocator.Allocate<VoiceUpdateState>(parameter.VoiceCount, VoiceUpdateState.Align); + + if (voiceUpdateStatesDsp.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _voiceContext.Initialize(sortedVoices, voices, voiceChannelResources, voiceUpdateStates, voiceUpdateStatesDsp, parameter.VoiceCount); + + if (parameter.PerformanceMetricFramesCount > 0) + { + ulong performanceBufferSize = PerformanceManager.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter, ref _behaviourContext) * (parameter.PerformanceMetricFramesCount + 1) + 0xC; + + _performanceBuffer = workBufferAllocator.Allocate(performanceBufferSize, Constants.BufferAlignment); + + if (_performanceBuffer.IsEmpty) + { + return ResultCode.WorkBufferTooSmall; + } + + _performanceManager = PerformanceManager.Create(_performanceBuffer, ref parameter, _behaviourContext); + } + else + { + _performanceManager = null; + } + + _totalElapsedTicksUpdating = 0; + _totalElapsedTicks = 0; + _renderingTimeLimitPercent = 100; + _voiceDropEnabled = parameter.VoiceDropEnabled && _executionMode == AudioRendererExecutionMode.Auto; + + AudioProcessorMemoryManager.InvalidateDataCache(workBuffer, workBufferSize); + + _processHandle = processHandle; + _elapsedFrameCount = 0; + + switch (_behaviourContext.GetCommandProcessingTimeEstimatorVersion()) + { + case 1: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion1(_sampleCount, _mixBufferCount); + break; + case 2: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion2(_sampleCount, _mixBufferCount); + break; + case 3: + _commandProcessingTimeEstimator = new CommandProcessingTimeEstimatorVersion3(_sampleCount, _mixBufferCount); + break; + default: + throw new NotImplementedException($"Unsupported processing time estimator version {_behaviourContext.GetCommandProcessingTimeEstimatorVersion()}."); + } + + return ResultCode.Success; + } + + public void Start() + { + Logger.Info?.Print(LogClass.AudioRenderer, $"Starting renderer id {_sessionId}"); + + lock (_lock) + { + _elapsedFrameCount = 0; + _isActive = true; + } + } + + public void Stop() + { + Logger.Info?.Print(LogClass.AudioRenderer, $"Stopping renderer id {_sessionId}"); + + lock (_lock) + { + _isActive = false; + } + + if (_executionMode == AudioRendererExecutionMode.Auto) + { + _terminationEvent.WaitOne(); + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Stopped renderer id {_sessionId}"); + } + + public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlyMemory<byte> input) + { + lock (_lock) + { + ulong updateStartTicks = GetSystemTicks(); + + output.Span.Fill(0); + + StateUpdater stateUpdater = new StateUpdater(input, output, _processHandle, _behaviourContext); + + ResultCode result; + + result = stateUpdater.UpdateBehaviourContext(); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateMemoryPools(_memoryPools.Span); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateVoiceChannelResources(_voiceContext); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + if (_behaviourContext.IsSplitterSupported()) + { + result = stateUpdater.UpdateSplitter(_splitterContext); + + if (result != ResultCode.Success) + { + return result; + } + } + + result = stateUpdater.UpdateMixes(_mixContext, GetMixBufferCount(), _effectContext, _splitterContext); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdatePerformanceBuffer(_performanceManager, performanceOutput.Span); + + if (result != ResultCode.Success) + { + return result; + } + + result = stateUpdater.UpdateErrorInfo(); + + if (result != ResultCode.Success) + { + return result; + } + + if (_behaviourContext.IsElapsedFrameCountSupported()) + { + result = stateUpdater.UpdateRendererInfo(_elapsedFrameCount); + + if (result != ResultCode.Success) + { + return result; + } + } + + result = stateUpdater.CheckConsumedSize(); + + if (result != ResultCode.Success) + { + return result; + } + + _systemEvent.Clear(); + + ulong updateEndTicks = GetSystemTicks(); + + _totalElapsedTicksUpdating += (updateEndTicks - updateStartTicks); + + return result; + } + } + + private ulong GetSystemTicks() + { + double ticks = ARMeilleure.State.ExecutionContext.ElapsedTicks * ARMeilleure.State.ExecutionContext.TickFrequency; + + return (ulong)(ticks * Constants.TargetTimerFrequency); + } + + private uint ComputeVoiceDrop(CommandBuffer commandBuffer, long voicesEstimatedTime, long deltaTimeDsp) + { + int i; + + for (i = 0; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand command = commandBuffer.CommandList.Commands[i]; + + CommandType commandType = command.CommandType; + + if (commandType == CommandType.AdpcmDataSourceVersion1 || + commandType == CommandType.AdpcmDataSourceVersion2 || + commandType == CommandType.PcmInt16DataSourceVersion1 || + commandType == CommandType.PcmInt16DataSourceVersion2 || + commandType == CommandType.PcmFloatDataSourceVersion1 || + commandType == CommandType.PcmFloatDataSourceVersion2 || + commandType == CommandType.Performance) + { + break; + } + } + + uint voiceDropped = 0; + + for (; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand targetCommand = commandBuffer.CommandList.Commands[i]; + + int targetNodeId = targetCommand.NodeId; + + if (voicesEstimatedTime <= deltaTimeDsp || NodeIdHelper.GetType(targetNodeId) != NodeIdType.Voice) + { + break; + } + + ref VoiceState voice = ref _voiceContext.GetState(NodeIdHelper.GetBase(targetNodeId)); + + if (voice.Priority == Constants.VoiceHighestPriority) + { + break; + } + + // We can safely drop this voice, disable all associated commands while activating depop preparation commands. + voiceDropped++; + voice.VoiceDropFlag = true; + + Logger.Warning?.Print(LogClass.AudioRenderer, $"Dropping voice {voice.NodeId}"); + + for (; i < commandBuffer.CommandList.Commands.Count; i++) + { + ICommand command = commandBuffer.CommandList.Commands[i]; + + if (command.NodeId != targetNodeId) + { + break; + } + + if (command.CommandType == CommandType.DepopPrepare) + { + command.Enabled = true; + } + else if (command.CommandType == CommandType.Performance || !command.Enabled) + { + continue; + } + else + { + command.Enabled = false; + + voicesEstimatedTime -= (long)command.EstimatedProcessingTime; + } + } + } + + return voiceDropped; + } + + private void GenerateCommandList(out CommandList commandList) + { + Debug.Assert(_executionMode == AudioRendererExecutionMode.Auto); + + PoolMapper.ClearUsageState(_memoryPools); + + ulong startTicks = GetSystemTicks(); + + commandList = new CommandList(this); + + if (_performanceManager != null) + { + _performanceManager.TapFrame(_isDspRunningBehind, _voiceDropCount, _renderingStartTick); + + _isDspRunningBehind = false; + _voiceDropCount = 0; + _renderingStartTick = 0; + } + + CommandBuffer commandBuffer = new CommandBuffer(commandList, _commandProcessingTimeEstimator); + + CommandGenerator commandGenerator = new CommandGenerator(commandBuffer, GetContext(), _voiceContext, _mixContext, _effectContext, _sinkContext, _splitterContext, _performanceManager); + + _voiceContext.Sort(); + commandGenerator.GenerateVoices(); + + long voicesEstimatedTime = (long)commandBuffer.EstimatedProcessingTime; + + commandGenerator.GenerateSubMixes(); + commandGenerator.GenerateFinalMixes(); + commandGenerator.GenerateSinks(); + + long totalEstimatedTime = (long)commandBuffer.EstimatedProcessingTime; + + if (_voiceDropEnabled) + { + long maxDspTime = GetMaxAllocatedTimeForDsp(); + + long restEstimateTime = totalEstimatedTime - voicesEstimatedTime; + + long deltaTimeDsp = Math.Max(maxDspTime - restEstimateTime, 0); + + _voiceDropCount = ComputeVoiceDrop(commandBuffer, voicesEstimatedTime, deltaTimeDsp); + } + + _voiceContext.UpdateForCommandGeneration(); + + ulong endTicks = GetSystemTicks(); + + _totalElapsedTicks = endTicks - startTicks; + + _renderingStartTick = GetSystemTicks(); + _elapsedFrameCount++; + } + + private int GetMaxAllocatedTimeForDsp() + { + return (int)(Constants.AudioProcessorMaxUpdateTimePerSessions * _behaviourContext.GetAudioRendererProcessingTimeLimit() * (GetRenderingTimeLimit() / 100.0f)); + } + + public void SendCommands() + { + lock (_lock) + { + if (_isActive) + { + _terminationEvent.Reset(); + + GenerateCommandList(out CommandList commands); + + _manager.Processor.Send(_sessionId, + commands, + GetMaxAllocatedTimeForDsp(), + _appletResourceId); + + _systemEvent.Signal(); + } + else + { + _terminationEvent.Set(); + } + } + } + + public uint GetMixBufferCount() + { + return _mixBufferCount; + } + + public void SetRenderingTimeLimitPercent(uint percent) + { + Debug.Assert(percent <= 100); + + _renderingTimeLimitPercent = percent; + } + + public uint GetRenderingTimeLimit() + { + return _renderingTimeLimitPercent; + } + + public Memory<float> GetMixBuffer() + { + return _mixBuffer; + } + + public uint GetSampleCount() + { + return _sampleCount; + } + + public uint GetSampleRate() + { + return _sampleRate; + } + + public uint GetVoiceChannelCountMax() + { + return _voiceChannelCountMax; + } + + public bool IsActive() + { + return _isActive; + } + + private RendererSystemContext GetContext() + { + return new RendererSystemContext + { + ChannelCount = _manager.Processor.OutputDevices[_sessionId].GetChannelCount(), + BehaviourContext = _behaviourContext, + DepopBuffer = _depopBuffer, + MixBufferCount = GetMixBufferCount(), + SessionId = _sessionId, + UpsamplerManager = _upsamplerManager + }; + } + + public int GetSessionId() + { + return _sessionId; + } + + public static ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter) + { + BehaviourContext behaviourContext = new BehaviourContext(); + + behaviourContext.SetUserRevision(parameter.Revision); + + uint mixesCount = parameter.SubMixBufferCount + 1; + + uint memoryPoolCount = parameter.EffectCount + parameter.VoiceCount * Constants.VoiceWaveBufferCount; + + ulong size = 0; + + // Mix Buffers + size = WorkBufferAllocator.GetTargetSize<float>(size, parameter.SampleCount * (Constants.VoiceChannelCountMax + parameter.MixBufferCount), 0x10); + + // Upsampler workbuffer + size = WorkBufferAllocator.GetTargetSize<float>(size, Constants.TargetSampleCount * (Constants.VoiceChannelCountMax + parameter.MixBufferCount) * (parameter.SinkCount + parameter.SubMixBufferCount), 0x10); + + // Depop buffer + size = WorkBufferAllocator.GetTargetSize<float>(size, (ulong)BitUtils.AlignUp(parameter.MixBufferCount, Constants.BufferAlignment), Constants.BufferAlignment); + + // Voice + size = WorkBufferAllocator.GetTargetSize<VoiceState>(size, parameter.VoiceCount, VoiceState.Alignment); + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.VoiceCount, 0x10); + size = WorkBufferAllocator.GetTargetSize<VoiceChannelResource>(size, parameter.VoiceCount, VoiceChannelResource.Alignment); + size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align); + + // Mix + size = WorkBufferAllocator.GetTargetSize<MixState>(size, mixesCount, MixState.Alignment); + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.EffectCount * mixesCount, 0x10); + size = WorkBufferAllocator.GetTargetSize<int>(size, mixesCount, 0x10); + + if (behaviourContext.IsSplitterSupported()) + { + size += (ulong)BitUtils.AlignUp(NodeStates.GetWorkBufferSize((int)mixesCount) + EdgeMatrix.GetWorkBufferSize((int)mixesCount), 0x10); + } + + // Memory Pool + size = WorkBufferAllocator.GetTargetSize<MemoryPoolState>(size, memoryPoolCount, MemoryPoolState.Alignment); + + // Splitter + size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter); + + // DSP Voice + size = WorkBufferAllocator.GetTargetSize<VoiceUpdateState>(size, parameter.VoiceCount, VoiceUpdateState.Align); + + // Performance + if (parameter.PerformanceMetricFramesCount > 0) + { + ulong performanceMetricsPerFramesSize = PerformanceManager.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter, ref behaviourContext) * (parameter.PerformanceMetricFramesCount + 1) + 0xC; + + size += BitUtils.AlignUp(performanceMetricsPerFramesSize, Constants.PerformanceMetricsPerFramesSizeAlignment); + } + + return BitUtils.AlignUp(size, Constants.WorkBufferAlignment); + } + + public ResultCode QuerySystemEvent(out IWritableEvent systemEvent) + { + systemEvent = default; + + if (_executionMode == AudioRendererExecutionMode.Manual) + { + return ResultCode.UnsupportedOperation; + } + + systemEvent = _systemEvent; + + return ResultCode.Success; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_isActive) + { + Stop(); + } + + PoolMapper mapper = new PoolMapper(_processHandle, false); + mapper.Unmap(ref _dspMemoryPoolState); + + PoolMapper.ClearUsageState(_memoryPools); + + for (int i = 0; i < _memoryPoolCount; i++) + { + ref MemoryPoolState memoryPool = ref _memoryPools.Span[i]; + + if (memoryPool.IsMapped()) + { + mapper.Unmap(ref memoryPool); + } + } + + _manager.Unregister(this); + _terminationEvent.Dispose(); + _workBufferMemoryPin.Dispose(); + _workBufferRegion.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs new file mode 100644 index 00000000..ec847948 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/AudioRendererManager.cs @@ -0,0 +1,334 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using Ryujinx.Audio.Renderer.Dsp; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Logging; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.Threading; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// The audio renderer manager. + /// </summary> + public class AudioRendererManager : IDisposable + { + /// <summary> + /// Lock used for session allocation. + /// </summary> + private object _sessionLock = new object(); + + /// <summary> + /// Lock used to control the <see cref="AudioProcessor"/> running state. + /// </summary> + private object _audioProcessorLock = new object(); + + /// <summary> + /// The session ids allocation table. + /// </summary> + private int[] _sessionIds; + + /// <summary> + /// The events linked to each session. + /// </summary> + private IWritableEvent[] _sessionsSystemEvent; + + /// <summary> + /// The <see cref="AudioRenderSystem"/> sessions instances. + /// </summary> + private AudioRenderSystem[] _sessions; + + /// <summary> + /// The count of active sessions. + /// </summary> + private int _activeSessionCount; + + /// <summary> + /// The worker thread used to run <see cref="SendCommands"/>. + /// </summary> + private Thread _workerThread; + + /// <summary> + /// Indicate if the worker thread and <see cref="AudioProcessor"/> are running. + /// </summary> + private bool _isRunning; + + /// <summary> + /// The audio device driver to create audio outputs. + /// </summary> + private IHardwareDeviceDriver _deviceDriver; + + /// <summary> + /// The <see cref="AudioProcessor"/> instance associated to this manager. + /// </summary> + public AudioProcessor Processor { get; } + + /// <summary> + /// Create a new <see cref="AudioRendererManager"/>. + /// </summary> + public AudioRendererManager() + { + Processor = new AudioProcessor(); + _sessionIds = new int[Constants.AudioRendererSessionCountMax]; + _sessions = new AudioRenderSystem[Constants.AudioRendererSessionCountMax]; + _activeSessionCount = 0; + + for (int i = 0; i < _sessionIds.Length; i++) + { + _sessionIds[i] = i; + } + } + + /// <summary> + /// Initialize the <see cref="AudioRendererManager"/>. + /// </summary> + /// <param name="sessionSystemEvents">The events associated to each session.</param> + /// <param name="deviceDriver">The device driver to use to create audio outputs.</param> + public void Initialize(IWritableEvent[] sessionSystemEvents, IHardwareDeviceDriver deviceDriver) + { + _sessionsSystemEvent = sessionSystemEvents; + _deviceDriver = deviceDriver; + } + + /// <summary> + /// Get the work buffer size required by a session. + /// </summary> + /// <param name="parameter">The user configuration</param> + /// <returns>The work buffer size required by a session.</returns> + public static ulong GetWorkBufferSize(ref AudioRendererConfiguration parameter) + { + return AudioRenderSystem.GetWorkBufferSize(ref parameter); + } + + /// <summary> + /// Acquire a new session id. + /// </summary> + /// <returns>A new session id.</returns> + private int AcquireSessionId() + { + lock (_sessionLock) + { + int index = _activeSessionCount; + + Debug.Assert(index < _sessionIds.Length); + + int sessionId = _sessionIds[index]; + + _sessionIds[index] = -1; + + _activeSessionCount++; + + Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new renderer ({sessionId})"); + + return sessionId; + } + } + + /// <summary> + /// Release a given <paramref name="sessionId"/>. + /// </summary> + /// <param name="sessionId">The session id to release.</param> + private void ReleaseSessionId(int sessionId) + { + lock (_sessionLock) + { + Debug.Assert(_activeSessionCount > 0); + + int newIndex = --_activeSessionCount; + + _sessionIds[newIndex] = sessionId; + } + + Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered renderer ({sessionId})"); + } + + /// <summary> + /// Check if there is any audio renderer active. + /// </summary> + /// <returns>Returns true if there is any audio renderer active.</returns> + private bool HasAnyActiveRendererLocked() + { + foreach (AudioRenderSystem renderer in _sessions) + { + if (renderer != null) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Start the <see cref="AudioProcessor"/> and worker thread. + /// </summary> + private void StartLocked() + { + _isRunning = true; + + // TODO: virtual device mapping (IAudioDevice) + Processor.Start(_deviceDriver); + + _workerThread = new Thread(SendCommands) + { + Name = "AudioRendererManager.Worker" + }; + + _workerThread.Start(); + } + + /// <summary> + /// Stop the <see cref="AudioProcessor"/> and worker thread. + /// </summary> + private void StopLocked() + { + _isRunning = false; + + _workerThread.Join(); + Processor.Stop(); + + Logger.Info?.Print(LogClass.AudioRenderer, "Stopped audio renderer"); + } + + /// <summary> + /// Worker main function. This is used to dispatch audio renderer commands to the <see cref="AudioProcessor"/>. + /// </summary> + private void SendCommands() + { + Logger.Info?.Print(LogClass.AudioRenderer, "Starting audio renderer"); + Processor.Wait(); + + while (_isRunning) + { + lock (_sessionLock) + { + foreach(AudioRenderSystem renderer in _sessions) + { + renderer?.SendCommands(); + } + } + + Processor.Signal(); + Processor.Wait(); + } + } + + /// <summary> + /// Register a new <see cref="AudioRenderSystem"/>. + /// </summary> + /// <param name="renderer">The <see cref="AudioRenderSystem"/> to register.</param> + private void Register(AudioRenderSystem renderer) + { + lock (_sessionLock) + { + _sessions[renderer.GetSessionId()] = renderer; + } + + lock (_audioProcessorLock) + { + if (!_isRunning) + { + StartLocked(); + } + } + } + + /// <summary> + /// Unregister a new <see cref="AudioRenderSystem"/>. + /// </summary> + /// <param name="renderer">The <see cref="AudioRenderSystem"/> to unregister.</param> + internal void Unregister(AudioRenderSystem renderer) + { + lock (_sessionLock) + { + int sessionId = renderer.GetSessionId(); + + _sessions[renderer.GetSessionId()] = null; + + ReleaseSessionId(sessionId); + } + + lock (_audioProcessorLock) + { + if (_isRunning && !HasAnyActiveRendererLocked()) + { + StopLocked(); + } + } + } + + /// <summary> + /// Open a new <see cref="AudioRenderSystem"/> + /// </summary> + /// <param name="renderer">The new <see cref="AudioRenderSystem"/></param> + /// <param name="memoryManager">The memory manager that will be used for all guest memory operations.</param> + /// <param name="parameter">The user configuration</param> + /// <param name="appletResourceUserId">The applet resource user id of the application.</param> + /// <param name="workBufferAddress">The guest work buffer address.</param> + /// <param name="workBufferSize">The guest work buffer size.</param> + /// <param name="processHandle">The process handle of the application.</param> + /// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns> + public ResultCode OpenAudioRenderer(out AudioRenderSystem renderer, IVirtualMemoryManager memoryManager, ref AudioRendererConfiguration parameter, ulong appletResourceUserId, ulong workBufferAddress, ulong workBufferSize, uint processHandle) + { + int sessionId = AcquireSessionId(); + + AudioRenderSystem audioRenderer = new AudioRenderSystem(this, _sessionsSystemEvent[sessionId]); + + ResultCode result = audioRenderer.Initialize(ref parameter, processHandle, workBufferAddress, workBufferSize, sessionId, appletResourceUserId, memoryManager); + + if (result == ResultCode.Success) + { + renderer = audioRenderer; + + Register(renderer); + } + else + { + ReleaseSessionId(sessionId); + + renderer = null; + } + + return result; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + lock (_audioProcessorLock) + { + if (_isRunning) + { + StopLocked(); + } + } + + Processor.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs new file mode 100644 index 00000000..b31f9e9f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs @@ -0,0 +1,405 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Behaviour context. + /// </summary> + /// <remarks>This handles features based on the audio renderer revision provided by the user.</remarks> + public class BehaviourContext + { + /// <summary> + /// The base magic of the Audio Renderer revision. + /// </summary> + public const int BaseRevisionMagic = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('0' << 24); + + /// <summary> + /// REV1: first revision. + /// </summary> + public const int Revision1 = 1 << 24; + + /// <summary> + /// REV2: Added support for splitter and fix GC-ADPCM context not being provided to the DSP. + /// </summary> + /// <remarks>This was added in system update 2.0.0</remarks> + public const int Revision2 = 2 << 24; + + /// <summary> + /// REV3: Incremented the max pre-delay from 150 to 350 for the reverb command and removed the (unused) codec system. + /// </summary> + /// <remarks>This was added in system update 3.0.0</remarks> + public const int Revision3 = 3 << 24; + + /// <summary> + /// REV4: Added USB audio device support and incremented the rendering limit percent to 75%. + /// </summary> + /// <remarks>This was added in system update 4.0.0</remarks> + public const int Revision4 = 4 << 24; + + /// <summary> + /// REV5: <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>, <see cref="Parameter.VoiceInParameter.FlushWaveBufferCount"/> were added to voice. + /// A new performance frame format (version 2) was added with support for more information about DSP timing. + /// <see cref="Parameter.RendererInfoOutStatus"/> was added to supply the count of update done sent to the DSP. + /// A new version of the command estimator was added to address timing changes caused by the voice changes. + /// Additionally, the rendering limit percent was incremented to 80%. + /// + /// </summary> + /// <remarks>This was added in system update 6.0.0</remarks> + public const int Revision5 = 5 << 24; + + /// <summary> + /// REV6: This fixed a bug in the biquad filter command not clearing up <see cref="Dsp.State.BiquadFilterState"/> with <see cref="Effect.UsageState.New"/> usage state. + /// </summary> + /// <remarks>This was added in system update 6.1.0</remarks> + public const int Revision6 = 6 << 24; + + /// <summary> + /// REV7: Client side (finally) doesn't send all the mix client state to the server and can do partial updates. + /// </summary> + /// <remarks>This was added in system update 8.0.0</remarks> + public const int Revision7 = 7 << 24; + + /// <summary> + /// REV8: + /// Wavebuffer was changed to support more control over loop (you can now specify where to start and end a loop, and how many times to loop). + /// <see cref="Parameter.VoiceInParameter.SrcQuality"/> was added (see <see cref="Parameter.VoiceInParameter.SampleRateConversionQuality"/> for more info). + /// Final leftovers of the codec system were removed. + /// <see cref="Common.SampleFormat.PcmFloat"/> support was added. + /// A new version of the command estimator was added to address timing changes caused by the voice and command changes. + /// </summary> + /// <remarks>This was added in system update 9.0.0</remarks> + public const int Revision8 = 8 << 24; + + /// <summary> + /// Last revision supported by the implementation. + /// </summary> + public const int LastRevision = Revision8; + + /// <summary> + /// Target revision magic supported by the implementation. + /// </summary> + public const int ProcessRevision = BaseRevisionMagic + LastRevision; + + /// <summary> + /// Get the revision number from the revision magic. + /// </summary> + /// <param name="revision">The revision magic.</param> + /// <returns>The revision number.</returns> + public static int GetRevisionNumber(int revision) => (revision - BaseRevisionMagic) >> 24; + + /// <summary> + /// Current active revision. + /// </summary> + public int UserRevision { get; private set; } + + /// <summary> + /// Error storage. + /// </summary> + private ErrorInfo[] _errorInfos; + + /// <summary> + /// Current position in the <see cref="_errorInfos"/> array. + /// </summary> + private uint _errorIndex; + + /// <summary> + /// Current flags of the <see cref="BehaviourContext"/>. + /// </summary> + private ulong _flags; + + /// <summary> + /// Create a new instance of <see cref="BehaviourContext"/>. + /// </summary> + public BehaviourContext() + { + UserRevision = 0; + _errorInfos = new ErrorInfo[Constants.MaxErrorInfos]; + _errorIndex = 0; + } + + /// <summary> + /// Set the active revision. + /// </summary> + /// <param name="userRevision">The active revision.</param> + public void SetUserRevision(int userRevision) + { + UserRevision = userRevision; + } + + /// <summary> + /// Update flags of the <see cref="BehaviourContext"/>. + /// </summary> + /// <param name="flags">The new flags.</param> + public void UpdateFlags(ulong flags) + { + _flags = flags; + } + + /// <summary> + /// Check if a given revision is valid/supported. + /// </summary> + /// <param name="revision">The revision magic to check.</param> + /// <returns>Returns true if the given revision is valid/supported</returns> + public static bool CheckValidRevision(int revision) + { + return GetRevisionNumber(revision) <= GetRevisionNumber(ProcessRevision); + } + + /// <summary> + /// Check if the given revision is greater than or equal the supported revision. + /// </summary> + /// <param name="revision">The revision magic to check.</param> + /// <param name="supportedRevision">The revision magic of the supported revision.</param> + /// <returns>Returns true if the given revision is greater than or equal the supported revision.</returns> + public static bool CheckFeatureSupported(int revision, int supportedRevision) + { + int revA = GetRevisionNumber(revision); + int revB = GetRevisionNumber(supportedRevision); + + if (revA > LastRevision) + { + revA = 1; + } + + if (revB > LastRevision) + { + revB = 1; + } + + return revA >= revB; + } + + /// <summary> + /// Check if the memory pool mapping bypass flag is active. + /// </summary> + /// <returns>True if the memory pool mapping bypass flag is active.</returns> + public bool IsMemoryPoolForceMappingEnabled() + { + return (_flags & 1) != 0; + } + + /// <summary> + /// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP. + /// </summary> + /// <returns>True if if the audio renderer should fix it.</returns> + public bool IsAdpcmLoopContextBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2); + } + + /// <summary> + /// Check if the audio renderer should accept splitters. + /// </summary> + /// <returns>True if the audio renderer should accept splitters.</returns> + public bool IsSplitterSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2); + } + + /// <summary> + /// Check if the audio renderer should use a max pre-delay of 350 instead of 150. + /// </summary> + /// <returns>True if the max pre-delay must be 350.</returns> + public bool IsLongSizePreDelaySupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision3); + } + + /// <summary> + /// Check if the audio renderer should expose USB audio device. + /// </summary> + /// <returns>True if the audio renderer should expose USB audio device.</returns> + public bool IsAudioUsbDeviceOutputSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision4); + } + + /// <summary> + /// Get the percentage allocated to the audio renderer on the DSP for processing. + /// </summary> + /// <returns>The percentage allocated to the audio renderer on the DSP for processing.</returns> + public float GetAudioRendererProcessingTimeLimit() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 0.80f; + } + else if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision4)) + { + return 0.75f; + } + + return 0.70f; + } + + /// <summary> + /// Check if the audio render should support voice flushing. + /// </summary> + /// <returns>True if the audio render should support voice flushing.</returns> + public bool IsFlushVoiceWaveBuffersSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Check if the audio renderer should trust the user destination count in <see cref="Splitter.SplitterState.Update(Splitter.SplitterContext, ref Parameter.SplitterInParameter, ReadOnlySpan{byte})"/>. + /// </summary> + /// <returns>True if the audio renderer should trust the user destination count.</returns> + public bool IsSplitterBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Check if the audio renderer should supply the elapsed frame count to the user when updating. + /// </summary> + /// <returns>True if the audio renderer should supply the elapsed frame count to the user when updating.</returns> + public bool IsElapsedFrameCountSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Get the performance metric data format version. + /// </summary> + /// <returns>The performance metric data format version.</returns> + public uint GetPerformanceMetricsDataFormat() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 2; + } + else + { + return 1; + } + } + + /// <summary> + /// Check if the audio renderer should support <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>. + /// </summary> + /// <returns>True if the audio renderer should support <see cref="Parameter.VoiceInParameter.DecodingBehaviour"/>.</returns> + public bool IsDecodingBehaviourFlagSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5); + } + + /// <summary> + /// Check if the audio renderer should fix the biquad filter command not clearing up <see cref="Dsp.State.BiquadFilterState"/> with <see cref="Effect.UsageState.New"/> usage state. + /// </summary> + /// <returns>True if the biquad filter state should be cleared.</returns> + public bool IsBiquadFilterEffectStateClearBugFixed() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision6); + } + + /// <summary> + /// Check if the audio renderer should accept partial mix updates. + /// </summary> + /// <returns>True if the audio renderer should accept partial mix updates.</returns> + public bool IsMixInParameterDirtyOnlyUpdateSupported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision7); + } + + /// <summary> + /// Check if the audio renderer should use the new wavebuffer format. + /// </summary> + /// <returns>True if the audio renderer should use the new wavebuffer format.</returns> + public bool IsWaveBufferVersion2Supported() + { + return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision8); + } + + /// <summary> + /// Get the version of the <see cref="ICommandProcessingTimeEstimator"/>. + /// </summary> + /// <returns>The version of the <see cref="ICommandProcessingTimeEstimator"/>.</returns> + public int GetCommandProcessingTimeEstimatorVersion() + { + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision8)) + { + return 3; + } + + if (CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision5)) + { + return 2; + } + + return 1; + } + + /// <summary> + /// Append a new <see cref="ErrorInfo"/> to the error array. + /// </summary> + /// <param name="errorInfo">The new <see cref="ErrorInfo"/> to add.</param> + public void AppendError(ref ErrorInfo errorInfo) + { + Debug.Assert(errorInfo.ErrorCode == ResultCode.Success); + + if (_errorIndex <= Constants.MaxErrorInfos - 1) + { + _errorInfos[_errorIndex++] = errorInfo; + } + } + + /// <summary> + /// Copy the internal <see cref="ErrorInfo"/> array to the given <see cref="Span{ErrorInfo}"/> and output the count copied. + /// </summary> + /// <param name="errorInfos">The output <see cref="Span{ErrorInfo}"/>.</param> + /// <param name="errorCount">The output error count containing the count of <see cref="ErrorInfo"/> copied.</param> + public void CopyErrorInfo(Span<ErrorInfo> errorInfos, out uint errorCount) + { + if (errorInfos.Length != Constants.MaxErrorInfos) + { + throw new ArgumentException("Invalid size of errorInfos span!"); + } + + errorCount = Math.Min(_errorIndex, Constants.MaxErrorInfos); + + for (int i = 0; i < Constants.MaxErrorInfos; i++) + { + if (i < errorCount) + { + errorInfos[i] = _errorInfos[i]; + } + else + { + errorInfos[i] = new ErrorInfo + { + ErrorCode = 0, + ExtraErrorInfo = 0 + }; + } + } + } + + /// <summary> + /// Clear the <see cref="ErrorInfo"/> array. + /// </summary> + public void ClearError() + { + _errorIndex = 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs new file mode 100644 index 00000000..ca37e090 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs @@ -0,0 +1,484 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using Ryujinx.Audio.Renderer.Server.Voice; +using System; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// An API to generate commands and aggregate them into a <see cref="CommandList"/>. + /// </summary> + public class CommandBuffer + { + /// <summary> + /// The command processing time estimator in use. + /// </summary> + private ICommandProcessingTimeEstimator _commandProcessingTimeEstimator; + + /// <summary> + /// The estimated total processing time. + /// </summary> + public ulong EstimatedProcessingTime { get; set; } + + /// <summary> + /// The command list that is populated by the <see cref="CommandBuffer"/>. + /// </summary> + public CommandList CommandList { get; } + + /// <summary> + /// Create a new <see cref="CommandBuffer"/>. + /// </summary> + /// <param name="commandList">The command list that will store the generated commands.</param> + /// <param name="commandProcessingTimeEstimator">The command processing time estimator to use.</param> + public CommandBuffer(CommandList commandList, ICommandProcessingTimeEstimator commandProcessingTimeEstimator) + { + CommandList = commandList; + EstimatedProcessingTime = 0; + _commandProcessingTimeEstimator = commandProcessingTimeEstimator; + } + + /// <summary> + /// Add a new generated command to the <see cref="CommandList"/>. + /// </summary> + /// <param name="command">The command to add.</param> + private void AddCommand(ICommand command) + { + EstimatedProcessingTime += command.EstimatedProcessingTime; + + CommandList.AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="ClearMixBufferCommand"/>. + /// </summary> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateClearMixBuffer(int nodeId) + { + ClearMixBufferCommand command = new ClearMixBufferCommand(nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="DepopPrepareCommand"/>. + /// </summary> + /// <param name="state">The voice state associated.</param> + /// <param name="depopBuffer">The depop buffer.</param> + /// <param name="bufferCount">The buffer count.</param> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="wasPlaying">Set to true if the voice was playing previously.</param> + public void GenerateDepopPrepare(Memory<VoiceUpdateState> state, Memory<float> depopBuffer, uint bufferCount, uint bufferOffset, int nodeId, bool wasPlaying) + { + DepopPrepareCommand command = new DepopPrepareCommand(state, depopBuffer, bufferCount, bufferOffset, nodeId, wasPlaying); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="PerformanceCommand"/>. + /// </summary> + /// <param name="performanceEntryAddresses">The <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="type">The performance operation to perform.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePerformance(ref PerformanceEntryAddresses performanceEntryAddresses, PerformanceCommand.Type type, int nodeId) + { + PerformanceCommand command = new PerformanceCommand(ref performanceEntryAddresses, type, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="VolumeRampCommand"/>. + /// </summary> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="bufferIndex">The index of the mix buffer to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateVolumeRamp(float previousVolume, float volume, uint bufferIndex, int nodeId) + { + VolumeRampCommand command = new VolumeRampCommand(previousVolume, volume, bufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DataSourceVersion2Command"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDataSourceVersion2(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + DataSourceVersion2Command command = new DataSourceVersion2Command(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="PcmInt16DataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePcmInt16DataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + PcmInt16DataSourceCommandVersion1 command = new PcmInt16DataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="PcmFloatDataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="channelIndex">The target channel index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GeneratePcmFloatDataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, ushort channelIndex, int nodeId) + { + PcmFloatDataSourceCommandVersion1 command = new PcmFloatDataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, channelIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="AdpcmDataSourceCommandVersion1"/>. + /// </summary> + /// <param name="voiceState">The <see cref="VoiceState"/> to generate the command from.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="outputBufferIndex">The output buffer index to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateAdpcmDataSourceVersion1(ref VoiceState voiceState, Memory<VoiceUpdateState> state, ushort outputBufferIndex, int nodeId) + { + AdpcmDataSourceCommandVersion1 command = new AdpcmDataSourceCommandVersion1(ref voiceState, state, outputBufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="BiquadFilterCommand"/>. + /// </summary> + /// <param name="baseIndex">The base index of the input and output buffer.</param> + /// <param name="filter">The biquad filter parameter.</param> + /// <param name="biquadFilterStateMemory">The biquad state.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="needInitialization">Set to true if the biquad filter state needs to be initialized.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateBiquadFilter(int baseIndex, ref BiquadFilterParameter filter, Memory<BiquadFilterState> biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, bool needInitialization, int nodeId) + { + BiquadFilterCommand command = new BiquadFilterCommand(baseIndex, ref filter, biquadFilterStateMemory, inputBufferOffset, outputBufferOffset, needInitialization, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixRampGroupedCommand"/>. + /// </summary> + /// <param name="mixBufferCount">The mix buffer count.</param> + /// <param name="inputBufferIndex">The base input index.</param> + /// <param name="outputBufferIndex">The base output index.</param> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span<float> previousVolume, Span<float> volume, Memory<VoiceUpdateState> state, int nodeId) + { + MixRampGroupedCommand command = new MixRampGroupedCommand(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixRampCommand"/>. + /// </summary> + /// <param name="previousVolume">The previous volume.</param> + /// <param name="volume">The new volume.</param> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="lastSampleIndex">The index in the <see cref="VoiceUpdateState.LastSamples"/> array to store the ramped sample.</param> + /// <param name="state">The <see cref="VoiceUpdateState"/> to generate the command from.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateMixRamp(float previousVolume, float volume, uint inputBufferIndex, uint outputBufferIndex, int lastSampleIndex, Memory<VoiceUpdateState> state, int nodeId) + { + MixRampCommand command = new MixRampCommand(previousVolume, volume, inputBufferIndex, outputBufferIndex, lastSampleIndex, state, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="DepopForMixBuffersCommand"/>. + /// </summary> + /// <param name="depopBuffer">The depop buffer.</param> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="bufferCount">The buffer count.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="sampleRate">The target sample rate in use.</param> + public void GenerateDepopForMixBuffersCommand(Memory<float> depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate) + { + DepopForMixBuffersCommand command = new DepopForMixBuffersCommand(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="CopyMixBufferCommand"/>. + /// </summary> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateCopyMixBuffer(uint inputBufferIndex, uint outputBufferIndex, int nodeId) + { + CopyMixBufferCommand command = new CopyMixBufferCommand(inputBufferIndex, outputBufferIndex, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="MixCommand"/>. + /// </summary> + /// <param name="inputBufferIndex">The input buffer index.</param> + /// <param name="outputBufferIndex">The output buffer index.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="volume">The mix volume.</param> + public void GenerateMix(uint inputBufferIndex, uint outputBufferIndex, int nodeId, float volume) + { + MixCommand command = new MixCommand(inputBufferIndex, outputBufferIndex, nodeId, volume); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Generate a new <see cref="ReverbCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The reverb parameter.</param> + /// <param name="state">The reverb state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + /// <param name="isLongSizePreDelaySupported">If set to true, the long size pre-delay is supported.</param> + public void GenerateReverbEffect(uint bufferOffset, ReverbParameter parameter, Memory<ReverbState> state, bool isEnabled, CpuAddress workBuffer, int nodeId, bool isLongSizePreDelaySupported) + { + if (parameter.IsChannelCountValid()) + { + ReverbCommand command = new ReverbCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId, isLongSizePreDelaySupported); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="Reverb3dCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The reverb 3d parameter.</param> + /// <param name="state">The reverb 3d state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateReverb3dEffect(uint bufferOffset, Reverb3dParameter parameter, Memory<Reverb3dState> state, bool isEnabled, CpuAddress workBuffer, int nodeId) + { + if (parameter.IsChannelCountValid()) + { + Reverb3dCommand command = new Reverb3dCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + + /// <summary> + /// Generate a new <see cref="DelayCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="parameter">The delay parameter.</param> + /// <param name="state">The delay state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="workBuffer">The work buffer to use for processing.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDelayEffect(uint bufferOffset, DelayParameter parameter, Memory<DelayState> state, bool isEnabled, CpuAddress workBuffer, int nodeId) + { + if (parameter.IsChannelCountValid()) + { + DelayCommand command = new DelayCommand(bufferOffset, parameter, state, isEnabled, workBuffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="AuxiliaryBufferCommand"/>. + /// </summary> + /// <param name="bufferOffset">The target buffer offset.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="state">The aux state.</param> + /// <param name="isEnabled">Set to true if the effect should be active.</param> + /// <param name="countMax">The limit of the circular buffer.</param> + /// <param name="outputBuffer">The guest address of the output buffer.</param> + /// <param name="inputBuffer">The guest address of the input buffer.</param> + /// <param name="updateCount">The count to add on the offset after write/read operations.</param> + /// <param name="writeOffset">The write offset.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateAuxEffect(uint bufferOffset, byte inputBufferOffset, byte outputBufferOffset, ref AuxiliaryBufferAddresses state, bool isEnabled, uint countMax, CpuAddress outputBuffer, CpuAddress inputBuffer, uint updateCount, uint writeOffset, int nodeId) + { + if (state.SendBufferInfoBase != 0 && state.ReturnBufferInfoBase != 0) + { + AuxiliaryBufferCommand command = new AuxiliaryBufferCommand(bufferOffset, inputBufferOffset, outputBufferOffset, ref state, isEnabled, countMax, outputBuffer, inputBuffer, updateCount, writeOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } + + /// <summary> + /// Generate a new <see cref="VolumeCommand"/>. + /// </summary> + /// <param name="volume">The target volume to apply.</param> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateVolume(float volume, uint bufferOffset, int nodeId) + { + VolumeCommand command = new VolumeCommand(volume, bufferOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="CircularBufferSinkCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="sink">The <see cref="BaseSink"/> of the circular buffer.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateCircularBuffer(uint bufferOffset, CircularBufferSink sink, int nodeId) + { + CircularBufferSinkCommand command = new CircularBufferSinkCommand(bufferOffset, ref sink.Parameter, ref sink.CircularBufferAddressInfo, sink.CurrentWriteOffset, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DownMixSurroundToStereoCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="inputBufferOffset">The input buffer offset.</param> + /// <param name="outputBufferOffset">The output buffer offset.</param> + /// <param name="downMixParameter">The downmixer parameters to use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDownMixSurroundToStereo(uint bufferOffset, Span<byte> inputBufferOffset, Span<byte> outputBufferOffset, ReadOnlySpan<float> downMixParameter, int nodeId) + { + DownMixSurroundToStereoCommand command = new DownMixSurroundToStereoCommand(bufferOffset, inputBufferOffset, outputBufferOffset, downMixParameter, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="UpsampleCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="upsampler">The <see cref="UpsamplerState"/> associated.</param> + /// <param name="inputCount">The total input count.</param> + /// <param name="inputBufferOffset">The input buffer mix offset.</param> + /// <param name="bufferCountPerSample">The buffer count per sample.</param> + /// <param name="sampleCount">The source sample count.</param> + /// <param name="sampleRate">The source sample rate.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateUpsample(uint bufferOffset, UpsamplerState upsampler, uint inputCount, Span<byte> inputBufferOffset, uint bufferCountPerSample, uint sampleCount, uint sampleRate, int nodeId) + { + UpsampleCommand command = new UpsampleCommand(bufferOffset, upsampler, inputCount, inputBufferOffset, bufferCountPerSample, sampleCount, sampleRate, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + + /// <summary> + /// Create a new <see cref="DeviceSinkCommand"/>. + /// </summary> + /// <param name="bufferOffset">The offset of the mix buffer.</param> + /// <param name="sink">The <see cref="BaseSink"/> of the device sink.</param> + /// <param name="sessionId">The current audio renderer session id.</param> + /// <param name="buffer">The mix buffer in use.</param> + /// <param name="nodeId">The node id associated to this command.</param> + public void GenerateDeviceSink(uint bufferOffset, DeviceSink sink, int sessionId, Memory<float> buffer, int nodeId) + { + DeviceSinkCommand command = new DeviceSinkCommand(bufferOffset, sink, sessionId, buffer, nodeId); + + command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command); + + AddCommand(command); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs new file mode 100644 index 00000000..d2499c1d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs @@ -0,0 +1,940 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Server.Voice; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + public class CommandGenerator + { + private CommandBuffer _commandBuffer; + private RendererSystemContext _rendererContext; + private VoiceContext _voiceContext; + private MixContext _mixContext; + private EffectContext _effectContext; + private SinkContext _sinkContext; + private SplitterContext _splitterContext; + private PerformanceManager _performanceManager; + + public CommandGenerator(CommandBuffer commandBuffer, RendererSystemContext rendererContext, VoiceContext voiceContext, MixContext mixContext, EffectContext effectContext, SinkContext sinkContext, SplitterContext splitterContext, PerformanceManager performanceManager) + { + _commandBuffer = commandBuffer; + _rendererContext = rendererContext; + _voiceContext = voiceContext; + _mixContext = mixContext; + _effectContext = effectContext; + _sinkContext = sinkContext; + _splitterContext = splitterContext; + _performanceManager = performanceManager; + + _commandBuffer.GenerateClearMixBuffer(Constants.InvalidNodeId); + } + + private void GenerateDataSource(ref VoiceState voiceState, Memory<VoiceUpdateState> dspState, int channelIndex) + { + if (voiceState.MixId != Constants.UnusedMixId) + { + ref MixState mix = ref _mixContext.GetState(voiceState.MixId); + + _commandBuffer.GenerateDepopPrepare(dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); + } + else if (voiceState.SplitterId != Constants.UnusedSplitterId) + { + int destinationId = 0; + + while (true) + { + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState mix = ref _mixContext.GetState(mixId); + + _commandBuffer.GenerateDepopPrepare(dspState, + _rendererContext.DepopBuffer, + mix.BufferCount, + mix.BufferOffset, + voiceState.NodeId, + voiceState.WasPlaying); + + destination.MarkAsNeedToUpdateInternalState(); + } + } + } + } + + if (!voiceState.WasPlaying) + { + Debug.Assert(voiceState.SampleFormat != SampleFormat.Adpcm || channelIndex == 0); + + if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported()) + { + _commandBuffer.GenerateDataSourceVersion2(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + } + else + { + switch (voiceState.SampleFormat) + { + case SampleFormat.PcmInt16: + _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + break; + case SampleFormat.PcmFloat: + _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + (ushort)channelIndex, + voiceState.NodeId); + break; + case SampleFormat.Adpcm: + _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState, + dspState, + (ushort)_rendererContext.MixBufferCount, + voiceState.NodeId); + break; + default: + throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}"); + } + } + } + } + + private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId) + { + for (int i = 0; i < voiceState.BiquadFilters.Length; i++) + { + ref BiquadFilterParameter filter = ref voiceState.BiquadFilters[i]; + + if (filter.Enable) + { + Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state).Slice(VoiceUpdateState.BiquadStateOffset, VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount); + + Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory); + + _commandBuffer.GenerateBiquadFilter(baseIndex, + ref filter, + stateMemory.Slice(i, 1), + bufferOffset, + bufferOffset, + !voiceState.BiquadFilterNeedInitialization[i], + nodeId); + } + } + } + + private void GenerateVoiceMix(Span<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId) + { + if (bufferCount > Constants.VoiceChannelCountMax) + { + _commandBuffer.GenerateMixRampGrouped(bufferCount, + bufferIndex, + bufferOffset, + previousMixVolumes, + mixVolumes, + state, + nodeId); + } + else + { + for (int i = 0; i < bufferCount; i++) + { + float previousMixVolume = previousMixVolumes[i]; + float mixVolume = mixVolumes[i]; + + if (mixVolume != 0.0f || previousMixVolume != 0.0f) + { + _commandBuffer.GenerateMixRamp(previousMixVolume, + mixVolume, + bufferIndex, + bufferOffset + (uint)i, + i, + state, + nodeId); + } + } + } + } + + private void GenerateVoice(ref VoiceState voiceState) + { + int nodeId = voiceState.NodeId; + uint channelsCount = voiceState.ChannelsCount; + + for (int channelIndex = 0; channelIndex < channelsCount; channelIndex++) + { + Memory<VoiceUpdateState> dspStateMemory = _voiceContext.GetUpdateStateForDsp(voiceState.ChannelResourceIds[channelIndex]); + + ref VoiceChannelResource channelResource = ref _voiceContext.GetChannelResource(voiceState.ChannelResourceIds[channelIndex]); + + PerformanceDetailType dataSourceDetailType = PerformanceDetailType.Adpcm; + + if (voiceState.SampleFormat == SampleFormat.PcmInt16) + { + dataSourceDetailType = PerformanceDetailType.PcmInt16; + } + else if (voiceState.SampleFormat == SampleFormat.PcmFloat) + { + dataSourceDetailType = PerformanceDetailType.PcmFloat; + } + + bool performanceInitialized = false; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, dataSourceDetailType, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateDataSource(ref voiceState, dspStateMemory, channelIndex); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + if (voiceState.WasPlaying) + { + voiceState.PreviousVolume = 0.0f; + } + else if (voiceState.HasAnyDestination()) + { + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.BiquadFilter, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateBiquadFilterForVoice(ref voiceState, dspStateMemory, (int)_rendererContext.MixBufferCount, channelIndex, nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume, + voiceState.Volume, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + voiceState.PreviousVolume = voiceState.Volume; + + if (voiceState.MixId == Constants.UnusedMixId) + { + if (voiceState.SplitterId != Constants.UnusedSplitterId) + { + int destinationId = channelIndex; + + while (true) + { + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + destinationId += (int)channelsCount; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState mix = ref _mixContext.GetState(mixId); + + GenerateVoiceMix(destination.MixBufferVolume, + destination.PreviousMixBufferVolume, + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + destination.MarkAsNeedToUpdateInternalState(); + } + } + } + } + } + else + { + ref MixState mix = ref _mixContext.GetState(voiceState.MixId); + + performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateVoiceMix(channelResource.Mix.ToSpan(), + channelResource.PreviousMix.ToSpan(), + dspStateMemory, + mix.BufferOffset, + mix.BufferCount, + _rendererContext.MixBufferCount + (uint)channelIndex, + nodeId); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + channelResource.UpdateState(); + } + + for (int i = 0; i < voiceState.BiquadFilterNeedInitialization.Length; i++) + { + voiceState.BiquadFilterNeedInitialization[i] = voiceState.BiquadFilters[i].Enable; + } + } + } + } + + public void GenerateVoices() + { + for (int i = 0; i < _voiceContext.GetCount(); i++) + { + ref VoiceState sortedState = ref _voiceContext.GetSortedState(i); + + if (!sortedState.ShouldSkip() && sortedState.UpdateForCommandGeneration(_voiceContext)) + { + int nodeId = sortedState.NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Voice, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateVoice(ref sortedState); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + + _splitterContext.UpdateInternalState(); + } + + public void GeneratePerformance(ref PerformanceEntryAddresses performanceEntryAddresses, PerformanceCommand.Type type, int nodeId) + { + _commandBuffer.GeneratePerformance(ref performanceEntryAddresses, type, nodeId); + } + + private void GenerateBufferMixerEffect(int bufferOffset, BufferMixEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.BufferMix); + + if (effect.IsEnabled) + { + for (int i = 0; i < effect.Parameter.MixesCount; i++) + { + if (effect.Parameter.Volumes[i] != 0.0f) + { + _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i], + (uint)bufferOffset + effect.Parameter.Output[i], + nodeId, + effect.Parameter.Volumes[i]); + } + } + } + } + + private void GenerateAuxEffect(uint bufferOffset, AuxiliaryBufferEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.AuxiliaryBuffer); + + if (effect.IsEnabled) + { + effect.GetWorkBuffer(0); + effect.GetWorkBuffer(1); + } + + if (effect.State.SendBufferInfoBase != 0 && effect.State.ReturnBufferInfoBase != 0) + { + int i = 0; + uint writeOffset = 0; + for (uint channelIndex = effect.Parameter.ChannelCount; channelIndex != 0; channelIndex--) + { + uint newUpdateCount = writeOffset + _commandBuffer.CommandList.SampleCount; + + uint updateCount; + + if ((channelIndex - 1) != 0) + { + updateCount = 0; + } + else + { + updateCount = newUpdateCount; + } + + _commandBuffer.GenerateAuxEffect(bufferOffset, + effect.Parameter.Input[i], + effect.Parameter.Output[i], + ref effect.State, + effect.IsEnabled, + effect.Parameter.BufferStorageSize, + effect.State.SendBufferInfoBase, + effect.State.ReturnBufferInfoBase, + updateCount, + writeOffset, + nodeId); + + writeOffset = newUpdateCount; + + i++; + } + } + } + + private void GenerateDelayEffect(uint bufferOffset, DelayEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.Delay); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateDelayEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId); + } + + private void GenerateReverbEffect(uint bufferOffset, ReverbEffect effect, int nodeId, bool isLongSizePreDelaySupported) + { + Debug.Assert(effect.Type == EffectType.Reverb); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateReverbEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId, isLongSizePreDelaySupported); + } + + private void GenerateReverb3dEffect(uint bufferOffset, Reverb3dEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.Reverb3d); + + ulong workBuffer = effect.GetWorkBuffer(-1); + + _commandBuffer.GenerateReverb3dEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId); + } + + private void GenerateBiquadFilterEffect(uint bufferOffset, BiquadFilterEffect effect, int nodeId) + { + Debug.Assert(effect.Type == EffectType.BiquadFilter); + + if (effect.IsEnabled) + { + bool needInitialization = effect.Parameter.Status == UsageState.Invalid || + (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed()); + + BiquadFilterParameter parameter = new BiquadFilterParameter(); + + parameter.Enable = true; + effect.Parameter.Denominator.ToSpan().CopyTo(parameter.Denominator.ToSpan()); + effect.Parameter.Numerator.ToSpan().CopyTo(parameter.Numerator.ToSpan()); + + for (int i = 0; i < effect.Parameter.ChannelCount; i++) + { + _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1), + effect.Parameter.Input[i], + effect.Parameter.Output[i], + needInitialization, + nodeId); + } + } + else + { + for (int i = 0; i < effect.Parameter.ChannelCount; i++) + { + uint inputBufferIndex = bufferOffset + effect.Parameter.Input[i]; + uint outputBufferIndex = bufferOffset + effect.Parameter.Output[i]; + + // If the input and output isn't the same, generate a command. + if (inputBufferIndex != outputBufferIndex) + { + _commandBuffer.GenerateCopyMixBuffer(inputBufferIndex, outputBufferIndex, nodeId); + } + } + } + } + + private void GenerateEffect(ref MixState mix, BaseEffect effect) + { + int nodeId = mix.NodeId; + + bool isFinalMix = mix.MixId == Constants.FinalMixId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(), + isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + switch (effect.Type) + { + case EffectType.BufferMix: + GenerateBufferMixerEffect((int)mix.BufferOffset, (BufferMixEffect)effect, nodeId); + break; + case EffectType.AuxiliaryBuffer: + GenerateAuxEffect(mix.BufferOffset, (AuxiliaryBufferEffect)effect, nodeId); + break; + case EffectType.Delay: + GenerateDelayEffect(mix.BufferOffset, (DelayEffect)effect, nodeId); + break; + case EffectType.Reverb: + GenerateReverbEffect(mix.BufferOffset, (ReverbEffect)effect, nodeId, mix.IsLongSizePreDelaySupported); + break; + case EffectType.Reverb3d: + GenerateReverb3dEffect(mix.BufferOffset, (Reverb3dEffect)effect, nodeId); + break; + case EffectType.BiquadFilter: + GenerateBiquadFilterEffect(mix.BufferOffset, (BiquadFilterEffect)effect, nodeId); + break; + default: + throw new NotImplementedException($"Unsupported effect type {effect.Type}"); + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + + effect.UpdateForCommandGeneration(); + } + + private void GenerateEffects(ref MixState mix) + { + ReadOnlySpan<int> effectProcessingOrderArray = mix.EffectProcessingOrderArray; + + Debug.Assert(_effectContext.GetCount() == 0 || !effectProcessingOrderArray.IsEmpty); + + for (int i = 0; i < _effectContext.GetCount(); i++) + { + int effectOrder = effectProcessingOrderArray[i]; + + if (effectOrder == Constants.InvalidProcessingOrder) + { + break; + } + + // BaseEffect is a class, we don't need to pass it by ref + BaseEffect effect = _effectContext.GetEffect(effectOrder); + + Debug.Assert(effect.Type != EffectType.Invalid); + Debug.Assert(effect.MixId == mix.MixId); + + if (!effect.ShouldSkip()) + { + GenerateEffect(ref mix, effect); + } + } + } + + private void GenerateMix(ref MixState mix) + { + if (mix.HasAnyDestination()) + { + Debug.Assert(mix.DestinationMixId != Constants.UnusedMixId || mix.DestinationSplitterId != Constants.UnusedSplitterId); + + if (mix.DestinationMixId == Constants.UnusedMixId) + { + if (mix.DestinationSplitterId != Constants.UnusedSplitterId) + { + int destinationId = 0; + + while (true) + { + int destinationIndex = destinationId++; + + Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex); + + if (destinationSpan.IsEmpty) + { + break; + } + + ref SplitterDestination destination = ref destinationSpan[0]; + + if (destination.IsConfigured()) + { + int mixId = destination.DestinationId; + + if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt) + { + ref MixState destinationMix = ref _mixContext.GetState(mixId); + + uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount); + + for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) + { + float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex); + + if (volume != 0.0f) + { + _commandBuffer.GenerateMix(inputBufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); + } + } + } + } + } + } + } + else + { + ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId); + + for (uint bufferIndex = 0; bufferIndex < mix.BufferCount; bufferIndex++) + { + for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++) + { + float volume = mix.Volume * mix.GetMixBufferVolume((int)bufferIndex, (int)bufferDestinationIndex); + + if (volume != 0.0f) + { + _commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex, + destinationMix.BufferOffset + bufferDestinationIndex, + mix.NodeId, + volume); + } + } + } + } + } + } + + private void GenerateSubMix(ref MixState subMix) + { + _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, + subMix.BufferOffset, + subMix.BufferCount, + subMix.NodeId, + subMix.SampleRate); + + GenerateEffects(ref subMix); + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + int nodeId = subMix.NodeId; + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateMix(ref subMix); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + public void GenerateSubMixes() + { + for (int id = 0; id < _mixContext.GetCount(); id++) + { + ref MixState sortedState = ref _mixContext.GetSortedState(id); + + if (sortedState.IsUsed && sortedState.MixId != Constants.FinalMixId) + { + int nodeId = sortedState.NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.SubMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateSubMix(ref sortedState); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + } + + private void GenerateFinalMix() + { + ref MixState finalMix = ref _mixContext.GetFinalState(); + + _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer, + finalMix.BufferOffset, + finalMix.BufferCount, + finalMix.NodeId, + finalMix.SampleRate); + + GenerateEffects(ref finalMix); + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + int nodeId = finalMix.NodeId; + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.FinalMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + // Only generate volume command if the volume isn't 100%. + if (finalMix.Volume != 1.0f) + { + for (uint bufferIndex = 0; bufferIndex < finalMix.BufferCount; bufferIndex++) + { + bool performanceSubInitialized = false; + + if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.FinalMix, nodeId)) + { + performanceSubInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + _commandBuffer.GenerateVolume(finalMix.Volume, + finalMix.BufferOffset + bufferIndex, + nodeId); + + if (performanceSubInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + public void GenerateFinalMixes() + { + int nodeId = _mixContext.GetFinalState().NodeId; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + bool performanceInitialized = false; + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.FinalMix, nodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId); + } + + GenerateFinalMix(); + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId); + } + } + + private void GenerateCircularBuffer(CircularBufferSink sink, ref MixState finalMix) + { + _commandBuffer.GenerateCircularBuffer(finalMix.BufferOffset, sink, Constants.InvalidNodeId); + } + + private void GenerateDevice(DeviceSink sink, ref MixState finalMix) + { + if (_commandBuffer.CommandList.SampleRate != 48000 && sink.UpsamplerState == null) + { + sink.UpsamplerState = _rendererContext.UpsamplerManager.Allocate(); + } + + bool useCustomDownMixingCommand = _rendererContext.ChannelCount == 2 && sink.Parameter.DownMixParameterEnabled; + + if (useCustomDownMixingCommand) + { + _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, + sink.Parameter.Input.ToSpan(), + sink.Parameter.Input.ToSpan(), + sink.DownMixCoefficients, + Constants.InvalidNodeId); + } + // NOTE: We do the downmixing at the DSP level as it's easier that way. + else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6) + { + _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset, + sink.Parameter.Input.ToSpan(), + sink.Parameter.Input.ToSpan(), + Constants.DefaultSurroundToStereoCoefficients, + Constants.InvalidNodeId); + } + + CommandList commandList = _commandBuffer.CommandList; + + if (sink.UpsamplerState != null) + { + _commandBuffer.GenerateUpsample(finalMix.BufferOffset, + sink.UpsamplerState, + sink.Parameter.InputCount, + sink.Parameter.Input.ToSpan(), + commandList.BufferCount, + commandList.SampleCount, + commandList.SampleRate, + Constants.InvalidNodeId); + } + + _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset, + sink, + _rendererContext.SessionId, + commandList.Buffers, + Constants.InvalidNodeId); + } + + private void GenerateSink(BaseSink sink, ref MixState finalMix) + { + bool performanceInitialized = false; + + PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses(); + + if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Sink, sink.NodeId)) + { + performanceInitialized = true; + + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, sink.NodeId); + } + + if (!sink.ShouldSkip) + { + switch (sink.Type) + { + case SinkType.CircularBuffer: + GenerateCircularBuffer((CircularBufferSink)sink, ref finalMix); + break; + case SinkType.Device: + GenerateDevice((DeviceSink)sink, ref finalMix); + break; + default: + throw new NotImplementedException($"Unsupported sink type {sink.Type}"); + } + + sink.UpdateForCommandGeneration(); + } + + if (performanceInitialized) + { + GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, sink.NodeId); + } + } + + public void GenerateSinks() + { + ref MixState finalMix = ref _mixContext.GetFinalState(); + + for (int i = 0; i < _sinkContext.GetCount(); i++) + { + // BaseSink is a class, we don't need to pass it by ref + BaseSink sink = _sinkContext.GetSink(i); + + if (sink.IsUsed) + { + GenerateSink(sink, ref finalMix); + } + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs new file mode 100644 index 00000000..81d3b57b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs @@ -0,0 +1,180 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 1. + /// </summary> + public class CommandProcessingTimeEstimatorVersion1 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion1(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + return 1454; + } + + public uint Estimate(ClearMixBufferCommand command) + { + return (uint)(_sampleCount * 0.83f * _bufferCount * 1.2f); + } + + public uint Estimate(BiquadFilterCommand command) + { + return (uint)(_sampleCount * 58.0f * 1.2f); + } + + public uint Estimate(MixRampGroupedCommand command) + { + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * 14.4f * 1.2f * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + return (uint)(_sampleCount * 14.4f * 1.2f); + } + + public uint Estimate(DepopPrepareCommand command) + { + return 1080; + } + + public uint Estimate(VolumeRampCommand command) + { + return (uint)(_sampleCount * 9.8f * 1.2f); + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + return (uint)(command.Pitch * 0.25f * 1.2f); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + return (uint)(command.Pitch * 0.46f * 1.2f); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + return (uint)(_sampleCount * 8.9f * command.MixBufferCount); + } + + public uint Estimate(CopyMixBufferCommand command) + { + // NOTE: Nintendo returns 0 here for some reasons even if it will generate a command like that on version 1.. maybe a mistake? + return 0; + } + + public uint Estimate(MixCommand command) + { + return (uint)(_sampleCount * 10.0f * 1.2f); + } + + public uint Estimate(DelayCommand command) + { + return (uint)(_sampleCount * command.Parameter.ChannelCount * 202.5f); + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(command.Parameter.IsChannelCountValid()); + + if (command.Enabled) + { + return (uint)(750 * _sampleCount * command.Parameter.ChannelCount * 1.2f); + } + + return 0; + } + + public uint Estimate(Reverb3dCommand command) + { + if (command.Enabled) + { + return (uint)(530 * _sampleCount * command.Parameter.ChannelCount * 1.2f); + } + + return 0; + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + if (command.Enabled) + { + return 15956; + } + + return 3765; + } + + public uint Estimate(VolumeCommand command) + { + return (uint)(_sampleCount * 8.8f * 1.2f); + } + + public uint Estimate(CircularBufferSinkCommand command) + { + return 55; + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + return 16108; + } + + public uint Estimate(UpsampleCommand command) + { + return 357915; + } + + public uint Estimate(DeviceSinkCommand command) + { + return 10042; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + return 0; + } + + public uint Estimate(DataSourceVersion2Command command) + { + return 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs new file mode 100644 index 00000000..0f4fe3c2 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs @@ -0,0 +1,544 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 2. (added with REV5) + /// </summary> + public class CommandProcessingTimeEstimatorVersion2 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion2(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)489.35f; + } + + return (uint)491.18f; + } + + public uint Estimate(ClearMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 668.8f; + float baseCost = 193.2f; + + if (_sampleCount == 160) + { + costPerBuffer = 260.4f; + baseCost = 139.65f; + } + + return (uint)(baseCost + costPerBuffer * _bufferCount); + } + + public uint Estimate(BiquadFilterCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)4813.2f; + } + + return (uint)6915.4f; + } + + public uint Estimate(MixRampGroupedCommand command) + { + const float costPerSample = 7.245f; + + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * costPerSample * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1859.0f; + } + + return (uint)2286.1f; + } + + public uint Estimate(DepopPrepareCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)306.62f; + } + + return (uint)293.22f; + } + + public uint Estimate(VolumeRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1403.9f; + } + + return (uint)1884.3f; + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 1195.5f; + float baseCost = 7797.0f; + + if (_sampleCount == 160) + { + costPerSample = 749.27f; + baseCost = 6138.9f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3564.1f; + float baseCost = 6225.5f; + + if (_sampleCount == 160) + { + costPerSample = 2125.6f; + baseCost = 9039.5f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)762.96f; + } + + return (uint)726.96f; + } + + public uint Estimate(CopyMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)836.32f; + } + + return (uint)1000.9f; + } + + public uint Estimate(MixCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1342.2f; + } + + return (uint)1833.2f; + } + + public uint Estimate(DelayCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)41636.0f; + case 2: + return (uint)97861.0f; + case 4: + return (uint)192520.0f; + case 6: + return (uint)301760.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)578.53f; + case 2: + return (uint)663.06f; + case 4: + return (uint)703.98f; + case 6: + return (uint)760.03f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)8770.3f; + case 2: + return (uint)25741.0f; + case 4: + return (uint)47551.0f; + case 6: + return (uint)81629.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)521.28f; + case 2: + return (uint)585.4f; + case 4: + return (uint)629.88f; + case 6: + return (uint)713.57f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)97192.0f; + case 2: + return (uint)103280.0f; + case 4: + return (uint)109580.0f; + case 6: + return (uint)115070.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)492.01f; + case 2: + return (uint)554.46f; + case 4: + return (uint)595.86f; + case 6: + return (uint)656.62f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)136460.0f; + case 2: + return (uint)145750.0f; + case 4: + return (uint)154800.0f; + case 6: + return (uint)161970.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)495.79f; + case 2: + return (uint)527.16f; + case 4: + return (uint)598.75f; + case 6: + return (uint)666.03f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(Reverb3dCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)138840.0f; + case 2: + return (uint)135430.0f; + case 4: + return (uint)199180.0f; + case 6: + return (uint)247350.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)718.7f; + case 2: + return (uint)751.3f; + case 4: + return (uint)797.46f; + case 6: + return (uint)867.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)199950.0f; + case 2: + return (uint)195200.0f; + case 4: + return (uint)290580.0f; + case 6: + return (uint)363490.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)534.24f; + case 2: + return (uint)570.87f; + case 4: + return (uint)660.93f; + case 6: + return (uint)694.6f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + return (uint)7177.9f; + } + + return (uint)489.16f; + } + + if (command.Enabled) + { + return (uint)9499.8f; + } + + return (uint)485.56f; + } + + public uint Estimate(VolumeCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1280.3f; + } + + return (uint)1737.8f; + } + + public uint Estimate(CircularBufferSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 1726.0f; + float baseCost = 1369.7f; + + if (_sampleCount == 160) + { + costPerBuffer = 853.63f; + baseCost = 1284.5f; + } + + return (uint)(baseCost + costPerBuffer * command.InputCount); + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)10009.0f; + } + + return (uint)14577.0f; + } + + public uint Estimate(UpsampleCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)292000.0f; + } + + return (uint)0.0f; + } + + public uint Estimate(DeviceSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + Debug.Assert(command.InputCount == 2 || command.InputCount == 6); + + if (command.InputCount == 2) + { + if (_sampleCount == 160) + { + return (uint)9261.5f; + } + + return (uint)9336.1f; + } + + if (_sampleCount == 160) + { + return (uint)9111.8f; + } + + return (uint)9566.7f; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + // NOTE: This was added between REV7 and REV8 and for some reasons the estimator v2 was changed... + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3490.9f; + float baseCost = 10091.0f; + + if (_sampleCount == 160) + { + costPerSample = 2310.4f; + baseCost = 7845.3f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DataSourceVersion2Command command) + { + return 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs new file mode 100644 index 00000000..b48ff8b5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs @@ -0,0 +1,636 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.Command; +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// <see cref="ICommandProcessingTimeEstimator"/> version 3. (added with REV8) + /// </summary> + public class CommandProcessingTimeEstimatorVersion3 : ICommandProcessingTimeEstimator + { + private uint _sampleCount; + private uint _bufferCount; + + public CommandProcessingTimeEstimatorVersion3(uint sampleCount, uint bufferCount) + { + _sampleCount = sampleCount; + _bufferCount = bufferCount; + } + + public uint Estimate(PerformanceCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)498.17f; + } + + return (uint)489.42f; + } + + public uint Estimate(ClearMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 440.68f; + float baseCost = 0; + + if (_sampleCount == 160) + { + costPerBuffer = 266.65f; + } + + return (uint)(baseCost + costPerBuffer * _bufferCount); + } + + public uint Estimate(BiquadFilterCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)4173.2f; + } + + return (uint)5585.1f; + } + + public uint Estimate(MixRampGroupedCommand command) + { + float costPerSample = 6.4434f; + + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + costPerSample = 6.708f; + } + + int volumeCount = 0; + + for (int i = 0; i < command.MixBufferCount; i++) + { + if (command.Volume0[i] != 0.0f || command.Volume1[i] != 0.0f) + { + volumeCount++; + } + } + + return (uint)(_sampleCount * costPerSample * volumeCount); + } + + public uint Estimate(MixRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1968.7f; + } + + return (uint)2459.4f; + } + + public uint Estimate(DepopPrepareCommand command) + { + return 0; + } + + public uint Estimate(VolumeRampCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1425.3f; + } + + return (uint)1700.0f; + } + + public uint Estimate(PcmInt16DataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 710.143f; + float baseCost = 7853.286f; + + if (_sampleCount == 160) + { + costPerSample = 427.52f; + baseCost = 6329.442f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(AdpcmDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3564.1f; + float baseCost = 9736.702f; + + if (_sampleCount == 160) + { + costPerSample = 2125.6f; + baseCost = 7913.808f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DepopForMixBuffersCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)739.64f; + } + + return (uint)910.97f; + } + + public uint Estimate(CopyMixBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)842.59f; + } + + return (uint)986.72f; + } + + public uint Estimate(MixCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1402.8f; + } + + return (uint)1853.2f; + } + + public uint Estimate(DelayCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)8929.04f; + case 2: + return (uint)25500.75f; + case 4: + return (uint)47759.62f; + case 6: + return (uint)82203.07f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)1295.20f; + case 2: + return (uint)1213.60f; + case 4: + return (uint)942.03f; + case 6: + return (uint)1001.55f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)11941.05f; + case 2: + return (uint)37197.37f; + case 4: + return (uint)69749.84f; + case 6: + return (uint)120042.40f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)997.67f; + case 2: + return (uint)977.63f; + case 4: + return (uint)792.30f; + case 6: + return (uint)875.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(ReverbCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)81475.05f; + case 2: + return (uint)84975.0f; + case 4: + return (uint)91625.15f; + case 6: + return (uint)95332.27f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)536.30f; + case 2: + return (uint)588.70f; + case 4: + return (uint)643.70f; + case 6: + return (uint)706.0f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)120174.47f; + case 2: + return (uint)25262.22f; + case 4: + return (uint)135751.23f; + case 6: + return (uint)141129.23f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)617.64f; + case 2: + return (uint)659.54f; + case 4: + return (uint)711.43f; + case 6: + return (uint)778.07f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(Reverb3dCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)116754.0f; + case 2: + return (uint)125912.05f; + case 4: + return (uint)146336.03f; + case 6: + return (uint)165812.66f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)734.0f; + case 2: + return (uint)766.62f; + case 4: + return (uint)797.46f; + case 6: + return (uint)867.43f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + if (command.Enabled) + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)170292.34f; + case 2: + return (uint)183875.63f; + case 4: + return (uint)214696.19f; + case 6: + return (uint)243846.77f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + else + { + switch (command.Parameter.ChannelCount) + { + case 1: + return (uint)508.47f; + case 2: + return (uint)582.45f; + case 4: + return (uint)626.42f; + case 6: + return (uint)682.47f; + default: + throw new NotImplementedException($"{command.Parameter.ChannelCount}"); + } + } + } + + public uint Estimate(AuxiliaryBufferCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + if (command.Enabled) + { + return (uint)7182.14f; + } + + return (uint)472.11f; + } + + if (command.Enabled) + { + return (uint)9435.96f; + } + + return (uint)462.62f; + } + + public uint Estimate(VolumeCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)1311.1f; + } + + return (uint)1713.6f; + } + + public uint Estimate(CircularBufferSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerBuffer = 770.26f; + float baseCost = 0f; + + if (_sampleCount == 160) + { + costPerBuffer = 531.07f; + } + + return (uint)(baseCost + costPerBuffer * command.InputCount); + } + + public uint Estimate(DownMixSurroundToStereoCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)9949.7f; + } + + return (uint)14679.0f; + } + + public uint Estimate(UpsampleCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + if (_sampleCount == 160) + { + return (uint)312990.0f; + } + + return (uint)0.0f; + } + + public uint Estimate(DeviceSinkCommand command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + Debug.Assert(command.InputCount == 2 || command.InputCount == 6); + + if (command.InputCount == 2) + { + if (_sampleCount == 160) + { + return (uint)8980.0f; + } + + return (uint)9221.9f; + } + + if (_sampleCount == 160) + { + return (uint)9177.9f; + } + + return (uint)9725.9f; + } + + public uint Estimate(PcmFloatDataSourceCommandVersion1 command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + float costPerSample = 3490.9f; + float baseCost = 10090.9f; + + if (_sampleCount == 160) + { + costPerSample = 2310.4f; + baseCost = 7845.25f; + } + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f)))); + } + + public uint Estimate(DataSourceVersion2Command command) + { + Debug.Assert(_sampleCount == 160 || _sampleCount == 240); + + (float baseCost, float costPerSample) = GetCostByFormat(_sampleCount, command.SampleFormat, command.SrcQuality); + + return (uint)(baseCost + (costPerSample * (((command.SampleRate / 200.0f) / _sampleCount) * (command.Pitch * 0.000030518f) - 1.0f))); + } + + private static (float, float) GetCostByFormat(uint sampleCount, SampleFormat format, SampleRateConversionQuality quality) + { + Debug.Assert(sampleCount == 160 || sampleCount == 240); + + switch (format) + { + case SampleFormat.PcmInt16: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (6329.44f, 427.52f); + } + + return (7853.28f, 710.14f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (8049.42f, 371.88f); + } + + return (10138.84f, 610.49f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (5062.66f, 423.43f); + } + + return (5810.96f, 676.72f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + case SampleFormat.PcmFloat: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (7845.25f, 2310.4f); + } + + return (10090.9f, 3490.9f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (9446.36f, 2308.91f); + } + + return (12520.85f, 3480.61f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (9446.36f, 2308.91f); + } + + return (12520.85f, 3480.61f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + case SampleFormat.Adpcm: + switch (quality) + { + case SampleRateConversionQuality.Default: + if (sampleCount == 160) + { + return (7913.81f, 1827.66f); + } + + return (9736.70f, 2756.37f); + case SampleRateConversionQuality.High: + if (sampleCount == 160) + { + return (9607.81f, 1829.29f); + } + + return (12154.38f, 2731.31f); + case SampleRateConversionQuality.Low: + if (sampleCount == 160) + { + return (6517.48f, 1824.61f); + } + + return (7929.44f, 2732.15f); + default: + throw new NotImplementedException($"{format} {quality}"); + } + default: + throw new NotImplementedException($"{format}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs new file mode 100644 index 00000000..df82945b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/AuxiliaryBufferEffect.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for an auxiliary buffer effect. + /// </summary> + public class AuxiliaryBufferEffect : BaseEffect + { + /// <summary> + /// The auxiliary buffer parameter. + /// </summary> + public AuxiliaryBufferParameter Parameter; + + /// <summary> + /// Auxiliary buffer state. + /// </summary> + public AuxiliaryBufferAddresses State; + + public override EffectType TargetEffectType => EffectType.AuxiliaryBuffer; + + public override DspAddress GetWorkBuffer(int index) + { + return WorkBuffers[index].GetReference(true); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (BufferUnmapped || parameter.IsNew) + { + ulong bufferSize = (ulong)Unsafe.SizeOf<int>() * Parameter.BufferStorageSize + (ulong)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + + bool sendBufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], Parameter.SendBufferInfoAddress, bufferSize); + bool returnBufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[1], Parameter.ReturnBufferInfoAddress, bufferSize); + + BufferUnmapped = sendBufferUnmapped && returnBufferUnmapped; + + if (!BufferUnmapped) + { + DspAddress sendDspAddress = WorkBuffers[0].GetReference(false); + DspAddress returnDspAddress = WorkBuffers[1].GetReference(false); + + State.SendBufferInfo = sendDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>(); + State.SendBufferInfoBase = sendDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + + State.ReturnBufferInfo = returnDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>(); + State.ReturnBufferInfoBase = returnDspAddress + (uint)Unsafe.SizeOf<AuxiliaryBufferHeader>() * 2; + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs new file mode 100644 index 00000000..7529f894 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BaseEffect.cs @@ -0,0 +1,257 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Base class used as a server state for an effect. + /// </summary> + public class BaseEffect + { + /// <summary> + /// The <see cref="EffectType"/> of the effect. + /// </summary> + public EffectType Type; + + /// <summary> + /// Set to true if the effect must be active. + /// </summary> + public bool IsEnabled; + + /// <summary> + /// Set to true if the internal effect work buffers used wasn't mapped. + /// </summary> + public bool BufferUnmapped; + + /// <summary> + /// The current state of the effect. + /// </summary> + public UsageState UsageState; + + /// <summary> + /// The target mix id of the effect. + /// </summary> + public int MixId; + + /// <summary> + /// Position of the effect while processing effects. + /// </summary> + public uint ProcessingOrder; + + /// <summary> + /// Array of all the work buffer used by the effect. + /// </summary> + protected AddressInfo[] WorkBuffers; + + /// <summary> + /// Create a new <see cref="BaseEffect"/>. + /// </summary> + public BaseEffect() + { + Type = TargetEffectType; + UsageState = UsageState.Invalid; + + IsEnabled = false; + BufferUnmapped = false; + MixId = Constants.UnusedMixId; + ProcessingOrder = uint.MaxValue; + + WorkBuffers = new AddressInfo[2]; + + foreach (ref AddressInfo info in WorkBuffers.AsSpan()) + { + info = AddressInfo.Create(); + } + } + + /// <summary> + /// The target <see cref="EffectType"/> handled by this <see cref="BaseEffect"/>. + /// </summary> + public virtual EffectType TargetEffectType => EffectType.Invalid; + + /// <summary> + /// Check if the <see cref="EffectType"/> sent by the user match the internal <see cref="EffectType"/>. + /// </summary> + /// <param name="parameter">The user parameter.</param> + /// <returns>Returns true if the <see cref="EffectType"/> sent by the user matches the internal <see cref="EffectType"/>.</returns> + public bool IsTypeValid(ref EffectInParameter parameter) + { + return parameter.Type == TargetEffectType; + } + + /// <summary> + /// Update the usage state during command generation. + /// </summary> + protected void UpdateUsageStateForCommandGeneration() + { + UsageState = IsEnabled ? UsageState.Enabled : UsageState.Disabled; + } + + /// <summary> + /// Update the internal common parameters from a user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + protected void UpdateParameterBase(ref EffectInParameter parameter) + { + MixId = parameter.MixId; + ProcessingOrder = parameter.ProcessingOrder; + } + + /// <summary> + /// Force unmap all the work buffers. + /// </summary> + /// <param name="mapper">The mapper to use.</param> + public void ForceUnmapBuffers(PoolMapper mapper) + { + foreach (ref AddressInfo info in WorkBuffers.AsSpan()) + { + if (info.GetReference(false) != 0) + { + mapper.ForceUnmap(ref info); + } + } + } + + /// <summary> + /// Check if the effect needs to be skipped. + /// </summary> + /// <returns>Returns true if the effect needs to be skipped.</returns> + public bool ShouldSkip() + { + return BufferUnmapped; + } + + /// <summary> + /// Update the <see cref="BaseEffect"/> state during command generation. + /// </summary> + public virtual void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetEffectType); + } + + /// <summary> + /// Update the internal state from a user parameter. + /// </summary> + /// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="mapper">The mapper to use.</param> + public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + updateErrorInfo = new ErrorInfo(); + } + + /// <summary> + /// Get the work buffer DSP address at the given index. + /// </summary> + /// <param name="index">The index of the work buffer</param> + /// <returns>The work buffer DSP address at the given index.</returns> + public virtual DspAddress GetWorkBuffer(int index) + { + throw new InvalidOperationException(); + } + + /// <summary> + /// Get the first work buffer DSP address. + /// </summary> + /// <returns>The first work buffer DSP address.</returns> + protected DspAddress GetSingleBuffer() + { + if (IsEnabled) + { + return WorkBuffers[0].GetReference(true); + } + + if (UsageState != UsageState.Disabled) + { + DspAddress address = WorkBuffers[0].GetReference(false); + ulong size = WorkBuffers[0].Size; + + if (address != 0 && size != 0) + { + AudioProcessorMemoryManager.InvalidateDataCache(address, size); + } + } + + return 0; + } + + /// <summary> + /// Store the output status to the given user output. + /// </summary> + /// <param name="outStatus">The given user output.</param> + /// <param name="isAudioRendererActive">If set to true, the <see cref="AudioRenderSystem"/> is active.</param> + public void StoreStatus(ref EffectOutStatus outStatus, bool isAudioRendererActive) + { + if (isAudioRendererActive) + { + if (UsageState == UsageState.Disabled) + { + outStatus.State = EffectOutStatus.EffectState.Disabled; + } + else + { + outStatus.State = EffectOutStatus.EffectState.Enabled; + } + } + else if (UsageState == UsageState.New) + { + outStatus.State = EffectOutStatus.EffectState.Enabled; + } + else + { + outStatus.State = EffectOutStatus.EffectState.Disabled; + } + } + + /// <summary> + /// Get the <see cref="PerformanceDetailType"/> associated to the <see cref="Type"/> of this effect. + /// </summary> + /// <returns>The <see cref="PerformanceDetailType"/> associated to the <see cref="Type"/> of this effect.</returns> + public PerformanceDetailType GetPerformanceDetailType() + { + switch (Type) + { + case EffectType.BiquadFilter: + return PerformanceDetailType.BiquadFilter; + case EffectType.AuxiliaryBuffer: + return PerformanceDetailType.Aux; + case EffectType.Delay: + return PerformanceDetailType.Delay; + case EffectType.Reverb: + return PerformanceDetailType.Reverb; + case EffectType.Reverb3d: + return PerformanceDetailType.Reverb3d; + case EffectType.BufferMix: + return PerformanceDetailType.Mix; + default: + throw new NotImplementedException($"{Type}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs new file mode 100644 index 00000000..35ba8a0d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BiquadFilterEffect.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a biquad filter effect. + /// </summary> + public class BiquadFilterEffect : BaseEffect + { + /// <summary> + /// The biquad filter parameter. + /// </summary> + public BiquadFilterEffectParameter Parameter; + + /// <summary> + /// The biquad filter state. + /// </summary> + public Memory<BiquadFilterState> State { get; } + + /// <summary> + /// Create a new <see cref="BiquadFilterEffect"/>. + /// </summary> + public BiquadFilterEffect() + { + Parameter = new BiquadFilterEffectParameter(); + State = new BiquadFilterState[Constants.ChannelCountMax]; + } + + public override EffectType TargetEffectType => EffectType.BiquadFilter; + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, BiquadFilterEffectParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs new file mode 100644 index 00000000..14ed3950 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/BufferMixEffect.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a buffer mix effect. + /// </summary> + public class BufferMixEffect : BaseEffect + { + /// <summary> + /// The buffer mix parameter. + /// </summary> + public BufferMixParameter Parameter; + + public override EffectType TargetEffectType => EffectType.BufferMix; + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + UpdateParameterBase(ref parameter); + + Parameter = MemoryMarshal.Cast<byte, BufferMixParameter>(parameter.SpecificData)[0]; + IsEnabled = parameter.IsEnabled; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs new file mode 100644 index 00000000..df3e5ee7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/DelayEffect.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a delay effect. + /// </summary> + public class DelayEffect : BaseEffect + { + /// <summary> + /// The delay parameter. + /// </summary> + public DelayParameter Parameter; + + /// <summary> + /// The delay state. + /// </summary> + public Memory<DelayState> State { get; } + + public DelayEffect() + { + State = new DelayState[1]; + } + + public override EffectType TargetEffectType => EffectType.Delay; + + public override DspAddress GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref DelayParameter delayParameter = ref MemoryMarshal.Cast<byte, DelayParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (delayParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.Status; + + Parameter = delayParameter; + + if (delayParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.Status = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.Status = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs b/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs new file mode 100644 index 00000000..ff6051ae --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/EffectContext.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Effect context. + /// </summary> + public class EffectContext + { + /// <summary> + /// Storage for <see cref="BaseEffect"/>. + /// </summary> + private BaseEffect[] _effects; + + /// <summary> + /// The total effect count. + /// </summary> + private uint _effectCount; + + /// <summary> + /// Create a new <see cref="EffectContext"/>. + /// </summary> + public EffectContext() + { + _effects = null; + _effectCount = 0; + } + + /// <summary> + /// Initialize the <see cref="EffectContext"/>. + /// </summary> + /// <param name="effectCount">The total effect count.</param> + public void Initialize(uint effectCount) + { + _effectCount = effectCount; + _effects = new BaseEffect[effectCount]; + + for (int i = 0; i < _effectCount; i++) + { + _effects[i] = new BaseEffect(); + } + } + + /// <summary> + /// Get the total effect count. + /// </summary> + /// <returns>The total effect count.</returns> + public uint GetCount() + { + return _effectCount; + } + + /// <summary> + /// Get a reference to a <see cref="BaseEffect"/> at the given <paramref name="index"/>. + /// </summary> + /// <param name="index">The index to use.</param> + /// <returns>A reference to a <see cref="BaseEffect"/> at the given <paramref name="index"/>.</returns> + public ref BaseEffect GetEffect(int index) + { + Debug.Assert(index >= 0 && index < _effectCount); + + return ref _effects[index]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs new file mode 100644 index 00000000..d9f23799 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/Reverb3dEffect.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a 3D reverberation effect. + /// </summary> + public class Reverb3dEffect : BaseEffect + { + /// <summary> + /// The 3D reverberation parameter. + /// </summary> + public Reverb3dParameter Parameter; + + /// <summary> + /// The 3D reverberation state. + /// </summary> + public Memory<Reverb3dState> State { get; } + + public Reverb3dEffect() + { + State = new Reverb3dState[1]; + } + + public override EffectType TargetEffectType => EffectType.Reverb3d; + + public override ulong GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast<byte, Reverb3dParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (reverbParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.ParameterStatus; + + Parameter = reverbParameter; + + if (reverbParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.ParameterStatus = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.ParameterStatus = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.ParameterStatus = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs b/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs new file mode 100644 index 00000000..4c81f729 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/ReverbEffect.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Dsp.State; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// Server state for a reverberation effect. + /// </summary> + public class ReverbEffect : BaseEffect + { + /// <summary> + /// The reverberation parameter. + /// </summary> + public ReverbParameter Parameter; + + /// <summary> + /// The reverberation state. + /// </summary> + public Memory<ReverbState> State { get; } + + /// <summary> + /// Create a new <see cref="ReverbEffect"/>. + /// </summary> + public ReverbEffect() + { + State = new ReverbState[1]; + } + + public override EffectType TargetEffectType => EffectType.Reverb; + + public override ulong GetWorkBuffer(int index) + { + return GetSingleBuffer(); + } + + public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameter parameter, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast<byte, ReverbParameter>(parameter.SpecificData)[0]; + + updateErrorInfo = new BehaviourParameter.ErrorInfo(); + + if (reverbParameter.IsChannelCountMaxValid()) + { + UpdateParameterBase(ref parameter); + + UsageState oldParameterStatus = Parameter.Status; + + Parameter = reverbParameter; + + if (reverbParameter.IsChannelCountValid()) + { + IsEnabled = parameter.IsEnabled; + + if (oldParameterStatus != UsageState.Enabled) + { + Parameter.Status = oldParameterStatus; + } + + if (BufferUnmapped || parameter.IsNew) + { + UsageState = UsageState.New; + Parameter.Status = UsageState.Invalid; + + BufferUnmapped = !mapper.TryAttachBuffer(out updateErrorInfo, ref WorkBuffers[0], parameter.BufferBase, parameter.BufferSize); + } + } + } + } + + public override void UpdateForCommandGeneration() + { + UpdateUsageStateForCommandGeneration(); + + Parameter.Status = UsageState.Enabled; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs b/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs new file mode 100644 index 00000000..a519acd4 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Effect/UsageState.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Effect +{ + /// <summary> + /// The usage state of an effect. + /// </summary> + public enum UsageState : byte + { + /// <summary> + /// The effect is in an invalid state. + /// </summary> + Invalid, + + /// <summary> + /// The effect is new. + /// </summary> + New, + + /// <summary> + /// The effect is enabled. + /// </summary> + Enabled, + + /// <summary> + /// The effect is disabled. + /// </summary> + Disabled + } +} diff --git a/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs new file mode 100644 index 00000000..119ec907 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Dsp.Command; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Estimate the time that a <see cref="ICommand"/> should take. + /// </summary> + /// <remarks>This is used for voice dropping.</remarks> + public interface ICommandProcessingTimeEstimator + { + uint Estimate(AuxiliaryBufferCommand command); + uint Estimate(BiquadFilterCommand command); + uint Estimate(ClearMixBufferCommand command); + uint Estimate(DelayCommand command); + uint Estimate(Reverb3dCommand command); + uint Estimate(ReverbCommand command); + uint Estimate(DepopPrepareCommand command); + uint Estimate(DepopForMixBuffersCommand command); + uint Estimate(MixCommand command); + uint Estimate(MixRampCommand command); + uint Estimate(MixRampGroupedCommand command); + uint Estimate(CopyMixBufferCommand command); + uint Estimate(PerformanceCommand command); + uint Estimate(VolumeCommand command); + uint Estimate(VolumeRampCommand command); + uint Estimate(PcmInt16DataSourceCommandVersion1 command); + uint Estimate(PcmFloatDataSourceCommandVersion1 command); + uint Estimate(AdpcmDataSourceCommandVersion1 command); + uint Estimate(DataSourceVersion2Command command); + uint Estimate(CircularBufferSinkCommand command); + uint Estimate(DeviceSinkCommand command); + uint Estimate(DownMixSurroundToStereoCommand command); + uint Estimate(UpsampleCommand command); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs new file mode 100644 index 00000000..2b625d06 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/AddressInfo.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Represents the information of a region shared between the CPU and DSP. + /// </summary> + public struct AddressInfo + { + /// <summary> + /// The target CPU address of the region. + /// </summary> + public CpuAddress CpuAddress; + + /// <summary> + /// The size of the region. + /// </summary> + public ulong Size; + + private unsafe MemoryPoolState* _memoryPools; + + /// <summary> + /// The forced DSP address of the region. + /// </summary> + public DspAddress ForceMappedDspAddress; + + private unsafe ref MemoryPoolState MemoryPoolState => ref *_memoryPools; + + public unsafe bool HasMemoryPoolState => (IntPtr)_memoryPools != IntPtr.Zero; + + /// <summary> + /// Create an new empty <see cref="AddressInfo"/>. + /// </summary> + /// <returns>A new empty <see cref="AddressInfo"/>.</returns> + public static AddressInfo Create() + { + return Create(0, 0); + } + + /// <summary> + /// Create a new <see cref="AddressInfo"/>. + /// </summary> + /// <param name="cpuAddress">The target <see cref="CpuAddress"/> of the region.</param> + /// <param name="size">The target size of the region.</param> + /// <returns>A new <see cref="AddressInfo"/>.</returns> + public static AddressInfo Create(CpuAddress cpuAddress, ulong size) + { + unsafe + { + return new AddressInfo + { + CpuAddress = cpuAddress, + _memoryPools = MemoryPoolState.Null, + Size = size, + ForceMappedDspAddress = 0 + }; + } + } + + /// <summary> + /// Setup the CPU address and size of the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="cpuAddress">The target <see cref="CpuAddress"/> of the region.</param> + /// <param name="size">The size of the region.</param> + public void Setup(CpuAddress cpuAddress, ulong size) + { + CpuAddress = cpuAddress; + Size = size; + ForceMappedDspAddress = 0; + + unsafe + { + _memoryPools = MemoryPoolState.Null; + } + } + + /// <summary> + /// Set the <see cref="MemoryPoolState"/> associated. + /// </summary> + /// <param name="memoryPoolState">The <see cref="MemoryPoolState"/> associated.</param> + public void SetupMemoryPool(Span<MemoryPoolState> memoryPoolState) + { + unsafe + { + fixed (MemoryPoolState* ptr = &MemoryMarshal.GetReference(memoryPoolState)) + { + SetupMemoryPool(ptr); + } + } + } + + /// <summary> + /// Set the <see cref="MemoryPoolState"/> associated. + /// </summary> + /// <param name="memoryPoolState">The <see cref="MemoryPoolState"/> associated.</param> + public unsafe void SetupMemoryPool(MemoryPoolState* memoryPoolState) + { + _memoryPools = memoryPoolState; + } + + /// <summary> + /// Check if the <see cref="MemoryPoolState"/> is mapped. + /// </summary> + /// <returns>Returns true if the <see cref="MemoryPoolState"/> is mapped.</returns> + public bool HasMappedMemoryPool() + { + return HasMemoryPoolState && MemoryPoolState.IsMapped(); + } + + /// <summary> + /// Get the DSP address associated to the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="markUsed">If true, mark the <see cref="MemoryPoolState"/> as used.</param> + /// <returns>Returns the DSP address associated to the <see cref="AddressInfo"/>.</returns> + public DspAddress GetReference(bool markUsed) + { + if (!HasMappedMemoryPool()) + { + return ForceMappedDspAddress; + } + + if (markUsed) + { + MemoryPoolState.IsUsed = true; + } + + return MemoryPoolState.Translate(CpuAddress, Size); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs new file mode 100644 index 00000000..ebde1403 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/MemoryPoolState.cs @@ -0,0 +1,148 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.InteropServices; + +using DspAddress = System.UInt64; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Server state for a memory pool. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = Alignment)] + public struct MemoryPoolState + { + public const int Alignment = 0x10; + + /// <summary> + /// The location of the <see cref="MemoryPoolState"/>. + /// </summary> + public enum LocationType : uint + { + /// <summary> + /// <see cref="MemoryPoolState"/> located on the CPU side for user use. + /// </summary> + Cpu, + + /// <summary> + /// <see cref="MemoryPoolState"/> located on the DSP side for system use. + /// </summary> + Dsp + } + + /// <summary> + /// The CPU address associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public CpuAddress CpuAddress; + + /// <summary> + /// The DSP address associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public DspAddress DspAddress; + + /// <summary> + /// The size associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public ulong Size; + + /// <summary> + /// The <see cref="LocationType"/> associated to the <see cref="MemoryPoolState"/>. + /// </summary> + public LocationType Location; + + /// <summary> + /// Set to true if the <see cref="MemoryPoolState"/> is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + public static unsafe MemoryPoolState* Null => (MemoryPoolState*)IntPtr.Zero.ToPointer(); + + /// <summary> + /// Create a new <see cref="MemoryPoolState"/> with the given <see cref="LocationType"/>. + /// </summary> + /// <param name="location">The location type to use.</param> + /// <returns>A new <see cref="MemoryPoolState"/> with the given <see cref="LocationType"/>.</returns> + public static MemoryPoolState Create(LocationType location) + { + return new MemoryPoolState + { + CpuAddress = 0, + DspAddress = 0, + Size = 0, + Location = location + }; + } + + /// <summary> + /// Set the <see cref="CpuAddress"/> and size of the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="cpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + public void SetCpuAddress(CpuAddress cpuAddress, ulong size) + { + CpuAddress = cpuAddress; + Size = size; + } + + /// <summary> + /// Check if the given <see cref="CpuAddress"/> and size is contains in the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="targetCpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + /// <returns>True if the <see cref="CpuAddress"/> is contained inside the <see cref="MemoryPoolState"/>.</returns> + public bool Contains(CpuAddress targetCpuAddress, ulong size) + { + if (CpuAddress <= targetCpuAddress && size + targetCpuAddress <= Size + CpuAddress) + { + return true; + } + + return false; + } + + /// <summary> + /// Translate the given CPU address to a DSP address. + /// </summary> + /// <param name="targetCpuAddress">The <see cref="CpuAddress"/>.</param> + /// <param name="size">The size.</param> + /// <returns>the target DSP address.</returns> + public DspAddress Translate(CpuAddress targetCpuAddress, ulong size) + { + if (Contains(targetCpuAddress, size) && IsMapped()) + { + ulong offset = targetCpuAddress - CpuAddress; + + return DspAddress + offset; + } + + return 0; + } + + /// <summary> + /// Is the <see cref="MemoryPoolState"/> mapped on the DSP? + /// </summary> + /// <returns>Returns true if the <see cref="MemoryPoolState"/> is mapped on the DSP.</returns> + public bool IsMapped() + { + return DspAddress != 0; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs b/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs new file mode 100644 index 00000000..b255960c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/MemoryPool/PoolMapper.cs @@ -0,0 +1,383 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common.Logging; +using System; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +using CpuAddress = System.UInt64; +using DspAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Server.MemoryPool +{ + /// <summary> + /// Memory pool mapping helper. + /// </summary> + public class PoolMapper + { + const uint CurrentProcessPseudoHandle = 0xFFFF8001; + + /// <summary> + /// The result of <see cref="Update(ref MemoryPoolState, ref MemoryPoolInParameter, ref MemoryPoolOutStatus)"/>. + /// </summary> + public enum UpdateResult : uint + { + /// <summary> + /// No error reported. + /// </summary> + Success = 0, + + /// <summary> + /// The user parameters were invalid. + /// </summary> + InvalidParameter = 1, + + /// <summary> + /// <see cref="Dsp.AudioProcessor"/> mapping failed. + /// </summary> + MapError = 2, + + /// <summary> + /// <see cref="Dsp.AudioProcessor"/> unmapping failed. + /// </summary> + UnmapError = 3 + } + + /// <summary> + /// The handle of the process owning the CPU memory manipulated. + /// </summary> + private uint _processHandle; + + /// <summary> + /// The <see cref="Memory{MemoryPoolState}"/> that will be manipulated. + /// </summary> + private Memory<MemoryPoolState> _memoryPools; + + /// <summary> + /// If set to true, this will try to force map memory pool even if their state are considered invalid. + /// </summary> + private bool _isForceMapEnabled; + + /// <summary> + /// Create a new <see cref="PoolMapper"/> used for system mapping. + /// </summary> + /// <param name="processHandle">The handle of the process owning the CPU memory manipulated.</param> + /// <param name="isForceMapEnabled">If set to true, this will try to force map memory pool even if their state are considered invalid.</param> + public PoolMapper(uint processHandle, bool isForceMapEnabled) + { + _processHandle = processHandle; + _isForceMapEnabled = isForceMapEnabled; + _memoryPools = Memory<MemoryPoolState>.Empty; + } + + /// <summary> + /// Create a new <see cref="PoolMapper"/> used for user mapping. + /// </summary> + /// <param name="processHandle">The handle of the process owning the CPU memory manipulated.</param> + /// <param name="memoryPool">The user memory pools.</param> + /// <param name="isForceMapEnabled">If set to true, this will try to force map memory pool even if their state are considered invalid.</param> + public PoolMapper(uint processHandle, Memory<MemoryPoolState> memoryPool, bool isForceMapEnabled) + { + _processHandle = processHandle; + _memoryPools = memoryPool; + _isForceMapEnabled = isForceMapEnabled; + } + + /// <summary> + /// Initialize the <see cref="MemoryPoolState"/> for system use. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> for system use.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to assign.</param> + /// <param name="size">The size to assign.</param> + /// <returns>Returns true if mapping on the <see cref="Dsp.AudioProcessor"/> succeeded.</returns> + public bool InitializeSystemPool(ref MemoryPoolState memoryPool, CpuAddress cpuAddress, ulong size) + { + if (memoryPool.Location != MemoryPoolState.LocationType.Dsp) + { + return false; + } + + return InitializePool(ref memoryPool, cpuAddress, size); + } + + /// <summary> + /// Initialize the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/>.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to assign.</param> + /// <param name="size">The size to assign.</param> + /// <returns>Returns true if mapping on the <see cref="Dsp.AudioProcessor"/> succeeded.</returns> + public bool InitializePool(ref MemoryPoolState memoryPool, CpuAddress cpuAddress, ulong size) + { + memoryPool.SetCpuAddress(cpuAddress, size); + + return Map(ref memoryPool) != 0; + } + + /// <summary> + /// Get the process handle associated to the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/>.</param> + /// <returns>Returns the process handle associated to the <see cref="MemoryPoolState"/>.</returns> + public uint GetProcessHandle(ref MemoryPoolState memoryPool) + { + if (memoryPool.Location == MemoryPoolState.LocationType.Cpu) + { + return CurrentProcessPseudoHandle; + } + else if (memoryPool.Location == MemoryPoolState.LocationType.Dsp) + { + return _processHandle; + } + + return 0; + } + + /// <summary> + /// Map the <see cref="MemoryPoolState"/> on the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to map.</param> + /// <returns>Returns the DSP address mapped.</returns> + public DspAddress Map(ref MemoryPoolState memoryPool) + { + DspAddress result = AudioProcessorMemoryManager.Map(GetProcessHandle(ref memoryPool), memoryPool.CpuAddress, memoryPool.Size); + + if (result != 0) + { + memoryPool.DspAddress = result; + } + + return result; + } + + /// <summary> + /// Unmap the <see cref="MemoryPoolState"/> from the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to unmap.</param> + /// <returns>Returns true if unmapped.</returns> + public bool Unmap(ref MemoryPoolState memoryPool) + { + if (memoryPool.IsUsed) + { + return false; + } + + AudioProcessorMemoryManager.Unmap(GetProcessHandle(ref memoryPool), memoryPool.CpuAddress, memoryPool.Size); + + memoryPool.SetCpuAddress(0, 0); + memoryPool.DspAddress = 0; + + return true; + } + + /// <summary> + /// Find a <see cref="MemoryPoolState"/> associated to the region given. + /// </summary> + /// <param name="cpuAddress">The region <see cref="CpuAddress"/>.</param> + /// <param name="size">The region size.</param> + /// <returns>Returns the <see cref="MemoryPoolState"/> found or <see cref="Memory{MemoryPoolState}.Empty"/> if not found.</returns> + private Span<MemoryPoolState> FindMemoryPool(CpuAddress cpuAddress, ulong size) + { + if (!_memoryPools.IsEmpty && _memoryPools.Length > 0) + { + for (int i = 0; i < _memoryPools.Length; i++) + { + if (_memoryPools.Span[i].Contains(cpuAddress, size)) + { + return _memoryPools.Span.Slice(i, 1); + } + } + } + + return Span<MemoryPoolState>.Empty; + } + + /// <summary> + /// Force unmap the given <see cref="AddressInfo"/>. + /// </summary> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to force unmap</param> + public void ForceUnmap(ref AddressInfo addressInfo) + { + if (_isForceMapEnabled) + { + Span<MemoryPoolState> memoryPool = FindMemoryPool(addressInfo.CpuAddress, addressInfo.Size); + + if (!memoryPool.IsEmpty) + { + AudioProcessorMemoryManager.Unmap(_processHandle, memoryPool[0].CpuAddress, memoryPool[0].Size); + + return; + } + + AudioProcessorMemoryManager.Unmap(_processHandle, addressInfo.CpuAddress, 0); + } + } + + /// <summary> + /// Try to attach the given region to the <see cref="AddressInfo"/>. + /// </summary> + /// <param name="errorInfo">The error information if an error was generated.</param> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to attach the region to.</param> + /// <param name="cpuAddress">The region <see cref="CpuAddress"/>.</param> + /// <param name="size">The region size.</param> + /// <returns>Returns true if mapping was performed.</returns> + public bool TryAttachBuffer(out ErrorInfo errorInfo, ref AddressInfo addressInfo, CpuAddress cpuAddress, ulong size) + { + errorInfo = new ErrorInfo(); + + addressInfo.Setup(cpuAddress, size); + + if (AssignDspAddress(ref addressInfo)) + { + errorInfo.ErrorCode = 0x0; + errorInfo.ExtraErrorInfo = 0x0; + + return true; + } + else + { + errorInfo.ErrorCode = ResultCode.InvalidAddressInfo; + errorInfo.ExtraErrorInfo = addressInfo.CpuAddress; + + return _isForceMapEnabled; + } + } + + /// <summary> + /// Update a <see cref="MemoryPoolState"/> using user parameters. + /// </summary> + /// <param name="memoryPool">The <see cref="MemoryPoolState"/> to update.</param> + /// <param name="inParameter">Input user parameter.</param> + /// <param name="outStatus">Output user parameter.</param> + /// <returns>Returns the <see cref="UpdateResult"/> of the operations performed.</returns> + public UpdateResult Update(ref MemoryPoolState memoryPool, ref MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus) + { + MemoryPoolUserState inputState = inParameter.State; + + MemoryPoolUserState outputState; + + const uint pageSize = 0x1000; + + if (inputState != MemoryPoolUserState.RequestAttach && inputState != MemoryPoolUserState.RequestDetach) + { + return UpdateResult.Success; + } + + if (inParameter.CpuAddress == 0 || (inParameter.CpuAddress & (pageSize - 1)) != 0) + { + return UpdateResult.InvalidParameter; + } + + if (inParameter.Size == 0 || (inParameter.Size & (pageSize - 1)) != 0) + { + return UpdateResult.InvalidParameter; + } + + if (inputState == MemoryPoolUserState.RequestAttach) + { + bool initializeSuccess = InitializePool(ref memoryPool, inParameter.CpuAddress, inParameter.Size); + + if (!initializeSuccess) + { + memoryPool.SetCpuAddress(0, 0); + + Logger.Error?.Print(LogClass.AudioRenderer, $"Map of memory pool (address: 0x{inParameter.CpuAddress:x}, size 0x{inParameter.Size:x}) failed!"); + return UpdateResult.MapError; + } + + outputState = MemoryPoolUserState.Attached; + } + else + { + if (memoryPool.CpuAddress != inParameter.CpuAddress || memoryPool.Size != inParameter.Size) + { + return UpdateResult.InvalidParameter; + } + + if (!Unmap(ref memoryPool)) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Unmap of memory pool (address: 0x{memoryPool.CpuAddress:x}, size 0x{memoryPool.Size:x}) failed!"); + return UpdateResult.UnmapError; + } + + outputState = MemoryPoolUserState.Detached; + } + + outStatus.State = outputState; + + return UpdateResult.Success; + } + + /// <summary> + /// Map the <see cref="AddressInfo"/> to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="addressInfo">The <see cref="AddressInfo"/> to map.</param> + /// <returns>Returns true if mapping was performed.</returns> + private bool AssignDspAddress(ref AddressInfo addressInfo) + { + if (addressInfo.CpuAddress == 0) + { + return false; + } + + if (_memoryPools.Length > 0) + { + Span<MemoryPoolState> memoryPool = FindMemoryPool(addressInfo.CpuAddress, addressInfo.Size); + + if (!memoryPool.IsEmpty) + { + addressInfo.SetupMemoryPool(memoryPool); + + return true; + } + } + + if (_isForceMapEnabled) + { + DspAddress dspAddress = AudioProcessorMemoryManager.Map(_processHandle, addressInfo.CpuAddress, addressInfo.Size); + + addressInfo.ForceMappedDspAddress = dspAddress; + + AudioProcessorMemoryManager.Map(_processHandle, addressInfo.CpuAddress, addressInfo.Size); + } + else + { + unsafe + { + addressInfo.SetupMemoryPool(MemoryPoolState.Null); + } + } + + return false; + } + + /// <summary> + /// Remove the usage flag from all the <see cref="MemoryPoolState"/>. + /// </summary> + /// <param name="memoryPool">The <see cref="Memory{MemoryPoolState}"/> to reset.</param> + public static void ClearUsageState(Memory<MemoryPoolState> memoryPool) + { + foreach (ref MemoryPoolState info in memoryPool.Span) + { + info.IsUsed = false; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs b/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs new file mode 100644 index 00000000..0e094665 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Mix/MixContext.cs @@ -0,0 +1,276 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Mix +{ + /// <summary> + /// Mix context. + /// </summary> + public class MixContext + { + /// <summary> + /// The total mix count. + /// </summary> + private uint _mixesCount; + + /// <summary> + /// Storage for <see cref="MixState"/>. + /// </summary> + private Memory<MixState> _mixes; + + /// <summary> + /// Storage of the sorted indices to <see cref="MixState"/>. + /// </summary> + private Memory<int> _sortedMixes; + + /// <summary> + /// Graph state. + /// </summary> + public NodeStates NodeStates { get; } + + /// <summary> + /// The instance of the adjacent matrix. + /// </summary> + public EdgeMatrix EdgeMatrix { get; } + + /// <summary> + /// Create a new instance of <see cref="MixContext"/>. + /// </summary> + public MixContext() + { + NodeStates = new NodeStates(); + EdgeMatrix = new EdgeMatrix(); + } + + /// <summary> + /// Initialize the <see cref="MixContext"/>. + /// </summary> + /// <param name="sortedMixes">The storage for sorted indices.</param> + /// <param name="mixes">The storage of <see cref="MixState"/>.</param> + /// <param name="nodeStatesWorkBuffer">The storage used for the <see cref="NodeStates"/>.</param> + /// <param name="edgeMatrixWorkBuffer">The storage used for the <see cref="EdgeMatrix"/>.</param> + public void Initialize(Memory<int> sortedMixes, Memory<MixState> mixes, Memory<byte> nodeStatesWorkBuffer, Memory<byte> edgeMatrixWorkBuffer) + { + _mixesCount = (uint)mixes.Length; + _mixes = mixes; + _sortedMixes = sortedMixes; + + if (!nodeStatesWorkBuffer.IsEmpty && !edgeMatrixWorkBuffer.IsEmpty) + { + NodeStates.Initialize(nodeStatesWorkBuffer, mixes.Length); + EdgeMatrix.Initialize(edgeMatrixWorkBuffer, mixes.Length); + } + + int sortedId = 0; + for (int i = 0; i < _mixes.Length; i++) + { + SetSortedState(sortedId++, i); + } + } + + /// <summary> + /// Associate the given <paramref name="targetIndex"/> to a given <paramref cref="id"/>. + /// </summary> + /// <param name="id">The sorted id.</param> + /// <param name="targetIndex">The index to associate.</param> + private void SetSortedState(int id, int targetIndex) + { + _sortedMixes.Span[id] = targetIndex; + } + + /// <summary> + /// Get a reference to the final <see cref="MixState"/>. + /// </summary> + /// <returns>A reference to the final <see cref="MixState"/>.</returns> + public ref MixState GetFinalState() + { + return ref GetState(Constants.FinalMixId); + } + + /// <summary> + /// Get a reference to a <see cref="MixState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="MixState"/> at the given <paramref name="id"/>.</returns> + public ref MixState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_mixes, id, _mixesCount); + } + + /// <summary> + /// Get a reference to a <see cref="MixState"/> at the given <paramref name="id"/> of the sorted mix info. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="MixState"/> at the given <paramref name="id"/>.</returns> + public ref MixState GetSortedState(int id) + { + Debug.Assert(id >= 0 && id < _mixesCount); + + return ref GetState(_sortedMixes.Span[id]); + } + + /// <summary> + /// Get the total mix count. + /// </summary> + /// <returns>The total mix count.</returns> + public uint GetCount() + { + return _mixesCount; + } + + /// <summary> + /// Update the internal distance from the final mix value of every <see cref="MixState"/>. + /// </summary> + private void UpdateDistancesFromFinalMix() + { + foreach (ref MixState mix in _mixes.Span) + { + mix.ClearDistanceFromFinalMix(); + } + + for (int i = 0; i < GetCount(); i++) + { + ref MixState mix = ref GetState(i); + + SetSortedState(i, i); + + if (mix.IsUsed) + { + uint distance; + + if (mix.MixId != Constants.FinalMixId) + { + int mixId = mix.MixId; + + for (distance = 0; distance < GetCount(); distance++) + { + if (mixId == Constants.UnusedMixId) + { + distance = MixState.InvalidDistanceFromFinalMix; + break; + } + + ref MixState distanceMix = ref GetState(mixId); + + if (distanceMix.DistanceFromFinalMix != MixState.InvalidDistanceFromFinalMix) + { + distance = distanceMix.DistanceFromFinalMix + 1; + break; + } + + mixId = distanceMix.DestinationMixId; + + if (mixId == Constants.FinalMixId) + { + break; + } + } + + if (distance > GetCount()) + { + distance = MixState.InvalidDistanceFromFinalMix; + } + } + else + { + distance = MixState.InvalidDistanceFromFinalMix; + } + + mix.DistanceFromFinalMix = distance; + } + } + } + + /// <summary> + /// Update the internal mix buffer offset of all <see cref="MixState"/>. + /// </summary> + private void UpdateMixBufferOffset() + { + uint offset = 0; + + foreach (ref MixState mix in _mixes.Span) + { + mix.BufferOffset = offset; + + offset += mix.BufferCount; + } + } + + /// <summary> + /// Sort the mixes using distance from the final mix. + /// </summary> + public void Sort() + { + UpdateDistancesFromFinalMix(); + + int[] sortedMixesTemp = _sortedMixes.Slice(0, (int)GetCount()).ToArray(); + + Array.Sort(sortedMixesTemp, (a, b) => + { + ref MixState stateA = ref GetState(a); + ref MixState stateB = ref GetState(b); + + return stateB.DistanceFromFinalMix.CompareTo(stateA.DistanceFromFinalMix); + }); + + sortedMixesTemp.AsSpan().CopyTo(_sortedMixes.Span); + + UpdateMixBufferOffset(); + } + + /// <summary> + /// Sort the mixes and splitters using an adjacency matrix. + /// </summary> + /// <param name="splitterContext">The <see cref="SplitterContext"/> used.</param> + /// <returns>Return true, if no errors in the graph were detected.</returns> + public bool Sort(SplitterContext splitterContext) + { + if (splitterContext.UsingSplitter()) + { + bool isValid = NodeStates.Sort(EdgeMatrix); + + if (isValid) + { + ReadOnlySpan<int> sortedMixesIndex = NodeStates.GetTsortResult(); + + int id = 0; + + for (int i = sortedMixesIndex.Length - 1; i >= 0; i--) + { + SetSortedState(id++, sortedMixesIndex[i]); + } + + UpdateMixBufferOffset(); + } + + return isValid; + } + else + { + UpdateMixBufferOffset(); + + return true; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs new file mode 100644 index 00000000..028d0885 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs @@ -0,0 +1,330 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using static Ryujinx.Audio.Constants; + +namespace Ryujinx.Audio.Renderer.Server.Mix +{ + /// <summary> + /// Server state for a mix. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x940, Pack = Alignment)] + public struct MixState + { + public const uint InvalidDistanceFromFinalMix = 0x80000000; + + public const int Alignment = 0x10; + + /// <summary> + /// Base volume of the mix. + /// </summary> + public float Volume; + + /// <summary> + /// Target sample rate of the mix. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Target buffer count. + /// </summary> + public uint BufferCount; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// The id of the mix. + /// </summary> + public int MixId; + + /// <summary> + /// The mix node id. + /// </summary> + public int NodeId; + + /// <summary> + /// the buffer offset to use for command generation. + /// </summary> + public uint BufferOffset; + + /// <summary> + /// The distance of the mix from the final mix. + /// </summary> + public uint DistanceFromFinalMix; + + /// <summary> + /// The effect processing order storage. + /// </summary> + private IntPtr _effectProcessingOrderArrayPointer; + + /// <summary> + /// The max element count that can be found in the effect processing order storage. + /// </summary> + public uint EffectProcessingOrderArrayMaxCount; + + /// <summary> + /// The mix to output the result of this mix. + /// </summary> + public int DestinationMixId; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixVolumeArray _mixVolumeArray; + + /// <summary> + /// The splitter to output the result of this mix. + /// </summary> + public uint DestinationSplitterId; + + /// <summary> + /// If set to true, the long size pre-delay is supported on the reverb command. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsLongSizePreDelaySupported; + + [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)] + private struct MixVolumeArray + { + private const int Size = 4 * MixBufferCountMax * MixBufferCountMax; + } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when no splitter id is specified.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixVolumeArray, float>(ref _mixVolumeArray); + + /// <summary> + /// Get the volume for a given connection destination. + /// </summary> + /// <param name="sourceIndex">The source node index.</param> + /// <param name="destinationIndex">The destination node index</param> + /// <returns>The volume for the given connection destination.</returns> + public float GetMixBufferVolume(int sourceIndex, int destinationIndex) + { + return MixBufferVolume[sourceIndex * MixBufferCountMax + destinationIndex]; + } + + /// <summary> + /// The array used to order effects associated to this mix. + /// </summary> + public Span<int> EffectProcessingOrderArray + { + get + { + if (_effectProcessingOrderArrayPointer == IntPtr.Zero) + { + return Span<int>.Empty; + } + + unsafe + { + return new Span<int>((void*)_effectProcessingOrderArrayPointer, (int)EffectProcessingOrderArrayMaxCount); + } + } + } + + /// <summary> + /// Create a new <see cref="MixState"/> + /// </summary> + /// <param name="effectProcessingOrderArray"></param> + /// <param name="behaviourContext"></param> + public MixState(Memory<int> effectProcessingOrderArray, ref BehaviourContext behaviourContext) : this() + { + MixId = UnusedMixId; + + DistanceFromFinalMix = InvalidDistanceFromFinalMix; + + DestinationMixId = UnusedMixId; + + DestinationSplitterId = UnusedSplitterId; + + unsafe + { + // SAFETY: safe as effectProcessingOrderArray comes from the work buffer memory that is pinned. + _effectProcessingOrderArrayPointer = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetReference(effectProcessingOrderArray.Span)); + } + + EffectProcessingOrderArrayMaxCount = (uint)effectProcessingOrderArray.Length; + + IsLongSizePreDelaySupported = behaviourContext.IsLongSizePreDelaySupported(); + + ClearEffectProcessingOrder(); + } + + /// <summary> + /// Clear the <see cref="DistanceFromFinalMix"/> value to its default state. + /// </summary> + public void ClearDistanceFromFinalMix() + { + DistanceFromFinalMix = InvalidDistanceFromFinalMix; + } + + /// <summary> + /// Clear the <see cref="EffectProcessingOrderArray"/> to its default state. + /// </summary> + public void ClearEffectProcessingOrder() + { + EffectProcessingOrderArray.Fill(-1); + } + + /// <summary> + /// Return true if the mix has any destinations. + /// </summary> + /// <returns>True if the mix has any destinations.</returns> + public bool HasAnyDestination() + { + return DestinationMixId != UnusedMixId || DestinationSplitterId != UnusedSplitterId; + } + + /// <summary> + /// Update the mix connection on the adjacency matrix. + /// </summary> + /// <param name="edgeMatrix">The adjacency matrix.</param> + /// <param name="parameter">The input parameter of the mix.</param> + /// <param name="splitterContext">The splitter context.</param> + /// <returns>Return true, new connections were done on the adjacency matrix.</returns> + private bool UpdateConnection(EdgeMatrix edgeMatrix, ref MixParameter parameter, ref SplitterContext splitterContext) + { + bool hasNewConnections; + + if (DestinationSplitterId == UnusedSplitterId) + { + hasNewConnections = false; + } + else + { + ref SplitterState splitter = ref splitterContext.GetState((int)DestinationSplitterId); + + hasNewConnections = splitter.HasNewConnection; + } + + if (DestinationMixId == parameter.DestinationMixId && DestinationSplitterId == parameter.DestinationSplitterId && !hasNewConnections) + { + return false; + } + + edgeMatrix.RemoveEdges(MixId); + + if (parameter.DestinationMixId == UnusedMixId) + { + if (parameter.DestinationSplitterId != UnusedSplitterId) + { + ref SplitterState splitter = ref splitterContext.GetState((int)parameter.DestinationSplitterId); + + for (int i = 0; i < splitter.DestinationCount; i++) + { + Span<SplitterDestination> destination = splitter.GetData(i); + + if (!destination.IsEmpty) + { + int destinationMixId = destination[0].DestinationId; + + if (destinationMixId != UnusedMixId) + { + edgeMatrix.Connect(MixId, destinationMixId); + } + } + } + } + } + else + { + edgeMatrix.Connect(MixId, parameter.DestinationMixId); + } + + DestinationMixId = parameter.DestinationMixId; + DestinationSplitterId = parameter.DestinationSplitterId; + + return true; + } + + /// <summary> + /// Update the mix from user information. + /// </summary> + /// <param name="edgeMatrix">The adjacency matrix.</param> + /// <param name="parameter">The input parameter of the mix.</param> + /// <param name="effectContext">The effect context.</param> + /// <param name="splitterContext">The splitter context.</param> + /// <param name="behaviourContext">The behaviour context.</param> + /// <returns>Return true if the mix was changed.</returns> + public bool Update(EdgeMatrix edgeMatrix, ref MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext) + { + bool isDirty; + + Volume = parameter.Volume; + SampleRate = parameter.SampleRate; + BufferCount = parameter.BufferCount; + IsUsed = parameter.IsUsed; + MixId = parameter.MixId; + NodeId = parameter.NodeId; + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + if (behaviourContext.IsSplitterSupported()) + { + isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext); + } + else + { + isDirty = DestinationMixId != parameter.DestinationMixId; + + if (DestinationMixId != parameter.DestinationMixId) + { + DestinationMixId = parameter.DestinationMixId; + } + + DestinationSplitterId = UnusedSplitterId; + } + + ClearEffectProcessingOrder(); + + for (int i = 0; i < effectContext.GetCount(); i++) + { + ref BaseEffect effect = ref effectContext.GetEffect(i); + + if (effect.MixId == MixId) + { + Debug.Assert(effect.ProcessingOrder <= EffectProcessingOrderArrayMaxCount); + + if (effect.ProcessingOrder > EffectProcessingOrderArrayMaxCount) + { + return isDirty; + } + + EffectProcessingOrderArray[(int)effect.ProcessingOrder] = i; + } + } + + return isDirty; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs new file mode 100644 index 00000000..893cc008 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceDetailEntry.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Represents a detailed entry in a performance frame. + /// </summary> + public interface IPerformanceDetailEntry + { + /// <summary> + /// Get the start time of this entry event (in microseconds). + /// </summary> + /// <returns>The start time of this entry event (in microseconds).</returns> + int GetStartTime(); + + /// <summary> + /// Get the start time offset in this structure. + /// </summary> + /// <returns>The start time offset in this structure.</returns> + int GetStartTimeOffset(); + + /// <summary> + /// Get the processing time of this entry event (in microseconds). + /// </summary> + /// <returns>The processing time of this entry event (in microseconds).</returns> + int GetProcessingTime(); + + /// <summary> + /// Get the processing time offset in this structure. + /// </summary> + /// <returns>The processing time offset in this structure.</returns> + int GetProcessingTimeOffset(); + + /// <summary> + /// Set the <paramref name="nodeId"/> of this entry. + /// </summary> + /// <param name="nodeId">The node id of this entry.</param> + void SetNodeId(int nodeId); + + /// <summary> + /// Set the <see cref="PerformanceEntryType"/> of this entry. + /// </summary> + /// <param name="type">The type to use.</param> + void SetEntryType(PerformanceEntryType type); + + /// <summary> + /// Set the <see cref="PerformanceDetailType"/> of this entry. + /// </summary> + /// <param name="detailType">The type to use.</param> + void SetDetailType(PerformanceDetailType detailType); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs new file mode 100644 index 00000000..2a2fa9cc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceEntry.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Represents an entry in a performance frame. + /// </summary> + public interface IPerformanceEntry + { + /// <summary> + /// Get the start time of this entry event (in microseconds). + /// </summary> + /// <returns>The start time of this entry event (in microseconds).</returns> + int GetStartTime(); + + /// <summary> + /// Get the start time offset in this structure. + /// </summary> + /// <returns>The start time offset in this structure.</returns> + int GetStartTimeOffset(); + + /// <summary> + /// Get the processing time of this entry event (in microseconds). + /// </summary> + /// <returns>The processing time of this entry event (in microseconds).</returns> + int GetProcessingTime(); + + /// <summary> + /// Get the processing time offset in this structure. + /// </summary> + /// <returns>The processing time offset in this structure.</returns> + int GetProcessingTimeOffset(); + + /// <summary> + /// Set the <paramref name="nodeId"/> of this entry. + /// </summary> + /// <param name="nodeId">The node id of this entry.</param> + void SetNodeId(int nodeId); + + /// <summary> + /// Set the <see cref="PerformanceEntryType"/> of this entry. + /// </summary> + /// <param name="type">The type to use.</param> + void SetEntryType(PerformanceEntryType type); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs new file mode 100644 index 00000000..5a26754d --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/IPerformanceHeader.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// The header of a performance frame. + /// </summary> + public interface IPerformanceHeader + { + /// <summary> + /// Get the entry count offset in this structure. + /// </summary> + /// <returns>The entry count offset in this structure.</returns> + int GetEntryCountOffset(); + + /// <summary> + /// Set the DSP running behind flag. + /// </summary> + /// <param name="isRunningBehind">The flag.</param> + void SetDspRunningBehind(bool isRunningBehind); + + /// <summary> + /// Set the count of voices that were dropped. + /// </summary> + /// <param name="voiceCount">The count of voices that were dropped.</param> + void SetVoiceDropCount(uint voiceCount); + + /// <summary> + /// Set the start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands) + /// </summary> + /// <param name="startTicks">The start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands)</param> + void SetStartRenderingTicks(ulong startTicks); + + /// <summary> + /// Set the header magic. + /// </summary> + /// <param name="magic">The header magic.</param> + void SetMagic(uint magic); + + /// <summary> + /// Set the offset of the next performance header. + /// </summary> + /// <param name="nextOffset">The offset of the next performance header.</param> + void SetNextOffset(int nextOffset); + + /// <summary> + /// Set the total time taken by all the commands profiled. + /// </summary> + /// <param name="totalProcessingTime">The total time taken by all the commands profiled.</param> + void SetTotalProcessingTime(int totalProcessingTime); + + /// <summary> + /// Set the index of this performance frame. + /// </summary> + /// <param name="index">The index of this performance frame.</param> + void SetIndex(uint index); + + /// <summary> + /// Get the total count of entries in this frame. + /// </summary> + /// <returns>The total count of entries in this frame.</returns> + int GetEntryCount(); + + /// <summary> + /// Get the total count of detailed entries in this frame. + /// </summary> + /// <returns>The total count of detailed entries in this frame.</returns> + int GetEntryDetailCount(); + + /// <summary> + /// Set the total count of entries in this frame. + /// </summary> + /// <param name="entryCount">The total count of entries in this frame.</param> + void SetEntryCount(int entryCount); + + /// <summary> + /// Set the total count of detailed entries in this frame. + /// </summary> + /// <param name="entryDetailCount">The total count of detailed entries in this frame.</param> + void SetEntryDetailCount(int entryDetailCount); + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs new file mode 100644 index 00000000..b7df7f28 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion1.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceDetailEntry"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + public struct PerformanceDetailVersion1 : IPerformanceDetailEntry + { + /// <summary> + /// The node id associated to this detailed entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this detailed entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this detailed entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The detailed entry type associated to this detailed entry. + /// </summary> + public PerformanceDetailType DetailType; + + /// <summary> + /// The entry type associated to this detailed entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetDetailType(PerformanceDetailType detailType) + { + DetailType = detailType; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs new file mode 100644 index 00000000..da898b29 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceDetailVersion2.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceDetailEntry"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceDetailVersion2 : IPerformanceDetailEntry + { + /// <summary> + /// The node id associated to this detailed entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this detailed entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this detailed entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The detailed entry type associated to this detailed entry. + /// </summary> + public PerformanceDetailType DetailType; + + /// <summary> + /// The entry type associated to this detailed entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetDetailType(PerformanceDetailType detailType) + { + DetailType = detailType; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs new file mode 100644 index 00000000..ad99867f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryAddresses.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Information used by the performance command to store informations in the performance entry. + /// </summary> + public class PerformanceEntryAddresses + { + /// <summary> + /// The memory storing the performance entry. + /// </summary> + public Memory<int> BaseMemory; + + /// <summary> + /// The offset to the start time field. + /// </summary> + public uint StartTimeOffset; + + /// <summary> + /// The offset to the entry count field. + /// </summary> + public uint EntryCountOffset; + + /// <summary> + /// The offset to the processing time field. + /// </summary> + public uint ProcessingTimeOffset; + + /// <summary> + /// Increment the entry count. + /// </summary> + public void IncrementEntryCount() + { + BaseMemory.Span[(int)EntryCountOffset / 4]++; + } + + /// <summary> + /// Set the start time in the entry. + /// </summary> + /// <param name="startTimeNano">The start time in nanoseconds.</param> + public void SetStartTime(ulong startTimeNano) + { + BaseMemory.Span[(int)StartTimeOffset / 4] = (int)(startTimeNano / 1000); + } + + /// <summary> + /// Set the processing time in the entry. + /// </summary> + /// <param name="endTimeNano">The end time in nanoseconds.</param> + public void SetProcessingTime(ulong endTimeNano) + { + BaseMemory.Span[(int)ProcessingTimeOffset / 4] = (int)(endTimeNano / 1000) - BaseMemory.Span[(int)StartTimeOffset / 4]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs new file mode 100644 index 00000000..ac91ca19 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion1.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceEntry"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)] + public struct PerformanceEntryVersion1 : IPerformanceEntry + { + /// <summary> + /// The node id associated to this entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The entry type associated to this entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs new file mode 100644 index 00000000..84ee5bc9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceEntryVersion2.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceEntry"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceEntryVersion2 : IPerformanceEntry + { + /// <summary> + /// The node id associated to this entry. + /// </summary> + public int NodeId; + + /// <summary> + /// The start time (in microseconds) associated to this entry. + /// </summary> + public int StartTime; + + /// <summary> + /// The processing time (in microseconds) associated to this entry. + /// </summary> + public int ProcessingTime; + + /// <summary> + /// The entry type associated to this entry. + /// </summary> + public PerformanceEntryType EntryType; + + public int GetProcessingTime() + { + return ProcessingTime; + } + + public int GetProcessingTimeOffset() + { + return 8; + } + + public int GetStartTime() + { + return StartTime; + } + + public int GetStartTimeOffset() + { + return 4; + } + + public void SetEntryType(PerformanceEntryType type) + { + EntryType = type; + } + + public void SetNodeId(int nodeId) + { + NodeId = nodeId; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs new file mode 100644 index 00000000..b0757b37 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion1.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceHeader"/> for performance metrics version 1. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)] + public struct PerformanceFrameHeaderVersion1 : IPerformanceHeader + { + /// <summary> + /// The magic of the performance header. + /// </summary> + public uint Magic; + + /// <summary> + /// The total count of entries in this frame. + /// </summary> + public int EntryCount; + + /// <summary> + /// The total count of detailed entries in this frame. + /// </summary> + public int EntryDetailCount; + + /// <summary> + /// The offset of the next performance header. + /// </summary> + public int NextOffset; + + /// <summary> + /// The total time taken by all the commands profiled. + /// </summary> + public int TotalProcessingTime; + + /// <summary> + /// The count of voices that were dropped. + /// </summary> + public uint VoiceDropCount; + + public int GetEntryCount() + { + return EntryCount; + } + + public int GetEntryCountOffset() + { + return 4; + } + + public int GetEntryDetailCount() + { + return EntryDetailCount; + } + + public void SetDspRunningBehind(bool isRunningBehind) + { + // NOTE: Not present in version 1 + } + + public void SetEntryCount(int entryCount) + { + EntryCount = entryCount; + } + + public void SetEntryDetailCount(int entryDetailCount) + { + EntryDetailCount = entryDetailCount; + } + + public void SetIndex(uint index) + { + // NOTE: Not present in version 1 + } + + public void SetMagic(uint magic) + { + Magic = magic; + } + + public void SetNextOffset(int nextOffset) + { + NextOffset = nextOffset; + } + + public void SetStartRenderingTicks(ulong startTicks) + { + // NOTE: not present in version 1 + } + + public void SetTotalProcessingTime(int totalProcessingTime) + { + TotalProcessingTime = totalProcessingTime; + } + + public void SetVoiceDropCount(uint voiceCount) + { + VoiceDropCount = voiceCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs new file mode 100644 index 00000000..ba873239 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceFrameHeaderVersion2.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// Implementation of <see cref="IPerformanceHeader"/> for performance metrics version 2. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x30)] + public struct PerformanceFrameHeaderVersion2 : IPerformanceHeader + { + /// <summary> + /// The magic of the performance header. + /// </summary> + public uint Magic; + + /// <summary> + /// The total count of entries in this frame. + /// </summary> + public int EntryCount; + + /// <summary> + /// The total count of detailed entries in this frame. + /// </summary> + public int EntryDetailCount; + + /// <summary> + /// The offset of the next performance header. + /// </summary> + public int NextOffset; + + /// <summary> + /// The total time taken by all the commands profiled. + /// </summary> + public int TotalProcessingTime; + + /// <summary> + /// The count of voices that were dropped. + /// </summary> + public uint VoiceDropCount; + + /// <summary> + /// The start ticks of the <see cref="Dsp.AudioProcessor"/>. (before sending commands) + /// </summary> + public ulong StartRenderingTicks; + + /// <summary> + /// The index of this performance frame. + /// </summary> + public uint Index; + + /// <summary> + /// If set to true, the DSP is running behind. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsDspRunningBehind; + + public int GetEntryCount() + { + return EntryCount; + } + + public int GetEntryCountOffset() + { + return 4; + } + + public int GetEntryDetailCount() + { + return EntryDetailCount; + } + + public void SetDspRunningBehind(bool isRunningBehind) + { + IsDspRunningBehind = isRunningBehind; + } + + public void SetEntryCount(int entryCount) + { + EntryCount = entryCount; + } + + public void SetEntryDetailCount(int entryDetailCount) + { + EntryDetailCount = entryDetailCount; + } + + public void SetIndex(uint index) + { + Index = index; + } + + public void SetMagic(uint magic) + { + Magic = magic; + } + + public void SetNextOffset(int nextOffset) + { + NextOffset = nextOffset; + } + + public void SetStartRenderingTicks(ulong startTicks) + { + StartRenderingTicks = startTicks; + } + + public void SetTotalProcessingTime(int totalProcessingTime) + { + TotalProcessingTime = totalProcessingTime; + } + + public void SetVoiceDropCount(uint voiceCount) + { + VoiceDropCount = voiceCount; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs new file mode 100644 index 00000000..3a336af3 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManager.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + public abstract class PerformanceManager + { + /// <summary> + /// Get the required size for a single performance frame. + /// </summary> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="behaviourContext">The behaviour context.</param> + /// <returns>The required size for a single performance frame.</returns> + public static ulong GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref AudioRendererConfiguration parameter, ref BehaviourContext behaviourContext) + { + uint version = behaviourContext.GetPerformanceMetricsDataFormat(); + + if (version == 2) + { + return (ulong)PerformanceManagerGeneric<PerformanceFrameHeaderVersion2, + PerformanceEntryVersion2, + PerformanceDetailVersion2>.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + } + else if (version == 1) + { + return (ulong)PerformanceManagerGeneric<PerformanceFrameHeaderVersion1, + PerformanceEntryVersion1, + PerformanceDetailVersion1>.GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + } + + throw new NotImplementedException($"Unknown Performance metrics data format version {version}"); + } + + /// <summary> + /// Copy the performance frame history to the supplied user buffer and returns the size copied. + /// </summary> + /// <param name="performanceOutput">The supplied user buffer to store the performance frame into.</param> + /// <returns>The size copied to the supplied buffer.</returns> + public abstract uint CopyHistories(Span<byte> performanceOutput); + + /// <summary> + /// Set the target node id to profile. + /// </summary> + /// <param name="target">The target node id to profile.</param> + public abstract void SetTargetNodeId(int target); + + /// <summary> + /// Check if the given target node id is profiled. + /// </summary> + /// <param name="target">The target node id to check.</param> + /// <returns>Return true, if the given target node id is profiled.</returns> + public abstract bool IsTargetNodeId(int target); + + /// <summary> + /// Get the next buffer to store a performance entry. + /// </summary> + /// <param name="performanceEntry">The output <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="entryType">The <see cref="PerformanceEntryType"/> info.</param> + /// <param name="nodeId">The node id of the entry.</param> + /// <returns>Return true, if a valid <see cref="PerformanceEntryAddresses"/> was returned.</returns> + public abstract bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceEntryType entryType, int nodeId); + + /// <summary> + /// Get the next buffer to store a performance detailed entry. + /// </summary> + /// <param name="performanceEntry">The output <see cref="PerformanceEntryAddresses"/>.</param> + /// <param name="detailType">The <see cref="PerformanceDetailType"/> info.</param> + /// <param name="entryType">The <see cref="PerformanceEntryType"/> info.</param> + /// <param name="nodeId">The node id of the entry.</param> + /// <returns>Return true, if a valid <see cref="PerformanceEntryAddresses"/> was returned.</returns> + public abstract bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceDetailType detailType, PerformanceEntryType entryType, int nodeId); + + /// <summary> + /// Finalize the current performance frame. + /// </summary> + /// <param name="dspRunningBehind">Indicate if the DSP is running behind.</param> + /// <param name="voiceDropCount">The count of voices that were dropped.</param> + /// <param name="startRenderingTicks">The start ticks of the audio rendering.</param> + public abstract void TapFrame(bool dspRunningBehind, uint voiceDropCount, ulong startRenderingTicks); + + /// <summary> + /// Create a new <see cref="PerformanceManager"/>. + /// </summary> + /// <param name="performanceBuffer">The backing memory available for use by the manager.</param> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="behaviourContext">The behaviour context;</param> + /// <returns>A new <see cref="PerformanceManager"/>.</returns> + public static PerformanceManager Create(Memory<byte> performanceBuffer, ref AudioRendererConfiguration parameter, BehaviourContext behaviourContext) + { + uint version = behaviourContext.GetPerformanceMetricsDataFormat(); + + switch (version) + { + case 1: + return new PerformanceManagerGeneric<PerformanceFrameHeaderVersion1, PerformanceEntryVersion1, PerformanceDetailVersion1>(performanceBuffer, + ref parameter); + case 2: + return new PerformanceManagerGeneric<PerformanceFrameHeaderVersion2, PerformanceEntryVersion2, PerformanceDetailVersion2>(performanceBuffer, + ref parameter); + default: + throw new NotImplementedException($"Unknown Performance metrics data format version {version}"); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs new file mode 100644 index 00000000..b17e6f3f --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Performance/PerformanceManagerGeneric.cs @@ -0,0 +1,311 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Performance +{ + /// <summary> + /// A Generic implementation of <see cref="PerformanceManager"/>. + /// </summary> + /// <typeparam name="THeader">The header implementation of the performance frame.</typeparam> + /// <typeparam name="TEntry">The entry implementation of the performance frame.</typeparam> + /// <typeparam name="TEntryDetail">A detailed implementation of the performance frame.</typeparam> + public class PerformanceManagerGeneric<THeader, TEntry, TEntryDetail> : PerformanceManager where THeader: unmanaged, IPerformanceHeader where TEntry : unmanaged, IPerformanceEntry where TEntryDetail: unmanaged, IPerformanceDetailEntry + { + /// <summary> + /// The magic used for the <see cref="THeader"/>. + /// </summary> + private const uint MagicPerformanceBuffer = 0x46524550; + + /// <summary> + /// The fixed amount of <see cref="TEntryDetail"/> that can be stored in a frame. + /// </summary> + private const int MaxFrameDetailCount = 100; + + private Memory<byte> _buffer; + private Memory<byte> _historyBuffer; + + private Memory<byte> CurrentBuffer => _buffer.Slice(0, _frameSize); + private Memory<byte> CurrentBufferData => CurrentBuffer.Slice(Unsafe.SizeOf<THeader>()); + + private ref THeader CurrentHeader => ref MemoryMarshal.Cast<byte, THeader>(CurrentBuffer.Span)[0]; + + private Span<TEntry> Entries => MemoryMarshal.Cast<byte, TEntry>(CurrentBufferData.Span.Slice(0, GetEntriesSize())); + private Span<TEntryDetail> EntriesDetail => MemoryMarshal.Cast<byte, TEntryDetail>(CurrentBufferData.Span.Slice(GetEntriesSize(), GetEntriesDetailSize())); + + private int _frameSize; + private int _availableFrameCount; + private int _entryCountPerFrame; + private int _detailTarget; + private int _entryIndex; + private int _entryDetailIndex; + private int _indexHistoryWrite; + private int _indexHistoryRead; + private uint _historyFrameIndex; + + public PerformanceManagerGeneric(Memory<byte> buffer, ref AudioRendererConfiguration parameter) + { + _buffer = buffer; + _frameSize = GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref parameter); + + _entryCountPerFrame = (int)GetEntryCount(ref parameter); + _availableFrameCount = buffer.Length / _frameSize - 1; + + _historyFrameIndex = 0; + + _historyBuffer = _buffer.Slice(_frameSize); + + SetupNewHeader(); + } + + private Span<byte> GetBufferFromIndex(Span<byte> data, int index) + { + return data.Slice(index * _frameSize, _frameSize); + } + + private ref THeader GetHeaderFromBuffer(Span<byte> data, int index) + { + return ref MemoryMarshal.Cast<byte, THeader>(GetBufferFromIndex(data, index))[0]; + } + + private Span<TEntry> GetEntriesFromBuffer(Span<byte> data, int index) + { + return MemoryMarshal.Cast<byte, TEntry>(GetBufferFromIndex(data, index).Slice(Unsafe.SizeOf<THeader>(), GetEntriesSize())); + } + + private Span<TEntryDetail> GetEntriesDetailFromBuffer(Span<byte> data, int index) + { + return MemoryMarshal.Cast<byte, TEntryDetail>(GetBufferFromIndex(data, index).Slice(Unsafe.SizeOf<THeader>() + GetEntriesSize(), GetEntriesDetailSize())); + } + + private void SetupNewHeader() + { + _entryIndex = 0; + _entryDetailIndex = 0; + + CurrentHeader.SetEntryCount(0); + CurrentHeader.SetEntryDetailCount(0); + } + + public static uint GetEntryCount(ref AudioRendererConfiguration parameter) + { + return parameter.VoiceCount + parameter.EffectCount + parameter.SubMixBufferCount + parameter.SinkCount + 1; + } + + public int GetEntriesSize() + { + return Unsafe.SizeOf<TEntry>() * _entryCountPerFrame; + } + + public static int GetEntriesDetailSize() + { + return Unsafe.SizeOf<TEntryDetail>() * MaxFrameDetailCount; + } + + public static int GetRequiredBufferSizeForPerformanceMetricsPerFrame(ref AudioRendererConfiguration parameter) + { + return Unsafe.SizeOf<TEntry>() * (int)GetEntryCount(ref parameter) + GetEntriesDetailSize() + Unsafe.SizeOf<THeader>(); + } + + public override uint CopyHistories(Span<byte> performanceOutput) + { + if (performanceOutput.IsEmpty) + { + return 0; + } + + int nextOffset = 0; + + while (_indexHistoryRead != _indexHistoryWrite) + { + if (nextOffset >= performanceOutput.Length) + { + break; + } + + ref THeader inputHeader = ref GetHeaderFromBuffer(_historyBuffer.Span, _indexHistoryRead); + Span<TEntry> inputEntries = GetEntriesFromBuffer(_historyBuffer.Span, _indexHistoryRead); + Span<TEntryDetail> inputEntriesDetail = GetEntriesDetailFromBuffer(_historyBuffer.Span, _indexHistoryRead); + + Span<byte> targetSpan = performanceOutput.Slice(nextOffset); + + ref THeader outputHeader = ref MemoryMarshal.Cast<byte, THeader>(targetSpan)[0]; + + nextOffset += Unsafe.SizeOf<THeader>(); + + Span<TEntry> outputEntries = MemoryMarshal.Cast<byte, TEntry>(targetSpan.Slice(nextOffset)); + + int totalProcessingTime = 0; + + int effectiveEntryCount = 0; + + for (int entryIndex = 0; entryIndex < inputHeader.GetEntryCount(); entryIndex++) + { + ref TEntry input = ref inputEntries[entryIndex]; + + if (input.GetProcessingTime() != 0 || input.GetStartTime() != 0) + { + ref TEntry output = ref outputEntries[effectiveEntryCount++]; + + output = input; + + nextOffset += Unsafe.SizeOf<TEntry>(); + + totalProcessingTime += input.GetProcessingTime(); + } + } + + Span<TEntryDetail> outputEntriesDetail = MemoryMarshal.Cast<byte, TEntryDetail>(targetSpan.Slice(nextOffset)); + + int effectiveEntryDetailCount = 0; + + for (int entryDetailIndex = 0; entryDetailIndex < inputHeader.GetEntryDetailCount(); entryDetailIndex++) + { + ref TEntryDetail input = ref inputEntriesDetail[entryDetailIndex]; + + if (input.GetProcessingTime() != 0 || input.GetStartTime() != 0) + { + ref TEntryDetail output = ref outputEntriesDetail[effectiveEntryDetailCount++]; + + output = input; + + nextOffset += Unsafe.SizeOf<TEntryDetail>(); + } + } + + outputHeader = inputHeader; + outputHeader.SetMagic(MagicPerformanceBuffer); + outputHeader.SetTotalProcessingTime(totalProcessingTime); + outputHeader.SetNextOffset(nextOffset); + outputHeader.SetEntryCount(effectiveEntryCount); + outputHeader.SetEntryDetailCount(effectiveEntryDetailCount); + + _indexHistoryRead = (_indexHistoryRead + 1) % _availableFrameCount; + } + + if (nextOffset < performanceOutput.Length && (performanceOutput.Length - nextOffset) >= Unsafe.SizeOf<THeader>()) + { + ref THeader outputHeader = ref MemoryMarshal.Cast<byte, THeader>(performanceOutput.Slice(nextOffset))[0]; + + outputHeader = default; + } + + return (uint)nextOffset; + } + + public override bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceEntryType entryType, int nodeId) + { + performanceEntry = new PerformanceEntryAddresses(); + performanceEntry.BaseMemory = SpanMemoryManager<int>.Cast(CurrentBuffer); + performanceEntry.EntryCountOffset = (uint)CurrentHeader.GetEntryCountOffset(); + + uint baseEntryOffset = (uint)(Unsafe.SizeOf<THeader>() + Unsafe.SizeOf<TEntry>() * _entryIndex); + + ref TEntry entry = ref Entries[_entryIndex]; + + performanceEntry.StartTimeOffset = baseEntryOffset + (uint)entry.GetStartTimeOffset(); + performanceEntry.ProcessingTimeOffset = baseEntryOffset + (uint)entry.GetProcessingTimeOffset(); + + entry = default; + entry.SetEntryType(entryType); + entry.SetNodeId(nodeId); + + _entryIndex++; + + return true; + } + + public override bool GetNextEntry(out PerformanceEntryAddresses performanceEntry, PerformanceDetailType detailType, PerformanceEntryType entryType, int nodeId) + { + performanceEntry = null; + + if (_entryDetailIndex > MaxFrameDetailCount) + { + return false; + } + + performanceEntry = new PerformanceEntryAddresses(); + performanceEntry.BaseMemory = SpanMemoryManager<int>.Cast(CurrentBuffer); + performanceEntry.EntryCountOffset = (uint)CurrentHeader.GetEntryCountOffset(); + + uint baseEntryOffset = (uint)(Unsafe.SizeOf<THeader>() + GetEntriesSize() + Unsafe.SizeOf<IPerformanceDetailEntry>() * _entryDetailIndex); + + ref TEntryDetail entryDetail = ref EntriesDetail[_entryDetailIndex]; + + performanceEntry.StartTimeOffset = baseEntryOffset + (uint)entryDetail.GetStartTimeOffset(); + performanceEntry.ProcessingTimeOffset = baseEntryOffset + (uint)entryDetail.GetProcessingTimeOffset(); + + entryDetail = default; + entryDetail.SetDetailType(detailType); + entryDetail.SetEntryType(entryType); + entryDetail.SetNodeId(nodeId); + + _entryDetailIndex++; + + return true; + } + + public override bool IsTargetNodeId(int target) + { + return _detailTarget == target; + } + + public override void SetTargetNodeId(int target) + { + _detailTarget = target; + } + + public override void TapFrame(bool dspRunningBehind, uint voiceDropCount, ulong startRenderingTicks) + { + if (_availableFrameCount > 0) + { + int targetIndexForHistory = _indexHistoryWrite; + + _indexHistoryWrite = (_indexHistoryWrite + 1) % _availableFrameCount; + + ref THeader targetHeader = ref GetHeaderFromBuffer(_historyBuffer.Span, targetIndexForHistory); + + CurrentBuffer.Span.CopyTo(GetBufferFromIndex(_historyBuffer.Span, targetIndexForHistory)); + + uint targetHistoryFrameIndex = _historyFrameIndex; + + if (_historyFrameIndex == uint.MaxValue) + { + _historyFrameIndex = 0; + } + else + { + _historyFrameIndex++; + } + + targetHeader.SetDspRunningBehind(dspRunningBehind); + targetHeader.SetVoiceDropCount(voiceDropCount); + targetHeader.SetStartRenderingTicks(startRenderingTicks); + targetHeader.SetIndex(targetHistoryFrameIndex); + + // Finally setup the new header + SetupNewHeader(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs b/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs new file mode 100644 index 00000000..a89a8a60 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/RendererSystemContext.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.Upsampler; +using System; + +namespace Ryujinx.Audio.Renderer.Server +{ + /// <summary> + /// Represents a lite version of <see cref="AudioRenderSystem"/> used by the <see cref="Dsp.AudioProcessor"/> + /// </summary> + /// <remarks> + /// This also allows to reduce dependencies on the <see cref="AudioRenderSystem"/> for unit testing. + /// </remarks> + public sealed class RendererSystemContext + { + /// <summary> + /// The session id of the current renderer. + /// </summary> + public int SessionId; + + /// <summary> + /// The target channel count for sink. + /// </summary> + /// <remarks>See <see cref="CommandGenerator.GenerateDevice(Sink.DeviceSink, ref Mix.MixState)"/> for usage.</remarks> + public uint ChannelCount; + + /// <summary> + /// The total count of mix buffer. + /// </summary> + public uint MixBufferCount; + + /// <summary> + /// Instance of the <see cref="BehaviourContext"/> used to derive bug fixes and features of the current audio renderer revision. + /// </summary> + public BehaviourContext BehaviourContext; + + /// <summary> + /// Instance of the <see cref="UpsamplerManager"/> used for upsampling (see <see cref="UpsamplerState"/>) + /// </summary> + public UpsamplerManager UpsamplerManager; + + /// <summary> + /// The memory to use for depop processing. + /// </summary> + /// <remarks> + /// See <see cref="Dsp.Command.DepopForMixBuffersCommand"/> and <see cref="Dsp.Command.DepopPrepareCommand"/> + /// </remarks> + public Memory<float> DepopBuffer; + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs new file mode 100644 index 00000000..949e3971 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/BaseSink.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Base class used for server information of a sink. + /// </summary> + public class BaseSink + { + /// <summary> + /// The type of this <see cref="BaseSink"/>. + /// </summary> + public SinkType Type; + + /// <summary> + /// Set to true if the sink is used. + /// </summary> + public bool IsUsed; + + /// <summary> + /// Set to true if the sink need to be skipped because of invalid state. + /// </summary> + public bool ShouldSkip; + + /// <summary> + /// The node id of the sink. + /// </summary> + public int NodeId; + + /// <summary> + /// Create a new <see cref="BaseSink"/>. + /// </summary> + public BaseSink() + { + CleanUp(); + } + + /// <summary> + /// Clean up the internal state of the <see cref="BaseSink"/>. + /// </summary> + public virtual void CleanUp() + { + Type = TargetSinkType; + IsUsed = false; + ShouldSkip = false; + } + + /// <summary> + /// The target <see cref="SinkType"/> handled by this <see cref="BaseSink"/>. + /// </summary> + public virtual SinkType TargetSinkType => SinkType.Invalid; + + /// <summary> + /// Check if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>. + /// </summary> + /// <param name="parameter">The user parameter.</param> + /// <returns>Return true, if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>.</returns> + public bool IsTypeValid(ref SinkInParameter parameter) + { + return parameter.Type == TargetSinkType; + } + + /// <summary> + /// Update the <see cref="BaseSink"/> state during command generation. + /// </summary> + public virtual void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetSinkType); + } + + /// <summary> + /// Update the internal common parameters from user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + protected void UpdateStandardParameter(ref SinkInParameter parameter) + { + if (IsUsed != parameter.IsUsed) + { + IsUsed = parameter.IsUsed; + NodeId = parameter.NodeId; + } + } + + /// <summary> + /// Update the internal state from user parameter. + /// </summary> + /// <param name="errorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="outStatus">The user output status.</param> + /// <param name="mapper">The mapper to use.</param> + public virtual void Update(out ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + errorInfo = new ErrorInfo(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs new file mode 100644 index 00000000..ceb955e5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/CircularBufferSink.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Sink; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Server information for a circular buffer sink. + /// </summary> + public class CircularBufferSink : BaseSink + { + /// <summary> + /// The circular buffer parameter. + /// </summary> + public CircularBufferParameter Parameter; + + /// <summary> + /// The last written data offset on the circular buffer. + /// </summary> + private uint _lastWrittenOffset; + + /// <summary> + /// THe previous written offset of the circular buffer. + /// </summary> + private uint _oldWrittenOffset; + + /// <summary> + /// The current offset to write data on the circular buffer. + /// </summary> + public uint CurrentWriteOffset { get; private set; } + + /// <summary> + /// The <see cref="AddressInfo"/> of the circular buffer. + /// </summary> + public AddressInfo CircularBufferAddressInfo; + + public CircularBufferSink() + { + CircularBufferAddressInfo = AddressInfo.Create(); + } + + public override SinkType TargetSinkType => SinkType.CircularBuffer; + + public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper) + { + errorInfo = new BehaviourParameter.ErrorInfo(); + outStatus = new SinkOutStatus(); + + Debug.Assert(IsTypeValid(ref parameter)); + + ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, CircularBufferParameter>(parameter.SpecificData)[0]; + + if (parameter.IsUsed != IsUsed || ShouldSkip) + { + UpdateStandardParameter(ref parameter); + + if (parameter.IsUsed) + { + Debug.Assert(CircularBufferAddressInfo.CpuAddress == 0); + Debug.Assert(CircularBufferAddressInfo.GetReference(false) == 0); + + ShouldSkip = !mapper.TryAttachBuffer(out errorInfo, ref CircularBufferAddressInfo, inputDeviceParameter.BufferAddress, inputDeviceParameter.BufferSize); + } + else + { + Debug.Assert(CircularBufferAddressInfo.CpuAddress != 0); + Debug.Assert(CircularBufferAddressInfo.GetReference(false) != 0); + } + + Parameter = inputDeviceParameter; + } + + outStatus.LastWrittenOffset = _lastWrittenOffset; + } + + public override void UpdateForCommandGeneration() + { + Debug.Assert(Type == TargetSinkType); + + if (IsUsed) + { + uint frameSize = Constants.TargetSampleSize * Parameter.SampleCount * Parameter.InputCount; + + _lastWrittenOffset = _oldWrittenOffset; + + _oldWrittenOffset = CurrentWriteOffset; + + CurrentWriteOffset += frameSize; + + if (Parameter.BufferSize > 0) + { + CurrentWriteOffset %= Parameter.BufferSize; + } + } + } + + public override void CleanUp() + { + CircularBufferAddressInfo = AddressInfo.Create(); + _lastWrittenOffset = 0; + _oldWrittenOffset = 0; + CurrentWriteOffset = 0; + base.CleanUp(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs b/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs new file mode 100644 index 00000000..79af05f7 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/DeviceSink.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Sink; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Upsampler; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Server information for a device sink. + /// </summary> + public class DeviceSink : BaseSink + { + /// <summary> + /// The downmix coefficients. + /// </summary> + public float[] DownMixCoefficients; + + /// <summary> + /// The device parameters. + /// </summary> + public DeviceParameter Parameter; + + /// <summary> + /// The upsampler instance used by this sink. + /// </summary> + /// <remarks>Null if no upsampling is needed.</remarks> + public UpsamplerState UpsamplerState; + + /// <summary> + /// Create a new <see cref="DeviceSink"/>. + /// </summary> + public DeviceSink() + { + DownMixCoefficients = new float[4]; + } + + public override void CleanUp() + { + UpsamplerState?.Release(); + + UpsamplerState = null; + + base.CleanUp(); + } + + public override SinkType TargetSinkType => SinkType.Device; + + public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper) + { + Debug.Assert(IsTypeValid(ref parameter)); + + ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, DeviceParameter>(parameter.SpecificData)[0]; + + if (parameter.IsUsed != IsUsed) + { + UpdateStandardParameter(ref parameter); + Parameter = inputDeviceParameter; + } + else + { + Parameter.DownMixParameterEnabled = inputDeviceParameter.DownMixParameterEnabled; + inputDeviceParameter.DownMixParameter.ToSpan().CopyTo(Parameter.DownMixParameter.ToSpan()); + } + + Parameter.DownMixParameter.ToSpan().CopyTo(DownMixCoefficients.AsSpan()); + + errorInfo = new BehaviourParameter.ErrorInfo(); + outStatus = new SinkOutStatus(); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs b/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs new file mode 100644 index 00000000..b57dbde9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Sink/SinkContext.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Sink +{ + /// <summary> + /// Sink context. + /// </summary> + public class SinkContext + { + /// <summary> + /// Storage for <see cref="BaseSink"/>. + /// </summary> + private BaseSink[] _sinks; + + /// <summary> + /// The total sink count. + /// </summary> + private uint _sinkCount; + + /// <summary> + /// Initialize the <see cref="SinkContext"/>. + /// </summary> + /// <param name="sinksCount">The total sink count.</param> + public void Initialize(uint sinksCount) + { + _sinkCount = sinksCount; + _sinks = new BaseSink[_sinkCount]; + + for (int i = 0; i < _sinkCount; i++) + { + _sinks[i] = new BaseSink(); + } + } + + /// <summary> + /// Get the total sink count. + /// </summary> + /// <returns>The total sink count.</returns> + public uint GetCount() + { + return _sinkCount; + } + + /// <summary> + /// Get a reference to a <see cref="BaseSink"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="BaseSink"/> at the given <paramref name="id"/>.</returns> + public ref BaseSink GetSink(int id) + { + Debug.Assert(id >= 0 && id < _sinkCount); + + return ref _sinks[id]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs new file mode 100644 index 00000000..0e107a53 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs @@ -0,0 +1,320 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Splitter context. + /// </summary> + public class SplitterContext + { + /// <summary> + /// Storage for <see cref="SplitterState"/>. + /// </summary> + private Memory<SplitterState> _splitters; + + /// <summary> + /// Storage for <see cref="SplitterDestination"/>. + /// </summary> + private Memory<SplitterDestination> _splitterDestinations; + + /// <summary> + /// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>. + /// </summary> + public bool IsBugFixed { get; private set; } + + /// <summary> + /// Initialize <see cref="SplitterContext"/>. + /// </summary> + /// <param name="behaviourContext">The behaviour context.</param> + /// <param name="parameter">The audio renderer configuration.</param> + /// <param name="workBufferAllocator">The <see cref="WorkBufferAllocator"/>.</param> + /// <returns>Return true if the initialization was successful.</returns> + public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator) + { + if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0) + { + Setup(Memory<SplitterState>.Empty, Memory<SplitterDestination>.Empty, false); + + return true; + } + + Memory<SplitterState> splitters = workBufferAllocator.Allocate<SplitterState>(parameter.SplitterCount, SplitterState.Alignment); + + if (splitters.IsEmpty) + { + return false; + } + + int splitterId = 0; + + foreach (ref SplitterState splitter in splitters.Span) + { + splitter = new SplitterState(splitterId++); + } + + Memory<SplitterDestination> splitterDestinations = workBufferAllocator.Allocate<SplitterDestination>(parameter.SplitterDestinationCount, + SplitterDestination.Alignment); + + if (splitterDestinations.IsEmpty) + { + return false; + } + + int splitterDestinationId = 0; + foreach (ref SplitterDestination data in splitterDestinations.Span) + { + data = new SplitterDestination(splitterDestinationId++); + } + + SplitterState.InitializeSplitters(splitters.Span); + + Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed()); + + return true; + } + + /// <summary> + /// Get the work buffer size while adding the size needed for splitter to operate. + /// </summary> + /// <param name="size">The current size.</param> + /// <param name="behaviourContext">The behaviour context.</param> + /// <param name="parameter">The renderer configuration.</param> + /// <returns>Return the new size taking splitter into account.</returns> + public static ulong GetWorkBufferSize(ulong size, ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter) + { + if (behaviourContext.IsSplitterSupported()) + { + size = WorkBufferAllocator.GetTargetSize<SplitterState>(size, parameter.SplitterCount, SplitterState.Alignment); + size = WorkBufferAllocator.GetTargetSize<SplitterDestination>(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment); + + if (behaviourContext.IsSplitterBugFixed()) + { + size = WorkBufferAllocator.GetTargetSize<int>(size, parameter.SplitterDestinationCount, 0x10); + } + + return size; + } + else + { + return size; + } + } + + /// <summary> + /// Setup the <see cref="SplitterContext"/> instance. + /// </summary> + /// <param name="splitters">The <see cref="SplitterState"/> storage.</param> + /// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param> + /// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.</param> + private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed) + { + _splitters = splitters; + _splitterDestinations = splitterDestinations; + IsBugFixed = isBugFixed; + } + + /// <summary> + /// Clear the new connection flag. + /// </summary> + private void ClearAllNewConnectionFlag() + { + foreach (ref SplitterState splitter in _splitters.Span) + { + splitter.ClearNewConnectionFlag(); + } + } + + /// <summary> + /// Get the destination count using the count of splitter. + /// </summary> + /// <returns>The destination count using the count of splitter.</returns> + public int GetDestinationCountPerStateForCompatibility() + { + if (_splitters.IsEmpty) + { + return 0; + } + + return _splitterDestinations.Length / _splitters.Length; + } + + /// <summary> + /// Update one or multiple <see cref="SplitterState"/> from user parameters. + /// </summary> + /// <param name="inputHeader">The splitter header.</param> + /// <param name="input">The raw data after the splitter header.</param> + private void UpdateState(ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input) + { + for (int i = 0; i < inputHeader.SplitterCount; i++) + { + SplitterInParameter parameter = MemoryMarshal.Read<SplitterInParameter>(input); + + Debug.Assert(parameter.IsMagicValid()); + + if (parameter.IsMagicValid()) + { + if (parameter.Id >= 0 && parameter.Id < _splitters.Length) + { + ref SplitterState splitter = ref GetState(parameter.Id); + + splitter.Update(this, ref parameter, input.Slice(Unsafe.SizeOf<SplitterInParameter>())); + } + + input = input.Slice(0x1C + (int)parameter.DestinationCount * 4); + } + } + } + + /// <summary> + /// Update one or multiple <see cref="SplitterDestination"/> from user parameters. + /// </summary> + /// <param name="inputHeader">The splitter header.</param> + /// <param name="input">The raw data after the splitter header.</param> + private void UpdateData(ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input) + { + for (int i = 0; i < inputHeader.SplitterDestinationCount; i++) + { + SplitterDestinationInParameter parameter = MemoryMarshal.Read<SplitterDestinationInParameter>(input); + + Debug.Assert(parameter.IsMagicValid()); + + if (parameter.IsMagicValid()) + { + if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length) + { + ref SplitterDestination destination = ref GetDestination(parameter.Id); + + destination.Update(parameter); + } + + input = input.Slice(Unsafe.SizeOf<SplitterDestinationInParameter>()); + } + } + } + + /// <summary> + /// Update splitter from user parameters. + /// </summary> + /// <param name="input">The input raw user data.</param> + /// <param name="consumedSize">The total consumed size.</param> + /// <returns>Return true if the update was successful.</returns> + public bool Update(ReadOnlySpan<byte> input, out int consumedSize) + { + if (_splitterDestinations.IsEmpty || _splitters.IsEmpty) + { + consumedSize = 0; + + return true; + } + + int originalSize = input.Length; + + SplitterInParameterHeader header = SpanIOHelper.Read<SplitterInParameterHeader>(ref input); + + if (header.IsMagicValid()) + { + ClearAllNewConnectionFlag(); + + UpdateState(ref header, ref input); + UpdateData(ref header, ref input); + + consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10); + + return true; + } + else + { + consumedSize = 0; + + return false; + } + } + + /// <summary> + /// Get a reference to a <see cref="SplitterState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="SplitterState"/> at the given <paramref name="id"/>.</returns> + public ref SplitterState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_splitters, id, (uint)_splitters.Length); + } + + /// <summary> + /// Get a reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="SplitterDestination"/> at the given <paramref name="id"/>.</returns> + public ref SplitterDestination GetDestination(int id) + { + return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); + } + + /// <summary> + /// Get a <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{SplitterDestination}"/> at the given <paramref name="id"/>.</returns> + public Memory<SplitterDestination> GetDestinationMemory(int id) + { + return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length); + } + + /// <summary> + /// Get a <see cref="Span{SplitterDestination}"/> in the <see cref="SplitterState"/> at <paramref name="id"/> and pass <paramref name="destinationId"/> to <see cref="SplitterState.GetData(int)"/>. + /// </summary> + /// <param name="id">The index to use to get the <see cref="SplitterState"/>.</param> + /// <param name="destinationId">The index of the <see cref="SplitterDestination"/>.</param> + /// <returns>A <see cref="Span{SplitterDestination}"/>.</returns> + public Span<SplitterDestination> GetDestination(int id, int destinationId) + { + ref SplitterState splitter = ref GetState(id); + + return splitter.GetData(destinationId); + } + + /// <summary> + /// Return true if the audio renderer has any splitters. + /// </summary> + /// <returns>True if the audio renderer has any splitters.</returns> + public bool UsingSplitter() + { + return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty; + } + + /// <summary> + /// Update the internal state of all splitters. + /// </summary> + public void UpdateInternalState() + { + foreach (ref SplitterState splitter in _splitters.Span) + { + splitter.UpdateInternalState(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs new file mode 100644 index 00000000..077bf8ae --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs @@ -0,0 +1,210 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Server state for a splitter destination. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)] + public struct SplitterDestination + { + public const int Alignment = 0x10; + + /// <summary> + /// The unique id of this <see cref="SplitterDestination"/>. + /// </summary> + public int Id; + + /// <summary> + /// The mix to output the result of the splitter. + /// </summary> + public int DestinationId; + + /// <summary> + /// Mix buffer volumes storage. + /// </summary> + private MixArray _mix; + private MixArray _previousMix; + + /// <summary> + /// Pointer to the next linked element. + /// </summary> + private unsafe SplitterDestination* _next; + + /// <summary> + /// Set to true if in use. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + /// <summary> + /// Set to true if the internal state need to be updated. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool NeedToUpdateInternalState; + + [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)] + private struct MixArray { } + + /// <summary> + /// Mix buffer volumes. + /// </summary> + /// <remarks>Used when a splitter id is specified in the mix.</remarks> + public Span<float> MixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _mix); + + /// <summary> + /// Previous mix buffer volumes. + /// </summary> + /// <remarks>Used when a splitter id is specified in the mix.</remarks> + public Span<float> PreviousMixBufferVolume => SpanHelpers.AsSpan<MixArray, float>(ref _previousMix); + + /// <summary> + /// Get the <see cref="Span{SplitterDestination}"/> of the next element or <see cref="Span{SplitterDestination}.Empty"/> if not present. + /// </summary> + public Span<SplitterDestination> Next + { + get + { + unsafe + { + return _next != null ? new Span<SplitterDestination>(_next, 1) : Span<SplitterDestination>.Empty; + } + } + } + + /// <summary> + /// Create a new <see cref="SplitterDestination"/>. + /// </summary> + /// <param name="id">The unique id of this <see cref="SplitterDestination"/>.</param> + public SplitterDestination(int id) : this() + { + Id = id; + DestinationId = Constants.UnusedMixId; + + ClearVolumes(); + } + + /// <summary> + /// Update the <see cref="SplitterDestination"/> from user parameter. + /// </summary> + /// <param name="parameter">The user parameter.</param> + public void Update(SplitterDestinationInParameter parameter) + { + Debug.Assert(Id == parameter.Id); + + if (parameter.IsMagicValid() && Id == parameter.Id) + { + DestinationId = parameter.DestinationId; + + parameter.MixBufferVolume.CopyTo(MixBufferVolume); + + if (!IsUsed && parameter.IsUsed) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + + NeedToUpdateInternalState = false; + } + + IsUsed = parameter.IsUsed; + } + } + + /// <summary> + /// Update the internal state of the instance. + /// </summary> + public void UpdateInternalState() + { + if (IsUsed && NeedToUpdateInternalState) + { + MixBufferVolume.CopyTo(PreviousMixBufferVolume); + } + + NeedToUpdateInternalState = false; + } + + /// <summary> + /// Set the update internal state marker. + /// </summary> + public void MarkAsNeedToUpdateInternalState() + { + NeedToUpdateInternalState = true; + } + + /// <summary> + /// Return true if the <see cref="SplitterDestination"/> is used and has a destination. + /// </summary> + /// <returns>True if the <see cref="SplitterDestination"/> is used and has a destination.</returns> + public bool IsConfigured() + { + return IsUsed && DestinationId != Constants.UnusedMixId; + } + + /// <summary> + /// Get the volume for a given destination. + /// </summary> + /// <param name="destinationIndex">The destination index to use.</param> + /// <returns>The volume for the given destination.</returns> + public float GetMixVolume(int destinationIndex) + { + Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax); + + return MixBufferVolume[destinationIndex]; + } + + /// <summary> + /// Clear the volumes. + /// </summary> + public void ClearVolumes() + { + MixBufferVolume.Fill(0); + PreviousMixBufferVolume.Fill(0); + } + + /// <summary> + /// Link the next element to the given <see cref="SplitterDestination"/>. + /// </summary> + /// <param name="next">The given <see cref="SplitterDestination"/> to link.</param> + public void Link(ref SplitterDestination next) + { + unsafe + { + fixed (SplitterDestination *nextPtr = &next) + { + _next = nextPtr; + } + } + } + + /// <summary> + /// Remove the link to the next element. + /// </summary> + public void Unlink() + { + unsafe + { + _next = null; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs new file mode 100644 index 00000000..c928ec53 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs @@ -0,0 +1,237 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// +using Ryujinx.Audio.Renderer.Parameter; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Splitter +{ + /// <summary> + /// Server state for a splitter. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = Alignment)] + public struct SplitterState + { + public const int Alignment = 0x10; + + /// <summary> + /// The unique id of this <see cref="SplitterState"/>. + /// </summary> + public int Id; + + /// <summary> + /// Target sample rate to use on the splitter. + /// </summary> + public uint SampleRate; + + /// <summary> + /// Count of splitter destinations (<see cref="SplitterDestination"/>). + /// </summary> + public int DestinationCount; + + /// <summary> + /// Set to true if the splitter has a new connection. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool HasNewConnection; + + /// <summary> + /// Linked list of <see cref="SplitterDestination"/>. + /// </summary> + private unsafe SplitterDestination* _destinationsData; + + /// <summary> + /// Span to the first element of the linked list of <see cref="SplitterDestination"/>. + /// </summary> + public Span<SplitterDestination> Destinations + { + get + { + unsafe + { + return (IntPtr)_destinationsData != IntPtr.Zero ? new Span<SplitterDestination>(_destinationsData, 1) : Span<SplitterDestination>.Empty; + } + } + } + + /// <summary> + /// Create a new <see cref="SplitterState"/>. + /// </summary> + /// <param name="id">The unique id of this <see cref="SplitterState"/>.</param> + public SplitterState(int id) : this() + { + Id = id; + } + + public Span<SplitterDestination> GetData(int index) + { + int i = 0; + + Span<SplitterDestination> result = Destinations; + + while (i < index) + { + if (result.IsEmpty) + { + break; + } + + result = result[0].Next; + i++; + } + + return result; + } + + /// <summary> + /// Clear the new connection flag. + /// </summary> + public void ClearNewConnectionFlag() + { + HasNewConnection = false; + } + + /// <summary> + /// Utility function to apply a given <see cref="SpanAction{T, TArg}"/> to all <see cref="Destinations"/>. + /// </summary> + /// <param name="action">The action to execute on each elements.</param> + private void ForEachDestination(SpanAction<SplitterDestination, int> action) + { + Span<SplitterDestination> temp = Destinations; + + int i = 0; + + while (true) + { + if (temp.IsEmpty) + { + break; + } + + Span<SplitterDestination> next = temp[0].Next; + + action.Invoke(temp, i++); + + temp = next; + } + } + + /// <summary> + /// Update the <see cref="SplitterState"/> from user parameter. + /// </summary> + /// <param name="context">The splitter context.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="input">The raw input data after the <paramref name="parameter"/>.</param> + public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan<byte> input) + { + ClearLinks(); + + int destinationCount; + + if (context.IsBugFixed) + { + destinationCount = parameter.DestinationCount; + } + else + { + destinationCount = Math.Min(context.GetDestinationCountPerStateForCompatibility(), parameter.DestinationCount); + } + + if (destinationCount > 0) + { + ReadOnlySpan<int> destinationIds = MemoryMarshal.Cast<byte, int>(input); + + Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationIds[0]); + + SetDestination(ref destination.Span[0]); + + DestinationCount = destinationCount; + + for (int i = 1; i < destinationCount; i++) + { + Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationIds[i]); + + destination.Span[0].Link(ref nextDestination.Span[0]); + destination = nextDestination; + } + } + + Debug.Assert(parameter.Id == Id); + + if (parameter.Id == Id) + { + SampleRate = parameter.SampleRate; + HasNewConnection = true; + } + } + + /// <summary> + /// Set the head of the linked list of <see cref="Destinations"/>. + /// </summary> + /// <param name="newValue">A reference to a <see cref="SplitterDestination"/>.</param> + public void SetDestination(ref SplitterDestination newValue) + { + unsafe + { + fixed (SplitterDestination* newValuePtr = &newValue) + { + _destinationsData = newValuePtr; + } + } + } + + /// <summary> + /// Update the internal state of this instance. + /// </summary> + public void UpdateInternalState() + { + ForEachDestination((destination, _) => destination[0].UpdateInternalState()); + } + + /// <summary> + /// Clear all links from the <see cref="Destinations"/>. + /// </summary> + public void ClearLinks() + { + ForEachDestination((destination, _) => destination[0].Unlink()); + + unsafe + { + _destinationsData = (SplitterDestination*)IntPtr.Zero; + } + } + + /// <summary> + /// Initialize a given <see cref="Span{SplitterState}"/>. + /// </summary> + /// <param name="splitters">All the <see cref="SplitterState"/> to initialize.</param> + public static void InitializeSplitters(Span<SplitterState> splitters) + { + foreach (ref SplitterState splitter in splitters) + { + unsafe + { + splitter._destinationsData = (SplitterDestination*)IntPtr.Zero; + } + + splitter.DestinationCount = 0; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/StateUpdater.cs b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs new file mode 100644 index 00000000..77935b75 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/StateUpdater.cs @@ -0,0 +1,575 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Parameter.Performance; +using Ryujinx.Audio.Renderer.Server.Effect; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Audio.Renderer.Server.Mix; +using Ryujinx.Audio.Renderer.Server.Performance; +using Ryujinx.Audio.Renderer.Server.Sink; +using Ryujinx.Audio.Renderer.Server.Splitter; +using Ryujinx.Audio.Renderer.Server.Voice; +using Ryujinx.Audio.Renderer.Utils; +using Ryujinx.Common.Logging; +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; + +namespace Ryujinx.Audio.Renderer.Server +{ + public class StateUpdater + { + private readonly ReadOnlyMemory<byte> _inputOrigin; + private ReadOnlyMemory <byte> _outputOrigin; + private ReadOnlyMemory<byte> _input; + + private Memory<byte> _output; + private uint _processHandle; + private BehaviourContext _behaviourContext; + + private UpdateDataHeader _inputHeader; + private Memory<UpdateDataHeader> _outputHeader; + + private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0]; + + public StateUpdater(ReadOnlyMemory<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext) + { + _input = input; + _inputOrigin = _input; + _output = output; + _outputOrigin = _output; + _processHandle = processHandle; + _behaviourContext = behaviourContext; + + _inputHeader = SpanIOHelper.Read<UpdateDataHeader>(ref _input); + + _outputHeader = SpanMemoryManager<UpdateDataHeader>.Cast(_output.Slice(0, Unsafe.SizeOf<UpdateDataHeader>())); + OutputHeader.Initialize(_behaviourContext.UserRevision); + _output = _output.Slice(Unsafe.SizeOf<UpdateDataHeader>()); + } + + public ResultCode UpdateBehaviourContext() + { + BehaviourParameter parameter = SpanIOHelper.Read<BehaviourParameter>(ref _input); + + if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision) + { + return ResultCode.InvalidUpdateInfo; + } + + _behaviourContext.ClearError(); + _behaviourContext.UpdateFlags(parameter.Flags); + + if (_inputHeader.BehaviourSize != Unsafe.SizeOf<BehaviourParameter>()) + { + return ResultCode.InvalidUpdateInfo; + } + + return ResultCode.Success; + } + + public ResultCode UpdateMemoryPools(Span<MemoryPoolState> memoryPools) + { + PoolMapper mapper = new PoolMapper(_processHandle, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + if (memoryPools.Length * Unsafe.SizeOf<MemoryPoolInParameter>() != _inputHeader.MemoryPoolsSize) + { + return ResultCode.InvalidUpdateInfo; + } + + foreach (ref MemoryPoolState memoryPool in memoryPools) + { + MemoryPoolInParameter parameter = SpanIOHelper.Read<MemoryPoolInParameter>(ref _input); + + ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef<MemoryPoolOutStatus>(ref _output)[0]; + + PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus); + + if (updateResult != PoolMapper.UpdateResult.Success && + updateResult != PoolMapper.UpdateResult.MapError && + updateResult != PoolMapper.UpdateResult.UnmapError) + { + if (updateResult != PoolMapper.UpdateResult.InvalidParameter) + { + throw new InvalidOperationException($"{updateResult}"); + } + + return ResultCode.InvalidUpdateInfo; + } + } + + OutputHeader.MemoryPoolsSize = (uint)(Unsafe.SizeOf<MemoryPoolOutStatus>() * memoryPools.Length); + OutputHeader.TotalSize += OutputHeader.MemoryPoolsSize; + + return ResultCode.Success; + } + + public ResultCode UpdateVoiceChannelResources(VoiceContext context) + { + if (context.GetCount() * Unsafe.SizeOf<VoiceChannelResourceInParameter>() != _inputHeader.VoiceResourcesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + for (int i = 0; i < context.GetCount(); i++) + { + VoiceChannelResourceInParameter parameter = SpanIOHelper.Read<VoiceChannelResourceInParameter>(ref _input); + + ref VoiceChannelResource resource = ref context.GetChannelResource(i); + + resource.Id = parameter.Id; + parameter.Mix.ToSpan().CopyTo(resource.Mix.ToSpan()); + resource.IsUsed = parameter.IsUsed; + } + + return ResultCode.Success; + } + + public ResultCode UpdateVoices(VoiceContext context, Memory<MemoryPoolState> memoryPools) + { + if (context.GetCount() * Unsafe.SizeOf<VoiceInParameter>() != _inputHeader.VoicesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<VoiceInParameter> parameters = MemoryMarshal.Cast<byte, VoiceInParameter>(_input.Slice(0, (int)_inputHeader.VoicesSize).Span); + + _input = _input.Slice((int)_inputHeader.VoicesSize); + + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + // First make everything not in use. + for (int i = 0; i < context.GetCount(); i++) + { + ref VoiceState state = ref context.GetState(i); + + state.InUse = false; + } + + // Start processing + for (int i = 0; i < context.GetCount(); i++) + { + VoiceInParameter parameter = parameters[i]; + + Memory<VoiceUpdateState>[] voiceUpdateStates = new Memory<VoiceUpdateState>[Constants.VoiceChannelCountMax]; + + ref VoiceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<VoiceOutStatus>(ref _output)[0]; + + if (parameter.InUse) + { + ref VoiceState currentVoiceState = ref context.GetState(i); + + for (int channelResourceIndex = 0; channelResourceIndex < parameter.ChannelCount; channelResourceIndex++) + { + int channelId = parameter.ChannelResourceIds[channelResourceIndex]; + + Debug.Assert(channelId >= 0 && channelId < context.GetCount()); + + voiceUpdateStates[channelResourceIndex] = context.GetUpdateStateForCpu(channelId); + } + + if (parameter.IsNew) + { + currentVoiceState.Initialize(); + } + + currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext); + + if (updateParameterError.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateParameterError); + } + + currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext); + + foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan()) + { + if (errorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref errorInfo); + } + } + + currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates); + } + } + + int currentOutputSize = _output.Length; + + OutputHeader.VoicesSize = (uint)(Unsafe.SizeOf<VoiceOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.VoicesSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize); + + return ResultCode.Success; + } + + private static void ResetEffect(ref BaseEffect effect, ref EffectInParameter parameter, PoolMapper mapper) + { + effect.ForceUnmapBuffers(mapper); + + switch (parameter.Type) + { + case EffectType.Invalid: + effect = new BaseEffect(); + break; + case EffectType.BufferMix: + effect = new BufferMixEffect(); + break; + case EffectType.AuxiliaryBuffer: + effect = new AuxiliaryBufferEffect(); + break; + case EffectType.Delay: + effect = new DelayEffect(); + break; + case EffectType.Reverb: + effect = new ReverbEffect(); + break; + case EffectType.Reverb3d: + effect = new Reverb3dEffect(); + break; + case EffectType.BiquadFilter: + effect = new BiquadFilterEffect(); + break; + default: + throw new NotImplementedException($"EffectType {parameter.Type} not implemented!"); + } + } + + public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools) + { + if (context.GetCount() * Unsafe.SizeOf<EffectInParameter>() != _inputHeader.EffectsSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<EffectInParameter> parameters = MemoryMarshal.Cast<byte, EffectInParameter>(_input.Slice(0, (int)_inputHeader.EffectsSize).Span); + + _input = _input.Slice((int)_inputHeader.EffectsSize); + + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + for (int i = 0; i < context.GetCount(); i++) + { + EffectInParameter parameter = parameters[i]; + + ref EffectOutStatus outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatus>(ref _output)[0]; + + ref BaseEffect effect = ref context.GetEffect(i); + + if (!effect.IsTypeValid(ref parameter)) + { + ResetEffect(ref effect, ref parameter, mapper); + } + + effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper); + + if (updateErrorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateErrorInfo); + } + + effect.StoreStatus(ref outStatus, isAudioRendererActive); + } + + int currentOutputSize = _output.Length; + + OutputHeader.EffectsSize = (uint)(Unsafe.SizeOf<EffectOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.EffectsSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize); + + return ResultCode.Success; + } + + public ResultCode UpdateSplitter(SplitterContext context) + { + if (context.Update(_input.Span, out int consumedSize)) + { + _input = _input.Slice(consumedSize); + + return ResultCode.Success; + } + else + { + return ResultCode.InvalidUpdateInfo; + } + } + + private bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan<MixParameter> parameters) + { + uint maxMixStateCount = mixContext.GetCount(); + uint totalRequiredMixBufferCount = 0; + + for (int i = 0; i < inputMixCount; i++) + { + if (parameters[i].IsUsed) + { + if (parameters[i].DestinationMixId != Constants.UnusedMixId && + parameters[i].DestinationMixId > maxMixStateCount && + parameters[i].MixId != Constants.FinalMixId) + { + return true; + } + + totalRequiredMixBufferCount += parameters[i].BufferCount; + } + } + + return totalRequiredMixBufferCount > mixBufferCount; + } + + public ResultCode UpdateMixes(MixContext mixContext, uint mixBufferCount, EffectContext effectContext, SplitterContext splitterContext) + { + uint mixCount; + uint inputMixSize; + uint inputSize = 0; + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast<byte, MixInParameterDirtyOnlyUpdate>(_input.Span)[0]; + + mixCount = parameter.MixCount; + + inputSize += (uint)Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>(); + } + else + { + mixCount = mixContext.GetCount(); + } + + inputMixSize = mixCount * (uint)Unsafe.SizeOf<MixParameter>(); + + inputSize += inputMixSize; + + if (inputSize != _inputHeader.MixesSize) + { + return ResultCode.InvalidUpdateInfo; + } + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + _input = _input.Slice(Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>()); + } + + ReadOnlySpan<MixParameter> parameters = MemoryMarshal.Cast<byte, MixParameter>(_input.Span.Slice(0, (int)inputMixSize)); + + _input = _input.Slice((int)inputMixSize); + + if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters)) + { + return ResultCode.InvalidUpdateInfo; + } + + bool isMixContextDirty = false; + + for (int i = 0; i < parameters.Length; i++) + { + MixParameter parameter = parameters[i]; + + int mixId = i; + + if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported()) + { + mixId = parameter.MixId; + } + + ref MixState mix = ref mixContext.GetState(mixId); + + if (parameter.IsUsed != mix.IsUsed) + { + mix.IsUsed = parameter.IsUsed; + + if (parameter.IsUsed) + { + mix.ClearEffectProcessingOrder(); + } + + isMixContextDirty = true; + } + + if (mix.IsUsed) + { + isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext); + } + } + + if (isMixContextDirty) + { + if (_behaviourContext.IsSplitterSupported() && splitterContext.UsingSplitter()) + { + if (!mixContext.Sort(splitterContext)) + { + return ResultCode.InvalidMixSorting; + } + } + else + { + mixContext.Sort(); + } + } + + return ResultCode.Success; + } + + private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter) + { + sink.CleanUp(); + + switch (parameter.Type) + { + case SinkType.Invalid: + sink = new BaseSink(); + break; + case SinkType.CircularBuffer: + sink = new CircularBufferSink(); + break; + case SinkType.Device: + sink = new DeviceSink(); + break; + default: + throw new NotImplementedException($"SinkType {parameter.Type} not implemented!"); + } + } + + public ResultCode UpdateSinks(SinkContext context, Memory<MemoryPoolState> memoryPools) + { + PoolMapper mapper = new PoolMapper(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled()); + + if (context.GetCount() * Unsafe.SizeOf<SinkInParameter>() != _inputHeader.SinksSize) + { + return ResultCode.InvalidUpdateInfo; + } + + int initialOutputSize = _output.Length; + + ReadOnlySpan<SinkInParameter> parameters = MemoryMarshal.Cast<byte, SinkInParameter>(_input.Slice(0, (int)_inputHeader.SinksSize).Span); + + _input = _input.Slice((int)_inputHeader.SinksSize); + + for (int i = 0; i < context.GetCount(); i++) + { + SinkInParameter parameter = parameters[i]; + ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef<SinkOutStatus>(ref _output)[0]; + ref BaseSink sink = ref context.GetSink(i); + + if (!sink.IsTypeValid(ref parameter)) + { + ResetSink(ref sink, ref parameter); + } + + sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper); + + if (updateErrorInfo.ErrorCode != ResultCode.Success) + { + _behaviourContext.AppendError(ref updateErrorInfo); + } + } + + int currentOutputSize = _output.Length; + + OutputHeader.SinksSize = (uint)(Unsafe.SizeOf<SinkOutStatus>() * context.GetCount()); + OutputHeader.TotalSize += OutputHeader.SinksSize; + + Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize); + + return ResultCode.Success; + } + + public ResultCode UpdatePerformanceBuffer(PerformanceManager manager, Span<byte> performanceOutput) + { + if (Unsafe.SizeOf<PerformanceInParameter>() != _inputHeader.PerformanceBufferSize) + { + return ResultCode.InvalidUpdateInfo; + } + + PerformanceInParameter parameter = SpanIOHelper.Read<PerformanceInParameter>(ref _input); + + ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<PerformanceOutStatus>(ref _output)[0]; + + if (manager != null) + { + outStatus.HistorySize = manager.CopyHistories(performanceOutput); + + manager.SetTargetNodeId(parameter.TargetNodeId); + } + else + { + outStatus.HistorySize = 0; + } + + OutputHeader.PerformanceBufferSize = (uint)Unsafe.SizeOf<PerformanceOutStatus>(); + OutputHeader.TotalSize += OutputHeader.PerformanceBufferSize; + + return ResultCode.Success; + } + + public ResultCode UpdateErrorInfo() + { + ref BehaviourErrorInfoOutStatus outStatus = ref SpanIOHelper.GetWriteRef<BehaviourErrorInfoOutStatus>(ref _output)[0]; + + _behaviourContext.CopyErrorInfo(outStatus.ErrorInfos.ToSpan(), out outStatus.ErrorInfosCount); + + OutputHeader.BehaviourSize = (uint)Unsafe.SizeOf<BehaviourErrorInfoOutStatus>(); + OutputHeader.TotalSize += OutputHeader.BehaviourSize; + + return ResultCode.Success; + } + + public ResultCode UpdateRendererInfo(ulong elapsedFrameCount) + { + ref RendererInfoOutStatus outStatus = ref SpanIOHelper.GetWriteRef<RendererInfoOutStatus>(ref _output)[0]; + + outStatus.ElapsedFrameCount = elapsedFrameCount; + + OutputHeader.RenderInfoSize = (uint)Unsafe.SizeOf<RendererInfoOutStatus>(); + OutputHeader.TotalSize += OutputHeader.RenderInfoSize; + + return ResultCode.Success; + } + + public ResultCode CheckConsumedSize() + { + int consumedInputSize = _inputOrigin.Length - _input.Length; + int consumedOutputSize = _outputOrigin.Length - _output.Length; + + if (consumedInputSize != _inputHeader.TotalSize) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Consumed input size mismatch (got {consumedInputSize} expected {_inputHeader.TotalSize})"); + + return ResultCode.InvalidUpdateInfo; + } + + if (consumedOutputSize != OutputHeader.TotalSize) + { + Logger.Error?.Print(LogClass.AudioRenderer, $"Consumed output size mismatch (got {consumedOutputSize} expected {OutputHeader.TotalSize})"); + + return ResultCode.InvalidUpdateInfo; + } + + return ResultCode.Success; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs new file mode 100644 index 00000000..7bb0507c --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererExecutionMode.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The execution mode of an <see cref="AudioRenderSystem"/>. + /// </summary> + public enum AudioRendererExecutionMode : byte + { + /// <summary> + /// Automatically send commands to the DSP at a fixed rate (see <see cref="AudioRenderSystem.SendCommands"/> + /// </summary> + Auto, + + /// <summary> + /// Audio renderer operation needs to be done manually via ExecuteAudioRenderer. + /// </summary> + /// <remarks>This is not supported on the DSP and is as such stubbed.</remarks> + Manual + } +}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs new file mode 100644 index 00000000..f89086f6 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/AudioRendererRenderingDevice.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The rendering device of an <see cref="AudioRenderSystem"/>. + /// </summary> + public enum AudioRendererRenderingDevice : byte + { + /// <summary> + /// Rendering is performed on the DSP. + /// </summary> + /// <remarks> + /// Only supports <see cref="AudioRendererExecutionMode.Auto"/>. + /// </remarks> + Dsp, + + /// <summary> + /// Rendering is performed on the CPU. + /// </summary> + /// <remarks> + /// Only supports <see cref="AudioRendererExecutionMode.Manual"/>. + /// </remarks> + Cpu + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs b/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs new file mode 100644 index 00000000..7f8895c9 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Types/PlayState.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio.Renderer.Server.Types +{ + /// <summary> + /// The internal play state of a <see cref="Voice.VoiceState"/> + /// </summary> + public enum PlayState + { + /// <summary> + /// The voice has been started and is playing. + /// </summary> + Started, + + /// <summary> + /// The voice has been stopped. + /// </summary> + /// <remarks> + /// This cannot be directly set by user. + /// See <see cref="Stopping"/> for correct usage. + /// </remarks> + Stopped, + + /// <summary> + /// The user asked the voice to be stopped. + /// </summary> + /// <remarks> + /// This is changed to the <see cref="Stopped"/> state after command generation. + /// <seealso cref="Voice.VoiceState.UpdateForCommandGeneration(Voice.VoiceContext)"/> + /// </remarks> + Stopping, + + /// <summary> + /// The voice has been paused by user request. + /// </summary> + /// <remarks> + /// The user can resume to the <see cref="Started"/> state. + /// </remarks> + Paused + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs new file mode 100644 index 00000000..6878123b --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerManager.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Upsampler +{ + /// <summary> + /// Upsampler manager. + /// </summary> + public class UpsamplerManager + { + /// <summary> + /// Work buffer for upsampler. + /// </summary> + private Memory<float> _upSamplerWorkBuffer; + + /// <summary> + /// Global lock of the object. + /// </summary> + private object Lock = new object(); + + /// <summary> + /// The upsamplers instances. + /// </summary> + private UpsamplerState[] _upsamplers; + + /// <summary> + /// The count of upsamplers. + /// </summary> + private uint _count; + + /// <summary> + /// Create a new <see cref="UpsamplerManager"/>. + /// </summary> + /// <param name="upSamplerWorkBuffer">Work buffer for upsampler.</param> + /// <param name="count">The count of upsamplers.</param> + public UpsamplerManager(Memory<float> upSamplerWorkBuffer, uint count) + { + _upSamplerWorkBuffer = upSamplerWorkBuffer; + _count = count; + + _upsamplers = new UpsamplerState[_count]; + } + + /// <summary> + /// Allocate a new <see cref="UpsamplerState"/>. + /// </summary> + /// <returns>A new <see cref="UpsamplerState"/> or null if out of memory.</returns> + public UpsamplerState Allocate() + { + int workBufferOffset = 0; + + lock (Lock) + { + for (int i = 0; i < _count; i++) + { + if (_upsamplers[i] == null) + { + _upsamplers[i] = new UpsamplerState(this, i, _upSamplerWorkBuffer.Slice(workBufferOffset, Constants.UpSampleEntrySize), Constants.TargetSampleCount); + + return _upsamplers[i]; + } + + workBufferOffset += Constants.UpSampleEntrySize; + } + } + + return null; + } + + /// <summary> + /// Free a <see cref="UpsamplerState"/> at the given index. + /// </summary> + /// <param name="index">The index of the <see cref="UpsamplerState"/> to free.</param> + public void Free(int index) + { + lock (Lock) + { + Debug.Assert(_upsamplers[index] != null); + + _upsamplers[index] = null; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs new file mode 100644 index 00000000..8ea55384 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Upsampler/UpsamplerState.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; + +namespace Ryujinx.Audio.Renderer.Server.Upsampler +{ + /// <summary> + /// Server state for a upsampling. + /// </summary> + public class UpsamplerState + { + /// <summary> + /// The output buffer containing the target samples. + /// </summary> + public Memory<float> OutputBuffer { get; } + + /// <summary> + /// The target sample count. + /// </summary> + public uint SampleCount { get; } + + /// <summary> + /// The index of the <see cref="UpsamplerState"/>. (used to free it) + /// </summary> + private int _index; + + /// <summary> + /// The <see cref="UpsamplerManager"/>. + /// </summary> + private UpsamplerManager _manager; + + /// <summary> + /// The source sample count. + /// </summary> + public uint SourceSampleCount; + + /// <summary> + /// The input buffer indices of the buffers holding the samples that need upsampling. + /// </summary> + public ushort[] InputBufferIndices; + + /// <summary> + /// Create a new <see cref="UpsamplerState"/>. + /// </summary> + /// <param name="manager">The upsampler manager.</param> + /// <param name="index">The index of the <see cref="UpsamplerState"/>. (used to free it)</param> + /// <param name="outputBuffer">The output buffer used to contain the target samples.</param> + /// <param name="sampleCount">The target sample count.</param> + public UpsamplerState(UpsamplerManager manager, int index, Memory<float> outputBuffer, uint sampleCount) + { + _manager = manager; + _index = index; + OutputBuffer = outputBuffer; + SampleCount = sampleCount; + } + + /// <summary> + /// Release the <see cref="UpsamplerState"/>. + /// </summary> + public void Release() + { + _manager.Free(_index); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs new file mode 100644 index 00000000..00520cfc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceChannelResource.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// Server state for a voice channel resource. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0xD0, Pack = Alignment)] + public struct VoiceChannelResource + { + public const int Alignment = 0x10; + + /// <summary> + /// Mix volumes for the resource. + /// </summary> + public Array24<float> Mix; + + /// <summary> + /// Previous mix volumes for resource. + /// </summary> + public Array24<float> PreviousMix; + + /// <summary> + /// The id of the resource. + /// </summary> + public uint Id; + + /// <summary> + /// Indicate if the resource is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsUsed; + + public void UpdateState() + { + Mix.ToSpan().CopyTo(PreviousMix.ToSpan()); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs new file mode 100644 index 00000000..0dfaa4d5 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceContext.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Utils; +using System; +using System.Diagnostics; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// Voice context. + /// </summary> + public class VoiceContext + { + /// <summary> + /// Storage of the sorted indices to <see cref="VoiceState"/>. + /// </summary> + private Memory<int> _sortedVoices; + + /// <summary> + /// Storage for <see cref="VoiceState"/>. + /// </summary> + private Memory<VoiceState> _voices; + + /// <summary> + /// Storage for <see cref="VoiceChannelResource"/>. + /// </summary> + private Memory<VoiceChannelResource> _voiceChannelResources; + + /// <summary> + /// Storage for <see cref="VoiceUpdateState"/> that are used during audio renderer server updates. + /// </summary> + private Memory<VoiceUpdateState> _voiceUpdateStatesCpu; + + /// <summary> + /// Storage for <see cref="VoiceUpdateState"/> for the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + private Memory<VoiceUpdateState> _voiceUpdateStatesDsp; + + /// <summary> + /// The total voice count. + /// </summary> + private uint _voiceCount; + + public void Initialize(Memory<int> sortedVoices, Memory<VoiceState> voices, Memory<VoiceChannelResource> voiceChannelResources, Memory<VoiceUpdateState> voiceUpdateStatesCpu, Memory<VoiceUpdateState> voiceUpdateStatesDsp, uint voiceCount) + { + _sortedVoices = sortedVoices; + _voices = voices; + _voiceChannelResources = voiceChannelResources; + _voiceUpdateStatesCpu = voiceUpdateStatesCpu; + _voiceUpdateStatesDsp = voiceUpdateStatesDsp; + _voiceCount = voiceCount; + } + + /// <summary> + /// Get the total voice count. + /// </summary> + /// <returns>The total voice count.</returns> + public uint GetCount() + { + return _voiceCount; + } + + /// <summary> + /// Get a reference to a <see cref="VoiceChannelResource"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="VoiceChannelResource"/> at the given <paramref name="id"/>.</returns> + public ref VoiceChannelResource GetChannelResource(int id) + { + return ref SpanIOHelper.GetFromMemory(_voiceChannelResources, id, _voiceCount); + } + + /// <summary> + /// Get a <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>.</returns> + /// <remarks>The returned <see cref="Memory{VoiceUpdateState}"/> should only be used when updating the server state.</remarks> + public Memory<VoiceUpdateState> GetUpdateStateForCpu(int id) + { + return SpanIOHelper.GetMemory(_voiceUpdateStatesCpu, id, _voiceCount); + } + + /// <summary> + /// Get a <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A <see cref="Memory{VoiceUpdateState}"/> at the given <paramref name="id"/>.</returns> + /// <remarks>The returned <see cref="Memory{VoiceUpdateState}"/> should only be used in the context of processing on the <see cref="Dsp.AudioProcessor"/>.</remarks> + public Memory<VoiceUpdateState> GetUpdateStateForDsp(int id) + { + return SpanIOHelper.GetMemory(_voiceUpdateStatesDsp, id, _voiceCount); + } + + /// <summary> + /// Get a reference to a <see cref="VoiceState"/> at the given <paramref name="id"/>. + /// </summary> + /// <param name="id">The index to use.</param> + /// <returns>A reference to a <see cref="VoiceState"/> at the given <paramref name="id"/>.</returns> + public ref VoiceState GetState(int id) + { + return ref SpanIOHelper.GetFromMemory(_voices, id, _voiceCount); + } + + public ref VoiceState GetSortedState(int id) + { + Debug.Assert(id >= 0 && id < _voiceCount); + + return ref GetState(_sortedVoices.Span[id]); + } + + /// <summary> + /// Update internal state during command generation. + /// </summary> + public void UpdateForCommandGeneration() + { + _voiceUpdateStatesDsp.CopyTo(_voiceUpdateStatesCpu); + } + + /// <summary> + /// Sort the internal voices by priority and sorting order (if the priorities match). + /// </summary> + public void Sort() + { + for (int i = 0; i < _voiceCount; i++) + { + _sortedVoices.Span[i] = i; + } + + int[] sortedVoicesTemp = _sortedVoices.Slice(0, (int)GetCount()).ToArray(); + + Array.Sort(sortedVoicesTemp, (a, b) => + { + ref VoiceState aState = ref GetState(a); + ref VoiceState bState = ref GetState(b); + + int result = aState.Priority.CompareTo(bState.Priority); + + if (result == 0) + { + return aState.SortingOrder.CompareTo(bState.SortingOrder); + } + + return result; + }); + + sortedVoicesTemp.AsSpan().CopyTo(_sortedVoices.Span); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs b/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs new file mode 100644 index 00000000..89abe9aa --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/VoiceState.cs @@ -0,0 +1,716 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Common; +using Ryujinx.Audio.Renderer.Parameter; +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Renderer.Common.BehaviourParameter; +using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + [StructLayout(LayoutKind.Sequential, Pack = Alignment)] + public struct VoiceState + { + public const int Alignment = 0x10; + + /// <summary> + /// Set to true if the voice is used. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool InUse; + + /// <summary> + /// Set to true if the voice is new. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsNew; + + [MarshalAs(UnmanagedType.I1)] + public bool WasPlaying; + + /// <summary> + /// The <see cref="SampleFormat"/> of the voice. + /// </summary> + public SampleFormat SampleFormat; + + /// <summary> + /// The sample rate of the voice. + /// </summary> + public uint SampleRate; + + /// <summary> + /// The total channel count used. + /// </summary> + public uint ChannelsCount; + + /// <summary> + /// Id of the voice. + /// </summary> + public int Id; + + /// <summary> + /// Node id of the voice. + /// </summary> + public int NodeId; + + /// <summary> + /// The target mix id of the voice. + /// </summary> + public int MixId; + + /// <summary> + /// The current voice <see cref="Types.PlayState"/>. + /// </summary> + public Types.PlayState PlayState; + + /// <summary> + /// The previous voice <see cref="Types.PlayState"/>. + /// </summary> + public Types.PlayState PreviousPlayState; + + /// <summary> + /// The priority of the voice. + /// </summary> + public uint Priority; + + /// <summary> + /// Target sorting position of the voice. (used to sort voice with the same <see cref="Priority"/>) + /// </summary> + public uint SortingOrder; + + /// <summary> + /// The pitch used on the voice. + /// </summary> + public float Pitch; + + /// <summary> + /// The output volume of the voice. + /// </summary> + public float Volume; + + /// <summary> + /// The previous output volume of the voice. + /// </summary> + public float PreviousVolume; + + /// <summary> + /// Biquad filters to apply to the output of the voice. + /// </summary> + public Array2<BiquadFilterParameter> BiquadFilters; + + /// <summary> + /// Total count of <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersCount; + + /// <summary> + /// Current playing <see cref="WaveBufferInternal"/> of the voice. + /// </summary> + public uint WaveBuffersIndex; + + /// <summary> + /// Change the behaviour of the voice. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public DecodingBehaviour DecodingBehaviour; + + /// <summary> + /// User state <see cref="AddressInfo"/> required by the data source. + /// </summary> + /// <remarks>Only used for <see cref="SampleFormat.Adpcm"/> as the GC-ADPCM coefficients.</remarks> + public AddressInfo DataSourceStateAddressInfo; + + /// <summary> + /// The wavebuffers of this voice. + /// </summary> + public Array4<WaveBuffer> WaveBuffers; + + /// <summary> + /// The channel resource ids associated to the voice. + /// </summary> + public Array6<int> ChannelResourceIds; + + /// <summary> + /// The target splitter id of the voice. + /// </summary> + public uint SplitterId; + + /// <summary> + /// Change the Sample Rate Conversion (SRC) quality of the voice. + /// </summary> + /// <remarks>This was added on REV8.</remarks> + public SampleRateConversionQuality SrcQuality; + + /// <summary> + /// If set to true, the voice was dropped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool VoiceDropFlag; + + /// <summary> + /// Set to true if the data source state work buffer wasn't mapped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool DataSourceStateUnmapped; + + /// <summary> + /// Set to true if any of the <see cref="WaveBuffer.BufferAddressInfo"/> work buffer wasn't mapped. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool BufferInfoUnmapped; + + /// <summary> + /// The biquad filter initialization state storage. + /// </summary> + private BiquadFilterNeedInitializationArrayStruct _biquadFilterNeedInitialization; + + /// <summary> + /// Flush the amount of wavebuffer specified. This will result in the wavebuffer being skipped and marked played. + /// </summary> + /// <remarks>This was added on REV5.</remarks> + public byte FlushWaveBufferCount; + + [StructLayout(LayoutKind.Sequential, Size = Constants.VoiceBiquadFilterCount)] + private struct BiquadFilterNeedInitializationArrayStruct { } + + /// <summary> + /// The biquad filter initialization state array. + /// </summary> + public Span<bool> BiquadFilterNeedInitialization => SpanHelpers.AsSpan<BiquadFilterNeedInitializationArrayStruct, bool>(ref _biquadFilterNeedInitialization); + + /// <summary> + /// Initialize the <see cref="VoiceState"/>. + /// </summary> + public void Initialize() + { + IsNew = false; + VoiceDropFlag = false; + DataSourceStateUnmapped = false; + BufferInfoUnmapped = false; + FlushWaveBufferCount = 0; + PlayState = Types.PlayState.Stopped; + Priority = Constants.VoiceLowestPriority; + Id = 0; + NodeId = 0; + SampleRate = 0; + SampleFormat = SampleFormat.Invalid; + ChannelsCount = 0; + Pitch = 0.0f; + Volume= 0.0f; + PreviousVolume = 0.0f; + BiquadFilters.ToSpan().Fill(new BiquadFilterParameter()); + WaveBuffersCount = 0; + WaveBuffersIndex = 0; + MixId = Constants.UnusedMixId; + SplitterId = Constants.UnusedSplitterId; + DataSourceStateAddressInfo.Setup(0, 0); + + InitializeWaveBuffers(); + } + + /// <summary> + /// Initialize the <see cref="WaveBuffer"/> in this <see cref="VoiceState"/>. + /// </summary> + private void InitializeWaveBuffers() + { + for (int i = 0; i < WaveBuffers.Length; i++) + { + WaveBuffers[i].StartSampleOffset = 0; + WaveBuffers[i].EndSampleOffset = 0; + WaveBuffers[i].ShouldLoop = false; + WaveBuffers[i].IsEndOfStream = false; + WaveBuffers[i].BufferAddressInfo.Setup(0, 0); + WaveBuffers[i].ContextAddressInfo.Setup(0, 0); + WaveBuffers[i].IsSendToAudioProcessor = true; + } + } + + /// <summary> + /// Check if the voice needs to be skipped. + /// </summary> + /// <returns>Returns true if the voice needs to be skipped.</returns> + public bool ShouldSkip() + { + return !InUse || WaveBuffersCount == 0 || DataSourceStateUnmapped || BufferInfoUnmapped || VoiceDropFlag; + } + + /// <summary> + /// Return true if the mix has any destinations. + /// </summary> + /// <returns>True if the mix has any destinations.</returns> + public bool HasAnyDestination() + { + return MixId != Constants.UnusedMixId || SplitterId != Constants.UnusedSplitterId; + } + + /// <summary> + /// Indicate if the server voice information needs to be updated. + /// </summary> + /// <param name="parameter">The user parameter.</param> + /// <returns>Return true, if the server voice information needs to be updated.</returns> + private bool ShouldUpdateParameters(ref VoiceInParameter parameter) + { + if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress) + { + return DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize; + } + + return DataSourceStateAddressInfo.CpuAddress != parameter.DataSourceStateAddress || + DataSourceStateAddressInfo.Size != parameter.DataSourceStateSize || + DataSourceStateUnmapped; + } + + /// <summary> + /// Update the internal state from a user parameter. + /// </summary> + /// <param name="outErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="poolMapper">The mapper to use.</param> + /// <param name="behaviourContext">The behaviour context.</param> + public void UpdateParameters(out ErrorInfo outErrorInfo, ref VoiceInParameter parameter, ref PoolMapper poolMapper, ref BehaviourContext behaviourContext) + { + InUse = parameter.InUse; + Id = parameter.Id; + NodeId = parameter.NodeId; + + UpdatePlayState(parameter.PlayState); + + SrcQuality = parameter.SrcQuality; + + Priority = parameter.Priority; + SortingOrder = parameter.SortingOrder; + SampleRate = parameter.SampleRate; + SampleFormat = parameter.SampleFormat; + ChannelsCount = parameter.ChannelCount; + Pitch = parameter.Pitch; + Volume = parameter.Volume; + parameter.BiquadFilters.ToSpan().CopyTo(BiquadFilters.ToSpan()); + WaveBuffersCount = parameter.WaveBuffersCount; + WaveBuffersIndex = parameter.WaveBuffersIndex; + + if (behaviourContext.IsFlushVoiceWaveBuffersSupported()) + { + FlushWaveBufferCount += parameter.FlushWaveBufferCount; + } + + MixId = parameter.MixId; + + if (behaviourContext.IsSplitterSupported()) + { + SplitterId = parameter.SplitterId; + } + else + { + SplitterId = Constants.UnusedSplitterId; + } + + parameter.ChannelResourceIds.ToSpan().CopyTo(ChannelResourceIds.ToSpan()); + + DecodingBehaviour behaviour = DecodingBehaviour.Default; + + if (behaviourContext.IsDecodingBehaviourFlagSupported()) + { + behaviour = parameter.DecodingBehaviourFlags; + } + + DecodingBehaviour = behaviour; + + if (parameter.ResetVoiceDropFlag) + { + VoiceDropFlag = false; + } + + if (ShouldUpdateParameters(ref parameter)) + { + DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize); + } + else + { + outErrorInfo = new ErrorInfo(); + } + } + + /// <summary> + /// Update the internal play state from user play state. + /// </summary> + /// <param name="userPlayState">The target user play state.</param> + public void UpdatePlayState(PlayState userPlayState) + { + Types.PlayState oldServerPlayState = PlayState; + + PreviousPlayState = oldServerPlayState; + + Types.PlayState newServerPlayState; + + switch (userPlayState) + { + case Common.PlayState.Start: + newServerPlayState = Types.PlayState.Started; + break; + + case Common.PlayState.Stop: + if (oldServerPlayState == Types.PlayState.Stopped) + { + return; + } + + newServerPlayState = Types.PlayState.Stopping; + break; + + case Common.PlayState.Pause: + newServerPlayState = Types.PlayState.Paused; + break; + + default: + throw new NotImplementedException($"Unhandled PlayState.{userPlayState}"); + } + + PlayState = newServerPlayState; + } + + /// <summary> + /// Write the status of the voice to the given user output. + /// </summary> + /// <param name="outStatus">The given user output.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, Memory<VoiceUpdateState>[] voiceUpdateStates) + { +#if DEBUG + // Sanity check in debug mode of the internal state + if (!parameter.IsNew && !IsNew) + { + for (int i = 1; i < ChannelsCount; i++) + { + ref VoiceUpdateState stateA = ref voiceUpdateStates[i - 1].Span[0]; + ref VoiceUpdateState stateB = ref voiceUpdateStates[i].Span[0]; + + Debug.Assert(stateA.WaveBufferConsumed == stateB.WaveBufferConsumed); + Debug.Assert(stateA.PlayedSampleCount == stateB.PlayedSampleCount); + Debug.Assert(stateA.Offset == stateB.Offset); + Debug.Assert(stateA.WaveBufferIndex == stateB.WaveBufferIndex); + Debug.Assert(stateA.Fraction == stateB.Fraction); + Debug.Assert(stateA.IsWaveBufferValid.SequenceEqual(stateB.IsWaveBufferValid)); + } + } +#endif + if (parameter.IsNew || IsNew) + { + IsNew = true; + + outStatus.VoiceDropFlag = false; + outStatus.PlayedWaveBuffersCount = 0; + outStatus.PlayedSampleCount = 0; + } + else + { + ref VoiceUpdateState state = ref voiceUpdateStates[0].Span[0]; + + outStatus.VoiceDropFlag = VoiceDropFlag; + outStatus.PlayedWaveBuffersCount = state.WaveBufferConsumed; + outStatus.PlayedSampleCount = state.PlayedSampleCount; + } + } + + /// <summary> + /// Update the internal state of all the <see cref="WaveBuffer"/> of the <see cref="VoiceState"/>. + /// </summary> + /// <param name="errorInfos">An array of <see cref="ErrorInfo"/> used to report errors when mapping any of the <see cref="WaveBuffer"/>.</param> + /// <param name="parameter">The user parameter.</param> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + /// <param name="mapper">The mapper to use.</param> + /// <param name="behaviourContext">The behaviour context.</param> + public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, Memory<VoiceUpdateState>[] voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext) + { + errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2]; + + if (parameter.IsNew) + { + InitializeWaveBuffers(); + + for (int i = 0; i < parameter.ChannelCount; i++) + { + voiceUpdateStates[i].Span[0].IsWaveBufferValid.Fill(false); + } + } + + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[0].Span[0]; + + for (int i = 0; i < Constants.VoiceWaveBufferCount; i++) + { + UpdateWaveBuffer(errorInfos.AsSpan().Slice(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext); + } + } + + /// <summary> + /// Update the internal state of one of the <see cref="WaveBuffer"/> of the <see cref="VoiceState"/>. + /// </summary> + /// <param name="errorInfos">A <see cref="Span{ErrorInfo}"/> used to report errors when mapping the <see cref="WaveBuffer"/>.</param> + /// <param name="waveBuffer">The <see cref="WaveBuffer"/> to update.</param> + /// <param name="inputWaveBuffer">The <see cref="WaveBufferInternal"/> from the user input.</param> + /// <param name="sampleFormat">The <see cref="SampleFormat"/> from the user input.</param> + /// <param name="isValid">If set to true, the server side wavebuffer is considered valid.</param> + /// <param name="mapper">The mapper to use.</param> + /// <param name="behaviourContext">The behaviour context.</param> + private void UpdateWaveBuffer(Span<ErrorInfo> errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext) + { + if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0) + { + mapper.ForceUnmap(ref waveBuffer.BufferAddressInfo); + waveBuffer.BufferAddressInfo.Setup(0, 0); + } + + if (!inputWaveBuffer.SentToServer || BufferInfoUnmapped) + { + if (inputWaveBuffer.IsSampleOffsetValid(sampleFormat)) + { + Debug.Assert(waveBuffer.IsSendToAudioProcessor); + + waveBuffer.IsSendToAudioProcessor = false; + waveBuffer.StartSampleOffset = inputWaveBuffer.StartSampleOffset; + waveBuffer.EndSampleOffset = inputWaveBuffer.EndSampleOffset; + waveBuffer.ShouldLoop = inputWaveBuffer.ShouldLoop; + waveBuffer.IsEndOfStream = inputWaveBuffer.IsEndOfStream; + waveBuffer.LoopStartSampleOffset = inputWaveBuffer.LoopFirstSampleOffset; + waveBuffer.LoopEndSampleOffset = inputWaveBuffer.LoopLastSampleOffset; + waveBuffer.LoopCount = inputWaveBuffer.LoopCount; + + BufferInfoUnmapped = !mapper.TryAttachBuffer(out ErrorInfo bufferInfoError, ref waveBuffer.BufferAddressInfo, inputWaveBuffer.Address, inputWaveBuffer.Size); + + errorInfos[0] = bufferInfoError; + + if (sampleFormat == SampleFormat.Adpcm && behaviourContext.IsAdpcmLoopContextBugFixed() && inputWaveBuffer.ContextAddress != 0) + { + bool adpcmLoopContextMapped = mapper.TryAttachBuffer(out ErrorInfo adpcmLoopContextInfoError, + ref waveBuffer.ContextAddressInfo, + inputWaveBuffer.ContextAddress, + inputWaveBuffer.ContextSize); + + errorInfos[1] = adpcmLoopContextInfoError; + + if (adpcmLoopContextMapped) + { + BufferInfoUnmapped = DataSourceStateUnmapped; + } + else + { + BufferInfoUnmapped = true; + } + } + else + { + waveBuffer.ContextAddressInfo.Setup(0, 0); + } + } + else + { + errorInfos[0].ErrorCode = ResultCode.InvalidAddressInfo; + errorInfos[0].ExtraErrorInfo = inputWaveBuffer.Address; + } + } + } + + /// <summary> + /// Reset the resources associated to this <see cref="VoiceState"/>. + /// </summary> + /// <param name="context">The voice context.</param> + private void ResetResources(VoiceContext context) + { + for (int i = 0; i < ChannelsCount; i++) + { + int channelResourceId = ChannelResourceIds[i]; + + ref VoiceChannelResource voiceChannelResource = ref context.GetChannelResource(channelResourceId); + + Debug.Assert(voiceChannelResource.IsUsed); + + Memory<VoiceUpdateState> dspSharedState = context.GetUpdateStateForDsp(channelResourceId); + + MemoryMarshal.Cast<VoiceUpdateState, byte>(dspSharedState.Span).Fill(0); + + voiceChannelResource.UpdateState(); + } + } + + /// <summary> + /// Flush a certain amount of <see cref="WaveBuffer"/>. + /// </summary> + /// <param name="waveBufferCount">The amount of wavebuffer to flush.</param> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + /// <param name="channelCount">The channel count from user input.</param> + private void FlushWaveBuffers(uint waveBufferCount, Memory<VoiceUpdateState>[] voiceUpdateStates, uint channelCount) + { + uint waveBufferIndex = WaveBuffersIndex; + + for (int i = 0; i < waveBufferCount; i++) + { + WaveBuffers[(int)waveBufferIndex].IsSendToAudioProcessor = true; + + for (int j = 0; j < channelCount; j++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[j].Span[0]; + + voiceUpdateState.WaveBufferIndex = (voiceUpdateState.WaveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + voiceUpdateState.WaveBufferConsumed++; + voiceUpdateState.IsWaveBufferValid[(int)waveBufferIndex] = false; + } + + waveBufferIndex = (waveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + } + } + + /// <summary> + /// Update the internal parameters for command generation. + /// </summary> + /// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param> + /// <returns>Return true if this voice should be played.</returns> + public bool UpdateParametersForCommandGeneration(Memory<VoiceUpdateState>[] voiceUpdateStates) + { + if (FlushWaveBufferCount != 0) + { + FlushWaveBuffers(FlushWaveBufferCount, voiceUpdateStates, ChannelsCount); + + FlushWaveBufferCount = 0; + } + + switch (PlayState) + { + case Types.PlayState.Started: + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref WaveBuffer wavebuffer = ref WaveBuffers[i]; + + if (!wavebuffer.IsSendToAudioProcessor) + { + for (int y = 0; y < ChannelsCount; y++) + { + Debug.Assert(!voiceUpdateStates[y].Span[0].IsWaveBufferValid[i]); + + voiceUpdateStates[y].Span[0].IsWaveBufferValid[i] = true; + } + + wavebuffer.IsSendToAudioProcessor = true; + } + } + + WasPlaying = false; + + ref VoiceUpdateState primaryVoiceUpdateState = ref voiceUpdateStates[0].Span[0]; + + for (int i = 0; i < primaryVoiceUpdateState.IsWaveBufferValid.Length; i++) + { + if (primaryVoiceUpdateState.IsWaveBufferValid[i]) + { + return true; + } + } + + return false; + + case Types.PlayState.Stopping: + for (int i = 0; i < WaveBuffers.Length; i++) + { + ref WaveBuffer wavebuffer = ref WaveBuffers[i]; + + wavebuffer.IsSendToAudioProcessor = true; + + for (int j = 0; j < ChannelsCount; j++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[j].Span[0]; + + if (voiceUpdateState.IsWaveBufferValid[i]) + { + voiceUpdateState.WaveBufferIndex = (voiceUpdateState.WaveBufferIndex + 1) % Constants.VoiceWaveBufferCount; + voiceUpdateState.WaveBufferConsumed++; + } + + voiceUpdateState.IsWaveBufferValid[i] = false; + } + } + + for (int i = 0; i < ChannelsCount; i++) + { + ref VoiceUpdateState voiceUpdateState = ref voiceUpdateStates[i].Span[0]; + + voiceUpdateState.Offset = 0; + voiceUpdateState.PlayedSampleCount = 0; + voiceUpdateState.Pitch.ToSpan().Fill(0); + voiceUpdateState.Fraction = 0; + voiceUpdateState.LoopContext = new Dsp.State.AdpcmLoopContext(); + } + + PlayState = Types.PlayState.Stopped; + WasPlaying = PreviousPlayState == Types.PlayState.Started; + + return WasPlaying; + + case Types.PlayState.Stopped: + case Types.PlayState.Paused: + foreach (ref WaveBuffer wavebuffer in WaveBuffers.ToSpan()) + { + wavebuffer.BufferAddressInfo.GetReference(true); + wavebuffer.ContextAddressInfo.GetReference(true); + } + + if (SampleFormat == SampleFormat.Adpcm) + { + if (DataSourceStateAddressInfo.CpuAddress != 0) + { + DataSourceStateAddressInfo.GetReference(true); + } + } + + WasPlaying = PreviousPlayState == Types.PlayState.Started; + + return WasPlaying; + default: + throw new NotImplementedException($"{PlayState}"); + } + } + + /// <summary> + /// Update the internal state for command generation. + /// </summary> + /// <param name="context">The voice context.</param> + /// <returns>Return true if this voice should be played.</returns> + public bool UpdateForCommandGeneration(VoiceContext context) + { + if (IsNew) + { + ResetResources(context); + PreviousVolume = Volume; + IsNew = false; + } + + Memory<VoiceUpdateState>[] voiceUpdateStates = new Memory<VoiceUpdateState>[Constants.VoiceChannelCountMax]; + + for (int i = 0; i < ChannelsCount; i++) + { + voiceUpdateStates[i] = context.GetUpdateStateForDsp(ChannelResourceIds[i]); + } + + return UpdateParametersForCommandGeneration(voiceUpdateStates); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs b/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs new file mode 100644 index 00000000..a03aa757 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Server/Voice/WaveBuffer.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Renderer.Server.MemoryPool; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Server.Voice +{ + /// <summary> + /// A wavebuffer used for server update. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x58, Pack = 1)] + public struct WaveBuffer + { + /// <summary> + /// The <see cref="AddressInfo"/> of the sample data of the wavebuffer. + /// </summary> + public AddressInfo BufferAddressInfo; + + /// <summary> + /// The <see cref="AddressInfo"/> of the context of the wavebuffer. + /// </summary> + /// <remarks>Only used by <see cref="Common.SampleFormat.Adpcm"/>.</remarks> + public AddressInfo ContextAddressInfo; + + + /// <summary> + /// First sample to play of the wavebuffer. + /// </summary> + public uint StartSampleOffset; + + /// <summary> + /// Last sample to play of the wavebuffer. + /// </summary> + public uint EndSampleOffset; + + /// <summary> + /// Set to true if the wavebuffer is looping. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool ShouldLoop; + + /// <summary> + /// Set to true if the wavebuffer is the end of stream. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsEndOfStream; + + /// <summary> + /// Set to true if the wavebuffer wasn't sent to the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool IsSendToAudioProcessor; + + /// <summary> + /// First sample to play when looping the wavebuffer. + /// </summary> + public uint LoopStartSampleOffset; + + /// <summary> + /// Last sample to play when looping the wavebuffer. + /// </summary> + public uint LoopEndSampleOffset; + + /// <summary> + /// The max loop count. + /// </summary> + public int LoopCount; + + /// <summary> + /// Create a new <see cref="Common.WaveBuffer"/> for use by the <see cref="Dsp.AudioProcessor"/>. + /// </summary> + /// <param name="version">The target version of the wavebuffer.</param> + /// <returns>A new <see cref="Common.WaveBuffer"/> for use by the <see cref="Dsp.AudioProcessor"/>.</returns> + public Common.WaveBuffer ToCommon(int version) + { + Common.WaveBuffer waveBuffer = new Common.WaveBuffer(); + + waveBuffer.Buffer = BufferAddressInfo.GetReference(true); + waveBuffer.BufferSize = (uint)BufferAddressInfo.Size; + + if (ContextAddressInfo.CpuAddress != 0) + { + waveBuffer.Context = ContextAddressInfo.GetReference(true); + waveBuffer.ContextSize = (uint)ContextAddressInfo.Size; + } + + waveBuffer.StartSampleOffset = StartSampleOffset; + waveBuffer.EndSampleOffset = EndSampleOffset; + waveBuffer.Looping = ShouldLoop; + waveBuffer.IsEndOfStream = IsEndOfStream; + + if (version == 2) + { + waveBuffer.LoopCount = LoopCount; + waveBuffer.LoopStartSampleOffset = LoopStartSampleOffset; + waveBuffer.LoopEndSampleOffset = LoopEndSampleOffset; + } + else + { + waveBuffer.LoopCount = -1; + } + + return waveBuffer; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/AudioProcessorMemoryManager.cs b/Ryujinx.Audio/Renderer/Utils/AudioProcessorMemoryManager.cs new file mode 100644 index 00000000..99cbfcdc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/AudioProcessorMemoryManager.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System.Runtime.CompilerServices; + +using DspAddress = System.UInt64; +using CpuAddress = System.UInt64; + +namespace Ryujinx.Audio.Renderer.Utils +{ + /// <summary> + /// The <see cref="Dsp.AudioProcessor"/> memory management + /// </summary> + /// <remarks>This is stub for the most part but is kept to permit LLE if wanted.</remarks> + static class AudioProcessorMemoryManager + { + /// <summary> + /// Map the given <see cref="CpuAddress"/> to the <see cref="Dsp.AudioProcessor"/> address space. + /// </summary> + /// <param name="processHandle">The process owning the CPU memory.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to map.</param> + /// <param name="size">The size of the CPU memory region to map.</param> + /// <returns>The address on the <see cref="Dsp.AudioProcessor"/> address space.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DspAddress Map(uint processHandle, CpuAddress cpuAddress, ulong size) + { + return cpuAddress; + } + + /// <summary> + /// Unmap the given <see cref="CpuAddress"/> from the <see cref="Dsp.AudioProcessor"/> address space. + /// </summary> + /// <param name="processHandle">The process owning the CPU memory.</param> + /// <param name="cpuAddress">The <see cref="CpuAddress"/> to unmap.</param> + /// <param name="size">The size of the CPU memory region to unmap.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Unmap(uint processHandle, CpuAddress cpuAddress, ulong size) + { + } + + /// <summary> + /// Invalidate the <see cref="Dsp.AudioProcessor"/> data cache at the given address. + /// </summary> + /// <param name="address">The base DSP address to invalidate</param> + /// <param name="size">The size of the DSP memory region to invalidate.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvalidateDspCache(DspAddress address, ulong size) + { + } + + /// <summary> + /// Invalidate the CPU data cache at the given address. + /// </summary> + /// <param name="address">The base <see cref="CpuAddress"/> to invalidate</param> + /// <param name="size">The size of the CPU memory region to invalidate.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvalidateDataCache(CpuAddress address, ulong size) + { + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/BitArray.cs b/Ryujinx.Audio/Renderer/Utils/BitArray.cs new file mode 100644 index 00000000..6109dd01 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/BitArray.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.Renderer.Utils +{ + /// <summary> + /// A simple bit array implementation backed by a <see cref="Memory{T}"/>. + /// </summary> + public class BitArray + { + /// <summary> + /// The backing storage of the <see cref="BitArray"/>. + /// </summary> + private Memory<byte> _storage; + + /// <summary> + /// Create a new <see cref="BitArray"/> from <see cref="Memory{T}"/>. + /// </summary> + /// <param name="storage">The backing storage of the <see cref="BitArray"/>.</param> + public BitArray(Memory<byte> storage) + { + _storage = storage; + } + + /// <summary> + /// Get the byte position of a given bit index. + /// </summary> + /// <param name="index">A bit index.</param> + /// <returns>The byte position of a given bit index.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ToPosition(int index) => index / 8; + + /// <summary> + /// Get the bit position of a given bit index inside a byte. + /// </summary> + /// <param name="index">A bit index.</param> + /// <returns>The bit position of a given bit index inside a byte.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ToBitPosition(int index) => index % 8; + + /// <summary> + /// Test if the bit at the given index is set. + /// </summary> + /// <param name="index">A bit index.</param> + /// <returns>Return true if the bit at the given index is set</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Test(int index) + { + ulong mask = 1ul << ToBitPosition(index); + + return (_storage.Span[ToPosition(index)] & mask) == mask; + } + + /// <summary> + /// Set the bit at the given index. + /// </summary> + /// <param name="index">A bit index.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index) + { + Set(index, true); + } + + /// <summary> + /// Reset the bit at the given index. + /// </summary> + /// <param name="index">A bit index.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset(int index) + { + Set(index, false); + } + + /// <summary> + /// Set a bit value at the given index. + /// </summary> + /// <param name="index">A bit index.</param> + /// <param name="value">The new bit value.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Set(int index, bool value) + { + byte mask = (byte)(1 << ToBitPosition(index)); + + if (value) + { + _storage.Span[ToPosition(index)] |= mask; + } + else + { + _storage.Span[ToPosition(index)] &= (byte)~mask; + } + } + + /// <summary> + /// Reset all bits in the storage. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _storage.Span.Fill(0); + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/FileHardwareDevice.cs b/Ryujinx.Audio/Renderer/Utils/FileHardwareDevice.cs new file mode 100644 index 00000000..2008bafc --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/FileHardwareDevice.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.Audio.Renderer.Utils +{ + /// <summary> + /// A <see cref="IHardwareDevice"/> that outputs to a wav file. + /// </summary> + public class FileHardwareDevice : IHardwareDevice + { + private FileStream _stream; + private uint _channelCount; + private uint _sampleRate; + + private const int HeaderSize = 44; + + public FileHardwareDevice(string path, uint channelCount, uint sampleRate) + { + _stream = File.OpenWrite(path); + _channelCount = channelCount; + _sampleRate = sampleRate; + + _stream.Seek(HeaderSize, SeekOrigin.Begin); + } + + private void UpdateHeader() + { + var writer = new BinaryWriter(_stream); + + long currentPos = writer.Seek(0, SeekOrigin.Current); + + writer.Seek(0, SeekOrigin.Begin); + + writer.Write(Encoding.ASCII.GetBytes("RIFF")); + writer.Write((int)(writer.BaseStream.Length - 8)); + writer.Write(Encoding.ASCII.GetBytes("WAVE")); + writer.Write(Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); + writer.Write((short)1); + writer.Write((short)GetChannelCount()); + writer.Write(GetSampleRate()); + writer.Write(GetSampleRate() * GetChannelCount() * sizeof(short)); + writer.Write((short)(GetChannelCount() * sizeof(short))); + writer.Write((short)(sizeof(short) * 8)); + writer.Write(Encoding.ASCII.GetBytes("data")); + writer.Write((int)(writer.BaseStream.Length - HeaderSize)); + + writer.Seek((int)currentPos, SeekOrigin.Begin); + } + + public void AppendBuffer(ReadOnlySpan<short> data, uint channelCount) + { + _stream.Write(MemoryMarshal.Cast<short, byte>(data)); + + UpdateHeader(); + _stream.Flush(); + } + + public uint GetChannelCount() + { + return _channelCount; + } + + public uint GetSampleRate() + { + return _sampleRate; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _stream?.Flush(); + _stream?.Dispose(); + + _stream = null; + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/Mailbox.cs b/Ryujinx.Audio/Renderer/Utils/Mailbox.cs new file mode 100644 index 00000000..a32de870 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/Mailbox.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Collections.Concurrent; + +namespace Ryujinx.Audio.Renderer.Utils +{ + /// <summary> + /// A simple generic message queue for unmanaged types. + /// </summary> + /// <typeparam name="T">The target unmanaged type used</typeparam> + public class Mailbox<T> : IDisposable where T : unmanaged + { + private BlockingCollection<T> _messageQueue; + private BlockingCollection<T> _responseQueue; + + public Mailbox() + { + _messageQueue = new BlockingCollection<T>(1); + _responseQueue = new BlockingCollection<T>(1); + } + + public void SendMessage(T data) + { + _messageQueue.Add(data); + } + + public void SendResponse(T data) + { + _responseQueue.Add(data); + } + + public T ReceiveMessage() + { + return _messageQueue.Take(); + } + + public T ReceiveResponse() + { + return _responseQueue.Take(); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _messageQueue.Dispose(); + _responseQueue.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs b/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs new file mode 100644 index 00000000..70a2fce6 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/SpanIOHelper.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Utils +{ + /// <summary> + /// Helper for IO operations on <see cref="Span{T}"/> and <see cref="Memory{T}"/>. + /// </summary> + public static class SpanIOHelper + { + /// <summary> + /// Write the given data to the given backing <see cref="Memory{T}"/> and move cursor after the written data. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="Memory{T}"/> to store the data.</param> + /// <param name="data">The data to write to the backing <see cref="Memory{T}"/>.</param> + public static void Write<T>(ref Memory<byte> backingMemory, ref T data) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + MemoryMarshal.Write<T>(backingMemory.Span.Slice(0, size), ref data); + + backingMemory = backingMemory.Slice(size); + } + + /// <summary> + /// Write the given data to the given backing <see cref="Span{T}"/> and move cursor after the written data. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="Span{T}"/> to store the data.</param> + /// <param name="data">The data to write to the backing <see cref="Span{T}"/>.</param> + public static void Write<T>(ref Span<byte> backingMemory, ref T data) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + MemoryMarshal.Write<T>(backingMemory.Slice(0, size), ref data); + + backingMemory = backingMemory.Slice(size); + } + + /// <summary> + /// Get a <see cref="Span{T}"/> out of a <paramref name="backingMemory"/> and move cursor after T size. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="Memory{T}"/> to get a <see cref="Span{T}"/> from.</param> + /// <returns>A <see cref="Span{T}"/> from backing <see cref="Memory{T}"/>.</returns> + public static Span<T> GetWriteRef<T>(ref Memory<byte> backingMemory) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + Span<T> result = MemoryMarshal.Cast<byte, T>(backingMemory.Span.Slice(0, size)); + + backingMemory = backingMemory.Slice(size); + + return result; + } + + /// <summary> + /// Get a <see cref="Span{T}"/> out of a backingMemory and move cursor after T size. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="Span{T}"/> to get a <see cref="Span{T}"/> from.</param> + /// <returns>A <see cref="Span{T}"/> from backing <see cref="Span{T}"/>.</returns> + public static Span<T> GetWriteRef<T>(ref Span<byte> backingMemory) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + Span<T> result = MemoryMarshal.Cast<byte, T>(backingMemory.Slice(0, size)); + + backingMemory = backingMemory.Slice(size); + + return result; + } + + /// <summary> + /// Read data from the given backing <see cref="ReadOnlyMemory{T}"/> and move cursor after the read data. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="ReadOnlyMemory{T}"/> to read data from.</param> + /// <returns>Return the read data.</returns> + public static T Read<T>(ref ReadOnlyMemory<byte> backingMemory) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + T result = MemoryMarshal.Read<T>(backingMemory.Span.Slice(0, size)); + + backingMemory = backingMemory.Slice(size); + + return result; + } + + /// <summary> + /// Read data from the given backing <see cref="ReadOnlySpan{T}"/> and move cursor after the read data. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="backingMemory">The backing <see cref="ReadOnlySpan{T}"/> to read data from.</param> + /// <returns>Return the read data.</returns> + public static T Read<T>(ref ReadOnlySpan<byte> backingMemory) where T : unmanaged + { + int size = Unsafe.SizeOf<T>(); + + if (size > backingMemory.Length) + { + throw new ArgumentOutOfRangeException(); + } + + T result = MemoryMarshal.Read<T>(backingMemory.Slice(0, size)); + + backingMemory = backingMemory.Slice(size); + + return result; + } + + /// <summary> + /// Extract a <see cref="Memory{T}"/> at the given index. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="memory">The <see cref="Memory{T}"/> to extract the data from.</param> + /// <param name="id">The id in the provided memory.</param> + /// <param name="count">The max allowed count. (for bound checking of the id in debug mode)</param> + /// <returns>a <see cref="Memory{T}"/> at the given id.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory<T> GetMemory<T>(Memory<T> memory, int id, uint count) where T : unmanaged + { + Debug.Assert(id >= 0 && id < count); + + return memory.Slice(id, 1); + } + + /// <summary> + /// Extract a ref T at the given index. + /// </summary> + /// <typeparam name="T">The data type.</typeparam> + /// <param name="memory">The <see cref="Memory{T}"/> to extract the data from.</param> + /// <param name="id">The id in the provided memory.</param> + /// <param name="count">The max allowed count. (for bound checking of the id in debug mode)</param> + /// <returns>a ref T at the given id.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T GetFromMemory<T>(Memory<T> memory, int id, uint count) where T : unmanaged + { + return ref GetMemory(memory, id, count).Span[0]; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/SpanMemoryManager.cs b/Ryujinx.Audio/Renderer/Utils/SpanMemoryManager.cs new file mode 100644 index 00000000..102a9f95 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/SpanMemoryManager.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Renderer.Utils +{ + public sealed unsafe class SpanMemoryManager<T> : MemoryManager<T> + where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + public SpanMemoryManager(Span<T> span) + { + fixed (T* ptr = &MemoryMarshal.GetReference(span)) + { + _pointer = ptr; + _length = span.Length; + } + } + + public override Span<T> GetSpan() => new Span<T>(_pointer, _length); + + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0 || elementIndex >= _length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + return new MemoryHandle(_pointer + elementIndex); + } + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } + + public static Memory<T> Cast<TFrom>(Memory<TFrom> memory) where TFrom : unmanaged + { + return new SpanMemoryManager<T>(MemoryMarshal.Cast<TFrom, T>(memory.Span)).Memory; + } + } +} diff --git a/Ryujinx.Audio/Renderer/Utils/SplitterHardwareDevice.cs b/Ryujinx.Audio/Renderer/Utils/SplitterHardwareDevice.cs new file mode 100644 index 00000000..c5411ac0 --- /dev/null +++ b/Ryujinx.Audio/Renderer/Utils/SplitterHardwareDevice.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +using Ryujinx.Audio.Integration; +using System; + +namespace Ryujinx.Audio.Renderer.Utils +{ + public class SplitterHardwareDevice : IHardwareDevice + { + private IHardwareDevice _baseDevice; + private IHardwareDevice _secondaryDevice; + + public SplitterHardwareDevice(IHardwareDevice baseDevice, IHardwareDevice secondaryDevice) + { + _baseDevice = baseDevice; + _secondaryDevice = secondaryDevice; + } + + public void AppendBuffer(ReadOnlySpan<short> data, uint channelCount) + { + _baseDevice.AppendBuffer(data, channelCount); + _secondaryDevice?.AppendBuffer(data, channelCount); + } + + public uint GetChannelCount() + { + return _baseDevice.GetChannelCount(); + } + + public uint GetSampleRate() + { + return _baseDevice.GetSampleRate(); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _baseDevice.Dispose(); + _secondaryDevice?.Dispose(); + } + } + } +} diff --git a/Ryujinx.Audio/Renderers/DummyAudioOut.cs b/Ryujinx.Audio/Renderers/DummyAudioOut.cs deleted file mode 100644 index cd197592..00000000 --- a/Ryujinx.Audio/Renderers/DummyAudioOut.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Ryujinx.Audio -{ - /// <summary> - /// A Dummy audio renderer that does not output any audio - /// </summary> - public class DummyAudioOut : IAalOutput - { - private int _lastTrackId = 1; - private float _volume = 1.0f; - - private ConcurrentQueue<int> _trackIds; - private ConcurrentQueue<long> _buffers; - private ConcurrentDictionary<int, ReleaseCallback> _releaseCallbacks; - private ulong _playedSampleCount; - - public DummyAudioOut() - { - _buffers = new ConcurrentQueue<long>(); - _trackIds = new ConcurrentQueue<int>(); - _releaseCallbacks = new ConcurrentDictionary<int, ReleaseCallback>(); - } - - /// <summary> - /// Dummy audio output is always available, Baka! - /// </summary> - public static bool IsSupported => true; - - public PlaybackState GetState(int trackId) => PlaybackState.Stopped; - - public bool SupportsChannelCount(int channels) - { - return true; - } - - public int OpenHardwareTrack(int sampleRate, int hardwareChannels, int virtualChannels, ReleaseCallback callback) - { - if (!_trackIds.TryDequeue(out int trackId)) - { - trackId = ++_lastTrackId; - } - - _releaseCallbacks[trackId] = callback; - - return trackId; - } - - public void CloseTrack(int trackId) - { - _trackIds.Enqueue(trackId); - _releaseCallbacks.Remove(trackId, out _); - } - - public bool ContainsBuffer(int trackID, long bufferTag) => false; - - public long[] GetReleasedBuffers(int trackId, int maxCount) - { - List<long> bufferTags = new List<long>(); - - for (int i = 0; i < maxCount; i++) - { - if (!_buffers.TryDequeue(out long tag)) - { - break; - } - - bufferTags.Add(tag); - } - - return bufferTags.ToArray(); - } - - public void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer) where T : struct - { - _buffers.Enqueue(bufferTag); - - _playedSampleCount += (ulong)buffer.Length; - - if (_releaseCallbacks.TryGetValue(trackId, out var callback)) - { - callback?.Invoke(); - } - } - - public void Start(int trackId) { } - - public void Stop(int trackId) { } - - public uint GetBufferCount(int trackId) => (uint)_buffers.Count; - - public ulong GetPlayedSampleCount(int trackId) => _playedSampleCount; - - public bool FlushBuffers(int trackId) => false; - - public float GetVolume(int trackId) => _volume; - - public void SetVolume(int trackId, float volume) - { - _volume = volume; - } - - public void Dispose() - { - _buffers.Clear(); - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs b/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs deleted file mode 100644 index 1ad82319..00000000 --- a/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs +++ /dev/null @@ -1,404 +0,0 @@ -using OpenTK.Audio; -using OpenTK.Audio.OpenAL; -using System; -using System.Collections.Concurrent; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Ryujinx.Audio -{ - /// <summary> - /// An audio renderer that uses OpenAL as the audio backend - /// </summary> - public class OpenALAudioOut : IAalOutput, IDisposable - { - /// <summary> - /// The maximum amount of tracks we can issue simultaneously - /// </summary> - private const int MaxTracks = 256; - - /// <summary> - /// The <see cref="OpenTK.Audio"/> audio context - /// </summary> - private AudioContext _context; - - /// <summary> - /// An object pool containing <see cref="OpenALAudioTrack"/> objects - /// </summary> - private ConcurrentDictionary<int, OpenALAudioTrack> _tracks; - - /// <summary> - /// True if the thread need to keep polling - /// </summary> - private bool _keepPolling; - - /// <summary> - /// The poller thread audio context - /// </summary> - private Thread _audioPollerThread; - - /// <summary> - /// True if OpenAL is supported on the device - /// </summary> - public static bool IsSupported - { - get - { - try - { - return AudioContext.AvailableDevices.Count > 0; - } - catch - { - return false; - } - } - } - - public OpenALAudioOut() - { - _context = new AudioContext(); - _tracks = new ConcurrentDictionary<int, OpenALAudioTrack>(); - _keepPolling = true; - _audioPollerThread = new Thread(AudioPollerWork) - { - Name = "Audio.PollerThread" - }; - - _audioPollerThread.Start(); - } - - private void AudioPollerWork() - { - do - { - foreach (OpenALAudioTrack track in _tracks.Values) - { - lock (track) - { - track.CallReleaseCallbackIfNeeded(); - } - } - - // If it's not slept it will waste cycles. - Thread.Sleep(10); - } - while (_keepPolling); - - foreach (OpenALAudioTrack track in _tracks.Values) - { - track.Dispose(); - } - - _tracks.Clear(); - _context.Dispose(); - } - - public bool SupportsChannelCount(int channels) - { - // NOTE: OpenAL doesn't give us a way to know if the 5.1 setup is supported by hardware or actually emulated. - // TODO: find a way to determine hardware support. - return channels == 1 || channels == 2; - } - - /// <summary> - /// Creates a new audio track with the specified parameters - /// </summary> - /// <param name="sampleRate">The requested sample rate</param> - /// <param name="hardwareChannels">The requested hardware channels</param> - /// <param name="virtualChannels">The requested virtual channels</param> - /// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param> - /// <returns>The created track's Track ID</returns> - public int OpenHardwareTrack(int sampleRate, int hardwareChannels, int virtualChannels, ReleaseCallback callback) - { - OpenALAudioTrack track = new OpenALAudioTrack(sampleRate, GetALFormat(hardwareChannels), hardwareChannels, virtualChannels, callback); - - for (int id = 0; id < MaxTracks; id++) - { - if (_tracks.TryAdd(id, track)) - { - return id; - } - } - - return -1; - } - - private ALFormat GetALFormat(int channels) - { - switch (channels) - { - case 1: return ALFormat.Mono16; - case 2: return ALFormat.Stereo16; - case 6: return ALFormat.Multi51Chn16Ext; - } - - throw new ArgumentOutOfRangeException(nameof(channels)); - } - - /// <summary> - /// Stops playback and closes the track specified by <paramref name="trackId"/> - /// </summary> - /// <param name="trackId">The ID of the track to close</param> - public void CloseTrack(int trackId) - { - if (_tracks.TryRemove(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - track.Dispose(); - } - } - } - - /// <summary> - /// Returns a value indicating whether the specified buffer is currently reserved by the specified track - /// </summary> - /// <param name="trackId">The track to check</param> - /// <param name="bufferTag">The buffer tag to check</param> - public bool ContainsBuffer(int trackId, long bufferTag) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - return track.ContainsBuffer(bufferTag); - } - } - - return false; - } - - /// <summary> - /// Gets a list of buffer tags the specified track is no longer reserving - /// </summary> - /// <param name="trackId">The track to retrieve buffer tags from</param> - /// <param name="maxCount">The maximum amount of buffer tags to retrieve</param> - /// <returns>Buffers released by the specified track</returns> - public long[] GetReleasedBuffers(int trackId, int maxCount) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - return track.GetReleasedBuffers(maxCount); - } - } - - return null; - } - - /// <summary> - /// Appends an audio buffer to the specified track - /// </summary> - /// <typeparam name="T">The sample type of the buffer</typeparam> - /// <param name="trackId">The track to append the buffer to</param> - /// <param name="bufferTag">The internal tag of the buffer</param> - /// <param name="buffer">The buffer to append to the track</param> - public void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer) where T : struct - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - int bufferId = track.AppendBuffer(bufferTag); - - // Do we need to downmix? - if (track.HardwareChannels != track.VirtualChannels) - { - short[] downmixedBuffer; - - ReadOnlySpan<short> bufferPCM16 = MemoryMarshal.Cast<T, short>(buffer); - - if (track.VirtualChannels == 6) - { - downmixedBuffer = Downmixing.DownMixSurroundToStereo(bufferPCM16); - - if (track.HardwareChannels == 1) - { - downmixedBuffer = Downmixing.DownMixStereoToMono(downmixedBuffer); - } - } - else if (track.VirtualChannels == 2) - { - downmixedBuffer = Downmixing.DownMixStereoToMono(bufferPCM16); - } - else - { - throw new NotImplementedException($"Downmixing from {track.VirtualChannels} to {track.HardwareChannels} not implemented!"); - } - - AL.BufferData(bufferId, track.Format, downmixedBuffer, downmixedBuffer.Length * sizeof(ushort), track.SampleRate); - } - else - { - AL.BufferData(bufferId, track.Format, buffer, buffer.Length * sizeof(ushort), track.SampleRate); - } - - AL.SourceQueueBuffer(track.SourceId, bufferId); - - StartPlaybackIfNeeded(track); - - track.PlayedSampleCount += (ulong)buffer.Length; - } - } - } - - /// <summary> - /// Starts playback - /// </summary> - /// <param name="trackId">The ID of the track to start playback on</param> - public void Start(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - track.State = PlaybackState.Playing; - - StartPlaybackIfNeeded(track); - } - } - } - - private void StartPlaybackIfNeeded(OpenALAudioTrack track) - { - AL.GetSource(track.SourceId, ALGetSourcei.SourceState, out int stateInt); - - ALSourceState State = (ALSourceState)stateInt; - - if (State != ALSourceState.Playing && track.State == PlaybackState.Playing) - { - AL.SourcePlay(track.SourceId); - } - } - - /// <summary> - /// Stops playback - /// </summary> - /// <param name="trackId">The ID of the track to stop playback on</param> - public void Stop(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - track.State = PlaybackState.Stopped; - - AL.SourceStop(track.SourceId); - } - } - } - - /// <summary> - /// Get track buffer count - /// </summary> - /// <param name="trackId">The ID of the track to get buffer count</param> - public uint GetBufferCount(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - return track.BufferCount; - } - } - - return 0; - } - - /// <summary> - /// Get track played sample count - /// </summary> - /// <param name="trackId">The ID of the track to get played sample count</param> - public ulong GetPlayedSampleCount(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - return track.PlayedSampleCount; - } - } - - return 0; - } - - /// <summary> - /// Flush all track buffers - /// </summary> - /// <param name="trackId">The ID of the track to flush</param> - public bool FlushBuffers(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - track.FlushBuffers(); - } - } - - return false; - } - - /// <summary> - /// Set track volume - /// </summary> - /// <param name="trackId">The ID of the track to set volume</param> - /// <param name="volume">The volume of the track</param> - public void SetVolume(int trackId, float volume) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - track.SetVolume(volume); - } - } - } - - /// <summary> - /// Get track volume - /// </summary> - /// <param name="trackId">The ID of the track to get volume</param> - public float GetVolume(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - lock (track) - { - return track.GetVolume(); - } - } - - return 1.0f; - } - - /// <summary> - /// Gets the current playback state of the specified track - /// </summary> - /// <param name="trackId">The track to retrieve the playback state for</param> - public PlaybackState GetState(int trackId) - { - if (_tracks.TryGetValue(trackId, out OpenALAudioTrack track)) - { - return track.State; - } - - return PlaybackState.Stopped; - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _keepPolling = false; - } - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioTrack.cs b/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioTrack.cs deleted file mode 100644 index 690129eb..00000000 --- a/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioTrack.cs +++ /dev/null @@ -1,183 +0,0 @@ -using OpenTK.Audio.OpenAL; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Ryujinx.Audio -{ - internal class OpenALAudioTrack : IDisposable - { - public int SourceId { get; private set; } - public int SampleRate { get; private set; } - public ALFormat Format { get; private set; } - public PlaybackState State { get; set; } - - public int HardwareChannels { get; } - public int VirtualChannels { get; } - public uint BufferCount => (uint)_buffers.Count; - public ulong PlayedSampleCount { get; set; } - - private ReleaseCallback _callback; - - private ConcurrentDictionary<long, int> _buffers; - - private Queue<long> _queuedTagsQueue; - private Queue<long> _releasedTagsQueue; - - private bool _disposed; - - public OpenALAudioTrack(int sampleRate, ALFormat format, int hardwareChannels, int virtualChannels, ReleaseCallback callback) - { - SampleRate = sampleRate; - Format = format; - State = PlaybackState.Stopped; - SourceId = AL.GenSource(); - - HardwareChannels = hardwareChannels; - VirtualChannels = virtualChannels; - - _callback = callback; - - _buffers = new ConcurrentDictionary<long, int>(); - - _queuedTagsQueue = new Queue<long>(); - _releasedTagsQueue = new Queue<long>(); - } - - public bool ContainsBuffer(long tag) - { - foreach (long queuedTag in _queuedTagsQueue) - { - if (queuedTag == tag) - { - return true; - } - } - - return false; - } - - public long[] GetReleasedBuffers(int count) - { - AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int releasedCount); - - releasedCount += _releasedTagsQueue.Count; - - if (count > releasedCount) - { - count = releasedCount; - } - - List<long> tags = new List<long>(); - - while (count-- > 0 && _releasedTagsQueue.TryDequeue(out long tag)) - { - tags.Add(tag); - } - - while (count-- > 0 && _queuedTagsQueue.TryDequeue(out long tag)) - { - AL.SourceUnqueueBuffers(SourceId, 1); - - tags.Add(tag); - } - - return tags.ToArray(); - } - - public int AppendBuffer(long tag) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - int id = AL.GenBuffer(); - - _buffers.AddOrUpdate(tag, id, (key, oldId) => - { - AL.DeleteBuffer(oldId); - - return id; - }); - - _queuedTagsQueue.Enqueue(tag); - - return id; - } - - public void CallReleaseCallbackIfNeeded() - { - AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int releasedCount); - - if (releasedCount > 0) - { - // If we signal, then we also need to have released buffers available - // to return when GetReleasedBuffers is called. - // If playback needs to be re-started due to all buffers being processed, - // then OpenAL zeros the counts (ReleasedCount), so we keep it on the queue. - while (releasedCount-- > 0 && _queuedTagsQueue.TryDequeue(out long tag)) - { - AL.SourceUnqueueBuffers(SourceId, 1); - - _releasedTagsQueue.Enqueue(tag); - } - - _callback(); - } - } - - public bool FlushBuffers() - { - while (_queuedTagsQueue.TryDequeue(out long tag)) - { - _releasedTagsQueue.Enqueue(tag); - } - - _callback(); - - foreach (var buffer in _buffers) - { - AL.DeleteBuffer(buffer.Value); - } - - bool heldBuffers = _buffers.Count > 0; - - _buffers.Clear(); - - return heldBuffers; - } - - public void SetVolume(float volume) - { - AL.Source(SourceId, ALSourcef.Gain, volume); - } - - public float GetVolume() - { - AL.GetSource(SourceId, ALSourcef.Gain, out float volume); - - return volume; - } - - public void Dispose() - { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing && !_disposed) - { - _disposed = true; - - AL.DeleteSource(SourceId); - - foreach (int id in _buffers.Values) - { - AL.DeleteBuffer(id); - } - } - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs deleted file mode 100644 index 92bf42c4..00000000 --- a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs +++ /dev/null @@ -1,361 +0,0 @@ -using Ryujinx.Audio.SoundIo; -using SoundIOSharp; -using System.Collections.Generic; - -namespace Ryujinx.Audio -{ - /// <summary> - /// An audio renderer that uses libsoundio as the audio backend - /// </summary> - public class SoundIoAudioOut : IAalOutput - { - /// <summary> - /// The maximum amount of tracks we can issue simultaneously - /// </summary> - private const int MaximumTracks = 256; - - /// <summary> - /// The <see cref="SoundIO"/> audio context - /// </summary> - private SoundIO _audioContext; - - /// <summary> - /// The <see cref="SoundIODevice"/> audio device - /// </summary> - private SoundIODevice _audioDevice; - - /// <summary> - /// An object pool containing <see cref="SoundIoAudioTrack"/> objects - /// </summary> - private SoundIoAudioTrackPool _trackPool; - - /// <summary> - /// True if SoundIO is supported on the device - /// </summary> - public static bool IsSupported - { - get - { - return IsSupportedInternal(); - } - } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoAudioOut"/> - /// </summary> - public SoundIoAudioOut() - { - _audioContext = new SoundIO(); - - _audioContext.Connect(); - _audioContext.FlushEvents(); - - _audioDevice = FindNonRawDefaultAudioDevice(_audioContext, true); - _trackPool = new SoundIoAudioTrackPool(_audioContext, _audioDevice, MaximumTracks); - } - - public bool SupportsChannelCount(int channels) - { - return _audioDevice.SupportsChannelCount(channels); - } - - /// <summary> - /// Creates a new audio track with the specified parameters - /// </summary> - /// <param name="sampleRate">The requested sample rate</param> - /// <param name="hardwareChannels">The requested hardware channels</param> - /// <param name="virtualChannels">The requested virtual channels</param> - /// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param> - /// <returns>The created track's Track ID</returns> - public int OpenHardwareTrack(int sampleRate, int hardwareChannels, int virtualChannels, ReleaseCallback callback) - { - if (!_trackPool.TryGet(out SoundIoAudioTrack track)) - { - return -1; - } - - // Open the output. We currently only support 16-bit signed LE - track.Open(sampleRate, hardwareChannels, virtualChannels, callback, SoundIOFormat.S16LE); - - return track.TrackID; - } - - /// <summary> - /// Stops playback and closes the track specified by <paramref name="trackId"/> - /// </summary> - /// <param name="trackId">The ID of the track to close</param> - public void CloseTrack(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - // Close and dispose of the track - track.Close(); - - // Recycle the track back into the pool - _trackPool.Put(track); - } - } - - /// <summary> - /// Returns a value indicating whether the specified buffer is currently reserved by the specified track - /// </summary> - /// <param name="trackId">The track to check</param> - /// <param name="bufferTag">The buffer tag to check</param> - public bool ContainsBuffer(int trackId, long bufferTag) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.ContainsBuffer(bufferTag); - } - - return false; - } - - /// <summary> - /// Gets a list of buffer tags the specified track is no longer reserving - /// </summary> - /// <param name="trackId">The track to retrieve buffer tags from</param> - /// <param name="maxCount">The maximum amount of buffer tags to retrieve</param> - /// <returns>Buffers released by the specified track</returns> - public long[] GetReleasedBuffers(int trackId, int maxCount) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - List<long> bufferTags = new List<long>(); - - while(maxCount-- > 0 && track.ReleasedBuffers.TryDequeue(out long tag)) - { - bufferTags.Add(tag); - } - - return bufferTags.ToArray(); - } - - return new long[0]; - } - - /// <summary> - /// Appends an audio buffer to the specified track - /// </summary> - /// <typeparam name="T">The sample type of the buffer</typeparam> - /// <param name="trackId">The track to append the buffer to</param> - /// <param name="bufferTag">The internal tag of the buffer</param> - /// <param name="buffer">The buffer to append to the track</param> - public void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer) where T : struct - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - track.AppendBuffer(bufferTag, buffer); - } - } - - /// <summary> - /// Starts playback - /// </summary> - /// <param name="trackId">The ID of the track to start playback on</param> - public void Start(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - track.Start(); - } - } - - /// <summary> - /// Stops playback - /// </summary> - /// <param name="trackId">The ID of the track to stop playback on</param> - public void Stop(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - track.Stop(); - } - } - - /// <summary> - /// Get track buffer count - /// </summary> - /// <param name="trackId">The ID of the track to get buffer count</param> - public uint GetBufferCount(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.BufferCount; - } - - return 0; - } - - /// <summary> - /// Get track played sample count - /// </summary> - /// <param name="trackId">The ID of the track to get played sample</param> - public ulong GetPlayedSampleCount(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.PlayedSampleCount; - } - - return 0; - } - - /// <summary> - /// Flush all track buffers - /// </summary> - /// <param name="trackId">The ID of the track to flush</param> - public bool FlushBuffers(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.FlushBuffers(); - } - - return false; - } - - /// <summary> - /// Set track volume - /// </summary> - /// <param name="volume">The volume of the playback</param> - public void SetVolume(int trackId, float volume) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - track.AudioStream.SetVolume(volume); - } - } - - /// <summary> - /// Get track volume - /// </summary> - public float GetVolume(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.AudioStream.Volume; - } - - return 1.0f; - } - - /// <summary> - /// Gets the current playback state of the specified track - /// </summary> - /// <param name="trackId">The track to retrieve the playback state for</param> - public PlaybackState GetState(int trackId) - { - if (_trackPool.TryGet(trackId, out SoundIoAudioTrack track)) - { - return track.State; - } - - return PlaybackState.Stopped; - } - - /// <summary> - /// Releases the unmanaged resources used by the <see cref="SoundIoAudioOut" /> - /// </summary> - public void Dispose() - { - _trackPool.Dispose(); - _audioContext.Disconnect(); - _audioContext.Dispose(); - } - - /// <summary> - /// Searches for a shared version of the default audio device - /// </summary> - /// <param name="audioContext">The <see cref="SoundIO"/> audio context</param> - /// <param name="fallback">Whether to fallback to the raw default audio device if a non-raw device cannot be found</param> - private static SoundIODevice FindNonRawDefaultAudioDevice(SoundIO audioContext, bool fallback = false) - { - SoundIODevice defaultAudioDevice = audioContext.GetOutputDevice(audioContext.DefaultOutputDeviceIndex); - - if (!defaultAudioDevice.IsRaw) - { - return defaultAudioDevice; - } - - for (int i = 0; i < audioContext.BackendCount; i++) - { - SoundIODevice audioDevice = audioContext.GetOutputDevice(i); - - if (audioDevice.Id == defaultAudioDevice.Id && !audioDevice.IsRaw) - { - return audioDevice; - } - } - - return fallback ? defaultAudioDevice : null; - } - - /// <summary> - /// Determines if SoundIO can connect to a supported backend - /// </summary> - /// <returns></returns> - private static bool IsSupportedInternal() - { - SoundIO context = null; - SoundIODevice device = null; - SoundIOOutStream stream = null; - - bool backendDisconnected = false; - - try - { - context = new SoundIO(); - - context.OnBackendDisconnect = (i) => { - backendDisconnected = true; - }; - - context.Connect(); - context.FlushEvents(); - - if (backendDisconnected) - { - return false; - } - - if (context.OutputDeviceCount == 0) - { - return false; - } - - device = FindNonRawDefaultAudioDevice(context); - - if (device == null || backendDisconnected) - { - return false; - } - - stream = device.CreateOutStream(); - - if (stream == null || backendDisconnected) - { - return false; - } - - return true; - } - catch - { - return false; - } - finally - { - if (stream != null) - { - stream.Dispose(); - } - - if (context != null) - { - context.Dispose(); - } - } - } - } -}
\ No newline at end of file diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs deleted file mode 100644 index 52c4ebc9..00000000 --- a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs +++ /dev/null @@ -1,646 +0,0 @@ -using SoundIOSharp; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Ryujinx.Audio.SoundIo -{ - internal class SoundIoAudioTrack : IDisposable - { - /// <summary> - /// The audio track ring buffer - /// </summary> - private SoundIoRingBuffer m_Buffer; - - /// <summary> - /// A list of buffers currently pending writeback to the audio backend - /// </summary> - private ConcurrentQueue<SoundIoBuffer> m_ReservedBuffers; - - /// <summary> - /// Occurs when a buffer has been released by the audio backend - /// </summary> - private event ReleaseCallback BufferReleased; - - /// <summary> - /// The track ID of this <see cref="SoundIoAudioTrack"/> - /// </summary> - public int TrackID { get; private set; } - - /// <summary> - /// The current playback state - /// </summary> - public PlaybackState State { get; private set; } - - /// <summary> - /// The <see cref="SoundIO"/> audio context this track belongs to - /// </summary> - public SoundIO AudioContext { get; private set; } - - /// <summary> - /// The <see cref="SoundIODevice"/> this track belongs to - /// </summary> - public SoundIODevice AudioDevice { get; private set; } - - /// <summary> - /// The audio output stream of this track - /// </summary> - public SoundIOOutStream AudioStream { get; private set; } - - /// <summary> - /// Released buffers the track is no longer holding - /// </summary> - public ConcurrentQueue<long> ReleasedBuffers { get; private set; } - - /// <summary> - /// Buffer count of the track - /// </summary> - public uint BufferCount => (uint)m_ReservedBuffers.Count; - - /// <summary> - /// Played sample count of the track - /// </summary> - public ulong PlayedSampleCount { get; private set; } - - private int _hardwareChannels; - private int _virtualChannels; - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoAudioTrack"/> - /// </summary> - /// <param name="trackId">The track ID</param> - /// <param name="audioContext">The SoundIO audio context</param> - /// <param name="audioDevice">The SoundIO audio device</param> - public SoundIoAudioTrack(int trackId, SoundIO audioContext, SoundIODevice audioDevice) - { - TrackID = trackId; - AudioContext = audioContext; - AudioDevice = audioDevice; - State = PlaybackState.Stopped; - ReleasedBuffers = new ConcurrentQueue<long>(); - - m_Buffer = new SoundIoRingBuffer(); - m_ReservedBuffers = new ConcurrentQueue<SoundIoBuffer>(); - } - - /// <summary> - /// Opens the audio track with the specified parameters - /// </summary> - /// <param name="sampleRate">The requested sample rate of the track</param> - /// <param name="hardwareChannels">The requested hardware channels</param> - /// <param name="virtualChannels">The requested virtual channels</param> - /// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param> - /// <param name="format">The requested sample format of the track</param> - public void Open( - int sampleRate, - int hardwareChannels, - int virtualChannels, - ReleaseCallback callback, - SoundIOFormat format = SoundIOFormat.S16LE) - { - // Close any existing audio streams - if (AudioStream != null) - { - Close(); - } - - if (!AudioDevice.SupportsSampleRate(sampleRate)) - { - throw new InvalidOperationException($"This sound device does not support a sample rate of {sampleRate}Hz"); - } - - if (!AudioDevice.SupportsFormat(format)) - { - throw new InvalidOperationException($"This sound device does not support SoundIOFormat.{Enum.GetName(typeof(SoundIOFormat), format)}"); - } - - if (!AudioDevice.SupportsChannelCount(hardwareChannels)) - { - throw new InvalidOperationException($"This sound device does not support channel count {hardwareChannels}"); - } - - _hardwareChannels = hardwareChannels; - _virtualChannels = virtualChannels; - - AudioStream = AudioDevice.CreateOutStream(); - - AudioStream.Name = $"SwitchAudioTrack_{TrackID}"; - AudioStream.Layout = SoundIOChannelLayout.GetDefault(hardwareChannels); - AudioStream.Format = format; - AudioStream.SampleRate = sampleRate; - - AudioStream.WriteCallback = WriteCallback; - - BufferReleased += callback; - - AudioStream.Open(); - } - - /// <summary> - /// This callback occurs when the sound device is ready to buffer more frames - /// </summary> - /// <param name="minFrameCount">The minimum amount of frames expected by the audio backend</param> - /// <param name="maxFrameCount">The maximum amount of frames that can be written to the audio backend</param> - private unsafe void WriteCallback(int minFrameCount, int maxFrameCount) - { - int bytesPerFrame = AudioStream.BytesPerFrame; - uint bytesPerSample = (uint)AudioStream.BytesPerSample; - - int bufferedFrames = m_Buffer.Length / bytesPerFrame; - long bufferedSamples = m_Buffer.Length / bytesPerSample; - - int frameCount = Math.Min(bufferedFrames, maxFrameCount); - - if (frameCount == 0) - { - return; - } - - SoundIOChannelAreas areas = AudioStream.BeginWrite(ref frameCount); - int channelCount = areas.ChannelCount; - - byte[] samples = new byte[frameCount * bytesPerFrame]; - - m_Buffer.Read(samples, 0, samples.Length); - - // This is a huge ugly block of code, but we save - // a significant amount of time over the generic - // loop that handles other channel counts. - - // Mono - if (channelCount == 1) - { - SoundIOChannelArea area = areas.GetArea(0); - - fixed (byte* srcptr = samples) - { - if (bytesPerSample == 1) - { - for (int frame = 0; frame < frameCount; frame++) - { - ((byte*)area.Pointer)[0] = srcptr[frame * bytesPerFrame]; - - area.Pointer += area.Step; - } - } - else if (bytesPerSample == 2) - { - for (int frame = 0; frame < frameCount; frame++) - { - ((short*)area.Pointer)[0] = ((short*)srcptr)[frame * bytesPerFrame >> 1]; - - area.Pointer += area.Step; - } - } - else if (bytesPerSample == 4) - { - for (int frame = 0; frame < frameCount; frame++) - { - ((int*)area.Pointer)[0] = ((int*)srcptr)[frame * bytesPerFrame >> 2]; - - area.Pointer += area.Step; - } - } - else - { - for (int frame = 0; frame < frameCount; frame++) - { - Unsafe.CopyBlockUnaligned((byte*)area.Pointer, srcptr + (frame * bytesPerFrame), bytesPerSample); - - area.Pointer += area.Step; - } - } - } - } - // Stereo - else if (channelCount == 2) - { - SoundIOChannelArea area1 = areas.GetArea(0); - SoundIOChannelArea area2 = areas.GetArea(1); - - fixed (byte* srcptr = samples) - { - if (bytesPerSample == 1) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; - - // Channel 2 - ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - } - } - else if (bytesPerSample == 2) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; - - // Channel 2 - ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - } - } - else if (bytesPerSample == 4) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; - - // Channel 2 - ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - } - } - else - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); - - // Channel 2 - Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - } - } - } - } - // Surround - else if (channelCount == 6) - { - SoundIOChannelArea area1 = areas.GetArea(0); - SoundIOChannelArea area2 = areas.GetArea(1); - SoundIOChannelArea area3 = areas.GetArea(2); - SoundIOChannelArea area4 = areas.GetArea(3); - SoundIOChannelArea area5 = areas.GetArea(4); - SoundIOChannelArea area6 = areas.GetArea(5); - - fixed (byte* srcptr = samples) - { - if (bytesPerSample == 1) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; - - // Channel 2 - ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; - - // Channel 3 - ((byte*)area3.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 2]; - - // Channel 4 - ((byte*)area4.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 3]; - - // Channel 5 - ((byte*)area5.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 4]; - - // Channel 6 - ((byte*)area6.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 5]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - area3.Pointer += area3.Step; - area4.Pointer += area4.Step; - area5.Pointer += area5.Step; - area6.Pointer += area6.Step; - } - } - else if (bytesPerSample == 2) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; - - // Channel 2 - ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; - - // Channel 3 - ((short*)area3.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 2]; - - // Channel 4 - ((short*)area4.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 3]; - - // Channel 5 - ((short*)area5.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 4]; - - // Channel 6 - ((short*)area6.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 5]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - area3.Pointer += area3.Step; - area4.Pointer += area4.Step; - area5.Pointer += area5.Step; - area6.Pointer += area6.Step; - } - } - else if (bytesPerSample == 4) - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; - - // Channel 2 - ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; - - // Channel 3 - ((int*)area3.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 2]; - - // Channel 4 - ((int*)area4.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 3]; - - // Channel 5 - ((int*)area5.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 4]; - - // Channel 6 - ((int*)area6.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 5]; - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - area3.Pointer += area3.Step; - area4.Pointer += area4.Step; - area5.Pointer += area5.Step; - area6.Pointer += area6.Step; - } - } - else - { - for (int frame = 0; frame < frameCount; frame++) - { - // Channel 1 - Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); - - // Channel 2 - Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); - - // Channel 3 - Unsafe.CopyBlockUnaligned((byte*)area3.Pointer, srcptr + (frame * bytesPerFrame) + (2 * bytesPerSample), bytesPerSample); - - // Channel 4 - Unsafe.CopyBlockUnaligned((byte*)area4.Pointer, srcptr + (frame * bytesPerFrame) + (3 * bytesPerSample), bytesPerSample); - - // Channel 5 - Unsafe.CopyBlockUnaligned((byte*)area5.Pointer, srcptr + (frame * bytesPerFrame) + (4 * bytesPerSample), bytesPerSample); - - // Channel 6 - Unsafe.CopyBlockUnaligned((byte*)area6.Pointer, srcptr + (frame * bytesPerFrame) + (5 * bytesPerSample), bytesPerSample); - - area1.Pointer += area1.Step; - area2.Pointer += area2.Step; - area3.Pointer += area3.Step; - area4.Pointer += area4.Step; - area5.Pointer += area5.Step; - area6.Pointer += area6.Step; - } - } - } - } - // Every other channel count - else - { - SoundIOChannelArea[] channels = new SoundIOChannelArea[channelCount]; - - // Obtain the channel area for each channel - for (int i = 0; i < channelCount; i++) - { - channels[i] = areas.GetArea(i); - } - - fixed (byte* srcptr = samples) - { - for (int frame = 0; frame < frameCount; frame++) - for (int channel = 0; channel < areas.ChannelCount; channel++) - { - // Copy channel by channel, frame by frame. This is slow! - Unsafe.CopyBlockUnaligned((byte*)channels[channel].Pointer, srcptr + (frame * bytesPerFrame) + (channel * bytesPerSample), bytesPerSample); - - channels[channel].Pointer += channels[channel].Step; - } - } - } - - AudioStream.EndWrite(); - - PlayedSampleCount += (ulong)samples.Length; - - UpdateReleasedBuffers(samples.Length); - } - - /// <summary> - /// Releases any buffers that have been fully written to the output device - /// </summary> - /// <param name="bytesRead">The amount of bytes written in the last device write</param> - private void UpdateReleasedBuffers(int bytesRead) - { - bool bufferReleased = false; - - while (bytesRead > 0) - { - if (m_ReservedBuffers.TryPeek(out SoundIoBuffer buffer)) - { - if (buffer.Length > bytesRead) - { - buffer.Length -= bytesRead; - bytesRead = 0; - } - else - { - bufferReleased = true; - bytesRead -= buffer.Length; - - m_ReservedBuffers.TryDequeue(out buffer); - ReleasedBuffers.Enqueue(buffer.Tag); - } - } - } - - if (bufferReleased) - { - OnBufferReleased(); - } - } - - /// <summary> - /// Starts audio playback - /// </summary> - public void Start() - { - if (AudioStream == null) - { - return; - } - - AudioStream.Start(); - AudioStream.Pause(false); - AudioContext.FlushEvents(); - State = PlaybackState.Playing; - } - - /// <summary> - /// Stops audio playback - /// </summary> - public void Stop() - { - if (AudioStream == null) - { - return; - } - - AudioStream.Pause(true); - AudioContext.FlushEvents(); - State = PlaybackState.Stopped; - } - - /// <summary> - /// Appends an audio buffer to the tracks internal ring buffer - /// </summary> - /// <typeparam name="T">The audio sample type</typeparam> - /// <param name="bufferTag">The unqiue tag of the buffer being appended</param> - /// <param name="buffer">The buffer to append</param> - public void AppendBuffer<T>(long bufferTag, T[] buffer) where T: struct - { - if (AudioStream == null) - { - return; - } - - int sampleSize = Unsafe.SizeOf<T>(); - int targetSize = sampleSize * buffer.Length; - - // Do we need to downmix? - if (_hardwareChannels != _virtualChannels) - { - if (sampleSize != sizeof(short)) - { - throw new NotImplementedException("Downmixing formats other than PCM16 is not supported!"); - } - - short[] downmixedBuffer; - - ReadOnlySpan<short> bufferPCM16 = MemoryMarshal.Cast<T, short>(buffer); - - if (_virtualChannels == 6) - { - downmixedBuffer = Downmixing.DownMixSurroundToStereo(bufferPCM16); - - if (_hardwareChannels == 1) - { - downmixedBuffer = Downmixing.DownMixStereoToMono(downmixedBuffer); - } - } - else if (_virtualChannels == 2) - { - downmixedBuffer = Downmixing.DownMixStereoToMono(bufferPCM16); - } - else - { - throw new NotImplementedException($"Downmixing from {_virtualChannels} to {_hardwareChannels} not implemented!"); - } - - targetSize = sampleSize * downmixedBuffer.Length; - - // Copy the memory to our ring buffer - m_Buffer.Write(downmixedBuffer, 0, targetSize); - - // Keep track of "buffered" buffers - m_ReservedBuffers.Enqueue(new SoundIoBuffer(bufferTag, targetSize)); - } - else - { - // Copy the memory to our ring buffer - m_Buffer.Write(buffer, 0, targetSize); - - // Keep track of "buffered" buffers - m_ReservedBuffers.Enqueue(new SoundIoBuffer(bufferTag, targetSize)); - } - } - - /// <summary> - /// Returns a value indicating whether the specified buffer is currently reserved by the track - /// </summary> - /// <param name="bufferTag">The buffer tag to check</param> - public bool ContainsBuffer(long bufferTag) - { - return m_ReservedBuffers.Any(x => x.Tag == bufferTag); - } - - /// <summary> - /// Flush all track buffers - /// </summary> - public bool FlushBuffers() - { - m_Buffer.Clear(); - - if (m_ReservedBuffers.Count > 0) - { - foreach (var buffer in m_ReservedBuffers) - { - ReleasedBuffers.Enqueue(buffer.Tag); - } - - OnBufferReleased(); - - return true; - } - - return false; - } - - /// <summary> - /// Closes the <see cref="SoundIoAudioTrack"/> - /// </summary> - public void Close() - { - if (AudioStream != null) - { - AudioStream.Pause(true); - AudioStream.Dispose(); - } - - m_Buffer.Clear(); - OnBufferReleased(); - ReleasedBuffers.Clear(); - - State = PlaybackState.Stopped; - AudioStream = null; - BufferReleased = null; - } - - private void OnBufferReleased() - { - BufferReleased?.Invoke(); - } - - /// <summary> - /// Releases the unmanaged resources used by the <see cref="SoundIoAudioTrack" /> - /// </summary> - public void Dispose() - { - Close(); - } - - ~SoundIoAudioTrack() - { - Dispose(); - } - } -} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs deleted file mode 100644 index 95f181dc..00000000 --- a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs +++ /dev/null @@ -1,193 +0,0 @@ -using SoundIOSharp; -using System; -using System.Collections.Concurrent; -using System.Linq; - -namespace Ryujinx.Audio.SoundIo -{ - /// <summary> - /// An object pool containing a set of audio tracks - /// </summary> - internal class SoundIoAudioTrackPool : IDisposable - { - /// <summary> - /// The current size of the <see cref="SoundIoAudioTrackPool"/> - /// </summary> - private int m_Size; - - /// <summary> - /// The maximum size of the <see cref="SoundIoAudioTrackPool"/> - /// </summary> - private int m_MaxSize; - - /// <summary> - /// The <see cref="SoundIO"/> audio context this track pool belongs to - /// </summary> - private SoundIO m_Context; - - /// <summary> - /// The <see cref="SoundIODevice"/> audio device this track pool belongs to - /// </summary> - private SoundIODevice m_Device; - - /// <summary> - /// The queue that keeps track of the available <see cref="SoundIoAudioTrack"/> in the pool. - /// </summary> - private ConcurrentQueue<SoundIoAudioTrack> m_Queue; - - /// <summary> - /// The dictionary providing mapping between a TrackID and <see cref="SoundIoAudioTrack"/> - /// </summary> - private ConcurrentDictionary<int, SoundIoAudioTrack> m_TrackList; - - /// <summary> - /// Gets the current size of the <see cref="SoundIoAudioTrackPool"/> - /// </summary> - public int Size { get => m_Size; } - - /// <summary> - /// Gets the maximum size of the <see cref="SoundIoAudioTrackPool"/> - /// </summary> - public int MaxSize { get => m_MaxSize; } - - /// <summary> - /// Gets a value that indicates whether the <see cref="SoundIoAudioTrackPool"/> is empty - /// </summary> - public bool IsEmpty { get => m_Queue.IsEmpty; } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoAudioTrackPool"/> that is empty - /// </summary> - /// <param name="maxSize">The maximum amount of tracks that can be created</param> - public SoundIoAudioTrackPool(SoundIO context, SoundIODevice device, int maxSize) - { - m_Size = 0; - m_Context = context; - m_Device = device; - m_MaxSize = maxSize; - - m_Queue = new ConcurrentQueue<SoundIoAudioTrack>(); - m_TrackList = new ConcurrentDictionary<int, SoundIoAudioTrack>(); - } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoAudioTrackPool"/> that contains - /// the specified amount of <see cref="SoundIoAudioTrack"/> - /// </summary> - /// <param name="maxSize">The maximum amount of tracks that can be created</param> - /// <param name="initialCapacity">The initial number of tracks that the pool contains</param> - public SoundIoAudioTrackPool(SoundIO context, SoundIODevice device, int maxSize, int initialCapacity) - : this(context, device, maxSize) - { - var trackCollection = Enumerable.Range(0, initialCapacity) - .Select(TrackFactory); - - m_Size = initialCapacity; - m_Queue = new ConcurrentQueue<SoundIoAudioTrack>(trackCollection); - } - - /// <summary> - /// Creates a new <see cref="SoundIoAudioTrack"/> with the proper AudioContext and AudioDevice - /// and the specified <paramref name="trackId" /> - /// </summary> - /// <param name="trackId">The ID of the track to be created</param> - /// <returns>A new AudioTrack with the specified ID</returns> - private SoundIoAudioTrack TrackFactory(int trackId) - { - // Create a new AudioTrack - SoundIoAudioTrack track = new SoundIoAudioTrack(trackId, m_Context, m_Device); - - // Keep track of issued tracks - m_TrackList[trackId] = track; - - return track; - } - - /// <summary> - /// Retrieves a <see cref="SoundIoAudioTrack"/> from the pool - /// </summary> - /// <returns>An AudioTrack from the pool</returns> - public SoundIoAudioTrack Get() - { - // If we have a track available, reuse it - if (m_Queue.TryDequeue(out SoundIoAudioTrack track)) - { - return track; - } - - // Have we reached the maximum size of our pool? - if (m_Size >= m_MaxSize) - { - return null; - } - - // We don't have any pooled tracks, so create a new one - return TrackFactory(m_Size++); - } - - /// <summary> - /// Retrieves the <see cref="SoundIoAudioTrack"/> associated with the specified <paramref name="trackId"/> from the pool - /// </summary> - /// <param name="trackId">The ID of the track to retrieve</param> - public SoundIoAudioTrack Get(int trackId) - { - if (m_TrackList.TryGetValue(trackId, out SoundIoAudioTrack track)) - { - return track; - } - - return null; - } - - /// <summary> - /// Attempts to get a <see cref="SoundIoAudioTrack"/> from the pool - /// </summary> - /// <param name="track">The track retrieved from the pool</param> - /// <returns>True if retrieve was successful</returns> - public bool TryGet(out SoundIoAudioTrack track) - { - track = Get(); - - return track != null; - } - - /// <summary> - /// Attempts to get the <see cref="SoundIoAudioTrack" /> associated with the specified <paramref name="trackId"/> from the pool - /// </summary> - /// <param name="trackId">The ID of the track to retrieve</param> - /// <param name="track">The track retrieved from the pool</param> - public bool TryGet(int trackId, out SoundIoAudioTrack track) - { - return m_TrackList.TryGetValue(trackId, out track); - } - - /// <summary> - /// Returns an <see cref="SoundIoAudioTrack"/> back to the pool for reuse - /// </summary> - /// <param name="track">The track to be returned to the pool</param> - public void Put(SoundIoAudioTrack track) - { - // Ensure the track is disposed and not playing audio - track.Close(); - - // Requeue the track for reuse later - m_Queue.Enqueue(track); - } - - /// <summary> - /// Releases the unmanaged resources used by the <see cref="SoundIoAudioTrackPool" /> - /// </summary> - public void Dispose() - { - foreach (var track in m_TrackList) - { - track.Value.Close(); - track.Value.Dispose(); - } - - m_Size = 0; - m_Queue.Clear(); - m_TrackList.Clear(); - } - } -} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs deleted file mode 100644 index 2a6190b5..00000000 --- a/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Ryujinx.Audio.SoundIo -{ - /// <summary> - /// Represents the remaining bytes left buffered for a specific buffer tag - /// </summary> - internal class SoundIoBuffer - { - /// <summary> - /// The buffer tag this <see cref="SoundIoBuffer"/> represents - /// </summary> - public long Tag { get; private set; } - - /// <summary> - /// The remaining bytes still to be released - /// </summary> - public int Length { get; set; } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoBuffer"/> - /// </summary> - /// <param name="tag">The buffer tag</param> - /// <param name="length">The size of the buffer</param> - public SoundIoBuffer(long tag, int length) - { - Tag = tag; - Length = length; - } - } -} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs deleted file mode 100644 index b2885021..00000000 --- a/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; - -namespace Ryujinx.Audio.SoundIo -{ - /// <summary> - /// A thread-safe variable-size circular buffer - /// </summary> - internal class SoundIoRingBuffer - { - private byte[] m_Buffer; - private int m_Size; - private int m_HeadOffset; - private int m_TailOffset; - - /// <summary> - /// Gets the available bytes in the ring buffer - /// </summary> - public int Length - { - get { return m_Size; } - } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoRingBuffer"/> - /// </summary> - public SoundIoRingBuffer() - { - m_Buffer = new byte[2048]; - } - - /// <summary> - /// Constructs a new instance of a <see cref="SoundIoRingBuffer"/> with the specified capacity - /// </summary> - /// <param name="capacity">The number of entries that the <see cref="SoundIoRingBuffer"/> can initially contain</param> - public SoundIoRingBuffer(int capacity) - { - m_Buffer = new byte[capacity]; - } - - /// <summary> - /// Clears the ring buffer - /// </summary> - public void Clear() - { - m_Size = 0; - m_HeadOffset = 0; - m_TailOffset = 0; - } - - /// <summary> - /// Clears the specified amount of bytes from the ring buffer - /// </summary> - /// <param name="size">The amount of bytes to clear from the ring buffer</param> - public void Clear(int size) - { - lock (this) - { - if (size > m_Size) - { - size = m_Size; - } - - if (size == 0) - { - return; - } - - m_HeadOffset = (m_HeadOffset + size) % m_Buffer.Length; - m_Size -= size; - - if (m_Size == 0) - { - m_HeadOffset = 0; - m_TailOffset = 0; - } - - return; - } - } - - /// <summary> - /// Extends the capacity of the ring buffer - /// </summary> - private void SetCapacity(int capacity) - { - byte[] buffer = new byte[capacity]; - - if (m_Size > 0) - { - if (m_HeadOffset < m_TailOffset) - { - Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, 0, m_Size); - } - else - { - Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, 0, m_Buffer.Length - m_HeadOffset); - Buffer.BlockCopy(m_Buffer, 0, buffer, m_Buffer.Length - m_HeadOffset, m_TailOffset); - } - } - - m_Buffer = buffer; - m_HeadOffset = 0; - m_TailOffset = m_Size; - } - - - /// <summary> - /// Writes a sequence of bytes to the ring buffer - /// </summary> - /// <param name="buffer">A byte array containing the data to write</param> - /// <param name="index">The zero-based byte offset in <paramref name="buffer" /> from which to begin copying bytes to the ring buffer</param> - /// <param name="count">The number of bytes to write</param> - public void Write<T>(T[] buffer, int index, int count) - { - if (count == 0) - { - return; - } - - lock (this) - { - if ((m_Size + count) > m_Buffer.Length) - { - SetCapacity((m_Size + count + 2047) & ~2047); - } - - if (m_HeadOffset < m_TailOffset) - { - int tailLength = m_Buffer.Length - m_TailOffset; - - if (tailLength >= count) - { - Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, count); - } - else - { - Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, tailLength); - Buffer.BlockCopy(buffer, index + tailLength, m_Buffer, 0, count - tailLength); - } - } - else - { - Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, count); - } - - m_Size += count; - m_TailOffset = (m_TailOffset + count) % m_Buffer.Length; - } - } - - /// <summary> - /// Reads a sequence of bytes from the ring buffer and advances the position within the ring buffer by the number of bytes read - /// </summary> - /// <param name="buffer">The buffer to write the data into</param> - /// <param name="index">The zero-based byte offset in <paramref name="buffer" /> at which the read bytes will be placed</param> - /// <param name="count">The maximum number of bytes to read</param> - /// <returns>The total number of bytes read into the buffer. This might be less than the number of bytes requested if that number of bytes are not currently available, or zero if the ring buffer is empty</returns> - public int Read<T>(T[] buffer, int index, int count) - { - lock (this) - { - if (count > m_Size) - { - count = m_Size; - } - - if (count == 0) - { - return 0; - } - - if (m_HeadOffset < m_TailOffset) - { - Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, count); - } - else - { - int tailLength = m_Buffer.Length - m_HeadOffset; - - if (tailLength >= count) - { - Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, count); - } - else - { - Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, tailLength); - Buffer.BlockCopy(m_Buffer, 0, buffer, index + tailLength, count - tailLength); - } - } - - m_Size -= count; - m_HeadOffset = (m_HeadOffset + count) % m_Buffer.Length; - - if (m_Size == 0) - { - m_HeadOffset = 0; - m_TailOffset = 0; - } - - return count; - } - } - } -} diff --git a/Ryujinx.Audio/ResultCode.cs b/Ryujinx.Audio/ResultCode.cs new file mode 100644 index 00000000..6708cdc1 --- /dev/null +++ b/Ryujinx.Audio/ResultCode.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019-2021 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 <https://www.gnu.org/licenses/>. +// + +namespace Ryujinx.Audio +{ + public enum ResultCode + { + ModuleId = 153, + ErrorCodeShift = 9, + + Success = 0, + + DeviceNotFound = (1 << ErrorCodeShift) | ModuleId, + OperationFailed = (2 << ErrorCodeShift) | ModuleId, + UnsupportedSampleRate = (3 << ErrorCodeShift) | ModuleId, + WorkBufferTooSmall = (4 << ErrorCodeShift) | ModuleId, + BufferRingFull = (8 << ErrorCodeShift) | ModuleId, + UnsupportedChannelConfiguration = (10 << ErrorCodeShift) | ModuleId, + InvalidUpdateInfo = (41 << ErrorCodeShift) | ModuleId, + InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId, + InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId, + UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId, + } +} diff --git a/Ryujinx.Audio/Ryujinx.Audio.csproj b/Ryujinx.Audio/Ryujinx.Audio.csproj index 6e0b668a..ccdeae3e 100644 --- a/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -6,26 +6,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" /> - </ItemGroup> - - <ItemGroup> <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> - </ItemGroup> - - <ItemGroup> - <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dll"> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - <TargetPath>libsoundio.dll</TargetPath> - </ContentWithTargetPath> - <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib"> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - <TargetPath>libsoundio.dylib</TargetPath> - </ContentWithTargetPath> - <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so"> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - <TargetPath>libsoundio.so</TargetPath> - </ContentWithTargetPath> + <ProjectReference Include="..\Ryujinx.Cpu\Ryujinx.Cpu.csproj" /> + <ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" /> </ItemGroup> </Project> |