Path: blob/master/thirdparty/sdl/core/windows/SDL_immdevice.c
9905 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)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 = 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;185} SDLMMNotificationClient;186187static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv)188{189if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) {190*ppv = client;191client->lpVtbl->AddRef(client);192return S_OK;193}194195*ppv = NULL;196return E_NOINTERFACE;197}198199static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient)200{201SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;202return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1);203}204205static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_Release(IMMNotificationClient *iclient)206{207// client is a static object; we don't ever free it.208SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;209const ULONG rc = SDL_AtomicDecRef(&client->refcount);210if (rc == 0) {211SDL_SetAtomicInt(&client->refcount, 0); // uhh...212return 0;213}214return rc - 1;215}216217// These are the entry points called when WASAPI device endpoints change.218static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *iclient, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)219{220if (role == SDL_IMMDevice_role) {221immcallbacks.default_audio_device_changed(SDL_IMMDevice_FindByDevID(pwstrDeviceId));222}223return S_OK;224}225226static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)227{228/* we ignore this; devices added here then progress to ACTIVE, if appropriate, in229OnDeviceStateChange, making that a better place to deal with device adds. More230importantly: the first time you plug in a USB audio device, this callback will231fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).232Plugging it back in won't fire this callback again. */233return S_OK;234}235236static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)237{238return S_OK; // See notes in OnDeviceAdded handler about why we ignore this.239}240241static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState)242{243IMMDevice *device = NULL;244245if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {246IMMEndpoint *endpoint = NULL;247if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) {248EDataFlow flow;249if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {250const bool recording = (flow == eCapture);251if (dwNewState == DEVICE_STATE_ACTIVE) {252char *utf8dev;253WAVEFORMATEXTENSIBLE fmt;254GUID dsoundguid;255GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);256if (utf8dev) {257SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid);258SDL_free(utf8dev);259}260} else {261immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId));262}263}264IMMEndpoint_Release(endpoint);265}266IMMDevice_Release(device);267}268269return S_OK;270}271272static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)273{274return S_OK; // we don't care about these.275}276277static const IMMNotificationClientVtbl notification_client_vtbl = {278SDLMMNotificationClient_QueryInterface,279SDLMMNotificationClient_AddRef,280SDLMMNotificationClient_Release,281SDLMMNotificationClient_OnDeviceStateChanged,282SDLMMNotificationClient_OnDeviceAdded,283SDLMMNotificationClient_OnDeviceRemoved,284SDLMMNotificationClient_OnDefaultDeviceChanged,285SDLMMNotificationClient_OnPropertyValueChanged286};287288static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 } };289290bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks)291{292HRESULT ret;293294// just skip the discussion with COM here.295if (!WIN_IsWindowsVistaOrGreater()) {296return SDL_SetError("IMMDevice support requires Windows Vista or later");297}298299if (FAILED(WIN_CoInitialize())) {300return SDL_SetError("IMMDevice: CoInitialize() failed");301}302303ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator);304if (FAILED(ret)) {305WIN_CoUninitialize();306return WIN_SetErrorFromHRESULT("IMMDevice CoCreateInstance(MMDeviceEnumerator)", ret);307}308309if (callbacks) {310SDL_copyp(&immcallbacks, callbacks);311} else {312SDL_zero(immcallbacks);313}314315if (!immcallbacks.audio_device_disconnected) {316immcallbacks.audio_device_disconnected = SDL_AudioDeviceDisconnected;317}318if (!immcallbacks.default_audio_device_changed) {319immcallbacks.default_audio_device_changed = SDL_DefaultAudioDeviceChanged;320}321322return true;323}324325void SDL_IMMDevice_Quit(void)326{327if (enumerator) {328IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client);329IMMDeviceEnumerator_Release(enumerator);330enumerator = NULL;331}332333SDL_zero(immcallbacks);334335WIN_CoUninitialize();336}337338bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool recording)339{340const 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.341342SDL_assert(device != NULL);343SDL_assert(immdevice != NULL);344345LPCWSTR devid = SDL_IMMDevice_GetDevID(device);346SDL_assert(devid != NULL);347348HRESULT ret;349while ((ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, immdevice)) == E_NOTFOUND) {350const Uint64 now = SDL_GetTicks();351if (timeout > now) {352const Uint64 ticksleft = timeout - now;353SDL_Delay((Uint32)SDL_min(ticksleft, 300)); // wait awhile and try again.354continue;355}356break;357}358359if (!SUCCEEDED(ret)) {360return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);361}362return true;363}364365static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device)366{367/* Note that WASAPI separates "adapter devices" from "audio endpoint devices"368...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */369370IMMDeviceCollection *collection = NULL;371if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, recording ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {372return;373}374375UINT total = 0;376if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {377IMMDeviceCollection_Release(collection);378return;379}380381LPWSTR default_devid = NULL;382if (default_device) {383IMMDevice *default_immdevice = NULL;384const EDataFlow dataflow = recording ? eCapture : eRender;385if (SUCCEEDED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &default_immdevice))) {386LPWSTR devid = NULL;387if (SUCCEEDED(IMMDevice_GetId(default_immdevice, &devid))) {388default_devid = SDL_wcsdup(devid); // if this fails, oh well.389CoTaskMemFree(devid);390}391IMMDevice_Release(default_immdevice);392}393}394395for (UINT i = 0; i < total; i++) {396IMMDevice *immdevice = NULL;397if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &immdevice))) {398LPWSTR devid = NULL;399if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) {400char *devname = NULL;401WAVEFORMATEXTENSIBLE fmt;402GUID dsoundguid;403SDL_zero(fmt);404SDL_zero(dsoundguid);405GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);406if (devname) {407SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid);408if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {409*default_device = sdldevice;410}411SDL_free(devname);412}413CoTaskMemFree(devid);414}415IMMDevice_Release(immdevice);416}417}418419SDL_free(default_devid);420421IMMDeviceCollection_Release(collection);422}423424void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording)425{426EnumerateEndpointsForFlow(false, default_playback);427EnumerateEndpointsForFlow(true, default_recording);428429// if this fails, we just won't get hotplug events. Carry on anyhow.430IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)¬ification_client);431}432433#endif // defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H)434435436