设备事件(核心音频 API)

设备事件通知客户端系统中音频终结点设备的状态发生变化。 以下是设备事件的示例:

  • 用户可通过设备管理器或 Windows 多媒体控制面板 Mmsys.cpl 来启用或禁用音频终结点设备。
  • 用户在系统中添加音频适配器或从系统中删除音频适配器。
  • 用户将音频终结点设备插入具有插孔状态检测功能的音频插孔,或从插孔中删除音频终结点设备。
  • 用户可更改分配给设备的设备角色
  • 设备属性的值会发生变化。

音频适配器的添加或删除会为连接到该适配器的所有音频终结点设备生成设备事件。 上表中的前四项是设备状态变化的示例。 有关音频终结点设备的设备状态的详细信息,请参阅 DEVICE_STATE_XXX 常量。 有关插孔状态检测的详细信息,请参阅音频终结点设备

客户端可以注册,以便在设备事件发生时收到通知。 为了响应这些通知,客户端可以动态更改使用特定设备的方式,或选择不同的设备用于特定目的。

例如,如果应用程序正在通过一组 USB 扬声器播放音轨,而用户断开了扬声器与 USB 连接器的连接,则应用程序就会收到设备事件通知。 在响应事件时,如果应用程序检测到一组台式机扬声器与系统主板上的集成音频适配器相连,则应用程序可以恢复通过台式机扬声器播放音轨。 在此示例中,从 USB 扬声器到桌面扬声器的转换是自动进行的,用户无需通过明确重定向应用程序加以干预。

要注册接以收设备通知,客户端需要调用 IMMDeviceEnumerator::RegisterEndpointNotificationCallback 方法。 当客户端不再需要通知时,它会调用 IMMDeviceEnumerator::UnregisterEndpointNotificationCallback 方法来取消通知。 这两个方法都会接收名为 pClient 的输入参数,该参数是指向 IMMNotificationClient 接口实例的指针。

IMMNotificationClient 接口由客户端实现。 该接口包含多种方法,每种方法都是特定类型设备事件的回调例程。 当音频终结点设备发生设备事件时,MMDevice 模块会调用当前注册以接收设备事件通知的每个客户端的 IMMNotificationClient 接口中的相应方法。 这些调用会将事件说明传递给客户端。 有关详细信息,请参见 IMMNotificationClient 接口

注册接收设备事件通知的客户端将收到系统中所有音频终结点设备发生的所有类型设备事件的通知。 如果客户端只对某些事件类型或某些设备感兴趣,那么其 IMMNotificationClient 实现中的方法应对事件进行适当的筛选。

Windows SDK 提供的示例包括 IMMNotificationClient 接口的多个实现。 有关详细信息,请参阅使用核心音频 API 的SDK 示例

以下代码示例展示了 IMMNotificationClient 接口可能的实现:

//-----------------------------------------------------------
// Example implementation of IMMNotificationClient interface.
// When the status of audio endpoint devices change, the
// MMDevice module calls these methods to notify the client.
//-----------------------------------------------------------

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

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

    // Private function to print device-friendly name
    HRESULT _PrintDeviceName(LPCWSTR  pwstrId);

public:
    CMMNotificationClient() :
        _cRef(1),
        _pEnumerator(NULL)
    {
    }

    ~CMMNotificationClient()
    {
        SAFE_RELEASE(_pEnumerator)
    }

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

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

    ULONG STDMETHODCALLTYPE Release()
    {
        ULONG ulRef = 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(IMMNotificationClient) == riid)
        {
            AddRef();
            *ppvInterface = (IMMNotificationClient*)this;
        }
        else
        {
            *ppvInterface = NULL;
            return E_NOINTERFACE;
        }
        return S_OK;
    }

    // Callback methods for device-event notifications.

    HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(
                                EDataFlow flow, ERole role,
                                LPCWSTR pwstrDeviceId)
    {
        char  *pszFlow = "?????";
        char  *pszRole = "?????";

        _PrintDeviceName(pwstrDeviceId);

        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, pszRole);
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId)
    {
        _PrintDeviceName(pwstrDeviceId);

        printf("  -->Added device\n");
        return S_OK;
    };

    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId)
    {
        _PrintDeviceName(pwstrDeviceId);

        printf("  -->Removed device\n");
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(
                                LPCWSTR pwstrDeviceId,
                                DWORD dwNewState)
    {
        char  *pszState = "?????";

        _PrintDeviceName(pwstrDeviceId);

        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, dwNewState);

        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(
                                LPCWSTR pwstrDeviceId,
                                const PROPERTYKEY key)
    {
        _PrintDeviceName(pwstrDeviceId);

        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;
    }
};

