ACX 流式处理

本主题讨论 ACX 流式处理和关联缓冲,这对无故障音频体验至关重要。 其中介绍了驱动程序用于传达流状态和管理流缓冲区的机制。 有关常用 ACX 音频术语列表和 ACX 简介,请参阅 ACX 音频类扩展概述

ACX 流式处理类型

AcxStream 表示特定线路硬件上的音频流。 AcxStream 可以聚合一个或多个类似 AcxElements 的对象。

ACX 框架支持两种流类型。 第一个流类型 RT 数据包流支持分配 RT 数据包,以及使用 RT 数据包将音频数据传输到设备硬件或从设备硬件传输音频数据,以及流状态转换。 第二种流类型(基本流)仅支持流状态转换。

在单线路终结点中,线路必须是创建 RT 数据包流的流式传输线路。 如果连接两条或更多条线路来创建终结点,则终结点中的第一条线路是流式传输线路,并创建 RT 数据包流;连接的线路将创建基本流,以接收与流状态转换相关的事件。

有关详细信息,请参阅 ACX 对象摘要中的 ACX 流。 流的 DDI 在 acxstreams.h 标头中定义。

ACX 流式处理通信堆栈

ACX 流式处理有两种类型的通信。 一个通信路径用于控制流式处理行为,适用于“开始”、“创建”和“分配”等命令,这些命令将使用标准 ACX 通信。 ACX 框架使用 IO 队列,并使用队列沿 WDF 请求传递。 可通过使用 Evt 回调和 ACX 函数在实际驱动程序代码中隐藏队列行为,但该驱动程序也有机会预处理所有 WDF 请求。

第二个通信路径较为有趣,用于音频流式处理信号。 这涉及到在驱动程序完成数据包处理后通知驱动程序何时准备好数据包并接收数据。 

流式处理信号的主要要求:

  • 支持无故障播放
    • 低延迟
    • 任何必要的锁定都仅限于有问题的流
  • 驱动程序开发人员可轻松使用

若要与驱动程序通信以发出流式处理状态信号,ACX 应使用具有共享缓冲区和直接 IRP 调用的事件。 接下来将介绍这些内容。

共享缓冲区

若要从驱动程序与客户端通信,请使用共享缓冲区和事件。 这可确保客户端不需要等待或轮询,客户端可以确定继续流式处理所需的所有内容,同时减少或消除直接 IRP 调用的需求。

设备驱动程序使用共享缓冲区与要从中呈现数据包或将数据包捕获到的客户端通信。 此共享缓冲区包括最后一个已完成数据包的数据包计数(基于 1),以及完成时间的 QPC (QueryPerformanceCounter) 值。 对于设备驱动程序,必须调用 AcxRtStreamNotifyPacketComplete 来指示此信息。 当设备驱动程序调用 AcxRtStreamNotifyPacketComplete 时,ACX 框架将使用新的数据包计数和 QPC 更新共享缓冲区,并发出与客户端共享的事件信号,以指示客户端可以读取新的数据包计数。

直接 IRP 调用

若要从客户端与驱动程序通信,将使用直接 IRP 调用。 这将减少确保 WDF 请求得到及时处理并且证明在现有体系结构中效果很好的复杂性。

客户端随时可以请求当前数据包计数,或指示设备驱动程序的当前数据包计数。 这些请求将调用 EvtAcxStreamGetCurrentPacketEvtAcxStreamSetRenderPacket 设备驱动程序事件处理程序。 客户端还可以请求当前捕获数据包,该数据包将调用 EvtAcxStreamGetCapturePacket 设备驱动程序事件处理程序。

与 PortCls 的相似之处

ACX 使用的直接 IRP 调用和共享缓冲区的组合类似于在 PortCls 中传达缓冲区完成处理的方式。 IRP 非常相似,共享缓冲区引入了驱动程序在不依赖于 IRP 的情况下直接传达数据包计数和计时的功能。   驱动程序需要确保它们不需访问流控制路径中使用的锁定,这是防止故障所必需的。 

低功率播放的大型缓冲区支持

若要减少播放媒体内容时消耗的电量,请务必减少 APU 在高功率状态下花费的时间量。 由于普通音频播放使用 10 毫秒缓冲区,因此 APU 始终需要处于活动状态。 为了让 APU 有所需的时间来降低状态,允许 ACX 驱动程序在 1-2 秒的大小范围内播发对明显更大的缓冲区支持。 这意味着 APU 可以每隔 1-2 秒唤醒一次,以最大速度执行所需的操作,以准备下一个 1-2 秒的缓冲区,然后转到尽可能低的功率状态,直到需要下一个缓冲区。 

在现有流式处理模型中,通过卸载播放支持低功率播放。 通过在终结点的波次筛选器上公开 AudioEngine 节点,音频驱动程序可播发对卸载播放的支持。 AudioEngine 节点提供了一种控制驱动程序用于通过所需处理从大型缓冲区呈现音频的 DSP 引擎的方法。

