Просмотр исходного кода

audio: Implement a SDL2 backend (#2258)

* audio: Implement a SDL2 backend

This adds support to SDL2 as an audio backend.
It has the same compatibility level as OpenAL without its issues.

I also took the liberty of restructuring the SDL2 code to have one
shared project between audio and input.

The configuration version was also incremented.

* Address gdkchan's comments

* Fix update logic

* Add an heuristic to pick the correct target sample count wanted by the game

* Address gdkchan's comments

* Address Ac_k's comments

* Fix audren output

* Address gdkchan's comments
Mary 5 лет назад
Родитель
Сommit
eb056218a1

+ 13 - 0
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>

+ 16 - 0
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;
+        }
+    }
+}

+ 184 - 0
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;
+        }
+    }
+}

+ 223 - 0
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);
+        }
+    }
+}

+ 8 - 1
Ryujinx.Audio/Backends/Common/HardwareDeviceSessionOutputBase.cs

@@ -18,6 +18,7 @@
 using Ryujinx.Audio.Common;
 using Ryujinx.Audio.Integration;
 using Ryujinx.Memory;
+using System.Runtime.CompilerServices;
 
 namespace Ryujinx.Audio.Backends.Common
 {
@@ -52,7 +53,13 @@ namespace Ryujinx.Audio.Backends.Common
 
         protected ulong GetSampleCount(AudioBuffer buffer)
         {
-            return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, (int)buffer.DataSize);
+            return GetSampleCount((int)buffer.DataSize);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        protected ulong GetSampleCount(int dataSize)
+        {
+            return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, dataSize);
         }
 
         public abstract void Dispose();

+ 2 - 1
Ryujinx.Common/Configuration/AudioBackend.cs

@@ -4,6 +4,7 @@
     {
         Dummy,
         OpenAl,
-        SoundIo
+        SoundIo,
+        SDL2
     }
 }

+ 1 - 1
Ryujinx.Common/Configuration/ConfigurationFileFormat.cs

@@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        public const int CurrentVersion = 24;
+        public const int CurrentVersion = 25;
 
         public int Version { get; set; }
 

+ 7 - 0
Ryujinx.Common/Configuration/ConfigurationState.cs

@@ -803,6 +803,13 @@ namespace Ryujinx.Configuration
                 configurationFileUpdated = true;
             }
 
+            if (configurationFileFormat.Version < 25)
+            {
+                Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25.");
+
+                configurationFileUpdated = true;
+            }
+
             Logger.EnableFileLog.Value             = configurationFileFormat.EnableFileLog;
             Graphics.ResScale.Value                = configurationFileFormat.ResScale;
             Graphics.ResScaleCustom.Value          = configurationFileFormat.ResScaleCustom;

+ 1 - 4
Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj

@@ -5,12 +5,9 @@
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
-  <ItemGroup>
-    <PackageReference Include="ppy.SDL2-CS" Version="1.0.225-alpha" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
+    <ProjectReference Include="..\Ryujinx.SDL2.Common\Ryujinx.SDL2.Common.csproj" />
   </ItemGroup>
 
 </Project>

+ 2 - 1
Ryujinx.Input.SDL2/SDL2GamepadDriver.cs

@@ -1,4 +1,5 @@
-using System;
+using Ryujinx.SDL2.Common;
+using System;
 using System.Collections.Generic;
 using static SDL2.SDL;
 

+ 15 - 0
Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ppy.SDL2-CS" Version="1.0.225-alpha" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
+  </ItemGroup>
+
+</Project>

+ 3 - 3
Ryujinx.Input.SDL2/SDL2Driver.cs → Ryujinx.SDL2.Common/SDL2Driver.cs

@@ -4,9 +4,9 @@ using System.IO;
 using System.Threading;
 using static SDL2.SDL;
 
-namespace Ryujinx.Input.SDL2
+namespace Ryujinx.SDL2.Common
 {
-    class SDL2Driver : IDisposable
+    public class SDL2Driver : IDisposable
     {
         private static SDL2Driver _instance;
 
@@ -25,7 +25,7 @@ namespace Ryujinx.Input.SDL2
             }
         }
 
