Files
UnrealEngine/Engine/Source/Runtime/AudioDeviceEnumeration/Windows/WindowsMMDeviceEnumeration/Private/WindowsMMNotificationClient.cpp
2025-05-18 13:04:45 +08:00

509 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WindowsMMNotificationClient.h"
#include "Windows/AllowWindowsPlatformTypes.h"
THIRD_PARTY_INCLUDES_START
#include <Functiondiscoverykeys_devpkey.h>
THIRD_PARTY_INCLUDES_END
#include "Windows/HideWindowsPlatformTypes.h"
#include "WindowsMMCvarUtils.h"
#include "WindowsMMStringUtils.h"
#include "ConversionHelpers.h"
#include "WindowsMMDeviceEnumerationLog.h"
namespace Audio
{
#if PLATFORM_WINDOWS
bool FWindowsMMNotificationClient::RegisterForSessionNotifications(const TComPtr<IMMDevice>& InDevice)
{
FScopeLock Lock(&SessionRegistrationCS);
// If we're already listening to this device, we can early out.
if (DeviceListeningToSessionEvents == InDevice)
{
return true;
}
UnregisterForSessionNotifications();
DeviceListeningToSessionEvents = InDevice;
if (InDevice)
{
if (SUCCEEDED(InDevice->Activate(__uuidof(IAudioSessionManager), CLSCTX_INPROC_SERVER, NULL, (void**)&SessionManager)))
{
if (SUCCEEDED(SessionManager->GetAudioSessionControl(NULL, 0, &SessionControls)))
{
if (SUCCEEDED(SessionControls->RegisterAudioSessionNotification(this)))
{
UE_LOG(LogAudioEnumeration, Verbose, TEXT("FWindowsMMNotificationClient: Registering for sessions events for '%s'"), *GetFriendlyName(DeviceListeningToSessionEvents.Get()));
return true;
}
}
}
}
return false;
}
bool FWindowsMMNotificationClient::RegisterForSessionNotifications(const FString& InDeviceId)
{
if (TComPtr<IMMDevice> Device = GetDevice(InDeviceId))
{
return RegisterForSessionNotifications(Device);
}
return false;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnSessionDisconnected(AudioSessionDisconnectReason InDisconnectReason)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Verbose, TEXT("Session Disconnect: Reason=%s, DeviceBound=%s, HasDisconnectSessionHappened=%d"),
ToString(InDisconnectReason), *GetFriendlyName(DeviceListeningToSessionEvents), (int32)bHasDisconnectSessionHappened);
if (!bHasDisconnectSessionHappened)
{
{
FReadScopeLock Lock(ListenersSetRwLock);
Audio::IAudioMixerDeviceChangedListener::EDisconnectReason Reason = AudioSessionDisconnectToEDisconnectReason(InDisconnectReason);
for (Audio::IAudioMixerDeviceChangedListener* i : Listeners)
{
i->OnSessionDisconnect(Reason);
}
}
// Mark this true.
bHasDisconnectSessionHappened = true;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnStateChanged(AudioSessionState NewState)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnGroupingParamChanged(LPCGUID NewGroupingParam, LPCGUID EventContext)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnChannelVolumeChanged(DWORD ChannelCount, float NewChannelVolumeArray[], DWORD ChangedChannel, LPCGUID EventContext)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnSimpleVolumeChanged(float NewVolume, BOOL NewMute, LPCGUID EventContext)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnIconPathChanged(LPCWSTR NewIconPath, LPCGUID EventContext)
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnDisplayNameChanged(LPCWSTR NewDisplayName, LPCGUID EventContext)
{
return S_OK;
}
#endif //PLATFORM_WINDOWS
void FWindowsMMNotificationClient::UnRegisterDeviceDeviceChangedListener(Audio::IAudioMixerDeviceChangedListener* DeviceChangedListener)
{
// Modifying container so get full write lock
FWriteScopeLock Lock(ListenersSetRwLock);
Listeners.Remove(DeviceChangedListener);
}
void FWindowsMMNotificationClient::RegisterDeviceChangedListener(Audio::IAudioMixerDeviceChangedListener* DeviceChangedListener)
{
// Modifying container so get full write lock
FWriteScopeLock Lock(ListenersSetRwLock);
Listeners.Add(DeviceChangedListener);
}
#include "Windows/AllowWindowsPlatformAtomics.h"
ULONG STDMETHODCALLTYPE FWindowsMMNotificationClient::AddRef()
{
return InterlockedIncrement(&Ref);
}
ULONG STDMETHODCALLTYPE FWindowsMMNotificationClient::Release()
{
ULONG ulRef = InterlockedDecrement(&Ref);
if (0 == ulRef)
{
delete this;
}
return ulRef;
}
#include "Windows/HideWindowsPlatformAtomics.h"
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::QueryInterface(const IID& IId, void** UnknownPtrPtr)
{
// Three rules of QueryInterface: https://docs.microsoft.com/en-us/windows/win32/com/rules-for-implementing-queryinterface
// 1. Objects must have identity.
// 2. The set of interfaces on an object instance must be static.
// 3. It must be possible to query successfully for any interface on an object from any other interface.
// If ppvObject(the address) is nullptr, then this method returns E_POINTER.
if (!UnknownPtrPtr)
{
return E_POINTER;
}
// https://docs.microsoft.com/en-us/windows/win32/com/implementing-reference-counting
// Whenever a client calls a method(or API function), such as QueryInterface, that returns a new interface pointer,
// the method being called is responsible for incrementing the reference count through the returned pointer.
// For example, when a client first creates an object, it receives an interface pointer to an object that,
// from the client's point of view, has a reference count of one. If the client then calls AddRef on the interface pointer,
// the reference count becomes two. The client must call Release twice on the interface pointer to drop all of its references to the object.
if (IId == __uuidof(IMMNotificationClient) || IId == __uuidof(IUnknown))
{
*UnknownPtrPtr = (IMMNotificationClient*)(this);
AddRef();
return S_OK;
}
else if (IId == __uuidof(IAudioSessionEvents))
{
*UnknownPtrPtr = (IAudioSessionEvents*)this;
AddRef();
return S_OK;
}
// This method returns S_OK if the interface is supported, and E_NOINTERFACE otherwise.
*UnknownPtrPtr = nullptr;
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Verbose, TEXT("OnPropertyValueChanged: %s : %s"), *GetFriendlyName(pwstrDeviceId), *ToFString(key));
if (key.fmtid == PKEY_AudioEngine_DeviceFormat.fmtid)
{
// Get device.
FString DeviceId = pwstrDeviceId;
TComPtr<IMMDevice> Device;
HRESULT Hr = DeviceEnumerator->GetDevice(*DeviceId, &Device);
// Get property store.
TComPtr<IPropertyStore> PropertyStore;
if (SUCCEEDED(Hr) && Device)
{
Hr = Device->OpenPropertyStore(STGM_READ, &PropertyStore);
if (SUCCEEDED(Hr) && PropertyStore)
{
// Device Format
PROPVARIANT Prop;
PropVariantInit(&Prop);
if (key.fmtid == PKEY_AudioEngine_DeviceFormat.fmtid)
{
// WAVEFORMATEX blobs.
if (SUCCEEDED(PropertyStore->GetValue(key, &Prop)) && Prop.blob.pBlobData)
{
const WAVEFORMATEX* WaveFormatEx = (const WAVEFORMATEX*)(Prop.blob.pBlobData);
Audio::IAudioMixerDeviceChangedListener::FFormatChangedData FormatChanged;
FormatChanged.NumChannels = FMath::Clamp((int32)WaveFormatEx->nChannels, 2, 8);
FormatChanged.SampleRate = WaveFormatEx->nSamplesPerSec;
FormatChanged.ChannelBitmask = WaveFormatEx->wFormatTag == WAVE_FORMAT_EXTENSIBLE ?
((const WAVEFORMATEXTENSIBLE*)WaveFormatEx)->dwChannelMask : 0;
FReadScopeLock ReadLock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* i : Listeners)
{
i->OnFormatChanged(DeviceId, FormatChanged);
}
}
}
PropVariantClear(&Prop);
}
}
}
return S_OK;
}
TComPtr<IMMDevice> FWindowsMMNotificationClient::GetDevice(const FString InDeviceID) const
{
// Get device.
TComPtr<IMMDevice> Device;
HRESULT Hr = DeviceEnumerator->GetDevice(*InDeviceID, &Device);
if (SUCCEEDED(Hr))
{
return Device;
}
// Fail.
return {};
}
uint32 FWindowsMMNotificationClient::ReleaseClient()
{
return Release();
}
FString FWindowsMMNotificationClient::GetFriendlyName(const TComPtr<IMMDevice>& InDevice)
{
FString FriendlyName = TEXT("[No Friendly Name for Device]");
if (InDevice)
{
// Get property store.
TComPtr<IPropertyStore> PropStore;
HRESULT Hr = InDevice->OpenPropertyStore(STGM_READ, &PropStore);
// Get friendly name.
if (SUCCEEDED(Hr) && PropStore)
{
PROPVARIANT PropString;
PropVariantInit(&PropString);
// Get the endpoint device's friendly-name property.
Hr = PropStore->GetValue(PKEY_Device_FriendlyName, &PropString);
if (SUCCEEDED(Hr))
{
// Copy friendly name.
if (PropString.pwszVal)
{
FriendlyName = PropString.pwszVal;
}
}
PropVariantClear(&PropString);
}
}
return FriendlyName;
}
FString FWindowsMMNotificationClient::GetFriendlyName(const FString InDeviceID)
{
if (InDeviceID.IsEmpty())
{
return TEXT("System Default");
}
FString FriendlyName = TEXT("[No Friendly Name for Device]");
// Get device.
if (TComPtr<IMMDevice> Device = GetDevice(*InDeviceID))
{
return GetFriendlyName(Device);
}
return FriendlyName;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Display, TEXT("FWindowsMMNotificationClient: OnDeviceStateChanged: %s, %d"), *GetFriendlyName(pwstrDeviceId), dwNewState);
if (WindowsMMCvarUtils::ShouldIgnoreDeviceSwaps())
{
return S_OK;
}
bool bIsRender = IsRenderDevice(pwstrDeviceId);
if (dwNewState == DEVICE_STATE_ACTIVE || dwNewState == DEVICE_STATE_DISABLED || dwNewState == DEVICE_STATE_UNPLUGGED || dwNewState == DEVICE_STATE_NOTPRESENT)
{
Audio::EAudioDeviceState State = ConvertWordToDeviceState(dwNewState);
FString DeviceString(pwstrDeviceId);
FReadScopeLock ReadLock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDeviceStateChanged(DeviceString, State, bIsRender);
}
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnDeviceRemoved(LPCWSTR pwstrDeviceId)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Display, TEXT("FWindowsMMNotificationClient: OnDeviceRemoved: %s"), *GetFriendlyName(pwstrDeviceId));
if (WindowsMMCvarUtils::ShouldIgnoreDeviceSwaps())
{
return S_OK;
}
bool bIsRender = IsRenderDevice(pwstrDeviceId);
FString DeviceString(pwstrDeviceId);
FReadScopeLock ReadLock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDeviceRemoved(DeviceString, bIsRender);
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnDeviceAdded(LPCWSTR pwstrDeviceId)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Display, TEXT("FWindowsMMNotificationClient: OnDeviceAdded: %s"), *GetFriendlyName(pwstrDeviceId));
if (WindowsMMCvarUtils::ShouldIgnoreDeviceSwaps())
{
return S_OK;
}
bool bIsRender = IsRenderDevice(pwstrDeviceId);
FString DeviceString(pwstrDeviceId);
FReadScopeLock ReadLock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDeviceAdded(DeviceString, bIsRender);
}
return S_OK;
}
bool FWindowsMMNotificationClient::IsRenderDevice(const FString& InDeviceId) const
{
bool bIsRender = true;
if (TComPtr<IMMDevice> Device = GetDevice(InDeviceId))
{
TComPtr<IMMEndpoint> Endpoint;
if (SUCCEEDED(Device->QueryInterface(IID_PPV_ARGS(&Endpoint))))
{
EDataFlow DataFlow = eRender;
if (SUCCEEDED(Endpoint->GetDataFlow(&DataFlow)))
{
bIsRender = DataFlow == eRender;
}
}
}
return bIsRender;
}
HRESULT STDMETHODCALLTYPE FWindowsMMNotificationClient::OnDefaultDeviceChanged(EDataFlow InFlow, ERole InRole, LPCWSTR pwstrDeviceId)
{
UE_CLOG(WindowsMMCvarUtils::ShouldLogDeviceSwaps(), LogAudioEnumeration, Display,
TEXT("FWindowsMMNotificationClient: OnDefaultDeviceChanged: %s, %s, %s - %s"), ToString(InFlow), ToString(InRole), pwstrDeviceId, *GetFriendlyName(pwstrDeviceId));
Audio::EAudioDeviceRole AudioDeviceRole;
if (WindowsMMCvarUtils::ShouldIgnoreDeviceSwaps())
{
return S_OK;
}
if (InRole == eConsole)
{
AudioDeviceRole = Audio::EAudioDeviceRole::Console;
}
else if (InRole == eMultimedia)
{
AudioDeviceRole = Audio::EAudioDeviceRole::Multimedia;
}
else
{
AudioDeviceRole = Audio::EAudioDeviceRole::Communications;
}
FString DeviceString(pwstrDeviceId);
if (InFlow == eRender)
{
FReadScopeLock Lock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDefaultRenderDeviceChanged(AudioDeviceRole, DeviceString);
}
}
else if (InFlow == eCapture)
{
FReadScopeLock Lock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDefaultCaptureDeviceChanged(AudioDeviceRole, DeviceString);
}
}
else
{
FReadScopeLock Lock(ListenersSetRwLock);
for (Audio::IAudioMixerDeviceChangedListener* Listener : Listeners)
{
Listener->OnDefaultCaptureDeviceChanged(AudioDeviceRole, DeviceString);
Listener->OnDefaultRenderDeviceChanged(AudioDeviceRole, DeviceString);
}
}
return S_OK;
}
FWindowsMMNotificationClient::~FWindowsMMNotificationClient()
{
#if PLATFORM_WINDOWS
UnregisterForSessionNotifications();
#endif //PLATFORM_WINDOWS
if (DeviceEnumerator)
{
DeviceEnumerator->UnregisterEndpointNotificationCallback(this);
}
#if PLATFORM_WINDOWS
if (bComInitialized)
{
FWindowsPlatformMisc::CoUninitialize();
}
#endif //PLATFORM_WINDOWS
}
#if PLATFORM_WINDOWS
void FWindowsMMNotificationClient::UnregisterForSessionNotifications()
{
FScopeLock Lock(&SessionRegistrationCS);
// Unregister for any device we're already listening to.
if (SessionControls)
{
UE_LOG(LogAudioEnumeration, Verbose, TEXT("FWindowsMMNotificationClient: Unregistering for sessions events for device '%s'"), DeviceListeningToSessionEvents ? *GetFriendlyName(DeviceListeningToSessionEvents.Get()) : TEXT("None"));
SessionControls->UnregisterAudioSessionNotification(this);
SessionControls.Reset();
}
if (SessionManager)
{
SessionManager.Reset();
}
DeviceListeningToSessionEvents.Reset();
// Reset this flag.
bHasDisconnectSessionHappened = false;
}
#endif //PLATFORM_WINDOWS
FWindowsMMNotificationClient::FWindowsMMNotificationClient()
{
#if PLATFORM_WINDOWS
bComInitialized = FWindowsPlatformMisc::CoInitialize();
#endif //PLATFORM_WINDOWS
HRESULT Result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&DeviceEnumerator));
if (Result == S_OK)
{
DeviceEnumerator->RegisterEndpointNotificationCallback(this);
}
#if PLATFORM_WINDOWS
// Register for session events from default endpoint.
if (DeviceEnumerator)
{
TComPtr<IMMDevice> DefaultDevice;
if (SUCCEEDED(DeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &DefaultDevice)))
{
RegisterForSessionNotifications(DefaultDevice);
}
}
#endif //PLATFORM_WINDOWS
}
}// namespace Audio