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 = { &notification_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 *) &notification_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 *) &notification_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