wasapi: Handle lost audio device endpoints.
authorRyan C. Gordon <icculus@icculus.org>
Wed, 29 Mar 2017 14:23:39 -0400
changeset 109483ff1b72962c3
parent 10947 56a0a429d309
child 10949 96539b1f6069
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
src/audio/wasapi/SDL_wasapi.c
src/audio/wasapi/SDL_wasapi.h
     1.1 --- a/src/audio/SDL_audio.c	Wed Mar 29 12:04:17 2017 -0400
     1.2 +++ b/src/audio/SDL_audio.c	Wed Mar 29 14:23:39 2017 -0400
     1.3 @@ -636,12 +636,9 @@
     1.4  SDL_RunAudio(void *devicep)
     1.5  {
     1.6      SDL_AudioDevice *device = (SDL_AudioDevice *) devicep;
     1.7 -    const int silence = (int) device->spec.silence;
     1.8 -    const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq);
     1.9 -    const int data_len = device->callbackspec.size;
    1.10 +    void *udata = device->callbackspec.userdata;
    1.11 +    SDL_AudioCallback callback = device->callbackspec.callback;
    1.12      Uint8 *data;
    1.13 -    void *udata = device->spec.userdata;
    1.14 -    SDL_AudioCallback callback = device->spec.callback;
    1.15  
    1.16      SDL_assert(!device->iscapture);
    1.17  
    1.18 @@ -654,6 +651,8 @@
    1.19  
    1.20      /* Loop, filling the audio buffers */
    1.21      while (!SDL_AtomicGet(&device->shutdown)) {
    1.22 +        const int data_len = device->callbackspec.size;
    1.23 +
    1.24          /* Fill the current buffer with sound */
    1.25          if (!device->stream && SDL_AtomicGet(&device->enabled)) {
    1.26              SDL_assert(data_len == device->spec.size);
    1.27 @@ -675,7 +674,7 @@
    1.28          /* !!! FIXME: this should be LockDevice. */
    1.29          SDL_LockMutex(device->mixer_lock);
    1.30          if (SDL_AtomicGet(&device->paused)) {
    1.31 -            SDL_memset(data, silence, data_len);
    1.32 +            SDL_memset(data, device->spec.silence, data_len);
    1.33          } else {
    1.34              callback(udata, data, data_len);
    1.35          }
    1.36 @@ -693,6 +692,7 @@
    1.37                  SDL_assert((got < 0) || (got == device->spec.size));
    1.38  
    1.39                  if (data == NULL) {  /* device is having issues... */
    1.40 +                    const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq);
    1.41                      SDL_Delay(delay);  /* wait for as long as this buffer would have played. Maybe device recovers later? */
    1.42                  } else {
    1.43                      if (got != device->spec.size) {
    1.44 @@ -704,6 +704,7 @@
    1.45              }
    1.46          } else if (data == device->work_buffer) {
    1.47              /* nothing to do; pause like we queued a buffer to play. */
    1.48 +            const Uint32 delay = ((device->spec.samples * 1000) / device->spec.freq);
    1.49              SDL_Delay(delay);
    1.50          } else {  /* writing directly to the device. */
    1.51              /* queue this buffer and wait for it to finish playing. */
     2.1 --- a/src/audio/wasapi/SDL_wasapi.c	Wed Mar 29 12:04:17 2017 -0400
     2.2 +++ b/src/audio/wasapi/SDL_wasapi.c	Wed Mar 29 14:23:39 2017 -0400
     2.3 @@ -358,10 +358,111 @@
     2.4      IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
     2.5  }
     2.6  
     2.7 +static int PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device);
     2.8 +static void ReleaseWasapiDevice(_THIS);
     2.9 +
    2.10 +static SDL_bool
    2.11 +RecoverWasapiDevice(_THIS)
    2.12 +{
    2.13 +    const SDL_AudioSpec oldspec = this->spec;
    2.14 +    IMMDevice *device = NULL;
    2.15 +    HRESULT ret = S_OK;
    2.16 +
    2.17 +    if (this->hidden->is_default_device) {
    2.18 +        const EDataFlow dataflow = this->iscapture ? eCapture : eRender;
    2.19 +        ReleaseWasapiDevice(this);  /* dump the lost device's handles. */
    2.20 +        ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
    2.21 +        if (FAILED(ret)) {
    2.22 +            return SDL_FALSE;  /* can't find a new default device! */
    2.23 +        }
    2.24 +    } else {
    2.25 +        device = this->hidden->device;
    2.26 +        this->hidden->device = NULL;  /* don't release this in ReleaseWasapiDevice(). */
    2.27 +        ReleaseWasapiDevice(this);  /* dump the lost device's handles. */
    2.28 +    }
    2.29 +
    2.30 +    SDL_assert(device != NULL);
    2.31 +
    2.32 +    /* this can fail for lots of reasons, but the most likely is we had a 
    2.33 +       non-default device that was disconnected, so we can't recover. Default
    2.34 +       devices try to reinitialize whatever the new default is, so it's more
    2.35 +       likely to carry on here, but this handles a non-default device that
    2.36 +       simply had its format changed in the Windows Control Panel. */
    2.37 +    if (PrepWasapiDevice(this, this->iscapture, device) == -1) {
    2.38 +        return SDL_FALSE;
    2.39 +    }
    2.40 +
    2.41 +    /* Since WASAPI requires us to handle all audio conversion, and our
    2.42 +       device format might have changed, we might have to add/remove/change
    2.43 +       the audio stream that the higher level uses to convert data, so
    2.44 +       SDL keeps firing the callback as if nothing happened here. */
    2.45 +
    2.46 +    if ( (this->callbackspec.channels == this->spec.channels) &&
    2.47 +         (this->callbackspec.format == this->spec.format) &&
    2.48 +         (this->callbackspec.freq == this->spec.freq) &&
    2.49 +         (this->callbackspec.samples == this->spec.samples) ) {
    2.50 +        /* no need to buffer/convert in an AudioStream! */
    2.51 +        SDL_FreeAudioStream(this->stream);
    2.52 +        this->stream = NULL;
    2.53 +    } else if ( (oldspec.channels == this->spec.channels) &&
    2.54 +         (oldspec.format == this->spec.format) &&
    2.55 +         (oldspec.freq == this->spec.freq) &&
    2.56 +         (oldspec.samples == this->spec.samples) ) {
    2.57 +        /* The existing audio stream is okay to keep using. */
    2.58 +    } else {
    2.59 +        /* replace the audiostream for new format */
    2.60 +        SDL_FreeAudioStream(this->stream);
    2.61 +        if (this->iscapture) {
    2.62 +            this->stream = SDL_NewAudioStream(this->spec.format,
    2.63 +                                this->spec.channels, this->spec.freq,
    2.64 +                                this->callbackspec.format,
    2.65 +                                this->callbackspec.channels,
    2.66 +                                this->callbackspec.freq);
    2.67 +        } else {
    2.68 +            this->stream = SDL_NewAudioStream(this->callbackspec.format,
    2.69 +                                this->callbackspec.channels,
    2.70 +                                this->callbackspec.freq, this->spec.format,
    2.71 +                                this->spec.channels, this->spec.freq);
    2.72 +        }
    2.73 +
    2.74 +        if (!this->stream) {
    2.75 +            return SDL_FALSE;
    2.76 +        }
    2.77 +    }
    2.78 +
    2.79 +    /* make sure our scratch buffer can cover the new device spec. */
    2.80 +    if (this->spec.size > this->work_buffer_len) {
    2.81 +        Uint8 *ptr = (Uint8 *) SDL_realloc(this->work_buffer, this->spec.size);
    2.82 +        if (ptr == NULL) {
    2.83 +            SDL_OutOfMemory();
    2.84 +            return SDL_FALSE;
    2.85 +        }
    2.86 +        this->work_buffer = ptr;
    2.87 +        this->work_buffer_len = this->spec.size;
    2.88 +    }
    2.89 +
    2.90 +    return SDL_TRUE;  /* okay, carry on with new device details! */
    2.91 +}
    2.92 +
    2.93 +
    2.94 +static SDL_bool
    2.95 +TryWasapiAgain(_THIS, const HRESULT err)
    2.96 +{
    2.97 +    SDL_bool retval = SDL_FALSE;
    2.98 +    if (err == AUDCLNT_E_DEVICE_INVALIDATED) {
    2.99 +        if (SDL_AtomicGet(&this->enabled)) {
   2.100 +            retval = RecoverWasapiDevice(this); 
   2.101 +        }
   2.102 +    }
   2.103 +    return retval;
   2.104 +}
   2.105 +
   2.106  static int
   2.107  WASAPI_GetPendingBytes(_THIS)
   2.108  {
   2.109      UINT32 frames = 0;
   2.110 +
   2.111 +    /* it's okay to fail with AUDCLNT_E_DEVICE_INVALIDATED; we'll try to recover lost devices in the audio thread. */
   2.112      if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &frames))) {
   2.113          return 0;  /* oh well. */
   2.114      }
   2.115 @@ -374,7 +475,13 @@
   2.116  {
   2.117      /* get an endpoint buffer from WASAPI. */
   2.118      BYTE *buffer = NULL;
   2.119 -	if (FAILED(IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer))) {
   2.120 +    HRESULT ret;
   2.121 +
   2.122 +    do {
   2.123 +        ret = IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer);
   2.124 +    } while (TryWasapiAgain(this, ret));
   2.125 +
   2.126 +	if (FAILED(ret)) {
   2.127  		IAudioClient_Stop(this->hidden->client);
   2.128          SDL_OpenedAudioDeviceDisconnected(this);  /* uhoh. */
   2.129          SDL_assert(buffer == NULL);
   2.130 @@ -385,11 +492,13 @@
   2.131  static void
   2.132  WASAPI_PlayDevice(_THIS)
   2.133  {
   2.134 -    if (SDL_AtomicGet(&this->enabled)) {  /* not shutting down? */
   2.135 -        if (FAILED(IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0))) {
   2.136 -            IAudioClient_Stop(this->hidden->client);
   2.137 -            SDL_OpenedAudioDeviceDisconnected(this);  /* uhoh. */
   2.138 -        }
   2.139 +    HRESULT ret = IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0);
   2.140 +    if (ret == AUDCLNT_E_DEVICE_INVALIDATED) {
   2.141 +        ret = S_OK;  /* it's okay if we lost the device here. Catch it later. */
   2.142 +    }
   2.143 +	if (FAILED(ret)) {
   2.144 +        IAudioClient_Stop(this->hidden->client);
   2.145 +        SDL_OpenedAudioDeviceDisconnected(this);  /* uhoh. */
   2.146      }
   2.147  }
   2.148  
   2.149 @@ -399,7 +508,13 @@
   2.150  	const UINT32 maxpadding = this->spec.samples;
   2.151      while (SDL_AtomicGet(&this->enabled)) {
   2.152  		UINT32 padding = 0;
   2.153 -		if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &padding))) {
   2.154 +		HRESULT ret;
   2.155 +
   2.156 +        do {
   2.157 +            ret = IAudioClient_GetCurrentPadding(this->hidden->client, &padding);
   2.158 +        } while (TryWasapiAgain(this, ret));        
   2.159 +        
   2.160 +        if (FAILED(ret)) {
   2.161  		    IAudioClient_Stop(this->hidden->client);
   2.162  			SDL_OpenedAudioDeviceDisconnected(this);
   2.163          }
   2.164 @@ -430,7 +545,10 @@
   2.165          UINT32 frames = 0;
   2.166          DWORD flags = 0;
   2.167  
   2.168 -        ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL);
   2.169 +        do {
   2.170 +            ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL);
   2.171 +        } while (TryWasapiAgain(this, ret));
   2.172 +
   2.173          if ((ret == AUDCLNT_S_BUFFER_EMPTY) || !frames) {
   2.174              WASAPI_WaitDevice(this);
   2.175          } else if (ret == S_OK) {
   2.176 @@ -475,6 +593,7 @@
   2.177          DWORD flags = 0;
   2.178          HRESULT ret;
   2.179          /* just read until we stop getting packets, throwing them away. */
   2.180 +        /* We don't care if we fail with AUDCLNT_E_DEVICE_INVALIDATED here; lost devices will be handled elsewhere. */
   2.181          while ((ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL)) == S_OK) {
   2.182              IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames);
   2.183          }
   2.184 @@ -483,41 +602,52 @@
   2.185  }
   2.186  
   2.187  static void
   2.188 +ReleaseWasapiDevice(_THIS)
   2.189 +{
   2.190 +	if (this->hidden->client) {
   2.191 +        IAudioClient_Stop(this->hidden->client);
   2.192 +        this->hidden->client = NULL;
   2.193 +    }
   2.194 +
   2.195 +    if (this->hidden->render) {
   2.196 +        IAudioRenderClient_Release(this->hidden->render);
   2.197 +        this->hidden->render = NULL;
   2.198 +    }
   2.199 +
   2.200 +    if (this->hidden->capture) {
   2.201 +        IAudioCaptureClient_Release(this->hidden->capture);
   2.202 +        this->hidden->client = NULL;
   2.203 +    }
   2.204 +
   2.205 +    if (this->hidden->waveformat) {
   2.206 +        CoTaskMemFree(this->hidden->waveformat);
   2.207 +        this->hidden->waveformat = NULL;
   2.208 +    }
   2.209 +
   2.210 +    if (this->hidden->device) {
   2.211 +        IMMDevice_Release(this->hidden->device);
   2.212 +        this->hidden->device = NULL;
   2.213 +    }
   2.214 +
   2.215 +    if (this->hidden->capturestream) {
   2.216 +        SDL_FreeAudioStream(this->hidden->capturestream);
   2.217 +        this->hidden->capturestream = NULL;
   2.218 +    }
   2.219 +}
   2.220 +
   2.221 +static void
   2.222  WASAPI_CloseDevice(_THIS)
   2.223  {
   2.224      /* don't touch this->hidden->task in here; it has to be reverted from
   2.225        our callback thread. We do that in WASAPI_ThreadDeinit().
   2.226        (likewise for this->hidden->coinitialized). */
   2.227 -
   2.228 -	if (this->hidden->client) {
   2.229 -        IAudioClient_Stop(this->hidden->client);
   2.230 -	}
   2.231 -
   2.232 -    if (this->hidden->render) {
   2.233 -        IAudioRenderClient_Release(this->hidden->render);
   2.234 -    }
   2.235 -
   2.236 -    if (this->hidden->client) {
   2.237 -        IAudioClient_Release(this->hidden->client);
   2.238 -    }
   2.239 -
   2.240 -    if (this->hidden->waveformat) {
   2.241 -        CoTaskMemFree(this->hidden->waveformat);
   2.242 -    }
   2.243 -
   2.244 -    if (this->hidden->device) {
   2.245 -        IMMDevice_Release(this->hidden->device);
   2.246 -    }
   2.247 -
   2.248 -    if (this->hidden->capturestream) {
   2.249 -        SDL_FreeAudioStream(this->hidden->capturestream);
   2.250 -    }
   2.251 -
   2.252 +    ReleaseWasapiDevice(this);
   2.253      SDL_free(this->hidden);
   2.254  }
   2.255  
   2.256 +
   2.257  static int
   2.258 -WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
   2.259 +PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device)
   2.260  {
   2.261      /* !!! FIXME: we could request an exclusive mode stream, which is lower latency;
   2.262         !!!  it will write into the kernel's audio buffer directly instead of
   2.263 @@ -531,10 +661,8 @@
   2.264         !!!  some point. To be sure, defaulting to shared mode is the right thing to
   2.265         !!!  do in any case. */
   2.266      const AUDCLNT_SHAREMODE sharemode = AUDCLNT_SHAREMODE_SHARED;
   2.267 -    const EDataFlow dataflow = iscapture ? eCapture : eRender;
   2.268      UINT32 bufsize = 0;  /* this is in sample frames, not samples, not bytes. */
   2.269      REFERENCE_TIME duration = 0;
   2.270 -    IMMDevice *device = NULL;
   2.271      IAudioClient *client = NULL;
   2.272      IAudioRenderClient *render = NULL;
   2.273      IAudioCaptureClient *capture = NULL;
   2.274 @@ -544,25 +672,6 @@
   2.275      SDL_bool valid_format = SDL_FALSE;
   2.276      HRESULT ret = S_OK;
   2.277  
   2.278 -    /* Initialize all variables that we clean on shutdown */
   2.279 -    this->hidden = (struct SDL_PrivateAudioData *)
   2.280 -        SDL_malloc((sizeof *this->hidden));
   2.281 -    if (this->hidden == NULL) {
   2.282 -        return SDL_OutOfMemory();
   2.283 -    }
   2.284 -    SDL_zerop(this->hidden);
   2.285 -
   2.286 -    if (handle == NULL) {
   2.287 -        ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
   2.288 -	} else {
   2.289 -        ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device);
   2.290 -    }
   2.291 -
   2.292 -    if (FAILED(ret)) {
   2.293 -        return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
   2.294 -    }
   2.295 -
   2.296 -    SDL_assert(device != NULL);
   2.297      this->hidden->device = device;
   2.298  
   2.299      ret = IMMDevice_Activate(device, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **) &client);
   2.300 @@ -677,6 +786,38 @@
   2.301      return 0;  /* good to go. */
   2.302  }
   2.303  
   2.304 +static int
   2.305 +WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
   2.306 +{
   2.307 +    const EDataFlow dataflow = iscapture ? eCapture : eRender;
   2.308 +    const SDL_bool is_default_device = (handle == NULL);
   2.309 +    IMMDevice *device = NULL;
   2.310 +    HRESULT ret = S_OK;
   2.311 +
   2.312 +    /* Initialize all variables that we clean on shutdown */
   2.313 +    this->hidden = (struct SDL_PrivateAudioData *)
   2.314 +        SDL_malloc((sizeof *this->hidden));
   2.315 +    if (this->hidden == NULL) {
   2.316 +        return SDL_OutOfMemory();
   2.317 +    }
   2.318 +    SDL_zerop(this->hidden);
   2.319 +
   2.320 +    this->hidden->is_default_device = is_default_device;
   2.321 +
   2.322 +    if (is_default_device) {
   2.323 +        ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
   2.324 +	} else {
   2.325 +        ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device);
   2.326 +    }
   2.327 +
   2.328 +    if (FAILED(ret)) {
   2.329 +        return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
   2.330 +    }
   2.331 +
   2.332 +    SDL_assert(device != NULL);
   2.333 +    return PrepWasapiDevice(this, iscapture, device);
   2.334 +}
   2.335 +
   2.336  static void
   2.337  WASAPI_ThreadInit(_THIS)
   2.338  {
     3.1 --- a/src/audio/wasapi/SDL_wasapi.h	Wed Mar 29 12:04:17 2017 -0400
     3.2 +++ b/src/audio/wasapi/SDL_wasapi.h	Wed Mar 29 14:23:39 2017 -0400
     3.3 @@ -39,6 +39,7 @@
     3.4      HANDLE task;
     3.5      SDL_bool coinitialized;
     3.6      int framesize;
     3.7 +    SDL_bool is_default_device;
     3.8  };
     3.9  
    3.10  #endif /* SDL_wasapi_h_ */