Windows 11 音频处理对象 API

本主题介绍音频驱动程序附带的一组适用于音频处理对象 (APO) 的新 Windows 11 API。

Windows 允许第三方音频硬件制造商包括基于主机的自定义数字信号处理效果。 这些效果打包为用户模式音频处理对象 (APO)。 有关详细信息,请参阅 Windows 音频处理对象

此处介绍的一些 API 为独立硬件供应商 (IHV) 和独立软件供应商 (ISV) 启用新方案,而其他 API 旨在提供提高整体音频可靠性和调试功能的替代方案。

  • 回声消除 (AEC) 框架允许 APO 将自身标识为 AEC APO,从而授予对引用流和其他控件的访问权限。
  • 设置框架将允许 APO 在音频终结点上公开用于查询和修改音频效果的属性存储区(“FX 属性存储区”)的方法。 由 APO 实现这些方法时,与该 APO 关联的硬件支持应用 (HSA) 可以调用这些方法。
  • 通知框架允许音频效果 (APO) 请求处理音量、终结点和音频效果属性存储区更改的通知。
  • 日志记录框架有助于开发和调试 APO。
  • 线程处理框架允许使用 OS 托管的 MMCSS 注册线程池对 APO 进行多线程处理。
  • 音频效果发现和控制 API 允许 OS 检测、启用和禁用可供在流上处理的效果。

为了利用这些新 API,APO 应利用新的 IAudioSystemEffects3 接口。 当 APO 实现此接口时,OS 将此解释为 APO 支持 APO 设置框架的隐式信号,并允许 APO 订阅来自音频引擎的常见音频相关通知。

Windows 11 APO CAPX 开发要求

Windows 11 版设备上附带的任何新 APO 都需要符合本主题中列出的 API,已通过 HLK 进行验证。 此外,任何利用 AEC 的 APO 都应遵循本主题中概述的实现,已通过 HLK 进行验证。 这些核心音频处理扩展(设置、日志记录、通知、线程处理、AEC)的自定义实现预计会利用 CAPX API。 这将通过 Windows 11 HLK 测试进行验证。 例如,如果 APO 在使用注册表数据保存设置,而不是使用设置框架,则关联的 HLK 测试将失败。

Windows 版本要求

本主题中介绍的 API 从 Windows 11 OS、WDK 和 SDK 内部版本 22000 开始提供。 Windows 10 将不支持这些 API。 如果 APO 有意在 Windows 10 和 Windows 11 上正常运行,它可以检查是使用 APOInitSystemEffects2 还是 APOInitSystemEffects3 结构对其进行初始化,以确定它是否在支持 CAPX API 的 OS 上运行。

可通过 Windows 预览体验计划在下面下载最新版本的 Windows、WDK 和 SDK。 通过合作伙伴中心与 Microsoft 合作的合作伙伴还可以通过 Collaborate 访问此内容。 有关 Collaborate 的详细信息,请参阅 Microsoft Collaborate 简介

Windows 11 WHCP 内容已更新,现可为合作伙伴提供验证这些 API 的方法。

本主题中概述的内容的示例代码可在此处找到:Audio/SYSVAD/APO - github

回声消除 (AEC)

回声消除 (AEC) 是一种常用音频效果,独立硬件供应商 (IHV) 和独立软件供应商 (ISV) 将其作为麦克风捕获管道中的音频处理对象 (APO) 实现。 此效果不同于 IHV 和 ISV 通常实现的其他效果,因为该效果需要 2 个输入,即来自麦克风的音频流,以及来自充当引用信号的呈现设备的音频流。

此新接口集允许 AEC APO 标识自身,例如向音频引擎标识。 这样做会导致音频引擎使用多路输入和单路输出相应配置 APO。

当 APO 实现新的 AEC 接口时,音频引擎将:

  • 使用附加输入配置 APO,该输入为 APO 提供来自相应呈现终结点的引用流。
  • 在呈现设备更改时换出引用流。
  • 允许 APO 控制输入麦克风和引用流的格式。
  • 允许 APO 获取麦克风和引用流的时间戳。

以前的方法 - Windows 10

APO 是单路输入 – 单个输出对象。 音频引擎向AEC APO 来自其输入处的麦克风终结点的音频。 若要获取引用流,APO 可以使用专有接口与驱动程序交互,以从呈现终结点检索引用音频,或使用 WASAPI 在呈现终结点上打开环回流。

上述两种方法都有缺点:

  • 若 AEC APO 使用专用通道从驱动程序获取引用流,通常只能从集成音频呈现设备执行此操作。 因此,如果用户正在从非集成设备(如 USB 或蓝牙音频设备)播放音频,则回声消除将不起作用。 只有 OS 才知道可用作引用终结点的正确呈现终结点。

  • APO 可以使用 WASAPI 选取默认呈现终结点来执行回声消除。 但是,从 audiodg.exe 进程(即 APO 托管位置)打开环回流时,需要注意一些陷阱。

    • 当音频引擎调用主要 APO 方法时,无法打开/销毁环回流,因为这可能会导致死锁。
    • 捕获 APO 不知道其客户端流的状态。 例如,捕获应用可能具有处于“停止”状态的捕获流,但 APO 不知道此状态,因此在“运行”状态中让环回流保持打开状态,这在耗电量方面效率低下。

API 定义 - AEC

AEC 框架提供 APO 可以利用的新结构和接口。 下面将介绍这些新结构和接口。

APO_CONNECTION_PROPERTY_V2 结构

实现 IApoAcousticEchoCancellation 接口的 APO 在调用 IAudioProcessingObjectRT::APOProcess 时,会有对象向其传递 APO_CONNECTION_PROPERTY_V2 结构。 除了 APO_CONNECTION_PROPERTY 结构中的所有字段外,结构版本 2 还提供音频缓冲区的时间戳信息。

APO 可以检查 APO_CONNECTION_PROPERTY.u32Signature 字段,以确定它从音频引擎收到的结构是类型 APO_CONNECTION_PROPERTY 还是 APO_CONNECTION_PROPERTY_V2。 APO_CONNECTION_PROPERTY 结构具有 APO_CONNECTION_PROPERTY_SIGNATURE 的签名,而 APO_CONNECTION_PROPERTY_V2 具有等于 APO_CONNECTION_PROPERTY_V2_SIGNATURE 的签名。 如果签名的值等于 APO_CONNECTION_PROPERTY_V2_SIGNATURE,则指向 APO_CONNECTION_PROPERTY 结构的指针可能会安全地将其类型强制转换为 APO_CONNECTION_PROPERTY_V2 指针。

以下代码来自 Aec APO MFX 示例 - AecApoMfx.cpp 并显示重新强制转换。

    if (ppInputConnections[0]->u32Signature == APO_CONNECTION_PROPERTY_V2_SIGNATURE)
    {
        const APO_CONNECTION_PROPERTY_V2* connectionV2 = reinterpret_cast<const APO_CONNECTION_PROPERTY_V2*>(ppInputConnections[0]);
    }

IApoAcousticEchoCancellation

IApoAcousticEchoCancellation 接口上没有显式方法。 其用途是向音频引擎标识 AEC APO。 此接口只能由捕获终结点上的模式效果 (MFX) 实现。 在任何其他 APO 上实现此接口时,都将导致加载该 APO 失败。 有关 MFX 的一般信息,请参阅音频处理对象体系结构

如果将捕获终结点上的模式效果作为一系列链接的 APO 实现,则只有最靠近设备的 APO 才能实现此接口。 实现此接口的 APO 将在调用 IAudioProcessingobjectRT::APOProcess 时收到 APO_CONNECTION_PROPERTY_V2 结构。 APO 可以在连接属性上检查是否有 APO_CONNECTION_PROPERTY_V2_SIGNATURE 签名,并将传入 APO_CONNECTION_PROPERTY 结构类型强制转换为 APO_CONNECTION_PROPERTY_V2 结构。

鉴于 AEC APO 通常以特定的采样率/声道计数运行其算法的事实,音频引擎会为实现 IApoAcousticEchoCancellation 接口的 APO 提供重新采样支持。

当 AEC APO 在调用 IAudioProcessingObject::OutInputFormatSupported 中返回 APOERR_FORMAT_NOT_SUPPORTED 时,音频引擎将使用 NULL 输出格式和非 null 输入格式再次调用 APO 上的 IAudioProcessingObject::IsInputFormatSupported,以获取 APO 的建议格式。 然后,音频引擎会将麦克风音频重新采样为建议的格式,然后再将其发送到 AEC APO。 这样就不需要 AEC APO 实现采样率和声道计数转换。

IApoAuxiliaryInputConfiguration

IApoAuxiliaryInputConfiguration 接口提供 APO 可以实现的方法,以便音频引擎可以添加和删除辅助输入流。

