PCに接続されているマイク(オーディオデバイス)を取得し、音量設定をする(EnumAudioEndpoints)(C++版)(ちゃんとReleaseする版)

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の記事で、マイクスピーカーを扱うC++を書いたが、メモリリークしまくりだった。

https://tera1707.com/entry/2023/10/10/224852

こういうコードだった。

※リークしている様子がわかりやすいように、処理を無限ループの中に入れた。

#include <windows.h>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys.h>
#include <endpointvolume.h>
#include <string>



int main()
{
    // COMの初期化(COMのお作法)
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

    while (TRUE)
    {
        IMMDeviceEnumerator* pEnum = NULL;
        IMMDeviceCollection* pCollection = NULL;
        UINT deviceCount = 0;

        // COMからMMDeviceEnumeratorを取ってくる
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));

        // オーディオエンドポイントの列挙を実行
        hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection);

        // とれた数を数える
        hr = pCollection->GetCount(&deviceCount);

        for (int i = 0; i < deviceCount; i++)
        {
            // デバイスの情報を取る
            IMMDevice* pEndpoint = NULL;
            hr = pCollection->Item(i, &pEndpoint);

            // デバイスのプロパティを取る
            IPropertyStore* pProperties;
            hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties);

            // 取ったプロパティから、値を指定して取得する
            PROPVARIANT vName;
            PropVariantInit(&vName);
            hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName);

            // エンドポイントの情報を取る
            IAudioEndpointVolume* pAudioEndVol = NULL;
            hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);

            // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい)
            float getVolume = 0.0f;
            hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume);             // 音量を取得

            float setVolume = 0.25f;
            hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL);  // 音量を設定

            std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n";
            OutputDebugString(outString.c_str());
        }
    }

    // COMの後処理(COMのお作法)
    CoUninitialize();

    return 0;
}

メモリリークの様子
→動かすと、ハンドルとWorkingSet(メモリ使用量)がガンガン増えていく...

これでは使い物にならないので、ちゃんと使用済みのCOMのポインタを開放する処理を入れたい。

Releaseを入れたコード

下記にあるSafeReleaseを使って、解放を行う。

https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/com-coding-practices#the-saferelease-pattern

#include <windows.h>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys.h>
#include <endpointvolume.h>
#include <string>

// SafeRelease
// https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/com-coding-practices#the-saferelease-pattern
// エラー処理
// https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com

template <class T> void SafeRelease(T** ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

int main()
{
    // COMの初期化(COMのお作法)
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

    while (TRUE)
    {
        IMMDeviceEnumerator* pEnum = NULL;
        IMMDeviceCollection* pCollection = NULL;
        UINT deviceCount = 0;

        // COMからMMDeviceEnumeratorを取ってくる
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));

        if (hr != S_OK)
        {
            return -1;
        }

        // オーディオエンドポイントの列挙を実行
        hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection);

        if (hr != S_OK)
        {
            SafeRelease(&pEnum);
            return -1;
        }

        // とれた数を数える
        hr = pCollection->GetCount(&deviceCount);

        if (hr != S_OK)
        {
            SafeRelease(&pEnum);
            SafeRelease(&pCollection);
            return -1;
        }

        for (int i = 0; i < deviceCount; i++)
        {
            // デバイスの情報を取る
            IMMDevice* pEndpoint = NULL;
            hr = pCollection->Item(i, &pEndpoint);

            if (hr != S_OK)
                continue;

            // デバイスのプロパティを取る
            IPropertyStore* pProperties;
            hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties);

            if (hr != S_OK)
            {
                SafeRelease(&pEndpoint);
                continue;
            }

            // 取ったプロパティから、値を指定して取得する
            PROPVARIANT vName;
            PropVariantInit(&vName);
            hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName);

            if (hr != S_OK)
            {
                SafeRelease(&pEndpoint);
                SafeRelease(&pProperties);
                PropVariantClear(&vName);
                continue;
            }

            // エンドポイントの情報を取る
            IAudioEndpointVolume* pAudioEndVol = NULL;
            hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);

            if (hr != S_OK)
            {
                SafeRelease(&pEndpoint);
                SafeRelease(&pProperties);
                PropVariantClear(&vName);
                continue;
            }

            // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい)
            float getVolume = 0.0f;
            hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume);             // 音量を取得

            if (hr != S_OK)
            {
                SafeRelease(&pEndpoint);
                SafeRelease(&pProperties);
                PropVariantClear(&vName);
                continue;
            }

            float setVolume = 0.25f;
            hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL);  // 音量を設定

            if (hr != S_OK)
            {
                SafeRelease(&pEndpoint);
                SafeRelease(&pProperties);
                PropVariantClear(&vName);
                continue;
            }

            std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n";
            OutputDebugString(outString.c_str());

            // 後処理
            SafeRelease(&pEndpoint);
            SafeRelease(&pProperties);
            PropVariantClear(&vName);
            SafeRelease(&pAudioEndVol);
        }

        // 後処理
        SafeRelease(&pCollection);
        SafeRelease(&pEnum);
    }

    // COMの後処理(COMのお作法)
    CoUninitialize();

    return 0;
}

