aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Horizon/Sdk
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Horizon/Sdk')
-rw-r--r--src/Ryujinx.Horizon/Sdk/Account/Uid.cs2
-rw-r--r--src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs71
-rw-r--r--src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs15
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs50
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs12
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs252
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs171
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs130
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs23
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs8
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs154
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs93
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs187
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs132
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs14
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs30
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs30
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs147
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs23
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs17
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs21
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs24
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs26
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs43
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs25
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs32
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs24
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs29
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs12
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs22
-rw-r--r--src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs16
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs16
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs336
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs386
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs11
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs13
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs15
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs17
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs20
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs19
-rw-r--r--src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs11
41 files changed, 2678 insertions, 1 deletions
diff --git a/src/Ryujinx.Horizon/Sdk/Account/Uid.cs b/src/Ryujinx.Horizon/Sdk/Account/Uid.cs
index ada2c02b..d612f479 100644
--- a/src/Ryujinx.Horizon/Sdk/Account/Uid.cs
+++ b/src/Ryujinx.Horizon/Sdk/Account/Uid.cs
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Horizon.Sdk.Account
{
- [StructLayout(LayoutKind.Sequential)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x8)]
public readonly record struct Uid
{
public readonly ulong High;
diff --git a/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs b/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs
new file mode 100644
index 00000000..2b81fbf6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Applet/AppletId.cs
@@ -0,0 +1,71 @@
+namespace Ryujinx.Horizon.Sdk.Applet
+{
+ enum AppletId : uint
+ {
+ None = 0x00,
+ Application = 0x01,
+ OverlayApplet = 0x02,
+ SystemAppletMenu = 0x03,
+ SystemApplication = 0x04,
+ LibraryAppletAuth = 0x0A,
+ LibraryAppletCabinet = 0x0B,
+ LibraryAppletController = 0x0C,
+ LibraryAppletDataErase = 0x0D,
+ LibraryAppletError = 0x0E,
+ LibraryAppletNetConnect = 0x0F,
+ LibraryAppletPlayerSelect = 0x10,
+ LibraryAppletSwkbd = 0x11,
+ LibraryAppletMiiEdit = 0x12,
+ LibraryAppletWeb = 0x13,
+ LibraryAppletShop = 0x14,
+ LibraryAppletPhotoViewer = 0x15,
+ LibraryAppletSet = 0x16,
+ LibraryAppletOfflineWeb = 0x17,
+ LibraryAppletLoginShare = 0x18,
+ LibraryAppletWifiWebAuth = 0x19,
+ LibraryAppletMyPage = 0x1A,
+ LibraryAppletGift = 0x1B,
+ LibraryAppletUserMigration = 0x1C,
+ LibraryAppletPreomiaSys = 0x1D,
+ LibraryAppletStory = 0x1E,
+ LibraryAppletPreomiaUsr = 0x1F,
+ LibraryAppletPreomiaUsrDummy = 0x20,
+ LibraryAppletSample = 0x21,
+ LibraryAppletPromoteQualification = 0x22,
+ LibraryAppletOfflineWebFw17 = 0x32,
+ LibraryAppletOfflineWeb2Fw17 = 0x33,
+ LibraryAppletLoginShareFw17 = 0x35,
+ LibraryAppletLoginShare2Fw17 = 0x36,
+ LibraryAppletLoginShare3Fw17 = 0x37,
+ Unknown38 = 0x38,
+ DevlopmentTool = 0x3E8,
+ CombinationLA = 0x3F1,
+ AeSystemApplet = 0x3F2,
+ AeOverlayApplet = 0x3F3,
+ AeStarter = 0x3F4,
+ AeLibraryAppletAlone = 0x3F5,
+ AeLibraryApplet1 = 0x3F6,
+ AeLibraryApplet2 = 0x3F7,
+ AeLibraryApplet3 = 0x3F8,
+ AeLibraryApplet4 = 0x3F9,
+ AppletISA = 0x3FA,
+ AppletIOA = 0x3FB,
+ AppletISTA = 0x3FC,
+ AppletILA1 = 0x3FD,
+ AppletILA2 = 0x3FE,
+ CombinationLAFw17 = 0x700000DC,
+ AeSystemAppletFw17 = 0x700000E6,
+ AeOverlayAppletFw17 = 0x700000E7,
+ AeStarterFw17 = 0x700000E8,
+ AeLibraryAppletAloneFw17 = 0x700000E9,
+ AeLibraryApplet1Fw17 = 0x700000EA,
+ AeLibraryApplet2Fw17 = 0x700000EB,
+ AeLibraryApplet3Fw17 = 0x700000EC,
+ AeLibraryApplet4Fw17 = 0x700000ED,
+ AppletISAFw17 = 0x700000F0,
+ AppletIOAFw17 = 0x700000F1,
+ AppletISTAFw17 = 0x700000F2,
+ AppletILA1Fw17 = 0x700000F3,
+ AppletILA2Fw17 = 0x700000F4,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs b/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs
new file mode 100644
index 00000000..00e2ad36
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Applet/AppletResourceUserId.cs
@@ -0,0 +1,15 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Applet
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x8)]
+ readonly struct AppletResourceUserId
+ {
+ public readonly ulong Id;
+
+ public AppletResourceUserId(ulong id)
+ {
+ Id = id;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs b/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs
new file mode 100644
index 00000000..efa8d5bc
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/AudioEvent.cs
@@ -0,0 +1,50 @@
+using Ryujinx.Audio.Integration;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio
+{
+ class AudioEvent : IWritableEvent, IDisposable
+ {
+ private SystemEventType _systemEvent;
+ private readonly IExternalEvent _externalEvent;
+
+ public AudioEvent()
+ {
+ Os.CreateSystemEvent(out _systemEvent, EventClearMode.ManualClear, interProcess: true);
+
+ // We need to do this because the event will be signalled from a different thread.
+ _externalEvent = HorizonStatic.Syscall.GetExternalEvent(Os.GetWritableHandleOfSystemEvent(ref _systemEvent));
+ }
+
+ public void Signal()
+ {
+ _externalEvent.Signal();
+ }
+
+ public void Clear()
+ {
+ _externalEvent.Clear();
+ }
+
+ public int GetReadableHandle()
+ {
+ return Os.GetReadableHandleOfSystemEvent(ref _systemEvent);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Os.DestroySystemEvent(ref _systemEvent);
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs b/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs
new file mode 100644
index 00000000..c18bfee9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/AudioResult.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Audio
+{
+ static class AudioResult
+ {
+ private const int ModuleId = 153;
+
+ public static Result DeviceNotFound => new(ModuleId, 1);
+ public static Result UnsupportedRevision => new(ModuleId, 2);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs
new file mode 100644
index 00000000..f67ea729
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioDevice.cs
@@ -0,0 +1,252 @@
+using Ryujinx.Audio.Renderer.Device;
+using Ryujinx.Audio.Renderer.Server;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioDevice : IAudioDevice, IDisposable
+ {
+ private readonly VirtualDeviceSessionRegistry _registry;
+ private readonly VirtualDeviceSession[] _sessions;
+ private readonly bool _isUsbDeviceSupported;
+
+ private SystemEventType _audioEvent;
+ private SystemEventType _audioInputEvent;
+ private SystemEventType _audioOutputEvent;
+
+ public AudioDevice(VirtualDeviceSessionRegistry registry, AppletResourceUserId appletResourceId, uint revision)
+ {
+ _registry = registry;
+
+ BehaviourContext behaviourContext = new();
+ behaviourContext.SetUserRevision((int)revision);
+
+ _isUsbDeviceSupported = behaviourContext.IsAudioUsbDeviceOutputSupported();
+ _sessions = registry.GetSessionByAppletResourceId(appletResourceId.Id);
+
+ Os.CreateSystemEvent(out _audioEvent, EventClearMode.AutoClear, interProcess: true);
+ Os.CreateSystemEvent(out _audioInputEvent, EventClearMode.AutoClear, interProcess: true);
+ Os.CreateSystemEvent(out _audioOutputEvent, EventClearMode.AutoClear, interProcess: true);
+ }
+
+ private bool TryGetDeviceByName(out VirtualDeviceSession result, string name, bool ignoreRevLimitation = false)
+ {
+ result = null;
+
+ foreach (VirtualDeviceSession session in _sessions)
+ {
+ if (session.Device.Name.Equals(name))
+ {
+ if (!ignoreRevLimitation && !_isUsbDeviceSupported && session.Device.IsUsbDevice())
+ {
+ return false;
+ }
+
+ result = session;
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ [CmifCommand(0)]
+ public Result ListAudioDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> names, out int nameCount)
+ {
+ int count = 0;
+
+ foreach (VirtualDeviceSession session in _sessions)
+ {
+ if (!_isUsbDeviceSupported && session.Device.IsUsbDevice())
+ {
+ continue;
+ }
+
+ if (count >= names.Length)
+ {
+ break;
+ }
+
+ names[count] = new DeviceName(session.Device.Name);
+
+ count++;
+ }
+
+ nameCount = count;
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result SetAudioDeviceOutputVolume([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<DeviceName> name, float volume)
+ {
+ if (name.Length > 0 && TryGetDeviceByName(out VirtualDeviceSession result, name[0].ToString(), ignoreRevLimitation: true))
+ {
+ if (!_isUsbDeviceSupported && result.Device.IsUsbDevice())
+ {
+ result = _sessions[0];
+ }
+
+ result.Volume = volume;
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(2)]
+ public Result GetAudioDeviceOutputVolume([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<DeviceName> name, out float volume)
+ {
+ if (name.Length > 0 && TryGetDeviceByName(out VirtualDeviceSession result, name[0].ToString()))
+ {
+ volume = result.Volume;
+ }
+ else
+ {
+ volume = 0f;
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(3)]
+ public Result GetActiveAudioDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> name)
+ {
+ VirtualDevice device = _registry.ActiveDevice;
+
+ if (!_isUsbDeviceSupported && device.IsUsbDevice())
+ {
+ device = _registry.DefaultDevice;
+ }
+
+ if (name.Length > 0)
+ {
+ name[0] = new DeviceName(device.Name);
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(4)]
+ public Result QueryAudioDeviceSystemEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioEvent);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)]
+ public Result GetActiveChannelCount(out int channelCount)
+ {
+ VirtualDevice device = _registry.ActiveDevice;
+
+ if (!_isUsbDeviceSupported && device.IsUsbDevice())
+ {
+ device = _registry.DefaultDevice;
+ }
+
+ channelCount = (int)device.ChannelCount;
+
+ return Result.Success;
+ }
+
+ [CmifCommand(6)] // 3.0.0+
+ public Result ListAudioDeviceNameAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> names, out int nameCount)
+ {
+ return ListAudioDeviceName(names, out nameCount);
+ }
+
+ [CmifCommand(7)] // 3.0.0+
+ public Result SetAudioDeviceOutputVolumeAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<DeviceName> name, float volume)
+ {
+ return SetAudioDeviceOutputVolume(name, volume);
+ }
+
+ [CmifCommand(8)] // 3.0.0+
+ public Result GetAudioDeviceOutputVolumeAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<DeviceName> name, out float volume)
+ {
+ return GetAudioDeviceOutputVolume(name, out volume);
+ }
+
+ [CmifCommand(10)] // 3.0.0+
+ public Result GetActiveAudioDeviceNameAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> name)
+ {
+ return GetActiveAudioDeviceName(name);
+ }
+
+ [CmifCommand(11)] // 3.0.0+
+ public Result QueryAudioDeviceInputEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioInputEvent);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(12)] // 3.0.0+
+ public Result QueryAudioDeviceOutputEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = Os.GetReadableHandleOfSystemEvent(ref _audioOutputEvent);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(13)] // 13.0.0+
+ public Result GetActiveAudioOutputDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> name)
+ {
+ if (name.Length > 0)
+ {
+ name[0] = new DeviceName(_registry.ActiveDevice.GetOutputDeviceName());
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(14)] // 13.0.0+
+ public Result ListAudioOutputDeviceName([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> names, out int nameCount)
+ {
+ int count = 0;
+
+ foreach (VirtualDeviceSession session in _sessions)
+ {
+ if (!_isUsbDeviceSupported && session.Device.IsUsbDevice())
+ {
+ continue;
+ }
+
+ if (count >= names.Length)
+ {
+ break;
+ }
+
+ names[count] = new DeviceName(session.Device.GetOutputDeviceName());
+
+ count++;
+ }
+
+ nameCount = count;
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Os.DestroySystemEvent(ref _audioEvent);
+ Os.DestroySystemEvent(ref _audioInputEvent);
+ Os.DestroySystemEvent(ref _audioOutputEvent);
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs
new file mode 100644
index 00000000..464ede58
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioIn.cs
@@ -0,0 +1,171 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Audio.Input;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioIn : IAudioIn, IDisposable
+ {
+ private readonly AudioInputSystem _impl;
+ private int _processHandle;
+
+ public AudioIn(AudioInputSystem impl, int processHandle)
+ {
+ _impl = impl;
+ _processHandle = processHandle;
+ }
+
+ [CmifCommand(0)]
+ public Result GetAudioInState(out AudioDeviceState state)
+ {
+ state = _impl.GetState();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result Start()
+ {
+ return new Result((int)_impl.Start());
+ }
+
+ [CmifCommand(2)]
+ public Result Stop()
+ {
+ return new Result((int)_impl.Stop());
+ }
+
+ [CmifCommand(3)]
+ public Result AppendAudioInBuffer(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<AudioUserBuffer> buffer)
+ {
+ AudioUserBuffer userBuffer = default;
+
+ if (buffer.Length > 0)
+ {
+ userBuffer = buffer[0];
+ }
+
+ return new Result((int)_impl.AppendBuffer(bufferTag, ref userBuffer));
+ }
+
+ [CmifCommand(4)]
+ public Result RegisterBufferEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = 0;
+
+ if (_impl.RegisterBufferEvent() is AudioEvent audioEvent)
+ {
+ eventHandle = audioEvent.GetReadableHandle();
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)]
+ public Result GetReleasedAudioInBuffers(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<ulong> bufferTags)
+ {
+ return new Result((int)_impl.GetReleasedBuffers(bufferTags, out count));
+ }
+
+ [CmifCommand(6)]
+ public Result ContainsAudioInBuffer(out bool contains, ulong bufferTag)
+ {
+ contains = _impl.ContainsBuffer(bufferTag);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(7)] // 3.0.0+
+ public Result AppendUacInBuffer(
+ ulong bufferTag,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<AudioUserBuffer> buffer,
+ [CopyHandle] int eventHandle)
+ {
+ AudioUserBuffer userBuffer = default;
+
+ if (buffer.Length > 0)
+ {
+ userBuffer = buffer[0];
+ }
+
+ return new Result((int)_impl.AppendUacBuffer(bufferTag, ref userBuffer, (uint)eventHandle));
+ }
+
+ [CmifCommand(8)] // 3.0.0+
+ public Result AppendAudioInBufferAuto(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<AudioUserBuffer> buffer)
+ {
+ return AppendAudioInBuffer(bufferTag, buffer);
+ }
+
+ [CmifCommand(9)] // 3.0.0+
+ public Result GetReleasedAudioInBuffersAuto(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<ulong> bufferTags)
+ {
+ return GetReleasedAudioInBuffers(out count, bufferTags);
+ }
+
+ [CmifCommand(10)] // 3.0.0+
+ public Result AppendUacInBufferAuto(
+ ulong bufferTag,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<AudioUserBuffer> buffer,
+ [CopyHandle] int eventHandle)
+ {
+ return AppendUacInBuffer(bufferTag, buffer, eventHandle);
+ }
+
+ [CmifCommand(11)] // 4.0.0+
+ public Result GetAudioInBufferCount(out uint bufferCount)
+ {
+ bufferCount = _impl.GetBufferCount();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(12)] // 4.0.0+
+ public Result SetDeviceGain(float gain)
+ {
+ _impl.SetVolume(gain);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(13)] // 4.0.0+
+ public Result GetDeviceGain(out float gain)
+ {
+ gain = _impl.GetVolume();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(14)] // 6.0.0+
+ public Result FlushAudioInBuffers(out bool pending)
+ {
+ pending = _impl.FlushBuffers();
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _impl.Dispose();
+
+ if (_processHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_processHandle);
+
+ _processHandle = 0;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs
new file mode 100644
index 00000000..d5d04720
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInManager.cs
@@ -0,0 +1,130 @@
+using Ryujinx.Audio;
+using Ryujinx.Audio.Common;
+using Ryujinx.Audio.Input;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioInManager : IAudioInManager
+ {
+ private readonly AudioInputManager _impl;
+
+ public AudioInManager(AudioInputManager impl)
+ {
+ _impl = impl;
+ }
+
+ [CmifCommand(0)]
+ public Result ListAudioIns(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> names)
+ {
+ string[] deviceNames = _impl.ListAudioIns(filtered: false);
+
+ count = 0;
+
+ foreach (string deviceName in deviceNames)
+ {
+ if (count >= names.Length)
+ {
+ break;
+ }
+
+ names[count++] = new DeviceName(deviceName);
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result OpenAudioIn(
+ out AudioOutputConfiguration outputConfiguration,
+ out IAudioIn audioIn,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ [CopyHandle] int processHandle,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<DeviceName> name,
+ [ClientProcessId] ulong pid)
+ {
+ var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle);
+
+ ResultCode rc = _impl.OpenAudioIn(
+ out string outputDeviceName,
+ out outputConfiguration,
+ out AudioInputSystem inSystem,
+ clientMemoryManager,
+ name.Length > 0 ? name[0].ToString() : string.Empty,
+ SampleFormat.PcmInt16,
+ ref parameter);
+
+ if (rc == ResultCode.Success && outName.Length > 0)
+ {
+ outName[0] = new DeviceName(outputDeviceName);
+ }
+
+ audioIn = new AudioIn(inSystem, processHandle);
+
+ return new Result((int)rc);
+ }
+
+ [CmifCommand(2)] // 3.0.0+
+ public Result ListAudioInsAuto(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> names)
+ {
+ return ListAudioIns(out count, names);
+ }
+
+ [CmifCommand(3)] // 3.0.0+
+ public Result OpenAudioInAuto(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioIn audioIn,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ [CopyHandle] int processHandle,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<DeviceName> name,
+ [ClientProcessId] ulong pid)
+ {
+ return OpenAudioIn(out outputConfig, out audioIn, outName, parameter, appletResourceId, processHandle, name, pid);
+ }
+
+ [CmifCommand(4)] // 3.0.0+
+ public Result ListAudioInsAutoFiltered(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> names)
+ {
+ string[] deviceNames = _impl.ListAudioIns(filtered: true);
+
+ count = 0;
+
+ foreach (string deviceName in deviceNames)
+ {
+ if (count >= names.Length)
+ {
+ break;
+ }
+
+ names[count++] = new DeviceName(deviceName);
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)] // 5.0.0+
+ public Result OpenAudioInProtocolSpecified(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioIn audioIn,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> outName,
+ AudioInProtocol protocol,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ [CopyHandle] int processHandle,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<DeviceName> name,
+ [ClientProcessId] ulong pid)
+ {
+ // NOTE: We always assume that only the default device will be plugged (we never report any USB Audio Class type devices).
+
+ return OpenAudioIn(out outputConfig, out audioIn, outName, parameter, appletResourceId, processHandle, name, pid);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs
new file mode 100644
index 00000000..48785f1c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocol.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x1)]
+ struct AudioInProtocol
+ {
+ public AudioInProtocolName Name;
+ public Array7<byte> Padding;
+
+ public AudioInProtocol(AudioInProtocolName name)
+ {
+ Name = name;
+ Padding = new();
+ }
+
+ public override readonly string ToString()
+ {
+ return Name.ToString();
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs
new file mode 100644
index 00000000..68d283cc
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioInProtocolName.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ enum AudioInProtocolName : byte
+ {
+ DeviceIn = 0,
+ UacIn = 1,
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs
new file mode 100644
index 00000000..7607e264
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOut.cs
@@ -0,0 +1,154 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Audio.Output;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioOut : IAudioOut, IDisposable
+ {
+ private readonly AudioOutputSystem _impl;
+ private int _processHandle;
+
+ public AudioOut(AudioOutputSystem impl, int processHandle)
+ {
+ _impl = impl;
+ _processHandle = processHandle;
+ }
+
+ [CmifCommand(0)]
+ public Result GetAudioOutState(out AudioDeviceState state)
+ {
+ state = _impl.GetState();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result Start()
+ {
+ return new Result((int)_impl.Start());
+ }
+
+ [CmifCommand(2)]
+ public Result Stop()
+ {
+ return new Result((int)_impl.Stop());
+ }
+
+ [CmifCommand(3)]
+ public Result AppendAudioOutBuffer(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<AudioUserBuffer> buffer)
+ {
+ AudioUserBuffer userBuffer = default;
+
+ if (buffer.Length > 0)
+ {
+ userBuffer = buffer[0];
+ }
+
+ return new Result((int)_impl.AppendBuffer(bufferTag, ref userBuffer));
+ }
+
+ [CmifCommand(4)]
+ public Result RegisterBufferEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = 0;
+
+ if (_impl.RegisterBufferEvent() is AudioEvent audioEvent)
+ {
+ eventHandle = audioEvent.GetReadableHandle();
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)]
+ public Result GetReleasedAudioOutBuffers(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<ulong> bufferTags)
+ {
+ return new Result((int)_impl.GetReleasedBuffer(bufferTags, out count));
+ }
+
+ [CmifCommand(6)]
+ public Result ContainsAudioOutBuffer(out bool contains, ulong bufferTag)
+ {
+ contains = _impl.ContainsBuffer(bufferTag);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(7)] // 3.0.0+
+ public Result AppendAudioOutBufferAuto(ulong bufferTag, [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<AudioUserBuffer> buffer)
+ {
+ return AppendAudioOutBuffer(bufferTag, buffer);
+ }
+
+ [CmifCommand(8)] // 3.0.0+
+ public Result GetReleasedAudioOutBuffersAuto(out uint count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<ulong> bufferTags)
+ {
+ return GetReleasedAudioOutBuffers(out count, bufferTags);
+ }
+
+ [CmifCommand(9)] // 4.0.0+
+ public Result GetAudioOutBufferCount(out uint bufferCount)
+ {
+ bufferCount = _impl.GetBufferCount();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(10)] // 4.0.0+
+ public Result GetAudioOutPlayedSampleCount(out ulong sampleCount)
+ {
+ sampleCount = _impl.GetPlayedSampleCount();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(11)] // 4.0.0+
+ public Result FlushAudioOutBuffers(out bool pending)
+ {
+ pending = _impl.FlushBuffers();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(12)] // 6.0.0+
+ public Result SetAudioOutVolume(float volume)
+ {
+ _impl.SetVolume(volume);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(13)] // 6.0.0+
+ public Result GetAudioOutVolume(out float volume)
+ {
+ volume = _impl.GetVolume();
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _impl.Dispose();
+
+ if (_processHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_processHandle);
+
+ _processHandle = 0;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs
new file mode 100644
index 00000000..3d129470
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioOutManager.cs
@@ -0,0 +1,93 @@
+using Ryujinx.Audio;
+using Ryujinx.Audio.Common;
+using Ryujinx.Audio.Output;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioOutManager : IAudioOutManager
+ {
+ private readonly AudioOutputManager _impl;
+
+ public AudioOutManager(AudioOutputManager impl)
+ {
+ _impl = impl;
+ }
+
+ [CmifCommand(0)]
+ public Result ListAudioOuts(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> names)
+ {
+ string[] deviceNames = _impl.ListAudioOuts();
+
+ count = 0;
+
+ foreach (string deviceName in deviceNames)
+ {
+ if (count >= names.Length)
+ {
+ break;
+ }
+
+ names[count++] = new DeviceName(deviceName);
+ }
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result OpenAudioOut(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioOut audioOut,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ [CopyHandle] int processHandle,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<DeviceName> name,
+ [ClientProcessId] ulong pid)
+ {
+ var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle);
+
+ ResultCode rc = _impl.OpenAudioOut(
+ out string outputDeviceName,
+ out outputConfig,
+ out AudioOutputSystem outSystem,
+ clientMemoryManager,
+ name.Length > 0 ? name[0].ToString() : string.Empty,
+ SampleFormat.PcmInt16,
+ ref parameter);
+
+ if (rc == ResultCode.Success && outName.Length > 0)
+ {
+ outName[0] = new DeviceName(outputDeviceName);
+ }
+
+ audioOut = new AudioOut(outSystem, processHandle);
+
+ return new Result((int)rc);
+ }
+
+ [CmifCommand(2)] // 3.0.0+
+ public Result ListAudioOutsAuto(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> names)
+ {
+ return ListAudioOuts(out count, names);
+ }
+
+ [CmifCommand(3)] // 3.0.0+
+ public Result OpenAudioOutAuto(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioOut audioOut,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ [CopyHandle] int processHandle,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<DeviceName> name,
+ [ClientProcessId] ulong pid)
+ {
+ return OpenAudioOut(out outputConfig, out audioOut, outName, parameter, appletResourceId, processHandle, name, pid);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs
new file mode 100644
index 00000000..776df641
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRenderer.cs
@@ -0,0 +1,187 @@
+using Ryujinx.Audio;
+using Ryujinx.Audio.Integration;
+using Ryujinx.Audio.Renderer.Server;
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+using System.Buffers;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioRenderer : IAudioRenderer, IDisposable
+ {
+ private readonly AudioRenderSystem _renderSystem;
+ private int _workBufferHandle;
+ private int _processHandle;
+
+ public AudioRenderer(AudioRenderSystem renderSystem, int workBufferHandle, int processHandle)
+ {
+ _renderSystem = renderSystem;
+ _workBufferHandle = workBufferHandle;
+ _processHandle = processHandle;
+ }
+
+ [CmifCommand(0)]
+ public Result GetSampleRate(out int sampleRate)
+ {
+ sampleRate = (int)_renderSystem.GetSampleRate();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result GetSampleCount(out int sampleCount)
+ {
+ sampleCount = (int)_renderSystem.GetSampleCount();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(2)]
+ public Result GetMixBufferCount(out int mixBufferCount)
+ {
+ mixBufferCount = (int)_renderSystem.GetMixBufferCount();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(3)]
+ public Result GetState(out int state)
+ {
+ state = _renderSystem.IsActive() ? 0 : 1;
+
+ return Result.Success;
+ }
+
+ [CmifCommand(4)]
+ public Result RequestUpdate(
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> output,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> performanceOutput,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+ {
+ using IMemoryOwner<byte> outputOwner = ByteMemoryPool.Rent(output.Length);
+ using IMemoryOwner<byte> performanceOutputOwner = ByteMemoryPool.Rent(performanceOutput.Length);
+
+ Memory<byte> outputMemory = outputOwner.Memory;
+ Memory<byte> performanceOutputMemory = performanceOutputOwner.Memory;
+
+ using MemoryHandle outputHandle = outputMemory.Pin();
+ using MemoryHandle performanceOutputHandle = performanceOutputMemory.Pin();
+
+ Result result = new Result((int)_renderSystem.Update(outputMemory, performanceOutputMemory, input.ToArray()));
+
+ outputMemory.Span.CopyTo(output);
+ performanceOutputMemory.Span.CopyTo(performanceOutput);
+
+ return result;
+ }
+
+ [CmifCommand(5)]
+ public Result Start()
+ {
+ _renderSystem.Start();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(6)]
+ public Result Stop()
+ {
+ _renderSystem.Stop();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(7)]
+ public Result QuerySystemEvent([CopyHandle] out int eventHandle)
+ {
+ ResultCode rc = _renderSystem.QuerySystemEvent(out IWritableEvent systemEvent);
+
+ eventHandle = 0;
+
+ if (rc == ResultCode.Success && systemEvent is AudioEvent audioEvent)
+ {
+ eventHandle = audioEvent.GetReadableHandle();
+ }
+
+ return new Result((int)rc);
+ }
+
+ [CmifCommand(8)]
+ public Result SetRenderingTimeLimit(int percent)
+ {
+ _renderSystem.SetRenderingTimeLimitPercent((uint)percent);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(9)]
+ public Result GetRenderingTimeLimit(out int percent)
+ {
+ percent = (int)_renderSystem.GetRenderingTimeLimit();
+
+ return Result.Success;
+ }
+
+ [CmifCommand(10)] // 3.0.0+
+ public Result RequestUpdateAuto(
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<byte> output,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<byte> performanceOutput,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<byte> input)
+ {
+ return RequestUpdate(output, performanceOutput, input);
+ }
+
+ [CmifCommand(11)] // 3.0.0+
+ public Result ExecuteAudioRendererRendering()
+ {
+ return new Result((int)_renderSystem.ExecuteAudioRendererRendering());
+ }
+
+ [CmifCommand(12)] // 15.0.0+
+ public Result SetVoiceDropParameter(float voiceDropParameter)
+ {
+ _renderSystem.SetVoiceDropParameter(voiceDropParameter);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(13)] // 15.0.0+
+ public Result GetVoiceDropParameter(out float voiceDropParameter)
+ {
+ voiceDropParameter = _renderSystem.GetVoiceDropParameter();
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _renderSystem.Dispose();
+
+ if (_workBufferHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_workBufferHandle);
+
+ _workBufferHandle = 0;
+ }
+
+ if (_processHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_processHandle);
+
+ _processHandle = 0;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs
new file mode 100644
index 00000000..7138d27c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererManager.cs
@@ -0,0 +1,132 @@
+using Ryujinx.Audio.Renderer.Device;
+using Ryujinx.Audio.Renderer.Server;
+using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioRendererManager : IAudioRendererManager
+ {
+ private const uint InitialRevision = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('1' << 24);
+
+ private readonly Ryujinx.Audio.Renderer.Server.AudioRendererManager _impl;
+ private readonly VirtualDeviceSessionRegistry _registry;
+
+ public AudioRendererManager(Ryujinx.Audio.Renderer.Server.AudioRendererManager impl, VirtualDeviceSessionRegistry registry)
+ {
+ _impl = impl;
+ _registry = registry;
+ }
+
+ [CmifCommand(0)]
+ public Result OpenAudioRenderer(
+ out IAudioRenderer renderer,
+ AudioRendererParameterInternal parameter,
+ [CopyHandle] int workBufferHandle,
+ [CopyHandle] int processHandle,
+ ulong workBufferSize,
+ AppletResourceUserId appletResourceId,
+ [ClientProcessId] ulong pid)
+ {
+ var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle);
+ ulong workBufferAddress = HorizonStatic.Syscall.GetTransferMemoryAddress(workBufferHandle);
+
+ Result result = new Result((int)_impl.OpenAudioRenderer(
+ out var renderSystem,
+ clientMemoryManager,
+ ref parameter.Configuration,
+ appletResourceId.Id,
+ workBufferAddress,
+ workBufferSize,
+ (uint)processHandle));
+
+ if (result.IsSuccess)
+ {
+ renderer = new AudioRenderer(renderSystem, workBufferHandle, processHandle);
+ }
+ else
+ {
+ renderer = null;
+
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+ HorizonStatic.Syscall.CloseHandle(processHandle);
+ }
+
+ return result;
+ }
+
+ [CmifCommand(1)]
+ public Result GetWorkBufferSize(out long workBufferSize, AudioRendererParameterInternal parameter)
+ {
+ if (BehaviourContext.CheckValidRevision(parameter.Configuration.Revision))
+ {
+ workBufferSize = (long)Ryujinx.Audio.Renderer.Server.AudioRendererManager.GetWorkBufferSize(ref parameter.Configuration);
+
+ Logger.Debug?.Print(LogClass.ServiceAudio, $"WorkBufferSize is 0x{workBufferSize:x16}.");
+
+ return Result.Success;
+ }
+ else
+ {
+ workBufferSize = 0;
+
+ Logger.Warning?.Print(LogClass.ServiceAudio, $"Library Revision REV{BehaviourContext.GetRevisionNumber(parameter.Configuration.Revision)} is not supported!");
+
+ return AudioResult.UnsupportedRevision;
+ }
+ }
+
+ [CmifCommand(2)]
+ public Result GetAudioDeviceService(out IAudioDevice audioDevice, AppletResourceUserId appletResourceId)
+ {
+ audioDevice = new AudioDevice(_registry, appletResourceId, InitialRevision);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(3)] // 3.0.0+
+ public Result OpenAudioRendererForManualExecution(
+ out IAudioRenderer renderer,
+ AudioRendererParameterInternal parameter,
+ ulong workBufferAddress,
+ [CopyHandle] int processHandle,
+ ulong workBufferSize,
+ AppletResourceUserId appletResourceId,
+ [ClientProcessId] ulong pid)
+ {
+ var clientMemoryManager = HorizonStatic.Syscall.GetMemoryManagerByProcessHandle(processHandle);
+
+ Result result = new Result((int)_impl.OpenAudioRenderer(
+ out var renderSystem,
+ clientMemoryManager,
+ ref parameter.Configuration,
+ appletResourceId.Id,
+ workBufferAddress,
+ workBufferSize,
+ (uint)processHandle));
+
+ if (result.IsSuccess)
+ {
+ renderer = new AudioRenderer(renderSystem, 0, processHandle);
+ }
+ else
+ {
+ renderer = null;
+
+ HorizonStatic.Syscall.CloseHandle(processHandle);
+ }
+
+ return result;
+ }
+
+ [CmifCommand(4)] // 4.0.0+
+ public Result GetAudioDeviceServiceWithRevisionInfo(out IAudioDevice audioDevice, AppletResourceUserId appletResourceId, uint revision)
+ {
+ audioDevice = new AudioDevice(_registry, appletResourceId, revision);
+
+ return Result.Success;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs
new file mode 100644
index 00000000..e5fcf7b3
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioRendererParameterInternal.cs
@@ -0,0 +1,14 @@
+using Ryujinx.Audio.Renderer.Parameter;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ struct AudioRendererParameterInternal
+ {
+ public AudioRendererConfiguration Configuration;
+
+ public AudioRendererParameterInternal(AudioRendererConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs
new file mode 100644
index 00000000..cf1fe3d1
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/AudioSnoopManager.cs
@@ -0,0 +1,30 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class AudioSnoopManager : IAudioSnoopManager
+ {
+ // Note: The interface changed completely on firmware 17.0.0, this implementation is for older firmware.
+
+ [CmifCommand(0)]
+ public Result EnableDspUsageMeasurement()
+ {
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result DisableDspUsageMeasurement()
+ {
+ return Result.Success;
+ }
+
+ [CmifCommand(6)]
+ public Result GetDspUsage(out uint usage)
+ {
+ usage = 0;
+
+ return Result.Success;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs
new file mode 100644
index 00000000..b77e2f40
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/DeviceName.cs
@@ -0,0 +1,30 @@
+using Ryujinx.Common.Memory;
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x100, Pack = 1)]
+ struct DeviceName
+ {
+ public Array256<byte> Name;
+
+ public DeviceName(string name)
+ {
+ Name = new();
+ Encoding.ASCII.GetBytes(name, Name.AsSpan());
+ }
+
+ public override string ToString()
+ {
+ int length = Name.AsSpan().IndexOf((byte)0);
+ if (length < 0)
+ {
+ length = 0x100;
+ }
+
+ return Encoding.ASCII.GetString(Name.AsSpan()[..length]);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs
new file mode 100644
index 00000000..39391437
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorder.cs
@@ -0,0 +1,147 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class FinalOutputRecorder : IFinalOutputRecorder, IDisposable
+ {
+ private int _processHandle;
+ private SystemEventType _event;
+
+ public FinalOutputRecorder(int processHandle)
+ {
+ _processHandle = processHandle;
+ Os.CreateSystemEvent(out _event, EventClearMode.ManualClear, interProcess: true);
+ }
+
+ [CmifCommand(0)]
+ public Result GetFinalOutputRecorderState(out uint state)
+ {
+ state = 0;
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result Start()
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(2)]
+ public Result Stop()
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(3)]
+ public Result AppendFinalOutputRecorderBuffer([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> buffer, ulong bufferClientPtr)
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferClientPtr });
+
+ return Result.Success;
+ }
+
+ [CmifCommand(4)]
+ public Result RegisterBufferEvent([CopyHandle] out int eventHandle)
+ {
+ eventHandle = Os.GetReadableHandleOfSystemEvent(ref _event);
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)]
+ public Result GetReleasedFinalOutputRecorderBuffers([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> buffer, out uint count, out ulong released)
+ {
+ count = 0;
+ released = 0;
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(6)]
+ public Result ContainsFinalOutputRecorderBuffer(ulong bufferPointer, out bool contains)
+ {
+ contains = false;
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferPointer });
+
+ return Result.Success;
+ }
+
+ [CmifCommand(7)]
+ public Result GetFinalOutputRecorderBufferEndTime(ulong bufferPointer, out ulong released)
+ {
+ released = 0;
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { bufferPointer });
+
+ return Result.Success;
+ }
+
+ [CmifCommand(8)] // 3.0.0+
+ public Result AppendFinalOutputRecorderBufferAuto([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ReadOnlySpan<byte> buffer, ulong bufferClientPtr)
+ {
+ return AppendFinalOutputRecorderBuffer(buffer, bufferClientPtr);
+ }
+
+ [CmifCommand(9)] // 3.0.0+
+ public Result GetReleasedFinalOutputRecorderBuffersAuto([Buffer(HipcBufferFlags.Out | HipcBufferFlags.AutoSelect)] Span<byte> buffer, out uint count, out ulong released)
+ {
+ return GetReleasedFinalOutputRecorderBuffers(buffer, out count, out released);
+ }
+
+ [CmifCommand(10)] // 6.0.0+
+ public Result FlushFinalOutputRecorderBuffers(out bool pending)
+ {
+ pending = false;
+
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(11)] // 9.0.0+
+ public Result AttachWorkBuffer(FinalOutputRecorderParameterInternal parameter)
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio, new { parameter });
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Os.DestroySystemEvent(ref _event);
+
+ if (_processHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_processHandle);
+
+ _processHandle = 0;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs
new file mode 100644
index 00000000..76491bb7
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderManager.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ partial class FinalOutputRecorderManager : IFinalOutputRecorderManager
+ {
+ [CmifCommand(0)]
+ public Result OpenFinalOutputRecorder(
+ out IFinalOutputRecorder recorder,
+ FinalOutputRecorderParameter parameter,
+ [CopyHandle] int processHandle,
+ out FinalOutputRecorderParameterInternal outParameter,
+ AppletResourceUserId appletResourceId)
+ {
+ recorder = new FinalOutputRecorder(processHandle);
+ outParameter = new(parameter.SampleRate, 2, 0);
+
+ return Result.Success;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs
new file mode 100644
index 00000000..afa060fc
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameter.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)]
+ readonly struct FinalOutputRecorderParameter
+ {
+ public readonly uint SampleRate;
+ public readonly uint Padding;
+
+ public FinalOutputRecorderParameter(uint sampleRate)
+ {
+ SampleRate = sampleRate;
+ Padding = 0;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs
new file mode 100644
index 00000000..e88398eb
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/FinalOutputRecorderParameterInternal.cs
@@ -0,0 +1,21 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x4)]
+ readonly struct FinalOutputRecorderParameterInternal
+ {
+ public readonly uint SampleRate;
+ public readonly uint ChannelCount;
+ public readonly uint UseLargeFrameSize;
+ public readonly uint Padding;
+
+ public FinalOutputRecorderParameterInternal(uint sampleRate, uint channelCount, uint useLargeFrameSize)
+ {
+ SampleRate = sampleRate;
+ ChannelCount = channelCount;
+ UseLargeFrameSize = useLargeFrameSize;
+ Padding = 0;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs
new file mode 100644
index 00000000..3df1fe22
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioDevice.cs
@@ -0,0 +1,24 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioDevice : IServiceObject
+ {
+ Result ListAudioDeviceName(Span<DeviceName> names, out int nameCount);
+ Result SetAudioDeviceOutputVolume(ReadOnlySpan<DeviceName> name, float volume);
+ Result GetAudioDeviceOutputVolume(ReadOnlySpan<DeviceName> name, out float volume);
+ Result GetActiveAudioDeviceName(Span<DeviceName> name);
+ Result QueryAudioDeviceSystemEvent(out int eventHandle);
+ Result GetActiveChannelCount(out int channelCount);
+ Result ListAudioDeviceNameAuto(Span<DeviceName> names, out int nameCount);
+ Result SetAudioDeviceOutputVolumeAuto(ReadOnlySpan<DeviceName> name, float volume);
+ Result GetAudioDeviceOutputVolumeAuto(ReadOnlySpan<DeviceName> name, out float volume);
+ Result GetActiveAudioDeviceNameAuto(Span<DeviceName> name);
+ Result QueryAudioDeviceInputEvent(out int eventHandle);
+ Result QueryAudioDeviceOutputEvent(out int eventHandle);
+ Result GetActiveAudioOutputDeviceName(Span<DeviceName> name);
+ Result ListAudioOutputDeviceName(Span<DeviceName> names, out int nameCount);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs
new file mode 100644
index 00000000..bdc3bcf6
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioIn.cs
@@ -0,0 +1,26 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioIn : IServiceObject
+ {
+ Result GetAudioInState(out AudioDeviceState state);
+ Result Start();
+ Result Stop();
+ Result AppendAudioInBuffer(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer);
+ Result RegisterBufferEvent(out int eventHandle);
+ Result GetReleasedAudioInBuffers(out uint count, Span<ulong> bufferTags);
+ Result ContainsAudioInBuffer(out bool contains, ulong bufferTag);
+ Result AppendUacInBuffer(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer, int eventHandle);
+ Result AppendAudioInBufferAuto(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer);
+ Result GetReleasedAudioInBuffersAuto(out uint count, Span<ulong> bufferTags);
+ Result AppendUacInBufferAuto(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer, int eventHandle);
+ Result GetAudioInBufferCount(out uint bufferCount);
+ Result SetDeviceGain(float gain);
+ Result GetDeviceGain(out float gain);
+ Result FlushAudioInBuffers(out bool pending);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs
new file mode 100644
index 00000000..e7f32fbd
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioInManager.cs
@@ -0,0 +1,43 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioInManager : IServiceObject
+ {
+ Result ListAudioIns(out int count, Span<DeviceName> names);
+ Result OpenAudioIn(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioIn audioIn,
+ Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ int processHandle,
+ ReadOnlySpan<DeviceName> name,
+ ulong pid);
+ Result ListAudioInsAuto(out int count, Span<DeviceName> names);
+ Result OpenAudioInAuto(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioIn audioIn,
+ Span<DeviceName> outName,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ int processHandle,
+ ReadOnlySpan<DeviceName> name,
+ ulong pid);
+ Result ListAudioInsAutoFiltered(out int count, Span<DeviceName> names);
+ Result OpenAudioInProtocolSpecified(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioIn audioIn,
+ Span<DeviceName> outName,
+ AudioInProtocol protocol,
+ AudioInputConfiguration parameter,
+ AppletResourceUserId appletResourceId,
+ int processHandle,
+ ReadOnlySpan<DeviceName> name,
+ ulong pid);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs
new file mode 100644
index 00000000..1b200926
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOut.cs
@@ -0,0 +1,25 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioOut : IServiceObject
+ {
+ Result GetAudioOutState(out AudioDeviceState state);
+ Result Start();
+ Result Stop();
+ Result AppendAudioOutBuffer(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer);
+ Result RegisterBufferEvent(out int eventHandle);
+ Result GetReleasedAudioOutBuffers(out uint count, Span<ulong> bufferTags);
+ Result ContainsAudioOutBuffer(out bool contains, ulong bufferTag);
+ Result AppendAudioOutBufferAuto(ulong bufferTag, ReadOnlySpan<AudioUserBuffer> buffer);
+ Result GetReleasedAudioOutBuffersAuto(out uint count, Span<ulong> bufferTags);
+ Result GetAudioOutBufferCount(out uint bufferCount);
+ Result GetAudioOutPlayedSampleCount(out ulong sampleCount);
+ Result FlushAudioOutBuffers(out bool pending);
+ Result SetAudioOutVolume(float volume);
+ Result GetAudioOutVolume(out float volume);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs
new file mode 100644
index 00000000..40d62836
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioOutManager.cs
@@ -0,0 +1,32 @@
+using Ryujinx.Audio.Common;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioOutManager : IServiceObject
+ {
+ Result ListAudioOuts(out int count, Span<DeviceName> names);
+ Result OpenAudioOut(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioOut audioOut,
+ Span<DeviceName> outName,
+ AudioInputConfiguration inputConfig,
+ AppletResourceUserId appletResourceId,
+ int processHandle,
+ ReadOnlySpan<DeviceName> name,
+ ulong pid);
+ Result ListAudioOutsAuto(out int count, Span<DeviceName> names);
+ Result OpenAudioOutAuto(
+ out AudioOutputConfiguration outputConfig,
+ out IAudioOut audioOut,
+ Span<DeviceName> outName,
+ AudioInputConfiguration inputConfig,
+ AppletResourceUserId appletResourceId,
+ int processHandle,
+ ReadOnlySpan<DeviceName> name,
+ ulong pid);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs
new file mode 100644
index 00000000..e4ca2e8e
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRenderer.cs
@@ -0,0 +1,24 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioRenderer : IServiceObject
+ {
+ Result GetSampleRate(out int sampleRate);
+ Result GetSampleCount(out int sampleCount);
+ Result GetMixBufferCount(out int mixBufferCount);
+ Result GetState(out int state);
+ Result RequestUpdate(Span<byte> output, Span<byte> performanceOutput, ReadOnlySpan<byte> input);
+ Result Start();
+ Result Stop();
+ Result QuerySystemEvent(out int eventHandle);
+ Result SetRenderingTimeLimit(int percent);
+ Result GetRenderingTimeLimit(out int percent);
+ Result RequestUpdateAuto(Span<byte> output, Span<byte> performanceOutput, ReadOnlySpan<byte> input);
+ Result ExecuteAudioRendererRendering();
+ Result SetVoiceDropParameter(float voiceDropParameter);
+ Result GetVoiceDropParameter(out float voiceDropParameter);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs
new file mode 100644
index 00000000..fe95a208
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioRendererManager.cs
@@ -0,0 +1,29 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioRendererManager : IServiceObject
+ {
+ Result OpenAudioRenderer(
+ out IAudioRenderer renderer,
+ AudioRendererParameterInternal parameter,
+ int processHandle,
+ int workBufferHandle,
+ ulong workBufferSize,
+ AppletResourceUserId appletUserId,
+ ulong pid);
+ Result GetWorkBufferSize(out long workBufferSize, AudioRendererParameterInternal parameter);
+ Result GetAudioDeviceService(out IAudioDevice audioDevice, AppletResourceUserId appletUserId);
+ Result OpenAudioRendererForManualExecution(
+ out IAudioRenderer renderer,
+ AudioRendererParameterInternal parameter,
+ ulong workBufferAddress,
+ int processHandle,
+ ulong workBufferSize,
+ AppletResourceUserId appletUserId,
+ ulong pid);
+ Result GetAudioDeviceServiceWithRevisionInfo(out IAudioDevice audioDevice, AppletResourceUserId appletUserId, uint revision);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs
new file mode 100644
index 00000000..72853886
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IAudioSnoopManager.cs
@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IAudioSnoopManager : IServiceObject
+ {
+ Result EnableDspUsageMeasurement();
+ Result DisableDspUsageMeasurement();
+ Result GetDspUsage(out uint usage);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs
new file mode 100644
index 00000000..be21c38b
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorder.cs
@@ -0,0 +1,22 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IFinalOutputRecorder : IServiceObject
+ {
+ Result GetFinalOutputRecorderState(out uint state);
+ Result Start();
+ Result Stop();
+ Result AppendFinalOutputRecorderBuffer(ReadOnlySpan<byte> buffer, ulong bufferClientPtr);
+ Result RegisterBufferEvent(out int eventHandle);
+ Result GetReleasedFinalOutputRecorderBuffers(Span<byte> buffer, out uint count, out ulong released);
+ Result ContainsFinalOutputRecorderBuffer(ulong bufferPointer, out bool contains);
+ Result GetFinalOutputRecorderBufferEndTime(ulong bufferPointer, out ulong released);
+ Result AppendFinalOutputRecorderBufferAuto(ReadOnlySpan<byte> buffer, ulong bufferClientPtr);
+ Result GetReleasedFinalOutputRecorderBuffersAuto(Span<byte> buffer, out uint count, out ulong released);
+ Result FlushFinalOutputRecorderBuffers(out bool pending);
+ Result AttachWorkBuffer(FinalOutputRecorderParameterInternal parameter);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs
new file mode 100644
index 00000000..bac41ca9
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Audio/Detail/IFinalOutputRecorderManager.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Applet;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Audio.Detail
+{
+ interface IFinalOutputRecorderManager : IServiceObject
+ {
+ Result OpenFinalOutputRecorder(
+ out IFinalOutputRecorder recorder,
+ FinalOutputRecorderParameter parameter,
+ int processHandle,
+ out FinalOutputRecorderParameterInternal outParameter,
+ AppletResourceUserId appletResourceId);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs b/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs
new file mode 100644
index 00000000..21508b7f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/CodecResult.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Codec
+{
+ static class CodecResult
+ {
+ private const int ModuleId = 111;
+
+ public static Result InvalidLength => new(ModuleId, 3);
+ public static Result OpusBadArg => new(ModuleId, 130);
+ public static Result OpusInvalidPacket => new(ModuleId, 133);
+ public static Result InvalidNumberOfStreams => new(ModuleId, 1000);
+ public static Result InvalidSampleRate => new(ModuleId, 1001);
+ public static Result InvalidChannelCount => new(ModuleId, 1002);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
new file mode 100644
index 00000000..5d279858
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
@@ -0,0 +1,336 @@
+using Concentus;
+using Concentus.Enums;
+using Concentus.Structs;
+using Ryujinx.Common.Logging;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ partial class HardwareOpusDecoder : IHardwareOpusDecoder, IDisposable
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ private struct OpusPacketHeader
+ {
+ public uint Length;
+ public uint FinalRange;
+
+ public static OpusPacketHeader FromSpan(ReadOnlySpan<byte> data)
+ {
+ return new()
+ {
+ Length = BinaryPrimitives.ReadUInt32BigEndian(data),
+ FinalRange = BinaryPrimitives.ReadUInt32BigEndian(data[sizeof(uint)..]),
+ };
+ }
+ }
+
+ private interface IDecoder
+ {
+ int SampleRate { get; }
+ int ChannelsCount { get; }
+
+ int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize);
+ void ResetState();
+ }
+
+ private class Decoder : IDecoder
+ {
+ private readonly OpusDecoder _decoder;
+
+ public int SampleRate => _decoder.SampleRate;
+ public int ChannelsCount => _decoder.NumChannels;
+
+ public Decoder(int sampleRate, int channelsCount)
+ {
+ _decoder = new OpusDecoder(sampleRate, channelsCount);
+ }
+
+ public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+ {
+ return _decoder.Decode(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize);
+ }
+
+ public void ResetState()
+ {
+ _decoder.ResetState();
+ }
+ }
+
+ private class MultiSampleDecoder : IDecoder
+ {
+ private readonly OpusMSDecoder _decoder;
+
+ public int SampleRate => _decoder.SampleRate;
+ public int ChannelsCount { get; }
+
+ public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping)
+ {
+ ChannelsCount = channelsCount;
+ _decoder = new OpusMSDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
+ }
+
+ public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+ {
+ return _decoder.DecodeMultistream(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize, 0);
+ }
+
+ public void ResetState()
+ {
+ _decoder.ResetState();
+ }
+ }
+
+ private readonly IDecoder _decoder;
+ private int _workBufferHandle;
+
+ private HardwareOpusDecoder(int workBufferHandle)
+ {
+ _workBufferHandle = workBufferHandle;
+ }
+
+ public HardwareOpusDecoder(int sampleRate, int channelsCount, int workBufferHandle) : this(workBufferHandle)
+ {
+ _decoder = new Decoder(sampleRate, channelsCount);
+ }
+
+ public HardwareOpusDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping, int workBufferHandle) : this(workBufferHandle)
+ {
+ _decoder = new MultiSampleDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
+ }
+
+ [CmifCommand(0)]
+ public Result DecodeInterleavedOld(
+ out int outConsumed,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out _, output, input, reset: false, withPerf: false);
+ }
+
+ [CmifCommand(1)]
+ public Result SetContext(ReadOnlySpan<byte> context)
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(2)] // 3.0.0+
+ public Result DecodeInterleavedForMultiStreamOld(
+ out int outConsumed,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out _, output, input, reset: false, withPerf: false);
+ }
+
+ [CmifCommand(3)] // 3.0.0+
+ public Result SetContextForMultiStream(ReadOnlySpan<byte> arg0)
+ {
+ Logger.Stub?.PrintStub(LogClass.ServiceAudio);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(4)] // 4.0.0+
+ public Result DecodeInterleavedWithPerfOld(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset: false, withPerf: true);
+ }
+
+ [CmifCommand(5)] // 4.0.0+
+ public Result DecodeInterleavedForMultiStreamWithPerfOld(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset: false, withPerf: true);
+ }
+
+ [CmifCommand(6)] // 6.0.0+
+ public Result DecodeInterleavedWithPerfAndResetOld(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input,
+ bool reset)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true);
+ }
+
+ [CmifCommand(7)] // 6.0.0+
+ public Result DecodeInterleavedForMultiStreamWithPerfAndResetOld(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> input,
+ bool reset)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true);
+ }
+
+ [CmifCommand(8)] // 7.0.0+
+ public Result DecodeInterleaved(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] ReadOnlySpan<byte> input,
+ bool reset)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true);
+ }
+
+ [CmifCommand(9)] // 7.0.0+
+ public Result DecodeInterleavedForMultiStream(
+ out int outConsumed,
+ out long timeTaken,
+ out int outSamples,
+ [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] Span<byte> output,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.MapTransferAllowsNonSecure)] ReadOnlySpan<byte> input,
+ bool reset)
+ {
+ return DecodeInterleavedInternal(out outConsumed, out outSamples, out timeTaken, output, input, reset, withPerf: true);
+ }
+
+ private Result DecodeInterleavedInternal(
+ out int outConsumed,
+ out int outSamples,
+ out long timeTaken,
+ Span<byte> output,
+ ReadOnlySpan<byte> input,
+ bool reset,
+ bool withPerf)
+ {
+ timeTaken = 0;
+
+ Result result = DecodeInterleaved(_decoder, reset, input, out short[] outPcmData, output.Length, out outConsumed, out outSamples);
+
+ if (withPerf)
+ {
+ // This is the time the DSP took to process the request, TODO: fill this.
+ timeTaken = 0;
+ }
+
+ MemoryMarshal.Cast<short, byte>(outPcmData).CopyTo(output[..(outPcmData.Length * sizeof(short))]);
+
+ return result;
+ }
+
+ private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, byte[] packet)
+ {
+ int result = OpusPacketInfo.GetNumSamples(packet, 0, packet.Length, decoder.SampleRate);
+
+ numSamples = result;
+
+ if (result == OpusError.OPUS_INVALID_PACKET)
+ {
+ return CodecResult.OpusInvalidPacket;
+ }
+ else if (result == OpusError.OPUS_BAD_ARG)
+ {
+ return CodecResult.OpusBadArg;
+ }
+
+ return Result.Success;
+ }
+
+ private static Result DecodeInterleaved(
+ IDecoder decoder,
+ bool reset,
+ ReadOnlySpan<byte> input,
+ out short[] outPcmData,
+ int outputSize,
+ out int outConsumed,
+ out int outSamples)
+ {
+ outPcmData = null;
+ outConsumed = 0;
+ outSamples = 0;
+
+ int streamSize = input.Length;
+
+ if (streamSize < Unsafe.SizeOf<OpusPacketHeader>())
+ {
+ return CodecResult.InvalidLength;
+ }
+
+ OpusPacketHeader header = OpusPacketHeader.FromSpan(input);
+ int headerSize = Unsafe.SizeOf<OpusPacketHeader>();
+ uint totalSize = header.Length + (uint)headerSize;
+
+ if (totalSize > streamSize)
+ {
+ return CodecResult.InvalidLength;
+ }
+
+ byte[] opusData = input.Slice(headerSize, (int)header.Length).ToArray();
+
+ Result result = GetPacketNumSamples(decoder, out int numSamples, opusData);
+
+ if (result.IsSuccess)
+ {
+ if ((uint)numSamples * (uint)decoder.ChannelsCount * sizeof(short) > outputSize)
+ {
+ return CodecResult.InvalidLength;
+ }
+
+ outPcmData = new short[numSamples * decoder.ChannelsCount];
+
+ if (reset)
+ {
+ decoder.ResetState();
+ }
+
+ try
+ {
+ outSamples = decoder.Decode(opusData, 0, opusData.Length, outPcmData, 0, outPcmData.Length / decoder.ChannelsCount);
+ outConsumed = (int)totalSize;
+ }
+ catch (OpusException)
+ {
+ // TODO: As OpusException doesn't return the exact error code, this is inaccurate in some cases...
+ return CodecResult.InvalidLength;
+ }
+ }
+
+ return Result.Success;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_workBufferHandle != 0)
+ {
+ HorizonStatic.Syscall.CloseHandle(_workBufferHandle);
+
+ _workBufferHandle = 0;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs
new file mode 100644
index 00000000..acec66e8
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderManager.cs
@@ -0,0 +1,386 @@
+using Ryujinx.Common;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ partial class HardwareOpusDecoderManager : IHardwareOpusDecoderManager
+ {
+ [CmifCommand(0)]
+ public Result OpenHardwareOpusDecoder(
+ out IHardwareOpusDecoder decoder,
+ HardwareOpusDecoderParameterInternal parameter,
+ [CopyHandle] int workBufferHandle,
+ int workBufferSize)
+ {
+ decoder = null;
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidSampleRate;
+ }
+
+ if (!IsValidChannelCount(parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidChannelCount;
+ }
+
+ decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(1)]
+ public Result GetWorkBufferSize(out int size, HardwareOpusDecoderParameterInternal parameter)
+ {
+ size = 0;
+
+ if (!IsValidChannelCount(parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ int opusDecoderSize = GetOpusDecoderSize(parameter.ChannelsCount);
+
+ int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
+ int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64);
+ size = opusDecoderSize + 1536 + frameSize;
+
+ return Result.Success;
+ }
+
+ [CmifCommand(2)] // 3.0.0+
+ public Result OpenHardwareOpusDecoderForMultiStream(
+ out IHardwareOpusDecoder decoder,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter,
+ [CopyHandle] int workBufferHandle,
+ int workBufferSize)
+ {
+ decoder = null;
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidSampleRate;
+ }
+
+ if (!IsValidMultiChannelCount(parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidNumberOfStreams;
+ }
+
+ decoder = new HardwareOpusDecoder(
+ parameter.SampleRate,
+ parameter.ChannelsCount,
+ parameter.NumberOfStreams,
+ parameter.NumberOfStereoStreams,
+ parameter.ChannelMappings.AsSpan().ToArray(),
+ workBufferHandle);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(3)] // 3.0.0+
+ public Result GetWorkBufferSizeForMultiStream(
+ out int size,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter)
+ {
+ size = 0;
+
+ if (!IsValidMultiChannelCount(parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ int opusDecoderSize = GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams);
+
+ int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64);
+ int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
+ int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64);
+ size = opusDecoderSize + streamSize + frameSize;
+
+ return Result.Success;
+ }
+
+ [CmifCommand(4)] // 12.0.0+
+ public Result OpenHardwareOpusDecoderEx(
+ out IHardwareOpusDecoder decoder,
+ HardwareOpusDecoderParameterInternalEx parameter,
+ [CopyHandle] int workBufferHandle,
+ int workBufferSize)
+ {
+ decoder = null;
+
+ if (!IsValidChannelCount(parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidSampleRate;
+ }
+
+ decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(5)] // 12.0.0+
+ public Result GetWorkBufferSizeEx(out int size, HardwareOpusDecoderParameterInternalEx parameter)
+ {
+ return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: false);
+ }
+
+ [CmifCommand(6)] // 12.0.0+
+ public Result OpenHardwareOpusDecoderForMultiStreamEx(
+ out IHardwareOpusDecoder decoder,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter,
+ [CopyHandle] int workBufferHandle,
+ int workBufferSize)
+ {
+ decoder = null;
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidSampleRate;
+ }
+
+ if (!IsValidMultiChannelCount(parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
+ {
+ HorizonStatic.Syscall.CloseHandle(workBufferHandle);
+
+ return CodecResult.InvalidNumberOfStreams;
+ }
+
+ decoder = new HardwareOpusDecoder(
+ parameter.SampleRate,
+ parameter.ChannelsCount,
+ parameter.NumberOfStreams,
+ parameter.NumberOfStereoStreams,
+ parameter.ChannelMappings.AsSpan().ToArray(),
+ workBufferHandle);
+
+ return Result.Success;
+ }
+
+ [CmifCommand(7)] // 12.0.0+
+ public Result GetWorkBufferSizeForMultiStreamEx(
+ out int size,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter)
+ {
+ return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: false);
+ }
+
+ [CmifCommand(8)] // 16.0.0+
+ public Result GetWorkBufferSizeExEx(out int size, HardwareOpusDecoderParameterInternalEx parameter)
+ {
+ return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: true);
+ }
+
+ [CmifCommand(9)] // 16.0.0+
+ public Result GetWorkBufferSizeForMultiStreamExEx(
+ out int size,
+ [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter)
+ {
+ return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: true);
+ }
+
+ private Result GetWorkBufferSizeExImpl(out int size, in HardwareOpusDecoderParameterInternalEx parameter, bool fromDsp)
+ {
+ size = 0;
+
+ if (!IsValidChannelCount(parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ int opusDecoderSize = fromDsp ? GetDspOpusDecoderSize(parameter.ChannelsCount) : GetOpusDecoderSize(parameter.ChannelsCount);
+
+ int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920;
+ int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
+ int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64);
+ size = opusDecoderSize + 1536 + frameSize;
+
+ return Result.Success;
+ }
+
+ private Result GetWorkBufferSizeForMultiStreamExImpl(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, bool fromDsp)
+ {
+ size = 0;
+
+ if (!IsValidMultiChannelCount(parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidChannelCount;
+ }
+
+ if (!IsValidSampleRate(parameter.SampleRate))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
+ {
+ return CodecResult.InvalidSampleRate;
+ }
+
+ int opusDecoderSize = fromDsp
+ ? GetDspOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams)
+ : GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams);
+
+ int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920;
+ int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64);
+ int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
+ int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64);
+ size = opusDecoderSize + streamSize + frameSize;
+
+ return Result.Success;
+ }
+
+ private static int GetDspOpusDecoderSize(int channelsCount)
+ {
+ // TODO: Figure out the size returned here.
+ // Not really important because we don't use the work buffer, and the size being lower is fine.
+
+ return 0;
+ }
+
+ private static int GetDspOpusMultistreamDecoderSize(int streams, int coupledStreams)
+ {
+ // TODO: Figure out the size returned here.
+ // Not really important because we don't use the work buffer, and the size being lower is fine.
+
+ return 0;
+ }
+
+ private static int GetOpusDecoderSize(int channelsCount)
+ {
+ const int SilkDecoderSize = 0x2160;
+
+ if (channelsCount < 1 || channelsCount > 2)
+ {
+ return 0;
+ }
+
+ int celtDecoderSize = GetCeltDecoderSize(channelsCount);
+ int opusDecoderSize = GetOpusDecoderAllocSize(channelsCount) | 0x50;
+
+ return opusDecoderSize + SilkDecoderSize + celtDecoderSize;
+ }
+
+ private static int GetOpusMultistreamDecoderSize(int streams, int coupledStreams)
+ {
+ if (streams < 1 || coupledStreams > streams || coupledStreams < 0)
+ {
+ return 0;
+ }
+
+ int coupledSize = GetOpusDecoderSize(2);
+ int monoSize = GetOpusDecoderSize(1);
+
+ return Align4(monoSize - GetOpusDecoderAllocSize(1)) * (streams - coupledStreams) +
+ Align4(coupledSize - GetOpusDecoderAllocSize(2)) * coupledStreams + 0xb920;
+ }
+
+ private static int Align4(int value)
+ {
+ return BitUtils.AlignUp(value, 4);
+ }
+
+ private static int GetOpusDecoderAllocSize(int channelsCount)
+ {
+ return channelsCount * 0x800 + 0x4800;
+ }
+
+ private static int GetCeltDecoderSize(int channelsCount)
+ {
+ const int DecodeBufferSize = 0x2030;
+ const int Overlap = 120;
+ const int EBandsCount = 21;
+
+ return (DecodeBufferSize + Overlap * 4) * channelsCount + EBandsCount * 16 + 0x54;
+ }
+
+ private static bool IsValidChannelCount(int channelsCount)
+ {
+ return channelsCount > 0 && channelsCount <= 2;
+ }
+
+ private static bool IsValidMultiChannelCount(int channelsCount)
+ {
+ return channelsCount > 0 && channelsCount <= 255;
+ }
+
+ private static bool IsValidSampleRate(int sampleRate)
+ {
+ switch (sampleRate)
+ {
+ case 8000:
+ case 12000:
+ case 16000:
+ case 24000:
+ case 48000:
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsValidNumberOfStreams(int numberOfStreams, int numberOfStereoStreams, int channelsCount)
+ {
+ return numberOfStreams > 0 &&
+ numberOfStreams + numberOfStereoStreams <= channelsCount &&
+ numberOfStereoStreams >= 0 &&
+ numberOfStereoStreams <= numberOfStreams;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs
new file mode 100644
index 00000000..271a592c
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternal.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 0x4)]
+ struct HardwareOpusDecoderParameterInternal
+ {
+ public int SampleRate;
+ public int ChannelsCount;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs
new file mode 100644
index 00000000..e2b81c77
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoderParameterInternalEx.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 0x4)]
+ struct HardwareOpusDecoderParameterInternalEx
+ {
+ public int SampleRate;
+ public int ChannelsCount;
+ public OpusDecoderFlags Flags;
+ public uint Reserved;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs
new file mode 100644
index 00000000..98536a4f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternal.cs
@@ -0,0 +1,15 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x110)]
+ struct HardwareOpusMultiStreamDecoderParameterInternal
+ {
+ public int SampleRate;
+ public int ChannelsCount;
+ public int NumberOfStreams;
+ public int NumberOfStereoStreams;
+ public Array256<byte> ChannelMappings;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs
new file mode 100644
index 00000000..8f8615df
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusMultiStreamDecoderParameterInternalEx.cs
@@ -0,0 +1,17 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x118)]
+ struct HardwareOpusMultiStreamDecoderParameterInternalEx
+ {
+ public int SampleRate;
+ public int ChannelsCount;
+ public int NumberOfStreams;
+ public int NumberOfStereoStreams;
+ public OpusDecoderFlags Flags;
+ public uint Reserved;
+ public Array256<byte> ChannelMappings;
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs
new file mode 100644
index 00000000..ae09ad15
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoder.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ interface IHardwareOpusDecoder : IServiceObject
+ {
+ Result DecodeInterleavedOld(out int outConsumed, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input);
+ Result SetContext(ReadOnlySpan<byte> context);
+ Result DecodeInterleavedForMultiStreamOld(out int outConsumed, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input);
+ Result SetContextForMultiStream(ReadOnlySpan<byte> context);
+ Result DecodeInterleavedWithPerfOld(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input);
+ Result DecodeInterleavedForMultiStreamWithPerfOld(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input);
+ Result DecodeInterleavedWithPerfAndResetOld(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input, bool reset);
+ Result DecodeInterleavedForMultiStreamWithPerfAndResetOld(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input, bool reset);
+ Result DecodeInterleaved(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input, bool reset);
+ Result DecodeInterleavedForMultiStream(out int outConsumed, out long timeTaken, out int outSamples, Span<byte> output, ReadOnlySpan<byte> input, bool reset);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs
new file mode 100644
index 00000000..fb6c787b
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/IHardwareOpusDecoderManager.cs
@@ -0,0 +1,19 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ interface IHardwareOpusDecoderManager : IServiceObject
+ {
+ Result OpenHardwareOpusDecoder(out IHardwareOpusDecoder decoder, HardwareOpusDecoderParameterInternal parameter, int workBufferHandle, int workBufferSize);
+ Result GetWorkBufferSize(out int size, HardwareOpusDecoderParameterInternal parameter);
+ Result OpenHardwareOpusDecoderForMultiStream(out IHardwareOpusDecoder decoder, in HardwareOpusMultiStreamDecoderParameterInternal parameter, int workBufferHandle, int workBufferSize);
+ Result GetWorkBufferSizeForMultiStream(out int size, in HardwareOpusMultiStreamDecoderParameterInternal parameter);
+ Result OpenHardwareOpusDecoderEx(out IHardwareOpusDecoder decoder, HardwareOpusDecoderParameterInternalEx parameter, int workBufferHandle, int workBufferSize);
+ Result GetWorkBufferSizeEx(out int size, HardwareOpusDecoderParameterInternalEx parameter);
+ Result OpenHardwareOpusDecoderForMultiStreamEx(out IHardwareOpusDecoder decoder, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, int workBufferHandle, int workBufferSize);
+ Result GetWorkBufferSizeForMultiStreamEx(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter);
+ Result GetWorkBufferSizeExEx(out int size, HardwareOpusDecoderParameterInternalEx parameter);
+ Result GetWorkBufferSizeForMultiStreamExEx(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter);
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs
new file mode 100644
index 00000000..d630b10f
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/OpusDecoderFlags.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Codec.Detail
+{
+ [Flags]
+ enum OpusDecoderFlags : uint
+ {
+ None,
+ LargeFrameSize = 1 << 0,
+ }
+}