From c85c57a05d356189e642a85f0bf2e19201b8c03e Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 29 Mar 2017 14:23:39 -0400 Subject: [PATCH] wasapi: Handle lost audio device endpoints. This gracefully recovers when a device format is changed, and will switch to the new default device if the current one is unplugged, etc. This does not handle when a new default device is added; it only notices if the current default goes away. That will be fixed by implementing the stubbed-out MMNotificationClient_OnDefaultDeviceChanged() function. --- src/audio/SDL_audio.c | 13 +- src/audio/wasapi/SDL_wasapi.c | 251 ++++++++++++++++++++++++++-------- src/audio/wasapi/SDL_wasapi.h | 1 + 3 files changed, 204 insertions(+), 61 deletions(-) diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 7b0e2e371..051ebdd47 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -636,12 +636,9 @@ static int SDLCALL SDL_RunAudio(void *devicep) { SDL_AudioDevice *device = (SDL_AudioDevice *) devicep; - const int silence = (int) device->spec.silence; - const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq); - const int data_len = device->callbackspec.size; + void *udata = device->callbackspec.userdata; + SDL_AudioCallback callback = device->callbackspec.callback; Uint8 *data; - void *udata = device->spec.userdata; - SDL_AudioCallback callback = device->spec.callback; SDL_assert(!device->iscapture); @@ -654,6 +651,8 @@ SDL_RunAudio(void *devicep) /* Loop, filling the audio buffers */ while (!SDL_AtomicGet(&device->shutdown)) { + const int data_len = device->callbackspec.size; + /* Fill the current buffer with sound */ if (!device->stream && SDL_AtomicGet(&device->enabled)) { SDL_assert(data_len == device->spec.size); @@ -675,7 +674,7 @@ SDL_RunAudio(void *devicep) /* !!! FIXME: this should be LockDevice. */ SDL_LockMutex(device->mixer_lock); if (SDL_AtomicGet(&device->paused)) { - SDL_memset(data, silence, data_len); + SDL_memset(data, device->spec.silence, data_len); } else { callback(udata, data, data_len); } @@ -693,6 +692,7 @@ SDL_RunAudio(void *devicep) SDL_assert((got < 0) || (got == device->spec.size)); if (data == NULL) { /* device is having issues... */ + const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq); SDL_Delay(delay); /* wait for as long as this buffer would have played. Maybe device recovers later? */ } else { if (got != device->spec.size) { @@ -704,6 +704,7 @@ SDL_RunAudio(void *devicep) } } else if (data == device->work_buffer) { /* nothing to do; pause like we queued a buffer to play. */ + const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq); SDL_Delay(delay); } else { /* writing directly to the device. */ /* queue this buffer and wait for it to finish playing. */ diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c index 40db3f06c..c648b88a4 100644 --- a/src/audio/wasapi/SDL_wasapi.c +++ b/src/audio/wasapi/SDL_wasapi.c @@ -358,10 +358,111 @@ WASAPI_DetectDevices(void) IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) ¬ification_client); } +static int PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device); +static void ReleaseWasapiDevice(_THIS); + +static SDL_bool +RecoverWasapiDevice(_THIS) +{ + const SDL_AudioSpec oldspec = this->spec; + IMMDevice *device = NULL; + HRESULT ret = S_OK; + + if (this->hidden->is_default_device) { + const EDataFlow dataflow = this->iscapture ? eCapture : eRender; + ReleaseWasapiDevice(this); /* dump the lost device's handles. */ + ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); + if (FAILED(ret)) { + return SDL_FALSE; /* can't find a new default device! */ + } + } else { + device = this->hidden->device; + this->hidden->device = NULL; /* don't release this in ReleaseWasapiDevice(). */ + ReleaseWasapiDevice(this); /* dump the lost device's handles. */ + } + + SDL_assert(device != NULL); + + /* this can fail for lots of reasons, but the most likely is we had a + non-default device that was disconnected, so we can't recover. Default + devices try to reinitialize whatever the new default is, so it's more + likely to carry on here, but this handles a non-default device that + simply had its format changed in the Windows Control Panel. */ + if (PrepWasapiDevice(this, this->iscapture, device) == -1) { + return SDL_FALSE; + } + + /* Since WASAPI requires us to handle all audio conversion, and our + device format might have changed, we might have to add/remove/change + the audio stream that the higher level uses to convert data, so + SDL keeps firing the callback as if nothing happened here. */ + + if ( (this->callbackspec.channels == this->spec.channels) && + (this->callbackspec.format == this->spec.format) && + (this->callbackspec.freq == this->spec.freq) && + (this->callbackspec.samples == this->spec.samples) ) { + /* no need to buffer/convert in an AudioStream! */ + SDL_FreeAudioStream(this->stream); + this->stream = NULL; + } else if ( (oldspec.channels == this->spec.channels) && + (oldspec.format == this->spec.format) && + (oldspec.freq == this->spec.freq) && + (oldspec.samples == this->spec.samples) ) { + /* The existing audio stream is okay to keep using. */ + } else { + /* replace the audiostream for new format */ + SDL_FreeAudioStream(this->stream); + if (this->iscapture) { + this->stream = SDL_NewAudioStream(this->spec.format, + this->spec.channels, this->spec.freq, + this->callbackspec.format, + this->callbackspec.channels, + this->callbackspec.freq); + } else { + this->stream = SDL_NewAudioStream(this->callbackspec.format, + this->callbackspec.channels, + this->callbackspec.freq, this->spec.format, + this->spec.channels, this->spec.freq); + } + + if (!this->stream) { + return SDL_FALSE; + } + } + + /* make sure our scratch buffer can cover the new device spec. */ + if (this->spec.size > this->work_buffer_len) { + Uint8 *ptr = (Uint8 *) SDL_realloc(this->work_buffer, this->spec.size); + if (ptr == NULL) { + SDL_OutOfMemory(); + return SDL_FALSE; + } + this->work_buffer = ptr; + this->work_buffer_len = this->spec.size; + } + + return SDL_TRUE; /* okay, carry on with new device details! */ +} + + +static SDL_bool +TryWasapiAgain(_THIS, const HRESULT err) +{ + SDL_bool retval = SDL_FALSE; + if (err == AUDCLNT_E_DEVICE_INVALIDATED) { + if (SDL_AtomicGet(&this->enabled)) { + retval = RecoverWasapiDevice(this); + } + } + return retval; +} + static int WASAPI_GetPendingBytes(_THIS) { UINT32 frames = 0; + + /* 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. */ } @@ -374,7 +475,13 @@ WASAPI_GetDeviceBuf(_THIS) { /* get an endpoint buffer from WASAPI. */ BYTE *buffer = NULL; - if (FAILED(IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer))) { + HRESULT ret; + + do { + ret = IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer); + } while (TryWasapiAgain(this, ret)); + + if (FAILED(ret)) { IAudioClient_Stop(this->hidden->client); SDL_OpenedAudioDeviceDisconnected(this); /* uhoh. */ SDL_assert(buffer == NULL); @@ -385,11 +492,13 @@ WASAPI_GetDeviceBuf(_THIS) static void WASAPI_PlayDevice(_THIS) { - if (SDL_AtomicGet(&this->enabled)) { /* not shutting down? */ - if (FAILED(IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0))) { - IAudioClient_Stop(this->hidden->client); - SDL_OpenedAudioDeviceDisconnected(this); /* uhoh. */ - } + HRESULT ret = IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0); + if (ret == AUDCLNT_E_DEVICE_INVALIDATED) { + 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. */ } } @@ -399,7 +508,13 @@ WASAPI_WaitDevice(_THIS) const UINT32 maxpadding = this->spec.samples; while (SDL_AtomicGet(&this->enabled)) { UINT32 padding = 0; - if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &padding))) { + 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); } @@ -430,7 +545,10 @@ WASAPI_CaptureFromDevice(_THIS, void *buffer, int buflen) UINT32 frames = 0; DWORD flags = 0; - ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL); + do { + ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL); + } while (TryWasapiAgain(this, ret)); + if ((ret == AUDCLNT_S_BUFFER_EMPTY) || !frames) { WASAPI_WaitDevice(this); } else if (ret == S_OK) { @@ -475,6 +593,7 @@ WASAPI_FlushCapture(_THIS) DWORD flags = 0; HRESULT ret; /* 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 ((ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL)) == S_OK) { IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames); } @@ -482,42 +601,53 @@ WASAPI_FlushCapture(_THIS) } } +static void +ReleaseWasapiDevice(_THIS) +{ + if (this->hidden->client) { + IAudioClient_Stop(this->hidden->client); + this->hidden->client = NULL; + } + + if (this->hidden->render) { + IAudioRenderClient_Release(this->hidden->render); + this->hidden->render = NULL; + } + + if (this->hidden->capture) { + IAudioCaptureClient_Release(this->hidden->capture); + this->hidden->client = NULL; + } + + if (this->hidden->waveformat) { + CoTaskMemFree(this->hidden->waveformat); + this->hidden->waveformat = NULL; + } + + if (this->hidden->device) { + IMMDevice_Release(this->hidden->device); + this->hidden->device = NULL; + } + + if (this->hidden->capturestream) { + SDL_FreeAudioStream(this->hidden->capturestream); + this->hidden->capturestream = NULL; + } +} + static void WASAPI_CloseDevice(_THIS) { /* don't touch this->hidden->task in here; it has to be reverted from our callback thread. We do that in WASAPI_ThreadDeinit(). (likewise for this->hidden->coinitialized). */ - - if (this->hidden->client) { - IAudioClient_Stop(this->hidden->client); - } - - if (this->hidden->render) { - IAudioRenderClient_Release(this->hidden->render); - } - - if (this->hidden->client) { - IAudioClient_Release(this->hidden->client); - } - - if (this->hidden->waveformat) { - CoTaskMemFree(this->hidden->waveformat); - } - - if (this->hidden->device) { - IMMDevice_Release(this->hidden->device); - } - - if (this->hidden->capturestream) { - SDL_FreeAudioStream(this->hidden->capturestream); - } - + ReleaseWasapiDevice(this); SDL_free(this->hidden); } + static int -WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) +PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device) { /* !!! FIXME: we could request an exclusive mode stream, which is lower latency; !!! it will write into the kernel's audio buffer directly instead of @@ -531,10 +661,8 @@ WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) !!! some point. To be sure, defaulting to shared mode is the right thing to !!! do in any case. */ const AUDCLNT_SHAREMODE sharemode = AUDCLNT_SHAREMODE_SHARED; - const EDataFlow dataflow = iscapture ? eCapture : eRender; UINT32 bufsize = 0; /* this is in sample frames, not samples, not bytes. */ REFERENCE_TIME duration = 0; - IMMDevice *device = NULL; IAudioClient *client = NULL; IAudioRenderClient *render = NULL; IAudioCaptureClient *capture = NULL; @@ -544,25 +672,6 @@ WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) SDL_bool valid_format = SDL_FALSE; HRESULT ret = S_OK; - /* Initialize all variables that we clean on shutdown */ - this->hidden = (struct SDL_PrivateAudioData *) - SDL_malloc((sizeof *this->hidden)); - if (this->hidden == NULL) { - return SDL_OutOfMemory(); - } - SDL_zerop(this->hidden); - - if (handle == NULL) { - ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); - } else { - ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device); - } - - if (FAILED(ret)) { - return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret); - } - - SDL_assert(device != NULL); this->hidden->device = device; ret = IMMDevice_Activate(device, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **) &client); @@ -677,6 +786,38 @@ WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) return 0; /* good to go. */ } +static int +WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) +{ + const EDataFlow dataflow = iscapture ? eCapture : eRender; + const SDL_bool is_default_device = (handle == NULL); + IMMDevice *device = NULL; + HRESULT ret = S_OK; + + /* Initialize all variables that we clean on shutdown */ + this->hidden = (struct SDL_PrivateAudioData *) + SDL_malloc((sizeof *this->hidden)); + if (this->hidden == NULL) { + return SDL_OutOfMemory(); + } + SDL_zerop(this->hidden); + + this->hidden->is_default_device = is_default_device; + + if (is_default_device) { + ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); + } else { + ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device); + } + + if (FAILED(ret)) { + return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret); + } + + SDL_assert(device != NULL); + return PrepWasapiDevice(this, iscapture, device); +} + static void WASAPI_ThreadInit(_THIS) { diff --git a/src/audio/wasapi/SDL_wasapi.h b/src/audio/wasapi/SDL_wasapi.h index 30fb573e5..657d1048d 100644 --- a/src/audio/wasapi/SDL_wasapi.h +++ b/src/audio/wasapi/SDL_wasapi.h @@ -39,6 +39,7 @@ struct SDL_PrivateAudioData HANDLE task; SDL_bool coinitialized; int framesize; + SDL_bool is_default_device; }; #endif /* SDL_wasapi_h_ */