aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Audio.Backends.SDL2
diff options
context:
space:
mode:
Diffstat (limited to 'Ryujinx.Audio.Backends.SDL2')
-rw-r--r--Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj13
-rw-r--r--Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs16
-rw-r--r--Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs184
-rw-r--r--Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs223
4 files changed, 436 insertions, 0 deletions
diff --git a/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
new file mode 100644
index 00000000..6619a500
--- /dev/null
+++ b/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
+ <ProjectReference Include="..\Ryujinx.SDL2.Common\Ryujinx.SDL2.Common.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs b/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs
new file mode 100644
index 00000000..71ef414a
--- /dev/null
+++ b/Ryujinx.Audio.Backends.SDL2/SDL2AudioBuffer.cs
@@ -0,0 +1,16 @@
+namespace Ryujinx.Audio.Backends.SDL2
+{
+ class SDL2AudioBuffer
+ {
+ public readonly ulong DriverIdentifier;
+ public readonly ulong SampleCount;
+ public ulong SamplePlayed;
+
+ public SDL2AudioBuffer(ulong driverIdentifier, ulong sampleCount)
+ {
+ DriverIdentifier = driverIdentifier;
+ SampleCount = sampleCount;
+ SamplePlayed = 0;
+ }
+ }
+}
diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
new file mode 100644
index 00000000..07131d1d
--- /dev/null
+++ b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceDriver.cs
@@ -0,0 +1,184 @@
+using Ryujinx.Audio.Backends.Common;
+using Ryujinx.Audio.Common;
+using Ryujinx.Audio.Integration;
+using Ryujinx.Memory;
+using Ryujinx.SDL2.Common;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
+using static SDL2.SDL;
+
+namespace Ryujinx.Audio.Backends.SDL2
+{
+ public class SDL2HardwareDeviceDriver : IHardwareDeviceDriver
+ {
+ private object _lock = new object();
+
+ private ManualResetEvent _updateRequiredEvent;
+ private List<SDL2HardwareDeviceSession> _sessions;
+
+ public SDL2HardwareDeviceDriver()
+ {
+ _updateRequiredEvent = new ManualResetEvent(false);
+ _sessions = new List<SDL2HardwareDeviceSession>();
+
+ SDL2Driver.Instance.Initialize();
+ }
+
+ public static bool IsSupported => IsSupportedInternal();
+
+ private static bool IsSupportedInternal()
+ {
+ uint device = OpenStream(SampleFormat.PcmInt16, Constants.TargetSampleRate, Constants.ChannelCountMax, Constants.TargetSampleCount, null);
+
+ if (device != 0)
+ {
+ SDL_CloseAudioDevice(device);
+ }
+
+ return device != 0;
+ }
+
+ public ManualResetEvent GetUpdateRequiredEvent()
+ {
+ return _updateRequiredEvent;
+ }
+
+ public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
+ {
+ if (channelCount == 0)
+ {
+ channelCount = 2;
+ }
+
+ if (sampleRate == 0)
+ {
+ sampleRate = Constants.TargetSampleRate;
+ }
+
+ if (direction != Direction.Output)
+ {
+ throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
+ }
+
+ lock (_lock)
+ {
+ SDL2HardwareDeviceSession session = new SDL2HardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
+
+ _sessions.Add(session);
+
+ return session;
+ }
+ }
+
+ internal void Unregister(SDL2HardwareDeviceSession session)
+ {
+ lock (_lock)
+ {
+ _sessions.Remove(session);
+ }
+ }
+
+ private static SDL_AudioSpec GetSDL2Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount)
+ {
+ return new SDL_AudioSpec
+ {
+ channels = (byte)requestedChannelCount,
+ format = GetSDL2Format(requestedSampleFormat),
+ freq = (int)requestedSampleRate,
+ samples = (ushort)sampleCount
+ };
+ }
+
+ internal static ushort GetSDL2Format(SampleFormat format)
+ {
+ return format switch
+ {
+ SampleFormat.PcmInt8 => AUDIO_S8,
+ SampleFormat.PcmInt16 => AUDIO_S16,
+ SampleFormat.PcmInt32 => AUDIO_S32,
+ SampleFormat.PcmFloat => AUDIO_F32,
+ _ => throw new ArgumentException($"Unsupported sample format {format}"),
+ };
+ }
+
+ // TODO: Fix this in SDL2-CS.
+ [DllImport("SDL2", EntryPoint = "SDL_OpenAudioDevice", CallingConvention = CallingConvention.Cdecl)]
+ private static extern uint SDL_OpenAudioDevice_Workaround(
+ IntPtr name,
+ int iscapture,
+ ref SDL_AudioSpec desired,
+ out SDL_AudioSpec obtained,
+ uint allowed_changes
+ );
+
+ internal static uint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, uint sampleCount, SDL_AudioCallback callback)
+ {
+ SDL_AudioSpec desired = GetSDL2Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount, sampleCount);
+
+ desired.callback = callback;
+
+ uint device = SDL_OpenAudioDevice_Workaround(IntPtr.Zero, 0, ref desired, out SDL_AudioSpec got, 0);
+
+ if (device == 0)
+ {
+ return 0;
+ }
+
+ bool isValid = got.format == desired.format && got.freq == desired.freq && got.channels == desired.channels;
+
+ if (!isValid)
+ {
+ SDL_CloseAudioDevice(device);
+
+ return 0;
+ }
+
+ return device;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ while (_sessions.Count > 0)
+ {
+ SDL2HardwareDeviceSession session = _sessions[_sessions.Count - 1];
+
+ session.Dispose();
+ }
+
+ SDL2Driver.Instance.Dispose();
+ }
+ }
+
+ public bool SupportsSampleRate(uint sampleRate)
+ {
+ return true;
+ }
+
+ public bool SupportsSampleFormat(SampleFormat sampleFormat)
+ {
+ return sampleFormat != SampleFormat.PcmInt24;
+ }
+
+ public bool SupportsChannelCount(uint channelCount)
+ {
+ return true;
+ }
+
+ public bool SupportsDirection(Direction direction)
+ {
+ // TODO: add direction input when supported.
+ return direction == Direction.Output;
+ }
+ }
+}
diff --git a/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
new file mode 100644
index 00000000..344dd9b6
--- /dev/null
+++ b/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
@@ -0,0 +1,223 @@
+using Ryujinx.Audio.Backends.Common;
+using Ryujinx.Audio.Common;
+using Ryujinx.Common.Logging;
+using Ryujinx.Memory;
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using static SDL2.SDL;
+
+namespace Ryujinx.Audio.Backends.SDL2
+{
+ class SDL2HardwareDeviceSession : HardwareDeviceSessionOutputBase
+ {
+ private SDL2HardwareDeviceDriver _driver;
+ private ConcurrentQueue<SDL2AudioBuffer> _queuedBuffers;
+ private DynamicRingBuffer _ringBuffer;
+ private ulong _playedSampleCount;
+ private ManualResetEvent _updateRequiredEvent;
+ private uint _outputStream;
+ private SDL_AudioCallback _callbackDelegate;
+ private int _bytesPerFrame;
+ private uint _sampleCount;
+ private bool _started;
+ private float _volume;
+ private ushort _nativeSampleFormat;
+
+ public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
+ {
+ _driver = driver;
+ _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
+ _queuedBuffers = new ConcurrentQueue<SDL2AudioBuffer>();
+ _ringBuffer = new DynamicRingBuffer();
+ _callbackDelegate = Update;
+ _bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount;
+ _nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
+ _sampleCount = uint.MaxValue;
+ _started = false;
+ _volume = 1.0f;
+ }
+
+ private void EnsureAudioStreamSetup(AudioBuffer buffer)
+ {
+ bool needAudioSetup = _outputStream == 0 || ((uint)GetSampleCount(buffer) % _sampleCount) != 0;
+
+ if (needAudioSetup)
+ {
+ _sampleCount = Math.Max(Constants.TargetSampleCount, (uint)GetSampleCount(buffer));
+
+ uint newOutputStream = SDL2HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount, _sampleCount, _callbackDelegate);
+
+ if (newOutputStream == 0)
+ {
+ // No stream in place, this is unexpected.
+ throw new InvalidOperationException($"OpenStream failed with error: \"{SDL_GetError()}\"");
+ }
+ else
+ {
+ if (_outputStream != 0)
+ {
+ SDL_CloseAudioDevice(_outputStream);
+ }
+
+ _outputStream = newOutputStream;
+
+ SDL_PauseAudioDevice(_outputStream, _started ? 0 : 1);
+
+ Logger.Info?.Print(LogClass.Audio, $"New audio stream setup with a target sample count of {_sampleCount}");
+ }
+ }
+ }
+
+ // TODO: Add this variant with pointer to SDL2-CS.
+ [DllImport("SDL2", EntryPoint = "SDL_MixAudioFormat", CallingConvention = CallingConvention.Cdecl)]
+ private static extern unsafe uint SDL_MixAudioFormat(IntPtr dst, IntPtr src, ushort format, uint len, int volume);
+
+ private unsafe void Update(IntPtr userdata, IntPtr stream, int streamLength)
+ {
+ Span<byte> streamSpan = new Span<byte>((void*)stream, streamLength);
+
+ int maxFrameCount = (int)GetSampleCount(streamLength);
+ int bufferedFrames = _ringBuffer.Length / _bytesPerFrame;
+
+ int frameCount = Math.Min(bufferedFrames, maxFrameCount);
+
+ if (frameCount == 0)
+ {
+ // SDL2 left the responsability to the user to clear the buffer.
+ streamSpan.Fill(0);
+
+ return;
+ }
+
+ byte[] samples = new byte[frameCount * _bytesPerFrame];
+
+ _ringBuffer.Read(samples, 0, samples.Length);
+
+ samples.AsSpan().CopyTo(streamSpan);
+ streamSpan.Slice(samples.Length).Fill(0);
+
+ // Apply volume to written data
+ SDL_MixAudioFormat(stream, stream, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
+
+ ulong sampleCount = GetSampleCount(samples.Length);
+
+ ulong availaibleSampleCount = sampleCount;
+
+ bool needUpdate = false;
+
+ while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SDL2AudioBuffer 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);
+ }
+
+ public override float GetVolume()
+ {
+ return _volume;
+ }
+
+ public override void PrepareToClose() { }
+
+ public override void QueueBuffer(AudioBuffer buffer)
+ {
+ EnsureAudioStreamSetup(buffer);
+
+ SDL2AudioBuffer driverBuffer = new SDL2AudioBuffer(buffer.DataPointer, GetSampleCount(buffer));
+
+ _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
+
+ _queuedBuffers.Enqueue(driverBuffer);
+ }
+
+ public override void SetVolume(float volume)
+ {
+ _volume = volume;
+ }
+
+ public override void Start()
+ {
+ if (!_started)
+ {
+ if (_outputStream != 0)
+ {
+ SDL_PauseAudioDevice(_outputStream, 0);
+ }
+
+ _started = true;
+ }
+ }
+
+ public override void Stop()
+ {
+ if (_started)
+ {
+ if (_outputStream != 0)
+ {
+ SDL_PauseAudioDevice(_outputStream, 1);
+ }
+
+ _started = false;
+ }
+ }
+
+ public override void UnregisterBuffer(AudioBuffer buffer) { }
+
+ public override bool WasBufferFullyConsumed(AudioBuffer buffer)
+ {
+ if (!_queuedBuffers.TryPeek(out SDL2AudioBuffer driverBuffer))
+ {
+ return true;
+ }
+
+ return driverBuffer.DriverIdentifier != buffer.DataPointer;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ PrepareToClose();
+ Stop();
+
+ if (_outputStream != 0)
+ {
+ SDL_CloseAudioDevice(_outputStream);
+ }
+
+ _driver.Unregister(this);
+ }
+ }
+
+ public override void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+}