using Ryujinx.Audio.Backends.SoundIo.Native; using Ryujinx.Audio.Common; using Ryujinx.Audio.Integration; using Ryujinx.Memory; using System; using System.Collections.Concurrent; using System.Threading; using static Ryujinx.Audio.Integration.IHardwareDeviceDriver; using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo; namespace Ryujinx.Audio.Backends.SoundIo { public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver { private readonly SoundIoContext _audioContext; private readonly SoundIoDeviceContext _audioDevice; private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _pauseEvent; private readonly ConcurrentDictionary<SoundIoHardwareDeviceSession, byte> _sessions; private int _disposeState; public SoundIoHardwareDeviceDriver() { _audioContext = SoundIoContext.Create(); _updateRequiredEvent = new ManualResetEvent(false); _pauseEvent = new ManualResetEvent(true); _sessions = new ConcurrentDictionary<SoundIoHardwareDeviceSession, byte>(); _audioContext.Connect(); _audioContext.FlushEvents(); _audioDevice = FindValidAudioDevice(_audioContext, true); } public static bool IsSupported => IsSupportedInternal(); private static bool IsSupportedInternal() { SoundIoContext context = null; SoundIoDeviceContext device = null; SoundIoOutStreamContext stream = null; bool backendDisconnected = false; try { context = SoundIoContext.Create(); context.OnBackendDisconnect = err => { backendDisconnected = true; }; context.Connect(); context.FlushEvents(); if (backendDisconnected) { return false; } if (context.OutputDeviceCount == 0) { return false; } device = FindValidAudioDevice(context); if (device == null || backendDisconnected) { return false; } stream = device.CreateOutStream(); if (stream == null || backendDisconnected) { return false; } return true; } catch { return false; } finally { stream?.Dispose(); context?.Dispose(); } } private static SoundIoDeviceContext FindValidAudioDevice(SoundIoContext audioContext, bool fallback = false) { SoundIoDeviceContext defaultAudioDevice = audioContext.GetOutputDevice(audioContext.DefaultOutputDeviceIndex); if (!defaultAudioDevice.IsRaw) { return defaultAudioDevice; } for (int i = 0; i < audioContext.OutputDeviceCount; i++) { SoundIoDeviceContext audioDevice = audioContext.GetOutputDevice(i); if (audioDevice.Id == defaultAudioDevice.Id && !audioDevice.IsRaw) { return audioDevice; } } return fallback ? defaultAudioDevice : null; } public ManualResetEvent GetUpdateRequiredEvent() { return _updateRequiredEvent; } public ManualResetEvent GetPauseEvent() { return _pauseEvent; } public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume) { if (channelCount == 0) { channelCount = 2; } if (sampleRate == 0) { sampleRate = Constants.TargetSampleRate; } volume = Math.Clamp(volume, 0, 1); if (direction != Direction.Output) { throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!"); } SoundIoHardwareDeviceSession session = new SoundIoHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount, volume); _sessions.TryAdd(session, 0); return session; } internal bool Unregister(SoundIoHardwareDeviceSession session) { return _sessions.TryRemove(session, out _); } public static SoundIoFormat GetSoundIoFormat(SampleFormat format) { return format switch { SampleFormat.PcmInt8 => SoundIoFormat.S8, SampleFormat.PcmInt16 => SoundIoFormat.S16LE, SampleFormat.PcmInt24 => SoundIoFormat.S24LE, SampleFormat.PcmInt32 => SoundIoFormat.S32LE, SampleFormat.PcmFloat => SoundIoFormat.Float32LE, _ => throw new ArgumentException ($"Unsupported sample format {format}"), }; } internal SoundIoOutStreamContext OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) { SoundIoFormat driverSampleFormat = GetSoundIoFormat(requestedSampleFormat); if (!_audioDevice.SupportsSampleRate((int)requestedSampleRate)) { throw new ArgumentException($"This sound device does not support a sample rate of {requestedSampleRate}Hz"); } if (!_audioDevice.SupportsFormat(driverSampleFormat)) { throw new ArgumentException($"This sound device does not support {requestedSampleFormat}"); } if (!_audioDevice.SupportsChannelCount((int)requestedChannelCount)) { throw new ArgumentException($"This sound device does not support channel count {requestedChannelCount}"); } SoundIoOutStreamContext result = _audioDevice.CreateOutStream(); result.Name = "Ryujinx"; result.Layout = SoundIoChannelLayout.GetDefaultValue((int)requestedChannelCount); result.Format = driverSampleFormat; result.SampleRate = (int)requestedSampleRate; return result; } internal void FlushContextEvents() { _audioContext.FlushEvents(); } public void Dispose() { if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0) { Dispose(true); } } protected virtual void Dispose(bool disposing) { if (disposing) { foreach (SoundIoHardwareDeviceSession session in _sessions.Keys) { session.Dispose(); } _audioContext.Disconnect(); _audioContext.Dispose(); _pauseEvent.Dispose(); } } public bool SupportsSampleRate(uint sampleRate) { return _audioDevice.SupportsSampleRate((int)sampleRate); } public bool SupportsSampleFormat(SampleFormat sampleFormat) { return _audioDevice.SupportsFormat(GetSoundIoFormat(sampleFormat)); } public bool SupportsChannelCount(uint channelCount) { return _audioDevice.SupportsChannelCount((int)channelCount); } public bool SupportsDirection(Direction direction) { // TODO: add direction input when supported. return direction == Direction.Output; } } }