AudioEngine 节点提供以下工具:

  • 音频引擎描述,用于指示音频堆栈波次筛选器上的哪个引脚提供卸载和环回支持(以及主机播放支持)。 
  • 缓冲区大小范围,用于指示音频堆栈卸载可以支持的最小和最大缓冲区大小。 播放。 缓冲区大小范围可以根据系统活动动态更改。 
  • 格式支持,包括支持的格式、当前设备混合格式和设备格式。 
  • 音量,包括渐变支持,因为使用较大的缓冲区软件时,音量将不会响应。
  • 环回保护,用于指示驱动程序在一个或多个卸载流包含受保护的内容时将 AudioEngine 环回引脚静音。 
  • 全局 FX 状态,用于在 AudioEngine 上启用或禁用 GFX。 

在卸载引脚上创建流时,流支持音量、本地 FX 和环回保护。 

使用 ACX 的低功率播放

ACX 框架使用相同的模型进行低功率播放。 驱动程序会为主机、卸载和环回流创建三个单独的 ACXPIN 对象,以及一个 ACXAUDIOENGINE 元素,用于描述将这些引脚中的哪一个用于主机、卸载和环回。 在线路创建期间,驱动程序会将引脚和 ACXAUDIOENGINE 元素添加到 ACXCIRCUIT。

卸载流创建

驱动程序还将向为卸载创建的流添加 ACXAUDIOENGINE 元素,以允许控制音量、静音和峰值计。

流式处理图

此图显示了多栈 ACX 驱动程序。

显示顶部带有内核流式处理接口的 DSP、CODEC 和 AMP 框的示意图。

每个 ACX 驱动程序都控制音频硬件的单独部分,并且可以由不同的供应商提供。 ACX 提供兼容的内核流式处理接口,以允许应用程序按原样运行。

流引脚

每个 ACXCIRCUIT 至少都有一个接收器引脚和一个源引脚。 ACX 框架使用这些引脚来公开线路与音频堆栈的连接。 对于呈现线路,源引脚用于控制根据线路创建的任何流的呈现行为。 对于捕获线路,接收器引脚用于控制根据线路创建的任何流的捕获行为。   ACXPIN 是用于控制音频路径中的流式处理的对象。 流式处理 ACXCIRCUIT 负责在线路创建时为终结点音频路径创建适当的 ACXPIN 对象,并向 ACX 注册 ACXPIN。 ACXCIRCUIT 只需创建呈现或捕获引脚或者线路引脚;ACX 框架将创建连接到线路并与线路通信所需的其他引脚。   

流式传输线路

当终结点由单个线路组成时,该线路是流式传输线路。

当终结点由一个或多个设备驱动程序创建的多个线路组成时,将按描述组合终结点的 ACXCOMPOSITETEMPLATE 确定的特定顺序连接线路。 终结点中的第一条线路是终结点的流式传输线路。

流式处理线路应使用 AcxRtStreamCreate 创建 RT 数据包流,以响应 EvtAcxCircuitCreateStream。 使用 AcxRtStreamCreate 创建的 ACXSTREAM 将允许流式传输线路驱动程序分配用于流式传输的缓冲区,并控制流式处理流,以响应客户端和硬件需求。

终结点中的以下线路应使用 AcxStreamCreate 来创建基本流,以响应 EvtAcxCircuitCreateStream。 通过以下线路使用 AcxStreamCreate 创建的 ACXSTREAM 对象将允许驱动程序配置硬件,以响应流状态更改,例如暂停或运行。

流式传输 ACXCIRCUIT 是接收创建流的请求的第一条线路。 请求包括设备、引脚和数据格式(包括模式)。

音频路径中的每个 ACXCIRCUIT 都将创建一个 ACXSTREAM 对象,该对象表示线路的流实例。 ACX 框架会将 ACXSTREAM 对象链接在一起(与 ACXCIRCUIT 对象链接的方式大致相同)。 

上游和下游线路

流创建从流线路开始,并按照线路的连接顺序转发到每个下游线路。 将在使用等于 AcxPinCommunicationNone 的 Communication 创建的桥接引脚之间建立连接。 如果驱动程序在线路创建时未添加桥接引脚,ACX 框架将为线路创建一个或多个桥接引脚。

对于从流式传输线路开始的每个线路,AcxPinTypeSource 桥接引脚都将连接到下一条下游线路。 最终线路将有一个终结点引脚,描述音频终结点硬件(例如终结点是麦克风还是扬声器,以及是否已插入插孔)。

对于流式传输线路后面的每个线路,AcxPinTypeSink 桥接引脚都将连接到下一个上游线路。

流格式协商

通过向用于使用 AcxPinAssignModeDataFormatListAcxPinGetRawDataFormatList 创建流的 ACXPIN 添加每种模式支持的格式,驱动程序可播发流创建支持的格式。 对于多线路终结点,ACXSTREAMBRIDGE 可用于协调 ACX 线路之间的模式和格式支持。 终结点支持的流格式由流式处理线路创建的流式处理 ACXPIN 决定。 以下线路使用的格式由终结点中上一个线路的桥接引脚确定。

