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