From ae105ae1c718c98d2c7c078b4f1d09e149fc8249 Mon Sep 17 00:00:00 2001 From: Ethan Lee Date: Sun, 10 Jul 2022 12:59:33 -0400 Subject: [PATCH] windows: Move IMMDevice work to common file, implement DirectSound enumeration support --- Makefile.w32 | 2 +- VisualC-GDK/SDL/SDL.vcxproj | 2 + VisualC-GDK/SDL/SDL.vcxproj.filters | 6 + VisualC/SDL/SDL.vcxproj | 2 + VisualC/SDL/SDL.vcxproj.filters | 6 + src/audio/directsound/SDL_directsound.c | 30 +- src/audio/wasapi/SDL_wasapi.c | 102 +---- src/audio/wasapi/SDL_wasapi.h | 6 - src/audio/wasapi/SDL_wasapi_win32.c | 318 +--------------- src/audio/wasapi/SDL_wasapi_winrt.cpp | 112 +++++- src/core/windows/SDL_immdevice.c | 486 ++++++++++++++++++++++++ src/core/windows/SDL_immdevice.h | 43 +++ 12 files changed, 695 insertions(+), 420 deletions(-) create mode 100644 src/core/windows/SDL_immdevice.c create mode 100644 src/core/windows/SDL_immdevice.h diff --git a/Makefile.w32 b/Makefile.w32 index 1ca1fa2ea..978afab33 100644 --- a/Makefile.w32 +++ b/Makefile.w32 @@ -73,7 +73,7 @@ SRCS+= SDL_locale.c SDL_syslocale.c SRCS+= SDL_url.c SDL_sysurl.c SRCS+= SDL_winmm.c SDL_directsound.c SDL_wasapi.c SDL_wasapi_win32.c -SRCS+= SDL_hid.c SDL_windows.c SDL_xinput.c +SRCS+= SDL_hid.c SDL_immdevice.c SDL_windows.c SDL_xinput.c SRCS+= SDL_dinputhaptic.c SDL_windowshaptic.c SDL_xinputhaptic.c SRCS+= SDL_dinputjoystick.c SDL_rawinputjoystick.c SDL_windowsjoystick.c SDL_windows_gaming_input.c SDL_xinputjoystick.c SRCS+= SDL_syspower.c diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index 1d31c8f32..c511fda62 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -382,6 +382,7 @@ + @@ -538,6 +539,7 @@ + CompileAsCpp diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 71aeb2400..59a3d7271 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -432,6 +432,9 @@ core\windows + + core\windows + core\windows @@ -876,6 +879,9 @@ core\windows + + core\windows + core\windows diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 418a18f6c..cb386bcf0 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -305,6 +305,7 @@ + @@ -456,6 +457,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 0e23dc505..c6efccb59 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -432,6 +432,9 @@ core\windows + + core\windows + core\windows @@ -869,6 +872,9 @@ core\windows + + core\windows + core\windows diff --git a/src/audio/directsound/SDL_directsound.c b/src/audio/directsound/SDL_directsound.c index 0242b139d..b4b2fa2a6 100644 --- a/src/audio/directsound/SDL_directsound.c +++ b/src/audio/directsound/SDL_directsound.c @@ -29,11 +29,19 @@ #include "SDL_audio.h" #include "../SDL_audio_c.h" #include "SDL_directsound.h" +#if HAVE_MMDEVICEAPI_H +#include "../../core/windows/SDL_immdevice.h" +#endif /* HAVE_MMDEVICEAPI_H */ #ifndef WAVE_FORMAT_IEEE_FLOAT #define WAVE_FORMAT_IEEE_FLOAT 0x0003 #endif +/* For Vista+, we can enumerate DSound devices with IMMDevice */ +#if HAVE_MMDEVICEAPI_H +static SDL_bool SupportsIMMDevice = SDL_FALSE; +#endif /* HAVE_MMDEVICEAPI_H */ + /* DirectX function pointers for audio */ static void* DSoundDLL = NULL; typedef HRESULT (WINAPI *fnDirectSoundCreate8)(LPGUID,LPDIRECTSOUND*,LPUNKNOWN); @@ -172,8 +180,16 @@ FindAllDevs(LPGUID guid, LPCWSTR desc, LPCWSTR module, LPVOID data) static void DSOUND_DetectDevices(void) { - pDirectSoundCaptureEnumerateW(FindAllDevs, (void *) ((size_t) 1)); - pDirectSoundEnumerateW(FindAllDevs, (void *) ((size_t) 0)); +#if HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + SDL_IMMDevice_EnumerateEndpoints(SDL_TRUE); + } else { +#endif /* HAVE_MMDEVICEAPI_H */ + pDirectSoundCaptureEnumerateW(FindAllDevs, (void *)((size_t)1)); + pDirectSoundEnumerateW(FindAllDevs, (void *)((size_t)0)); +#if HAVE_MMDEVICEAPI_H + } +#endif /* HAVE_MMDEVICEAPI_H*/ } @@ -567,6 +583,12 @@ DSOUND_OpenDevice(_THIS, const char *devname) static void DSOUND_Deinitialize(void) { +#if HAVE_MMDEVICEAPI_H + if (SupportsIMMDevice) { + SDL_IMMDevice_Quit(); + SupportsIMMDevice = SDL_FALSE; + } +#endif /* HAVE_MMDEVICEAPI_H */ DSOUND_Unload(); } @@ -578,6 +600,10 @@ DSOUND_Init(SDL_AudioDriverImpl * impl) return SDL_FALSE; } +#if HAVE_MMDEVICEAPI_H + SupportsIMMDevice = !(SDL_IMMDevice_Init() < 0); +#endif /* HAVE_MMDEVICEAPI_H */ + /* Set the function pointers */ impl->DetectDevices = DSOUND_DetectDevices; impl->OpenDevice = DSOUND_OpenDevice; diff --git a/src/audio/wasapi/SDL_wasapi.c b/src/audio/wasapi/SDL_wasapi.c index 5b966aa31..a678ca3e5 100644 --- a/src/audio/wasapi/SDL_wasapi.c +++ b/src/audio/wasapi/SDL_wasapi.c @@ -23,13 +23,13 @@ #if SDL_AUDIO_DRIVER_WASAPI #include "../../core/windows/SDL_windows.h" +#include "../../core/windows/SDL_immdevice.h" #include "SDL_audio.h" #include "SDL_timer.h" #include "../SDL_audio_c.h" #include "../SDL_sysaudio.h" #define COBJMACROS -#include #include #include "SDL_wasapi.h" @@ -45,10 +45,6 @@ #define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000 #endif -/* these increment as default devices change. Opened default devices pick up changes in their threads. */ -SDL_atomic_t WASAPI_DefaultPlaybackGeneration; -SDL_atomic_t WASAPI_DefaultCaptureGeneration; - /* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */ typedef struct DevIdList { @@ -61,93 +57,6 @@ static DevIdList *deviceid_list = NULL; /* Some GUIDs we need to know without linking to libraries that aren't available before Vista. */ static const IID SDL_IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483,{ 0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2 } }; static const IID SDL_IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0,{ 0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17 } }; -static const GUID SDL_KSDATAFORMAT_SUBTYPE_PCM = { 0x00000001, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; -static const GUID SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { 0x00000003, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; - - -void -WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid) -{ - DevIdList *i; - DevIdList *next; - DevIdList *prev = NULL; - for (i = deviceid_list; i; i = next) { - next = i->next; - if (SDL_wcscmp(i->str, devid) == 0) { - if (prev) { - prev->next = next; - } else { - deviceid_list = next; - } - SDL_RemoveAudioDevice(iscapture, i->str); - SDL_free(i->str); - SDL_free(i); - } - prev = i; - } -} - -static SDL_AudioFormat -WaveFormatToSDLFormat(WAVEFORMATEX *waveformat) -{ - if ((waveformat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) && (waveformat->wBitsPerSample == 32)) { - return AUDIO_F32SYS; - } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 16)) { - return AUDIO_S16SYS; - } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 32)) { - return AUDIO_S32SYS; - } else if (waveformat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { - const WAVEFORMATEXTENSIBLE *ext = (const WAVEFORMATEXTENSIBLE *) waveformat; - if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof (GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { - return AUDIO_F32SYS; - } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof (GUID)) == 0) && (waveformat->wBitsPerSample == 16)) { - return AUDIO_S16SYS; - } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof (GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { - return AUDIO_S32SYS; - } - } - return 0; -} - -void -WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid) -{ - DevIdList *devidlist; - SDL_AudioSpec spec; - - /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever). - In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for - phones and tablets, where you might have an internal speaker and a headphone jack and expect both to be - available and switch automatically. (!!! FIXME...?) */ - - /* see if we already have this one. */ - for (devidlist = deviceid_list; devidlist; devidlist = devidlist->next) { - if (SDL_wcscmp(devidlist->str, devid) == 0) { - return; /* we already have this. */ - } - } - - devidlist = (DevIdList *) SDL_malloc(sizeof (*devidlist)); - if (!devidlist) { - return; /* oh well. */ - } - - devid = SDL_wcsdup(devid); - if (!devid) { - SDL_free(devidlist); - return; /* oh well. */ - } - - devidlist->str = (WCHAR *) devid; - devidlist->next = deviceid_list; - deviceid_list = devidlist; - - SDL_zero(spec); - spec.channels = (Uint8)fmt->Format.nChannels; - spec.freq = fmt->Format.nSamplesPerSec; - spec.format = WaveFormatToSDLFormat((WAVEFORMATEX *) fmt); - SDL_AddAudioDevice(iscapture, devname, &spec, (void *) devid); -} static void WASAPI_DetectDevices(void) @@ -235,7 +144,7 @@ RecoverWasapiDevice(_THIS) ReleaseWasapiDevice(this); /* dump the lost device's handles. */ if (this->hidden->default_device_generation) { - this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ? &WASAPI_DefaultCaptureGeneration : &WASAPI_DefaultPlaybackGeneration); + this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ? &SDL_IMMDevice_DefaultCaptureGeneration : &SDL_IMMDevice_DefaultPlaybackGeneration); } /* this can fail for lots of reasons, but the most likely is we had a @@ -268,7 +177,7 @@ RecoverWasapiIfLost(_THIS) } if (!lost && (generation > 0)) { /* is a default device? */ - const int newgen = SDL_AtomicGet(this->iscapture ? &WASAPI_DefaultCaptureGeneration : &WASAPI_DefaultPlaybackGeneration); + const int newgen = SDL_AtomicGet(this->iscapture ? &SDL_IMMDevice_DefaultCaptureGeneration : &SDL_IMMDevice_DefaultPlaybackGeneration); if (generation != newgen) { /* the desired default device was changed, jump over to it. */ lost = SDL_TRUE; } @@ -655,7 +564,7 @@ WASAPI_OpenDevice(_THIS, const char *devname) WASAPI_RefDevice(this); /* so CloseDevice() will unref to zero. */ if (!devid) { /* is default device? */ - this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ? &WASAPI_DefaultCaptureGeneration : &WASAPI_DefaultPlaybackGeneration); + this->hidden->default_device_generation = SDL_AtomicGet(this->iscapture ? &SDL_IMMDevice_DefaultCaptureGeneration : &SDL_IMMDevice_DefaultPlaybackGeneration); } else { this->hidden->devid = SDL_wcsdup(devid); if (!this->hidden->devid) { @@ -709,9 +618,6 @@ WASAPI_Deinitialize(void) static SDL_bool WASAPI_Init(SDL_AudioDriverImpl * impl) { - SDL_AtomicSet(&WASAPI_DefaultPlaybackGeneration, 1); - SDL_AtomicSet(&WASAPI_DefaultCaptureGeneration, 1); - if (WASAPI_PlatformInit() == -1) { return SDL_FALSE; } diff --git a/src/audio/wasapi/SDL_wasapi.h b/src/audio/wasapi/SDL_wasapi.h index af79637b7..d4e81a351 100644 --- a/src/audio/wasapi/SDL_wasapi.h +++ b/src/audio/wasapi/SDL_wasapi.h @@ -55,16 +55,10 @@ struct SDL_PrivateAudioData SDL_atomic_t just_activated; }; -/* these increment as default devices change. Opened default devices pick up changes in their threads. */ -extern SDL_atomic_t WASAPI_DefaultPlaybackGeneration; -extern SDL_atomic_t WASAPI_DefaultCaptureGeneration; - /* win32 and winrt implementations call into these. */ int WASAPI_PrepDevice(_THIS, const SDL_bool updatestream); void WASAPI_RefDevice(_THIS); void WASAPI_UnrefDevice(_THIS); -void WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid); -void WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid); /* These are functions that are implemented differently for Windows vs WinRT. */ int WASAPI_PlatformInit(void); diff --git a/src/audio/wasapi/SDL_wasapi_win32.c b/src/audio/wasapi/SDL_wasapi_win32.c index 137f37013..ba8962f29 100644 --- a/src/audio/wasapi/SDL_wasapi_win32.c +++ b/src/audio/wasapi/SDL_wasapi_win32.c @@ -29,28 +29,16 @@ #if SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__) #include "../../core/windows/SDL_windows.h" +#include "../../core/windows/SDL_immdevice.h" #include "SDL_audio.h" #include "SDL_timer.h" #include "../SDL_audio_c.h" #include "../SDL_sysaudio.h" -#define COBJMACROS -#include #include #include "SDL_wasapi.h" -static const ERole SDL_WASAPI_role = eConsole; /* !!! FIXME: should this be eMultimedia? Should be a hint? */ - -/* This is global to the WASAPI target, to handle hotplug and default device lookup. */ -static IMMDeviceEnumerator *enumerator = NULL; - -/* PropVariantInit() is an inline function/macro in PropIdl.h that calls the C runtime's memset() directly. Use ours instead, to avoid dependency. */ -#ifdef PropVariantInit -#undef PropVariantInit -#endif -#define PropVariantInit(p) SDL_zerop(p) - /* handle to Avrt.dll--Vista and later!--for flagging the callback thread as "Pro Audio" (low latency). */ static HMODULE libavrt = NULL; typedef HANDLE(WINAPI *pfnAvSetMmThreadCharacteristicsW)(LPCWSTR, LPDWORD); @@ -59,203 +47,13 @@ static pfnAvSetMmThreadCharacteristicsW pAvSetMmThreadCharacteristicsW = NULL; static pfnAvRevertMmThreadCharacteristics pAvRevertMmThreadCharacteristics = NULL; /* Some GUIDs we need to know without linking to libraries that aren't available before Vista. */ -static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } }; -static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } }; -static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } }; -static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } }; static const IID SDL_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32,{ 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } }; -static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 }; -static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 }; - - -static void -GetWasapiDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt) -{ - /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be - "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in - its own UIs, like Volume Control, etc. */ - IPropertyStore *props = NULL; - *utf8dev = NULL; - SDL_zerop(fmt); - if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) { - PROPVARIANT var; - PropVariantInit(&var); - if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) { - *utf8dev = WIN_StringToUTF8W(var.pwszVal); - } - PropVariantClear(&var); - if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEngine_DeviceFormat, &var))) { - SDL_memcpy(fmt, var.blob.pBlobData, SDL_min(var.blob.cbSize, sizeof(WAVEFORMATEXTENSIBLE))); - } - PropVariantClear(&var); - IPropertyStore_Release(props); - } -} - - -/* We need a COM subclass of IMMNotificationClient for hotplug support, which is - easy in C++, but we have to tapdance more to make work in C. - Thanks to this page for coaching on how to make this work: - https://www.codeproject.com/Articles/13601/COM-in-plain-C */ - -typedef struct SDLMMNotificationClient -{ - const IMMNotificationClientVtbl *lpVtbl; - SDL_atomic_t refcount; -} SDLMMNotificationClient; - -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_QueryInterface(IMMNotificationClient *this, REFIID iid, void **ppv) -{ - if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) - { - *ppv = this; - this->lpVtbl->AddRef(this); - return S_OK; - } - - *ppv = NULL; - return E_NOINTERFACE; -} - -static ULONG STDMETHODCALLTYPE -SDLMMNotificationClient_AddRef(IMMNotificationClient *ithis) -{ - SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis; - return (ULONG) (SDL_AtomicIncRef(&this->refcount) + 1); -} - -static ULONG STDMETHODCALLTYPE -SDLMMNotificationClient_Release(IMMNotificationClient *ithis) -{ - /* this is a static object; we don't ever free it. */ - SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis; - const ULONG retval = SDL_AtomicDecRef(&this->refcount); - if (retval == 0) { - SDL_AtomicSet(&this->refcount, 0); /* uhh... */ - return 0; - } - return retval - 1; -} - -/* These are the entry points called when WASAPI device endpoints change. */ -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) -{ - if (role != SDL_WASAPI_role) { - return S_OK; /* ignore it. */ - } - - /* Increment the "generation," so opened devices will pick this up in their threads. */ - switch (flow) { - case eRender: - SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1); - break; - - case eCapture: - SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1); - break; - - case eAll: - SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1); - SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1); - break; - - default: - SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!"); - break; - } - - return S_OK; -} - -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId) -{ - /* we ignore this; devices added here then progress to ACTIVE, if appropriate, in - OnDeviceStateChange, making that a better place to deal with device adds. More - importantly: the first time you plug in a USB audio device, this callback will - fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT). - Plugging it back in won't fire this callback again. */ - return S_OK; -} - -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId) -{ - /* See notes in OnDeviceAdded handler about why we ignore this. */ - return S_OK; -} - -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId, DWORD dwNewState) -{ - IMMDevice *device = NULL; - - if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) { - IMMEndpoint *endpoint = NULL; - if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **) &endpoint))) { - EDataFlow flow; - if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) { - const SDL_bool iscapture = (flow == eCapture); - if (dwNewState == DEVICE_STATE_ACTIVE) { - char *utf8dev; - WAVEFORMATEXTENSIBLE fmt; - GetWasapiDeviceInfo(device, &utf8dev, &fmt); - if (utf8dev) { - WASAPI_AddDevice(iscapture, utf8dev, &fmt, pwstrDeviceId); - SDL_free(utf8dev); - } - } else { - WASAPI_RemoveDevice(iscapture, pwstrDeviceId); - } - } - IMMEndpoint_Release(endpoint); - } - IMMDevice_Release(device); - } - - return S_OK; -} - -static HRESULT STDMETHODCALLTYPE -SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *this, LPCWSTR pwstrDeviceId, const PROPERTYKEY key) -{ - return S_OK; /* we don't care about these. */ -} - -static const IMMNotificationClientVtbl notification_client_vtbl = { - SDLMMNotificationClient_QueryInterface, - SDLMMNotificationClient_AddRef, - SDLMMNotificationClient_Release, - SDLMMNotificationClient_OnDeviceStateChanged, - SDLMMNotificationClient_OnDeviceAdded, - SDLMMNotificationClient_OnDeviceRemoved, - SDLMMNotificationClient_OnDefaultDeviceChanged, - SDLMMNotificationClient_OnPropertyValueChanged -}; - -static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 } }; - int WASAPI_PlatformInit(void) { - HRESULT ret; - - /* just skip the discussion with COM here. */ - if (!WIN_IsWindowsVistaOrGreater()) { - return SDL_SetError("WASAPI support requires Windows Vista or later"); - } - - if (FAILED(WIN_CoInitialize())) { - return SDL_SetError("WASAPI: CoInitialize() failed"); - } - - ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *) &enumerator); - if (FAILED(ret)) { - WIN_CoUninitialize(); - return WIN_SetErrorFromHRESULT("WASAPI CoCreateInstance(MMDeviceEnumerator)", ret); + if (SDL_IMMDevice_Init() < 0) { + return -1; /* This is set by SDL_IMMDevice_Init */ } libavrt = LoadLibrary(TEXT("avrt.dll")); /* this library is available in Vista and later. No WinXP, so have to LoadLibrary to use it for now! */ @@ -270,11 +68,7 @@ WASAPI_PlatformInit(void) void WASAPI_PlatformDeinit(void) { - if (enumerator) { - IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) ¬ification_client); - IMMDeviceEnumerator_Release(enumerator); - enumerator = NULL; - } + SDL_IMMDevice_Quit(); if (libavrt) { FreeLibrary(libavrt); @@ -320,21 +114,12 @@ WASAPI_PlatformThreadDeinit(_THIS) int WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery) { - LPCWSTR devid = this->hidden->devid; IMMDevice *device = NULL; HRESULT ret; - if (devid == NULL) { - const EDataFlow dataflow = this->iscapture ? eCapture : eRender; - ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device); - } else { - ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, &device); - } - - if (FAILED(ret)) { - SDL_assert(device == NULL); + if (SDL_IMMDevice_Get(this->hidden->devid, &device, this->iscapture) < 0) { this->hidden->client = NULL; - return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret); + return -1; /* This is already set by SDL_IMMDevice_Get */ } /* this is not async in standard win32, yay! */ @@ -354,99 +139,10 @@ WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery) return 0; /* good to go. */ } - -typedef struct -{ - LPWSTR devid; - char *devname; - WAVEFORMATEXTENSIBLE fmt; -} EndpointItem; - -static int SDLCALL sort_endpoints(const void *_a, const void *_b) -{ - LPWSTR a = ((const EndpointItem *) _a)->devid; - LPWSTR b = ((const EndpointItem *) _b)->devid; - if (!a && b) { - return -1; - } else if (a && !b) { - return 1; - } - - while (SDL_TRUE) { - if (*a < *b) { - return -1; - } else if (*a > *b) { - return 1; - } else if (*a == 0) { - break; - } - a++; - b++; - } - - return 0; -} - -static void -WASAPI_EnumerateEndpointsForFlow(const SDL_bool iscapture) -{ - IMMDeviceCollection *collection = NULL; - EndpointItem *items; - UINT i, total; - - /* Note that WASAPI separates "adapter devices" from "audio endpoint devices" - ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */ - - if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, iscapture ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) { - return; - } - - if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) { - IMMDeviceCollection_Release(collection); - return; - } - - items = (EndpointItem *) SDL_calloc(total, sizeof (EndpointItem)); - if (!items) { - return; /* oh well. */ - } - - for (i = 0; i < total; i++) { - EndpointItem *item = items + i; - IMMDevice *device = NULL; - if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &device))) { - if (SUCCEEDED(IMMDevice_GetId(device, &item->devid))) { - GetWasapiDeviceInfo(device, &item->devname, &item->fmt); - } - IMMDevice_Release(device); - } - } - - /* sort the list of devices by their guid so list is consistent between runs */ - SDL_qsort(items, total, sizeof (*items), sort_endpoints); - - /* Send the sorted list on to the SDL's higher level. */ - for (i = 0; i < total; i++) { - EndpointItem *item = items + i; - if ((item->devid) && (item->devname)) { - WASAPI_AddDevice(iscapture, item->devname, &item->fmt, item->devid); - } - SDL_free(item->devname); - CoTaskMemFree(item->devid); - } - - SDL_free(items); - IMMDeviceCollection_Release(collection); -} - void WASAPI_EnumerateEndpoints(void) { - WASAPI_EnumerateEndpointsForFlow(SDL_FALSE); /* playback */ - WASAPI_EnumerateEndpointsForFlow(SDL_TRUE); /* capture */ - - /* if this fails, we just won't get hotplug events. Carry on anyhow. */ - IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) ¬ification_client); + SDL_IMMDevice_EnumerateEndpoints(SDL_FALSE); } void diff --git a/src/audio/wasapi/SDL_wasapi_winrt.cpp b/src/audio/wasapi/SDL_wasapi_winrt.cpp index e063e6a41..11ea0de06 100644 --- a/src/audio/wasapi/SDL_wasapi_winrt.cpp +++ b/src/audio/wasapi/SDL_wasapi_winrt.cpp @@ -55,6 +55,13 @@ using namespace Microsoft::WRL; static Platform::String^ SDL_PKEY_AudioEngine_DeviceFormat = L"{f19f064d-082c-4e27-bc73-6882a1bb8e4c} 0"; +static void WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid); +static void WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid); +extern "C" { + SDL_atomic_t SDL_IMMDevice_DefaultPlaybackGeneration; + SDL_atomic_t SDL_IMMDevice_DefaultCaptureGeneration; +} + class SDL_WasapiDeviceEventHandler { public: @@ -174,14 +181,14 @@ void SDL_WasapiDeviceEventHandler::OnDefaultRenderDeviceChanged(Platform::Object^ sender, DefaultAudioRenderDeviceChangedEventArgs^ args) { SDL_assert(this->iscapture); - SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1); + SDL_AtomicAdd(&SDL_IMMDevice_DefaultPlaybackGeneration, 1); } void SDL_WasapiDeviceEventHandler::OnDefaultCaptureDeviceChanged(Platform::Object^ sender, DefaultAudioCaptureDeviceChangedEventArgs^ args) { SDL_assert(!this->iscapture); - SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1); + SDL_AtomicAdd(&SDL_IMMDevice_DefaultCaptureGeneration, 1); } @@ -190,6 +197,8 @@ static SDL_WasapiDeviceEventHandler *capture_device_event_handler; int WASAPI_PlatformInit(void) { + SDL_AtomicSet(&SDL_IMMDevice_DefaultPlaybackGeneration, 1); + SDL_AtomicSet(&SDL_IMMDevice_DefaultCaptureGeneration, 1); return 0; } @@ -317,6 +326,105 @@ WASAPI_PlatformThreadDeinit(_THIS) // !!! FIXME: set this thread to "Pro Audio" priority. } +/* Everything below was copied from SDL_wasapi.c, before it got moved to SDL_immdevice.c! */ + +static const GUID SDL_KSDATAFORMAT_SUBTYPE_PCM = { 0x00000001, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; +static const GUID SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { 0x00000003, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; + +extern "C" SDL_AudioFormat +WaveFormatToSDLFormat(WAVEFORMATEX *waveformat) +{ + if ((waveformat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_F32SYS; + } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 16)) { + return AUDIO_S16SYS; + } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_S32SYS; + } else if (waveformat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + const WAVEFORMATEXTENSIBLE *ext = (const WAVEFORMATEXTENSIBLE *)waveformat; + if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_F32SYS; + } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 16)) { + return AUDIO_S16SYS; + } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_S32SYS; + } + } + return 0; +} + +/* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */ +typedef struct DevIdList +{ + WCHAR *str; + struct DevIdList *next; +} DevIdList; + +static DevIdList *deviceid_list = NULL; + +static void +WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid) +{ + DevIdList *i; + DevIdList *next; + DevIdList *prev = NULL; + for (i = deviceid_list; i; i = next) { + next = i->next; + if (SDL_wcscmp(i->str, devid) == 0) { + if (prev) { + prev->next = next; + } + else { + deviceid_list = next; + } + SDL_RemoveAudioDevice(iscapture, i->str); + SDL_free(i->str); + SDL_free(i); + } + prev = i; + } +} + +static void +WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid) +{ + DevIdList *devidlist; + SDL_AudioSpec spec; + + /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever). + In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for + phones and tablets, where you might have an internal speaker and a headphone jack and expect both to be + available and switch automatically. (!!! FIXME...?) */ + + /* see if we already have this one. */ + for (devidlist = deviceid_list; devidlist; devidlist = devidlist->next) { + if (SDL_wcscmp(devidlist->str, devid) == 0) { + return; /* we already have this. */ + } + } + + devidlist = (DevIdList *)SDL_malloc(sizeof(*devidlist)); + if (!devidlist) { + return; /* oh well. */ + } + + devid = SDL_wcsdup(devid); + if (!devid) { + SDL_free(devidlist); + return; /* oh well. */ + } + + devidlist->str = (WCHAR *)devid; + devidlist->next = deviceid_list; + deviceid_list = devidlist; + + SDL_zero(spec); + spec.channels = (Uint8)fmt->Format.nChannels; + spec.freq = fmt->Format.nSamplesPerSec; + spec.format = WaveFormatToSDLFormat((WAVEFORMATEX *)fmt); + SDL_AddAudioDevice(iscapture, devname, &spec, (void *)devid); +} + #endif // SDL_AUDIO_DRIVER_WASAPI && defined(__WINRT__) /* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/core/windows/SDL_immdevice.c b/src/core/windows/SDL_immdevice.c new file mode 100644 index 000000000..415f983d8 --- /dev/null +++ b/src/core/windows/SDL_immdevice.c @@ -0,0 +1,486 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2022 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../../SDL_internal.h" + +#if (defined(__WIN32__) || defined(__GDK__)) && HAVE_MMDEVICEAPI_H + +#include "SDL_windows.h" +#include "SDL_immdevice.h" +#include "../../audio/SDL_sysaudio.h" +#include /* For CLSIDFromString */ + +static const ERole SDL_IMMDevice_role = eConsole; /* !!! FIXME: should this be eMultimedia? Should be a hint? */ + +/* This is global to the WASAPI target, to handle hotplug and default device lookup. */ +static IMMDeviceEnumerator *enumerator = NULL; + +/* PropVariantInit() is an inline function/macro in PropIdl.h that calls the C runtime's memset() directly. Use ours instead, to avoid dependency. */ +#ifdef PropVariantInit +#undef PropVariantInit +#endif +#define PropVariantInit(p) SDL_zerop(p) + +/* Some GUIDs we need to know without linking to libraries that aren't available before Vista. */ +static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } }; +static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } }; +static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } }; +static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } }; +static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 }; +static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 }; +static const PROPERTYKEY SDL_PKEY_AudioEndpoint_GUID = { { 0x1da5d803, 0xd492, 0x4edd,{ 0x8c, 0x23, 0xe0, 0xc0, 0xff, 0xee, 0x7f, 0x0e, } }, 4 }; +static const GUID SDL_KSDATAFORMAT_SUBTYPE_PCM = { 0x00000001, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; +static const GUID SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { 0x00000003, 0x0000, 0x0010,{ 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; + +/* these increment as default devices change. Opened default devices pick up changes in their threads. */ +SDL_atomic_t SDL_IMMDevice_DefaultPlaybackGeneration; +SDL_atomic_t SDL_IMMDevice_DefaultCaptureGeneration; + +static void +GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt, GUID *guid) +{ + /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be + "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in + its own UIs, like Volume Control, etc. */ + IPropertyStore *props = NULL; + *utf8dev = NULL; + SDL_zerop(fmt); + if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) { + PROPVARIANT var; + PropVariantInit(&var); + if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) { + *utf8dev = WIN_StringToUTF8W(var.pwszVal); + } + PropVariantClear(&var); + if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEngine_DeviceFormat, &var))) { + SDL_memcpy(fmt, var.blob.pBlobData, SDL_min(var.blob.cbSize, sizeof(WAVEFORMATEXTENSIBLE))); + } + PropVariantClear(&var); + if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEndpoint_GUID, &var))) { + CLSIDFromString(var.pwszVal, guid); + } + PropVariantClear(&var); + IPropertyStore_Release(props); + } +} + +/* This is a list of device id strings we have inflight, so we have consistent pointers to the same device. */ +typedef struct DevIdList +{ + WCHAR *str; + LPGUID guid; + struct DevIdList *next; +} DevIdList; + +static DevIdList *deviceid_list = NULL; + +static void +SDL_IMMDevice_Remove(const SDL_bool iscapture, LPCWSTR devid, SDL_bool useguid) +{ + DevIdList *i; + DevIdList *next; + DevIdList *prev = NULL; + for (i = deviceid_list; i; i = next) { + next = i->next; + if (SDL_wcscmp(i->str, devid) == 0) { + if (prev) { + prev->next = next; + } else { + deviceid_list = next; + } + SDL_RemoveAudioDevice(iscapture, useguid ? ((void *) i->guid) : ((void *) i->str)); + SDL_free(i->str); + SDL_free(i); + } + prev = i; + } +} + +static void +SDL_IMMDevice_Add(const SDL_bool iscapture, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, SDL_bool useguid) +{ + DevIdList *devidlist; + SDL_AudioSpec spec; + LPGUID cpyguid; + + /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever). + In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for + phones and tablets, where you might have an internal speaker and a headphone jack and expect both to be + available and switch automatically. (!!! FIXME...?) */ + + /* see if we already have this one. */ + for (devidlist = deviceid_list; devidlist; devidlist = devidlist->next) { + if (SDL_wcscmp(devidlist->str, devid) == 0) { + return; /* we already have this. */ + } + } + + devidlist = (DevIdList *)SDL_malloc(sizeof(*devidlist)); + if (!devidlist) { + return; /* oh well. */ + } + + devid = SDL_wcsdup(devid); + if (!devid) { + SDL_free(devidlist); + return; /* oh well. */ + } + + cpyguid = (LPGUID)SDL_malloc(sizeof(GUID)); + if (!cpyguid) { + SDL_free(devidlist); + return; /* oh well. */ + } + SDL_memcpy(cpyguid, dsoundguid, sizeof(GUID)); + + devidlist->str = (WCHAR *)devid; + devidlist->guid = cpyguid; + devidlist->next = deviceid_list; + deviceid_list = devidlist; + + SDL_zero(spec); + spec.channels = (Uint8)fmt->Format.nChannels; + spec.freq = fmt->Format.nSamplesPerSec; + spec.format = WaveFormatToSDLFormat((WAVEFORMATEX *)fmt); + SDL_AddAudioDevice(iscapture, devname, &spec, useguid ? ((void *) cpyguid) : ((void *) devid)); +} + +/* We need a COM subclass of IMMNotificationClient for hotplug support, which is + easy in C++, but we have to tapdance more to make work in C. + Thanks to this page for coaching on how to make this work: + https://www.codeproject.com/Articles/13601/COM-in-plain-C */ + +typedef struct SDLMMNotificationClient +{ + const IMMNotificationClientVtbl *lpVtbl; + SDL_atomic_t refcount; + SDL_bool useguid; +} SDLMMNotificationClient; + +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_QueryInterface(IMMNotificationClient *this, REFIID iid, void **ppv) +{ + if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) { + *ppv = this; + this->lpVtbl->AddRef(this); + return S_OK; + } + + *ppv = NULL; + return E_NOINTERFACE; +} + +static ULONG STDMETHODCALLTYPE +SDLMMNotificationClient_AddRef(IMMNotificationClient *ithis) +{ + SDLMMNotificationClient *this = (SDLMMNotificationClient *)ithis; + return (ULONG)(SDL_AtomicIncRef(&this->refcount) + 1); +} + +static ULONG STDMETHODCALLTYPE +SDLMMNotificationClient_Release(IMMNotificationClient *ithis) +{ + /* this is a static object; we don't ever free it. */ + SDLMMNotificationClient *this = (SDLMMNotificationClient *)ithis; + const ULONG retval = SDL_AtomicDecRef(&this->refcount); + if (retval == 0) { + SDL_AtomicSet(&this->refcount, 0); /* uhh... */ + return 0; + } + return retval - 1; +} + +/* These are the entry points called when WASAPI device endpoints change. */ +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) +{ + if (role != SDL_IMMDevice_role) { + return S_OK; /* ignore it. */ + } + + /* Increment the "generation," so opened devices will pick this up in their threads. */ + switch (flow) { + case eRender: + SDL_AtomicAdd(&SDL_IMMDevice_DefaultPlaybackGeneration, 1); + break; + + case eCapture: + SDL_AtomicAdd(&SDL_IMMDevice_DefaultCaptureGeneration, 1); + break; + + case eAll: + SDL_AtomicAdd(&SDL_IMMDevice_DefaultPlaybackGeneration, 1); + SDL_AtomicAdd(&SDL_IMMDevice_DefaultCaptureGeneration, 1); + break; + + default: + SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!"); + break; + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId) +{ + /* we ignore this; devices added here then progress to ACTIVE, if appropriate, in + OnDeviceStateChange, making that a better place to deal with device adds. More + importantly: the first time you plug in a USB audio device, this callback will + fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT). + Plugging it back in won't fire this callback again. */ + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId) +{ + /* See notes in OnDeviceAdded handler about why we ignore this. */ + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId, DWORD dwNewState) +{ + IMMDevice *device = NULL; + + if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) { + IMMEndpoint *endpoint = NULL; + if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) { + EDataFlow flow; + if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) { + const SDL_bool iscapture = (flow == eCapture); + const SDLMMNotificationClient *client = (SDLMMNotificationClient*) ithis; + if (dwNewState == DEVICE_STATE_ACTIVE) { + char *utf8dev; + WAVEFORMATEXTENSIBLE fmt; + GUID dsoundguid; + GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid); + if (utf8dev) { + SDL_IMMDevice_Add(iscapture, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->useguid); + SDL_free(utf8dev); + } + } else { + SDL_IMMDevice_Remove(iscapture, pwstrDeviceId, client->useguid); + } + } + IMMEndpoint_Release(endpoint); + } + IMMDevice_Release(device); + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE +SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *this, LPCWSTR pwstrDeviceId, const PROPERTYKEY key) +{ + return S_OK; /* we don't care about these. */ +} + +static const IMMNotificationClientVtbl notification_client_vtbl = { + SDLMMNotificationClient_QueryInterface, + SDLMMNotificationClient_AddRef, + SDLMMNotificationClient_Release, + SDLMMNotificationClient_OnDeviceStateChanged, + SDLMMNotificationClient_OnDeviceAdded, + SDLMMNotificationClient_OnDeviceRemoved, + SDLMMNotificationClient_OnDefaultDeviceChanged, + SDLMMNotificationClient_OnPropertyValueChanged +}; + +static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 } }; + +int +SDL_IMMDevice_Init(void) +{ + HRESULT ret; + + SDL_AtomicSet(&SDL_IMMDevice_DefaultPlaybackGeneration, 1); + SDL_AtomicSet(&SDL_IMMDevice_DefaultCaptureGeneration, 1); + + /* just skip the discussion with COM here. */ + if (!WIN_IsWindowsVistaOrGreater()) { + return SDL_SetError("WASAPI support requires Windows Vista or later"); + } + + if (FAILED(WIN_CoInitialize())) { + return SDL_SetError("WASAPI: CoInitialize() failed"); + } + + ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator); + if (FAILED(ret)) { + WIN_CoUninitialize(); + return WIN_SetErrorFromHRESULT("WASAPI CoCreateInstance(MMDeviceEnumerator)", ret); + } + return 0; +} + +void +SDL_IMMDevice_Quit(void) +{ + if (enumerator) { + IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client); + IMMDeviceEnumerator_Release(enumerator); + enumerator = NULL; + } +} + +int +SDL_IMMDevice_Get(LPCWSTR devid, IMMDevice **device, SDL_bool iscapture) +{ + HRESULT ret; + + SDL_assert(device != NULL); + + if (devid == NULL) { + const EDataFlow dataflow = iscapture ? eCapture : eRender; + ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, device); + } else { + ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, device); + } + + if (FAILED(ret)) { + SDL_assert(*device == NULL); + return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret); + } + return 0; +} + +typedef struct +{ + LPWSTR devid; + char *devname; + WAVEFORMATEXTENSIBLE fmt; + GUID dsoundguid; +} EndpointItem; + +static int SDLCALL sort_endpoints(const void *_a, const void *_b) +{ + LPWSTR a = ((const EndpointItem *)_a)->devid; + LPWSTR b = ((const EndpointItem *)_b)->devid; + if (!a && b) { + return -1; + } else if (a && !b) { + return 1; + } + + while (SDL_TRUE) { + if (*a < *b) { + return -1; + } else if (*a > *b) { + return 1; + } else if (*a == 0) { + break; + } + a++; + b++; + } + + return 0; +} + +static void +EnumerateEndpointsForFlow(const SDL_bool iscapture) +{ + IMMDeviceCollection *collection = NULL; + EndpointItem *items; + UINT i, total; + + /* Note that WASAPI separates "adapter devices" from "audio endpoint devices" + ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */ + + if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, iscapture ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) { + return; + } + + if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) { + IMMDeviceCollection_Release(collection); + return; + } + + items = (EndpointItem *)SDL_calloc(total, sizeof(EndpointItem)); + if (!items) { + return; /* oh well. */ + } + + for (i = 0; i < total; i++) { + EndpointItem *item = items + i; + IMMDevice *device = NULL; + if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &device))) { + if (SUCCEEDED(IMMDevice_GetId(device, &item->devid))) { + GetMMDeviceInfo(device, &item->devname, &item->fmt, &item->dsoundguid); + } + IMMDevice_Release(device); + } + } + + /* sort the list of devices by their guid so list is consistent between runs */ + SDL_qsort(items, total, sizeof(*items), sort_endpoints); + + /* Send the sorted list on to the SDL's higher level. */ + for (i = 0; i < total; i++) { + EndpointItem *item = items + i; + if ((item->devid) && (item->devname)) { + SDL_IMMDevice_Add(iscapture, item->devname, &item->fmt, item->devid, &item->dsoundguid, notification_client.useguid); + } + SDL_free(item->devname); + CoTaskMemFree(item->devid); + } + + SDL_free(items); + IMMDeviceCollection_Release(collection); +} + +void +SDL_IMMDevice_EnumerateEndpoints(SDL_bool useguid) +{ + notification_client.useguid = useguid; + + EnumerateEndpointsForFlow(SDL_FALSE); /* playback */ + EnumerateEndpointsForFlow(SDL_TRUE); /* capture */ + + /* if this fails, we just won't get hotplug events. Carry on anyhow. */ + IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client); +} + +SDL_AudioFormat +WaveFormatToSDLFormat(WAVEFORMATEX *waveformat) +{ + if ((waveformat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_F32SYS; + } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 16)) { + return AUDIO_S16SYS; + } else if ((waveformat->wFormatTag == WAVE_FORMAT_PCM) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_S32SYS; + } else if (waveformat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + const WAVEFORMATEXTENSIBLE *ext = (const WAVEFORMATEXTENSIBLE *)waveformat; + if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_F32SYS; + } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 16)) { + return AUDIO_S16SYS; + } else if ((SDL_memcmp(&ext->SubFormat, &SDL_KSDATAFORMAT_SUBTYPE_PCM, sizeof(GUID)) == 0) && (waveformat->wBitsPerSample == 32)) { + return AUDIO_S32SYS; + } + } + return 0; +} + +#endif /* (defined(__WIN32__) || defined(__GDK__)) && HAVE_MMDEVICEAPI_H */ diff --git a/src/core/windows/SDL_immdevice.h b/src/core/windows/SDL_immdevice.h new file mode 100644 index 000000000..cdb87c5fb --- /dev/null +++ b/src/core/windows/SDL_immdevice.h @@ -0,0 +1,43 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2022 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef SDL_IMMDEVICE_H +#define SDL_IMMDEVICE_H + +#include "SDL_atomic.h" +#include "SDL_audio.h" + +#define COBJMACROS +#include +#include + +int SDL_IMMDevice_Init(void); +void SDL_IMMDevice_Quit(void); +int SDL_IMMDevice_Get(LPCWSTR devid, IMMDevice **device, SDL_bool iscapture); +void SDL_IMMDevice_EnumerateEndpoints(SDL_bool useguid); + +SDL_AudioFormat WaveFormatToSDLFormat(WAVEFORMATEX *waveformat); + +/* these increment as default devices change. Opened default devices pick up changes in their threads. */ +extern SDL_atomic_t SDL_IMMDevice_DefaultPlaybackGeneration; +extern SDL_atomic_t SDL_IMMDevice_DefaultCaptureGeneration; + +#endif /* SDL_IMMDEVICE_H */