Atlas - SDL_immdevice.c

Home / ext / SDL / src / core / windows Lines: 1 | Size: 19618 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)]
[FILE BEGIN]
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2025 Sam Lantinga <[email protected]> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "SDL_internal.h" 22 23#if defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H) 24 25#include "SDL_windows.h" 26#include "SDL_immdevice.h" 27#include "../../audio/SDL_sysaudio.h" 28#include <objbase.h> // For CLSIDFromString 29 30typedef struct SDL_IMMDevice_HandleData 31{ 32 LPWSTR immdevice_id; 33 GUID directsound_guid; 34} SDL_IMMDevice_HandleData; 35 36static const ERole SDL_IMMDevice_role = eConsole; // !!! FIXME: should this be eMultimedia? Should be a hint? 37 38// This is global to the WASAPI target, to handle hotplug and default device lookup. 39static IMMDeviceEnumerator *enumerator = NULL; 40static SDL_IMMDevice_callbacks immcallbacks; 41 42// PropVariantInit() is an inline function/macro in PropIdl.h that calls the C runtime's memset() directly. Use ours instead, to avoid dependency. 43#ifdef PropVariantInit 44#undef PropVariantInit 45#endif 46#define PropVariantInit(p) SDL_zerop(p) 47 48// Some GUIDs we need to know without linking to libraries that aren't available before Vista. 49/* *INDENT-OFF* */ // clang-format off 50static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } }; 51static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } }; 52static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } }; 53static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } }; 54static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 }; 55static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 }; 56static const PROPERTYKEY SDL_PKEY_AudioEndpoint_GUID = { { 0x1da5d803, 0xd492, 0x4edd,{ 0x8c, 0x23, 0xe0, 0xc0, 0xff, 0xee, 0x7f, 0x0e, } }, 4 }; 57/* *INDENT-ON* */ // clang-format on 58 59static bool FindByDevIDCallback(SDL_AudioDevice *device, void *userdata) 60{ 61 LPCWSTR devid = (LPCWSTR)userdata; 62 if (devid && device && device->handle) { 63 const SDL_IMMDevice_HandleData *handle = (const SDL_IMMDevice_HandleData *)device->handle; 64 if (handle->immdevice_id && SDL_wcscmp(handle->immdevice_id, devid) == 0) { 65 return true; 66 } 67 } 68 return false; 69} 70 71static SDL_AudioDevice *SDL_IMMDevice_FindByDevID(LPCWSTR devid) 72{ 73 return SDL_FindPhysicalAudioDeviceByCallback(FindByDevIDCallback, (void *) devid); 74} 75 76LPGUID SDL_IMMDevice_GetDirectSoundGUID(SDL_AudioDevice *device) 77{ 78 return (device && device->handle) ? &(((SDL_IMMDevice_HandleData *) device->handle)->directsound_guid) : NULL; 79} 80 81LPCWSTR SDL_IMMDevice_GetDevID(SDL_AudioDevice *device) 82{ 83 return (device && device->handle) ? ((const SDL_IMMDevice_HandleData *) device->handle)->immdevice_id : NULL; 84} 85 86static void GetMMDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt, GUID *guid) 87{ 88 /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be 89 "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in 90 its own UIs, like Volume Control, etc. */ 91 IPropertyStore *props = NULL; 92 *utf8dev = NULL; 93 SDL_zerop(fmt); 94 if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) { 95 PROPVARIANT var; 96 PropVariantInit(&var); 97 if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) { 98 *utf8dev = WIN_StringToUTF8W(var.pwszVal); 99 } 100 PropVariantClear(&var); 101 if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEngine_DeviceFormat, &var))) { 102 SDL_memcpy(fmt, var.blob.pBlobData, SDL_min(var.blob.cbSize, sizeof(WAVEFORMATEXTENSIBLE))); 103 } 104 PropVariantClear(&var); 105 if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEndpoint_GUID, &var))) { 106 (void)CLSIDFromString(var.pwszVal, guid); 107 } 108 PropVariantClear(&var); 109 IPropertyStore_Release(props); 110 } 111} 112 113void SDL_IMMDevice_FreeDeviceHandle(SDL_AudioDevice *device) 114{ 115 if (device && device->handle) { 116 SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *) device->handle; 117 SDL_free(handle->immdevice_id); 118 SDL_free(handle); 119 device->handle = NULL; 120 } 121} 122 123static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid, SDL_AudioFormat force_format, bool supports_recording_playback_devices) 124{ 125 /* You can have multiple endpoints on a device that are mutually exclusive ("Speakers" vs "Line Out" or whatever). 126 In a perfect world, things that are unplugged won't be in this collection. The only gotcha is probably for 127 phones and tablets, where you might have an internal speaker and a headphone jack and expect both to be 128 available and switch automatically. (!!! FIXME...?) */ 129 130 if (!devname) { 131 return NULL; 132 } 133 134 // see if we already have this one first. 135 SDL_AudioDevice *device = SDL_IMMDevice_FindByDevID(devid); 136 if (device) { 137 if (SDL_GetAtomicInt(&device->zombie)) { 138 // whoa, it came back! This can happen if you unplug and replug USB headphones while we're still keeping the SDL object alive. 139 // Kill this device's IMMDevice id; the device will go away when the app closes it, or maybe a new default device is chosen 140 // (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. 141 SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *) device->handle; 142 SDL_free(handle->immdevice_id); 143 handle->immdevice_id = NULL; 144 device = NULL; // add a new device, below. 145 } 146 } 147 148 if (!device) { 149 // handle is freed by SDL_IMMDevice_FreeDeviceHandle! 150 SDL_IMMDevice_HandleData *handle = (SDL_IMMDevice_HandleData *)SDL_calloc(1, sizeof(*handle)); 151 if (!handle) { 152 return NULL; 153 } 154 handle->immdevice_id = SDL_wcsdup(devid); 155 if (!handle->immdevice_id) { 156 SDL_free(handle); 157 return NULL; 158 } 159 SDL_copyp(&handle->directsound_guid, dsoundguid); 160 161 SDL_AudioSpec spec; 162 SDL_zero(spec); 163 spec.channels = (Uint8)fmt->Format.nChannels; 164 spec.freq = fmt->Format.nSamplesPerSec; 165 spec.format = (force_format != SDL_AUDIO_UNKNOWN) ? force_format : SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt); 166 167 device = SDL_AddAudioDevice(recording, devname, &spec, handle); 168 169 if (!recording && supports_recording_playback_devices) { 170 // handle is freed by SDL_IMMDevice_FreeDeviceHandle! 171 SDL_IMMDevice_HandleData *recording_handle = (SDL_IMMDevice_HandleData *)SDL_malloc(sizeof(*recording_handle)); 172 if (!recording_handle) { 173 return NULL; 174 } 175 176 recording_handle->immdevice_id = SDL_wcsdup(devid); 177 if (!recording_handle->immdevice_id) { 178 SDL_free(recording_handle); 179 return NULL; 180 } 181 182 SDL_copyp(&recording_handle->directsound_guid, dsoundguid); 183 184 if (!SDL_AddAudioDevice(true, devname, &spec, recording_handle)) { 185 SDL_free(recording_handle->immdevice_id); 186 SDL_free(recording_handle); 187 } 188 } 189 190 if (!device) { 191 SDL_free(handle->immdevice_id); 192 SDL_free(handle); 193 } 194 } 195 196 return device; 197} 198 199/* We need a COM subclass of IMMNotificationClient for hotplug support, which is 200 easy in C++, but we have to tapdance more to make work in C. 201 Thanks to this page for coaching on how to make this work: 202 https://www.codeproject.com/Articles/13601/COM-in-plain-C */ 203 204typedef struct SDLMMNotificationClient 205{ 206 const IMMNotificationClientVtbl *lpVtbl; 207 SDL_AtomicInt refcount; 208 SDL_AudioFormat force_format; 209 bool supports_recording_playback_devices; 210} SDLMMNotificationClient; 211 212static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv) 213{ 214 if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) { 215 *ppv = client; 216 client->lpVtbl->AddRef(client); 217 return S_OK; 218 } 219 220 *ppv = NULL; 221 return E_NOINTERFACE; 222} 223 224static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient) 225{ 226 SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient; 227 return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1); 228} 229 230static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_Release(IMMNotificationClient *iclient) 231{ 232 // client is a static object; we don't ever free it. 233 SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient; 234 const ULONG rc = SDL_AtomicDecRef(&client->refcount); 235 if (rc == 0) { 236 SDL_SetAtomicInt(&client->refcount, 0); // uhh... 237 return 0; 238 } 239 return rc - 1; 240} 241 242// These are the entry points called when WASAPI device endpoints change. 243static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *iclient, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) 244{ 245 if (role == SDL_IMMDevice_role) { 246 immcallbacks.default_audio_device_changed(SDL_IMMDevice_FindByDevID(pwstrDeviceId)); 247 } 248 return S_OK; 249} 250 251static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId) 252{ 253 /* we ignore this; devices added here then progress to ACTIVE, if appropriate, in 254 OnDeviceStateChange, making that a better place to deal with device adds. More 255 importantly: the first time you plug in a USB audio device, this callback will 256 fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT). 257 Plugging it back in won't fire this callback again. */ 258 return S_OK; 259} 260 261static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId) 262{ 263 return S_OK; // See notes in OnDeviceAdded handler about why we ignore this. 264} 265 266static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState) 267{ 268 SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient; 269 IMMDevice *device = NULL; 270 271 if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) { 272 IMMEndpoint *endpoint = NULL; 273 if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) { 274 EDataFlow flow; 275 if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) { 276 const bool recording = (flow == eCapture); 277 if (dwNewState == DEVICE_STATE_ACTIVE) { 278 char *utf8dev; 279 WAVEFORMATEXTENSIBLE fmt; 280 GUID dsoundguid; 281 GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid); 282 if (utf8dev) { 283 SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format, client->supports_recording_playback_devices); 284 SDL_free(utf8dev); 285 } 286 } else { 287 immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId)); 288 } 289 } 290 IMMEndpoint_Release(endpoint); 291 } 292 IMMDevice_Release(device); 293 } 294 295 return S_OK; 296} 297 298static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key) 299{ 300 return S_OK; // we don't care about these. 301} 302 303static const IMMNotificationClientVtbl notification_client_vtbl = { 304 SDLMMNotificationClient_QueryInterface, 305 SDLMMNotificationClient_AddRef, 306 SDLMMNotificationClient_Release, 307 SDLMMNotificationClient_OnDeviceStateChanged, 308 SDLMMNotificationClient_OnDeviceAdded, 309 SDLMMNotificationClient_OnDeviceRemoved, 310 SDLMMNotificationClient_OnDefaultDeviceChanged, 311 SDLMMNotificationClient_OnPropertyValueChanged 312}; 313 314static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN, false }; 315 316bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks) 317{ 318 HRESULT ret; 319 320 // just skip the discussion with COM here. 321 if (!WIN_IsWindowsVistaOrGreater()) { 322 return SDL_SetError("IMMDevice support requires Windows Vista or later"); 323 } 324 325 if (FAILED(WIN_CoInitialize())) { 326 return SDL_SetError("IMMDevice: CoInitialize() failed"); 327 } 328 329 ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator); 330 if (FAILED(ret)) { 331 WIN_CoUninitialize(); 332 return WIN_SetErrorFromHRESULT("IMMDevice CoCreateInstance(MMDeviceEnumerator)", ret); 333 } 334 335 if (callbacks) { 336 SDL_copyp(&immcallbacks, callbacks); 337 } else { 338 SDL_zero(immcallbacks); 339 } 340 341 if (!immcallbacks.audio_device_disconnected) { 342 immcallbacks.audio_device_disconnected = SDL_AudioDeviceDisconnected; 343 } 344 if (!immcallbacks.default_audio_device_changed) { 345 immcallbacks.default_audio_device_changed = SDL_DefaultAudioDeviceChanged; 346 } 347 348 return true; 349} 350 351void SDL_IMMDevice_Quit(void) 352{ 353 if (enumerator) { 354 IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client); 355 IMMDeviceEnumerator_Release(enumerator); 356 enumerator = NULL; 357 } 358 359 SDL_zero(immcallbacks); 360 361 WIN_CoUninitialize(); 362} 363 364bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool recording) 365{ 366 const 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. 367 368 SDL_assert(device != NULL); 369 SDL_assert(immdevice != NULL); 370 371 LPCWSTR devid = SDL_IMMDevice_GetDevID(device); 372 SDL_assert(devid != NULL); 373 374 HRESULT ret; 375 while ((ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, immdevice)) == E_NOTFOUND) { 376 const Uint64 now = SDL_GetTicks(); 377 if (timeout > now) { 378 const Uint64 ticksleft = timeout - now; 379 SDL_Delay((Uint32)SDL_min(ticksleft, 300)); // wait awhile and try again. 380 continue; 381 } 382 break; 383 } 384 385 if (!SUCCEEDED(ret)) { 386 return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret); 387 } 388 return true; 389} 390 391bool SDL_IMMDevice_GetIsCapture(IMMDevice *device) 392{ 393 bool iscapture = false; 394 IMMEndpoint *endpoint = NULL; 395 if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) { 396 EDataFlow flow; 397 398 if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) { 399 iscapture = (flow == eCapture); 400 } 401 } 402 403 IMMEndpoint_Release(endpoint); 404 return iscapture; 405} 406 407static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format, bool supports_recording_playback_devices) 408{ 409 /* Note that WASAPI separates "adapter devices" from "audio endpoint devices" 410 ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */ 411 412 IMMDeviceCollection *collection = NULL; 413 if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, recording ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) { 414 return; 415 } 416 417 UINT total = 0; 418 if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) { 419 IMMDeviceCollection_Release(collection); 420 return; 421 } 422 423 LPWSTR default_devid = NULL; 424 if (default_device) { 425 IMMDevice *default_immdevice = NULL; 426 const EDataFlow dataflow = recording ? eCapture : eRender; 427 if (SUCCEEDED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &default_immdevice))) { 428 LPWSTR devid = NULL; 429 if (SUCCEEDED(IMMDevice_GetId(default_immdevice, &devid))) { 430 default_devid = SDL_wcsdup(devid); // if this fails, oh well. 431 CoTaskMemFree(devid); 432 } 433 IMMDevice_Release(default_immdevice); 434 } 435 } 436 437 for (UINT i = 0; i < total; i++) { 438 IMMDevice *immdevice = NULL; 439 if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &immdevice))) { 440 LPWSTR devid = NULL; 441 if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) { 442 char *devname = NULL; 443 WAVEFORMATEXTENSIBLE fmt; 444 GUID dsoundguid; 445 SDL_zero(fmt); 446 SDL_zero(dsoundguid); 447 GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid); 448 if (devname) { 449 SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format, supports_recording_playback_devices); 450 if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) { 451 *default_device = sdldevice; 452 } 453 SDL_free(devname); 454 } 455 CoTaskMemFree(devid); 456 } 457 IMMDevice_Release(immdevice); 458 } 459 } 460 461 SDL_free(default_devid); 462 463 IMMDeviceCollection_Release(collection); 464} 465 466void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format, bool supports_recording_playback_devices) 467{ 468 EnumerateEndpointsForFlow(false, default_playback, force_format, supports_recording_playback_devices); 469 EnumerateEndpointsForFlow(true, default_recording, force_format, supports_recording_playback_devices); 470 471 notification_client.force_format = force_format; 472 notification_client.supports_recording_playback_devices = supports_recording_playback_devices; 473 474 // if this fails, we just won't get hotplug events. Carry on anyhow. 475 IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client); 476} 477 478#endif // defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H) 479
[FILE END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.