此接口由 AEC APO 实现,并由音频引擎用于初始化引用输入。 在 Windows 11 中,将仅使用单路辅助输入初始化 AEC APO,其中一路输入具有用于回声消除的引用音频流。 AddAuxiliaryInput 方法将用于向 APO 添加引用输入。 初始化参数将包含对从中获取环回流的呈现终结点的引用。

音频引擎会调用 IsInputFormatSupported method 方法,以协商辅助输入的格式。 如果 AEC APO 首选特定格式,则可以在调用 IsInputFormatSupported 时返回 S_FALSE,并指定建议的格式。 音频引擎会将引用音频重新采样为建议的格式,并在 AEC APO 的辅助输入中提供它。

IApoAuxiliaryInputRT

IApoAuxiliaryInputRT 接口是实时安全接口,用于驱动 APO 的辅助输入。

此接口用于在 APO 的辅助输入上提供音频数据。 请注意,辅助音频输入与 IAudioProcessingObjectRT::APOProcess 调用不同步。 当呈现终结点未呈现任何音频时,环回数据在辅助输入中将不可用。 即不会调用 IApoAuxiliaryInputRT::AcceptInput

AEC CAPX API 摘要

有关详细信息,请在以下页面上查找其他信息。

示例代码 - AEC

请参阅以下 Sysvad 音频 AecApo 代码示例。

Aec APO 示例标头 - AecAPO.h 中的以下代码显示了要添加的三个新公共方法。

 public IApoAcousticEchoCancellation,
 public IApoAuxiliaryInputConfiguration,
 public IApoAuxiliaryInputRT

...

 COM_INTERFACE_ENTRY(IApoAcousticEchoCancellation)
 COM_INTERFACE_ENTRY(IApoAuxiliaryInputConfiguration)
 COM_INTERFACE_ENTRY(IApoAuxiliaryInputRT)

...


    // IAPOAuxiliaryInputConfiguration
    STDMETHOD(AddAuxiliaryInput)(
        DWORD dwInputId,
        UINT32 cbDataSize,
        BYTE *pbyData,
        APO_CONNECTION_DESCRIPTOR *pInputConnection
        ) override;
    STDMETHOD(RemoveAuxiliaryInput)(
        DWORD dwInputId
        ) override;
    STDMETHOD(IsInputFormatSupported)(
        IAudioMediaType* pRequestedInputFormat,
        IAudioMediaType** ppSupportedInputFormat
        ) override;
...

    // IAPOAuxiliaryInputRT
    STDMETHOD_(void, AcceptInput)(
        DWORD dwInputId,
        const APO_CONNECTION_PROPERTY *pInputConnection
        ) override;

    // IAudioSystemEffects3
    STDMETHODIMP GetControllableSystemEffectsList(_Outptr_result_buffer_maybenull_(*numEffects) AUDIO_SYSTEMEFFECT** effects, _Out_ UINT* numEffects, _In_opt_ HANDLE event) override
    {
        UNREFERENCED_PARAMETER(effects);
        UNREFERENCED_PARAMETER(numEffects);
        UNREFERENCED_PARAMETER(event);
        return S_OK; 
    }

以下代码来自 Aec APO MFX 示例 - AecApoMfx.cpp,并显示当 APO 只能处理一路辅助输入时的 AddAuxiliaryInput 实现。

