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