默认情况下,ACX 框架将在多线路终结点中的每个线路之间创建 ACXSTREAMBRIDGE。 将流创建请求转发给下游线路时,默认 ACXSTREAMBRIDGE 将使用上游线路的桥接引脚的 RAW 模式默认格式。 如果上游线路的桥接引脚没有格式,则将使用原始流格式。 如果下游线路的已连接引脚不支持所使用的格式,则流创建将失败。

如果设备线路正在执行流格式更改,设备驱动程序应将下游格式添加到下游桥接引脚。  

流创建

流创建的第一步是为终结点音频路径中的每个 ACXCIRCUIT 创建 ACXSTREAM 实例。 ACX 将调用每个线路的 EvtAcxCircuitCreateStream。 ACX 将从头线路开始,并按顺序调用每个线路的 EvtAcxCircuitCreateStream,以尾线路结尾。 通过为流桥接指定 AcxStreamBridgeInvertChangeStateSequence 标志(在 ACX_STREAM_BRIDGE_CONFIG_FLAGS 中定义),可以反转顺序。 所有线路都创建了流对象后,流对象将处理流式处理逻辑。

通过调用在头线路创建期间指定的 EvtAcxCircuitCreateStream,可将流创建请求发送到作为头线路拓扑生成的一部分生成的相应 PIN。 

流式处理线路是最初处理流创建请求的上游线路。

  • 它会更新 ACXSTREAM_INIT 结构,从而分配 AcxStreamCallbacks 和 AcxRtStreamCallbacks
  • 它会使用 AcxRtStreamCreate 创建 ACXSTREAM 对象
  • 它会创建任何特定于流的元素(例如 ACXVOLUME 或 ACXAUDIOENGINE)
  • 它会将元素添加到 ACXSTREAM 对象
  • 它会将已创建的 ACXSTREAM 对象返回到 ACX 框架

接着,ACX 将流创建转发给下一条下游线路。

  • 它会更新ACXSTREAM_INIT结构,从而分配 AcxStreamCallbacks
  • 它会使用 AcxStreamCreate 创建 ACXSTREAM 对象
  • 它会创建任何特定于流的元素
  • 它会将元素添加到 ACXSTREAM 对象
  • 它会将已创建的 ACXSTREAM 对象返回到 ACX 框架

音频路径中的线路之间的通信声道将使用 ACXTARGETSTREAM 对象。 在此示例中,每个线路将有权访问其前面的线路的 IO 队列,以及终结点音频路径中其后面的线路。 此外,终结点音频路径是线性和双向的。 ACX 框架会执行实际的 IO 队列处理。    创建 ACXSTREAM 对象时,每个线路都可以将上下文信息添加到 ACXSTREAM 对象,以存储和跟踪流的专用数据。

呈现流示例

在由三个线路组成的终结点音频路径上创建呈现流:DSP、CODEC 和 AMP。 DSP 线路充当流式传输线路,并提供了 EvtAcxPinCreateStream 处理程序。 DSP 线路还充当筛选器线路:根据流模式和配置,它可将信号处理应用于音频数据。 CODEC 线路表示 DAC,从而提供音频接收器功能。 AMP 线路表示 DAC 和扬声器之间的模拟硬件。 AMP 线路可以处理插孔检测或其他终结点硬件详细信息。

  1. AudioKSE 会调用 NtCreateFile 来创建流。
  2. 此筛选器通过 ACX,最后会使用引脚、数据格式(包括模式)和设备信息调用 DSP 线路的 EvtAcxPinCreateStream。 
  3. DSP 线路会验证数据格式信息,以确保它可以处理创建的流。 
  4. DSP 线路会创建 ACXSTREAM 对象来表示流。 
  5. DSP 线路会分配专用上下文结构,并将其与 ACXSTREAM 相关联。 
  6. DSP 线路会将执行流返回到 ACX 框架,然后调用终结点音频路径(CODEC 线路)中的下一个线路。 
  7. CODEC 线路会验证数据格式信息,以确认它可以处理数据呈现。 
  8. CODEC 线路会分配专用上下文结构,并将其与 ACXSTREAM 相关联。 
  9. CODEC 线路会将自身作为流接收器添加到 ACXSTREAM。
  10. CODEC 线路会将执行流返回到 ACX 框架,然后调用终结点音频路径(AMP 线路)中的下一个线路。 
  11. AMP 线路会分配专用上下文结构,并将其与 ACXSTREAM 相关联。 
  12. AMP 线路会将执行流返回到 ACX 框架。 此时,流创建已完成。 

大型缓冲区流

大型缓冲区流可在 ACXCIRCUIT 的 ACXAUDIOENGINE 元素为卸载指定的 ACXPIN 上创建。