STDMETHODIMP
CAecApoMFX::AddAuxiliaryInput(
    DWORD dwInputId,
    UINT32 cbDataSize,
    BYTE *pbyData,
    APO_CONNECTION_DESCRIPTOR * pInputConnection
)
{
    HRESULT hResult = S_OK;

    CComPtr<IAudioMediaType> spSupportedType;
    ASSERT_NONREALTIME();

    IF_TRUE_ACTION_JUMP(m_bIsLocked, hResult = APOERR_APO_LOCKED, Exit);
    IF_TRUE_ACTION_JUMP(!m_bIsInitialized, hResult = APOERR_NOT_INITIALIZED, Exit);

    BOOL bSupported = FALSE;
    hResult = IsInputFormatSupportedForAec(pInputConnection->pFormat, &bSupported);
    IF_FAILED_JUMP(hResult, Exit);
    IF_TRUE_ACTION_JUMP(!bSupported, hResult = APOERR_FORMAT_NOT_SUPPORTED, Exit);

    // This APO can only handle 1 auxiliary input
    IF_TRUE_ACTION_JUMP(m_auxiliaryInputId != 0, hResult = APOERR_NUM_CONNECTIONS_INVALID, Exit);

    m_auxiliaryInputId = dwInputId;

另请查看显示 CAecApoMFX::IsInputFormatSupportedCAecApoMFX::AcceptInput 实现以及 APO_CONNECTION_PROPERTY_V2 处理的示例代码。

操作顺序 - AEC

初始化时:

  1. IAudioProcessingObject::Initialize
  2. IApoAuxiliaryInputConfiguration::AddAuxiliaryInput
  3. IAudioProcessingObjectConfiguration:: LockForProcess
  4. IAudioProcessingObjectConfiguration ::UnlockForProcess
  5. IApoAuxiliaryInputConfiguration::RemoveAuxiliaryInput

呈现设备更改时:

  1. IAudioProcessingObject::Initialize
  2. IApoAuxiliaryInputConfiguration::AddAuxiliaryInput
  3. IAudioProcessingObjectConfiguration::LockForProcess
  4. 默认设备更改
  5. IAudioProcessingObjectConfiguration::UnlockForProcess
  6. IApoAuxiliaryInputConfiguration::RemoveAuxiliaryInput
  7. IApoAuxiliaryInputConfiguration::AddAuxiliaryInput
  8. IAudioProcessingObjectConfiguration::LockForProcess

这是 AEC 的建议缓冲区行为。

  • 应将在调用 IApoAuxiliaryInputRT::AcceptInput 时获取的缓冲区写入循环缓冲区,而无需锁定主线程。
  • 在调用 IAudioProcessingObjectRT::APOProcess 时,应从引用流中读取最新音频数据包的循环缓冲区,并且此数据包应用于通过回声消除算法运行。
  • 引用和麦克风数据的时间戳可用于排列扬声器和麦克风数据。

引用环回流

默认情况下,环回流会在应用任何音量或静音之前“点击接近”(侦听)音频流。 在应用音量之前点击的环回流称为预音量环回流。 使用预音量环回流的好处是清晰统一的音频流,而不考虑当前的音量设置。

某些 AEC 算法可能更倾向于获取任何音量处理(包括静音)后连接的环回流。 此配置称为后音量环回。

在下一个主要版本的 Windows 中,AEC APO 可以在支持的终结点上请求后音量环回。

限制

与可用于所有呈现终结点的预音量环回流不同,可能无法在所有终结点上使用后音量环回流。

请求后音量环回

希望使用后音量环回的 AEC APO 应实现 IApoAcousticEchoCancellation2 接口。

通过 Properties 参数在其 IApoAcousticEchoCancellation2::GetDesiredReferenceStreamProperties 实现 中返回 APO_REFERENCE_STREAM_PROPERTIES_POST_VOLUME_LOOPBACK 标志,AEC APO 可以来请求后音量环回。

根据当前使用的呈现终结点,后音量环回可能不可用。 如果在调用其 IApoAuxiliaryInputConfiguration::AddAuxiliaryInput 方法时使用后音量环回,则会通知 AEC APO。 如果 AcousticEchoCanceller_Reference_Input streamProperties 字段包含 APO_REFERENCE_STREAM_PROPERTIES_POST_VOLUME_LOOPBACK,则使用后音量环回。

AEC APO 示例标头 - AecAPO.h 中的以下代码显示了要添加的三个新公共方法。

public:
  // IApoAcousticEchoCancellation2
  STDMETHOD(GetDesiredReferenceStreamProperties)(
    _Out_ APO_REFERENCE_STREAM_PROPERTIES * properties) override;

  // IApoAuxiliaryInputConfiguration
  STDMETHOD(AddAuxiliaryInput)(
    DWORD dwInputId,
    UINT32 cbDataSize,
    _In_ BYTE* pbyData,
    _In_ APO_CONNECTION_DESCRIPTOR *pInputConnection
    ) override;

以下代码片段来自 AEC APO MFX 示例 - AecApoMfx.cpp,并显示 GetDesiredReferenceStreamProperties 的实现,以及 AddAuxiliaryInput 的相关部分。

STDMETHODIMP SampleApo::GetDesiredReferenceStreamProperties(
  _Out_ APO_REFERENCE_STREAM_PROPERTIES * properties)
{
  RETURN_HR_IF_NULL(E_INVALIDARG, properties);

  // Always request that a post-volume loopback stream be used, if
  // available. We will find out which type of stream was actually
  // created when AddAuxiliaryInput is invoked.
  *properties = APO_REFERENCE_STREAM_PROPERTIES_POST_VOLUME_LOOPBACK;
  return S_OK;
}

STDMETHODIMP
CAecApoMFX::AddAuxiliaryInput(
    DWORD dwInputId,
    UINT32 cbDataSize,
    BYTE *pbyData,
    APO_CONNECTION_DESCRIPTOR * pInputConnection
)
{
   // Parameter checking skipped for brevity, please see sample for 
   // full implementation.

  AcousticEchoCanceller_Reference_Input* referenceInput = nullptr;
  APOInitSystemEffects3* papoSysFxInit3 = nullptr;

  if (cbDataSize == sizeof(AcousticEchoCanceller_Reference_Input))
  {
    referenceInput = 
      reinterpret_cast<AcousticEchoCanceller_Reference_Input*>(pbyData);

    if (WI_IsFlagSet(
          referenceInput->streamProperties,
          APO_REFERENCE_STREAM_PROPERTIES_POST_VOLUME_LOOPBACK))
    {
      // Post-volume loopback is being used.
      m_bUsingPostVolumeLoopback = TRUE;
        
      // Note that we can get to the APOInitSystemEffects3 from     
      // AcousticEchoCanceller_Reference_Input.
      papoSysFxInit3 = (APOInitSystemEffects3*)pbyData;
    }
    else  if (cbDataSize == sizeof(APOInitSystemEffects3))
    {
      // Post-volume loopback is not supported.
      papoSysFxInit3 = (APOInitSystemEffects3*)pbyData;
    }

    // Remainder of method skipped for brevity.

设置框架

设置框架允许 APO 在音频终结点上公开用于查询和修改音频效果的属性存储区(“FX 属性存储区”)的方法。 此框架可由 APO 和硬件支持应用 (HSA) 使用,这些应用希望将设置传达给该 APO。 HSA 可以是通用 Windows 平台 (UWP) 应用,并且需要特殊功能才能调用设置框架中的 API。 有关 HSA 应用的详细信息,请参阅 UWP 设备应用

FxProperty 存储区结构

新的 FxProperty 存储区有三个子存储区:Default、User 和 Volatile。

“Default”子项包含自定义效果属性,并从 INF 文件填充。 这些属性不会在 OS 升级之间持久。 例如,通常在 INF 中定义的属性将适合此处。 然后,将从 INF 重新填充这些内容。

“User”子项包含与效果属性相关的用户设置。 这些设置会由 OS 跨升级和迁移持久保留。 例如,用户可以配置的任何预设,这些预设预期会在升级后持久保留。

“Volatile”子项包含可变效果属性。 在设备重新启动时会丢失这些属性,每次终结点转换为活动时也会清除这些属性。 这些属性应包含时间变体属性(例如,基于当前正在运行的应用程序、设备状况等)例如,依赖于当前环境的任何设置。

考虑用户与默认值的方式是是否希望属性在 OS 和驱动程序升级之间持久保留。 用户属性将持久保留。 默认属性将从 INF 重新填充。

APO 上下文

CAPX 设置框架允许 APO 作者按上下文对 APO 属性进行分组。 每个 APO 都可以定义其自己的上下文,并相对于其自己的上下文更新属性。 音频终结点的效果属性存储区可能具有零个或多个上下文。 供应商可以自由创建上下文,无论是按 SFX/MFX/EFX 还是模式创建。 供应商还可以选择为该供应商提供的所有 APO 创建单个上下文。

设置受限功能

设置 API 旨在向所有有兴趣查询和修改与音频设备关联的音频效果设置的 OEM 和 HSA 开发人员提供支持。 此 API 已向 HSA 和 Win32 应用程序公开,以通过必须在清单中声明的受限功能“audioDeviceConfiguration”提供对属性存储区的访问权限。 此外,必须声明相应的命名空间,具体如下:

<Package
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  IgnorableNamespaces="uap mp rescap">
  ...
 
  <Capabilities>
    <rescap:Capability Name="audioDeviceConfiguration" />
  </Capabilities>
</Package>

IAudioSystemEffectsPropertyStore 可由 ISV/IHV 服务、UWP 存储区应用程序、非管理桌面应用程序和 APO 读取和写入。 此外,这可以充当 APO 将消息传递回服务或 UWP 存储区应用程序的机制。

注意

这是一项受限功能:如果使用此功能将应用程序提交到 Microsoft Store,则会触发严格审查。 该应用必须是硬件支持应用 (HSA),系统会对其进行检查,以在提交获得批准之前评估它是否确实是 HSA。

API 定义 - 设置框架

新的 IAudioSystemEffectsPropertyStore 接口允许 HSA 访问音频系统效果属性存储区,以及注册获取属性更改通知。

ActiveAudioInterfaceAsync 函数提供异步获取 IAudioSystemEffectsPropertyStore 接口的方法。

当系统效果属性存储区更改时,应用可以使用新的 IAudioSystemEffectsPropertyChangeNotificationClient 回调接口接收通知。

尝试使用 IMMDevice::Activate 获取 IAudioSystemEffectsPropertyStore 的应用程序

此示例演示了硬件支持应用如何使用 IMMDevice::Activate 激活 IAudioSystemEffectsPropertyStore。 此示例演示如何使用 IAudioSystemEffectsPropertyStore 打开具有用户设置的 IPropertyStore。

#include <mmdeviceapi.h>

// This function opens an IPropertyStore with user settings on the specified IMMDevice.
// Input parameters:
// device - IMMDevice object that identifies the audio endpoint.
// propertyStoreContext - GUID that identifies the property store. Each APO can have its own GUID. These 
// GUIDs are chosen by the audio driver at installation time.
HRESULT GetPropertyStoreFromMMDevice(_In_ IMMDevice* device,
    REFGUID propertyStoreContext,
    _COM_Outptr_ IPropertyStore** userPropertyStore)
{
    *userPropertyStore = nullptr;

    wil::unique_prop_variant activationParam;
    RETURN_IF_FAILED(InitPropVariantFromCLSID(propertyStoreContext, &activationParam));

    wil::com_ptr_nothrow<IAudioSystemEffectsPropertyStore> effectsPropertyStore;
    RETURN_IF_FAILED(device->Activate(__uuidof(effectsPropertyStore), CLSCTX_INPROC_SERVER, activationParam.addressof(), effectsPropertyStore.put_void()));

    RETURN_IF_FAILED(effectsPropertyStore->OpenUserPropertyStore(STGM_READWRITE, userPropertyStore));
    return S_OK;
}

使用 ActivateAudioInterfaceAsync 的示例

此示例执行与上一个示例相同的操作,但不使用 IMMDevice,而是使用 ActivateAudioInterfaceAsync API 异步获取 IAudioSystemEffectsPropertyStore 接口。

include <mmdeviceapi.h>

class PropertyStoreHelper : 
    public winrt::implements<PropertyStoreHelper, IActivateAudioInterfaceCompletionHandler>
{
public:
    wil::unique_event_nothrow m_asyncOpCompletedEvent;

    HRESULT GetPropertyStoreAsync(
        _In_ PCWSTR deviceInterfacePath,
        REFGUID propertyStoreContext,
        _COM_Outptr_ IActivateAudioInterfaceAsyncOperation** operation);

    HRESULT GetPropertyStoreResult(_COM_Outptr_ IPropertyStore** userPropertyStore);

    // IActivateAudioInterfaceCompletionHandler
    STDMETHOD(ActivateCompleted)(_In_ IActivateAudioInterfaceAsyncOperation *activateOperation);

private:
    wil::com_ptr_nothrow<IPropertyStore> m_userPropertyStore;
    HRESULT m_hrAsyncOperationResult = E_FAIL;

    HRESULT GetUserPropertyStore(
        _In_ IActivateAudioInterfaceAsyncOperation* operation,
        _COM_Outptr_ IPropertyStore** userPropertyStore);
};

// This function opens an IPropertyStore with user settings asynchronously on the specified audio endpoint.
// Input parameters:
// deviceInterfacePath - the Device Interface Path string that identifies the audio endpoint. Can be 
// obtained from Windows.Devices.Enumeration.DeviceInformation.
// propertyStoreContext - GUID that identifies the property store. Each APO can have its own GUID. These 
// GUIDs are chosen by the audio driver at installation time.
//
// The function returns an IActivateAudioInterfaceAsyncOperation, which can be used to check the result of
// the asynchronous operation.
HRESULT PropertyStoreHelper::GetPropertyStoreAsync(
    _In_ PCWSTR deviceInterfacePath,
    REFGUID propertyStoreContext,
    _COM_Outptr_ IActivateAudioInterfaceAsyncOperation** operation)
{
    *operation = nullptr;

    wil::unique_prop_variant activationParam;
    RETURN_IF_FAILED(InitPropVariantFromCLSID(propertyStoreContext, &activationParam));

    RETURN_IF_FAILED(ActivateAudioInterfaceAsync(deviceInterfacePath,
        __uuidof(IAudioSystemEffectsPropertyStore),
        activationParam.addressof(),
        this,
        operation));
    return S_OK;
}

// Once the IPropertyStore is available, the app can call this function to retrieve it.
// (The m_asyncOpCompletedEvent event is signaled when the asynchronous operation to retrieve
// the IPropertyStore has completed.)
HRESULT PropertyStoreHelper::GetPropertyStoreResult(_COM_Outptr_ IPropertyStore** userPropertyStore)
{
    *userPropertyStore = nullptr;

    // First check if the asynchronous operation completed. If it failed, the error code
    // is stored in the m_hrAsyncOperationResult variable.
    RETURN_IF_FAILED(m_hrAsyncOperationResult);

    RETURN_IF_FAILED(m_userPropertyStore.copy_to(userPropertyStore));
    return S_OK;
}

// Implementation of IActivateAudioInterfaceCompletionHandler::ActivateCompleted.
STDMETHODIMP PropertyStoreHelper::ActivateCompleted(_In_ IActivateAudioInterfaceAsyncOperation* operation)
{
    m_hrAsyncOperationResult = GetUserPropertyStore(operation, m_userPropertyStore.put());

    // Always signal the event that our caller might be waiting on before we exit,
    // even in case of failure.
    m_asyncOpCompletedEvent.SetEvent();
    return S_OK;
}

HRESULT PropertyStoreHelper::GetUserPropertyStore(
    _In_ IActivateAudioInterfaceAsyncOperation* operation,
    _COM_Outptr_ IPropertyStore** userPropertyStore)
{
    *userPropertyStore = nullptr;

    // Check if the asynchronous operation completed successfully, and retrieve an
    // IUnknown pointer to the result.
    HRESULT hrActivateResult;
    wil::com_ptr_nothrow<IUnknown> audioInterfaceUnknown;
    RETURN_IF_FAILED(operation->GetActivateResult(&hrActivateResult, audioInterfaceUnknown.put()));
    RETURN_IF_FAILED(hrActivateResult);

    // Convert the result to IAudioSystemEffectsPropertyStore
    wil::com_ptr_nothrow<IAudioSystemEffectsPropertyStore> effctsPropertyStore;
    RETURN_IF_FAILED(audioInterfaceUnknown.query_to(&effectsPropertyStore));

    // Open an IPropertyStore with the user settings.
    RETURN_IF_FAILED(effectsPropertyStore->OpenUserPropertyStore(STGM_READWRITE, userPropertyStore));
    return S_OK;
}

使用 IAudioSystemEffectsPropertyStore 的 IAudioProcessingObject::Initialize 代码

此示例演示 APO 的实现可以使用 APOInitSystemEffects3 结构,在 APO 初始化期间检索 APO 的用户、默认和可变 IPropertyStore 接口。

#include <audioenginebaseapo.h>

// Partial implementation of APO to show how an APO that implements IAudioSystemEffects3 can handle
// being initialized with the APOInitSystemEffects3 structure.
class SampleApo : public winrt::implements<SampleApo, IAudioProcessingObject,
    IAudioSystemEffects, IAudioSystemEffects2, IAudioSystemEffects3>
{
public:
    // IAudioProcessingObject
    STDMETHOD(Initialize)(UINT32 cbDataSize, BYTE* pbyData);

    // Implementation of IAudioSystemEffects2, IAudioSystemEffects3 has been omitted from this sample for brevity.  

private:

    wil::com_ptr_nothrow<IPropertyStore> m_defaultStore;
    wil::com_ptr_nothrow<IPropertyStore> m_userStore;
    wil::com_ptr_nothrow<IPropertyStore> m_volatileStore;

    // Each APO has its own private collection of properties. The collection is identified through a
    // a property store context GUID, which is defined below and in the audio driver INF file.
    const GUID m_propertyStoreContext = ...;
};

// Implementation of IAudioProcessingObject::Initialize
STDMETHODIMP SampleApo::Initialize(UINT32 cbDataSize, BYTE* pbyData)
{
    if (cbDataSize == sizeof(APOInitSystemEffects3))
    {
        // SampleApo supports the new IAudioSystemEffects3 interface so it will receive APOInitSystemEffects3
        // in pbyData if the audio driver has declared support for this.

        // Use IMMDevice to activate IAudioSystemEffectsPropertyStore that contains the default, user and
        // volatile settings.
        IMMDeviceCollection* deviceCollection = 
            reinterpret_cast<APOInitSystemEffects3*>(pbyData)->pDeviceCollection;
        if (deviceCollection != nullptr)
        {
            UINT32 numDevices;
            wil::com_ptr_nothrow<IMMDevice> endpoint;

            // Get the endpoint on which this APO has been created
            // (It is the last device in the device collection)
            if (SUCCEEDED(deviceCollection->GetCount(&numDevices)) &&
                numDevices > 0 &&
                SUCCEEDED(deviceCollection->Item(numDevices - 1, &endpoint)))
            {
                wil::unique_prop_variant activationParam;
                RETURN_IF_FAILED(InitPropVariantFromCLSID(m_propertyStoreContext, &activationParam));

                wil::com_ptr_nothrow<IAudioSystemEffectsPropertyStore> effectsPropertyStore;
                RETURN_IF_FAILED(endpoint->Activate(__uuidof(effectsPropertyStore), CLSCTX_ALL, &activationParam, effectsPropertyStore.put_void()));

                // Read default, user and volatile property values to set up initial operation of the APO
                RETURN_IF_FAILED(effectsPropertyStore->OpenDefaultPropertyStore(STGM_READWRITE, m_defaultStore.put()));
                RETURN_IF_FAILED(effectsPropertyStore->OpenUserPropertyStore(STGM_READWRITE, m_userStore.put()));
                RETURN_IF_FAILED(effectsPropertyStore->OpenVolatilePropertyStore(STGM_READWRITE, m_volatileStore.put()));

                // At this point the APO can read and write settings in the various property stores,
                // as appropriate. (Not shown.)
                // Note that APOInitSystemEffects3 contains all the members of APOInitSystemEffects2,
                // so an APO that knows how to initialize from APOInitSystemEffects2 can use the same
                // code to continue its initialization here.
            }
        }
    }
    else if (cbDataSize == sizeof(APOInitSystemEffects2))
    {
        // Use APOInitSystemEffects2 for the initialization of the APO.
        // If we get here, the audio driver did not declare support for IAudioSystemEffects3.
    }
    else if (cbDataSize == sizeof(APOInitSystemEffects))
    {
        // Use APOInitSystemEffects for the initialization of the APO.
    }

    return S_OK;
}

注册获取属性更改通知的应用程序

此示例演示如何使用注册获取属性更改通知。 这不应与 APO 一起使用,并且应该由 Win32 应用程序使用。

class PropertyChangeNotificationClient : public 
    winrt::implements<PropertyChangeNotificationClient, IAudioSystemEffectsPropertyChangeNotificationClient>
{
private:
    wil::com_ptr_nothrow<IAudioSystemEffectsPropertyStore> m_propertyStore;
    bool m_isListening = false;

public:
    HRESULT OpenPropertyStoreOnDefaultRenderEndpoint(REFGUID propertyStoreContext);
    HRESULT StartListeningForPropertyStoreChanges();
    HRESULT StopListeningForPropertyStoreChanges();

    // IAudioSystemEffectsPropertyChangeNotificationClient
    STDMETHOD(OnPropertyChanged)(AUDIO_SYSTEMEFFECTS_PROPERTYSTORE_TYPE type, const PROPERTYKEY key);
};

// Open the IAudioSystemEffectsPropertyStore. This should be the first method invoked on this class.
HRESULT PropertyChangeNotificationClient::OpenPropertyStoreOnDefaultRenderEndpoint(
    REFGUID propertyStoreContext)
{
    wil::com_ptr_nothrow<IMMDeviceEnumerator> deviceEnumerator;
    RETURN_IF_FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator)));

    wil::com_ptr_nothrow<IMMDevice> device;
    RETURN_IF_FAILED(deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.put()));

    wil::unique_prop_variant activationParam;
    RETURN_IF_FAILED(InitPropVariantFromCLSID(propertyStoreContext, &activationParam));

    RETURN_IF_FAILED(device->Activate(__uuidof(m_propertyStore), CLSCTX_INPROC_SERVER,
        &activationParam, m_propertyStore.put_void()));
    return S_OK;
}

