mirror of
https://github.com/Relintai/sdl2_frt.git
synced 2024-12-16 11:06:49 +01:00
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:
parent
f2179944cc
commit
c85c57a05d
@ -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. */
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -39,6 +39,7 @@ struct SDL_PrivateAudioData
|
||||
HANDLE task;
|
||||
SDL_bool coinitialized;
|
||||
int framesize;
|
||||
SDL_bool is_default_device;
|
||||
};
|
||||
|
||||
#endif /* SDL_wasapi_h_ */
|
||||
|
Loading…
Reference in New Issue
Block a user