// Copyright Epic Games, Inc. All Rights Reserved. #include "WasapiDeviceEnumeration.h" #include "Misc/ScopeExit.h" #include "WasapiCaptureLog.h" #include "Windows/AllowWindowsPlatformTypes.h" #include "Windows/AllowWindowsPlatformAtomics.h" THIRD_PARTY_INCLUDES_START #include THIRD_PARTY_INCLUDES_END #include "Windows/HideWindowsPlatformAtomics.h" #include "Windows/HideWindowsPlatformTypes.h" namespace Audio { using FDeviceInfo = FWasapiDeviceEnumeration::FDeviceInfo; void FWasapiDeviceEnumeration::Initialize() { // Assumes FWindowsPlatformMisc::CoInitialize() has been called upstream ensure( SUCCEEDED( ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&DeviceEnumerator)) ) && (DeviceEnumerator != nullptr) ); EnumerateDefaults(); } void FWasapiDeviceEnumeration::EnumerateDefaults() { auto GetDefaultDeviceID = [this](EDataFlow InDataFlow, ERole InRole, FString& OutDeviceId) -> bool { // Mark default device. bool bSuccess = false; TComPtr DefaultDevice; if (SUCCEEDED(DeviceEnumerator->GetDefaultAudioEndpoint(InDataFlow, InRole, &DefaultDevice))) { LPWSTR IdString = nullptr; if (SUCCEEDED(DefaultDevice->GetId(&IdString)) && IdString) { OutDeviceId = FString(IdString); ::CoTaskMemFree(IdString); bSuccess = true; } } return bSuccess; }; // Get defaults (render, capture). FString DeviceIdName; if (GetDefaultDeviceID(eRender, eConsole, DeviceIdName)) { UE_LOG(LogAudioCaptureCore, Verbose, TEXT("FWasapiDeviceEnumeration: Default Device='%s'"), *DeviceIdName); DefaultRenderId = DeviceIdName; } else { UE_LOG(LogAudioCaptureCore, Verbose, TEXT("FWasapiDeviceEnumeration: no default render device found")); } if (GetDefaultDeviceID(eCapture, eConsole, DeviceIdName)) { UE_LOG(LogAudioCaptureCore, Verbose, TEXT("FWasapiDeviceEnumeration: Default Device='%s'"), *DeviceIdName); DefaultCaptureId = DeviceIdName; } else { UE_LOG(LogAudioCaptureCore, Verbose, TEXT("FWasapiDeviceEnumeration: no default capture device found")); } } FString FWasapiDeviceEnumeration::GetDefaultInputDeviceId() { return DefaultCaptureId; } FString FWasapiDeviceEnumeration::GetDefaultOutputDeviceId() { return DefaultRenderId; } bool FWasapiDeviceEnumeration::GetDeviceInfo(const FString& InDeviceId, FDeviceInfo& OutDeviceInfo) { TComPtr Device; if (GetIMMDevice(InDeviceId, Device)) { FDeviceInfo DeviceInfo; if (GetDeviceProperties(Device, DeviceInfo)) { OutDeviceInfo = MoveTemp(DeviceInfo); return true; } } return false; } bool FWasapiDeviceEnumeration::GetDeviceIdFromIndex(int32 InDeviceIndex, EDataFlow InDataFlow, FString& OutDeviceId) { TComPtr Collection; HRESULT Result = DeviceEnumerator->EnumAudioEndpoints(InDataFlow, DEVICE_STATE_ACTIVE, &Collection); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::EnumAudioEndpoints", Result); return false; } TComPtr Device; Result = Collection->Item(InDeviceIndex, &Device); if (SUCCEEDED(Result)) { LPWSTR IdString = nullptr; if (SUCCEEDED(Device->GetId(&IdString)) && IdString) { OutDeviceId = FString(IdString); ::CoTaskMemFree(IdString); return true; } } return false; } bool FWasapiDeviceEnumeration::GetDeviceIndexFromId(const FString& InDeviceId, int32& OutDeviceIndex) { OutDeviceIndex = INDEX_NONE; TComPtr Collection; // For compatibility with the RtAudio implementation, we get all device types here HRESULT Result = DeviceEnumerator->EnumAudioEndpoints(eAll, DEVICE_STATE_ACTIVE, &Collection); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::EnumAudioEndpoints", Result); return false; } uint32 Count; Result = Collection->GetCount(&Count); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::GetCount", Result); return false; } for (uint32 DeviceIndex = 0; DeviceIndex < Count; ++DeviceIndex) { TComPtr Device; Result = Collection->Item(DeviceIndex, &Device); if (SUCCEEDED(Result)) { FString DeviceId; if (GetDeviceId(Device, DeviceId)) { if (DeviceId == InDeviceId) { OutDeviceIndex = DeviceIndex; return true; } } } } return false; } bool FWasapiDeviceEnumeration::GetIMMDevice(const FString& InDeviceId, TComPtr& OutDevice) { TComPtr Device; HRESULT Result = DeviceEnumerator->GetDevice(TCHAR_TO_WCHAR(*InDeviceId), &Device); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceEnumerator::GetDevice", Result); return false; } OutDevice = Device; return true; } bool FWasapiDeviceEnumeration::GetInputDevicesAvailable(TArray& OutDevices) { TComPtr Collection; HRESULT Result = DeviceEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &Collection); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::EnumAudioEndpoints", Result); return false; } uint32 Count; Result = Collection->GetCount(&Count); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::GetCount", Result); return false; } for (uint32 DeviceIndex = 0; DeviceIndex < Count; ++DeviceIndex) { TComPtr Device; Result = Collection->Item(DeviceIndex, &Device); if (SUCCEEDED(Result)) { FDeviceInfo DeviceInfo; if (GetDeviceProperties(Device, DeviceInfo)) { UE_LOG(LogAudioCaptureCore, Verbose, TEXT("Device %d: \"%s\" (%s)"), DeviceIndex, DeviceInfo.FriendlyName.GetCharArray().GetData(), DeviceInfo.DeviceId.GetCharArray().GetData()); UE_LOG(LogAudioCaptureCore, Verbose, TEXT("\tBPS: %d, SR: %d"), DeviceInfo.BitsPerSample, DeviceInfo.PreferredSampleRate); OutDevices.Emplace(MoveTemp(DeviceInfo)); } else { UE_LOG(LogAudioCaptureCore, Warning, TEXT("Unable to fetch audio capture device properties...skipping")); } } else { UE_LOG(LogAudioCaptureCore, Warning, TEXT("Unable to fetch audio capture device...skipping")); } } return true; } bool FWasapiDeviceEnumeration::GetDeviceProperties(TComPtr InDevice, FDeviceInfo& OutInfo) { FString IdString; FString FriendlyName; if (GetDeviceId(InDevice, IdString) && GetDeviceFriendlyName(InDevice, FriendlyName)) { TComPtr AudioClient; HRESULT Result = InDevice->Activate(__uuidof(IAudioClient3), CLSCTX_INPROC_SERVER, nullptr, IID_PPV_ARGS_Helper(&AudioClient)); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IAudioClient3::Activate", Result); return false; } WAVEFORMATEX* DeviceFormat = nullptr; if (GetAudioClientMixFormat(AudioClient, &DeviceFormat)) { ON_SCOPE_EXIT{ ::CoTaskMemFree(DeviceFormat); }; OutInfo.DeviceId = IdString; OutInfo.FriendlyName = FriendlyName; OutInfo.BitsPerSample = DeviceFormat->wBitsPerSample; OutInfo.PreferredSampleRate = DeviceFormat->nSamplesPerSec; OutInfo.EndpointType = GetEndpointType(InDevice); if (OutInfo.EndpointType == FDeviceInfo::EEndpointType::Capture) { OutInfo.NumInputChannels = DeviceFormat->nChannels; } else if (OutInfo.EndpointType == FDeviceInfo::EEndpointType::Render) { OutInfo.NumOutputChannels = DeviceFormat->nChannels; } return true; } } return false; } bool FWasapiDeviceEnumeration::GetDeviceId(TComPtr InDevice, FString& OutString) { LPWSTR IdString = nullptr; HRESULT Result = InDevice->GetId(&IdString); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDevice::GetId", Result); return false; } OutString = FString(IdString); ::CoTaskMemFree(IdString); return true; } bool FWasapiDeviceEnumeration::GetDeviceFriendlyName(TComPtr InDevice, FString& OutString) { TComPtr Props; HRESULT Result = InDevice->OpenPropertyStore(STGM_READ, &Props); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IMMDevice::OpenPropertyStore", Result); return false; } PROPVARIANT VarName; // Initialize container for property value. ::PropVariantInit(&VarName); // Get the endpoint's friendly-name property. Result = Props->GetValue(PKEY_Device_FriendlyName, &VarName); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IPropertyStore::GetValue", Result); return false; } OutString = FString(VarName.pwszVal); ::PropVariantClear(&VarName); return true; } FDeviceInfo::EEndpointType FWasapiDeviceEnumeration::GetEndpointType(TComPtr InDevice) { TComPtr Endpoint; HRESULT Result = InDevice->QueryInterface(IID_PPV_ARGS(&Endpoint)); if (SUCCEEDED(Result)) { EDataFlow DataFlow = eRender; Result = Endpoint->GetDataFlow(&DataFlow); if (SUCCEEDED(Result)) { switch (DataFlow) { case eRender: return FDeviceInfo::EEndpointType::Render; case eCapture: return FDeviceInfo::EEndpointType::Capture; default: break; } } else { WASAPI_CAPTURE_LOG_RESULT("IMMEndpoint::GetDataFlow", Result); } } else { WASAPI_CAPTURE_LOG_RESULT("IMMDevice::QueryInterface", Result); } return FDeviceInfo::EEndpointType::Unknown; } bool FWasapiDeviceEnumeration::GetAudioClientMixFormat(TComPtr AudioClient, WAVEFORMATEX** OutFormat) { HRESULT Result = AudioClient->GetMixFormat(OutFormat); if (FAILED(Result)) { WASAPI_CAPTURE_LOG_RESULT("IAudioClient3::GetMixFormat", Result); return false; } return true; } }