// Start subscribing to callbacks that are invoked when there are changes to any of the IPropertyStores
// that are managed by IAudioSystemEffectsPropertyStore.
// The OpenPropertyStoreOnDefaultRenderEndpoint should have been invoked prior to invoking this function.
HRESULT PropertyChangeNotificationClient::StartListeningForPropertyStoreChanges()
{
    RETURN_HR_IF(E_FAIL, !m_propertyStore);
    RETURN_IF_FAILED(m_propertyStore->RegisterPropertyChangeNotification(this));
    m_isListening = true;
    return S_OK;
}

// Unsubscribe to event callbacks. Since IAudioSystemEffectsPropertyStore takes a reference on our
// PropertyChangeNotificationClient class, it is important that this method is invoked prior to cleanup,
// to break the circular reference.
HRESULT PropertyChangeNotificationClient::StopListeningForPropertyStoreChanges()
{
    if (m_propertyStore != nullptr && m_isListening)
    {
        RETURN_IF_FAILED(m_propertyStore->UnregisterPropertyChangeNotification(this));
        m_isListening = false;
    }
    return S_OK;
}

// Callback method that gets invoked when there have been changes to any of the IPropertyStores
// that are managed by IAudioSystemEffectsPropertyStore. Note that calls to 
// IAudioSystemEffectsPropertyChangeNotificationClient are not marshalled across COM apartments.
// Therefore, the OnPropertyChanged is most likely invoked on a different thread than the one used when
// invoking RegisterPropertyChangeNotification. If necessary, concurrent access to shared state should be
// protected with a critical section. 
STDMETHODIMP PropertyChangeNotificationClient::OnPropertyChanged(AUDIO_SYSTEMEFFECTS_PROPERTYSTORE_TYPE type, const PROPERTYKEY key)
{
    if (type == AUDIO_SYSTEMEFFECTS_PROPERTYSTORE_TYPE_USER)
    {
        // Handle changes to the User property store.

        wil::com_ptr_nothrow<IPropertyStore> userPropertyStore;
        RETURN_IF_FAILED(m_propertyStore->OpenUserPropertyStore(STGM_READ, userPropertyStore.put()));

        // Here we can call IPropertyStore::GetValue to read the current value of PROPERTYKEYs that we are
        // interested in.
    }
    else if (type == AUDIO_SYSTEMEFFECTS_PROPERTYSTORE_TYPE_VOLATILE)
    {
        // Handle changes to the Volatile property store, if desired
    }

    return S_OK;
}