-        private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK;
+        private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO;
 
         private bool _isRunning;
         private uint _refereceCount;

+ 14 - 2
Ryujinx.sln

@@ -59,9 +59,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Open
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SoundIo", "Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj", "{716364DE-B988-41A6-BAB4-327964266ECC}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input", "Ryujinx.Input\Ryujinx.Input.csproj", "{C16F112F-38C3-40BC-9F5F-4791112063D6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input", "Ryujinx.Input\Ryujinx.Input.csproj", "{C16F112F-38C3-40BC-9F5F-4791112063D6}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL2", "Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj", "{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Input.SDL2", "Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj", "{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL2.Common", "Ryujinx.SDL2.Common\Ryujinx.SDL2.Common.csproj", "{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Audio.Backends.SDL2", "Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj", "{D99A395A-8569-4DB0-B336-900647890052}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -177,6 +181,14 @@ Global
 		{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 1 - 0
Ryujinx/Ryujinx.csproj

@@ -26,6 +26,7 @@
     <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
     <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
     <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
+    <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
     <ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
     <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
     <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />

+ 13 - 1
Ryujinx/Ui/MainWindow.cs

@@ -5,6 +5,7 @@ using LibHac.Common;
 using LibHac.Ns;
 using Ryujinx.Audio.Backends.Dummy;
 using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
 using Ryujinx.Audio.Backends.SoundIo;
 using Ryujinx.Audio.Integration;
 using Ryujinx.Common.Configuration;
@@ -327,7 +328,18 @@ namespace Ryujinx.Ui
 
             IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver();
 
-            if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo)
+            if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SDL2)
+            {
+                if (SDL2HardwareDeviceDriver.IsSupported)
+                {
+                    deviceDriver = new SDL2HardwareDeviceDriver();
+                }
+                else
+                {
+                    Logger.Warning?.Print(LogClass.Audio, "SDL2 audio is not supported, falling back to dummy audio out.");
+                }
+            }
+            else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo)
             {
                 if (SoundIoHardwareDeviceDriver.IsSupported)
                 {

+ 8 - 0
Ryujinx/Ui/Windows/SettingsWindow.cs

@@ -1,5 +1,6 @@
 using Gtk;
 using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
 using Ryujinx.Audio.Backends.SoundIo;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
@@ -302,6 +303,7 @@ namespace Ryujinx.Ui.Windows
 
             TreeIter openAlIter  = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl);
             TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo);
+            TreeIter sdl2Iter    = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2);
             TreeIter dummyIter   = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy);
 
             _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore);
@@ -316,6 +318,9 @@ namespace Ryujinx.Ui.Windows
                 case AudioBackend.SoundIo:
                     _audioBackendSelect.SetActiveIter(soundIoIter);
                     break;
+                case AudioBackend.SDL2:
+                    _audioBackendSelect.SetActiveIter(sdl2Iter);
+                    break;
                 case AudioBackend.Dummy:
                     _audioBackendSelect.SetActiveIter(dummyIter);
                     break;
@@ -328,11 +333,13 @@ namespace Ryujinx.Ui.Windows
 
             bool openAlIsSupported  = false;
             bool soundIoIsSupported = false;
+            bool sdl2IsSupported    = false;
 
             Task.Run(() =>
             {
                 openAlIsSupported  = OpenALHardwareDeviceDriver.IsSupported;
                 soundIoIsSupported = SoundIoHardwareDeviceDriver.IsSupported;
+                sdl2IsSupported    = SDL2HardwareDeviceDriver.IsSupported;
             });
 
             // This function runs whenever the dropdown is opened
@@ -342,6 +349,7 @@ namespace Ryujinx.Ui.Windows
                 {
                     AudioBackend.OpenAl  => openAlIsSupported,
                     AudioBackend.SoundIo => soundIoIsSupported,
+                    AudioBackend.SDL2    => sdl2IsSupported,
                     AudioBackend.Dummy   => true,
                     _ => throw new ArgumentOutOfRangeException()
                 };