用于音频处理对象的 Windows 11 API

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

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

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

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

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

Windows 11 APO CAPX 开发要求

适用于 Windows 11 的设备上搭载的任何新的 APO 都需要符合在本主题中列出的 API,并通过 HLK 进行验证。 此外,任何利用 AEC 的 API 都遵循本主题中概述的实现,通过 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 访问此内容。 有关“协作”的详细信息,请参阅 Microsoft协作简介

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 或蓝牙音频设备)播放音频,则回声取消将不起作用。 只有操作系统才清楚哪些呈现终结点可以用作引用终结点。

  • APO 可以使用 WASAPI 选取默认渲染端点来执行回声消除。 但是,当从 audiodg.exe 进程(即 APO 的托管位置)打开回环流时,需要特别注意一些潜在的陷阱。

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

API 定义 - AEC

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

APO_CONNECTION_PROPERTY_V2结构

实现 IApoAcousticEchoCancellation 接口的 API 将在对 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 才能实现此接口。 实现此接口的 API 将在调用 IAudioProcessingobjectRT::APOProcess 时提供APO_CONNECTION_PROPERTY_V2结构。 APO 可以检查连接属性中的 APO_CONNECTION_PROPERTY_V2_SIGNATURE 签名,并将传入的 APO_CONNECTION_PROPERTY 结构转换为 APO_CONNECTION_PROPERTY_V2 结构。

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

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

IApoAuxiliaryInputConfiguration

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

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

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

IApoAuxiliaryInputRT

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

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

AEC CAPX API 摘要

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

示例代码 - AEC

请参阅以下 Sysvad Audio 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 并显示 AddAuxiliaryInput 的实现,当时 APO 只能处理一个辅助输入。

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 API 中,可以在支持的终结点上请求卷后环回。

局限性

与可用于所有渲染终端节点的预卷环回流不同,卷后环回流可能无法在所有终端节点上使用。

请求音量后环回测试

希望使用音量后环回的 AEC 音频处理对象应实现 IApoAcousticEchoCancellation2 接口。

AEC APO 可以通过在其实现的 IApoAcousticEchoCancellation2::GetDesiredReferenceStreamProperties 方法中返回 APO_REFERENCE_STREAM_PROPERTIES_POST_VOLUME_LOOPBACK 标志来请求后音量环回。

根据当前使用的渲染终结点,卷后环回可能不可用。 如果在调用其 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 存储应用程序、非管理员桌面应用程序和 API 读取和写入。 此外,这可以充当 API 将消息传递回服务或 UWP 存储应用程序的机制。

注释

这是一项受限功能:如果将应用程序与此功能一起提交到 Microsoft 应用商店,则会触发密切审查。 该应用必须是硬件支持应用(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;
}

IAudioProcessingObject Initialize代码使用IAudioSystemEffectsPropertyStore

此示例展示了在 APO 实现中,如何在初始化过程中使用 APOInitSystemEffects3 结构,检索 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}"

通知框架

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

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

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

实现 - 通知框架

为了利用通知框架,APO 声明其感兴趣的通知。 没有显式的注册或注销调用。 APO 的所有通知都是序列化的,并且重要的是不要让通知回调线程被阻塞过长时间。

API 定义 - 通知框架

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

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

示例代码 - 通知框架

此示例演示 APO 如何实现 IAudioProcessingObjectNotifications 接口。 在 GetApoNotificationRegistrationInfo 方法中,示例 APO 注册通知以更改系统效果属性存储。
OS 调用 HandleNotification 方法,以通知 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;
    }
}

以下代码来自 Swap 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 可能导致泵线程被操作系统 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)任务的工作队列进行多线程处理。 操作系统负责创建实时工作队列并将其与主泵线程关联。 此框架允许 APO 将短暂运行的工作项排入队列。 任务之间的同步仍然是 APO 的责任。 有关 MMCSS 线程的详细信息,请参阅 多媒体类计划程序服务和Real-Time 工作队列 API

API 定义 - 线程框架

Threading 框架引入了 IAudioProcessingObjectQueueService 接口,该接口提供对 APOS 实时工作队列的访问权限。

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

示例代码 - 线程框架

此示例演示了如何使用 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 为应用程序用户需要控制对流的某些影响(例如深度噪音抑制)的方案提供支持。 为此,此框架将添加以下内容:

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

实现 - 音频效果发现

如果 APO 想要公开可以动态启用和禁用的效果,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 API

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

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

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

使用 INF DDInstall 节段

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

   Include=wdmaudio.inf
   Needs=mssysfx.CopyFilesAndRegisterCapX

使用扩展功能的 INF 文件

wdmaudioapo.inf 是 AudioProcessingObject 类扩展 inf。 它包含专用于设备的 SFX 和 MFX APOs 的注册信息。

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

若要直接引用这些 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 用于新的 APOs。

PKEY_FX_Association 在 INF 示例中。 HKR,"FX\\0",%PKEY_FX_Association%,,%KSNODETYPE_ANY%应替换为 HKR,"FX\\0\\%WMALFXGFXAPO_Context%",%PKEY_FX_Association%,,%KSNODETYPE_ANY%

另请参阅

Windows 音频处理对象