diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Audio.Backends.SoundIo | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Audio.Backends.SoundIo')
17 files changed, 1381 insertions, 0 deletions
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs new file mode 100644 index 00000000..9c3e686d --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIo.cs @@ -0,0 +1,178 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public static partial class SoundIo + { + private const string LibraryName = "libsoundio"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void OnDeviceChangeNativeDelegate(IntPtr ctx); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void OnBackendDisconnectedDelegate(IntPtr ctx, SoundIoError err); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void OnEventsSignalDelegate(IntPtr ctx); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void EmitRtPrioWarningDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void JackCallbackDelegate(IntPtr msg); + + [StructLayout(LayoutKind.Sequential)] + public struct SoundIoStruct + { + public IntPtr UserData; + public IntPtr OnDeviceChange; + public IntPtr OnBackendDisconnected; + public IntPtr OnEventsSignal; + public SoundIoBackend CurrentBackend; + public IntPtr ApplicationName; + public IntPtr EmitRtPrioWarning; + public IntPtr JackInfoCallback; + public IntPtr JackErrorCallback; + } + + public struct SoundIoChannelLayout + { + public IntPtr Name; + public int ChannelCount; + public Array24<SoundIoChannelId> Channels; + + public static IntPtr GetDefault(int channelCount) + { + return soundio_channel_layout_get_default(channelCount); + } + + public static unsafe SoundIoChannelLayout GetDefaultValue(int channelCount) + { + return Unsafe.AsRef<SoundIoChannelLayout>((SoundIoChannelLayout*)GetDefault(channelCount)); + } + } + + public struct SoundIoSampleRateRange + { + public int Min; + public int Max; + } + + public struct SoundIoDevice + { + public IntPtr SoundIo; + public IntPtr Id; + public IntPtr Name; + public SoundIoDeviceAim Aim; + public IntPtr Layouts; + public int LayoutCount; + public SoundIoChannelLayout CurrentLayout; + public IntPtr Formats; + public int FormatCount; + public SoundIoFormat CurrentFormat; + public IntPtr SampleRates; + public int SampleRateCount; + public int SampleRateCurrent; + public double SoftwareLatencyMin; + public double SoftwareLatencyMax; + public double SoftwareLatencyCurrent; + public bool IsRaw; + public int RefCount; + public SoundIoError ProbeError; + } + + public struct SoundIoOutStream + { + public IntPtr Device; + public SoundIoFormat Format; + public int SampleRate; + public SoundIoChannelLayout Layout; + public double SoftwareLatency; + public float Volume; + public IntPtr UserData; + public IntPtr WriteCallback; + public IntPtr UnderflowCallback; + public IntPtr ErrorCallback; + public IntPtr Name; + public bool NonTerminalHint; + public int BytesPerFrame; + public int BytesPerSample; + public SoundIoError LayoutError; + } + + public struct SoundIoChannelArea + { + public IntPtr Pointer; + public int Step; + } + + [LibraryImport(LibraryName)] + public static partial IntPtr soundio_create(); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_connect(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial void soundio_disconnect(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial void soundio_flush_events(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial int soundio_output_device_count(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial int soundio_default_output_device_index(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial IntPtr soundio_get_output_device(IntPtr ctx, int index); + + [LibraryImport(LibraryName)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool soundio_device_supports_format(IntPtr devCtx, SoundIoFormat format); + + [LibraryImport(LibraryName)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool soundio_device_supports_layout(IntPtr devCtx, IntPtr layout); + + [LibraryImport(LibraryName)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool soundio_device_supports_sample_rate(IntPtr devCtx, int sampleRate); + + [LibraryImport(LibraryName)] + public static partial IntPtr soundio_outstream_create(IntPtr devCtx); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_open(IntPtr outStreamCtx); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_start(IntPtr outStreamCtx); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_begin_write(IntPtr outStreamCtx, IntPtr areas, IntPtr frameCount); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_end_write(IntPtr outStreamCtx); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_pause(IntPtr devCtx, [MarshalAs(UnmanagedType.Bool)] bool pause); + + [LibraryImport(LibraryName)] + public static partial SoundIoError soundio_outstream_set_volume(IntPtr devCtx, double volume); + + [LibraryImport(LibraryName)] + public static partial void soundio_outstream_destroy(IntPtr streamCtx); + + [LibraryImport(LibraryName)] + public static partial void soundio_destroy(IntPtr ctx); + + [LibraryImport(LibraryName)] + public static partial IntPtr soundio_channel_layout_get_default(int channelCount); + + [LibraryImport(LibraryName)] + public static partial IntPtr soundio_strerror(SoundIoError err); + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoBackend.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoBackend.cs new file mode 100644 index 00000000..92f8ea37 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoBackend.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public enum SoundIoBackend : int + { + None = 0, + Jack = 1, + PulseAudio = 2, + Alsa = 3, + CoreAudio = 4, + Wasapi = 5, + Dummy = 6 + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoChannelId.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoChannelId.cs new file mode 100644 index 00000000..70346e0b --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoChannelId.cs @@ -0,0 +1,75 @@ +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public enum SoundIoChannelId + { + Invalid = 0, + FrontLeft = 1, + FrontRight = 2, + FrontCenter = 3, + Lfe = 4, + BackLeft = 5, + BackRight = 6, + FrontLeftCenter = 7, + FrontRightCenter = 8, + BackCenter = 9, + SideLeft = 10, + SideRight = 11, + TopCenter = 12, + TopFrontLeft = 13, + TopFrontCenter = 14, + TopFrontRight = 15, + TopBackLeft = 16, + TopBackCenter = 17, + TopBackRight = 18, + BackLeftCenter = 19, + BackRightCenter = 20, + FrontLeftWide = 21, + FrontRightWide = 22, + FrontLeftHigh = 23, + FrontCenterHigh = 24, + FrontRightHigh = 25, + TopFrontLeftCenter = 26, + TopFrontRightCenter = 27, + TopSideLeft = 28, + TopSideRight = 29, + LeftLfe = 30, + RightLfe = 31, + Lfe2 = 32, + BottomCenter = 33, + BottomLeftCenter = 34, + BottomRightCenter = 35, + MsMid = 36, + MsSide = 37, + AmbisonicW = 38, + AmbisonicX = 39, + AmbisonicY = 40, + AmbisonicZ = 41, + XyX = 42, + XyY = 43, + HeadphonesLeft = 44, + HeadphonesRight = 45, + ClickTrack = 46, + ForeignLanguage = 47, + HearingImpaired = 48, + Narration = 49, + Haptic = 50, + DialogCentricMix = 51, + Aux = 52, + Aux0 = 53, + Aux1 = 54, + Aux2 = 55, + Aux3 = 56, + Aux4 = 57, + Aux5 = 58, + Aux6 = 59, + Aux7 = 60, + Aux8 = 61, + Aux9 = 62, + Aux10 = 63, + Aux11 = 64, + Aux12 = 65, + Aux13 = 66, + Aux14 = 67, + Aux15 = 68, + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs new file mode 100644 index 00000000..3744c2e6 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoContext.cs @@ -0,0 +1,107 @@ +using System; +using System.Reflection.Metadata; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; + +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public class SoundIoContext : IDisposable + { + private IntPtr _context; + private Action<SoundIoError> _onBackendDisconnect; + private OnBackendDisconnectedDelegate _onBackendDisconnectNative; + + public IntPtr Context => _context; + + internal SoundIoContext(IntPtr context) + { + _context = context; + _onBackendDisconnect = null; + _onBackendDisconnectNative = null; + } + + public SoundIoError Connect() => soundio_connect(_context); + public void Disconnect() => soundio_disconnect(_context); + + public void FlushEvents() => soundio_flush_events(_context); + + public int OutputDeviceCount => soundio_output_device_count(_context); + + public int DefaultOutputDeviceIndex => soundio_default_output_device_index(_context); + + public Action<SoundIoError> OnBackendDisconnect + { + get { return _onBackendDisconnect; } + set + { + _onBackendDisconnect = value; + + if (_onBackendDisconnect == null) + { + _onBackendDisconnectNative = null; + } + else + { + _onBackendDisconnectNative = (ctx, err) => _onBackendDisconnect(err); + } + + GetContext().OnBackendDisconnected = Marshal.GetFunctionPointerForDelegate(_onBackendDisconnectNative); + } + } + + private ref SoundIoStruct GetContext() + { + unsafe + { + return ref Unsafe.AsRef<SoundIoStruct>((SoundIoStruct*)_context); + } + } + + public SoundIoDeviceContext GetOutputDevice(int index) + { + IntPtr deviceContext = soundio_get_output_device(_context, index); + + if (deviceContext == IntPtr.Zero) + { + return null; + } + + return new SoundIoDeviceContext(deviceContext); + } + + public static SoundIoContext Create() + { + IntPtr context = soundio_create(); + + if (context == IntPtr.Zero) + { + return null; + } + + return new SoundIoContext(context); + } + + protected virtual void Dispose(bool disposing) + { + IntPtr currentContext = Interlocked.Exchange(ref _context, IntPtr.Zero); + + if (currentContext != IntPtr.Zero) + { + soundio_destroy(currentContext); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~SoundIoContext() + { + Dispose(false); + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceAim.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceAim.cs new file mode 100644 index 00000000..a0689d6d --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceAim.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public enum SoundIoDeviceAim + { + SoundIoDeviceAimInput = 0, + SoundIoDeviceAimOutput = 1 + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs new file mode 100644 index 00000000..42bcc6e3 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoDeviceContext.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; + +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public class SoundIoDeviceContext + { + private readonly IntPtr _context; + + public IntPtr Context => _context; + + internal SoundIoDeviceContext(IntPtr context) + { + _context = context; + } + + private ref SoundIoDevice GetDeviceContext() + { + unsafe + { + return ref Unsafe.AsRef<SoundIoDevice>((SoundIoDevice*)_context); + } + } + + public bool IsRaw => GetDeviceContext().IsRaw; + + public string Id => Marshal.PtrToStringAnsi(GetDeviceContext().Id); + + public bool SupportsSampleRate(int sampleRate) => soundio_device_supports_sample_rate(_context, sampleRate); + + public bool SupportsFormat(SoundIoFormat format) => soundio_device_supports_format(_context, format); + + public bool SupportsChannelCount(int channelCount) => soundio_device_supports_layout(_context, SoundIoChannelLayout.GetDefault(channelCount)); + + public SoundIoOutStreamContext CreateOutStream() + { + IntPtr context = soundio_outstream_create(_context); + + if (context == IntPtr.Zero) + { + return null; + } + + return new SoundIoOutStreamContext(context); + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoError.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoError.cs new file mode 100644 index 00000000..9e33fa19 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoError.cs @@ -0,0 +1,22 @@ +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public enum SoundIoError + { + None = 0, + NoMem = 1, + InitAudioBackend = 2, + SystemResources = 3, + OpeningDevice = 4, + NoSuchDevice = 5, + Invalid = 6, + BackendUnavailable = 7, + Streaming = 8, + IncompatibleDevice = 9, + NoSuchClient = 10, + IncompatibleBackend = 11, + BackendDisconnected = 12, + Interrupted = 13, + Underflow = 14, + EncodingString = 15, + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoException.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoException.cs new file mode 100644 index 00000000..a033356e --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoException.cs @@ -0,0 +1,11 @@ +using System; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; + +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + internal class SoundIoException : Exception + { + internal SoundIoException(SoundIoError error) : base(Marshal.PtrToStringAnsi(soundio_strerror(error))) { } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoFormat.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoFormat.cs new file mode 100644 index 00000000..0eee9780 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoFormat.cs @@ -0,0 +1,25 @@ +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public enum SoundIoFormat + { + Invalid = 0, + S8 = 1, + U8 = 2, + S16LE = 3, + S16BE = 4, + U16LE = 5, + U16BE = 6, + S24LE = 7, + S24BE = 8, + U24LE = 9, + U24BE = 10, + S32LE = 11, + S32BE = 12, + U32LE = 13, + U32BE = 14, + Float32LE = 15, + Float32BE = 16, + Float64LE = 17, + Float64BE = 18, + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs new file mode 100644 index 00000000..2e432b31 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/SoundIoOutStreamContext.cs @@ -0,0 +1,164 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; + +namespace Ryujinx.Audio.Backends.SoundIo.Native +{ + public class SoundIoOutStreamContext : IDisposable + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate void WriteCallbackDelegate(IntPtr ctx, int frameCountMin, int frameCountMax); + + private IntPtr _context; + private IntPtr _nameStored; + private Action<int, int> _writeCallback; + private WriteCallbackDelegate _writeCallbackNative; + + public IntPtr Context => _context; + + internal SoundIoOutStreamContext(IntPtr context) + { + _context = context; + _nameStored = IntPtr.Zero; + _writeCallback = null; + _writeCallbackNative = null; + } + + private ref SoundIoOutStream GetOutContext() + { + unsafe + { + return ref Unsafe.AsRef<SoundIoOutStream>((SoundIoOutStream*)_context); + } + } + + public string Name + { + get => Marshal.PtrToStringAnsi(GetOutContext().Name); + set + { + var context = GetOutContext(); + + if (_nameStored != IntPtr.Zero && context.Name == _nameStored) + { + Marshal.FreeHGlobal(_nameStored); + } + + _nameStored = Marshal.StringToHGlobalAnsi(value); + GetOutContext().Name = _nameStored; + } + } + + public SoundIoChannelLayout Layout + { + get => GetOutContext().Layout; + set => GetOutContext().Layout = value; + } + + public SoundIoFormat Format + { + get => GetOutContext().Format; + set => GetOutContext().Format = value; + } + + public int SampleRate + { + get => GetOutContext().SampleRate; + set => GetOutContext().SampleRate = value; + } + + public float Volume + { + get => GetOutContext().Volume; + set => GetOutContext().Volume = value; + } + + public int BytesPerFrame + { + get => GetOutContext().BytesPerFrame; + set => GetOutContext().BytesPerFrame = value; + } + + public int BytesPerSample + { + get => GetOutContext().BytesPerSample; + set => GetOutContext().BytesPerSample = value; + } + + public Action<int, int> WriteCallback + { + get { return _writeCallback; } + set + { + _writeCallback = value; + + if (_writeCallback == null) + { + _writeCallbackNative = null; + } + else + { + _writeCallbackNative = (ctx, frameCountMin, frameCountMax) => _writeCallback(frameCountMin, frameCountMax); + } + + GetOutContext().WriteCallback = Marshal.GetFunctionPointerForDelegate(_writeCallbackNative); + } + } + + private static void CheckError(SoundIoError error) + { + if (error != SoundIoError.None) + { + throw new SoundIoException(error); + } + } + + public void Open() => CheckError(soundio_outstream_open(_context)); + + public void Start() => CheckError(soundio_outstream_start(_context)); + + public void Pause(bool pause) => CheckError(soundio_outstream_pause(_context, pause)); + + public void SetVolume(double volume) => CheckError(soundio_outstream_set_volume(_context, volume)); + + public Span<SoundIoChannelArea> BeginWrite(ref int frameCount) + { + IntPtr arenas = default; + int nativeFrameCount = frameCount; + + unsafe + { + var frameCountPtr = &nativeFrameCount; + var arenasPtr = &arenas; + CheckError(soundio_outstream_begin_write(_context, (IntPtr)arenasPtr, (IntPtr)frameCountPtr)); + + frameCount = *frameCountPtr; + + return new Span<SoundIoChannelArea>((void*)arenas, Layout.ChannelCount); + } + } + + public void EndWrite() => CheckError(soundio_outstream_end_write(_context)); + + protected virtual void Dispose(bool disposing) + { + if (_context != IntPtr.Zero) + { + soundio_outstream_destroy(_context); + _context = IntPtr.Zero; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~SoundIoOutStreamContext() + { + Dispose(false); + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dll b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dll Binary files differnew file mode 100644 index 00000000..48804312 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dll diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dylib b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dylib Binary files differnew file mode 100644 index 00000000..10171f4f --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.dylib diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.so b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.so Binary files differnew file mode 100644 index 00000000..87c8b506 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Native/libsoundio/libs/libsoundio.so diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj new file mode 100644 index 00000000..9f242dbe --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj @@ -0,0 +1,28 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <RuntimeIdentifiers>win10-x64;linux-x64;osx-x64</RuntimeIdentifiers> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" /> + </ItemGroup> + + <ItemGroup> + <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dll" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <TargetPath>libsoundio.dll</TargetPath> + </ContentWithTargetPath> + <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win10-x64'"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <TargetPath>libsoundio.dylib</TargetPath> + </ContentWithTargetPath> + <ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win10-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <TargetPath>libsoundio.so</TargetPath> + </ContentWithTargetPath> + </ItemGroup> + +</Project> diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoAudioBuffer.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoAudioBuffer.cs new file mode 100644 index 00000000..7f32b953 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoAudioBuffer.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Audio.Backends.SoundIo +{ + class SoundIoAudioBuffer + { + public readonly ulong DriverIdentifier; + public readonly ulong SampleCount; + public ulong SamplePlayed; + + public SoundIoAudioBuffer(ulong driverIdentifier, ulong sampleCount) + { + DriverIdentifier = driverIdentifier; + SampleCount = sampleCount; + SamplePlayed = 0; + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs new file mode 100644 index 00000000..02da2776 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs @@ -0,0 +1,247 @@ +using Ryujinx.Audio.Backends.SoundIo.Native; +using Ryujinx.Audio.Common; +using Ryujinx.Audio.Integration; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Threading; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; +using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; + +namespace Ryujinx.Audio.Backends.SoundIo +{ + public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver + { + private readonly SoundIoContext _audioContext; + private readonly SoundIoDeviceContext _audioDevice; + private readonly ManualResetEvent _updateRequiredEvent; + private readonly ManualResetEvent _pauseEvent; + private readonly ConcurrentDictionary<SoundIoHardwareDeviceSession, byte> _sessions; + private int _disposeState; + + public SoundIoHardwareDeviceDriver() + { + _audioContext = SoundIoContext.Create(); + _updateRequiredEvent = new ManualResetEvent(false); + _pauseEvent = new ManualResetEvent(true); + _sessions = new ConcurrentDictionary<SoundIoHardwareDeviceSession, byte>(); + + _audioContext.Connect(); + _audioContext.FlushEvents(); + + _audioDevice = FindValidAudioDevice(_audioContext, true); + } + + public static bool IsSupported => IsSupportedInternal(); + + private static bool IsSupportedInternal() + { + SoundIoContext context = null; + SoundIoDeviceContext device = null; + SoundIoOutStreamContext stream = null; + + bool backendDisconnected = false; + + try + { + context = SoundIoContext.Create(); + context.OnBackendDisconnect = err => + { + backendDisconnected = true; + }; + + context.Connect(); + context.FlushEvents(); + + if (backendDisconnected) + { + return false; + } + + if (context.OutputDeviceCount == 0) + { + return false; + } + + device = FindValidAudioDevice(context); + + if (device == null || backendDisconnected) + { + return false; + } + + stream = device.CreateOutStream(); + + if (stream == null || backendDisconnected) + { + return false; + } + + return true; + } + catch + { + return false; + } + finally + { + stream?.Dispose(); + context?.Dispose(); + } + } + + private static SoundIoDeviceContext FindValidAudioDevice(SoundIoContext audioContext, bool fallback = false) + { + SoundIoDeviceContext defaultAudioDevice = audioContext.GetOutputDevice(audioContext.DefaultOutputDeviceIndex); + + if (!defaultAudioDevice.IsRaw) + { + return defaultAudioDevice; + } + + for (int i = 0; i < audioContext.OutputDeviceCount; i++) + { + SoundIoDeviceContext audioDevice = audioContext.GetOutputDevice(i); + + if (audioDevice.Id == defaultAudioDevice.Id && !audioDevice.IsRaw) + { + return audioDevice; + } + } + + return fallback ? defaultAudioDevice : null; + } + + public ManualResetEvent GetUpdateRequiredEvent() + { + return _updateRequiredEvent; + } + + public ManualResetEvent GetPauseEvent() + { + return _pauseEvent; + } + + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume) + { + if (channelCount == 0) + { + channelCount = 2; + } + + if (sampleRate == 0) + { + sampleRate = Constants.TargetSampleRate; + } + + volume = Math.Clamp(volume, 0, 1); + + if (direction != Direction.Output) + { + throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!"); + } + + SoundIoHardwareDeviceSession session = new SoundIoHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount, volume); + + _sessions.TryAdd(session, 0); + + return session; + } + + internal bool Unregister(SoundIoHardwareDeviceSession session) + { + return _sessions.TryRemove(session, out _); + } + + public static SoundIoFormat GetSoundIoFormat(SampleFormat format) + { + return format switch + { + SampleFormat.PcmInt8 => SoundIoFormat.S8, + SampleFormat.PcmInt16 => SoundIoFormat.S16LE, + SampleFormat.PcmInt24 => SoundIoFormat.S24LE, + SampleFormat.PcmInt32 => SoundIoFormat.S32LE, + SampleFormat.PcmFloat => SoundIoFormat.Float32LE, + _ => throw new ArgumentException ($"Unsupported sample format {format}"), + }; + } + + internal SoundIoOutStreamContext OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) + { + SoundIoFormat driverSampleFormat = GetSoundIoFormat(requestedSampleFormat); + + if (!_audioDevice.SupportsSampleRate((int)requestedSampleRate)) + { + throw new ArgumentException($"This sound device does not support a sample rate of {requestedSampleRate}Hz"); + } + + if (!_audioDevice.SupportsFormat(driverSampleFormat)) + { + throw new ArgumentException($"This sound device does not support {requestedSampleFormat}"); + } + + if (!_audioDevice.SupportsChannelCount((int)requestedChannelCount)) + { + throw new ArgumentException($"This sound device does not support channel count {requestedChannelCount}"); + } + + SoundIoOutStreamContext result = _audioDevice.CreateOutStream(); + + result.Name = "Ryujinx"; + result.Layout = SoundIoChannelLayout.GetDefaultValue((int)requestedChannelCount); + result.Format = driverSampleFormat; + result.SampleRate = (int)requestedSampleRate; + + return result; + } + + internal void FlushContextEvents() + { + _audioContext.FlushEvents(); + } + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0) + { + Dispose(true); + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (SoundIoHardwareDeviceSession session in _sessions.Keys) + { + session.Dispose(); + } + + _audioContext.Disconnect(); + _audioContext.Dispose(); + _pauseEvent.Dispose(); + } + } + + public bool SupportsSampleRate(uint sampleRate) + { + return _audioDevice.SupportsSampleRate((int)sampleRate); + } + + public bool SupportsSampleFormat(SampleFormat sampleFormat) + { + return _audioDevice.SupportsFormat(GetSoundIoFormat(sampleFormat)); + } + + public bool SupportsChannelCount(uint channelCount) + { + return _audioDevice.SupportsChannelCount((int)channelCount); + } + + public bool SupportsDirection(Direction direction) + { + // TODO: add direction input when supported. + return direction == Direction.Output; + } + } +} diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs new file mode 100644 index 00000000..96d9ce97 --- /dev/null +++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs @@ -0,0 +1,438 @@ +using Ryujinx.Audio.Backends.Common; +using Ryujinx.Audio.Backends.SoundIo.Native; +using Ryujinx.Audio.Common; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; +using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; + +namespace Ryujinx.Audio.Backends.SoundIo +{ + class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase + { + private SoundIoHardwareDeviceDriver _driver; + private ConcurrentQueue<SoundIoAudioBuffer> _queuedBuffers; + private SoundIoOutStreamContext _outputStream; + private DynamicRingBuffer _ringBuffer; + private ulong _playedSampleCount; + private ManualResetEvent _updateRequiredEvent; + private int _disposeState; + + public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount) + { + _driver = driver; + _updateRequiredEvent = _driver.GetUpdateRequiredEvent(); + _queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>(); + _ringBuffer = new DynamicRingBuffer(); + + SetupOutputStream(requestedVolume); + } + + private void SetupOutputStream(float requestedVolume) + { + _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount); + _outputStream.WriteCallback += Update; + _outputStream.Volume = requestedVolume; + // TODO: Setup other callbacks (errors, ect). + + _outputStream.Open(); + } + + public override ulong GetPlayedSampleCount() + { + return Interlocked.Read(ref _playedSampleCount); + } + + public override float GetVolume() + { + return _outputStream.Volume; + } + + public override void PrepareToClose() { } + + public override void QueueBuffer(AudioBuffer buffer) + { + SoundIoAudioBuffer driverBuffer = new SoundIoAudioBuffer(buffer.DataPointer, GetSampleCount(buffer)); + + _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length); + + _queuedBuffers.Enqueue(driverBuffer); + } + + public override void SetVolume(float volume) + { + _outputStream.SetVolume(volume); + } + + public override void Start() + { + _outputStream.Start(); + _outputStream.Pause(false); + + _driver.FlushContextEvents(); + } + + public override void Stop() + { + _outputStream.Pause(true); + + _driver.FlushContextEvents(); + } + + public override void UnregisterBuffer(AudioBuffer buffer) {} + + public override bool WasBufferFullyConsumed(AudioBuffer buffer) + { + if (!_queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer)) + { + return true; + } + + return driverBuffer.DriverIdentifier != buffer.DataPointer; + } + + private unsafe void Update(int minFrameCount, int maxFrameCount) + { + int bytesPerFrame = _outputStream.BytesPerFrame; + uint bytesPerSample = (uint)_outputStream.BytesPerSample; + + int bufferedFrames = _ringBuffer.Length / bytesPerFrame; + + int frameCount = Math.Min(bufferedFrames, maxFrameCount); + + if (frameCount == 0) + { + return; + } + + Span<SoundIoChannelArea> areas = _outputStream.BeginWrite(ref frameCount); + + int channelCount = areas.Length; + + byte[] samples = new byte[frameCount * bytesPerFrame]; + + _ringBuffer.Read(samples, 0, samples.Length); + + // This is a huge ugly block of code, but we save + // a significant amount of time over the generic + // loop that handles other channel counts. + // TODO: Is this still right in 2022? + + // Mono + if (channelCount == 1) + { + ref SoundIoChannelArea area = ref areas[0]; + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((byte*)area.Pointer)[0] = srcptr[frame * bytesPerFrame]; + + area.Pointer += area.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((short*)area.Pointer)[0] = ((short*)srcptr)[frame * bytesPerFrame >> 1]; + + area.Pointer += area.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((int*)area.Pointer)[0] = ((int*)srcptr)[frame * bytesPerFrame >> 2]; + + area.Pointer += area.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + Unsafe.CopyBlockUnaligned((byte*)area.Pointer, srcptr + (frame * bytesPerFrame), bytesPerSample); + + area.Pointer += area.Step; + } + } + } + } + // Stereo + else if (channelCount == 2) + { + ref SoundIoChannelArea area1 = ref areas[0]; + ref SoundIoChannelArea area2 = ref areas[1]; + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; + + // Channel 2 + ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; + + // Channel 2 + ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; + + // Channel 2 + ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); + + // Channel 2 + Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + } + } + // Surround + else if (channelCount == 6) + { + ref SoundIoChannelArea area1 = ref areas[0]; + ref SoundIoChannelArea area2 = ref areas[1]; + ref SoundIoChannelArea area3 = ref areas[2]; + ref SoundIoChannelArea area4 = ref areas[3]; + ref SoundIoChannelArea area5 = ref areas[4]; + ref SoundIoChannelArea area6 = ref areas[5]; + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; + + // Channel 2 + ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; + + // Channel 3 + ((byte*)area3.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 2]; + + // Channel 4 + ((byte*)area4.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 3]; + + // Channel 5 + ((byte*)area5.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 4]; + + // Channel 6 + ((byte*)area6.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; + + // Channel 2 + ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; + + // Channel 3 + ((short*)area3.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 2]; + + // Channel 4 + ((short*)area4.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 3]; + + // Channel 5 + ((short*)area5.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 4]; + + // Channel 6 + ((short*)area6.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; + + // Channel 2 + ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; + + // Channel 3 + ((int*)area3.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 2]; + + // Channel 4 + ((int*)area4.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 3]; + + // Channel 5 + ((int*)area5.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 4]; + + // Channel 6 + ((int*)area6.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); + + // Channel 2 + Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); + + // Channel 3 + Unsafe.CopyBlockUnaligned((byte*)area3.Pointer, srcptr + (frame * bytesPerFrame) + (2 * bytesPerSample), bytesPerSample); + + // Channel 4 + Unsafe.CopyBlockUnaligned((byte*)area4.Pointer, srcptr + (frame * bytesPerFrame) + (3 * bytesPerSample), bytesPerSample); + + // Channel 5 + Unsafe.CopyBlockUnaligned((byte*)area5.Pointer, srcptr + (frame * bytesPerFrame) + (4 * bytesPerSample), bytesPerSample); + + // Channel 6 + Unsafe.CopyBlockUnaligned((byte*)area6.Pointer, srcptr + (frame * bytesPerFrame) + (5 * bytesPerSample), bytesPerSample); + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + } + } + // Every other channel count + else + { + fixed (byte* srcptr = samples) + { + for (int frame = 0; frame < frameCount; frame++) + { + for (int channel = 0; channel < areas.Length; channel++) + { + // Copy channel by channel, frame by frame. This is slow! + Unsafe.CopyBlockUnaligned((byte*)areas[channel].Pointer, srcptr + (frame * bytesPerFrame) + (channel * bytesPerSample), bytesPerSample); + + areas[channel].Pointer += areas[channel].Step; + } + } + } + } + + _outputStream.EndWrite(); + + ulong sampleCount = (ulong)(samples.Length / bytesPerSample / channelCount); + + ulong availaibleSampleCount = sampleCount; + + bool needUpdate = false; + + while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer)) + { + ulong sampleStillNeeded = driverBuffer.SampleCount - Interlocked.Read(ref driverBuffer.SamplePlayed); + ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount); + + Interlocked.Add(ref driverBuffer.SamplePlayed, playedAudioBufferSampleCount); + availaibleSampleCount -= playedAudioBufferSampleCount; + + if (Interlocked.Read(ref driverBuffer.SamplePlayed) == driverBuffer.SampleCount) + { + _queuedBuffers.TryDequeue(out _); + + needUpdate = true; + } + + Interlocked.Add(ref _playedSampleCount, playedAudioBufferSampleCount); + } + + // Notify the output if needed. + if (needUpdate) + { + _updateRequiredEvent.Set(); + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && _driver.Unregister(this)) + { + PrepareToClose(); + Stop(); + + _outputStream.Dispose(); + } + } + + public override void Dispose() + { + if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0) + { + Dispose(true); + } + } + } +} |