为了支持卸载流,设备驱动程序应在流式传输线路创建过程中执行以下操作:

  1. 创建主机、卸载和环回 ACXPIN 对象,并将其添加到 ACXCIRCUIT。
  2. 创建 ACXVOLUME、ACXMUTE 和 ACXPEAKMETER 元素。 不会将这些内容直接添加到 ACXCIRCUIT。
  3. 初始化 ACX_AUDIOENGINE_CONFIG structure 结构,从而分配 HostPin、OffloadPin、LoopbackPin、VolumeElement、MuteElement 和 PeakMeterElement 对象。
  4. 创建 ACXAUDIOENGINE 元素。

驱动程序需要在卸载引脚上创建流时执行类似的步骤,以添加 ACXSTREAMAUDIOENGINE 元素。

流资源分配

ACX 的流式处理模型基于数据包,并且支持一个或两个流数据包。 将为用于流式传输线路的呈现或捕获 ACXPIN 提供分配流中使用的内存数据包的请求。 若要支持重新平衡,分配的内存必须是系统内存,而不是映射到系统中的设备内存。 驱动程序可以使用现有的 WDF 函数来执行分配,并将返回指向缓冲区分配的指针数组。 如果驱动程序需要单个连续块,它可以将这两个数据包都分配为单个缓冲区,并将指向缓冲区偏移量的指针作为第二个数据包返回。

如果分配了单个数据包,则必须对数据包进行页面对齐,并将数据包映射到用户模式两次:

| packet 0 | packet 0 |

这使 GetBuffer 能够返回指向单个连续内存缓冲区的指针,该缓冲区可跨越缓冲区的末尾到开头,而无需应用程序处理包装内存访问。 

如果分配了两个数据包,则会将其映射到用户模式:

| packet 0 | packet 1 |

初始 ACX 数据包流式处理时,仅在开头分配了两个数据包。 执行分配和映射后,客户端虚拟内存映射将保持有效状态,而不会更改流的生存期。 有一个与流关联的事件,用于指示两个数据包的数据包完成情况。 ACX 框架还将使用共享缓冲区来传达使用事件完成的数据包。  

大型缓冲区流数据包大小

公开对大型缓冲区的支持时,驱动程序还会提供一个回调,用于确定大型缓冲区播放的最小和最大数据包大小。   用于流缓冲区分配的数据包大小可根据最小值和最大值确定。

由于最小和最大缓冲区大小可能可变,因此如果最小值和最大值已更改,驱动程序可能会使得数据包分配调用失败。

指定 ACX 缓冲区约束

若要指定 ACX 缓冲区约束,ACX 驱动程序可以使用 KS/PortCls 属性设置,即 KSAUDIO_PACKETSIZE_CONSTRAINTS2KSAUDIO_PACKETSIZE_PROCESSINGMODE_CONSTRAINT 结构

以下代码示例演示如何为不同的信号处理模式设置 WaveRT 缓冲区的缓冲区大小约束。

//
// Describe buffer size constraints for WaveRT buffers
// Note: 10msec for each of the Modes is the default system behavior.
//
static struct
{
    KSAUDIO_PACKETSIZE_CONSTRAINTS2                 TransportPacketConstraints;         // 1
    KSAUDIO_PACKETSIZE_PROCESSINGMODE_CONSTRAINT    AdditionalProcessingConstraints[4]; // + 4 = 5
} DspR_RtPacketSizeConstraints =
{
    {
        10 * HNSTIME_PER_MILLISECOND,                           // 10 ms minimum processing interval
        FILE_BYTE_ALIGNMENT,                                    // 1 byte packet size alignment
        0,                                                      // no maximum packet size constraint
        5,                                                      // 5 processing constraints follow
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_RAW,              // constraint for raw processing mode
            0,                                                  // NA samples per processing frame
            10 * HNSTIME_PER_MILLISECOND,                       // 100000 hns (10ms) per processing frame
        },
    },
    {
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_DEFAULT,          // constraint for default processing mode
            0,                                                  // NA samples per processing frame
            10 * HNSTIME_PER_MILLISECOND,                       // 100000 hns (10ms) per processing frame
        },
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_COMMUNICATIONS,   // constraint for movie communications mode
            0,                                                  // NA samples per processing frame
            10 * HNSTIME_PER_MILLISECOND,                       // 100000 hns (10ms) per processing frame
        },
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_MEDIA,            // constraint for default media mode
            0,                                                  // NA samples per processing frame
            10 * HNSTIME_PER_MILLISECOND,                       // 100000 hns (10ms) per processing frame
        },
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_MOVIE,            // constraint for movie movie mode
            0,                                                  // NA samples per processing frame
            10 * HNSTIME_PER_MILLISECOND,                       // 100000 hns (10ms) per processing frame
        },
    }
};

DSP_DEVPROPERTY 结构用于存储约束。

typedef struct _DSP_DEVPROPERTY {
    const DEVPROPKEY   *PropertyKey;
    DEVPROPTYPE Type;
    ULONG BufferSize;
    __field_bcount_opt(BufferSize) PVOID Buffer;
} DSP_DEVPROPERTY, PDSP_DEVPROPERTY;

将创建这些结构的数组。

