sdl

FORK: Simple Directmedia Layer
git clone https://git.neptards.moe/neptards/sdl.git
Log | Files | Refs

SDL_wasapi_win32.c (15703B)


      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:
    147             SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1);
    148             break;
    149 
    150         case eCapture:
    151             SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1);
    152             break;
    153 
    154         case eAll:
    155             SDL_AtomicAdd(&WASAPI_DefaultPlaybackGeneration, 1);
    156             SDL_AtomicAdd(&WASAPI_DefaultCaptureGeneration, 1);
    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
    235 WASAPI_PlatformInit(void)
    236 {
    237     HRESULT ret;
    238 
    239     /* just skip the discussion with COM here. */
    240     if (!WIN_IsWindowsVistaOrGreater()) {
    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)) {
    250         WIN_CoUninitialize();
    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
    264 WASAPI_PlatformDeinit(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 
    280     WIN_CoUninitialize();
    281 }
    282 
    283 void
    284 WASAPI_PlatformThreadInit(_THIS)
    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
    299 WASAPI_PlatformThreadDeinit(_THIS)
    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) {
    308         WIN_CoUninitialize();
    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
    435 WASAPI_EnumerateEndpoints(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
    445 WASAPI_PlatformDeleteActivationHandler(void *handler)
    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