src/audio/wasapi/SDL_wasapi_win32.c
author Ryan C. Gordon <icculus@icculus.org>
Sun, 21 Oct 2018 22:40:17 -0400
changeset 12345 50e1cca28b39
parent 12222 09e314140a28
child 12503 806492103856
permissions -rw-r--r--
wasapi/win32: Sort initial device lists by device GUID.

This makes an unchanged set of hardware always report devices in the same
order on each run.
     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 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
   438 WASAPI_EnumerateEndpoints(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
   448 WASAPI_PlatformDeleteActivationHandler(void *handler)
   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