示例代码 - 设置框架

此示例代码来自 sysvad SFX 交换 APO 示例 - SwapAPOSFX.cpp

// SampleApo supports the new IAudioSystemEffects3 interface so it will receive APOInitSystemEffects3
// in pbyData if the audio driver has declared support for this.

// Use IMMDevice to activate IAudioSystemEffectsPropertyStore that contains the default, user and
// volatile settings.
IMMDeviceCollection* deviceCollection = reinterpret_cast<APOInitSystemEffects3*>(pbyData)->pDeviceCollection;
if (deviceCollection != nullptr)
{
    UINT32 numDevices;
    wil::com_ptr_nothrow<IMMDevice> endpoint;

    // Get the endpoint on which this APO has been created
    // (It is the last device in the device collection)
    if (SUCCEEDED(deviceCollection->GetCount(&numDevices)) && numDevices > 0 &&
        SUCCEEDED(deviceCollection->Item(numDevices - 1, &endpoint)))
    {
        wil::unique_prop_variant activationParam;
        hr = InitPropVariantFromCLSID(SWAP_APO_SFX_CONTEXT, &activationParam);
        IF_FAILED_JUMP(hr, Exit);

        wil::com_ptr_nothrow<IAudioSystemEffectsPropertyStore> effectsPropertyStore;
        hr = endpoint->Activate(__uuidof(effectsPropertyStore), CLSCTX_ALL, &activationParam, effectsPropertyStore.put_void());
        IF_FAILED_JUMP(hr, Exit);

        // This is where an APO might want to open the volatile or default property stores as well 
        // Use STGM_READWRITE if IPropertyStore::SetValue is needed.
        hr = effectsPropertyStore->OpenUserPropertyStore(STGM_READ, m_userStore.put());
        IF_FAILED_JUMP(hr, Exit);
    }
}

INF 部分 - 设置框架

使用新的 CAPX 设置框架声明效果属性的 INF 文件语法如下所示:

HKR, FX\0\{ApoContext}\{Default|User}, %CUSTOM_PROPERTY_KEY%,,,

这将替换用于声明效果属性的旧语法,具体如下:

# Old way of declaring FX properties
HKR, FX\0, %CUSTOM_PROPERTY_KEY_1%,,,

INF 不能同时具有同一音频终结点的 IAudioSystemEffectsPropertyStore 条目和 IPropertyStore 条目。 这不受支持。

显示使用新属性存储区的示例:

HKR,FX\0\%SWAP_APO_CONTEXT%,%PKEY_FX_Association%,,%KSNODETYPE_ANY%
; Enable the channel swap in the APO
HKR,FX\0\%SWAP_APO_CONTEXT%\User,%PKEY_Endpoint_Enable_Channel_Swap_SFX%,REG_DWORD,0x1

PKEY_Endpoint_Enable_Channel_Swap_SFX = "{A44531EF-5377-4944-AE15-53789A9629C7},2"
REG_DWORD = 0x00010001 ; FLG_ADDREG_TYPE_DWORD
SWAP_APO_CONTEXT = "{24E7F619-5B33-4084-9607-878DA8722417}"
PKEY_FX_Association  = "{D04E05A6-594B-4FB6-A80D-01AF5EED7D1D},0"
KSNODETYPE_ANY   = "{00000000-0000-0000-0000-000000000000}"

通知框架

通知框架允许音频效果 (APO) 请求和处理音量、终结点和音频效果属性存储区更改通知。 此框架旨在替换 APO 用于注册和注销通知的现有 API。

新的 API 引入了一个接口,APO 可以利用该接口来声明 APO 感兴趣的通知类型。 Windows 将查询 APO 以获取其感兴趣的通知,并将通知转发到 APO。 APO 不再需要显式调用注册或注销 API。

将使用串行队列将通知传送到 APO。 如果适用,第一个通知将广播请求值的初始状态(例如音频终结点音量)。 通知会在 audiodg.exe 停止打算使用 APO 进行流式处理后停止。 在 UnlockForProcess 之后,APO 将停止接收通知。 仍需要同步 UnlockForProcess 和任何动态通知。

实现 - 通知框架

为了利用通知框架,APO 会声明其感兴趣的通知。 没有显式注册/注销调用。 APO 的所有通知都已序列化,并且务必不要阻止通知回调线程太长时间。

API 定义 - 通知框架

通知框架会实现新的 IAudioProcessingObjectNotifications 接口,客户端可以实现该接口,以注册和接收 APO 终结点的常见音频相关通知,以及系统效果通知。

有关详细信息,请参阅以下页面上的其他内容:

示例代码 - 通知框架

此示例演示 APO 如何实现 IAudioProcessingObjectNotifications 接口。 在 GetApoNotificationRegistrationInfo 方法中,示例 APO 会注册获取系统效果属性存储区更改通知。
OS 会调用 HandleNotification 方法,以通知 APO 与 APO 注册的内容匹配的更改。

class SampleApo : public winrt::implements<SampleApo, IAudioProcessingObject,
    IAudioSystemEffects, IAudioSystemEffects2, IAudioSystemEffects3,
    IAudioProcessingObjectNotifications>
{
public:
    // IAudioProcessingObjectNotifications
    STDMETHOD(GetApoNotificationRegistrationInfo)(
        _Out_writes_(count) APO_NOTIFICATION_DESCRIPTOR** apoNotifications, _Out_ DWORD* count);
    STDMETHOD_(void, HandleNotification)(_In_ APO_NOTIFICATION *apoNotification);

    // Implementation of IAudioSystemEffects2, IAudioSystemEffects3 has been omitted from this sample for brevity. 

private:
    wil::com_ptr_nothrow<IMMDevice> m_device;

    // Each APO has its own private collection of properties. The collection is identified through a
    // a property store context GUID, which is defined below and in the audio driver INF file.
    const GUID m_propertyStoreContext = ...;

    float m_masterVolume = 1.0f;
    BOOL m_isMuted = FALSE;
    BOOL m_allowOffloading = FALSE;

    // The rest of the implementation of IAudioProcessingObject is omitted for brevity
};

