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.
This commit is contained in:
Ryan C. Gordon 2017-03-29 14:23:39 -04:00
parent f2179944cc
commit c85c57a05d
3 changed files with 204 additions and 61 deletions

View File

@ -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. */

View File

@ -358,10 +358,111 @@ WASAPI_DetectDevices(void)
IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_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)
{

View File

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