diff --git a/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs b/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs index 7a42a29f5d..5f31828a29 100644 --- a/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs +++ b/src/BizHawk.Bizware.Audio/DirectSoundSoundOutput.cs @@ -15,6 +15,7 @@ namespace BizHawk.Bizware.Audio public sealed class DirectSoundSoundOutput : ISoundOutput { private readonly IHostAudioManager _sound; + private readonly IntPtr _mainWindowHandle; private bool _disposed; private DirectSound _device; private SecondarySoundBuffer _deviceBuffer; @@ -27,6 +28,7 @@ namespace BizHawk.Bizware.Audio public DirectSoundSoundOutput(IHostAudioManager sound, IntPtr mainWindowHandle, string soundDevice) { _sound = sound; + _mainWindowHandle = mainWindowHandle; // needed for resetting _device on device invalidation _retryCounter = 5; var deviceInfo = DirectSound.GetDevices().Find(d => d.Description == soundDevice); @@ -44,6 +46,16 @@ namespace BizHawk.Bizware.Audio _disposed = true; } + private void ResetToDefaultDevice() + { + _deviceBuffer?.Dispose(); + _deviceBuffer = null; + + _device.Dispose(); + _device = new(); + _device.SetCooperativeLevel(_mainWindowHandle, CooperativeLevel.Priority); + } + public static IEnumerable GetDeviceNames() { return DirectSound.GetDevices().Select(d => d.Description); @@ -55,9 +67,30 @@ namespace BizHawk.Bizware.Audio public int MaxSamplesDeficit { get; private set; } - private bool IsPlaying => _deviceBuffer != null && - ((BufferStatus)_deviceBuffer.Status & BufferStatus.BufferLost) == 0 && - ((BufferStatus)_deviceBuffer.Status & BufferStatus.Playing) == BufferStatus.Playing; + private bool IsPlaying + { + get + { + if (_deviceBuffer == null) + { + return false; + } + + try + { + var status = (BufferStatus)_deviceBuffer.Status; + return (status & BufferStatus.BufferLost) == 0 && + (status & BufferStatus.Playing) == BufferStatus.Playing; + } + catch (SharpDXException) + { + // this only seems to ever occur if the device is disconnected... + ResetToDefaultDevice(); + StartPlaying(); + return false; + } + } + } private void StartPlaying() { diff --git a/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs b/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs index caf89ffc93..67fe4580c9 100644 --- a/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs +++ b/src/BizHawk.Bizware.Audio/XAudio2SoundOutput.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using BizHawk.Client.Common; +using BizHawk.Common; using Vortice.MediaFoundation; using Vortice.Multimedia; @@ -14,8 +16,9 @@ namespace BizHawk.Bizware.Audio { private bool _disposed; private readonly IHostAudioManager _sound; - private readonly IXAudio2 _device; - private readonly IXAudio2MasteringVoice _masteringVoice; + private readonly DeferredXAudio2ErrorCallback _deferredErrorCallback; + private IXAudio2 _device; + private IXAudio2MasteringVoice _masteringVoice; private IXAudio2SourceVoice _sourceVoice; private BufferPool _bufferPool; private long _runningSamplesQueued; @@ -40,10 +43,34 @@ namespace BizHawk.Bizware.Audio return $"{MMDEVAPI_TOKEN}{device.Id}{DEVINTERFACE_AUDIO_RENDER}"; } + private void ResetToDefaultDevice() + { + var wasPlaying = _sourceVoice != null; + _sourceVoice?.Dispose(); + _bufferPool?.Dispose(); + _masteringVoice.Dispose(); + _device.Dispose(); + + _device = XAudio2.XAudio2Create(); + _device.CriticalError += (_, _) => _deferredErrorCallback.OnCriticalError(); + _masteringVoice = _device.CreateMasteringVoice( + inputChannels: _sound.ChannelCount, + inputSampleRate: _sound.SampleRate); + + if (wasPlaying) + { + StartSound(); + } + } + public XAudio2SoundOutput(IHostAudioManager sound, string chosenDeviceName) { _sound = sound; _device = XAudio2.XAudio2Create(); + // this is for fatal errors which require resetting to the default audio device + // note that this won't be called on the main thread, so we'll defer the reset to the main thread + _deferredErrorCallback = new(ResetToDefaultDevice); + _device.CriticalError += (_, _) => _deferredErrorCallback.OnCriticalError(); _masteringVoice = _device.CreateMasteringVoice( inputChannels: _sound.ChannelCount, inputSampleRate: _sound.SampleRate, @@ -56,6 +83,7 @@ namespace BizHawk.Bizware.Audio _masteringVoice.Dispose(); _device.Dispose(); + _deferredErrorCallback.Dispose(); _disposed = true; } @@ -105,8 +133,9 @@ namespace BizHawk.Bizware.Audio public int CalculateSamplesNeeded() { var isInitializing = _runningSamplesQueued == 0; - var detectedUnderrun = !isInitializing && _sourceVoice.State.BuffersQueued == 0; - var samplesAwaitingPlayback = _runningSamplesQueued - (long)_sourceVoice.State.SamplesPlayed; + var voiceState = _sourceVoice.State; + var detectedUnderrun = !isInitializing && voiceState.BuffersQueued == 0; + var samplesAwaitingPlayback = _runningSamplesQueued - (long)voiceState.SamplesPlayed; var samplesNeeded = (int)Math.Max(BufferSizeSamples - samplesAwaitingPlayback, 0); if (isInitializing || detectedUnderrun) { @@ -183,5 +212,99 @@ namespace BizHawk.Bizware.Audio } } } + + private sealed class DeferredXAudio2ErrorCallback : IDisposable + { + private const int WM_CLOSE = 0x0010; + private const int WM_DEFERRED_ERROR_CALLBACK = 0x0400 + 1; + + private static readonly WmImports.WNDPROC _wndProc = WndProc; + + private static readonly Lazy _deferredXAudio2CallbackWindowAtom = new(() => + { + var wc = default(WmImports.WNDCLASSW); + wc.lpfnWndProc = _wndProc; + wc.hInstance = LoaderApiImports.GetModuleHandleW(null); + wc.lpszClassName = "DeferredXAudio2ErrorCallbackClass"; + + var atom = WmImports.RegisterClassW(ref wc); + if (atom == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to register deferred XAudio2 error callback window class"); + } + + return atom; + }); + + private static IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam) + { + var ud = WmImports.GetWindowLongPtrW(hWnd, WmImports.GWLP_USERDATA); + if (ud == IntPtr.Zero) + { + return WmImports.DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + + if (uMsg != WM_DEFERRED_ERROR_CALLBACK) + { + if (uMsg == WM_CLOSE) + { + WmImports.SetWindowLongPtrW(hWnd, WmImports.GWLP_USERDATA, IntPtr.Zero); + GCHandle.FromIntPtr(ud).Free(); + } + + return WmImports.DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + + // reset to the default audio device + + var deferredCallback = (DeferredXAudio2ErrorCallback)GCHandle.FromIntPtr(ud).Target; + deferredCallback.ResetToDefaultDeviceCallback(); + + return WmImports.DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + + private readonly Action ResetToDefaultDeviceCallback; + private IntPtr _deferredErrorCallbackWindow; + + public DeferredXAudio2ErrorCallback(Action resetToDefaultDeviceCallback) + { + ResetToDefaultDeviceCallback = resetToDefaultDeviceCallback; + + const int WS_CHILD = 0x40000000; + _deferredErrorCallbackWindow = WmImports.CreateWindowExW( + dwExStyle: 0, + lpClassName: _deferredXAudio2CallbackWindowAtom.Value, + lpWindowName: "DeferredXAudio2ErrorCallback", + dwStyle: WS_CHILD, + X: 0, + Y: 0, + nWidth: 1, + nHeight: 1, + hWndParent: WmImports.HWND_MESSAGE, + hMenu: IntPtr.Zero, + hInstance: LoaderApiImports.GetModuleHandleW(null), + lpParam: IntPtr.Zero); + + if (_deferredErrorCallbackWindow == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to create deferred XAudio2 error callback window"); + } + + var handle = GCHandle.Alloc(this, GCHandleType.Normal); + WmImports.SetWindowLongPtrW(_deferredErrorCallbackWindow, WmImports.GWLP_USERDATA, GCHandle.ToIntPtr(handle)); + } + + public void OnCriticalError() + => WmImports.PostMessageW(_deferredErrorCallbackWindow, WM_DEFERRED_ERROR_CALLBACK, IntPtr.Zero, IntPtr.Zero); + + public void Dispose() + { + if (_deferredErrorCallbackWindow != IntPtr.Zero) + { + WmImports.DestroyWindow(_deferredErrorCallbackWindow); + _deferredErrorCallbackWindow = IntPtr.Zero; + } + } + } } }