From 60442064e42f93d85a6b5181eaad406bd7cc692f Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sat, 6 Jan 2024 20:06:55 +0000 Subject: [PATCH] Audio: Add optional artificial audio delay layer This driver runs ahead by the given duration (measured in samples at 48000hz). For games that rely on buffer release feedback to know when to provide audio frames, this can trick them into running with a higher audio latency on platforms that require it to avoid stuttering. See `Switch.cs` for the sampleDelay definition. It's currently 0, though in future it could be configurable or switch on automatically for certain configurations. For audio sources that _do not_ care about sample release timing, such as the current AudioRenderer, this will not change their behaviour. --- .../OpenALHardwareDeviceSession.cs | 4 +- .../SDL2HardwareDeviceSession.cs | 4 +- .../SoundIoHardwareDeviceSession.cs | 4 +- .../Common/HardwareDeviceSessionOutputBase.cs | 2 +- .../CompatLayerHardwareDeviceSession.cs | 6 + .../DelayLayerHardwareDeviceDriver.cs | 86 ++++++++++ .../DelayLayerHardwareDeviceSession.cs | 151 ++++++++++++++++++ src/Ryujinx.Audio/Common/AudioBuffer.cs | 10 +- src/Ryujinx.HLE/Switch.cs | 16 +- 9 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs create mode 100644 src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs diff --git a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs index a52821616..73e914083 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Audio.Backends.OpenAL { OpenALAudioBuffer driverBuffer = new() { - DriverIdentifier = buffer.DataPointer, + DriverIdentifier = buffer.HostTag, BufferId = AL.GenBuffer(), SampleCount = GetSampleCount(buffer), }; @@ -131,7 +131,7 @@ namespace Ryujinx.Audio.Backends.OpenAL return true; } - return driverBuffer.DriverIdentifier != buffer.DataPointer; + return driverBuffer.DriverIdentifier != buffer.HostTag; } } diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs index 7a683f4ed..cf3be473e 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs @@ -151,7 +151,7 @@ namespace Ryujinx.Audio.Backends.SDL2 if (_outputStream != 0) { - SDL2AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer)); + SDL2AudioBuffer driverBuffer = new(buffer.HostTag, GetSampleCount(buffer)); _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); @@ -205,7 +205,7 @@ namespace Ryujinx.Audio.Backends.SDL2 return true; } - return driverBuffer.DriverIdentifier != buffer.DataPointer; + return driverBuffer.DriverIdentifier != buffer.HostTag; } protected virtual void Dispose(bool disposing) diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs index 123cfd27a..b9070dc48 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs @@ -54,7 +54,7 @@ namespace Ryujinx.Audio.Backends.SoundIo public override void QueueBuffer(AudioBuffer buffer) { - SoundIoAudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer)); + SoundIoAudioBuffer driverBuffer = new(buffer.HostTag, GetSampleCount(buffer)); _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); @@ -90,7 +90,7 @@ namespace Ryujinx.Audio.Backends.SoundIo return true; } - return driverBuffer.DriverIdentifier != buffer.DataPointer; + return driverBuffer.DriverIdentifier != buffer.HostTag; } private unsafe void Update(int minFrameCount, int maxFrameCount) diff --git a/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs b/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs index 5599c0827..f193d9861 100644 --- a/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs +++ b/src/Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs @@ -40,7 +40,7 @@ namespace Ryujinx.Audio.Backends.Common } [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected ulong GetSampleCount(int dataSize) + public virtual ulong GetSampleCount(int dataSize) { return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, dataSize); } diff --git a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs index a9acabec9..0cfbefd1e 100644 --- a/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs +++ b/src/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs @@ -39,6 +39,11 @@ namespace Ryujinx.Audio.Backends.CompatLayer _realSession.PrepareToClose(); } + public override ulong GetSampleCount(int dataSize) + { + return _realSession.GetSampleCount(dataSize); + } + public override void QueueBuffer(AudioBuffer buffer) { SampleFormat realSampleFormat = _realSession.RequestedSampleFormat; @@ -119,6 +124,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer AudioBuffer fakeBuffer = new() { BufferTag = buffer.BufferTag, + HostTag = buffer.HostTag, DataPointer = buffer.DataPointer, DataSize = (ulong)samples.Length, }; diff --git a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs new file mode 100644 index 000000000..cdd5eb8a8 --- /dev/null +++ b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceDriver.cs @@ -0,0 +1,86 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using System; +using System.Threading; +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.DelayLayer +{ + public class DelayLayerHardwareDeviceDriver : IHardwareDeviceDriver + { + private readonly IHardwareDeviceDriver _realDriver; + + public static bool IsSupported => true; + + public ulong SampleDelay48k; + + public DelayLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice, ulong sampleDelay48k) + { + _realDriver = realDevice; + SampleDelay48k = sampleDelay48k; + } + + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume) + { + IHardwareDeviceSession session = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, channelCount, volume); + + if (direction == Direction.Output) + { + return new DelayLayerHardwareDeviceSession(this, session as HardwareDeviceSessionOutputBase, sampleFormat, channelCount); + } + + return session; + } + + public ManualResetEvent GetUpdateRequiredEvent() + { + return _realDriver.GetUpdateRequiredEvent(); + } + + public ManualResetEvent GetPauseEvent() + { + return _realDriver.GetPauseEvent(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _realDriver.Dispose(); + } + } + + public bool SupportsSampleRate(uint sampleRate) + { + return _realDriver.SupportsSampleRate(sampleRate); + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + return _realDriver.SupportsSampleFormat(sampleFormat); + } + + public bool SupportsDirection(Direction direction) + { + return _realDriver.SupportsDirection(direction); + } + + public bool SupportsChannelCount(uint channelCount) + { + return _realDriver.SupportsChannelCount(channelCount); + } + + public IHardwareDeviceDriver GetRealDeviceDriver() + { + return _realDriver.GetRealDeviceDriver(); + } + } +} diff --git a/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs new file mode 100644 index 000000000..996a2a369 --- /dev/null +++ b/src/Ryujinx.Audio/Backends/DelayLayer/DelayLayerHardwareDeviceSession.cs @@ -0,0 +1,151 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Common; +using System.Collections.Generic; +using System.Threading; + +namespace Ryujinx.Audio.Backends.DelayLayer +{ + internal class DelayLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private readonly HardwareDeviceSessionOutputBase _realSession; + private readonly ManualResetEvent _updateRequiredEvent; + + private readonly ulong _delayTarget; + + private object _sampleCountLock = new(); + + private List _buffers = new(); + + public DelayLayerHardwareDeviceSession(DelayLayerHardwareDeviceDriver driver, HardwareDeviceSessionOutputBase realSession, SampleFormat userSampleFormat, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount) + { + _realSession = realSession; + _delayTarget = driver.SampleDelay48k; + + _updateRequiredEvent = driver.GetUpdateRequiredEvent(); + } + + public override void Dispose() + { + _realSession.Dispose(); + } + + public override ulong GetPlayedSampleCount() + { + lock (_sampleCountLock) + { + // Update the played samples count. + WasBufferFullyConsumed(null); + + return _playedSamplesCount; + } + } + + public override float GetVolume() + { + return _realSession.GetVolume(); + } + + public override void PrepareToClose() + { + _realSession.PrepareToClose(); + } + + public override void QueueBuffer(AudioBuffer buffer) + { + _realSession.QueueBuffer(buffer); + + ulong samples = GetSampleCount(buffer); + + lock (_sampleCountLock) + { + _buffers.Add(buffer); + } + + _updateRequiredEvent.Set(); + } + + public override ulong GetSampleCount(int dataSize) + { + return _realSession.GetSampleCount(dataSize); + } + + public override void SetVolume(float volume) + { + _realSession.SetVolume(volume); + } + + public override void Start() + { + _realSession.Start(); + } + + public override void Stop() + { + _realSession.Stop(); + } + + private ulong _playedSamplesCount = 0; + private int _frontIndex = -1; + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + ulong delaySamples = 0; + bool isConsumed = true; + // True if it's in the _delayedSamples range. + lock (_sampleCountLock) + { + for (int i = 0; i < _buffers.Count; i++) + { + AudioBuffer elem = _buffers[i]; + isConsumed = isConsumed && _realSession.WasBufferFullyConsumed(elem); + ulong samples = GetSampleCount(elem); + + bool afterFront = i > _frontIndex; + + if (isConsumed) + { + if (_frontIndex > -1) + { + _frontIndex--; + } + + _buffers.RemoveAt(i--); + + if (afterFront) + { + _playedSamplesCount += samples; + } + + if (buffer == elem) + { + return true; + } + } + else + { + if (afterFront && delaySamples < _delayTarget) + { + _playedSamplesCount += samples; + _frontIndex = i; + } + + if (buffer == elem) + { + return i <= _frontIndex; + } + + delaySamples += samples; + } + } + + // Buffer was not queued. + return true; + } + } + + public override bool RegisterBuffer(AudioBuffer buffer, byte[] samples) + { + return _realSession.RegisterBuffer(buffer, samples); + } + } +} diff --git a/src/Ryujinx.Audio/Common/AudioBuffer.cs b/src/Ryujinx.Audio/Common/AudioBuffer.cs index 87a7d5f32..2c04e9e60 100644 --- a/src/Ryujinx.Audio/Common/AudioBuffer.cs +++ b/src/Ryujinx.Audio/Common/AudioBuffer.cs @@ -1,4 +1,5 @@ using Ryujinx.Audio.Integration; +using System.Threading; namespace Ryujinx.Audio.Common { @@ -7,12 +8,19 @@ namespace Ryujinx.Audio.Common /// public class AudioBuffer { + private static ulong UniqueIdGlobal = 0; + /// - /// Unique tag of this buffer. + /// Unique tag of this buffer, from the guest. /// /// Unique per session public ulong BufferTag; + /// + /// Globally unique ID of the buffer on the host. + /// + public ulong HostTag = Interlocked.Increment(ref UniqueIdGlobal); + /// /// Pointer to the user samples. /// diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index ae063a47d..859eac5f5 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -1,4 +1,5 @@ using Ryujinx.Audio.Backends.CompatLayer; +using Ryujinx.Audio.Backends.DelayLayer; using Ryujinx.Audio.Integration; using Ryujinx.Common.Configuration; using Ryujinx.Graphics.Gpu; @@ -46,7 +47,7 @@ namespace Ryujinx.HLE : MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable; #pragma warning disable IDE0055 // Disable formatting - AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); + AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Gpu = new GpuContext(Configuration.GpuRenderer); System = new HOS.Horizon(this); @@ -67,6 +68,19 @@ namespace Ryujinx.HLE #pragma warning restore IDE0055 } + private IHardwareDeviceDriver AddAudioCompatLayers(IHardwareDeviceDriver driver) + { + ulong sampleDelay = 0; + driver = new CompatLayerHardwareDeviceDriver(driver); + + if (sampleDelay > 0) + { + driver = new DelayLayerHardwareDeviceDriver(driver, sampleDelay); + } + + return driver; + } + public bool LoadCart(string exeFsDir, string romFsFile = null) { return Processes.LoadUnpackedNca(exeFsDir, romFsFile);