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