设备拓扑

DeviceTopology API 使客户端能够控制无法通过 MMDevice APIWASAPIEndpointVolume API访问的各种音频适配器的内部功能。

如前所述,MMDevice APIWASAPIEndpointVolume API 将麦克风、扬声器、耳机和其他音频输入和输出设备作为 音频终结点设备呈现给客户端。 终结点设备模型为客户端提供了对音频设备中的音量和静音控件的便捷访问。 仅需要这些简单控件的客户端可以避免遍历音频适配器中硬件设备的内部拓扑。

在 Windows Vista 中,音频引擎会自动配置音频设备的拓扑供音频应用程序使用。 因此,应用程序很少(如果有的话)需要使用 DeviceTopology API 实现此目的。 例如,假设音频适配器包含一个输入复用器,该多路复用器可以从线路输入或麦克风捕获流,但不能同时从两个终结点设备捕获流。 假设用户已启用独占模式应用程序,以便通过共享模式应用程序抢占使用音频终结点设备,如 Exclusive-Mode Streams中所述。 如果共享模式应用程序正在从行输入录制流,而独占模式应用程序开始从麦克风录制流时,音频引擎会自动将多路复用器从线路输入切换到麦克风。 相比之下,在早期版本的 Windows(包括 Windows XP)中,此示例中的独占模式应用程序将使用 Windows 多媒体 API 中的 mixerXxx 函数遍历适配器设备的拓扑,发现多路复用器,并将多路复用器配置为选择麦克风输入。 在 Windows Vista 中,不再需要这些步骤。

但是,某些客户端可能需要显式控制无法通过 MMDevice API、WASAPI 或 EndpointVolume API 访问的音频硬件控件的类型。 对于这些客户端,DeviceTopology API 提供遍历适配器设备的拓扑,以发现和管理设备中的音频控件。 必须谨慎设计使用 DeviceTopology API 的应用程序,以避免干扰 Windows 音频策略并干扰与其他应用程序共享的音频设备的内部配置。 有关 Windows 音频策略的详细信息,请参阅 User-Mode 音频组件

DeviceTopology API 提供用于发现和管理设备拓扑中以下类型的音频控件的接口:

  • 自动增益控制
  • 低音控件
  • 输入选择器(多路复用器)
  • 音量控制
  • Midrange 控件
  • 静音控件
  • 输出选择器 (demultiplexer)
  • 峰值计量
  • 高音控件
  • 音量控制

此外,DeviceTopology API 使客户端能够查询适配器设备,以获取有关它们支持的流格式的信息。 头文件 Devicetopology.h 定义 DeviceTopology API 中的接口。

下图显示了 PCI 适配器部分的多个连接的设备拓扑示例,这些拓扑从麦克风、线路输入和 CD 播放器捕获音频。

示例

上图显示了从模拟输入到系统总线的数据路径。 以下每个设备都表示为具有 IDeviceTopology 接口的设备拓扑对象:

  • 波形捕获设备
  • 输入多路复用器设备
  • 终结点设备 A
  • 终结点设备 B

请注意,拓扑图将适配器设备(波形捕获和输入多路复用器设备)与终结点设备相结合。 通过设备之间的连接,音频数据从一台设备传递到下一台设备。 连接的每一端都是连接器(关系图中标记为 Con),通过连接数据进入或离开设备。

在关系图的左边缘,来自线输入和麦克风插孔的信号进入终结点设备。

在波形捕获设备和输入多路复用器设备中,流处理函数称为 DeviceTopology API 术语中的子单位。 以下子单元类型显示在上图中:

  • 音量控制(标记为 Vol)
  • 静音控件(标记为静音)
  • 多路复用器(或输入选择器;标记为 MUX)
  • 模拟数字转换器 (标记 ADC)

卷、静音和多路复用器子单元中的设置可由客户端控制,DeviceTopology API 为客户端提供控制接口来控制它们。 在此示例中,ADC 子单元没有控制设置。 因此,DeviceTopology API 不提供 ADC 的控制接口。