const DSP_DEVPROPERTY DspR_InterfaceProperties[] =
{
    {
        &DEVPKEY_KsAudio_PacketSize_Constraints2,       // Key
        DEVPROP_TYPE_BINARY,                            // Type
        sizeof(DspR_RtPacketSizeConstraints),           // BufferSize
        &DspR_RtPacketSizeConstraints,                  // Buffer
    },
};

稍后在 EvtCircuitCompositeCircuitInitialize 函数中,AddPropertyToCircuitInterface 帮助程序函数用于将接口属性数组添加到线路。

   // Set RT buffer constraints.
    //
    status = AddPropertyToCircuitInterface(Circuit, ARRAYSIZE(DspC_InterfaceProperties), DspC_InterfaceProperties);

AddPropertyToCircuitInterface 帮助程序函数会采用线路的 AcxCircuitGetSymbolicLinkName,然后调用 IoGetDeviceInterfaceAlias 来找到线路使用的音频接口。

接着,SetDeviceInterfacePropertyDataMultiple 函数会调用 IoSetDeviceInterfacePropertyData 函数,以修改设备接口属性的当前值,即 ACXCIRCUIT 音频接口上的 KS 音频属性值。

PAGED_CODE_SEG
NTSTATUS AddPropertyToCircuitInterface(
    _In_ ACXCIRCUIT                                         Circuit,
    _In_ ULONG                                              PropertyCount,
    _In_reads_opt_(PropertyCount) const DSP_DEVPROPERTY   * Properties
)
{
    PAGED_CODE();

    NTSTATUS        status      = STATUS_UNSUCCESSFUL;
    UNICODE_STRING  acxLink     = {0};
    UNICODE_STRING  audioLink   = {0};
    WDFSTRING       wdfLink     = AcxCircuitGetSymbolicLinkName(Circuit);
    bool            freeStr     = false;

    // Get the underline unicode string.
    WdfStringGetUnicodeString(wdfLink, &acxLink);

    // Make sure there is a string.
    if (!acxLink.Length || !acxLink.Buffer)
    {
        status = STATUS_INVALID_DEVICE_STATE;
        DrvLogError(g_BthLeVDspLog, FLAG_INIT,
            L"AcxCircuitGetSymbolicLinkName failed, Circuit: %p, %!STATUS!",
            Circuit, status);
        goto exit;
    }

    // Get the audio interface.
    status = IoGetDeviceInterfaceAlias(&acxLink, &KSCATEGORY_AUDIO, &audioLink);
    if (!NT_SUCCESS(status))
    {
        DrvLogError(g_BthLeVDspLog, FLAG_INIT,
            L"IoGetDeviceInterfaceAlias failed, Circuit: %p, symbolic link name: %wZ, %!STATUS!",
            Circuit, &acxLink, status);
        goto exit;
    }

    freeStr = true;

    // Set specified properties on the audio interface for the ACXCIRCUIT.
    status = SetDeviceInterfacePropertyDataMultiple(&audioLink, PropertyCount, Properties);
    if (!NT_SUCCESS(status))
    {
        DrvLogError(g_BthLeVDspLog, FLAG_INIT,
            L"SetDeviceInterfacePropertyDataMultiple failed, Circuit: %p, symbolic link name: %wZ, %!STATUS!",
            Circuit, &audioLink, status);
        goto exit;
    }

    status = STATUS_SUCCESS;

exit:

    if (freeStr)
    {
        RtlFreeUnicodeString(&audioLink);
        freeStr = false;
    }

    return status;
}

流状态更改

发生流状态更改时,流终结点音频路径中的每个流对象都将从 ACX 框架收到通知事件。 发生这种情况的顺序取决于流的状态更改和流动方向。

  • 对于从不太活动的状态变为较为活动的状态的呈现流,流式处理线路(已注册接收器)将首先收到事件。 处理事件后,终结点音频路径中的下一个线路将收到该事件。

  • 对于从较为活动的状态变为不太活动的状态的呈现流,流式处理线路将最后收到事件。 

  • 对于从不太活动的状态变为较为活动的状态的捕获流,流式处理线路将最后收到事件。 

  • 对于从较为活动的状态变为不太活动的状态的捕获流,流式处理线路将先收到事件。 

上述排序是 ACX 框架提供的默认顺序。 通过在创建驱动程序添加到流式处理线路的 ACXSTREAMBRIDGE 时设置 AcxStreamBridgeInvertChangeStateSequence(在 ACX_STREAM_BRIDGE_CONFIG_FLAGS 中定义),驱动程序可以请求相反的行为。  

流式处理音频数据

创建流并分配适当的缓冲区后,流处于等待流启动的暂停状态。 当客户端将流置于“播放”状态时,ACX 框架将调用与流关联的所有 ACXSTREAM 对象,以指示流状态为“播放”。 接着,将 ACXPIN 置于“播放”状态,此时数据将开始流动。 

呈现音频数据

创建流并分配资源后,应用程序将在流上调用“开始”以开始播放。 请注意,应用程序应在启动流之前调用 GetBuffer/ReleaseBuffer,以确保将立即开始播放的第一个数据包将具有有效的音频数据。 

