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