1 /*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2021 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 /* This is code that Windows uses to talk to WASAPI-related system APIs.
24 This is for non-WinRT desktop apps. The C++/CX implementation of these
25 functions, exclusive to WinRT, are in SDL_wasapi_winrt.cpp.
26 The code in SDL_wasapi.c is used by both standard Windows and WinRT builds
27 to deal with audio and calls into these functions. */
28
29 #if SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__)
30
31 #include "../../core/windows/SDL_windows.h"
32 #include "SDL_audio.h"
33 #include "SDL_timer.h"
34 #include "../SDL_audio_c.h"
35 #include "../SDL_sysaudio.h"
36
37 #define COBJMACROS
38 #include <mmdeviceapi.h>
39 #include <audioclient.h>
40
41 #include "SDL_wasapi.h"
42
43 static const ERole SDL_WASAPI_role = eConsole; /* !!! FIXME: should this be eMultimedia? Should be a hint? */
44
45 /* This is global to the WASAPI target, to handle hotplug and default device lookup. */
46 static IMMDeviceEnumerator *enumerator = NULL;
47
48 /* PropVariantInit() is an inline function/macro in PropIdl.h that calls the C runtime's memset() directly. Use ours instead, to avoid dependency. */
49 #ifdef PropVariantInit
50 #undef PropVariantInit
51 #endif
52 #define PropVariantInit(p) SDL_zerop(p)
53
54 /* handle to Avrt.dll--Vista and later!--for flagging the callback thread as "Pro Audio" (low latency). */
55 static HMODULE libavrt = NULL;
56 typedef HANDLE(WINAPI *pfnAvSetMmThreadCharacteristicsW)(LPCWSTR, LPDWORD);
57 typedef BOOL(WINAPI *pfnAvRevertMmThreadCharacteristics)(HANDLE);
58 static pfnAvSetMmThreadCharacteristicsW pAvSetMmThreadCharacteristicsW = NULL;
59 static pfnAvRevertMmThreadCharacteristics pAvRevertMmThreadCharacteristics = NULL;
60
61 /* Some GUIDs we need to know without linking to libraries that aren't available before Vista. */
62 static const CLSID SDL_CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c,{ 0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e } };
63 static const IID SDL_IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35,{ 0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6 } };
64 static const IID SDL_IID_IMMNotificationClient = { 0x7991eec9, 0x7e89, 0x4d85,{ 0x83, 0x90, 0x6c, 0x70, 0x3c, 0xec, 0x60, 0xc0 } };
65 static const IID SDL_IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089,{ 0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5 } };
66 static const IID SDL_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32,{ 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } };
67 static const PROPERTYKEY SDL_PKEY_Device_FriendlyName = { { 0xa45c254e, 0xdf1c, 0x4efd,{ 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, } }, 14 };
68 static const PROPERTYKEY SDL_PKEY_AudioEngine_DeviceFormat = { { 0xf19f064d, 0x82c, 0x4e27,{ 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, } }, 0 };
69
70
71 static void
GetWasapiDeviceInfo(IMMDevice * device,char ** utf8dev,WAVEFORMATEXTENSIBLE * fmt)72 GetWasapiDeviceInfo(IMMDevice *device, char **utf8dev, WAVEFORMATEXTENSIBLE *fmt)
73 {
74 /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be
75 "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in
76 its own UIs, like Volume Control, etc. */
77 IPropertyStore *props = NULL;
78 *utf8dev = NULL;
79 SDL_zerop(fmt);
80 if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) {
81 PROPVARIANT var;
82 PropVariantInit(&var);
83 if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) {
84 *utf8dev = WIN_StringToUTF8W(var.pwszVal);
85 }
86 PropVariantClear(&var);
87 if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_AudioEngine_DeviceFormat, &var))) {
88 SDL_memcpy(fmt, var.blob.pBlobData, SDL_min(var.blob.cbSize, sizeof(WAVEFORMATEXTENSIBLE)));
89 }
90 PropVariantClear(&var);
91 IPropertyStore_Release(props);
92 }
93 }
94
95
96 /* We need a COM subclass of IMMNotificationClient for hotplug support, which is
97 easy in C++, but we have to tapdance more to make work in C.
98 Thanks to this page for coaching on how to make this work:
99 https://www.codeproject.com/Articles/13601/COM-in-plain-C */
100
101 typedef struct SDLMMNotificationClient
102 {
103 const IMMNotificationClientVtbl *lpVtbl;
104 SDL_atomic_t refcount;
105 } SDLMMNotificationClient;
106
107 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_QueryInterface(IMMNotificationClient * this,REFIID iid,void ** ppv)108 SDLMMNotificationClient_QueryInterface(IMMNotificationClient *this, REFIID iid, void **ppv)
109 {
110 if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient)))
111 {
112 *ppv = this;
113 this->lpVtbl->AddRef(this);
114 return S_OK;
115 }
116
117 *ppv = NULL;
118 return E_NOINTERFACE;
119 }
120
121 static ULONG STDMETHODCALLTYPE
SDLMMNotificationClient_AddRef(IMMNotificationClient * ithis)122 SDLMMNotificationClient_AddRef(IMMNotificationClient *ithis)
123 {
124 SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
125 return (ULONG) (SDL_AtomicIncRef(&this->refcount) + 1);
126 }
127
128 static ULONG STDMETHODCALLTYPE
SDLMMNotificationClient_Release(IMMNotificationClient * ithis)129 SDLMMNotificationClient_Release(IMMNotificationClient *ithis)
130 {
131 /* this is a static object; we don't ever free it. */
132 SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
133 const ULONG retval = SDL_AtomicDecRef(&this->refcount);
134 if (retval == 0) {
135 SDL_AtomicSet(&this->refcount, 0); /* uhh... */
136 return 0;
137 }
138 return retval - 1;
139 }
140
141 /* These are the entry points called when WASAPI device endpoints change. */
142 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient * ithis,EDataFlow flow,ERole role,LPCWSTR pwstrDeviceId)143 SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
144 {
145 if (role != SDL_WASAPI_role) {
146 return S_OK; /* ignore it. */
147 }
148
149 /* Increment the "generation," so opened devices will pick this up in their threads. */
150 switch (flow) {
151 case eRender:
152 SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1);
153 break;
154
155 case eCapture:
156 SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1);
157 break;
158
159 case eAll:
160 SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1);
161 SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1);
162 break;
163
164 default:
165 SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!");
166 break;
167 }
168
169 return S_OK;
170 }
171
172 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient * ithis,LPCWSTR pwstrDeviceId)173 SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId)
174 {
175 /* we ignore this; devices added here then progress to ACTIVE, if appropriate, in
176 OnDeviceStateChange, making that a better place to deal with device adds. More
177 importantly: the first time you plug in a USB audio device, this callback will
178 fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).
179 Plugging it back in won't fire this callback again. */
180 return S_OK;
181 }
182
183 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient * ithis,LPCWSTR pwstrDeviceId)184 SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId)
185 {
186 /* See notes in OnDeviceAdded handler about why we ignore this. */
187 return S_OK;
188 }
189
190 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient * ithis,LPCWSTR pwstrDeviceId,DWORD dwNewState)191 SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId, DWORD dwNewState)
192 {
193 IMMDevice *device = NULL;
194
195 if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {
196 IMMEndpoint *endpoint = NULL;
197 if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **) &endpoint))) {
198 EDataFlow flow;
199 if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {
200 const SDL_bool iscapture = (flow == eCapture);
201 if (dwNewState == DEVICE_STATE_ACTIVE) {
202 char *utf8dev;
203 WAVEFORMATEXTENSIBLE fmt;
204 GetWasapiDeviceInfo(device, &utf8dev, &fmt);
205 if (utf8dev) {
206 WASAPI_AddDevice(iscapture, utf8dev, &fmt, pwstrDeviceId);
207 SDL_free(utf8dev);
208 }
209 } else {
210 WASAPI_RemoveDevice(iscapture, pwstrDeviceId);
211 }
212 }
213 IMMEndpoint_Release(endpoint);
214 }
215 IMMDevice_Release(device);
216 }
217
218 return S_OK;
219 }
220
221 static HRESULT STDMETHODCALLTYPE
SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient * this,LPCWSTR pwstrDeviceId,const PROPERTYKEY key)222 SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *this, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
223 {
224 return S_OK; /* we don't care about these. */
225 }
226
227 static const IMMNotificationClientVtbl notification_client_vtbl = {
228 SDLMMNotificationClient_QueryInterface,
229 SDLMMNotificationClient_AddRef,
230 SDLMMNotificationClient_Release,
231 SDLMMNotificationClient_OnDeviceStateChanged,
232 SDLMMNotificationClient_OnDeviceAdded,
233 SDLMMNotificationClient_OnDeviceRemoved,
234 SDLMMNotificationClient_OnDefaultDeviceChanged,
235 SDLMMNotificationClient_OnPropertyValueChanged
236 };
237
238 static SDLMMNotificationClient notification_client = { ¬ification_client_vtbl, { 1 } };
239
240
241 int
WASAPI_PlatformInit(void)242 WASAPI_PlatformInit(void)
243 {
244 HRESULT ret;
245
246 /* just skip the discussion with COM here. */
247 if (!WIN_IsWindowsVistaOrGreater()) {
248 return SDL_SetError("WASAPI support requires Windows Vista or later");
249 }
250
251 if (FAILED(WIN_CoInitialize())) {
252 return SDL_SetError("WASAPI: CoInitialize() failed");
253 }
254
255 ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *) &enumerator);
256 if (FAILED(ret)) {
257 WIN_CoUninitialize();
258 return WIN_SetErrorFromHRESULT("WASAPI CoCreateInstance(MMDeviceEnumerator)", ret);
259 }
260
261 libavrt = LoadLibrary(TEXT("avrt.dll")); /* this library is available in Vista and later. No WinXP, so have to LoadLibrary to use it for now! */
262 if (libavrt) {
263 pAvSetMmThreadCharacteristicsW = (pfnAvSetMmThreadCharacteristicsW) GetProcAddress(libavrt, "AvSetMmThreadCharacteristicsW");
264 pAvRevertMmThreadCharacteristics = (pfnAvRevertMmThreadCharacteristics) GetProcAddress(libavrt, "AvRevertMmThreadCharacteristics");
265 }
266
267 return 0;
268 }
269
270 void
WASAPI_PlatformDeinit(void)271 WASAPI_PlatformDeinit(void)
272 {
273 if (enumerator) {
274 IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) ¬ification_client);
275 IMMDeviceEnumerator_Release(enumerator);
276 enumerator = NULL;
277 }
278
279 if (libavrt) {
280 FreeLibrary(libavrt);
281 libavrt = NULL;
282 }
283
284 pAvSetMmThreadCharacteristicsW = NULL;
285 pAvRevertMmThreadCharacteristics = NULL;
286
287 WIN_CoUninitialize();
288 }
289
290 void
WASAPI_PlatformThreadInit(_THIS)291 WASAPI_PlatformThreadInit(_THIS)
292 {
293 /* this thread uses COM. */
294 if (SUCCEEDED(WIN_CoInitialize())) { /* can't report errors, hope it worked! */
295 this->hidden->coinitialized = SDL_TRUE;
296 }
297
298 /* Set this thread to very high "Pro Audio" priority. */
299 if (pAvSetMmThreadCharacteristicsW) {
300 DWORD idx = 0;
301 this->hidden->task = pAvSetMmThreadCharacteristicsW(L"Pro Audio", &idx);
302 }
303 }
304
305 void
WASAPI_PlatformThreadDeinit(_THIS)306 WASAPI_PlatformThreadDeinit(_THIS)
307 {
308 /* Set this thread back to normal priority. */
309 if (this->hidden->task && pAvRevertMmThreadCharacteristics) {
310 pAvRevertMmThreadCharacteristics(this->hidden->task);
311 this->hidden->task = NULL;
312 }
313
314 if (this->hidden->coinitialized) {
315 WIN_CoUninitialize();
316 this->hidden->coinitialized = SDL_FALSE;
317 }
318 }
319
320 int
WASAPI_ActivateDevice(_THIS,const SDL_bool isrecovery)321 WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
322 {
323 LPCWSTR devid = this->hidden->devid;
324 IMMDevice *device = NULL;
325 HRESULT ret;
326
327 if (devid == NULL) {
328 const EDataFlow dataflow = this->iscapture ? eCapture : eRender;
329 ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
330 } else {
331 ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, &device);
332 }
333
334 if (FAILED(ret)) {
335 SDL_assert(device == NULL);
336 this->hidden->client = NULL;
337 return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
338 }
339
340 /* this is not async in standard win32, yay! */
341 ret = IMMDevice_Activate(device, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **) &this->hidden->client);
342 IMMDevice_Release(device);
343
344 if (FAILED(ret)) {
345 SDL_assert(this->hidden->client == NULL);
346 return WIN_SetErrorFromHRESULT("WASAPI can't activate audio endpoint", ret);
347 }
348
349 SDL_assert(this->hidden->client != NULL);
350 if (WASAPI_PrepDevice(this, isrecovery) == -1) { /* not async, fire it right away. */
351 return -1;
352 }
353
354 return 0; /* good to go. */
355 }
356
357
358 typedef struct
359 {
360 LPWSTR devid;
361 char *devname;
362 WAVEFORMATEXTENSIBLE fmt;
363 } EndpointItem;
364
sort_endpoints(const void * _a,const void * _b)365 static int sort_endpoints(const void *_a, const void *_b)
366 {
367 LPWSTR a = ((const EndpointItem *) _a)->devid;
368 LPWSTR b = ((const EndpointItem *) _b)->devid;
369 if (!a && b) {
370 return -1;
371 } else if (a && !b) {
372 return 1;
373 }
374
375 while (SDL_TRUE) {
376 if (*a < *b) {
377 return -1;
378 } else if (*a > *b) {
379 return 1;
380 } else if (*a == 0) {
381 break;
382 }
383 a++;
384 b++;
385 }
386
387 return 0;
388 }
389
390 static void
WASAPI_EnumerateEndpointsForFlow(const SDL_bool iscapture)391 WASAPI_EnumerateEndpointsForFlow(const SDL_bool iscapture)
392 {
393 IMMDeviceCollection *collection = NULL;
394 EndpointItem *items;
395 UINT i, total;
396
397 /* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
398 ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
399
400 if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, iscapture ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {
401 return;
402 }
403
404 if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {
405 IMMDeviceCollection_Release(collection);
406 return;
407 }
408
409 items = (EndpointItem *) SDL_calloc(total, sizeof (EndpointItem));
410 if (!items) {
411 return; /* oh well. */
412 }
413
414 for (i = 0; i < total; i++) {
415 EndpointItem *item = items + i;
416 IMMDevice *device = NULL;
417 if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &device))) {
418 if (SUCCEEDED(IMMDevice_GetId(device, &item->devid))) {
419 GetWasapiDeviceInfo(device, &item->devname, &item->fmt);
420 }
421 IMMDevice_Release(device);
422 }
423 }
424
425 /* sort the list of devices by their guid so list is consistent between runs */
426 SDL_qsort(items, total, sizeof (*items), sort_endpoints);
427
428 /* Send the sorted list on to the SDL's higher level. */
429 for (i = 0; i < total; i++) {
430 EndpointItem *item = items + i;
431 if ((item->devid) && (item->devname)) {
432 WASAPI_AddDevice(iscapture, item->devname, &item->fmt, item->devid);
433 }
434 SDL_free(item->devname);
435 CoTaskMemFree(item->devid);
436 }
437
438 SDL_free(items);
439 IMMDeviceCollection_Release(collection);
440 }
441
442 void
WASAPI_EnumerateEndpoints(void)443 WASAPI_EnumerateEndpoints(void)
444 {
445 WASAPI_EnumerateEndpointsForFlow(SDL_FALSE); /* playback */
446 WASAPI_EnumerateEndpointsForFlow(SDL_TRUE); /* capture */
447
448 /* if this fails, we just won't get hotplug events. Carry on anyhow. */
449 IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) ¬ification_client);
450 }
451
452 void
WASAPI_PlatformDeleteActivationHandler(void * handler)453 WASAPI_PlatformDeleteActivationHandler(void *handler)
454 {
455 /* not asynchronous. */
456 SDL_assert(!"This function should have only been called on WinRT.");
457 }
458
459 #endif /* SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__) */
460
461 /* vi: set ts=4 sw=4 expandtab: */
462
463