设备拓扑

DeviceTopology API 可让客户端控制音频适配器的各种内部功能,这些功能无法通过 MMDevice APIWASAPIEndpointVolume API 来访问。

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

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

不过,某些客户端可能需要对音频硬件控件类型进行显式控制,而这些控件无法通过 MMDevice API、WASAPI 或 EndpointVolume API 来访问。 对于这些客户端,DeviceTopology API 提供了遍历适配器设备拓扑结构的能力,以发现和管理设备中的音频控件。 在设计使用 DeviceTopology API 的应用程序时必须小心谨慎,以避免干扰 Windows 音频策略和与其他应用程序共享的音频设备的内部配置。 有关 Windows 音频策略的更多信息,请参阅用户模式音频组件

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

  • 自动增益控制
  • 低音控件
  • 输入选择器(多路复用器)
  • 响度控件
  • 中音控件
  • 静音控件
  • 输出选择器(信号分离器)
  • 峰值计量
  • 高音控件
  • 音量控件

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

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

example of four connected device topologies

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

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

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

在图表的左侧,来自线路输入和麦克风插孔的信号会进入终结点设备。

波形捕获设备和输入多路复用器设备内部都具有流处理功能,按照 DeviceTopology API 的术语,这些功能被称为子单元。 上图显示了以下几种子单元:

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

客户端可以控制音量、静音和多路复用器子单元中的设置,DeviceTopology API 为客户端提供了控制这些设置的控制接口。 在此示例中,ADC 子单元没有控制设置。 因此,DeviceTopology API 没有为 ADC 提供控制接口。

在 DeviceTopology API 的术语中,连接器和子单元属于同一大类 — 部件。 无论是连接器还是子单元,所有部件都提供一组常用的函数。 DeviceTopology API 实现了一个 IPart 接口,用于表示连接器和子单元常见的通用函数。 该 API 实现了 IConnectorISubunit 接口,以表示连接器和子单元的特定方面。

DeviceTopology API 通过内核流 (KS) 筛选器来构建波形捕获设备和输入多路复用器设备的拓扑结构,而音频驱动程序会将这些筛选器公开给操作系统以表示这些设备。 (音频适配器驱动程序实现了 IMiniportWaveXxx and IMiniportTopology 接口,用于表示这些筛选器中与硬件相关的部分;有关这些接口和 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 或 Network。 输入多路复用器设备与终结点设备 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 接口封装一个信号分离器。)在前面的代码示例中,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 方法来获取这些信息。

编程指南