SDL  2.0
SDL_wasapi_win32.c
Go to the documentation of this file.
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 #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)(LPWSTR, 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 
69 
70 static char *
71 GetWasapiDeviceName(IMMDevice *device)
72 {
73  /* PKEY_Device_FriendlyName gives you "Speakers (SoundBlaster Pro)" which drives me nuts. I'd rather it be
74  "SoundBlaster Pro (Speakers)" but I guess that's developers vs users. Windows uses the FriendlyName in
75  its own UIs, like Volume Control, etc. */
76  char *utf8dev = NULL;
77  IPropertyStore *props = NULL;
78  if (SUCCEEDED(IMMDevice_OpenPropertyStore(device, STGM_READ, &props))) {
79  PROPVARIANT var;
80  PropVariantInit(&var);
81  if (SUCCEEDED(IPropertyStore_GetValue(props, &SDL_PKEY_Device_FriendlyName, &var))) {
82  utf8dev = WIN_StringToUTF8(var.pwszVal);
83  }
84  PropVariantClear(&var);
85  IPropertyStore_Release(props);
86  }
87  return utf8dev;
88 }
89 
90 
91 /* We need a COM subclass of IMMNotificationClient for hotplug support, which is
92  easy in C++, but we have to tapdance more to make work in C.
93  Thanks to this page for coaching on how to make this work:
94  https://www.codeproject.com/Articles/13601/COM-in-plain-C */
95 
96 typedef struct SDLMMNotificationClient
97 {
98  const IMMNotificationClientVtbl *lpVtbl;
99  SDL_atomic_t refcount;
100 } SDLMMNotificationClient;
101 
102 static HRESULT STDMETHODCALLTYPE
103 SDLMMNotificationClient_QueryInterface(IMMNotificationClient *this, REFIID iid, void **ppv)
104 {
105  if ((WIN_IsEqualIID(iid, &IID_IUnknown)) || (WIN_IsEqualIID(iid, &SDL_IID_IMMNotificationClient)))
106  {
107  *ppv = this;
108  this->lpVtbl->AddRef(this);
109  return S_OK;
110  }
111 
112  *ppv = NULL;
113  return E_NOINTERFACE;
114 }
115 
116 static ULONG STDMETHODCALLTYPE
117 SDLMMNotificationClient_AddRef(IMMNotificationClient *ithis)
118 {
119  SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
120  return (ULONG) (SDL_AtomicIncRef(&this->refcount) + 1);
121 }
122 
123 static ULONG STDMETHODCALLTYPE
124 SDLMMNotificationClient_Release(IMMNotificationClient *ithis)
125 {
126  /* this is a static object; we don't ever free it. */
127  SDLMMNotificationClient *this = (SDLMMNotificationClient *) ithis;
128  const ULONG retval = SDL_AtomicDecRef(&this->refcount);
129  if (retval == 0) {
130  SDL_AtomicSet(&this->refcount, 0); /* uhh... */
131  return 0;
132  }
133  return retval - 1;
134 }
135 
136 /* These are the entry points called when WASAPI device endpoints change. */
137 static HRESULT STDMETHODCALLTYPE
138 SDLMMNotificationClient_OnDefaultDeviceChanged(IMMNotificationClient *ithis, EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
139 {
140  if (role != SDL_WASAPI_role) {
141  return S_OK; /* ignore it. */
142  }
143 
144  /* Increment the "generation," so opened devices will pick this up in their threads. */
145  switch (flow) {
146  case eRender:
148  break;
149 
150  case eCapture:
152  break;
153 
154  case eAll:
157  break;
158 
159  default:
160  SDL_assert(!"uhoh, unexpected OnDefaultDeviceChange flow!");
161  break;
162  }
163 
164  return S_OK;
165 }
166 
167 static HRESULT STDMETHODCALLTYPE
168 SDLMMNotificationClient_OnDeviceAdded(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId)
169 {
170  /* we ignore this; devices added here then progress to ACTIVE, if appropriate, in
171  OnDeviceStateChange, making that a better place to deal with device adds. More
172  importantly: the first time you plug in a USB audio device, this callback will
173  fire, but when you unplug it, it isn't removed (it's state changes to NOTPRESENT).
174  Plugging it back in won't fire this callback again. */
175  return S_OK;
176 }
177 
178 static HRESULT STDMETHODCALLTYPE
179 SDLMMNotificationClient_OnDeviceRemoved(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId)
180 {
181  /* See notes in OnDeviceAdded handler about why we ignore this. */
182  return S_OK;
183 }
184 
185 static HRESULT STDMETHODCALLTYPE
186 SDLMMNotificationClient_OnDeviceStateChanged(IMMNotificationClient *ithis, LPCWSTR pwstrDeviceId, DWORD dwNewState)
187 {
188  IMMDevice *device = NULL;
189 
190  if (SUCCEEDED(IMMDeviceEnumerator_GetDevice(enumerator, pwstrDeviceId, &device))) {
191  IMMEndpoint *endpoint = NULL;
192  if (SUCCEEDED(IMMDevice_QueryInterface(device, &SDL_IID_IMMEndpoint, (void **) &endpoint))) {
193  EDataFlow flow;
194  if (SUCCEEDED(IMMEndpoint_GetDataFlow(endpoint, &flow))) {
195  const SDL_bool iscapture = (flow == eCapture);
196  if (dwNewState == DEVICE_STATE_ACTIVE) {
197  char *utf8dev = GetWasapiDeviceName(device);
198  if (utf8dev) {
199  WASAPI_AddDevice(iscapture, utf8dev, pwstrDeviceId);
200  SDL_free(utf8dev);
201  }
202  } else {
203  WASAPI_RemoveDevice(iscapture, pwstrDeviceId);
204  }
205  }
206  IMMEndpoint_Release(endpoint);
207  }
208  IMMDevice_Release(device);
209  }
210 
211  return S_OK;
212 }
213 
214 static HRESULT STDMETHODCALLTYPE
215 SDLMMNotificationClient_OnPropertyValueChanged(IMMNotificationClient *this, LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
216 {
217  return S_OK; /* we don't care about these. */
218 }
219 
220 static const IMMNotificationClientVtbl notification_client_vtbl = {
221  SDLMMNotificationClient_QueryInterface,
222  SDLMMNotificationClient_AddRef,
223  SDLMMNotificationClient_Release,
224  SDLMMNotificationClient_OnDeviceStateChanged,
225  SDLMMNotificationClient_OnDeviceAdded,
226  SDLMMNotificationClient_OnDeviceRemoved,
227  SDLMMNotificationClient_OnDefaultDeviceChanged,
228  SDLMMNotificationClient_OnPropertyValueChanged
229 };
230 
231 static SDLMMNotificationClient notification_client = { &notification_client_vtbl, { 1 } };
232 
233 
234 int
236 {
237  HRESULT ret;
238 
239  /* just skip the discussion with COM here. */
241  return SDL_SetError("WASAPI support requires Windows Vista or later");
242  }
243 
244  if (FAILED(WIN_CoInitialize())) {
245  return SDL_SetError("WASAPI: CoInitialize() failed");
246  }
247 
248  ret = CoCreateInstance(&SDL_CLSID_MMDeviceEnumerator, NULL, CLSCTX_INPROC_SERVER, &SDL_IID_IMMDeviceEnumerator, (LPVOID *) &enumerator);
249  if (FAILED(ret)) {
251  return WIN_SetErrorFromHRESULT("WASAPI CoCreateInstance(MMDeviceEnumerator)", ret);
252  }
253 
254  libavrt = LoadLibraryW(L"avrt.dll"); /* this library is available in Vista and later. No WinXP, so have to LoadLibrary to use it for now! */
255  if (libavrt) {
256  pAvSetMmThreadCharacteristicsW = (pfnAvSetMmThreadCharacteristicsW) GetProcAddress(libavrt, "AvSetMmThreadCharacteristicsW");
257  pAvRevertMmThreadCharacteristics = (pfnAvRevertMmThreadCharacteristics) GetProcAddress(libavrt, "AvRevertMmThreadCharacteristics");
258  }
259 
260  return 0;
261 }
262 
263 void
265 {
266  if (enumerator) {
267  IMMDeviceEnumerator_UnregisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
268  IMMDeviceEnumerator_Release(enumerator);
269  enumerator = NULL;
270  }
271 
272  if (libavrt) {
273  FreeLibrary(libavrt);
274  libavrt = NULL;
275  }
276 
277  pAvSetMmThreadCharacteristicsW = NULL;
278  pAvRevertMmThreadCharacteristics = NULL;
279 
281 }
282 
283 void
285 {
286  /* this thread uses COM. */
287  if (SUCCEEDED(WIN_CoInitialize())) { /* can't report errors, hope it worked! */
288  this->hidden->coinitialized = SDL_TRUE;
289  }
290 
291  /* Set this thread to very high "Pro Audio" priority. */
292  if (pAvSetMmThreadCharacteristicsW) {
293  DWORD idx = 0;
294  this->hidden->task = pAvSetMmThreadCharacteristicsW(TEXT("Pro Audio"), &idx);
295  }
296 }
297 
298 void
300 {
301  /* Set this thread back to normal priority. */
302  if (this->hidden->task && pAvRevertMmThreadCharacteristics) {
303  pAvRevertMmThreadCharacteristics(this->hidden->task);
304  this->hidden->task = NULL;
305  }
306 
307  if (this->hidden->coinitialized) {
309  this->hidden->coinitialized = SDL_FALSE;
310  }
311 }
312 
313 int
314 WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
315 {
316  LPCWSTR devid = this->hidden->devid;
317  IMMDevice *device = NULL;
318  HRESULT ret;
319 
320  if (devid == NULL) {
321  const EDataFlow dataflow = this->iscapture ? eCapture : eRender;
322  ret = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, dataflow, SDL_WASAPI_role, &device);
323  } else {
324  ret = IMMDeviceEnumerator_GetDevice(enumerator, devid, &device);
325  }
326 
327  if (FAILED(ret)) {
328  SDL_assert(device == NULL);
329  this->hidden->client = NULL;
330  return WIN_SetErrorFromHRESULT("WASAPI can't find requested audio endpoint", ret);
331  }
332 
333  /* this is not async in standard win32, yay! */
334  ret = IMMDevice_Activate(device, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **) &this->hidden->client);
335  IMMDevice_Release(device);
336 
337  if (FAILED(ret)) {
338  SDL_assert(this->hidden->client == NULL);
339  return WIN_SetErrorFromHRESULT("WASAPI can't activate audio endpoint", ret);
340  }
341 
342  SDL_assert(this->hidden->client != NULL);
343  if (WASAPI_PrepDevice(this, isrecovery) == -1) { /* not async, fire it right away. */
344  return -1;
345  }
346 
347  return 0; /* good to go. */
348 }
349 
350 
351 typedef struct
352 {
353  LPWSTR devid;
354  char *devname;
355 } EndpointItem;
356 
357 static int sort_endpoints(const void *_a, const void *_b)
358 {
359  LPWSTR a = ((const EndpointItem *) _a)->devid;
360  LPWSTR b = ((const EndpointItem *) _b)->devid;
361  if (!a && b) {
362  return -1;
363  } else if (a && !b) {
364  return 1;
365  }
366 
367  while (SDL_TRUE) {
368  if (*a < *b) {
369  return -1;
370  } else if (*a > *b) {
371  return 1;
372  } else if (*a == 0) {
373  break;
374  }
375  a++;
376  b++;
377  }
378 
379  return 0;
380 }
381 
382 static void
383 WASAPI_EnumerateEndpointsForFlow(const SDL_bool iscapture)
384 {
385  IMMDeviceCollection *collection = NULL;
386  EndpointItem *items;
387  UINT i, total;
388 
389  /* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
390  ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
391 
392  if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, iscapture ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {
393  return;
394  }
395 
396  if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {
397  IMMDeviceCollection_Release(collection);
398  return;
399  }
400 
401  items = (EndpointItem *) SDL_calloc(total, sizeof (EndpointItem));
402  if (!items) {
403  return; /* oh well. */
404  }
405 
406  for (i = 0; i < total; i++) {
407  EndpointItem *item = items + i;
408  IMMDevice *device = NULL;
409  if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &device))) {
410  if (SUCCEEDED(IMMDevice_GetId(device, &item->devid))) {
411  item->devname = GetWasapiDeviceName(device);
412  }
413  IMMDevice_Release(device);
414  }
415  }
416 
417  /* sort the list of devices by their guid so list is consistent between runs */
418  SDL_qsort(items, total, sizeof (*items), sort_endpoints);
419 
420  /* Send the sorted list on to the SDL's higher level. */
421  for (i = 0; i < total; i++) {
422  EndpointItem *item = items + i;
423  if ((item->devid) && (item->devname)) {
424  WASAPI_AddDevice(iscapture, item->devname, item->devid);
425  }
426  SDL_free(item->devname);
427  CoTaskMemFree(item->devid);
428  }
429 
430  SDL_free(items);
431  IMMDeviceCollection_Release(collection);
432 }
433 
434 void
436 {
437  WASAPI_EnumerateEndpointsForFlow(SDL_FALSE); /* playback */
438  WASAPI_EnumerateEndpointsForFlow(SDL_TRUE); /* capture */
439 
440  /* if this fails, we just won't get hotplug events. Carry on anyhow. */
441  IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
442 }
443 
444 void
446 {
447  /* not asynchronous. */
448  SDL_assert(!"This function should have only been called on WinRT.");
449 }
450 
451 #endif /* SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__) */
452 
453 /* vi: set ts=4 sw=4 expandtab: */
454 
#define _THIS
#define SDL_assert(condition)
Definition: SDL_assert.h:171
#define SDL_AtomicDecRef(a)
Decrement an atomic variable used as a reference count.
Definition: SDL_atomic.h:262
#define SDL_AtomicIncRef(a)
Increment an atomic variable used as a reference count.
Definition: SDL_atomic.h:252
#define SUCCEEDED(x)
Definition: SDL_directx.h:51
#define S_OK
Definition: SDL_directx.h:47
#define E_NOINTERFACE
Definition: SDL_directx.h:61
#define FAILED(x)
Definition: SDL_directx.h:54
#define SDL_AtomicSet
#define SDL_SetError
#define SDL_qsort
#define SDL_free
#define SDL_AtomicAdd
#define SDL_calloc
GLboolean GLboolean GLboolean b
GLenum GLuint GLsizei const GLenum * props
GLboolean GLboolean GLboolean GLboolean a
SDL_bool
Definition: SDL_stdinc.h:168
@ SDL_TRUE
Definition: SDL_stdinc.h:170
@ SDL_FALSE
Definition: SDL_stdinc.h:169
void WASAPI_PlatformDeleteActivationHandler(void *handler)
void WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid)
SDL_atomic_t WASAPI_DefaultPlaybackGeneration
void WASAPI_PlatformThreadDeinit(_THIS)
int WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
SDL_atomic_t WASAPI_DefaultCaptureGeneration
void WASAPI_PlatformThreadInit(_THIS)
int WASAPI_PlatformInit(void)
int WASAPI_PrepDevice(_THIS, const SDL_bool updatestream)
void WASAPI_EnumerateEndpoints(void)
void WASAPI_PlatformDeinit(void)
void WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, LPCWSTR devid)
HRESULT WIN_CoInitialize(void)
void WIN_CoUninitialize(void)
BOOL WIN_IsWindowsVistaOrGreater(void)
BOOL WIN_IsEqualIID(REFIID a, REFIID b)
#define WIN_StringToUTF8(S)
Definition: SDL_windows.h:46
int WIN_SetErrorFromHRESULT(const char *prefix, HRESULT hr)
return Display return Display Bool Bool int int int return Display XEvent Bool(*) XPointer return Display return Display Drawable _Xconst char unsigned int unsigned int return Display Pixmap Pixmap XColor XColor unsigned int unsigned int return Display _Xconst char char int char return Display Visual unsigned int int int char unsigned int unsigned int in i)
Definition: SDL_x11sym.h:50
#define NULL
Definition: begin_code.h:163
GLuint64 key
Definition: gl2ext.h:2192
static SDL_AudioDeviceID device
Definition: loopwave.c:37
set set set set set set set macro pixldst1 abits if abits op else op endif endm macro pixldst2 abits if abits op else op endif endm macro pixldst4 abits if abits op else op endif endm macro pixldst0 idx
A type representing an atomic integer value. It is a struct so people don't accidentally use numeric ...
Definition: SDL_atomic.h:216
SDL_bool retval