Files
UnrealEngine/Engine/Source/Runtime/AudioCaptureImplementations/Windows/AudioCaputureWasapi/Private/WasapiDeviceEnumeration.cpp
2025-05-18 13:04:45 +08:00

363 lines
9.5 KiB
C++

// 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 <Functiondiscoverykeys_devpkey.h>
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<IMMDevice> 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<IMMDevice> 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<IMMDeviceCollection> Collection;
HRESULT Result = DeviceEnumerator->EnumAudioEndpoints(InDataFlow, DEVICE_STATE_ACTIVE, &Collection);
if (FAILED(Result))
{
WASAPI_CAPTURE_LOG_RESULT("IMMDeviceCollection::EnumAudioEndpoints", Result);
return false;
}
TComPtr<IMMDevice> 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<IMMDeviceCollection> 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<IMMDevice> 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<IMMDevice>& OutDevice)
{
TComPtr<IMMDevice> 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<FDeviceInfo>& OutDevices)
{
TComPtr<IMMDeviceCollection> 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<IMMDevice> 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<IMMDevice> InDevice, FDeviceInfo& OutInfo)
{
FString IdString;
FString FriendlyName;
if (GetDeviceId(InDevice, IdString) && GetDeviceFriendlyName(InDevice, FriendlyName))
{
TComPtr<IAudioClient3> 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<IMMDevice> 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<IMMDevice> InDevice, FString& OutString)
{
TComPtr<IPropertyStore> 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<IMMDevice> InDevice)
{
TComPtr<IMMEndpoint> 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<IAudioClient3> AudioClient, WAVEFORMATEX** OutFormat)
{
HRESULT Result = AudioClient->GetMixFormat(OutFormat);
if (FAILED(Result))
{
WASAPI_CAPTURE_LOG_RESULT("IAudioClient3::GetMixFormat", Result);
return false;
}
return true;
}
}