解放を入れた後のハンドルとメモリ使用量

これでうまく解放できているっぽいが、コードがSafeRelease()だらけになる....

CComPtrを使う その①(エラー処理無し)

CComPtr<>という、自動でCOMのポインタを解放してくれる便利なものがあるらしい。

それでやってみる。

#include <windows.h>
#include <string>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys.h>
#include <endpointvolume.h>
#include "atlbase.h" // CComPtr

int main()
{
    // COMの初期化(COMのお作法)
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

    while (TRUE)
    {
        CComPtr<IMMDeviceEnumerator> pEnum = NULL;
        CComPtr<IMMDeviceCollection> pCollection = NULL;
        UINT deviceCount = 0;

        // COMからMMDeviceEnumeratorを取ってくる
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));

        // オーディオエンドポイントの列挙を実行
        hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection);

        // とれた数を数える
        hr = pCollection->GetCount(&deviceCount);

        for (int i = 0; i < deviceCount; i++)
        {
            // デバイスの情報を取る
            CComPtr<IMMDevice> pEndpoint = NULL;
            hr = pCollection->Item(i, &pEndpoint);

            // デバイスのプロパティを取る
            CComPtr<IPropertyStore> pProperties;
            hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties);

            // 取ったプロパティから、値を指定して取得する
            PROPVARIANT vName;
            PropVariantInit(&vName);
            hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName);

            // エンドポイントの情報を取る
            CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL;
            hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);

            // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい)
            float getVolume = 0.0f;
            hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume);             // 音量を取得

            float setVolume = 0.25f;
            hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL);  // 音量を設定

            std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n";
            PropVariantClear(&vName);
            OutputDebugString(outString.c_str());
        }

        // 後処理はかってにやってくれる!
    }

    // COMの後処理(COMのお作法)
    CoUninitialize();

    return 0;
}

これでも、メモリはちゃんと解放されてくれてるようで、メモリ使用量は増えなかった。

が、👆のコードだとエラー発生時に、その後の処理を継続してしまうのでエラー処理を入れたい。

CComPtrを使う その②(エラー処理あり:Cascading ifs パターン)

↓のやり方で、エラー処理を記述してみた。

https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#cascading-ifs

それがこれ。

#include <windows.h>
#include <string>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys.h>
#include <endpointvolume.h>
#include <comdef.h>
#include "atlbase.h" // CComPtr

int main()
{
    // COMの初期化(COMのお作法)
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

    while (TRUE)
    {
        CComPtr<IMMDeviceEnumerator> pEnum = NULL;
        CComPtr<IMMDeviceCollection> pCollection = NULL;
        UINT deviceCount = 0;

        // COMからMMDeviceEnumeratorを取ってくる
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum));

        if (SUCCEEDED(hr))
        {
            // オーディオエンドポイントの列挙を実行
            hr = pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection);
        }

        if (SUCCEEDED(hr))
        {
            // とれた数を数える
            hr = pCollection->GetCount(&deviceCount);
        }


        for (int i = 0; i < deviceCount; i++)
        {
            // デバイスの情報を取る
            CComPtr<IMMDevice> pEndpoint = NULL;
            if (SUCCEEDED(hr))
            {
                // デバイスの情報を取る
                hr = pCollection->Item(i, &pEndpoint);
            }

            CComPtr<IPropertyStore> pProperties;
            if (SUCCEEDED(hr))
            {
                // デバイスのプロパティを取る
                hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProperties);
            }

            PROPVARIANT vName;
            if (SUCCEEDED(hr))
            {
                // 取ったプロパティから、値を指定して取得する
                PropVariantInit(&vName);
                hr = pProperties->GetValue(PKEY_Device_FriendlyName, &vName);
            }

            CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL;
            if (SUCCEEDED(hr))
            {
                // エンドポイントの情報を取る
                hr = pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol);
            }

            // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい)
            float getVolume = 0.0f;
            if (SUCCEEDED(hr))
            {
                // 音量を取得
                hr = pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume);
            }

            float setVolume = 0.25f;
            if (SUCCEEDED(hr))
            {
                // 音量を設定
                hr = pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL);
            }

            if (SUCCEEDED(hr))
            {
                // 表示
                std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n";
                PropVariantClear(&vName);
                OutputDebugString(outString.c_str());
            }
        }
    }

    // COMの後処理(COMのお作法)
    CoUninitialize();

    return 0;
}