// Given an endpoint ID string, print the friendly device name.
HRESULT CMMNotificationClient::_PrintDeviceName(LPCWSTR pwstrId)
{
    HRESULT hr = S_OK;
    IMMDevice *pDevice = NULL;
    IPropertyStore *pProps = NULL;
    PROPVARIANT varString;

    CoInitialize(NULL);
    PropVariantInit(&varString);

    if (_pEnumerator == NULL)
    {
        // Get enumerator for audio endpoint devices.
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
                              NULL, CLSCTX_INPROC_SERVER,
                              __uuidof(IMMDeviceEnumerator),
                              (void**)&_pEnumerator);
    }
    if (hr == S_OK)
    {
        hr = _pEnumerator->GetDevice(pwstrId, &pDevice);
    }
    if (hr == S_OK)
    {
        hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);
    }
    if (hr == S_OK)
    {
        // Get the endpoint device's friendly-name property.
        hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString);
    }
    printf("----------------------\nDevice name: \"%S\"\n"
           "  Endpoint ID string: \"%S\"\n",
           (hr == S_OK) ? varString.pwszVal : L"null device",
           (pwstrId != NULL) ? pwstrId : L"null ID");

    PropVariantClear(&varString);

    SAFE_RELEASE(pProps)
    SAFE_RELEASE(pDevice)
    CoUninitialize();
    return hr;
}

前面代码示例中的 CMMNotificationClient 类是 IMMNotificationClient 接口的实现。 由于 IMMNotificationClient 继承自 IUnknown,因此该类的定义包含对 IUnknown 方法 AddRefReleaseQueryInterface 的实现。 类定义中的其余公开方法是 IMMNotificationClient 接口所专用的。 这些方法包括:

  • OnDefaultDeviceChanged,当用户更改音频终结点设备的设备角色时调用。
  • OnDeviceAdded,在用户向系统添加音频终结点设备时调用。
  • OnDeviceRemoved,当用户从系统中删除音频终结点设备时调用。
  • OnDeviceStateChanged,当音频终结点设备的设备状态发生变化时被调用。 (有关设备状态的详细信息,请参阅 DEVICE_STATE_ XXX 常量。)
  • OnPropertyValueChanged,当音频终结点设备的属性值发生变化时被调用。

每种方法都需要一个输入参数 pwstrDeviceId,它是指向终结点 ID 字符串的指针。 该字符串用于标识发生设备事件的音频终结点设备。

在前面的代码示例中,_PrintDeviceName 是 CMMNotificationClient 类的一个专用方法,用于打印设备的友好名称。 _PrintDeviceName 会将终结点 ID 字符串作为输入参数。 它会将字符串传递给 IMMDeviceEnumerator::GetDevice 方法。 GetDevice 会创建一个终结点设备对象来表示设备,并为该对象提供 IMMDevice 接口。 接下来,_PrintDeviceName 会调用 IMMDevice::OpenPropertyStore 方法来检索设备属性存储的 IPropertyStore 接口。 最后,_PrintDeviceName 会调用 IPropertyStore::GetItem 方法来获取设备的友好名称属性。 有关 IPropertyStore 的详细信息,请参阅 Windows SDK 文档。

除设备事件外,客户端还可以注册以接收音频会话事件和终结点音量事件的通知。 有关详细信息,请参阅 IAudioSessionEvents 接口IAudioEndpointVolumeCallback 接口

音频终结点设备