summaryrefslogtreecommitdiff
path: root/SDL-3.2.8/src/core/windows/SDL_immdevice.c
diff options
context:
space:
mode:
Diffstat (limited to 'SDL-3.2.8/src/core/windows/SDL_immdevice.c')
-rw-r--r--SDL-3.2.8/src/core/windows/SDL_immdevice.c434
1 files changed, 434 insertions, 0 deletions
diff --git a/SDL-3.2.8/src/core/windows/SDL_immdevice.c b/SDL-3.2.8/src/core/windows/SDL_immdevice.c
new file mode 100644
index 0000000..802a412
--- /dev/null
+++ b/SDL-3.2.8/src/core/windows/SDL_immdevice.c
@@ -0,0 +1,434 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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)
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
182typedef struct SDLMMNotificationClient
183{
184 const IMMNotificationClientVtbl *lpVtbl;
185 SDL_AtomicInt refcount;
186} SDLMMNotificationClient;
187
188static 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
200static ULONG STDMETHODCALLTYPE SDLMMNotificationClient_AddRef(IMMNotificationClient *iclient)
201{
202 SDLMMNotificationClient *client = (SDLMMNotificationClient *)iclient;
203 return (ULONG)(SDL_AtomicIncRef(&client->refcount) + 1);
204}
205
206static 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.
219static 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
227static 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
237static 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
242static 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
273static HRESULT STDMETHODCALLTYPE SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *client, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
274{
275 return S_OK; // we don't care about these.
276}
277
278static 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
289static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 } };
290
291bool 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
326void 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
339bool 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
366static 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
425void 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)