客户端首先预先滚动缓冲区。 当客户端调用 ReleaseBuffer 时,这将转换为 AudioKSE 中的调用,这将调用 ACX 层,这将在活动 ACXSTREAM 上调用 EvtAcxStreamSetRenderPacket。 该属性将包括数据包索引(基于 0),如果适用,则为包含当前数据包中流末尾的字节偏移量的 EOS 标志。    流式传输线路使用数据包完成后,将触发缓冲区完成通知,该通知将释放等待使用呈现音频数据填充下一个数据包的客户端。 

支持计时器驱动的流式处理模式,并在调用驱动程序的 EvtAcxStreamAllocateRtPackets 回调时使用 PacketCount 值 1 指示。

捕获音频数据

创建流并分配资源后,应用程序将在流上调用“开始”以开始播放。 

流正在运行时,源线路会使用音频数据填充捕获数据包。 填充第一个数据包后,源线路会将数据包释放到 ACX 框架。 此时,ACX 框架会发出流通知事件信号。 

发出流通知信号后,客户端可以发送 KSPROPERTY_RTAUDIO_GETREADPACKET,以获取已完成捕获的数据包的索引(基于 0)。 当客户端发送 GETCAPTUREPACKET 时,驱动程序可以假定所有以前的数据包都已处理,并且可用于填充。 

对于突发捕获,源线路可以在调用 GETREADPACKET 后,立即将新数据包释放到 ACX 框架。

客户端还可以使用 KSPROPERTY_RTAUDIO_PACKETVREGISTER,获取指向流的 RTAUDIO_PACKETVREGISTER 结构的指针。 在发出数据包完成信号之前,ACX 框架将更新此结构。

旧版 KS 内核流式处理行为

在某些情况下,例如驱动程序实现突发捕获(如在关键字检测工具实现中),需要使用旧版内核流式处理数据包处理行为,而不是 PacketVRegister。 若要使用以前基于数据包的行为,驱动程序应为 KSPROPERTY_RTAUDIO_PACKETVREGISTER 返回 STATUS_NOT_SUPPORTED。

以下示例演示如何在 ACXSTREAM 的 AcxStreamInitAssignAcxRequestPreprocessCallback 中执行此操作。 有关详细信息,请参阅 AcxStreamDispatchAcxRequest

Circuit_EvtStreamRequestPreprocess(
    _In_  ACXOBJECT  Object,
    _In_  ACXCONTEXT DriverContext,
    _In_  WDFREQUEST Request)
{
    ACX_REQUEST_PARAMETERS params;
    PCIRCUIT_STREAM_CONTEXT streamCtx;

    streamCtx = GetCircuitStreamContext(Object);
    // The driver would define the pin type to track which pin is the keyword pin.
    // The driver would add this to the driver-defined context when the stream is created.
    // The driver would use AcxStreamInitAssignAcxRequestPreprocessCallback to set
    // the Circuit_EvtStreamRequestPreprocess callback for the stream.
    if (streamCtx && streamCtx->PinType == CapturePinTypeKeyword)
    {
        if (IsEqualGUID(params.Parameters.Property.Set, KSPROPSETID_RtAudio) &&
            params.Parameters.Property.Id == KSPROPERTY_RTAUDIO_PACKETVREGISTER)
        {
            status = STATUS_NOT_SUPPORTED;
            outDataCb = 0;

            WdfRequestCompleteWithInformation(Request, status, outDataCb);
            return;
        }
    }

    (VOID)AcxStreamDispatchAcxRequest((ACXSTREAM)Object, Request);
}

流位置

ACX 框架将调用 EvtAcxStreamGetPresentationPosition 回调,以获取当前流位置。 当前流位置将包括 PlayOffset 和 WriteOffset。 

WaveRT 流式处理模型允许音频驱动程序向客户端公开 HW 位置寄存器。 ACX 流式处理模型不支持公开任何 HW 寄存器,因为这些寄存器将防止发生重新平衡。 

每次流式处理线路完成数据包时,它都会使用基于 0 的数据包索引调用 AcxRtStreamNotifyPacketComplete,以及采用尽可能接近数据包完成的 QPC 值(例如,中断服务例程可以计算 QPC 值)。 此信息通过 KSPROPERTY_RTAUDIO_PACKETVREGISTER 提供给客户端,它会返回指向包含 CompletedPacketCount、CompletedPacketQPC 的结构的指针,以及合并这两项的值(这允许客户端确保 CompletedPacketCount 和 CompletedPacketQPC 来自同一数据包)。  

流状态转换

创建流后,ACX 会使用以下回调将流转换为不同的状态:

  • EvtAcxStreamPrepareHardware 会将流从 AcxStreamStateStop 状态转换为 AcxStreamStatePause 状态。 驱动程序在收到 EvtAcxStreamPrepareHardware 时应保留所需的硬件,例如 DMA 引擎。
  • EvtAcxStreamRun 会将流从 AcxStreamStatePause 状态转换为 AcxStreamStateRun 状态。
  • EvtAcxStreamPause 会将流从 AcxStreamStateRun 状态转换为 AcxStreamStatePause 状态。
  • EvtAcxStreamReleaseHardware 会将流从 AcxStreamStatePause 状态转换为 AcxStreamStateStop 状态。 驱动程序应在收到 EvtAcxStreamReleaseHardware 时释放所需的硬件,例如 DMA 引擎。

