aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Horizon/Sdk/Audio
diff options
context:
space:
mode:
authorgdkchan <gab.dark.100@gmail.com>2024-02-22 16:58:33 -0300
committerGitHub <noreply@github.com>2024-02-22 16:58:33 -0300
commitd4d0a48bfe89d6e8e12ce16829bb2c440b56007c (patch)
tree2376566ed2c06181b3dbc547b1f99f5b533d918b /src/Ryujinx.Horizon/Sdk/Audio
parent57d8afd0c99bb43d1ba1e3cc630d257c5da92741 (diff)
Migrate Audio service to new IPC (#6285)
* Migrate audren to new IPC * Migrate audout * Migrate audin * Migrate hwopus * Bye bye old audio service * Switch volume control to IHardwareDeviceDriver * Somewhat unrelated changes * Remove Concentus reference from HLE * Implement OpenAudioRendererForManualExecution * Remove SetVolume/GetVolume methods that are not necessary * Remove SetVolume/GetVolume methods that are not necessary (2) * Fix incorrect volume update * PR feedback * PR feedback * Stub audrec * Init outParameter * Make FinalOutputRecorderParameter/Internal readonly * Make FinalOutputRecorder IDisposable * Fix HardwareOpusDecoderManager parameter buffers * Opus work buffer size and error handling improvements * Add AudioInProtocolName enum * Fix potential divisions by zero
Diffstat (limited to 'src/Ryujinx.Horizon/Sdk/Audio')
-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
28 files changed, 1747 insertions, 0 deletions
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);
+ }
+}