forked from MeloNX/MeloNX
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:
parent
7ac6b8e742
commit
60442064e4
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user