// The OS invokes this method on the APO to find out what notifications the APO is interested in.
STDMETHODIMP SampleApo::GetApoNotificationRegistrationInfo(
    _Out_writes_(count) APO_NOTIFICATION_DESCRIPTOR** apoNotificationDescriptorsReturned,
    _Out_ DWORD* count)
{
    *apoNotificationDescriptorsReturned = nullptr;
    *count = 0;

    // Before this function can be called, our m_device member variable should already have been initialized.
    // This would typically be done in our implementation of IAudioProcessingObject::Initialize, by using
    // APOInitSystemEffects3::pDeviceCollection to obtain the last IMMDevice in the collection.
    RETURN_HR_IF_NULL(E_FAIL, m_device);

    // Let the OS know what notifications we are interested in by returning an array of
    // APO_NOTIFICATION_DESCRIPTORs.
    constexpr DWORD numDescriptors = 3;
    wil::unique_cotaskmem_ptr<APO_NOTIFICATION_DESCRIPTOR[]> apoNotificationDescriptors;

    apoNotificationDescriptors.reset(static_cast<APO_NOTIFICATION_DESCRIPTOR*>(
        CoTaskMemAlloc(sizeof(APO_NOTIFICATION_DESCRIPTOR) * numDescriptors)));
    RETURN_IF_NULL_ALLOC(apoNotificationDescriptors);

    // Our APO wants to get notified when any change occurs on the user property store on the audio endpoint
    // identified by m_device.
    // The user property store is different for each APO. Ours is identified by m_propertyStoreContext.
    apoNotificationDescriptors[0].type = APO_NOTIFICATION_TYPE_AUDIO_SYSTEM_EFFECTS_PROPERTY_CHANGE;
    (void)m_device.query_to(&apoNotificationDescriptors[0].audioSystemEffectsPropertyChange.device);
    apoNotificationDescriptors[0].audioSystemEffectsPropertyChange.propertyStoreContext =   m_propertyStoreContext;

    // Our APO wants to get notified when an endpoint property changes on the audio endpoint.
    apoNotificationDescriptors[1].type = APO_NOTIFICATION_TYPE_ENDPOINT_PROPERTY_CHANGE;
    (void)m_device.query_to(&apoNotificationDescriptors[1].audioEndpointPropertyChange.device);


    // Our APO also wants to get notified when the volume level changes on the audio endpoint.
    apoNotificationDescriptors   [2].type = APO_NOTIFICATION_TYPE_ENDPOINT_VOLUME;
    (void)m_device.query_to(&apoNotificationDescriptors[2].audioEndpointVolume.device);

    *apoNotificationDescriptorsReturned = apoNotificationDescriptors.release();
    *count = numDescriptors;
    return S_OK;
}

static bool IsSameEndpointId(IMMDevice* device1, IMMDevice* device2)
{
    bool isSameEndpointId = false;

    wil::unique_cotaskmem_string deviceId1;
    if (SUCCEEDED(device1->GetId(&deviceId1)))
    {
        wil::unique_cotaskmem_string deviceId2;
        if (SUCCEEDED(device2->GetId(&deviceId2)))
        {
            isSameEndpointId = (CompareStringOrdinal(deviceId1.get(), -1, deviceId2.get(), -1, TRUE) == CSTR_EQUAL);
        }
    }
    return isSameEndpointId;
}

