Path: blob/master/thirdparty/sdl/core/windows/SDL_immdevice.c
21649 views
/*1Simple DirectMedia Layer2Copyright (C) 1997-2025 Sam Lantinga <[email protected]>34This software is provided 'as-is', without any express or implied5warranty. In no event will the authors be held liable for any damages6arising from the use of this software.78Permission is granted to anyone to use this software for any purpose,9including commercial applications, and to alter it and redistribute it10freely, subject to the following restrictions:11121. The origin of this software must not be misrepresented; you must not13claim that you wrote the original software. If you use this software14in a product, an acknowledgment in the product documentation would be15appreciated but is not required.162. Altered source versions must be plainly marked as such, and must not be17misrepresented as being the original software.183. This notice may not be removed or altered from any source distribution.19*/20#include "SDL_internal.h"2122#if defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H)2324#include "SDL_windows.h"25#include "SDL_immdevice.h"26#include "../../audio/SDL_sysaudio.h"27#include <objbase.h> // For CLSIDFromString2829typedef struct SDL_IMMDevice_HandleData30{31LPWSTR immdevice_id;32GUID directsound_guid;33} SDL_IMMDevice_HandleData;3435static const ERole SDL_IMMDevice_role = eConsole; // !!! FIXME: should this be eMultimedia? Should be a hint?3637// This is global to the WASAPI target, to handle hotplug and default device lookup.38static IMMDeviceEnumerator *enumerator = NULL;39static SDL_IMMDevice_callbacks immcallbacks;4041// PropVariantInit() is an inline function/macro in PropIdl.h that calls the C runtime's memset() directly. Use ours instead, to avoid dependency.42#ifdef PropVariantInit43#undef PropVariantInit44#endif45#define PropVariantInit(p) SDL_zerop(p)4647// Some GUIDs we need to know without linking to libraries that aren't available before Vista.48/* *INDENT-OFF* */ // clang-format off49static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } };50static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } };51static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } };52static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } };53static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 };54static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 };55static const PROPERTYKEY SDL_PKEY_AudioEndpoint_GUID = { { 0x1da5d803, 0xd492, 0x4edd,{ 0x8c, 0x23, 0xe0, 0xc0, 0xff, 0xee, 0x7f, 0x0e, } }, 4 };56/* *INDENT-ON* */ // clang-format on5758static bool FindByDevIDCallback(SDL_AudioDevice *device, void *userdata)59{60LPCWSTR devid = (LPCWSTR)userdata;61if (devid && device && device->handle) {62const SDL_IMMDevice_HandleData *handle = (const SDL_IMMDevice_HandleData *)device->handle;63if (handle->immdevice_id && SDL_wcscmp(handle->immdevice_id, devid) == 0) {64return true;65}66}67return false;68}6970static SDL_AudioDevice *SDL_IMMDevice_FindByDevID(LPCWSTR devid)71{72return SDL_FindPhysicalAudioDeviceByCallback(FindByDevIDCallback, (void *) devid);73}7475LPGUID SDL_IMMDevice_GetDirectSoundGUID(SDL_AudioDevice *device)76{77return (device && device->handle) ? &(((SDL_IMMDevice_HandleData *) device->handle)->directsound_guid) : NULL;78}7980LPCWSTR SDL_IMMDevice_GetDevID(SDL_AudioDevice *device)81{82return (device && device->handle) ? ((const SDL_IMMDevice_HandleData *) device->handle)->immdevice_id : NULL;83}8485static void GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt, GUID *guid)86{87/* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be88"SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in89its own UIs, like Volume Control, etc. */90IPropertyStore *props = NULL;91*utf8dev = NULL;92SDL_zerop(fmt);93if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) {94PROPVARIANT var;95PropVariantInit(&var);96if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) {97*utf8dev = WIN_StringToUTF8W(var.pwszVal);98}99PropVariantClear(&var);100if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEngine_DeviceFormat, &var))) {101SDL_memcpy(fmt, var.blob.pBlobData, SDL_min(var.blob.cbSize, sizeof(WAVEFORMATEXTENSIBLE)));102}103PropVariantClear(&var);104if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEndpoint_GUID, &var))) {105(void)CLSIDFromString(var.pwszVal, guid);106}107PropVariantClear(&var);108IPropertyStore_Release(props);109}110}111112void SDL_IMMDevice_FreeDeviceHandle(SDL_AudioDevice *device)113{114if (device && device->handle) {115SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *) device->handle;116SDL_free(handle->immdevice_id);117SDL_free(handle);118device->handle = NULL;119}120}121122static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, SDL_AudioFormat force_format)123{124/* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever).125In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for126phones and tablets, where you might have an internal speaker and a headphone jack and expect both to be127available and switch automatically. (!!! FIXME...?) */128129if (!devname) {130return NULL;131}132133// see if we already have this one first.134SDL_AudioDevice *device = SDL_IMMDevice_FindByDevID(devid);135if (device) {136if (SDL_GetAtomicInt(&device->zombie)) {137// whoa, it came back! This can happen if you unplug and replug USB headphones while we're still keeping the SDL object alive.138// Kill this device's IMMDevice id; the device will go away when the app closes it, or maybe a new default device is chosen139// (possibly this reconnected device), so we just want to make sure IMMDevice doesn't try to find the old device by the existing ID string.140SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *) device->handle;141SDL_free(handle->immdevice_id);142handle->immdevice_id = NULL;143device = NULL; // add a new device, below.144}145}146147if (!device) {148// handle is freed by SDL_IMMDevice_FreeDeviceHandle!149SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *)SDL_malloc(sizeof(SDL_IMMDevice_HandleData));150if (!handle) {151return NULL;152}153handle->immdevice_id = SDL_wcsdup(devid);154if (!handle->immdevice_id) {155SDL_free(handle);156return NULL;157}158SDL_memcpy(&handle->directsound_guid, dsoundguid, sizeof(GUID));159160SDL_AudioSpec spec;161SDL_zero(spec);162spec.channels = (Uint8)fmt->Format.nChannels;163spec.freq = fmt->Format.nSamplesPerSec;164spec.format = (force_format != SDL_AUDIO_UNKNOWN) ? force_format : SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt);165166device = SDL_AddAudioDevice(recording, devname, &spec, handle);167if (!device) {168SDL_free(handle->immdevice_id);169SDL_free(handle);170}171}172173return device;174}175176/* We need a COM subclass of IMMNotificationClient for hotplug support, which is177easy in C++, but we have to tapdance more to make work in C.178Thanks to this page for coaching on how to make this work:179https://www.codeproject.com/Articles/13601/COM-in-plain-C */180181typedef struct SDLMMNotificationClient182{183const IMMNotificationClientVtbl *lpVtbl;184SDL_AtomicInt refcount;185SDL_AudioFormat force_format;186} SDLMMNotificationClient;187188static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv)189{190if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) {191*ppv = client;192client->lpVtbl->AddRef(client);193return S_OK;194}195196*ppv = NULL;197return E_NOINTERFACE;198}199200static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient)201{202SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;203return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1);204}205206static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_Release(IMMNotificationClient *iclient)207{208// client is a static object; we don't ever free it.209SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;210const ULONG rc = SDL_AtomicDecRef(&client->refcount);211if (rc == 0) {212SDL_SetAtomicInt(&client->refcount, 0); // uhh...213return 0;214}215return rc - 1;216}217218// These are the entry points called when WASAPI device endpoints change.219static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *iclient, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)220{221if (role == SDL_IMMDevice_role) {222immcallbacks.default_audio_device_changed(SDL_IMMDevice_FindByDevID(pwstrDeviceId));223}224return S_OK;225}226227static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)228{229/* we ignore this; devices added here then progress to ACTIVE, if appropriate, in230OnDeviceStateChange, making that a better place to deal with device adds. More231importantly: the first time you plug in a USB audio device, this callback will232fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).233Plugging it back in won't fire this callback again. */234return S_OK;235}236237static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)238{239return S_OK; // See notes in OnDeviceAdded handler about why we ignore this.240}241242static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState)243{244SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;245IMMDevice *device = NULL;246247if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {248IMMEndpoint *endpoint = NULL;249if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) {250EDataFlow flow;251if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {252const bool recording = (flow == eCapture);253if (dwNewState == DEVICE_STATE_ACTIVE) {254char *utf8dev;255WAVEFORMATEXTENSIBLE fmt;256GUID dsoundguid;257GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);258if (utf8dev) {259SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format);260SDL_free(utf8dev);261}262} else {263immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId));264}265}266IMMEndpoint_Release(endpoint);267}268IMMDevice_Release(device);269}270271return S_OK;272}273274static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)275{276return S_OK; // we don't care about these.277}278279static const IMMNotificationClientVtbl notification_client_vtbl = {280SDLMMNotificationClient_QueryInterface,281SDLMMNotificationClient_AddRef,282SDLMMNotificationClient_Release,283SDLMMNotificationClient_OnDeviceStateChanged,284SDLMMNotificationClient_OnDeviceAdded,285SDLMMNotificationClient_OnDeviceRemoved,286SDLMMNotificationClient_OnDefaultDeviceChanged,287SDLMMNotificationClient_OnPropertyValueChanged288};289290static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN };291292bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks)293{294HRESULT ret;295296// just skip the discussion with COM here.297if (!WIN_IsWindowsVistaOrGreater()) {298return SDL_SetError("IMMDevice support requires Windows Vista or later");299}300301if (FAILED(WIN_CoInitialize())) {302return SDL_SetError("IMMDevice: CoInitialize() failed");303}304305ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator);306if (FAILED(ret)) {307WIN_CoUninitialize();308return WIN_SetErrorFromHRESULT("IMMDevice CoCreateInstance(MMDeviceEnumerator)", ret);309}310311if (callbacks) {312SDL_copyp(&immcallbacks, callbacks);313} else {314SDL_zero(immcallbacks);315}316317if (!immcallbacks.audio_device_disconnected) {318immcallbacks.audio_device_disconnected = SDL_AudioDeviceDisconnected;319}320if (!immcallbacks.default_audio_device_changed) {321immcallbacks.default_audio_device_changed = SDL_DefaultAudioDeviceChanged;322}323324return true;325}326327void SDL_IMMDevice_Quit(void)328{329if (enumerator) {330IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client);331IMMDeviceEnumerator_Release(enumerator);332enumerator = NULL;333}334335SDL_zero(immcallbacks);336337WIN_CoUninitialize();338}339340bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool recording)341{342const Uint64 timeout = SDL_GetTicks() + 8000; // intel's audio drivers can fail for up to EIGHT SECONDS after a device is connected or we wake from sleep.343344SDL_assert(device != NULL);345SDL_assert(immdevice != NULL);346347LPCWSTR devid = SDL_IMMDevice_GetDevID(device);348SDL_assert(devid != NULL);349350HRESULT ret;351while ((ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, immdevice)) == E_NOTFOUND) {352const Uint64 now = SDL_GetTicks();353if (timeout > now) {354const Uint64 ticksleft = timeout - now;355SDL_Delay((Uint32)SDL_min(ticksleft, 300)); // wait awhile and try again.356continue;357}358break;359}360361if (!SUCCEEDED(ret)) {362return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);363}364return true;365}366367static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format)368{369/* Note that WASAPI separates "adapter devices" from "audio endpoint devices"370...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */371372IMMDeviceCollection *collection = NULL;373if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, recording ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {374return;375}376377UINT total = 0;378if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {379IMMDeviceCollection_Release(collection);380return;381}382383LPWSTR default_devid = NULL;384if (default_device) {385IMMDevice *default_immdevice = NULL;386const EDataFlow dataflow = recording ? eCapture : eRender;387if (SUCCEEDED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &default_immdevice))) {388LPWSTR devid = NULL;389if (SUCCEEDED(IMMDevice_GetId(default_immdevice, &devid))) {390default_devid = SDL_wcsdup(devid); // if this fails, oh well.391CoTaskMemFree(devid);392}393IMMDevice_Release(default_immdevice);394}395}396397for (UINT i = 0; i < total; i++) {398IMMDevice *immdevice = NULL;399if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &immdevice))) {400LPWSTR devid = NULL;401if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) {402char *devname = NULL;403WAVEFORMATEXTENSIBLE fmt;404GUID dsoundguid;405SDL_zero(fmt);406SDL_zero(dsoundguid);407GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);408if (devname) {409SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format);410if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {411*default_device = sdldevice;412}413SDL_free(devname);414}415CoTaskMemFree(devid);416}417IMMDevice_Release(immdevice);418}419}420421SDL_free(default_devid);422423IMMDeviceCollection_Release(collection);424}425426void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format)427{428EnumerateEndpointsForFlow(false, default_playback, force_format);429EnumerateEndpointsForFlow(true, default_recording, force_format);430431notification_client.force_format = force_format;432433// if this fails, we just won't get hotplug events. Carry on anyhow.434IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client);435}436437#endif // defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H)438439440