流在收到 EvtAcxStreamReleaseHardware 回调后,会收到 EvtAcxStreamPrepareHardware 回调。 这会将流转换回 AcxStreamStatePause 状态。

使用 EvtAcxStreamAllocateRtPacket 进行数据包分配通常在首次调用 EvtAcxStreamPrepareHardware 之前进行。 在上次调用 EvtAcxStreamReleaseHardware 后,通常将使用 EvtAcxStreamFreeRtPackets 释放分配的数据包。 无法保证这种排序。

不使用 AcxStreamStateAcquire 状态。 ACX 消除了驱动程序具有获取状态的需求,因为此状态是隐式的,具有准备硬件 (EvtAcxStreamPrepareHardware) 和释放硬件 (EvtAcxStreamReleaseHardware) 回调。

大型缓冲区流和卸载引擎支持

ACX 会使用 ACXAUDIOENGINE 元素指定一个 ACXPIN,用于处理卸载流创建,以及卸载流音量、静音和峰值计状态所需的不同元素。 这类似于 WaveRT 驱动程序中的现有音频引擎节点。

流关闭进程

当客户端关闭流时,驱动程序将在 ACX 框架删除 ACXSTREAM 对象之前收到 EvtAcxStreamPause 和 EvtAcxStreamReleaseHardware。 驱动程序可以在调用 AcxStreamCreate 执行 ACXSTREAM 的最终清理时,在 WDF_OBJECT_ATTRIBUTES 结构中提供标准 WDF EvtCleanupCallback 条目。 当框架尝试删除对象时,WDF 将调用 EvtCleanupCallback。 请勿使用 EvtDestroyCallback,仅在释放对象的所有引用后才调用此项,这是不确定的。

如果尚未在 EvtAcxStreamReleaseHardware 中清理资源,驱动程序应清理与 EvtCleanupCallback 中的 ACXSTREAM 对象关联的系统内存资源。

在客户端请求之前,驱动程序不会清理支持流的资源,这一点很重要。

不使用 AcxStreamStateAcquire 状态。 ACX 消除了驱动程序具有获取状态的需求,因为此状态是隐式的,具有准备硬件 (EvtAcxStreamPrepareHardware) 和释放硬件 (EvtAcxStreamReleaseHardware) 回调。

流意外删除和失效

如果驱动程序确定流已失效(例如未拔出插孔),线路将关闭所有流。 

流内存清理

流资源的处置可以在驱动程序的流上下文清理(而不是销毁)中完成。 切记不要处理对象的上下文销毁回调中共享的任何内容。 本指南适用于所有 ACX 对象。

销毁回调是在最后一个引用消失后调用的,此时它是未知的。

通常,当句柄关闭时,会调用流的清理回调。 一个例外是当驱动程序在其回调中创建流时。 如果 ACX 在从流创建操作返回之前未能将此流添加到其流桥,则该流将被异步取消,并且当前线程将向创建流客户端返回错误。 此时不应为流分配任何内存分配。 有关详细信息,请参阅 EVT_ACX_STREAM_RELEASE_HARDWARE 回调

流内存清理序列

流缓冲区是一种系统资源,只有当用户模式客户端关闭流的句柄时才应该释放它。 缓冲区(不同于设备的硬件资源)具有与流句柄相同的生存期。 当客户端关闭时,句柄 ACX 调用流对象清理回调,然后当对象上的 ref 变为零时,调用流 obj 的删除回调。

当驱动程序创建流对象,然后创建流回调失败时,ACX 有可能将 STREAM 对象的删除推迟到工作项。 为了防止关闭 WDF 线程时出现死锁,ACX 将删除推迟到其他线程。 为了避免这种行为可能产生的任何副作用(资源延迟释放),驱动程序可以在从流创建返回错误之前释放分配的流资源。

当 ACX 调用 EVT_ACX_STREAM_FREE_RTPACKETS 回调时,驱动程序必须释放音频缓冲区。 当用户关闭流句柄时,会调用此回调。

由于 RT 缓冲区在用户模式下映射,因此缓冲区生存期与句柄生存期相同。 在 ACX 调用此回调之前,驱动程序不会尝试放开/释放音频缓冲区。

EVT_ACX_STREAM_FREE_RTPACKETS 回调EVT_ACX_STREAM_RELEASE_HARDWARE 回调后调用,并在 EvtDeviceReleaseHardware 之前结束。

此回调可能发生在驱动程序处理 WDF 发布硬件回调之后,因为用户模式客户端可以长时间保留其句柄。 驱动程序不会尝试等待这些句柄消失,这只会创建一个 0x9f DRIVER_POWER_STATE_FAILURE 错误检查。 有关详细信息,请参阅 EVT_WDF_DEVICE_RELEASE_HARDWARE 回调函数

