wasapi: deal with default device changes, and more robust failure recovery.

This commit is contained in:
Ryan C. Gordon 2017-03-30 16:33:47 -04:00
parent c85c57a05d
commit 028716e79f
2 changed files with 103 additions and 83 deletions

View File

@ -42,6 +42,10 @@ static const ERole SDL_WASAPI_role = eConsole; /* !!! FIXME: should this be eMu
/* This is global to the WASAPI target, to handle hotplug and default device lookup. */ /* This is global to the WASAPI target, to handle hotplug and default device lookup. */
static IMMDeviceEnumerator *enumerator = NULL; static IMMDeviceEnumerator *enumerator = NULL;
/* these increment as default devices change. Opened default devices pick up changes in their threads. */
static SDL_atomic_t default_playback_generation;
static SDL_atomic_t default_capture_generation;
/* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */ /* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */
typedef struct DevIdList typedef struct DevIdList
{ {
@ -130,25 +134,29 @@ SDLMMNotificationClient_Release(IMMNotificationClient *ithis)
static HRESULT STDMETHODCALLTYPE static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
{ {
#if 0
const char *flowstr = "?";
char *utf8;
SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
if (role != SDL_WASAPI_role) { if (role != SDL_WASAPI_role) {
return S_OK; /* ignore it. */ return S_OK; /* ignore it. */
} }
// !!! FIXME: should probably switch endpoints if we have a default device opened; it's not clear how trivial this is, though. /* Increment the "generation," so opened devices will pick this up in their threads. */
// !!! FIXME: also not clear yet how painful it is to switch when someone opens a tablet's speaker and then plugs in headphones.
utf8 = pwstrDeviceId ? WIN_StringToUTF8(pwstrDeviceId) : NULL;
switch (flow) { switch (flow) {
case eRender: flowstr = "RENDER"; break; case eRender:
case eCapture: flowstr = "CAPTURE"; break; SDL_AtomicAdd(&default_playback_generation, 1);
case eAll: flowstr = "ALL"; break; break;
case eCapture:
SDL_AtomicAdd(&default_capture_generation, 1);
break;
case eAll:
SDL_AtomicAdd(&default_playback_generation, 1);
SDL_AtomicAdd(&default_capture_generation, 1);
break;
default:
SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!");
break;
} }
SDL_Log("WASAPI: OnDefaultDeviceChanged! '%s' [%s]", utf8, flowstr);
SDL_free(utf8);
#endif
return S_OK; return S_OK;
} }
@ -358,6 +366,37 @@ WASAPI_DetectDevices(void)
IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client); IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
} }
static int
WASAPI_GetPendingBytes(_THIS)
{
UINT32 frames = 0;
/* it's okay to fail here; we'll deal with failures in the audio thread. */
if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &frames))) {
return 0; /* oh well. */
}
return ((int) frames) * this->hidden->framesize;
}
static SDL_INLINE SDL_bool
WasapiFailed(_THIS, const HRESULT err)
{
if (err == S_OK) {
return SDL_FALSE;
}
if (err == AUDCLNT_E_DEVICE_INVALIDATED) {
this->hidden->device_lost = SDL_TRUE;
} else if (SDL_AtomicGet(&this->enabled)) {
IAudioClient_Stop(this->hidden->client);
SDL_OpenedAudioDeviceDisconnected(this);
SDL_assert(!SDL_AtomicGet(&this->enabled));
}
return SDL_TRUE;
}
static int PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device); static int PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device);
static void ReleaseWasapiDevice(_THIS); static void ReleaseWasapiDevice(_THIS);
@ -368,9 +407,10 @@ RecoverWasapiDevice(_THIS)
IMMDevice *device = NULL; IMMDevice *device = NULL;
HRESULT ret = S_OK; HRESULT ret = S_OK;
if (this->hidden->is_default_device) { if (this->hidden->default_device_generation) {
const EDataFlow dataflow = this->iscapture ? eCapture : eRender; const EDataFlow dataflow = this->iscapture ? eCapture : eRender;
ReleaseWasapiDevice(this); /* dump the lost device's handles. */ ReleaseWasapiDevice(this); /* dump the lost device's handles. */
this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ? &default_capture_generation : &default_playback_generation);
ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
if (FAILED(ret)) { if (FAILED(ret)) {
return SDL_FALSE; /* can't find a new default device! */ return SDL_FALSE; /* can't find a new default device! */
@ -406,8 +446,7 @@ RecoverWasapiDevice(_THIS)
this->stream = NULL; this->stream = NULL;
} else if ( (oldspec.channels == this->spec.channels) && } else if ( (oldspec.channels == this->spec.channels) &&
(oldspec.format == this->spec.format) && (oldspec.format == this->spec.format) &&
(oldspec.freq == this->spec.freq) && (oldspec.freq == this->spec.freq) ) {
(oldspec.samples == this->spec.samples) ) {
/* The existing audio stream is okay to keep using. */ /* The existing audio stream is okay to keep using. */
} else { } else {
/* replace the audiostream for new format */ /* replace the audiostream for new format */
@ -441,33 +480,29 @@ RecoverWasapiDevice(_THIS)
this->work_buffer_len = this->spec.size; this->work_buffer_len = this->spec.size;
} }
this->hidden->device_lost = SDL_FALSE;
return SDL_TRUE; /* okay, carry on with new device details! */ return SDL_TRUE; /* okay, carry on with new device details! */
} }
static SDL_bool static SDL_bool
TryWasapiAgain(_THIS, const HRESULT err) RecoverWasapiIfLost(_THIS)
{ {
SDL_bool retval = SDL_FALSE; const int generation = this->hidden->default_device_generation;
if (err == AUDCLNT_E_DEVICE_INVALIDATED) { SDL_bool lost = this->hidden->device_lost;
if (SDL_AtomicGet(&this->enabled)) {
retval = RecoverWasapiDevice(this); if (!SDL_AtomicGet(&this->enabled)) {
} return SDL_FALSE; /* already failed. */
}
return retval;
} }
static int if (!lost && (generation > 0)) { /* is a default device? */
WASAPI_GetPendingBytes(_THIS) const int newgen = SDL_AtomicGet(this->iscapture ? &default_capture_generation : &default_playback_generation);
{ if (generation != newgen) { /* the desired default device was changed, jump over to it. */
UINT32 frames = 0; lost = SDL_TRUE;
}
/* it's okay to fail with AUDCLNT_E_DEVICE_INVALIDATED; we'll try to recover lost devices in the audio thread. */
if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &frames))) {
return 0; /* oh well. */
} }
return ((int) frames) * this->hidden->framesize; return lost ? RecoverWasapiDevice(this) : SDL_TRUE;
} }
static Uint8 * static Uint8 *
@ -475,58 +510,40 @@ WASAPI_GetDeviceBuf(_THIS)
{ {
/* get an endpoint buffer from WASAPI. */ /* get an endpoint buffer from WASAPI. */
BYTE *buffer = NULL; BYTE *buffer = NULL;
HRESULT ret;
do { while (RecoverWasapiIfLost(this)) {
ret = IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer); if (!WasapiFailed(this, IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer))) {
} while (TryWasapiAgain(this, ret)); return (Uint8 *) buffer;
}
if (FAILED(ret)) {
IAudioClient_Stop(this->hidden->client);
SDL_OpenedAudioDeviceDisconnected(this); /* uhoh. */
SDL_assert(buffer == NULL); SDL_assert(buffer == NULL);
} }
return (Uint8 *) buffer; return (Uint8 *) buffer;
} }
static void static void
WASAPI_PlayDevice(_THIS) WASAPI_PlayDevice(_THIS)
{ {
HRESULT ret = IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0); /* WasapiFailed() will mark the device for reacquisition or removal elsewhere. */
if (ret == AUDCLNT_E_DEVICE_INVALIDATED) { WasapiFailed(this, IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0));
ret = S_OK; /* it's okay if we lost the device here. Catch it later. */
}
if (FAILED(ret)) {
IAudioClient_Stop(this->hidden->client);
SDL_OpenedAudioDeviceDisconnected(this); /* uhoh. */
}
} }
static void static void
WASAPI_WaitDevice(_THIS) WASAPI_WaitDevice(_THIS)
{ {
const UINT32 maxpadding = this->spec.samples; const UINT32 maxpadding = this->spec.samples;
while (SDL_AtomicGet(&this->enabled)) { while (RecoverWasapiIfLost(this)) {
UINT32 padding = 0; UINT32 padding = 0;
HRESULT ret;
do {
ret = IAudioClient_GetCurrentPadding(this->hidden->client, &padding);
} while (TryWasapiAgain(this, ret));
if (FAILED(ret)) {
IAudioClient_Stop(this->hidden->client);
SDL_OpenedAudioDeviceDisconnected(this);
}
if (!WasapiFailed(this, IAudioClient_GetCurrentPadding(this->hidden->client, &padding))) {
if (padding <= maxpadding) { if (padding <= maxpadding) {
break; break;
} }
/* Sleep long enough for half the buffer to be free. */ /* Sleep long enough for half the buffer to be free. */
SDL_Delay(((padding - maxpadding) * 1000) / this->spec.freq); SDL_Delay(((padding - maxpadding) * 1000) / this->spec.freq);
} }
} }
}
static int static int
WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen) WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen)
@ -539,15 +556,14 @@ WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen)
return cpy; return cpy;
} }
while (SDL_AtomicGet(&this->enabled)) { while (RecoverWasapiIfLost(this)) {
HRESULT ret; HRESULT ret;
BYTE *ptr = NULL; BYTE *ptr = NULL;
UINT32 frames = 0; UINT32 frames = 0;
DWORD flags = 0; DWORD flags = 0;
do {
ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL); ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL);
} while (TryWasapiAgain(this, ret)); WasapiFailed(this, ret); /* mark device lost/failed if necessary. */
if ((ret == AUDCLNT_S_BUFFER_EMPTY) || !frames) { if ((ret == AUDCLNT_S_BUFFER_EMPTY) || !frames) {
WASAPI_WaitDevice(this); WASAPI_WaitDevice(this);
@ -574,10 +590,10 @@ WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen)
} }
} }
IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames); ret = IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames);
WasapiFailed(this, ret); /* mark device lost/failed if necessary. */
return cpy; return cpy;
} else {
break; /* something totally failed. */
} }
} }
@ -587,15 +603,16 @@ WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen)
static void static void
WASAPI_FlushCapture(_THIS) WASAPI_FlushCapture(_THIS)
{ {
if (SDL_AtomicGet(&this->enabled)) { if (RecoverWasapiIfLost(this)) {
BYTE *ptr = NULL; BYTE *ptr = NULL;
UINT32 frames = 0; UINT32 frames = 0;
DWORD flags = 0; DWORD flags = 0;
HRESULT ret;
/* just read until we stop getting packets, throwing them away. */ /* just read until we stop getting packets, throwing them away. */
/* We don't care if we fail with AUDCLNT_E_DEVICE_INVALIDATED here; lost devices will be handled elsewhere. */ while (!WasapiFailed(this, IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL))) {
while ((ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL)) == S_OK) { if (WasapiFailed(this, IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames))) {
IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames); break;
}
} }
SDL_AudioStreamClear(this->hidden->capturestream); SDL_AudioStreamClear(this->hidden->capturestream);
} }
@ -789,7 +806,6 @@ PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device)
static int static int
WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
{ {
const EDataFlow dataflow = iscapture ? eCapture : eRender;
const SDL_bool is_default_device = (handle == NULL); const SDL_bool is_default_device = (handle == NULL);
IMMDevice *device = NULL; IMMDevice *device = NULL;
HRESULT ret = S_OK; HRESULT ret = S_OK;
@ -802,9 +818,9 @@ WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
} }
SDL_zerop(this->hidden); SDL_zerop(this->hidden);
this->hidden->is_default_device = is_default_device;
if (is_default_device) { if (is_default_device) {
const EDataFlow dataflow = iscapture ? eCapture : eRender;
this->hidden->default_device_generation = SDL_AtomicGet(iscapture ? &default_capture_generation : &default_playback_generation);
ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
} else { } else {
ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device); ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device);
@ -889,6 +905,9 @@ WASAPI_Init(SDL_AudioDriverImpl * impl)
return SDL_SetError("WASAPI support requires Windows Vista or later"); return SDL_SetError("WASAPI support requires Windows Vista or later");
} }
SDL_AtomicSet(&default_playback_generation, 1);
SDL_AtomicSet(&default_capture_generation, 1);
if (FAILED(WIN_CoInitialize())) { if (FAILED(WIN_CoInitialize())) {
SDL_SetError("WASAPI: CoInitialize() failed"); SDL_SetError("WASAPI: CoInitialize() failed");
return 0; return 0;

View File

@ -39,7 +39,8 @@ struct SDL_PrivateAudioData
HANDLE task; HANDLE task;
SDL_bool coinitialized; SDL_bool coinitialized;
int framesize; int framesize;
SDL_bool is_default_device; int default_device_generation;
SDL_bool device_lost;
}; };
#endif /* SDL_wasapi_h_ */ #endif /* SDL_wasapi_h_ */