/ src / Ryujinx.Audio.Backends.SoundIo / SoundIoHardwareDeviceDriver.cs
SoundIoHardwareDeviceDriver.cs
  1  using Ryujinx.Audio.Backends.SoundIo.Native;
  2  using Ryujinx.Audio.Common;
  3  using Ryujinx.Audio.Integration;
  4  using Ryujinx.Memory;
  5  using System;
  6  using System.Collections.Concurrent;
  7  using System.Threading;
  8  using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo;
  9  using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
 10  
 11  namespace Ryujinx.Audio.Backends.SoundIo
 12  {
 13      public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
 14      {
 15          private readonly SoundIoContext _audioContext;
 16          private readonly SoundIoDeviceContext _audioDevice;
 17          private readonly ManualResetEvent _updateRequiredEvent;
 18          private readonly ManualResetEvent _pauseEvent;
 19          private readonly ConcurrentDictionary<SoundIoHardwareDeviceSession, byte> _sessions;
 20          private int _disposeState;
 21  
 22          private float _volume = 1f;
 23  
 24          public float Volume
 25          {
 26              get
 27              {
 28                  return _volume;
 29              }
 30              set
 31              {
 32                  _volume = value;
 33  
 34                  foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
 35                  {
 36                      session.UpdateMasterVolume(value);
 37                  }
 38              }
 39          }
 40  
 41          public SoundIoHardwareDeviceDriver()
 42          {
 43              _audioContext = SoundIoContext.Create();
 44              _updateRequiredEvent = new ManualResetEvent(false);
 45              _pauseEvent = new ManualResetEvent(true);
 46              _sessions = new ConcurrentDictionary<SoundIoHardwareDeviceSession, byte>();
 47  
 48              _audioContext.Connect();
 49              _audioContext.FlushEvents();
 50  
 51              _audioDevice = FindValidAudioDevice(_audioContext, true);
 52          }
 53  
 54          public static bool IsSupported => IsSupportedInternal();
 55  
 56          private static bool IsSupportedInternal()
 57          {
 58              SoundIoContext context = null;
 59              SoundIoDeviceContext device = null;
 60              SoundIoOutStreamContext stream = null;
 61  
 62              bool backendDisconnected = false;
 63  
 64              try
 65              {
 66                  context = SoundIoContext.Create();
 67                  context.OnBackendDisconnect = err =>
 68                  {
 69                      backendDisconnected = true;
 70                  };
 71  
 72                  context.Connect();
 73                  context.FlushEvents();
 74  
 75                  if (backendDisconnected)
 76                  {
 77                      return false;
 78                  }
 79  
 80                  if (context.OutputDeviceCount == 0)
 81                  {
 82                      return false;
 83                  }
 84  
 85                  device = FindValidAudioDevice(context);
 86  
 87                  if (device == null || backendDisconnected)
 88                  {
 89                      return false;
 90                  }
 91  
 92                  stream = device.CreateOutStream();
 93  
 94                  if (stream == null || backendDisconnected)
 95                  {
 96                      return false;
 97                  }
 98  
 99                  return true;
100              }
101              catch
102              {
103                  return false;
104              }
105              finally
106              {
107                  stream?.Dispose();
108                  context?.Dispose();
109              }
110          }
111  
112          private static SoundIoDeviceContext FindValidAudioDevice(SoundIoContext audioContext, bool fallback = false)
113          {
114              SoundIoDeviceContext defaultAudioDevice = audioContext.GetOutputDevice(audioContext.DefaultOutputDeviceIndex);
115  
116              if (!defaultAudioDevice.IsRaw)
117              {
118                  return defaultAudioDevice;
119              }
120  
121              for (int i = 0; i < audioContext.OutputDeviceCount; i++)
122              {
123                  SoundIoDeviceContext audioDevice = audioContext.GetOutputDevice(i);
124  
125                  if (audioDevice.Id == defaultAudioDevice.Id && !audioDevice.IsRaw)
126                  {
127                      return audioDevice;
128                  }
129              }
130  
131              return fallback ? defaultAudioDevice : null;
132          }
133  
134          public ManualResetEvent GetUpdateRequiredEvent()
135          {
136              return _updateRequiredEvent;
137          }
138  
139          public ManualResetEvent GetPauseEvent()
140          {
141              return _pauseEvent;
142          }
143  
144          public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
145          {
146              if (channelCount == 0)
147              {
148                  channelCount = 2;
149              }
150  
151              if (sampleRate == 0)
152              {
153                  sampleRate = Constants.TargetSampleRate;
154              }
155  
156              if (direction != Direction.Output)
157              {
158                  throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
159              }
160  
161              SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
162  
163              _sessions.TryAdd(session, 0);
164  
165              return session;
166          }
167  
168          internal bool Unregister(SoundIoHardwareDeviceSession session)
169          {
170              return _sessions.TryRemove(session, out _);
171          }
172  
173          public static SoundIoFormat GetSoundIoFormat(SampleFormat format)
174          {
175              return format switch
176              {
177                  SampleFormat.PcmInt8 => SoundIoFormat.S8,
178                  SampleFormat.PcmInt16 => SoundIoFormat.S16LE,
179                  SampleFormat.PcmInt24 => SoundIoFormat.S24LE,
180                  SampleFormat.PcmInt32 => SoundIoFormat.S32LE,
181                  SampleFormat.PcmFloat => SoundIoFormat.Float32LE,
182                  _ => throw new ArgumentException($"Unsupported sample format {format}"),
183              };
184          }
185  
186          internal SoundIoOutStreamContext OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount)
187          {
188              SoundIoFormat driverSampleFormat = GetSoundIoFormat(requestedSampleFormat);
189  
190              if (!_audioDevice.SupportsSampleRate((int)requestedSampleRate))
191              {
192                  throw new ArgumentException($"This sound device does not support a sample rate of {requestedSampleRate}Hz");
193              }
194  
195              if (!_audioDevice.SupportsFormat(driverSampleFormat))
196              {
197                  throw new ArgumentException($"This sound device does not support {requestedSampleFormat}");
198              }
199  
200              if (!_audioDevice.SupportsChannelCount((int)requestedChannelCount))
201              {
202                  throw new ArgumentException($"This sound device does not support channel count {requestedChannelCount}");
203              }
204  
205              SoundIoOutStreamContext result = _audioDevice.CreateOutStream();
206  
207              result.Name = "Ryujinx";
208              result.Layout = SoundIoChannelLayout.GetDefaultValue((int)requestedChannelCount);
209              result.Format = driverSampleFormat;
210              result.SampleRate = (int)requestedSampleRate;
211  
212              return result;
213          }
214  
215          internal void FlushContextEvents()
216          {
217              _audioContext.FlushEvents();
218          }
219  
220          public void Dispose()
221          {
222              GC.SuppressFinalize(this);
223  
224              if (Interlocked.CompareExchange(ref _disposeState, 1, 0) == 0)
225              {
226                  Dispose(true);
227              }
228          }
229  
230          protected virtual void Dispose(bool disposing)
231          {
232              if (disposing)
233              {
234                  foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
235                  {
236                      session.Dispose();
237                  }
238  
239                  _audioContext.Disconnect();
240                  _audioContext.Dispose();
241                  _pauseEvent.Dispose();
242              }
243          }
244  
245          public bool SupportsSampleRate(uint sampleRate)
246          {
247              return _audioDevice.SupportsSampleRate((int)sampleRate);
248          }
249  
250          public bool SupportsSampleFormat(SampleFormat sampleFormat)
251          {
252              return _audioDevice.SupportsFormat(GetSoundIoFormat(sampleFormat));
253          }
254  
255          public bool SupportsChannelCount(uint channelCount)
256          {
257              return _audioDevice.SupportsChannelCount((int)channelCount);
258          }
259  
260          public bool SupportsDirection(Direction direction)
261          {
262              // TODO: add direction input when supported.
263              return direction == Direction.Output;
264          }
265      }
266  }