// HandleNotification is called whenever there is a change that matches any of the
// APO_NOTIFICATION_DESCRIPTOR elements in the array that was returned by GetApoNotificationRegistrationInfo.
// Note that the APO will have to query each property once to get its initial value because this method is
// only invoked when any of the properties have changed.
STDMETHODIMP_(void) SampleApo::HandleNotification(_In_ APO_NOTIFICATION* apoNotification)
{
    // Check if a property in the user property store has changed.
    if (apoNotification->type == APO_NOTIFICATION_TYPE_AUDIO_SYSTEM_EFFECTS_PROPERTY_CHANGE
        && IsSameEndpointId(apoNotification->audioSystemEffectsPropertyChange.endpoint, m_device.get())
        && apoNotification->audioSystemEffectsPropertyChange.propertyStoreContext == m_propertyStoreContext
        && apoNotification->audioSystemEffectsPropertyChange.propertyStoreType == AUDIO_SYSTEMEFFECTS_PROPERTYSTORE_TYPE_USER)
    {
        // Check if one of the properties that we are interested in has changed.
        // As an example, we check for "PKEY_Endpoint_Enable_Channel_Swap_SFX" which is a fictitious
        // PROPERTYKEY that could be set on our user property store.
        if (apoNotification->audioSystemEffectsPropertyChange.propertyKey ==
            PKEY_Endpoint_Enable_Channel_Swap_SFX)
        {
            wil::unique_prop_variant var;
            if (SUCCEEDED(apoNotification->audioSystemEffectsPropertyChange.propertyStore->GetValue(
                    PKEY_Endpoint_Enable_Channel_Swap_SFX, &var)) &&
                var.vt != VT_EMPTY)
            {
                // We have retrieved the property value. Now we can do something interesting with it.
            }
        }
    }
    else if (apoNotification->type == APO_NOTIFICATION_TYPE_ENDPOINT_PROPERTY_CHANGE
        
        && IsSameEndpointId(apoNotification->audioEndpointPropertyChange.endpoint, m_device.get())
    {
        // Handle changes to PROPERTYKEYs in the audio endpoint's own property store.
        // In this example, we are interested in a property called "PKEY_Endpoint_AllowOffloading" that the
        // user might change in the audio control panel, and we update our member variable if this
        // property changes.
        if (apoNotification->audioEndpointPropertyChange.propertyKey == PKEY_Endpoint_AllowOffloading)
        {
            wil::unique_prop_variant var;
            if (SUCCEEDED(propertyStore->GetValue(PKEY_Endpoint_AllowOffloading, &var)) && var.vt == VT_BOOL)
            {
                m_allowOffloading = var.boolVal;
            }
        }    
    }
    else if (apoNotification->type == APO_NOTIFICATION_TYPE_ENDPOINT_VOLUME
        
        && IsSameEndpointId(apoNotification->audioEndpointVolumeChange.endpoint, m_device.get())
    {
        // Handle endpoint volume change
        m_masterVolume = apoNotification->audioEndpointVolumeChange.volume->fMasterVolume;
        m_isMuted = apoNotification->audioEndpointVolumeChange.volume->bMuted;
    }
}

以下代码来自 交换 APO MFX 示例 - swapapomfx.cpp,并显示如何注册事件,方法是返回 APO_NOTIFICATION_DESCRIPTORs 数组。

HRESULT CSwapAPOMFX::GetApoNotificationRegistrationInfo(_Out_writes_(*count) APO_NOTIFICATION_DESCRIPTOR **apoNotifications, _Out_ DWORD *count)
{
    *apoNotifications = nullptr;
    *count = 0;

    RETURN_HR_IF_NULL(E_FAIL, m_device);

    // Let the OS know what notifications we are interested in by returning an array of
    // APO_NOTIFICATION_DESCRIPTORs.
    constexpr DWORD numDescriptors = 1;
    wil::unique_cotaskmem_ptr<APO_NOTIFICATION_DESCRIPTOR[]> apoNotificationDescriptors;

    apoNotificationDescriptors.reset(static_cast<APO_NOTIFICATION_DESCRIPTOR*>(
        CoTaskMemAlloc(sizeof(APO_NOTIFICATION_DESCRIPTOR) * numDescriptors)));
    RETURN_IF_NULL_ALLOC(apoNotificationDescriptors);

    // Our APO wants to get notified when an endpoint property changes on the audio endpoint.
    apoNotificationDescriptors[0].type = APO_NOTIFICATION_TYPE_ENDPOINT_PROPERTY_CHANGE;
    (void)m_device.query_to(&apoNotificationDescriptors[0].audioEndpointPropertyChange.device);

    *apoNotifications = apoNotificationDescriptors.release();
    *count = numDescriptors;

    return S_OK;
}

以下代码来自 SwapAPO MFX HandleNotifications 示例 - swapapomfx.cpp,并演示如何处理通知。

void CSwapAPOMFX::HandleNotification(APO_NOTIFICATION *apoNotification)
{
    if (apoNotification->type == APO_NOTIFICATION_TYPE_ENDPOINT_PROPERTY_CHANGE)
    {
        // If either the master disable or our APO's enable properties changed...
        if (PK_EQUAL(apoNotification->audioEndpointPropertyChange.propertyKey, PKEY_Endpoint_Enable_Channel_Swap_MFX) ||
            PK_EQUAL(apoNotification->audioEndpointPropertyChange.propertyKey, PKEY_AudioEndpoint_Disable_SysFx))
        {
            struct KeyControl
            {
                PROPERTYKEY key;
                LONG* value;
            };

            KeyControl controls[] = {
                {PKEY_Endpoint_Enable_Channel_Swap_MFX, &m_fEnableSwapMFX},
            };

            m_apoLoggingService->ApoLog(APO_LOG_LEVEL_INFO, L"HandleNotification - pkey: " GUID_FORMAT_STRING L" %d", GUID_FORMAT_ARGS(apoNotification->audioEndpointPropertyChange.propertyKey.fmtid), apoNotification->audioEndpointPropertyChange.propertyKey.pid);

            for (int i = 0; i < ARRAYSIZE(controls); i++)
            {
                LONG fNewValue = true;

                // Get the state of whether channel swap MFX is enabled or not
                fNewValue = GetCurrentEffectsSetting(m_userStore.get(), controls[i].key, m_AudioProcessingMode);

                SetAudioSystemEffectState(m_effectInfos[i].id, fNewValue ? AUDIO_SYSTEMEFFECT_STATE_ON : AUDIO_SYSTEMEFFECT_STATE_OFF);
            }
        }
    }
}

日志记录框架

日志记录框架为 APO 开发人员提供了额外的数据收集方式,以改善开发和调试。 此框架统一了不同供应商使用的日志记录方法,并将其与音频跟踪日志记录提供程序绑定,以创建更有意义的日志记录。 新框架提供日志记录 API,让操作系统完成剩余的工作。

提供程序的定义为:

IMPLEMENT_TRACELOGGING_CLASS(ApoTelemetryProvider, "Microsoft.Windows.Audio.ApoTrace",
    // {8b4a0b51-5dcf-5a9c-2817-95d0ec876a87}
    (0x8b4a0b51, 0x5dcf, 0x5a9c, 0x28, 0x17, 0x95, 0xd0, 0xec, 0x87, 0x6a, 0x87));

每个 APO 都有其自己的活动 ID。 由于这使用现有的跟踪日志记录机制,因此可以使用现有的控制台工具来筛选这些事件,并实时显示这些事件。 可以使用跟踪日志和 tracefmt 等现有工具,如软件跟踪工具 - Windows 驱动程序中所述。 有关跟踪会话的详细信息,请参阅使用控制 GUID 创建跟踪会话

跟踪日志记录事件不会被标记为遥测,也不会在 xperf 等工具中显示为遥测提供程序。

实现 - 日志记录框架

日志记录框架基于 ETW 跟踪提供的日志记录机制。 有关 ETW 的详细信息,请参阅事件跟踪。 这不适用于记录音频数据,而是记录通常在生产中记录的事件。 不应从实时流式处理线程使用日志记录 API,因为这些 API 可能会导致泵线程被 OS CPU 计划程序抢占。 日志记录应主要用于有助于调试在字段中经常发现的问题的事件。

API 定义 - 日志记录框架

日志记录框架引入了 IAudioProcessingObjectLoggingService 接口,该接口为 APO 提供了新的日志记录服务。

有关详细信息,请参阅 IAudioProcessingObjectLoggingService

示例代码 - 日志记录框架

此示例演示了如何使用 IAudioProcessingObjectLoggingService::ApoLog 方法,以及如何在 IAudioProcessingObject::Initialize 中获取此接口指针。

AecApoMfx 日志记录示例

class SampleApo : public winrt::implements<SampleApo, IAudioProcessingObject,
    IAudioSystemEffects, IAudioSystemEffects2, IAudioSystemEffects3>
{
private:
    wil::com_ptr_nothrow<IAudioProcessingObjectLoggingService> m_apoLoggingService;

public:
    // IAudioProcessingObject
    STDMETHOD(Initialize)(UINT32 cbDataSize, BYTE* pbyData);

    // Implementation of IAudioProcessingObject, IAudioSystemEffects2 andIAudioSystemEffects3 has been omitted for brevity.
};

// Implementation of IAudioProcessingObject::Initialize
STDMETHODIMP SampleApo::Initialize(UINT32 cbDataSize, BYTE* pbyData)
{
    if (cbDataSize == sizeof(APOInitSystemEffects3))
    {
        APOInitSystemEffects3* apoInitSystemEffects3 = reinterpret_cast<APOInitSystemEffects3*>(pbyData);

        // Try to get the logging service, but ignore errors as failure to do logging it is not fatal.
        (void)apoInitSystemEffects3->pServiceProvider->QueryService(SID_AudioProcessingObjectLoggingService, 
            __uuidof(IAudioProcessingObjectLoggingService), IID_PPV_ARGS(&m_apoLoggingService));
    }

    // Do other APO initialization work

    if (m_apoLoggingService != nullptr)
    {
        m_apoLoggingService->ApoLog(APO_LOG_LEVEL_INFO, L"APO Initialization completed");
    }
    return S_OK;
}

线程处理框架

线程处理框架允许通过简单的 API,使用来自相应多媒体类计划程序服务 (MMCSS) 任务的工作队列,对效果进行多线程处理。 OS 将处理实时串行工作队列及其与主泵线程的关联的创建。 此框架允许 APO 将短期运行的工作项加入队列。 任务之间的同步仍然由 APO 负责。 有关 MMCSS 线程处理的详细信息,请参阅多媒体类计划程序服务实时工作队列 API

API 定义 - 线程处理框架

线程处理框架引入了 IAudioProcessingObjectQueueService 接口,该接口提供对 APO 实时工作队列的访问权限。

有关详细信息,请参阅以下页面上的其他内容:

示例代码 - 线程处理框架

此示例演示了如何使用 IAudioProcessingObjectRTQueueService::GetRealTimeWorkQueue 方法,以及如何在 IAudioProcessingObject::Initialize 中获取 IAudioProcessingObjectRTQueueService 接口指针。

#include <rtworkq.h>

class SampleApo3 :
    public winrt::implements<SampleApo3, IAudioProcessingObject, IAudioProcessingObjectConfiguration,
        IAudioSystemEffects, IAudioSystemEffects2, IAudioSystemEffects3>
{
private:
    DWORD m_queueId = 0;
    wil::com_ptr_nothrow<SampleApo3AsyncCallback> m_asyncCallback;

public:
    // IAudioProcessingObject
    STDMETHOD(Initialize)(UINT32 cbDataSize, BYTE* pbyData);

    // IAudioProcessingObjectConfiguration
    STDMETHOD(LockForProcess)(
        _In_ UINT32 u32NumInputConnections,
        _In_reads_(u32NumInputConnections) APO_CONNECTION_DESCRIPTOR** ppInputConnections,
        _In_ UINT32 u32NumOutputConnections,
        _In_reads_(u32NumOutputConnections) APO_CONNECTION_DESCRIPTOR** ppOutputConnections);

    // Non-interface methods called by the SampleApo3AsyncCallback helper class.
    HRESULT DoWorkOnRealTimeThread()
    {
        // Do the actual work here
        return S_OK;
    }
    void HandleWorkItemCompleted(_In_ IRtwqAsyncResult* asyncResult);

    // Implementation of IAudioProcessingObject, IAudioSystemEffects2, IAudioSystemEffects3   and IAudioProcessingObjectConfiguration is omitted
    // for brevity.
};

// Implementation of IAudioProcessingObject::Initialize
STDMETHODIMP SampleApo3::Initialize(UINT32 cbDataSize, BYTE* pbyData)
{
    if (cbDataSize == sizeof(APOInitSystemEffects3))
    {
        APOInitSystemEffects3* apoInitSystemEffects3 = reinterpret_cast<APOInitSystemEffects3*>(pbyData);

        wil::com_ptr_nothrow<IAudioProcessingObjectRTQueueService> apoRtQueueService;
        RETURN_IF_FAILED(apoInitSystemEffects3->pServiceProvider->QueryService(
            SID_AudioProcessingObjectRTQueue, IID_PPV_ARGS(&apoRtQueueService)));

        // Call the GetRealTimeWorkQueue to get the ID of a work queue that can be used for scheduling tasks
        // that need to run at a real-time priority. The work queue ID is used with the Rtwq APIs.
        RETURN_IF_FAILED(apoRtQueueService->GetRealTimeWorkQueue(&m_queueId));
    }

    // Do other initialization here
    return S_OK;
}

STDMETHODIMP SampleApo3::LockForProcess(
    _In_ UINT32 u32NumInputConnections,
    _In_reads_(u32NumInputConnections) APO_CONNECTION_DESCRIPTOR** ppInputConnections,
    _In_ UINT32 u32NumOutputConnections,
    _In_reads_(u32NumOutputConnections) APO_CONNECTION_DESCRIPTOR** ppOutputConnections)
{
    // Implementation details of LockForProcess omitted for brevity
    m_asyncCallback = winrt::make<SampleApo3AsyncCallback>(m_queueId).get();
    RETURN_IF_NULL_ALLOC(m_asyncCallback);

    wil::com_ptr_nothrow<IRtwqAsyncResult> asyncResult;	
    RETURN_IF_FAILED(RtwqCreateAsyncResult(this, m_asyncCallback.get(), nullptr, &asyncResult));

    RETURN_IF_FAILED(RtwqPutWorkItem(m_queueId, 0, asyncResult.get())); 
    return S_OK;
}

void SampleApo3::HandleWorkItemCompleted(_In_ IRtwqAsyncResult* asyncResult)
{
    // check the status of the result
    if (FAILED(asyncResult->GetStatus()))
    {
        // Handle failure
    }

    // Here the app could call RtwqPutWorkItem again with m_queueId if it has more work that needs to
    // execute on a real-time thread.
}


class SampleApo3AsyncCallback :
    public winrt::implements<SampleApo3AsyncCallback, IRtwqAsyncCallback>
{
private:
    DWORD m_queueId;

public:
    SampleApo3AsyncCallback(DWORD queueId) : m_queueId(queueId) {}

    // IRtwqAsyncCallback
    STDMETHOD(GetParameters)(_Out_ DWORD* pdwFlags, _Out_ DWORD* pdwQueue)
    {
        *pdwFlags = 0;
        *pdwQueue = m_queueId;
        return S_OK;
    }
    STDMETHOD(Invoke)(_In_ IRtwqAsyncResult* asyncResult);
};


STDMETHODIMP SampleApo3AsyncCallback::Invoke(_In_ IRtwqAsyncResult* asyncResult)
{
    // We are now executing on the real-time thread. Invoke the APO and let it execute the work.
    wil::com_ptr_nothrow<IUnknown> objectUnknown;
    RETURN_IF_FAILED(asyncResult->GetObject(objectUnknown.put_unknown()));

    wil::com_ptr_nothrow<SampleApo3> sampleApo3 = static_cast<SampleApo3*>(objectUnknown.get());
    HRESULT hr = sampleApo3->DoWorkOnRealTimeThread();
    RETURN_IF_FAILED(asyncResult->SetStatus(hr));

    sampleApo3->HandleWorkItemCompleted(asyncResult);
    return S_OK;
}

有关如何使用此接口的更多示例,请参阅以下示例代码:

效果的音频效果发现和控制

发现框架允许 OS 控制其流上的音频效果。 这些 API 为应用程序用户需要控制流上的某些效果(例如深度噪音抑制)的场景提供支持。 为此,此框架将添加以下功能:

  • 用于从 APO 查询的新 API,可用于确定是否可以启用或禁用音频效果。
  • 用于将音频效果的状态设置为 on/off 的新 API。
  • 当音频效果列表发生更改或资源可用时的通知,以便现在可以启用/禁用音频效果。

实现 - 音频效果发现

如果 APO 想要公开可以动态启用和禁用的效果,则需要实现 IAudioSystemEffects3 接口。 APO 通过 IAudioSystemEffects3::GetControllableSystemEffectsList 函数公开其音频效果,并通过 IAudioSystemEffects3::SetAudioSystemEffectState 函数启用和禁用其音频效果。

示例代码 - 音频效果发现

此音频效果发现示例代码可以在 SwapAPOSFX 示例 - swapaposfx.cpp 中找到。

以下示例代码演示如何检索可配置效果的列表。 GetControllableSystemEffectsList 示例 - swapaposfx.cpp

HRESULT CSwapAPOSFX::GetControllableSystemEffectsList(_Outptr_result_buffer_maybenull_(*numEffects) AUDIO_SYSTEMEFFECT** effects, _Out_ UINT* numEffects, _In_opt_ HANDLE event)
{
    RETURN_HR_IF_NULL(E_POINTER, effects);
    RETURN_HR_IF_NULL(E_POINTER, numEffects);

    *effects = nullptr;
    *numEffects = 0;

    // Always close existing effects change event handle
    if (m_hEffectsChangedEvent != NULL)
    {
        CloseHandle(m_hEffectsChangedEvent);
        m_hEffectsChangedEvent = NULL;
    }

    // If an event handle was specified, save it here (duplicated to control lifetime)
    if (event != NULL)
    {
        if (!DuplicateHandle(GetCurrentProcess(), event, GetCurrentProcess(), &m_hEffectsChangedEvent, EVENT_MODIFY_STATE, FALSE, 0))
        {
            RETURN_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
        }
    }

    if (!IsEqualGUID(m_AudioProcessingMode, AUDIO_SIGNALPROCESSINGMODE_RAW))
    {
        wil::unique_cotaskmem_array_ptr<AUDIO_SYSTEMEFFECT> audioEffects(
            static_cast<AUDIO_SYSTEMEFFECT*>(CoTaskMemAlloc(NUM_OF_EFFECTS * sizeof(AUDIO_SYSTEMEFFECT))), NUM_OF_EFFECTS);
        RETURN_IF_NULL_ALLOC(audioEffects.get());

        for (UINT i = 0; i < NUM_OF_EFFECTS; i++)
        {
            audioEffects[i].id = m_effectInfos[i].id;
            audioEffects[i].state = m_effectInfos[i].state;
            audioEffects[i].canSetState = m_effectInfos[i].canSetState;
        }

        *numEffects = (UINT)audioEffects.size();
        *effects = audioEffects.release();
    }

    return S_OK;
}

以下示例代码演示如何启用和禁用效果。 SetAudioSystemEffectState 示例 - swapaposfx.cpp

HRESULT CSwapAPOSFX::SetAudioSystemEffectState(GUID effectId, AUDIO_SYSTEMEFFECT_STATE state)
{
    for (auto effectInfo : m_effectInfos)
    {
        if (effectId == effectInfo.id)
        {
            AUDIO_SYSTEMEFFECT_STATE oldState = effectInfo.state;
            effectInfo.state = state;

            // Synchronize access to the effects list and effects changed event
            m_EffectsLock.Enter();

            // If anything changed and a change event handle exists
            if (oldState != effectInfo.state)
            {
                SetEvent(m_hEffectsChangedEvent);
                m_apoLoggingService->ApoLog(APO_LOG_LEVEL_INFO, L"SetAudioSystemEffectState - effect: " GUID_FORMAT_STRING L", state: %i", effectInfo.id, effectInfo.state);
            }

            m_EffectsLock.Leave();
            
            return S_OK;
        }
    }

    return E_NOTFOUND;
}

在 Windows 11 版本 22H2 中重复使用 WM SFX 和 MFX APO

从 Windows 11 版本 22H2 开始,重复使用收件箱 WM SFX 和 MFX APO 的 INF 配置文件现在可以重复使用 CAPX SFX 和 MFX APO。 本部分介绍执行此操作的三种方法。

APO 有三个插入点:预混合呈现、后混合呈现和捕获。 每个逻辑设备的音频引擎都支持每个流预混合呈现 APO(呈现 SFX)和一个后混合呈现 APO (MFX) 的一个实例。 音频引擎还支持在每个捕获流中插入的捕获 APO(捕获 SFX)的一个实例。 有关如何重复使用或包装收件箱 APO 的详细信息,请参阅合并自定义 和 Windows APO

可通过以下三种方式之一重复使用 CAPX SFX 和 MFX APO。

使用 INF DDInstall 部分

通过添加以下条目,使用来自 wdmaudio.inf 的 mssysfx.CopyFilesAndRegisterCapX。

   Include=wdmaudio.inf
   Needs=mssysfx.CopyFilesAndRegisterCapX

使用扩展 INF 文件

wdmaudioapo.inf 是 AudioProcessingObject 类扩展 inf。 它包含 SFX 和 MFX APO 特定于设备的注册。

直接引用 WM SFX 和 MFX APO 以获取流和模式效果

若要直接引用这些 APO 以获取流和模式效果,请使用以下 GUID 值。

  • {C9453E73-8C5C-4463-9984-AF8BAB2F5447} 用作 WM SFX APO
  • {13AB3EBD-137E-4903-9D89-60BE8277FD17} 用作 WM MFX APO。

SFX(流)和 MFX(模式)在 Windows 8.1 中被引用为 LFX(本地),而 MFX 被引用为 GFX(全局)。 这些注册表项继续使用以前的名称。

设备特定的注册使用 HKR 而不是 HKCR。

INF 文件需要添加以下条目。

  HKR,"FX\\0\\%WMALFXGFXAPO_Context%",%PKEY_FX_Association%,,%KSNODETYPE_ANY%
  HKR,"FX\\0\\%WMALFXGFXAPO_Context%\\User",,,
  WMALFXGFXAPO_Context = "{B13412EE-07AF-4C57-B08B-E327F8DB085B}"

这些 INF 文件条目将创建一个属性存储区,该存储区将由 Windows 11 API 用于新的 APO。

INF 中的 PKEY_FX_Association(如 HKR,"FX\\0",%PKEY_FX_Association%,,%KSNODETYPE_ANY%)应被替换为 HKR,"FX\\0\\%WMALFXGFXAPO_Context%",%PKEY_FX_Association%,,%KSNODETYPE_ANY%

另请参阅

Windows 音频处理对象