Commit c85c57a05d356189e642a85f0bf2e19201b8c03e

Ryan C. Gordon 2017-03-29T14:23:39

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.

diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index 7b0e2e3..051ebdd 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 40db3f0..c648b88 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 *) &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);
         }
@@ -483,41 +602,52 @@ WASAPI_FlushCapture(_THIS)
 }
 
 static void
-WASAPI_CloseDevice(_THIS)
+ReleaseWasapiDevice(_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);
-	}
+        this->hidden->client = NULL;
+    }
 
     if (this->hidden->render) {
         IAudioRenderClient_Release(this->hidden->render);
+        this->hidden->render = NULL;
     }
 
-    if (this->hidden->client) {
-        IAudioClient_Release(this->hidden->client);
+    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). */
+    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 30fb573..657d104 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_ */