wasapi: deal with default device changes, and more robust failure recovery.
authorRyan C. Gordon <icculus@icculus.org>
Thu, 30 Mar 2017 16:33:47 -0400
changeset 1094996539b1f6069
parent 10948 3ff1b72962c3
child 10950 fd1b8fd3b937
wasapi: deal with default device changes, and more robust failure recovery.
src/audio/wasapi/SDL_wasapi.c
src/audio/wasapi/SDL_wasapi.h
     1.1 --- a/src/audio/wasapi/SDL_wasapi.c	Wed Mar 29 14:23:39 2017 -0400
     1.2 +++ b/src/audio/wasapi/SDL_wasapi.c	Thu Mar 30 16:33:47 2017 -0400
     1.3 @@ -42,6 +42,10 @@
     1.4  /* This is global to the WASAPI target, to handle hotplug and default device lookup. */
     1.5  static IMMDeviceEnumerator *enumerator = NULL;
     1.6  
     1.7 +/* these increment as default devices change. Opened default devices pick up changes in their threads. */
     1.8 +static SDL_atomic_t default_playback_generation;
     1.9 +static SDL_atomic_t default_capture_generation;
    1.10 +
    1.11  /* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */
    1.12  typedef struct DevIdList
    1.13  {
    1.14 @@ -130,25 +134,29 @@
    1.15  static HRESULT STDMETHODCALLTYPE
    1.16  SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
    1.17  {
    1.18 -#if 0
    1.19 -	const char *flowstr = "?";
    1.20 -	char *utf8;
    1.21 -	SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
    1.22  	if (role != SDL_WASAPI_role) {
    1.23  		return S_OK;  /* ignore it. */
    1.24  	}
    1.25  
    1.26 -	// !!! FIXME: should probably switch endpoints if we have a default device opened; it's not clear how trivial this is, though.
    1.27 -	// !!! FIXME:  also not clear yet how painful it is to switch when someone opens a tablet's speaker and then plugs in headphones.
    1.28 -	utf8 = pwstrDeviceId ? WIN_StringToUTF8(pwstrDeviceId) : NULL;
    1.29 +    /* Increment the "generation," so opened devices will pick this up in their threads. */
    1.30  	switch (flow) {
    1.31 -	case eRender: flowstr = "RENDER"; break;
    1.32 -	case eCapture: flowstr = "CAPTURE"; break;
    1.33 -	case eAll: flowstr = "ALL"; break;
    1.34 +    	case eRender:
    1.35 +            SDL_AtomicAdd(&default_playback_generation, 1);
    1.36 +            break;
    1.37 +
    1.38 +	    case eCapture:
    1.39 +            SDL_AtomicAdd(&default_capture_generation, 1);
    1.40 +            break;
    1.41 +
    1.42 +    	case eAll:
    1.43 +            SDL_AtomicAdd(&default_playback_generation, 1);
    1.44 +            SDL_AtomicAdd(&default_capture_generation, 1);
    1.45 +            break;
    1.46 +
    1.47 +        default:
    1.48 +            SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!");
    1.49 +            break;
    1.50  	}
    1.51 -	SDL_Log("WASAPI: OnDefaultDeviceChanged! '%s' [%s]", utf8, flowstr);
    1.52 -	SDL_free(utf8);
    1.53 -#endif
    1.54  
    1.55  	return S_OK;
    1.56  }
    1.57 @@ -358,6 +366,37 @@
    1.58      IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
    1.59  }
    1.60  
    1.61 +static int
    1.62 +WASAPI_GetPendingBytes(_THIS)
    1.63 +{
    1.64 +    UINT32 frames = 0;
    1.65 +
    1.66 +    /* it's okay to fail here; we'll deal with failures in the audio thread. */
    1.67 +    if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &frames))) {
    1.68 +        return 0;  /* oh well. */
    1.69 +    }
    1.70 +
    1.71 +    return ((int) frames) * this->hidden->framesize;
    1.72 +}
    1.73 +
    1.74 +static SDL_INLINE SDL_bool
    1.75 +WasapiFailed(_THIS, const HRESULT err)
    1.76 +{
    1.77 +    if (err == S_OK) {
    1.78 +        return SDL_FALSE;
    1.79 +    }
    1.80 +
    1.81 +    if (err == AUDCLNT_E_DEVICE_INVALIDATED) {
    1.82 +        this->hidden->device_lost = SDL_TRUE;
    1.83 +    } else if (SDL_AtomicGet(&this->enabled)) {
    1.84 +    	IAudioClient_Stop(this->hidden->client);
    1.85 +	    SDL_OpenedAudioDeviceDisconnected(this);
    1.86 +        SDL_assert(!SDL_AtomicGet(&this->enabled));
    1.87 +    }
    1.88 +
    1.89 +    return SDL_TRUE;
    1.90 +}
    1.91 +
    1.92  static int PrepWasapiDevice(_THIS, const int iscapture, IMMDevice *device);
    1.93  static void ReleaseWasapiDevice(_THIS);
    1.94  
    1.95 @@ -368,9 +407,10 @@
    1.96      IMMDevice *device = NULL;
    1.97      HRESULT ret = S_OK;
    1.98  
    1.99 -    if (this->hidden->is_default_device) {
   1.100 +    if (this->hidden->default_device_generation) {
   1.101          const EDataFlow dataflow = this->iscapture ? eCapture : eRender;
   1.102          ReleaseWasapiDevice(this);  /* dump the lost device's handles. */
   1.103 +        this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ?  &default_capture_generation : &default_playback_generation);
   1.104          ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
   1.105          if (FAILED(ret)) {
   1.106              return SDL_FALSE;  /* can't find a new default device! */
   1.107 @@ -406,8 +446,7 @@
   1.108          this->stream = NULL;
   1.109      } else if ( (oldspec.channels == this->spec.channels) &&
   1.110           (oldspec.format == this->spec.format) &&
   1.111 -         (oldspec.freq == this->spec.freq) &&
   1.112 -         (oldspec.samples == this->spec.samples) ) {
   1.113 +         (oldspec.freq == this->spec.freq) ) {
   1.114          /* The existing audio stream is okay to keep using. */
   1.115      } else {
   1.116          /* replace the audiostream for new format */
   1.117 @@ -441,33 +480,29 @@
   1.118          this->work_buffer_len = this->spec.size;
   1.119      }
   1.120  
   1.121 +    this->hidden->device_lost = SDL_FALSE;
   1.122 +
   1.123      return SDL_TRUE;  /* okay, carry on with new device details! */
   1.124  }
   1.125  
   1.126 -
   1.127  static SDL_bool
   1.128 -TryWasapiAgain(_THIS, const HRESULT err)
   1.129 +RecoverWasapiIfLost(_THIS)
   1.130  {
   1.131 -    SDL_bool retval = SDL_FALSE;
   1.132 -    if (err == AUDCLNT_E_DEVICE_INVALIDATED) {
   1.133 -        if (SDL_AtomicGet(&this->enabled)) {
   1.134 -            retval = RecoverWasapiDevice(this); 
   1.135 +    const int generation = this->hidden->default_device_generation;
   1.136 +    SDL_bool lost = this->hidden->device_lost;
   1.137 +
   1.138 +    if (!SDL_AtomicGet(&this->enabled)) {
   1.139 +        return SDL_FALSE;  /* already failed. */
   1.140 +    }
   1.141 +
   1.142 +    if (!lost && (generation > 0)) { /* is a default device? */
   1.143 +        const int newgen = SDL_AtomicGet(this->iscapture ? &default_capture_generation : &default_playback_generation);
   1.144 +        if (generation != newgen) {  /* the desired default device was changed, jump over to it. */
   1.145 +            lost = SDL_TRUE;
   1.146          }
   1.147      }
   1.148 -    return retval;
   1.149 -}
   1.150  
   1.151 -static int
   1.152 -WASAPI_GetPendingBytes(_THIS)
   1.153 -{
   1.154 -    UINT32 frames = 0;
   1.155 -
   1.156 -    /* it's okay to fail with AUDCLNT_E_DEVICE_INVALIDATED; we'll try to recover lost devices in the audio thread. */
   1.157 -    if (FAILED(IAudioClient_GetCurrentPadding(this->hidden->client, &frames))) {
   1.158 -        return 0;  /* oh well. */
   1.159 -    }
   1.160 -
   1.161 -    return ((int) frames) * this->hidden->framesize;
   1.162 +    return lost ? RecoverWasapiDevice(this) : SDL_TRUE;
   1.163  }
   1.164  
   1.165  static Uint8 *
   1.166 @@ -475,56 +510,38 @@
   1.167  {
   1.168      /* get an endpoint buffer from WASAPI. */
   1.169      BYTE *buffer = NULL;
   1.170 -    HRESULT ret;
   1.171  
   1.172 -    do {
   1.173 -        ret = IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer);
   1.174 -    } while (TryWasapiAgain(this, ret));
   1.175 -
   1.176 -	if (FAILED(ret)) {
   1.177 -		IAudioClient_Stop(this->hidden->client);
   1.178 -        SDL_OpenedAudioDeviceDisconnected(this);  /* uhoh. */
   1.179 +    while (RecoverWasapiIfLost(this)) {
   1.180 +        if (!WasapiFailed(this, IAudioRenderClient_GetBuffer(this->hidden->render, this->spec.samples, &buffer))) {
   1.181 +            return (Uint8 *) buffer;
   1.182 +        }
   1.183          SDL_assert(buffer == NULL);
   1.184      }
   1.185 +
   1.186      return (Uint8 *) buffer;
   1.187  }
   1.188  
   1.189  static void
   1.190  WASAPI_PlayDevice(_THIS)
   1.191  {
   1.192 -    HRESULT ret = IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0);
   1.193 -    if (ret == AUDCLNT_E_DEVICE_INVALIDATED) {
   1.194 -        ret = S_OK;  /* it's okay if we lost the device here. Catch it later. */
   1.195 -    }
   1.196 -	if (FAILED(ret)) {
   1.197 -        IAudioClient_Stop(this->hidden->client);
   1.198 -        SDL_OpenedAudioDeviceDisconnected(this);  /* uhoh. */
   1.199 -    }
   1.200 +    /* WasapiFailed() will mark the device for reacquisition or removal elsewhere. */
   1.201 +    WasapiFailed(this, IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0));
   1.202  }
   1.203  
   1.204  static void
   1.205  WASAPI_WaitDevice(_THIS)
   1.206  {
   1.207  	const UINT32 maxpadding = this->spec.samples;
   1.208 -    while (SDL_AtomicGet(&this->enabled)) {
   1.209 +    while (RecoverWasapiIfLost(this)) {
   1.210  		UINT32 padding = 0;
   1.211 -		HRESULT ret;
   1.212  
   1.213 -        do {
   1.214 -            ret = IAudioClient_GetCurrentPadding(this->hidden->client, &padding);
   1.215 -        } while (TryWasapiAgain(this, ret));        
   1.216 -        
   1.217 -        if (FAILED(ret)) {
   1.218 -		    IAudioClient_Stop(this->hidden->client);
   1.219 -			SDL_OpenedAudioDeviceDisconnected(this);
   1.220 +        if (!WasapiFailed(this, IAudioClient_GetCurrentPadding(this->hidden->client, &padding))) {
   1.221 +            if (padding <= maxpadding) {
   1.222 +	            break;
   1.223 +            }
   1.224 +		    /* Sleep long enough for half the buffer to be free. */
   1.225 +    		SDL_Delay(((padding - maxpadding) * 1000) / this->spec.freq);
   1.226          }
   1.227 -
   1.228 -        if (padding <= maxpadding) {
   1.229 -			break;
   1.230 -		}
   1.231 -
   1.232 -		/* Sleep long enough for half the buffer to be free. */
   1.233 -		SDL_Delay(((padding - maxpadding) * 1000) / this->spec.freq);
   1.234  	}
   1.235  }
   1.236  
   1.237 @@ -539,15 +556,14 @@
   1.238          return cpy;
   1.239      }
   1.240  
   1.241 -    while (SDL_AtomicGet(&this->enabled)) {
   1.242 +    while (RecoverWasapiIfLost(this)) {
   1.243          HRESULT ret;
   1.244          BYTE *ptr = NULL;
   1.245          UINT32 frames = 0;
   1.246          DWORD flags = 0;
   1.247  
   1.248 -        do {
   1.249 -            ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL);
   1.250 -        } while (TryWasapiAgain(this, ret));
   1.251 +        ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL);
   1.252 +        WasapiFailed(this, ret); /* mark device lost/failed if necessary. */
   1.253  
   1.254          if ((ret == AUDCLNT_S_BUFFER_EMPTY) || !frames) {
   1.255              WASAPI_WaitDevice(this);
   1.256 @@ -574,10 +590,10 @@
   1.257                  }
   1.258              }
   1.259  
   1.260 -            IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames);
   1.261 +            ret = IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames);
   1.262 +            WasapiFailed(this, ret); /* mark device lost/failed if necessary. */
   1.263 +
   1.264              return cpy;
   1.265 -        } else {
   1.266 -            break;  /* something totally failed. */
   1.267          }
   1.268      }
   1.269  
   1.270 @@ -587,15 +603,16 @@
   1.271  static void
   1.272  WASAPI_FlushCapture(_THIS)
   1.273  {
   1.274 -    if (SDL_AtomicGet(&this->enabled)) {
   1.275 +    if (RecoverWasapiIfLost(this)) {
   1.276          BYTE *ptr = NULL;
   1.277          UINT32 frames = 0;
   1.278          DWORD flags = 0;
   1.279 -        HRESULT ret;
   1.280 +
   1.281          /* just read until we stop getting packets, throwing them away. */
   1.282 -        /* We don't care if we fail with AUDCLNT_E_DEVICE_INVALIDATED here; lost devices will be handled elsewhere. */
   1.283 -        while ((ret = IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL)) == S_OK) {
   1.284 -            IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames);
   1.285 +        while (!WasapiFailed(this, IAudioCaptureClient_GetBuffer(this->hidden->capture, &ptr, &frames, &flags, NULL, NULL))) {
   1.286 +            if (WasapiFailed(this, IAudioCaptureClient_ReleaseBuffer(this->hidden->capture, frames))) {
   1.287 +                break;
   1.288 +            }
   1.289          }
   1.290          SDL_AudioStreamClear(this->hidden->capturestream);
   1.291      }
   1.292 @@ -789,7 +806,6 @@
   1.293  static int
   1.294  WASAPI_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
   1.295  {
   1.296 -    const EDataFlow dataflow = iscapture ? eCapture : eRender;
   1.297      const SDL_bool is_default_device = (handle == NULL);
   1.298      IMMDevice *device = NULL;
   1.299      HRESULT ret = S_OK;
   1.300 @@ -802,9 +818,9 @@
   1.301      }
   1.302      SDL_zerop(this->hidden);
   1.303  
   1.304 -    this->hidden->is_default_device = is_default_device;
   1.305 -
   1.306      if (is_default_device) {
   1.307 +        const EDataFlow dataflow = iscapture ? eCapture : eRender;
   1.308 +        this->hidden->default_device_generation = SDL_AtomicGet(iscapture ? &default_capture_generation : &default_playback_generation);
   1.309          ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
   1.310  	} else {
   1.311          ret = IMMDeviceEnumerator_GetDevice(enumerator, (LPCWSTR) handle, &device);
   1.312 @@ -889,6 +905,9 @@
   1.313  		return SDL_SetError("WASAPI support requires Windows Vista or later");
   1.314  	}
   1.315  
   1.316 +    SDL_AtomicSet(&default_playback_generation, 1);
   1.317 +    SDL_AtomicSet(&default_capture_generation, 1);
   1.318 +
   1.319  	if (FAILED(WIN_CoInitialize())) {
   1.320          SDL_SetError("WASAPI: CoInitialize() failed");
   1.321          return 0;
     2.1 --- a/src/audio/wasapi/SDL_wasapi.h	Wed Mar 29 14:23:39 2017 -0400
     2.2 +++ b/src/audio/wasapi/SDL_wasapi.h	Thu Mar 30 16:33:47 2017 -0400
     2.3 @@ -39,7 +39,8 @@
     2.4      HANDLE task;
     2.5      SDL_bool coinitialized;
     2.6      int framesize;
     2.7 -    SDL_bool is_default_device;
     2.8 +    int default_device_generation;
     2.9 +    SDL_bool device_lost;
    2.10  };
    2.11  
    2.12  #endif /* SDL_wasapi_h_ */