Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/thirdparty/sdl/core/windows/SDL_immdevice.c
9905 views
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
30
typedef struct SDL_IMMDevice_HandleData
31
{
32
LPWSTR immdevice_id;
33
GUID directsound_guid;
34
} SDL_IMMDevice_HandleData;
35
36
static 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.
39
static IMMDeviceEnumerator *enumerator = NULL;
40
static 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
50
static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } };
51
static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } };
52
static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } };
53
static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } };
54
static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 };
55
static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 };
56
static 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
59
static 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
71
static SDL_AudioDevice *SDL_IMMDevice_FindByDevID(LPCWSTR devid)
72
{
73
return SDL_FindPhysicalAudioDeviceByCallback(FindByDevIDCallback, (void *) devid);
74
}
75
76
LPGUID SDL_IMMDevice_GetDirectSoundGUID(SDL_AudioDevice *device)
77
{
78
return (device && device->handle) ? &(((SDL_IMMDevice_HandleData *) device->handle)->directsound_guid) : NULL;
79
}
80
81
LPCWSTR SDL_IMMDevice_GetDevID(SDL_AudioDevice *device)
82
{
83
return (device && device->handle) ? ((const SDL_IMMDevice_HandleData *) device->handle)->immdevice_id : NULL;
84
}
85
86
static 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
113
void 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
123
static SDL_AudioDevice *SDL_IMMDevice_Add(const bool recording, const char *devname, WAVEFORMATEXTENSIBLE *fmt, LPCWSTR devid, GUID *dsoundguid)
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_malloc(sizeof(SDL_IMMDevice_HandleData));
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_memcpy(&handle->directsound_guid, dsoundguid, sizeof(GUID));
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 = SDL_WaveFormatExToSDLFormat((WAVEFORMATEX *)fmt);
166
167
device = SDL_AddAudioDevice(recording, devname, &spec, handle);
168
if (!device) {
169
SDL_free(handle->immdevice_id);
170
SDL_free(handle);
171
}
172
}
173
174
return device;
175
}
176
177
/* We need a COM subclass of IMMNotificationClient for hotplug support, which is
178
easy in C++, but we have to tapdance more to make work in C.
179
Thanks to this page for coaching on how to make this work:
180
https://www.codeproject.com/Articles/13601/COM-in-plain-C */
181
182
typedef struct SDLMMNotificationClient
183
{
184
const IMMNotificationClientVtbl *lpVtbl;
185
SDL_AtomicInt refcount;
186
} SDLMMNotificationClient;
187
188
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv)
189
{
190
if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) {
191
*ppv = client;
192
client->lpVtbl->AddRef(client);
193
return S_OK;
194
}
195
196
*ppv = NULL;
197
return E_NOINTERFACE;
198
}
199
200
static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient)
201
{
202
SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
203
return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1);
204
}
205
206
static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_Release(IMMNotificationClient *iclient)
207
{
208
// client is a static object; we don't ever free it.
209
SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
210
const ULONG rc = SDL_AtomicDecRef(&client->refcount);
211
if (rc == 0) {
212
SDL_SetAtomicInt(&client->refcount, 0); // uhh...
213
return 0;
214
}
215
return rc - 1;
216
}
217
218
// These are the entry points called when WASAPI device endpoints change.
219
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *iclient, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
220
{
221
if (role == SDL_IMMDevice_role) {
222
immcallbacks.default_audio_device_changed(SDL_IMMDevice_FindByDevID(pwstrDeviceId));
223
}
224
return S_OK;
225
}
226
227
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)
228
{
229
/* we ignore this; devices added here then progress to ACTIVE, if appropriate, in
230
OnDeviceStateChange, making that a better place to deal with device adds. More
231
importantly: the first time you plug in a USB audio device, this callback will
232
fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).
233
Plugging it back in won't fire this callback again. */
234
return S_OK;
235
}
236
237
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)
238
{
239
return S_OK; // See notes in OnDeviceAdded handler about why we ignore this.
240
}
241
242
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState)
243
{
244
IMMDevice *device = NULL;
245
246
if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {
247
IMMEndpoint *endpoint = NULL;
248
if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) {
249
EDataFlow flow;
250
if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {
251
const bool recording = (flow == eCapture);
252
if (dwNewState == DEVICE_STATE_ACTIVE) {
253
char *utf8dev;
254
WAVEFORMATEXTENSIBLE fmt;
255
GUID dsoundguid;
256
GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);
257
if (utf8dev) {
258
SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid);
259
SDL_free(utf8dev);
260
}
261
} else {
262
immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId));
263
}
264
}
265
IMMEndpoint_Release(endpoint);
266
}
267
IMMDevice_Release(device);
268
}
269
270
return S_OK;
271
}
272
273
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
274
{
275
return S_OK; // we don't care about these.
276
}
277
278
static const IMMNotificationClientVtbl notification_client_vtbl = {
279
SDLMMNotificationClient_QueryInterface,
280
SDLMMNotificationClient_AddRef,
281
SDLMMNotificationClient_Release,
282
SDLMMNotificationClient_OnDeviceStateChanged,
283
SDLMMNotificationClient_OnDeviceAdded,
284
SDLMMNotificationClient_OnDeviceRemoved,
285
SDLMMNotificationClient_OnDefaultDeviceChanged,
286
SDLMMNotificationClient_OnPropertyValueChanged
287
};
288
289
static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 } };
290
291
bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks)
292
{
293
HRESULT ret;
294
295
// just skip the discussion with COM here.
296
if (!WIN_IsWindowsVistaOrGreater()) {
297
return SDL_SetError("IMMDevice support requires Windows Vista or later");
298
}
299
300
if (FAILED(WIN_CoInitialize())) {
301
return SDL_SetError("IMMDevice: CoInitialize() failed");
302
}
303
304
ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator);
305
if (FAILED(ret)) {
306
WIN_CoUninitialize();
307
return WIN_SetErrorFromHRESULT("IMMDevice CoCreateInstance(MMDeviceEnumerator)", ret);
308
}
309
310
if (callbacks) {
311
SDL_copyp(&immcallbacks, callbacks);
312
} else {
313
SDL_zero(immcallbacks);
314
}
315
316
if (!immcallbacks.audio_device_disconnected) {
317
immcallbacks.audio_device_disconnected = SDL_AudioDeviceDisconnected;
318
}
319
if (!immcallbacks.default_audio_device_changed) {
320
immcallbacks.default_audio_device_changed = SDL_DefaultAudioDeviceChanged;
321
}
322
323
return true;
324
}
325
326
void SDL_IMMDevice_Quit(void)
327
{
328
if (enumerator) {
329
IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
330
IMMDeviceEnumerator_Release(enumerator);
331
enumerator = NULL;
332
}
333
334
SDL_zero(immcallbacks);
335
336
WIN_CoUninitialize();
337
}
338
339
bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool recording)
340
{
341
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.
342
343
SDL_assert(device != NULL);
344
SDL_assert(immdevice != NULL);
345
346
LPCWSTR devid = SDL_IMMDevice_GetDevID(device);
347
SDL_assert(devid != NULL);
348
349
HRESULT ret;
350
while ((ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, immdevice)) == E_NOTFOUND) {
351
const Uint64 now = SDL_GetTicks();
352
if (timeout > now) {
353
const Uint64 ticksleft = timeout - now;
354
SDL_Delay((Uint32)SDL_min(ticksleft, 300)); // wait awhile and try again.
355
continue;
356
}
357
break;
358
}
359
360
if (!SUCCEEDED(ret)) {
361
return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
362
}
363
return true;
364
}
365
366
static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device)
367
{
368
/* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
369
...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
370
371
IMMDeviceCollection *collection = NULL;
372
if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, recording ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {
373
return;
374
}
375
376
UINT total = 0;
377
if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {
378
IMMDeviceCollection_Release(collection);
379
return;
380
}
381
382
LPWSTR default_devid = NULL;
383
if (default_device) {
384
IMMDevice *default_immdevice = NULL;
385
const EDataFlow dataflow = recording ? eCapture : eRender;
386
if (SUCCEEDED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &default_immdevice))) {
387
LPWSTR devid = NULL;
388
if (SUCCEEDED(IMMDevice_GetId(default_immdevice, &devid))) {
389
default_devid = SDL_wcsdup(devid); // if this fails, oh well.
390
CoTaskMemFree(devid);
391
}
392
IMMDevice_Release(default_immdevice);
393
}
394
}
395
396
for (UINT i = 0; i < total; i++) {
397
IMMDevice *immdevice = NULL;
398
if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &immdevice))) {
399
LPWSTR devid = NULL;
400
if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) {
401
char *devname = NULL;
402
WAVEFORMATEXTENSIBLE fmt;
403
GUID dsoundguid;
404
SDL_zero(fmt);
405
SDL_zero(dsoundguid);
406
GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);
407
if (devname) {
408
SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid);
409
if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {
410
*default_device = sdldevice;
411
}
412
SDL_free(devname);
413
}
414
CoTaskMemFree(devid);
415
}
416
IMMDevice_Release(immdevice);
417
}
418
}
419
420
SDL_free(default_devid);
421
422
IMMDeviceCollection_Release(collection);
423
}
424
425
void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording)
426
{
427
EnumerateEndpointsForFlow(false, default_playback);
428
EnumerateEndpointsForFlow(true, default_recording);
429
430
// if this fails, we just won't get hotplug events. Carry on anyhow.
431
IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
432
}
433
434
#endif // defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H)
435
436