sdl3 audio callback

This commit is contained in:
madwind 2025-01-15 22:38:21 +08:00
parent 37f4c1ea1a
commit dfbcdfa83a
4 changed files with 134 additions and 25 deletions

View File

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

View File

@ -48,7 +48,8 @@ namespace Ryujinx.Audio.Backends.SDL3
private static bool IsSupportedInternal() 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) if (device != 0)
{ {
@ -99,7 +100,7 @@ namespace Ryujinx.Audio.Backends.SDL3
} }
private static SDL_AudioSpec GetSDL3Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate, private static SDL_AudioSpec GetSDL3Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate,
uint requestedChannelCount) uint requestedChannelCount, uint sampleCount)
{ {
return new SDL_AudioSpec return new SDL_AudioSpec
{ {
@ -122,11 +123,13 @@ namespace Ryujinx.Audio.Backends.SDL3
} }
internal static nint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, internal static nint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate,
uint requestedChannelCount) uint requestedChannelCount, uint sampleCount, SDL_AudioStreamCallback callback)
{ {
SDL_AudioSpec spec = GetSDL3Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount); 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) if (stream == 0)
{ {
@ -136,6 +139,15 @@ namespace Ryujinx.Audio.Backends.SDL3
return 0; 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; return stream;
} }

View File

@ -1,8 +1,11 @@
using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common; using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Threading; using System.Threading;
using static SDL3.SDL; using static SDL3.SDL;
@ -11,10 +14,14 @@ namespace Ryujinx.Audio.Backends.SDL3
class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
{ {
private readonly SDL3HardwareDeviceDriver _driver; private readonly SDL3HardwareDeviceDriver _driver;
private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
private readonly DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount; private ulong _playedSampleCount;
private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _updateRequiredEvent;
private nint _outputStream; private nint _outputStream;
private bool _hasSetupError; private bool _hasSetupError;
private readonly SDL_AudioStreamCallback _callbackDelegate;
private readonly int _bytesPerFrame;
private uint _sampleCount; private uint _sampleCount;
private bool _started; private bool _started;
private float _volume; private float _volume;
@ -26,6 +33,10 @@ namespace Ryujinx.Audio.Backends.SDL3
{ {
_driver = driver; _driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent(); _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue<SDL3AudioBuffer>();
_ringBuffer = new DynamicRingBuffer();
_callbackDelegate = Update;
_bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount;
_nativeSampleFormat = SDL3HardwareDeviceDriver.GetSDL3Format(RequestedSampleFormat); _nativeSampleFormat = SDL3HardwareDeviceDriver.GetSDL3Format(RequestedSampleFormat);
_sampleCount = uint.MaxValue; _sampleCount = uint.MaxValue;
_started = false; _started = false;
@ -43,8 +54,8 @@ namespace Ryujinx.Audio.Backends.SDL3
{ {
_sampleCount = Math.Max(Constants.TargetSampleCount, bufferSampleCount); _sampleCount = Math.Max(Constants.TargetSampleCount, bufferSampleCount);
var newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, nint newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate,
RequestedChannelCount); RequestedChannelCount, _sampleCount, _callbackDelegate);
_hasSetupError = newOutputStream == 0; _hasSetupError = newOutputStream == 0;
@ -56,13 +67,87 @@ namespace Ryujinx.Audio.Backends.SDL3
} }
_outputStream = newOutputStream; _outputStream = newOutputStream;
// SDL_PauseAudioDevice(_outputStream, _started ? 0 : 1);
SDL_ResumeAudioStreamDevice(_outputStream); SDL_ResumeAudioStreamDevice(_outputStream);
Logger.Info?.Print(LogClass.Audio, Logger.Info?.Print(LogClass.Audio,
$"New audio stream setup with a target sample count of {_sampleCount}"); $"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<byte> 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<byte> samplesOwner = SpanOwner<byte>.Rent(frameCount * _bytesPerFrame);
using SpanOwner<byte> destinationOwner = SpanOwner<byte>.Rent(frameCount * _bytesPerFrame);
Span<byte> samples = samplesOwner.Span;
Span<byte> 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() public override ulong GetPlayedSampleCount()
{ {
return Interlocked.Read(ref _playedSampleCount); return Interlocked.Read(ref _playedSampleCount);
@ -78,23 +163,14 @@ namespace Ryujinx.Audio.Backends.SDL3
public override void QueueBuffer(AudioBuffer buffer) public override void QueueBuffer(AudioBuffer buffer)
{ {
EnsureAudioStreamSetup(buffer); EnsureAudioStreamSetup(buffer);
if (_outputStream != 0) if (_outputStream != 0)
{ {
if (SDL_GetAudioStreamAvailable(_outputStream) < int.MaxValue) SDL3AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer));
{
unsafe _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
{
fixed (byte* samplesPtr = buffer.Data) _queuedBuffers.Enqueue(driverBuffer);
{
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);
}
}
}
} }
else else
{ {
@ -139,7 +215,12 @@ namespace Ryujinx.Audio.Backends.SDL3
public override bool WasBufferFullyConsumed(AudioBuffer buffer) 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) protected virtual void Dispose(bool disposing)

View File

@ -968,7 +968,7 @@ public static unsafe partial class SDL
[LibraryImport(nativeLibName)] [LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [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)] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void SDL_AudioPostmixCallback(IntPtr userdata, SDL_AudioSpec* spec, float* buffer, int buflen); public delegate void SDL_AudioPostmixCallback(IntPtr userdata, SDL_AudioSpec* spec, float* buffer, int buflen);