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.
This commit is contained in:
riperiperi 2024-01-06 20:06:55 +00:00
parent 7ac6b8e742
commit 60442064e4
9 changed files with 274 additions and 9 deletions

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -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,
};

View File

@ -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();
}
}
}

View File

@ -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<AudioBuffer> _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);
}
}
}

View File

@ -1,4 +1,5 @@
using Ryujinx.Audio.Integration;
using System.Threading;
namespace Ryujinx.Audio.Common
{
@ -7,12 +8,19 @@ namespace Ryujinx.Audio.Common
/// </summary>
public class AudioBuffer
{
private static ulong UniqueIdGlobal = 0;
/// <summary>
/// Unique tag of this buffer.
/// Unique tag of this buffer, from the guest.
/// </summary>
/// <remarks>Unique per session</remarks>
public ulong BufferTag;
/// <summary>
/// Globally unique ID of the buffer on the host.
/// </summary>
public ulong HostTag = Interlocked.Increment(ref UniqueIdGlobal);
/// <summary>
/// Pointer to the user samples.
/// </summary>

View File

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