在 DeviceTopology API 的术语中,连接器和子单元属于同一常规类别-部件。 无论它们是连接器还是子单元,所有部分都提供一组常见的函数。 DeviceTopology API 实现 IPart 接口来表示连接器和子单元通用的泛型函数。 API 实现 IConnectorISubunit 接口来表示连接器和子单元的特定方面。

DeviceTopology API 从内核流式处理(KS)筛选器构造波形捕获设备和输入多路复用器设备的拓扑,音频驱动程序向作系统公开以表示这些设备。 (音频适配器驱动程序实现 IMiniportWaveXxxIMiniportTopology 接口来表示这些筛选器的硬件相关部分;有关这些接口和 KS 筛选器的详细信息,请参阅 Windows DDK 文档。

DeviceTopology API 构造简单拓扑来表示上图中的终结点设备 A 和 B。 终结点设备的设备拓扑由单个连接器组成。 此拓扑只是终结点设备的占位符,不包含用于处理音频数据的子单元。 事实上,适配器设备包含客户端应用程序用来控制音频处理的所有子单元。 终结点设备的设备拓扑主要用作探索适配器设备的设备拓扑的起点。

设备拓扑中两个部分之间的内部连接称为链接。 DeviceTopology API 提供了用于在设备拓扑中遍历从一个部分到下一部分的链接的方法。 该 API 还提供用于遍历设备拓扑之间的连接的方法。

为了开始探索一组连接的设备拓扑,客户端应用程序激活音频终结点设备的 IDeviceTopology 接口。 终结点设备中的连接器连接到音频适配器中的连接器或网络。 如果终结点连接到音频适配器上的设备,则 DeviceTopology API 中的方法允许应用程序通过获取对连接的另一端适配器设备的 IDeviceTopology 接口的引用,使应用程序能够跨从终结点到适配器的连接。 另一方面,网络没有设备拓扑。 网络连接通过管道将音频流传递给远程访问系统的客户端。

DeviceTopology API 仅提供对音频适配器中硬件设备的拓扑的访问权限。 关系图左边缘的外部设备以及右侧边缘的软件组件超出了 API 的范围。 关系图两侧虚线表示 DeviceTopology API 的限制。 客户端可以使用 API 浏览从输入插孔延伸到系统总线的数据路径,但 API 无法突破这些边界。

上图中的每个连接器都有一个关联的连接类型,指示连接线建立的连接类型。 因此,连接两端的连接器始终具有相同的连接类型。 连接类型由 ConnectorType 枚举值(Physical_External、Physical_Internal、Software_Fixed、Software_IO 或网络)指示。 输入多路复用器设备和终结点设备 A 和 B 之间的连接类型为 Physical_External,这意味着连接表示与外部设备的物理连接(换句话说,用户可访问的音频插孔)。 从内部 CD 播放器连接到模拟信号的类型为Physical_Internal,这表示与安装在系统机箱内的辅助设备的物理连接。 波形捕获设备和输入多路复用器设备之间的连接类型为Software_Fixed,指示固定且无法在软件控制下配置永久连接。 最后,与关系图右侧的系统总线的连接类型为Software_IO,这表示连接的数据 I/O 由软件控制的 DMA 引擎实现。 (该图不包括网络连接类型的示例。

客户端开始在终结点设备上遍历数据路径。 首先,客户端获取表示终结点设备的 IMMDevice 接口,如 枚举音频设备中所述。 若要获取终结点设备的 IDeviceTopology 接口,客户端调用 IMMDevice::Activate 方法,参数 iid 设置为 REFIID IID_IDeviceTopology。

在上图中的示例中,输入多路复用器设备包含来自行输入和麦克风插孔的捕获流的所有硬件控制(音量、静音和多路复用器)。 下面的代码示例演示如何从 IMMDevice 接口获取用于线路输入或麦克风的终结点设备的输入多路复用器设备的 IDevicetopology 接口:

//-----------------------------------------------------------
// The input argument to this function is a pointer to the
// IMMDevice interface of an endpoint device. The function
// outputs a pointer (counted reference) to the
// IDeviceTopology interface of the adapter device that
// connects to the endpoint device.
//-----------------------------------------------------------
#define EXIT_ON_ERROR(hres)  \
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

const IID IID_IDeviceTopology = __uuidof(IDeviceTopology);
const IID IID_IPart = __uuidof(IPart);

HRESULT GetHardwareDeviceTopology(
            IMMDevice *pEndptDev,
            IDeviceTopology **ppDevTopo)
{
    HRESULT hr = S_OK;
    IDeviceTopology *pDevTopoEndpt = NULL;
    IConnector *pConnEndpt = NULL;
    IConnector *pConnHWDev = NULL;
    IPart *pPartConn = NULL;

    // Get the endpoint device's IDeviceTopology interface.

    hr = pEndptDev->Activate(
                      IID_IDeviceTopology, CLSCTX_ALL,
                      NULL, (void**)&pDevTopoEndpt);
    EXIT_ON_ERROR(hr)

    // The device topology for an endpoint device always
    // contains just one connector (connector number 0).

    hr = pDevTopoEndpt->GetConnector(0, &pConnEndpt);
    EXIT_ON_ERROR(hr)

    // Use the connector in the endpoint device to get the
    // connector in the adapter device.

    hr = pConnEndpt->GetConnectedTo(&pConnHWDev);
    EXIT_ON_ERROR(hr)

    // Query the connector in the adapter device for
    // its IPart interface.

    hr = pConnHWDev->QueryInterface(
                         IID_IPart, (void**)&pPartConn);
    EXIT_ON_ERROR(hr)

    // Use the connector's IPart interface to get the
    // IDeviceTopology interface for the adapter device.

    hr = pPartConn->GetTopologyObject(ppDevTopo);

Exit:
    SAFE_RELEASE(pDevTopoEndpt)
    SAFE_RELEASE(pConnEndpt)
    SAFE_RELEASE(pConnHWDev)
    SAFE_RELEASE(pPartConn)

    return hr;
}

上一代码示例中的 GetHardwareDeviceTopology 函数执行以下步骤以获取输入多路复用器设备的 IDeviceTopology 接口:

  1. 调用 IMMDevice::Activate 方法以获取终结点设备的 IDeviceTopology 接口。
  2. 在上一步中获取的 IDeviceTopology 接口后,调用 IDeviceTopology::GetConnector 方法,以获取终结点设备中单个连接器(连接器号 0)的 IConnector 接口。
  3. 在上一步中获取的 IConnector 接口后,调用 IConnector::GetConnectedTo 方法,以获取输入多路复用器设备中连接器的 IConnector 接口。
  4. 查询在上一步中获取的 IConnector 接口,以获取其 IPart 接口。
  5. 在上一步中获取的 IPart 接口后,调用 IPart::GetTopologyObject 方法以获取输入多路复用器设备的 IDeviceTopology 接口。

在用户可以从上图中的麦克风录制之前,客户端应用程序必须确保多路复用器选择麦克风输入。 下面的代码示例演示客户端如何遍历麦克风的数据路径,直到找到多路复用器,然后它会将其程序设置为选择麦克风输入:

//-----------------------------------------------------------
// The input argument to this function is a pointer to the
// IMMDevice interface for a capture endpoint device. The
// function traverses the data path that extends from the
// endpoint device to the system bus (for example, PCI)
// or external bus (USB). If the function discovers a MUX
// (input selector) in the path, it selects the MUX input
// that connects to the stream from the endpoint device.
//-----------------------------------------------------------
#define EXIT_ON_ERROR(hres)  \
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

const IID IID_IDeviceTopology = __uuidof(IDeviceTopology);
const IID IID_IPart = __uuidof(IPart);
const IID IID_IConnector = __uuidof(IConnector);
const IID IID_IAudioInputSelector = __uuidof(IAudioInputSelector);

HRESULT SelectCaptureDevice(IMMDevice *pEndptDev)
{
    HRESULT hr = S_OK;
    DataFlow flow;
    IDeviceTopology *pDeviceTopology = NULL;
    IConnector *pConnFrom = NULL;
    IConnector *pConnTo = NULL;
    IPart *pPartPrev = NULL;
    IPart *pPartNext = NULL;
    IAudioInputSelector *pSelector = NULL;

    if (pEndptDev == NULL)
    {
        EXIT_ON_ERROR(hr = E_POINTER)
    }

    // Get the endpoint device's IDeviceTopology interface.
    hr = pEndptDev->Activate(
                      IID_IDeviceTopology, CLSCTX_ALL, NULL,
                      (void**)&pDeviceTopology);
    EXIT_ON_ERROR(hr)

    // The device topology for an endpoint device always
    // contains just one connector (connector number 0).
    hr = pDeviceTopology->GetConnector(0, &pConnFrom);
    SAFE_RELEASE(pDeviceTopology)
    EXIT_ON_ERROR(hr)

    // Make sure that this is a capture device.
    hr = pConnFrom->GetDataFlow(&flow);
    EXIT_ON_ERROR(hr)

    if (flow != Out)
    {
        // Error -- this is a rendering device.
        EXIT_ON_ERROR(hr = AUDCLNT_E_WRONG_ENDPOINT_TYPE)
    }

    // Outer loop: Each iteration traverses the data path
    // through a device topology starting at the input
    // connector and ending at the output connector.
    while (TRUE)
    {
        BOOL bConnected;
        hr = pConnFrom->IsConnected(&bConnected);
        EXIT_ON_ERROR(hr)

        // Does this connector connect to another device?
        if (bConnected == FALSE)
        {
            // This is the end of the data path that
            // stretches from the endpoint device to the
            // system bus or external bus. Verify that
            // the connection type is Software_IO.
            ConnectorType  connType;
            hr = pConnFrom->GetType(&connType);
            EXIT_ON_ERROR(hr)

            if (connType == Software_IO)
            {
                break;  // finished
            }
            EXIT_ON_ERROR(hr = E_FAIL)
        }

        // Get the connector in the next device topology,
        // which lies on the other side of the connection.
        hr = pConnFrom->GetConnectedTo(&pConnTo);
        EXIT_ON_ERROR(hr)
        SAFE_RELEASE(pConnFrom)

        // Get the connector's IPart interface.
        hr = pConnTo->QueryInterface(
                        IID_IPart, (void**)&pPartPrev);
        EXIT_ON_ERROR(hr)
        SAFE_RELEASE(pConnTo)

        // Inner loop: Each iteration traverses one link in a
        // device topology and looks for input multiplexers.
        while (TRUE)
        {
            PartType parttype;
            UINT localId;
            IPartsList *pParts;

            // Follow downstream link to next part.
            hr = pPartPrev->EnumPartsOutgoing(&pParts);
            EXIT_ON_ERROR(hr)

            hr = pParts->GetPart(0, &pPartNext);
            pParts->Release();
            EXIT_ON_ERROR(hr)

            hr = pPartNext->GetPartType(&parttype);
            EXIT_ON_ERROR(hr)

            if (parttype == Connector)
            {
                // We've reached the output connector that
                // lies at the end of this device topology.
                hr = pPartNext->QueryInterface(
                                  IID_IConnector,
                                  (void**)&pConnFrom);
                EXIT_ON_ERROR(hr)

                SAFE_RELEASE(pPartPrev)
                SAFE_RELEASE(pPartNext)
                break;
            }

            // Failure of the following call means only that
            // the part is not a MUX (input selector).
            hr = pPartNext->Activate(
                              CLSCTX_ALL,
                              IID_IAudioInputSelector,
                              (void**)&pSelector);
            if (hr == S_OK)
            {
                // We found a MUX (input selector), so select
                // the input from our endpoint device.
                hr = pPartPrev->GetLocalId(&localId);
                EXIT_ON_ERROR(hr)

                hr = pSelector->SetSelection(localId, NULL);
                EXIT_ON_ERROR(hr)

                SAFE_RELEASE(pSelector)
            }

            SAFE_RELEASE(pPartPrev)
            pPartPrev = pPartNext;
            pPartNext = NULL;
        }
    }

Exit:
    SAFE_RELEASE(pConnFrom)
    SAFE_RELEASE(pConnTo)
    SAFE_RELEASE(pPartPrev)
    SAFE_RELEASE(pPartNext)
    SAFE_RELEASE(pSelector)
    return hr;
}

DeviceTopology API 实现 IAudioInputSelector 接口来封装多路复用器,例如上图中的复用器。 (IAudioOutputSelector 接口封装了 demultiplexer。)在前面的代码示例中,SelectCaptureDevice 函数的内部循环查询它发现子单元是否为多路复用器的每个子单元。 如果子单元是多路复用器,则该函数将调用 IAudioInputSelector::SetSelection 方法,以从终结点设备选择连接到流的输入。

在前面的代码示例中,外部循环的每个迭代都会遍历一个设备拓扑。 在上图中遍历设备拓扑时,第一次迭代将遍历输入多路复用器设备,第二次迭代遍历波形捕获设备。 当函数到达关系图右边缘的连接器时,该函数将终止。 当函数检测到具有Software_IO连接类型的连接器时,将终止。 此连接类型标识适配器设备连接到系统总线的点。

对上述代码示例中的 IPart::GetPartType 方法的调用获取 IPartType 枚举值,该值指示当前部件是连接器还是音频处理子单元。

前面的代码示例中的内部循环通过调用 IPart::EnumPartsOutgoing 方法跨一个部分到下一个部分的链接。 (还有一个 IPart::EnumPartsIncoming 方法,用于单步执行相反的方向。此方法检索 IPartsList 对象,该对象包含所有传出部件的列表。 但是,SelectCaptureDevice 函数期望在捕获设备中遇到的任何部分始终只有一个传出部分。 因此,后续调用 IPartsList::GetPart 始终请求列表中的第一部分,第 0 部分,因为该函数假定这是列表中唯一的一部分。

如果 SelectCaptureDevice 函数遇到该假设无效的拓扑,该函数可能无法正确配置设备。 为了避免此类故障,函数的更常规用途版本可能会执行以下作:

  • 调用 IPartsList::GetCount 方法来确定传出部件的数量。
  • 对于每个传出部件,请调用 IPartsList::GetPart,开始遍历来自该部件的数据路径。

部分(但不一定全部)具有客户端可以设置或获取的相关硬件控制。 特定部件可能具有零个、一个或多个硬件控件。 硬件控件由以下接口对表示:

  • 泛型控件接口,IControlInterface,它具有所有硬件控件通用的方法。
  • 特定于函数的接口(例如,IAudioVolumeLevel),用于公开特定类型硬件控件(例如卷控件)的控制参数。

若要枚举部件的硬件控件,客户端首先调用 IPart::GetControlInterfaceCount 方法来确定与部件关联的硬件控件数。 接下来,客户端对 IPart::GetControlInterface 方法进行一系列调用,以获取每个硬件控件的 IControlInterface 接口。 最后,客户端通过调用 IControlInterface::GetIID 方法来获取每个硬件控件的特定于函数的接口,以获取接口 ID。 客户端使用此 ID 调用 IPart::Activate 方法以获取特定于函数的接口。

作为连接器的部件可能支持以下特定于函数的控件接口之一:

属于子单元的部件可能支持以下一个或多个特定于函数的控件接口:

仅当基础硬件控件具有特定于设备的控件值,并且该控件无法充分表示上述列表中任何其他特定于函数的接口时,部件才支持 IDeviceSpecificProperty 接口。 通常,特定于设备的属性仅适用于可以从部件类型、部件子类型和部件名称等信息推断属性值的含义的客户端。 客户端可以通过调用 IPart::GetPartTypeIPart::GetSubTypeIPart::GetName 方法来获取此信息。

编程指南