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 
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:
151  break;
152 
153  case eCapture:
155  break;
156 
157  case eAll:
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
239 {
240  HRESULT ret;
241 
242  /* just skip the discussion with COM here. */
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)) {
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
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 
284 }
285 
286 void
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
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) {
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 typedef struct
355 {
356  LPWSTR devid;
357  char *devname;
358 } EndpointItem;
359 
360 static int sort_endpoints(const void *_a, const void *_b)
361 {
362  LPWSTR a = ((const EndpointItem *) _a)->devid;
363  LPWSTR b = ((const EndpointItem *) _b)->devid;
364  if (!a && b) {
365  return -1;
366  } else if (a && !b) {
367  return 1;
368  }
369 
370  while (SDL_TRUE) {
371  if (*a < *b) {
372  return -1;
373  } else if (*a > *b) {
374  return 1;
375  } else if (*a == 0) {
376  break;
377  }
378  a++;
379  b++;
380  }
381 
382  return 0;
383 }
384 
385 static void
386 WASAPI_EnumerateEndpointsForFlow(const SDL_bool iscapture)
387 {
388  IMMDeviceCollection *collection = NULL;
389  EndpointItem *items;
390  UINT i, total;
391 
392  /* Note that WASAPI separates "adapter devices" from "audio endpoint devices"
393  ...one adapter device ("SoundBlaster Pro") might have multiple endpoint devices ("Speakers", "Line-Out"). */
394 
395  if (FAILED(IMMDeviceEnumerator_EnumAudioEndpoints(enumerator, iscapture ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection))) {
396  return;
397  }
398 
399  if (FAILED(IMMDeviceCollection_GetCount(collection, &total))) {
400  IMMDeviceCollection_Release(collection);
401  return;
402  }
403 
404  items = (EndpointItem *) SDL_calloc(total, sizeof (EndpointItem));
405  if (!items) {
406  return; /* oh well. */
407  }
408 
409  for (i = 0; i < total; i++) {
410  EndpointItem *item = items + i;
411  IMMDevice *device = NULL;
412  if (SUCCEEDED(IMMDeviceCollection_Item(collection, i, &device))) {
413  if (SUCCEEDED(IMMDevice_GetId(device, &item->devid))) {
414  item->devname = GetWasapiDeviceName(device);
415  }
416  IMMDevice_Release(device);
417  }
418  }
419 
420  /* sort the list of devices by their guid so list is consistent between runs */
421  SDL_qsort(items, total, sizeof (*items), sort_endpoints);
422 
423  /* Send the sorted list on to the SDL's higher level. */
424  for (i = 0; i < total; i++) {
425  EndpointItem *item = items + i;
426  if ((item->devid) && (item->devname)) {
427  WASAPI_AddDevice(iscapture, item->devname, item->devid);
428  }
429  SDL_free(item->devname);
430  CoTaskMemFree(item->devid);
431  }
432 
433  SDL_free(items);
434  IMMDeviceCollection_Release(collection);
435 }
436 
437 void
439 {
440  WASAPI_EnumerateEndpointsForFlow(SDL_FALSE); /* playback */
441  WASAPI_EnumerateEndpointsForFlow(SDL_TRUE); /* capture */
442 
443  /* if this fails, we just won't get hotplug events. Carry on anyhow. */
444  IMMDeviceEnumerator_RegisterEndpointNotificationCallback(enumerator, (IMMNotificationClient *) &notification_client);
445 }
446 
447 void
449 {
450  /* not asynchronous. */
451  SDL_assert(!"This function should have only been called on WinRT.");
452 }
453 
454 #endif /* SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__) */
455 
456 /* vi: set ts=4 sw=4 expandtab: */
457 
WIN_IsWindowsVistaOrGreater
BOOL WIN_IsWindowsVistaOrGreater(void)
WASAPI_AddDevice
void WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, LPCWSTR devid)
WASAPI_PlatformDeleteActivationHandler
void WASAPI_PlatformDeleteActivationHandler(void *handler)
WASAPI_PlatformInit
int WASAPI_PlatformInit(void)
NULL
#define NULL
Definition: begin_code.h:167
SDL_timer.h
b
GLboolean GLboolean GLboolean b
Definition: SDL_opengl_glext.h:1112
SDL_qsort
#define SDL_qsort
Definition: SDL_dynapi_overrides.h:380
E_NOINTERFACE
#define E_NOINTERFACE
Definition: SDL_directx.h:61
SDL_log.h
SDL_wasapi.h
SDL_AtomicIncRef
#define SDL_AtomicIncRef(a)
Increment an atomic variable used as a reference count.
Definition: SDL_atomic.h:252
a
GLboolean GLboolean GLboolean GLboolean a
Definition: SDL_opengl_glext.h:1112
WASAPI_PlatformThreadDeinit
void WASAPI_PlatformThreadDeinit(_THIS)
SDL_AtomicDecRef
#define SDL_AtomicDecRef(a)
Decrement an atomic variable used as a reference count.
Definition: SDL_atomic.h:262
WIN_IsEqualIID
BOOL WIN_IsEqualIID(REFIID a, REFIID b)
SDL_FALSE
@ SDL_FALSE
Definition: SDL_stdinc.h:163
SDL_audio.h
retval
SDL_bool retval
Definition: testgamecontroller.c:65
WIN_CoUninitialize
void WIN_CoUninitialize(void)
SDL_free
#define SDL_free
Definition: SDL_dynapi_overrides.h:377
SUCCEEDED
#define SUCCEEDED(x)
Definition: SDL_directx.h:51
SDL_assert.h
key
GLuint64 key
Definition: gl2ext.h:2192
props
GLenum GLuint GLsizei const GLenum * props
Definition: SDL_opengl_glext.h:2468
_THIS
#define _THIS
Definition: SDL_alsa_audio.h:31
WASAPI_RemoveDevice
void WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid)
WIN_SetErrorFromHRESULT
int WIN_SetErrorFromHRESULT(const char *prefix, HRESULT hr)
SDL_assert
#define SDL_assert(condition)
Definition: SDL_assert.h:169
WASAPI_PlatformDeinit
void WASAPI_PlatformDeinit(void)
WIN_CoInitialize
HRESULT WIN_CoInitialize(void)
WASAPI_DefaultPlaybackGeneration
SDL_atomic_t WASAPI_DefaultPlaybackGeneration
SDL_calloc
#define SDL_calloc
Definition: SDL_dynapi_overrides.h:375
SDL_TRUE
@ SDL_TRUE
Definition: SDL_stdinc.h:164
WASAPI_DefaultCaptureGeneration
SDL_atomic_t WASAPI_DefaultCaptureGeneration
SDL_AtomicAdd
#define SDL_AtomicAdd
Definition: SDL_dynapi_overrides.h:69
SDL_SetError
#define SDL_SetError
Definition: SDL_dynapi_overrides.h:30
S_OK
#define S_OK
Definition: SDL_directx.h:47
SDL_atomic_t
A type representing an atomic integer value. It is a struct so people don't accidentally use numeric ...
Definition: SDL_atomic.h:216
VULKAN_HPP_NAMESPACE::ShaderStageFlagBits::eAll
@ eAll
WIN_StringToUTF8
#define WIN_StringToUTF8(S)
Definition: SDL_windows.h:46
FAILED
#define FAILED(x)
Definition: SDL_directx.h:54
WASAPI_PlatformThreadInit
void WASAPI_PlatformThreadInit(_THIS)
WASAPI_ActivateDevice
int WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
WASAPI_EnumerateEndpoints
void WASAPI_EnumerateEndpoints(void)
SDL_AtomicSet
#define SDL_AtomicSet
Definition: SDL_dynapi_overrides.h:67
device
static SDL_AudioDeviceID device
Definition: loopwave.c:37
idx
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
Definition: pixman-arm-neon-asm.h:100
WASAPI_PrepDevice
int WASAPI_PrepDevice(_THIS, const SDL_bool updatestream)
i
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
SDL_bool
SDL_bool
Definition: SDL_stdinc.h:161