Fix disconnecting an audio device potentially crashing/hanging for DirectSound and XAudio2

OpenAL seems to only do this on Windows, Linux does not care. This issue is also only relevant for audio throttle wrt XAudio2/OpenAL.
This commit is contained in:
CasualPokePlayer 2023-12-17 04:09:19 -08:00
parent 7f21cbd029
commit d5fc092c0a
2 changed files with 163 additions and 7 deletions

View File

@ -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<string> 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()
{

View File

@ -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<IntPtr> _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;
}
}
}
}
}