UnrealEngineでWindowsのマスターボリュームを取得する

UnrealEngineでWindowsのマスターボリュームを取得してみたので内容をメモしておきます。
Windows Vista以降ではマルチメディア関数(waveXxx 関数など)ではなくてコアオーディオAPI(MMDeviceなど)が使われるようになったみたいなのでMMDeviceを使っています。

## 確認した環境

* UnrealEngine 4.27、5.2.0
* Windows11 22H2

## 参照サイト

* [コア オーディオ API - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/coreaudio/core-audio-apis-in-windows-vista)
* [マスタボリュームのミュートの状態を取得・変更](http://yamatyuu.net/computer/program/vc2013/mute/index.html)
* [Windows音量变化通知 - 系统音量监控_水墨长天的博客-CSDN博客](https://blog.csdn.net/gehong3641/article/details/114448462)

## Blueprintノード

以下のように音声入力・出力の音量の取得と、Windowsなどで音量やミュート状態が変更されたときにDelegateで取得できるようにしてみました。

![スクリーンショット 2023-05-29 124319.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16768/04663eb5-662c-3238-8c10-b5e45085ab15.png)

## C++コード

コードを見てもらえばわかりますが、音量やミュート状態が変更されたときにイベントを受け取る関数を複数呼び出しすると、以前の分が解放されてしまって最後の分しか有効になりません。

```csharp:MyUtils.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyUtils.generated.h"

class CVolumeNotification;
struct IAudioEndpointVolume;

DECLARE_DYNAMIC_DELEGATE_TwoParams(FMicChangeDelegate, float, Volume, bool, IsMute);

/**
 * 音声入力/出力の種別
 */
UENUM(BlueprintType)
enum class ESoundTypeEnum : uint8
{
	Mic = 0,
	Speaker = 1,
};

/**
 * 音量関係取得のためのUtilityクラス
 */
UCLASS()
class WINSOUNDTEST_API UMyUtils : public UObject
{
	GENERATED_BODY()

public:
    /*
     * 音量取得
     */
	UFUNCTION(BlueprintCallable)
		static float GetSoundVolumeLevel(ESoundTypeEnum Type);

    /*
     * 音量変更時の通知登録
     */
	UFUNCTION(BlueprintCallable)
		static void RegisterSoundChangeNotify(ESoundTypeEnum Type, UPARAM(DisplayName = "Event") FMicChangeDelegate Delegate);

private:
#if PLATFORM_WINDOWS
	static TSharedPtr<CVolumeNotification> MicVolumeNotification;
	static TSharedPtr<IAudioEndpointVolume> PMicAudioEndVol;
	static TSharedPtr<CVolumeNotification> SpeakerVolumeNotification;
	static TSharedPtr<IAudioEndpointVolume> PSpeakerAudioEndVol;
#endif
};
```

```csharp:MyUtils.cpp
#include "MyUtils.h"

#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h"
#include "Windows/PreWindowsApi.h"

#include <windows.h>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
#include <stdio.h>
#include <locale.h>
#include <tchar.h>

#include "Windows/PostWindowsApi.h"
#include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h"
#endif

#if PLATFORM_WINDOWS
TSharedPtr<CVolumeNotification> UMyUtils::MicVolumeNotification;
TSharedPtr<IAudioEndpointVolume> UMyUtils::PMicAudioEndVol;
TSharedPtr<CVolumeNotification> UMyUtils::SpeakerVolumeNotification;
TSharedPtr<IAudioEndpointVolume> UMyUtils::PSpeakerAudioEndVol;

/**
 * 通知受け取り用のクラス
 */
class CVolumeNotification : public IAudioEndpointVolumeCallback
{
    FMicChangeDelegate _Delegate;
    int32 _cRef;
public:
    CVolumeNotification(FMicChangeDelegate InDelegate) : _Delegate(InDelegate), _cRef(1) {}

    virtual ~CVolumeNotification() {}

    ULONG STDMETHODCALLTYPE AddRef()
    {
        return FPlatformAtomics::InterlockedIncrement(&_cRef);
    }

    ULONG STDMETHODCALLTYPE Release()
    {
        ULONG ulRef = FPlatformAtomics::InterlockedDecrement(&_cRef);
        if (0 == ulRef)
        {
            delete this;
        }
        return ulRef;
    }

    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface)
    {
        if (IID_IUnknown == riid)
        {
            AddRef();
            *ppvInterface = (IUnknown*)this;
        }
        else if (__uuidof(IAudioEndpointVolumeCallback) == riid)
        {
            AddRef();
            *ppvInterface = (IAudioEndpointVolumeCallback*)this;
        }
        else
        {
            *ppvInterface = NULL;
            return E_NOINTERFACE;
        }

        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify)
    {
        if (!pNotify)
        {
            return E_INVALIDARG;
        }

        _Delegate.ExecuteIfBound(pNotify->fMasterVolume, pNotify->bMuted);
        UE_LOG(LogTemp, Warning, TEXT("Nofity:%f, %d"), pNotify->fMasterVolume, pNotify->bMuted);

        return S_OK;
    }
};
#endif

/*
 * 音量取得
 */
float UMyUtils::GetSoundVolumeLevel(ESoundTypeEnum Type)
{
#if PLATFORM_WINDOWS
    HRESULT hr;

    IMMDeviceEnumerator* pEnum = NULL;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));
    if (FAILED(hr)) {
        return -1.0f;
    }
    TSharedPtr<IMMDeviceEnumerator> PEnum(pEnum, [](IMMDeviceEnumerator* enumerator) {
        if (enumerator) { enumerator->Release(); }
        });

    IMMDevice* pEndpoint = NULL;
    hr = PEnum->GetDefaultAudioEndpoint(Type == ESoundTypeEnum::Speaker ? eRender : eCapture, eConsole, &pEndpoint);
    if (FAILED(hr))
    {
        return -1.0f;
    }
    TSharedPtr<IMMDevice> PEndPoint(pEndpoint, [](IMMDevice* device) {
        if (device) { device->Release(); }
        });

    IAudioEndpointVolume* pAudioEndVol = NULL;
    hr = PEndPoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);
    if (FAILED(hr))
    {
        return -1.0f;
    }
    TSharedPtr<IAudioEndpointVolume> PAudioEndVol
        = TSharedPtr<IAudioEndpointVolume>(pAudioEndVol, [](IAudioEndpointVolume* volume) {
              if (volume) { volume->Release(); }
          });

    float level;
    hr = PAudioEndVol->GetMasterVolumeLevelScalar(&level);
    if (FAILED(hr))
    {
        return -1.0f;
    }
    UE_LOG(LogTemp, Warning, TEXT("VolumeLevel:%f"), level);

    return level;
#else
    return -1.0f;
#endif
}

/*
 * 音量変更時の通知登録
 */
void UMyUtils::RegisterSoundChangeNotify(ESoundTypeEnum Type, FMicChangeDelegate Delegate)
{
#if PLATFORM_WINDOWS
    HRESULT hr;

    TSharedPtr<IAudioEndpointVolume>& PAudioEndVol
        = Type == ESoundTypeEnum::Speaker ? PSpeakerAudioEndVol : PMicAudioEndVol;

    IMMDeviceEnumerator* pEnum = NULL;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));
    if (FAILED(hr)) {
        return;
    }
    TSharedPtr<IMMDeviceEnumerator> PEnum(pEnum, [](IMMDeviceEnumerator* enumerator) {
        if (enumerator) { enumerator->Release(); }
        });

    IMMDevice* pEndpoint = NULL;
    hr = PEnum->GetDefaultAudioEndpoint(Type == ESoundTypeEnum::Speaker ? eRender : eCapture, eConsole, &pEndpoint);
    if (FAILED(hr))
    {
        return;
    }
    TSharedPtr<IMMDevice> PEndPoint(pEndpoint, [](IMMDevice* device) {
        if (device) { device->Release(); }
        });

    IAudioEndpointVolume* pAudioEndVol = NULL;
    hr = PEndPoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);
    if (FAILED(hr))
    {
        return;
    }

    PAudioEndVol = TSharedPtr<IAudioEndpointVolume>(pAudioEndVol, [](IAudioEndpointVolume* volume) {
        if (volume) { volume->Release(); }
    });

    TSharedPtr<CVolumeNotification>& VolumeNotification = 
        Type == ESoundTypeEnum::Speaker ? SpeakerVolumeNotification : MicVolumeNotification;
    VolumeNotification = TSharedPtr<CVolumeNotification>(new CVolumeNotification(Delegate));

    PAudioEndVol->RegisterControlChangeNotify(VolumeNotification.Get());
#endif
}
```