SDL  2.0
SDL_wasapi_win32.c
Go to the documentation of this file.
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:
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 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
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
403 {
404  /* not asynchronous. */
405  SDL_assert(!"This function should have only been called on WinRT.");
406 }
407 
408 void
410 {
411  /* no-op. */
412 }
413 
414 #endif /* SDL_AUDIO_DRIVER_WASAPI && !defined(__WINRT__) */
415 
416 /* vi: set ts=4 sw=4 expandtab: */
417 
BOOL WIN_IsWindowsVistaOrGreater(void)
void WASAPI_AddDevice(const SDL_bool iscapture, const char *devname, LPCWSTR devid)
A type representing an atomic integer value. It is a struct so people don&#39;t accidentally use numeric ...
Definition: SDL_atomic.h:198
void WASAPI_BeginLoopIteration(_THIS)
int WIN_SetErrorFromHRESULT(const char *prefix, HRESULT hr)
int WASAPI_ActivateDevice(_THIS, const SDL_bool isrecovery)
void WASAPI_PlatformThreadDeinit(_THIS)
void WASAPI_PlatformDeinit(void)
#define E_NOINTERFACE
Definition: SDL_directx.h:61
int WASAPI_PrepDevice(_THIS, const SDL_bool updatestream)
static SDL_AudioDeviceID device
Definition: loopwave.c:37
SDL_bool retval
#define FAILED(x)
Definition: SDL_directx.h:54
GLuint64 key
Definition: gl2ext.h:2192
void WASAPI_EnumerateEndpoints(void)
HRESULT WIN_CoInitialize(void)
#define _THIS
#define SDL_free
#define WIN_StringToUTF8(S)
Definition: SDL_windows.h:46
void WASAPI_RemoveDevice(const SDL_bool iscapture, LPCWSTR devid)
#define S_OK
Definition: SDL_directx.h:47
#define SDL_AtomicIncRef(a)
Increment an atomic variable used as a reference count.
Definition: SDL_atomic.h:234
int WASAPI_PlatformInit(void)
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_atomic_t WASAPI_DefaultPlaybackGeneration
#define SDL_assert(condition)
Definition: SDL_assert.h:169
#define NULL
Definition: begin_code.h:164
#define SDL_AtomicAdd
SDL_bool
Definition: SDL_stdinc.h:139
void WIN_CoUninitialize(void)
#define SUCCEEDED(x)
Definition: SDL_directx.h:51
#define SDL_SetError
SDL_atomic_t WASAPI_DefaultCaptureGeneration
#define SDL_AtomicDecRef(a)
Decrement an atomic variable used as a reference count.
Definition: SDL_atomic.h:244
void WASAPI_PlatformDeleteActivationHandler(void *handler)
BOOL WIN_IsEqualIID(REFIID a, REFIID b)
#define SDL_AtomicSet
void WASAPI_PlatformThreadInit(_THIS)
GLenum GLuint GLsizei const GLenum * props