diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3AudioBuffer.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3AudioBuffer.cs new file mode 100644 index 000000000..55a4a60e1 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3AudioBuffer.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Audio.Backends.SDL3 +{ + class SDL3AudioBuffer + { + public readonly ulong DriverIdentifier; + public readonly ulong SampleCount; + public ulong SamplePlayed; + + public SDL3AudioBuffer(ulong driverIdentifier, ulong sampleCount) + { + DriverIdentifier = driverIdentifier; + SampleCount = sampleCount; + SamplePlayed = 0; + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs index 831732ce2..eb018aec9 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceDriver.cs @@ -48,7 +48,8 @@ namespace Ryujinx.Audio.Backends.SDL3 private static bool IsSupportedInternal() { - var device = OpenStream(SampleFormat.PcmInt16, Constants.TargetSampleRate, Constants.ChannelCountMax); + nint device = OpenStream(SampleFormat.PcmInt16, Constants.TargetSampleRate, Constants.ChannelCountMax, + Constants.TargetSampleCount, null); if (device != 0) { @@ -99,7 +100,7 @@ namespace Ryujinx.Audio.Backends.SDL3 } private static SDL_AudioSpec GetSDL3Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate, - uint requestedChannelCount) + uint requestedChannelCount, uint sampleCount) { return new SDL_AudioSpec { @@ -122,11 +123,13 @@ namespace Ryujinx.Audio.Backends.SDL3 } internal static nint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, - uint requestedChannelCount) - { - SDL_AudioSpec spec = GetSDL3Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount); + uint requestedChannelCount, uint sampleCount, SDL_AudioStreamCallback callback) + { + SDL_AudioSpec desired = GetSDL3Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount, + sampleCount); - var stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, ref spec,null,IntPtr.Zero); + nint stream = + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, ref desired, callback, nint.Zero); if (stream == 0) { @@ -136,6 +139,15 @@ namespace Ryujinx.Audio.Backends.SDL3 return 0; } + // bool isValid = got.format == desired.format && got.freq == desired.freq && got.channels == desired.channels; + // + // if (!isValid) + // { + // Logger.Error?.Print(LogClass.Application, "SDL3 open audio device is not valid"); + // SDL_DestroyAudioStream(stream); + // + // return 0; + // } return stream; } diff --git a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs index 1151ac473..b2518428d 100644 --- a/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs +++ b/src/Ryujinx.Audio.Backends.SDL3/SDL3HardwareDeviceSession.cs @@ -1,8 +1,11 @@ using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Common; using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; using Ryujinx.Memory; using System; +using System.Buffers; +using System.Collections.Concurrent; using System.Threading; using static SDL3.SDL; @@ -11,10 +14,14 @@ namespace Ryujinx.Audio.Backends.SDL3 class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase { private readonly SDL3HardwareDeviceDriver _driver; + private readonly ConcurrentQueue _queuedBuffers; + private readonly DynamicRingBuffer _ringBuffer; private ulong _playedSampleCount; private readonly ManualResetEvent _updateRequiredEvent; private nint _outputStream; private bool _hasSetupError; + private readonly SDL_AudioStreamCallback _callbackDelegate; + private readonly int _bytesPerFrame; private uint _sampleCount; private bool _started; private float _volume; @@ -26,6 +33,10 @@ namespace Ryujinx.Audio.Backends.SDL3 { _driver = driver; _updateRequiredEvent = _driver.GetUpdateRequiredEvent(); + _queuedBuffers = new ConcurrentQueue(); + _ringBuffer = new DynamicRingBuffer(); + _callbackDelegate = Update; + _bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount; _nativeSampleFormat = SDL3HardwareDeviceDriver.GetSDL3Format(RequestedSampleFormat); _sampleCount = uint.MaxValue; _started = false; @@ -43,8 +54,8 @@ namespace Ryujinx.Audio.Backends.SDL3 { _sampleCount = Math.Max(Constants.TargetSampleCount, bufferSampleCount); - var newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, - RequestedChannelCount); + nint newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, + RequestedChannelCount, _sampleCount, _callbackDelegate); _hasSetupError = newOutputStream == 0; @@ -56,13 +67,87 @@ namespace Ryujinx.Audio.Backends.SDL3 } _outputStream = newOutputStream; + + // SDL_PauseAudioDevice(_outputStream, _started ? 0 : 1); SDL_ResumeAudioStreamDevice(_outputStream); + Logger.Info?.Print(LogClass.Audio, $"New audio stream setup with a target sample count of {_sampleCount}"); } } } + private unsafe void Update(nint userdata, nint stream, int additional_amount, int total_amount) + { + Span streamSpan = new((void*)stream, additional_amount); + + int maxFrameCount = (int)GetSampleCount(additional_amount); + int bufferedFrames = _ringBuffer.Length / _bytesPerFrame; + + int frameCount = Math.Min(bufferedFrames, maxFrameCount); + + if (frameCount == 0) + { + // SDL3 left the responsibility to the user to clear the buffer. + streamSpan.Clear(); + + return; + } + + using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * _bytesPerFrame); + using SpanOwner destinationOwner = SpanOwner.Rent(frameCount * _bytesPerFrame); + + Span samples = samplesOwner.Span; + Span destinationBuffer = destinationOwner.Span; + + _ringBuffer.Read(samples, 0, samples.Length); + + fixed (byte* pSrc = samples) + fixed (byte* pDst = destinationBuffer) + { + nint pStreamSrc = (nint)pSrc; + nint pStreamDst = (nint)pDst; + + // Zero the dest buffer + streamSpan.Clear(); + + // Apply volume to written data + SDL_MixAudio(pStreamDst, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, _driver.Volume); + SDL_PutAudioStreamData(stream, pStreamSrc, samples.Length); + } + + ulong sampleCount = GetSampleCount(samples.Length); + + ulong availaibleSampleCount = sampleCount; + + bool needUpdate = false; + + while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer)) + { + ulong sampleStillNeeded = driverBuffer.SampleCount - Interlocked.Read(ref driverBuffer.SamplePlayed); + ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount); + + ulong currentSamplePlayed = + Interlocked.Add(ref driverBuffer.SamplePlayed, playedAudioBufferSampleCount); + availaibleSampleCount -= playedAudioBufferSampleCount; + + if (currentSamplePlayed == driverBuffer.SampleCount) + { + _queuedBuffers.TryDequeue(out _); + + needUpdate = true; + } + + Interlocked.Add(ref _playedSampleCount, playedAudioBufferSampleCount); + } + + // Notify the output if needed. + if (needUpdate) + { + _updateRequiredEvent.Set(); + } + } + public override ulong GetPlayedSampleCount() { return Interlocked.Read(ref _playedSampleCount); @@ -78,23 +163,14 @@ namespace Ryujinx.Audio.Backends.SDL3 public override void QueueBuffer(AudioBuffer buffer) { EnsureAudioStreamSetup(buffer); + if (_outputStream != 0) { - if (SDL_GetAudioStreamAvailable(_outputStream) < int.MaxValue) - { - unsafe - { - fixed (byte* samplesPtr = buffer.Data) - { - var len = buffer.Data.Length; - IntPtr src = (IntPtr)samplesPtr; - byte* dst = stackalloc byte[len]; - IntPtr dstPtr = (IntPtr)dst; - SDL_MixAudio(dstPtr, src, _nativeSampleFormat, (uint)len, _driver.Volume); - SDL_PutAudioStreamData(_outputStream, dstPtr, len); - } - } - } + SDL3AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer)); + + _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); + + _queuedBuffers.Enqueue(driverBuffer); } else { @@ -139,7 +215,12 @@ namespace Ryujinx.Audio.Backends.SDL3 public override bool WasBufferFullyConsumed(AudioBuffer buffer) { - return true; + if (!_queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer)) + { + return true; + } + + return driverBuffer.DriverIdentifier != buffer.DataPointer; } protected virtual void Dispose(bool disposing) diff --git a/src/Ryujinx.SDL3-CS/SDL3.cs b/src/Ryujinx.SDL3-CS/SDL3.cs index 1ef410ddf..8afa0a94c 100644 --- a/src/Ryujinx.SDL3-CS/SDL3.cs +++ b/src/Ryujinx.SDL3-CS/SDL3.cs @@ -968,7 +968,7 @@ public static unsafe partial class SDL [LibraryImport(nativeLibName)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - public static partial IntPtr SDL_OpenAudioDeviceStream(uint devid, ref SDL_AudioSpec spec, SDL_AudioStreamCallback? callback, IntPtr userdata); + public static partial IntPtr SDL_OpenAudioDeviceStream(uint devid, ref SDL_AudioSpec spec, SDL_AudioStreamCallback callback, IntPtr userdata); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void SDL_AudioPostmixCallback(IntPtr userdata, SDL_AudioSpec* spec, float* buffer, int buflen);