PCにマイクやスピーカー(オーディオデバイス)を抜きさししたときのイベントを取る(C++版)

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

やりたいこと

前回の記事(下記)で、マイクやスピーカのデバイス(エンドポイント)の名前を取得したり、現在の音量を取得、設定したりした。

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

今回は、それらのデバイスを抜きさししたときに、それを知りたい。(抜きさし時のイベントを取りたい。)

やったこと

IMMNotificationClientインターフェースクラスを継承したクラスを作って、それをRegisterEndpointNotificationCallback()する。

下記MSDocsページを参考にした。

https://learn.microsoft.com/ja-jp/windows/win32/coreaudio/device-events

サンプルコード

ここにコードを置いた。

https://github.com/tera1707/AudioDevice

IMMNotificationClientを実装したクラス

#include "CMMNotificationClient.h"

std::string WStringToString(std::wstring oWString);

#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

CMMNotificationClient::CMMNotificationClient(IMMDeviceEnumerator* pEnum) : _cRef(1)
{
    _pEnumerator = pEnum;
}

CMMNotificationClient::~CMMNotificationClient()
{
    //SAFE_RELEASE(_pEnumerator)
}

////////////////////////////////////////////////////////////////
// IUnknown methods -- AddRef, Release, and QueryInterface
////////////////////////////////////////////////////////////////

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

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

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

////////////////////////////////////////////////////////////////
// Callback methods for device-event notifications.
////////////////////////////////////////////////////////////////

HRESULT STDMETHODCALLTYPE CMMNotificationClient::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
{
    if (pwstrDeviceId != NULL)
    {
        auto devName = GetDevName(pwstrDeviceId);
        printf(devName.c_str());
    }

    std::string pszFlow = LPSTR("?????");
    std::string pszRole = LPSTR("?????");

    switch (flow)
    {
    case eRender: pszFlow = "eRender"; break;
    case eCapture: pszFlow = "eCapture"; break;
    }

    switch (role)
    {
    case eConsole: pszRole = "eConsole"; break;
    case eMultimedia: pszRole = "eMultimedia"; break;
    case eCommunications: pszRole = "eCommunications"; break;
    }

    printf("  -->New default device: flow = %s, role = %s\n", pszFlow.c_str(), pszRole.c_str());
    return S_OK;
}

HRESULT STDMETHODCALLTYPE CMMNotificationClient::OnDeviceAdded(LPCWSTR pwstrDeviceId)
{
    auto devName = GetDevName(pwstrDeviceId);
    printf(devName.c_str());
    printf("  -->Added device\n");
    return S_OK;
};

HRESULT STDMETHODCALLTYPE CMMNotificationClient::OnDeviceRemoved(LPCWSTR pwstrDeviceId)
{
    auto devName = GetDevName(pwstrDeviceId);
    printf(devName.c_str());
    printf("  -->Removed device\n");
    return S_OK;
}

HRESULT STDMETHODCALLTYPE CMMNotificationClient::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState)
{
    auto devName = GetDevName(pwstrDeviceId);
    printf(devName.c_str());

    std::string pszState = "?????";

    switch (dwNewState)
    {
    case DEVICE_STATE_ACTIVE: pszState = "ACTIVE"; break;
    case DEVICE_STATE_DISABLED: pszState = "DISABLED"; break;
    case DEVICE_STATE_NOTPRESENT: pszState = "NOTPRESENT"; break;
    case DEVICE_STATE_UNPLUGGED: pszState = "UNPLUGGED"; break;
    }

    printf("  -->New device state is DEVICE_STATE_%s (0x%8.8x)\n", pszState.c_str(), dwNewState);

    return S_OK;
}

HRESULT STDMETHODCALLTYPE CMMNotificationClient::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
{
    auto devName = GetDevName(pwstrDeviceId);
    printf(devName.c_str());

    printf("  -->Changed device property "
        "{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}#%d\n",
        key.fmtid.Data1, key.fmtid.Data2, key.fmtid.Data3,
        key.fmtid.Data4[0], key.fmtid.Data4[1],
        key.fmtid.Data4[2], key.fmtid.Data4[3],
        key.fmtid.Data4[4], key.fmtid.Data4[5],
        key.fmtid.Data4[6], key.fmtid.Data4[7],
        key.pid);
    return S_OK;
}

std::string CMMNotificationClient::GetDevName(LPCWSTR pwstrDeviceId)
{
    IMMDevice* device = NULL;
    _pEnumerator->GetDevice(pwstrDeviceId, &device);

    IPropertyStore* pProperties;
    device->OpenPropertyStore(STGM_READ, &pProperties);
    PROPVARIANT vName;
    PropVariantInit(&vName);
    pProperties->GetValue(PKEY_Device_FriendlyName, &vName);
    return WStringToString(std::wstring(vName.pwszVal == NULL ? L"" : vName.pwszVal));
}

// ------------------------------------------------------------------------

std::string WStringToString(std::wstring oWString)
{
    // wstring → SJIS
    int iBufferSize = WideCharToMultiByte(CP_OEMCP, 0, oWString.c_str()
        , -1, (char*)NULL, 0, NULL, NULL);

    // バッファの取得
    CHAR* cpMultiByte = new CHAR[iBufferSize];

    // wstring → SJIS
    WideCharToMultiByte(CP_OEMCP, 0, oWString.c_str(), -1, cpMultiByte
        , iBufferSize, NULL, NULL);

    // stringの生成
    std::string oRet(cpMultiByte, cpMultiByte + iBufferSize - 1);

    // バッファの破棄
    delete[] cpMultiByte;

    // 変換結果を返す
    return(oRet);
}

IMMNotificationClientを実装したクラスのヘッダ

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

class CMMNotificationClient : public IMMNotificationClient
{
    LONG _cRef;
    IMMDeviceEnumerator* _pEnumerator;

public:
    CMMNotificationClient(IMMDeviceEnumerator* pEnum);
    ~CMMNotificationClient();
    ULONG STDMETHODCALLTYPE AddRef();
    ULONG STDMETHODCALLTYPE Release();
    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface);
    std::string GetDevName(LPCWSTR pwstrDeviceId);
    HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId);
    HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId);
    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId);
    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState);
    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key);
};

メイン

抜きさし時のイベントを取るだけなら、メインはこれだけでOK。

参考:メインの全体はこちら

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

#include "CMMNotificationClient.h"

int main()
{
    HRESULT hr;
    IMMDeviceEnumerator* pEnum = NULL;
    IMMDeviceCollection* pCollection = NULL;
    UINT deviceCount = 0;

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

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

    auto nc = new CMMNotificationClient(pEnum);
    pEnum->RegisterEndpointNotificationCallback(nc);

    while (1)
    {
        Sleep(100000);
    }

    //// 後処理系
    if (pEnum)
        pEnum->Release();

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

備考

「COM」というしくみを使っているやり方のようだが、COMが全然わかってない。

今回作った「CMMNotificationClient」クラスが、

  • ULONG STDMETHODCALLTYPE AddRef();
  • ULONG STDMETHODCALLTYPE Release();
  • HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface);

を実装しないといけないのも、COMのお作法らしい。

オーディオの話とは別に、COMについても調べたい。

参考

バイス イベント (コア オーディオ API)

https://learn.microsoft.com/ja-jp/windows/win32/coreaudio/device-events

前回の自分の記事(PCに接続されているマイク(オーディオデバイス)を取得し、音量設定をする(EnumAudioEndpoints))

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