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);