これもメモリはちゃんと解放はされてるが、なんかコードの流れは分かる..?のだが、if (SUCCEEDED(hr))だらけになるのと、今後の変更で途中になにか処理を(間違えて)入れてしまったらいやだな、、、というのが気になる。。。

CComPtrを使う その③(エラー処理あり:Throw on Fail パターン)

↓のやり方で、エラー処理を記述してみた。

https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#throw-on-fail

それがこれ。

#include <windows.h>
#include <string>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys.h>
#include <endpointvolume.h>
#include <comdef.h>
#include "atlbase.h" // CComPtr

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

int main()
{
    // COMの初期化(COMのお作法)
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

    while (TRUE)
    {
        CComPtr<IMMDeviceEnumerator> pEnum = NULL;
        CComPtr<IMMDeviceCollection> pCollection = NULL;
        UINT deviceCount = 0;

        try
        {
            // COMからMMDeviceEnumeratorを取ってくる
            throw_if_fail(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)));

            // オーディオエンドポイントの列挙を実行
            throw_if_fail(pEnum->EnumAudioEndpoints(EDataFlow::eCapture, DEVICE_STATE_ACTIVE, &pCollection));

            // とれた数を数える
            throw_if_fail(pCollection->GetCount(&deviceCount));

            for (int i = 0; i < deviceCount; i++)
            {
                try
                {
                    // デバイスの情報を取る
                    CComPtr<IMMDevice> pEndpoint = NULL;
                    throw_if_fail(pCollection->Item(i, &pEndpoint));

                    // デバイスのプロパティを取る
                    CComPtr<IPropertyStore> pProperties;
                    throw_if_fail(pEndpoint->OpenPropertyStore(STGM_READ, &pProperties));

                    // 取ったプロパティから、値を指定して取得する
                    PROPVARIANT vName;
                    PropVariantInit(&vName);
                    throw_if_fail(pProperties->GetValue(PKEY_Device_FriendlyName, &vName));

                    // エンドポイントの情報を取る
                    CComPtr<IAudioEndpointVolume> pAudioEndVol = NULL;
                    throw_if_fail(pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void**)&pAudioEndVol));

                    // エンドポイントに指示を出す(「IAudioEndpointVolume」のVolumeは、マイクの怨霊という意味の「ボリューム」とは別の意味っぽい)
                    float getVolume = 0.0f;
                    throw_if_fail(pAudioEndVol->GetMasterVolumeLevelScalar(&getVolume));             // 音量を取得

                    float setVolume = 0.25f;
                    throw_if_fail(pAudioEndVol->SetMasterVolumeLevelScalar(setVolume, &GUID_NULL));  // 音量を設定

                    std::wstring outString = std::to_wstring(i) + L":" + vName.pwszVal + std::to_wstring(getVolume) + L"\r\n";
                    PropVariantClear(&vName);
                    OutputDebugString(outString.c_str());
                }
                catch (_com_error err)
                {
                    OutputDebugString(L"デバイス情報取得のCOMエラー");
                }
            }
        }
        catch (_com_error err)
        {
            OutputDebugString(L"デバイス列挙のCOMエラー");
        }
    }

    // COMの後処理(COMのお作法)
    CoUninitialize();

    return 0;
}

これが、流れも違和感なくわかるし、一番すっきりしてよさそう。

今後、COMが出てくる箇所あれば、これでやってみようと思う。

備考

エラー処理の記述方法には、gotoを使う書き方もあるらしい。

https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/error-handling-in-com#jump-on-fail

が、個人的にgotoを使うのはなんかいやなので、これはナシとした。

参考

以前の記事(マイクスピーカーを扱うC++を書いたがメモリリークしまくり)

https://tera1707.com/entry/2023/10/10/224852

learn.microsoft.com

learn.microsoft.com

learn.microsoft.com