这个来自示例 ACX 驱动程序的 EvtDeviceReleaseHardware 代码显示了一个调用 AcxDeviceRemoveCircuit 然后释放流式 h/w 内存的示例。

    RETURN_NTSTATUS_IF_FAILED(AcxDeviceRemoveCircuit(Device, devCtx->Render));
    RETURN_NTSTATUS_IF_FAILED(AcxDeviceRemoveCircuit(Device, devCtx->Capture));

    // NOTE: Release streaming h/w resources here.

    CSaveData::DestroyWorkItems();
    CWaveReader::DestroyWorkItems();

综上所述:

wdf 设备发布硬件 -> 发布设备的 h/w 资源

AcxStreamFreeRtPackets -> 放开/释放与句柄关联的音频缓冲区

有关管理 WDF 和线路对象的详细信息,请参阅 ACX WDF 驱动程序生命周期管理

流式处理 DDI

流式处理结构

ACX_RTPACKET 结构

此结构表示单个分配的数据包。 PacketBuffer 可以是 WDFMEMORY 句柄、MDL 或缓冲区。 它具有关联的初始化函数 ACX_RTPACKET_INIT

ACX_STREAM_CALLBACKS

此结构标识用于流式传输到 ACX 框架的驱动程序回调。 此结构是 ACX_PIN_CONFIG 结构的一部分。

流式处理回调

EvtAcxStreamAllocateRtPackets

EvtAcxStreamAllocateRtPackets 事件会指示驱动程序分配用于流式传输的 RtPacket。 AcxRtStream 将收到 PacketCount = 2(对于事件驱动的流式处理),或者 PacketCount = 1(对于基于计时器的流式处理)。 如果驱动程序对两个数据包使用单个缓冲区,则第二个 RtPacketBuffer 应具有类型 = WdfMemoryDescriptorTypeInvalid 的 WDF_MEMORY_DESCRIPTOR,其 RtPacketOffset 与第一个数据包的末尾对齐 (packet[2].RtPacketOffset = packet[1].RtPacketOffset+packet[1].RtPacketSize)。

EvtAcxStreamFreeRtPackets

EvtAcxStreamFreeRtPackets 事件指示驱动程序释放在之前在 EvtAcxStreamAllocateRtPackets 调用中分配的 RtPacket。 包含来自该调用的相同数据包。

EvtAcxStreamGetHwLatency

EvtAcxStreamGetHwLatency 事件指示驱动程序为此流的特定线路提供流延迟(总延迟将是不同线路的延迟总和)。 FifoSize 以字节为单位,延迟以 100 纳秒为单位。

EvtAcxStreamSetRenderPacket

EvtAcxStreamSetRenderPacket 事件会指示驱动程序客户端刚刚释放的数据包。 如果没有故障,则此数据包应为 (CurrentRenderPacket + 1),其中 CurrentRenderPacket 是驱动程序当前正在从中流式处理的数据包。

标志可以是 0 或 KSSTREAM_HEADER_OPTIONSF_ENDOFSTREAM = 0x200,指示数据包是流中的最后一个数据包,EosPacketLength 是数据包的有效长度(以字节为单位)。 有关详细信息,请参阅 KSSTREAM_HEADER 结构 (ks.h) 中的 OptionsFlags

驱动程序应继续增加 CurrentRenderPacket,因为呈现数据包,而不是更改其 CurrentRenderPacket 来与此值匹配。

EvtAcxStreamGetCurrentPacket

EvtAcxStreamGetCurrentPacket 会指示驱动程序当前正在向硬件呈现哪个数据包(基于 0),或者当前正在由捕获硬件填充。

EvtAcxStreamGetCapturePacket

EvtAcxStreamGetCapturePacket 会指示驱动程序最近完全填充了哪个数据包(基于 0),包括在驱动程序开始填充数据包时的 QPC 值。

EvtAcxStreamGetPresentationPosition

EvtAcxStreamGetPresentationPosition 会指示驱动程序在计算当前位置时指示当前位置以及 QPC 值。

流状态事件

ACXSTREAM 的流状态由以下 API 管理。

EVT_ACX_STREAM_PREPARE_HARDWARE

EVT_ACX_STREAM_RELEASE_HARDWARE

EVT_ACX_STREAM_RUN

EVT_ACX_STREAM_PAUSE

流式处理 ACX API

AcxStreamCreate

AcxStreamCreate 将创建可用于控制流式处理行为的 ACX 流。

AcxRtStreamCreate

AcxRtStreamCreate 会创建可用于控制流式处理行为并处理数据包分配和传递流式处理状态的 ACX 流。

AcxRtStreamNotifyPacketComplete

驱动程序会在数据包完成时调用此 ACX API。 数据包完成时间和基于 0 的数据包索引包含在内,以提高客户端性能。 ACX 框架将设置与流关联的任何通知事件。

另请参阅

ACX 音频类扩展概述

ACX 参考文档

ACX 对象摘要