Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/thirdparty/sdl/core/windows/SDL_immdevice.c
21649 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, SDL_AudioFormat force_format)
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 = (force_format != SDL_AUDIO_UNKNOWN) ? force_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
SDL_AudioFormat force_format;
187
} SDLMMNotificationClient;
188
189
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_QueryInterface(IMMNotificationClient *client, REFIID iid, void **ppv)
190
{
191
if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient))) {
192
*ppv = client;
193
client->lpVtbl->AddRef(client);
194
return S_OK;
195
}
196
197
*ppv = NULL;
198
return E_NOINTERFACE;
199
}
200
201
static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient)
202
{
203
SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
204
return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1);
205
}
206
207
static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_Release(IMMNotificationClient *iclient)
208
{
209
// client is a static object; we don't ever free it.
210
SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
211
const ULONG rc = SDL_AtomicDecRef(&client->refcount);
212
if (rc == 0) {
213
SDL_SetAtomicInt(&client->refcount, 0); // uhh...
214
return 0;
215
}
216
return rc - 1;
217
}
218
219
// These are the entry points called when WASAPI device endpoints change.
220
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *iclient, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
221
{
222
if (role == SDL_IMMDevice_role) {
223
immcallbacks.default_audio_device_changed(SDL_IMMDevice_FindByDevID(pwstrDeviceId));
224
}
225
return S_OK;
226
}
227
228
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)
229
{
230
/* we ignore this; devices added here then progress to ACTIVE, if appropriate, in
231
OnDeviceStateChange, making that a better place to deal with device adds. More
232
importantly: the first time you plug in a USB audio device, this callback will
233
fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).
234
Plugging it back in won't fire this callback again. */
235
return S_OK;
236
}
237
238
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId)
239
{
240
return S_OK; // See notes in OnDeviceAdded handler about why we ignore this.
241
}
242
243
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *iclient, LPCWSTR pwstrDeviceId, DWORD dwNewState)
244
{
245
SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
246
IMMDevice *device = NULL;
247
248
if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {
249
IMMEndpoint *endpoint = NULL;
250
if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **)&endpoint))) {
251
EDataFlow flow;
252
if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {
253
const bool recording = (flow == eCapture);
254
if (dwNewState == DEVICE_STATE_ACTIVE) {
255
char *utf8dev;
256
WAVEFORMATEXTENSIBLE fmt;
257
GUID dsoundguid;
258
GetMMDeviceInfo(device, &utf8dev, &fmt, &dsoundguid);
259
if (utf8dev) {
260
SDL_IMMDevice_Add(recording, utf8dev, &fmt, pwstrDeviceId, &dsoundguid, client->force_format);
261
SDL_free(utf8dev);
262
}
263
} else {
264
immcallbacks.audio_device_disconnected(SDL_IMMDevice_FindByDevID(pwstrDeviceId));
265
}
266
}
267
IMMEndpoint_Release(endpoint);
268
}
269
IMMDevice_Release(device);
270
}
271
272
return S_OK;
273
}
274
275
static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
276
{
277
return S_OK; // we don't care about these.
278
}
279
280
static const IMMNotificationClientVtbl notification_client_vtbl = {
281
SDLMMNotificationClient_QueryInterface,
282
SDLMMNotificationClient_AddRef,
283
SDLMMNotificationClient_Release,
284
SDLMMNotificationClient_OnDeviceStateChanged,
285
SDLMMNotificationClient_OnDeviceAdded,
286
SDLMMNotificationClient_OnDeviceRemoved,
287
SDLMMNotificationClient_OnDefaultDeviceChanged,
288
SDLMMNotificationClient_OnPropertyValueChanged
289
};
290
291
static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 }, SDL_AUDIO_UNKNOWN };
292
293
bool SDL_IMMDevice_Init(const SDL_IMMDevice_callbacks *callbacks)
294
{
295
HRESULT ret;
296
297
// just skip the discussion with COM here.
298
if (!WIN_IsWindowsVistaOrGreater()) {
299
return SDL_SetError("IMMDevice support requires Windows Vista or later");
300
}
301
302
if (FAILED(WIN_CoInitialize())) {
303
return SDL_SetError("IMMDevice: CoInitialize() failed");
304
}
305
306
ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *)&enumerator);
307
if (FAILED(ret)) {
308
WIN_CoUninitialize();
309
return WIN_SetErrorFromHRESULT("IMMDevice CoCreateInstance(MMDeviceEnumerator)", ret);
310
}
311
312
if (callbacks) {
313
SDL_copyp(&immcallbacks, callbacks);
314
} else {
315
SDL_zero(immcallbacks);
316
}
317
318
if (!immcallbacks.audio_device_disconnected) {
319
immcallbacks.audio_device_disconnected = SDL_AudioDeviceDisconnected;
320
}
321
if (!immcallbacks.default_audio_device_changed) {
322
immcallbacks.default_audio_device_changed = SDL_DefaultAudioDeviceChanged;
323
}
324
325
return true;
326
}
327
328
void SDL_IMMDevice_Quit(void)
329
{
330
if (enumerator) {
331
IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
332
IMMDeviceEnumerator_Release(enumerator);
333
enumerator = NULL;
334
}
335
336
SDL_zero(immcallbacks);
337
338
WIN_CoUninitialize();
339
}
340
341
bool SDL_IMMDevice_Get(SDL_AudioDevice *device, IMMDevice **immdevice, bool recording)
342
{
343
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.
344
345
SDL_assert(device != NULL);
346
SDL_assert(immdevice != NULL);
347
348
LPCWSTR devid = SDL_IMMDevice_GetDevID(device);
349
SDL_assert(devid != NULL);
350
351
HRESULT ret;
352
while ((ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, immdevice)) == E_NOTFOUND) {
353
const Uint64 now = SDL_GetTicks();
354
if (timeout > now) {
355
const Uint64 ticksleft = timeout - now;
356
SDL_Delay((Uint32)SDL_min(ticksleft, 300)); // wait awhile and try again.
357
continue;
358
}
359
break;
360
}
361
362
if (!SUCCEEDED(ret)) {
363
return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
364
}
365
return true;
366
}
367
368
static void EnumerateEndpointsForFlow(const bool recording, SDL_AudioDevice **default_device, SDL_AudioFormat force_format)
369
{
370
/* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
371
...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
372
373
IMMDeviceCollection *collection = NULL;
374
if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, recording ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {
375
return;
376
}
377
378
UINT total = 0;
379
if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {
380
IMMDeviceCollection_Release(collection);
381
return;
382
}
383
384
LPWSTR default_devid = NULL;
385
if (default_device) {
386
IMMDevice *default_immdevice = NULL;
387
const EDataFlow dataflow = recording ? eCapture : eRender;
388
if (SUCCEEDED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_IMMDevice_role, &default_immdevice))) {
389
LPWSTR devid = NULL;
390
if (SUCCEEDED(IMMDevice_GetId(default_immdevice, &devid))) {
391
default_devid = SDL_wcsdup(devid); // if this fails, oh well.
392
CoTaskMemFree(devid);
393
}
394
IMMDevice_Release(default_immdevice);
395
}
396
}
397
398
for (UINT i = 0; i < total; i++) {
399
IMMDevice *immdevice = NULL;
400
if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &immdevice))) {
401
LPWSTR devid = NULL;
402
if (SUCCEEDED(IMMDevice_GetId(immdevice, &devid))) {
403
char *devname = NULL;
404
WAVEFORMATEXTENSIBLE fmt;
405
GUID dsoundguid;
406
SDL_zero(fmt);
407
SDL_zero(dsoundguid);
408
GetMMDeviceInfo(immdevice, &devname, &fmt, &dsoundguid);
409
if (devname) {
410
SDL_AudioDevice *sdldevice = SDL_IMMDevice_Add(recording, devname, &fmt, devid, &dsoundguid, force_format);
411
if (default_device && default_devid && SDL_wcscmp(default_devid, devid) == 0) {
412
*default_device = sdldevice;
413
}
414
SDL_free(devname);
415
}
416
CoTaskMemFree(devid);
417
}
418
IMMDevice_Release(immdevice);
419
}
420
}
421
422
SDL_free(default_devid);
423
424
IMMDeviceCollection_Release(collection);
425
}
426
427
void SDL_IMMDevice_EnumerateEndpoints(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording, SDL_AudioFormat force_format)
428
{
429
EnumerateEndpointsForFlow(false, default_playback, force_format);
430
EnumerateEndpointsForFlow(true, default_recording, force_format);
431
432
notification_client.force_format = force_format;
433
434
// if this fails, we just won't get hotplug events. Carry on anyhow.
435
IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *)&notification_client);
436
}
437
438
#endif // defined(SDL_PLATFORM_WINDOWS) && defined(HAVE_MMDEVICEAPI_H)
439
440