適用於音訊處理物件的 Windows 11 API

本主題介紹一組隨附於音訊驅動程式的新 Windows 11 API for Audio Processing Objects (APOs)。

Windows 允許第三方音訊硬體製造商包含自定義主機型數位訊號處理效果。 這些效果會封裝為使用者模式音訊處理物件(APOS)。 如需詳細資訊,請參閱 Windows 音訊處理物件

此處所述的一些 API 可為獨立硬體廠商(IHV)和獨立軟體供應商(ISV)啟用新的案例,而其他 API 則旨在提供可改善整體音訊可靠性和偵錯功能的替代方案。

  • 聲場 Echo Cancellation (AEC) 架構可讓 APO 將自己識別為 AEC APO,並授與參考數據流和其他控件的存取權。
  • 設定 架構可讓 APO 在音訊端點上公開查詢和修改音訊效果屬性存放區的方法(“FX 屬性存放區”。 當這些方法由 APO 實作時,硬體支援應用程式 (HSA) 可以叫用與該 APO 相關聯的方法。
  • Notifications 架構可讓音訊效果 (APOs) 要求處理音量、端點和音訊效果屬性存放區變更的通知。
  • 記錄架構有助於開發及偵錯APOS。
  • 線程架構允許使用受OS管理的 MMCSS 註冊線程集區進行多線程處理。
  • 音訊效果探索和控制 API 可讓 OS 偵測、啟用和停用可用於處理數據流的效果。

若要利用這些新的 API,API 預期會利用新的 IAudioSystemEffects3 介面。 當 APO 實作此介面時,OS 會將此解譯為 APO 支援 APO 設定 架構的隱含訊號,並允許 APO 訂閱來自音訊引擎的常見音訊相關通知。

Windows 11 APO CAPX 開發需求

Windows 11 裝置上隨附的任何新 API 都必須符合本主題中所列 API 的規範,並透過 HLK 進行驗證。 此外,任何利用 AEC 的 AEC 的 API 都應該遵循本主題中所述的實作,並透過 HLK 進行驗證。 這些核心音訊處理延伸模組的自定義實作(設定、記錄、通知、線程、AEC)預期會利用 CAPX API。 這會透過 Windows 11 HLK 測試進行驗證。 例如,如果 APO 使用登錄資料來儲存設定,而不是使用 設定 Framework,相關聯的 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 合作的合作夥伴也可以透過共同作業存取此內容。 如需共同作業的詳細資訊,請參閱 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

ADO 是單一輸入 – 單一輸出物件。 音訊引擎會在其輸入時提供來自麥克風端點的音訊 AEC APO。 若要取得參考數據流,APO 可以使用專屬介面與驅動程序互動,以從轉譯端點擷取參考音訊,或使用WASAPI在轉譯端點上開啟回送數據流。

上述兩種方法都有缺點:

  • AEC APO,使用私人通道從驅動程式取得參考數據流,通常只能從整合式音訊轉譯裝置執行此動作。 因此,如果使用者正在播放非整合式裝置的音訊,例如USB或 藍牙音訊裝置,則回音取消將無法運作。 只有OS才知道可做為參考端點的正確轉譯端點。

  • 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 APOs 通常會以特定取樣率/通道計數執行其演算法的事實,音訊引擎會為實作 IApoAcousticEchoCancellation 介面的 APOs 提供重新取樣支援。

當 AEC APO 在呼叫 IAudioProcessingObject::OutInputFormatSupported 中傳回APOERR_FORMAT_NOT_SUPPORTED時,音訊引擎會以 NULL 輸出格式和非 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 音訊 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::AcceptInputCAecApoMFX::IsInputFormatSupported實作,以及的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 API 應該實 作 IApoAcousticEchoCancellation2 介面。

AEC APO 可以透過 Properties 參數在其 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.

設定 Framework

設定 Framework 可讓 APO 在音訊端點上公開查詢和修改音訊效果的屬性存放區的方法。。 此架構可以供 APO 使用,以及想要與該 APO 通訊設定的硬體支援應用程式(HSA)。 HSA 可以是 通用 Windows 平台 (UWP) 應用程式,而且需要特殊功能,才能在 設定 Framework 中叫用 API。 如需 HSA 應用程式的詳細資訊,請參閱 UWP 裝置應用程式

FxProperty 存放區結構

新的 FxProperty 存放區有三個子存放區:Default、User 和 Volatile。

“Default” 子機碼包含自定義效果屬性,並從 INF 檔案填入。 這些屬性不會在OS升級之間保存。 例如,通常定義於 INF 中的屬性會適合此處。 然後,這些會從 INF 重新填入。

“User” 子機碼包含與效果屬性相關的用戶設定。 這些設定會由操作系統跨升級和移轉保存。 例如,用戶可以設定的任何預設,預期會在升級之間保存。

“Volatile” 子機碼包含 volatile 效果屬性。 這些屬性會在裝置重新啟動時遺失,而且每次端點轉換為作用中時都會清除。 這些預期會包含時間變化屬性(例如,根據目前執行的應用程式、裝置狀態等)例如,相依於目前環境的任何設定。

思考使用者與預設值的方式,是您希望屬性在OS和驅動程序升級之間保存。 用戶屬性將會保存。 默認屬性將會從 INF 重新填入。

APO 內容

CAPX 設定架構可讓 APO 作者依 內容將 APO 屬性分組。 每個 APO 都可以定義自己的內容,並更新相對於其本身內容的屬性。 音訊端點的效果屬性存放區可能有零個或多個內容。 廠商可以選擇建立內容,無論是透過SFX/MFX/EFX還是模式。 廠商也可以選擇針對該廠商所出貨的所有APOs有單一內容。

設定 受限制的功能

設定 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 市集應用程式、非系統管理員桌面應用程式和 APOs 讀取和寫入。 此外,這可作為 ADO 將訊息傳遞回服務或 UWP 市集應用程式的機制。

注意

這是受限制的功能:如果應用程式以這項功能提交至 Microsoft Store,則會觸發仔細審查。 應用程式必須是硬體支援應用程式 (HSA),而且會在提交核准之前,先檢查它是否確實是 HSA。

API 定義 - 設定 Framework

新的 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 初始化程式代碼::初始化程序代碼

此範例示範 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;
}

範例程式代碼 - 設定 Framework

此範例程式代碼來自 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 區段 - 設定 Framework

使用新 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}"

通知架構

Notifications 架構可讓音訊效果 (APOs) 要求並處理音量、端點和音訊效果屬性存放區變更通知。 此架構旨在取代 API,由 API 用來註冊和取消註冊通知的現有 API。

新的 API 引進了 API,API 可用來宣告 APO 感興趣的通知類型。 Windows 會查詢 APO 以取得其感興趣的通知,並將通知轉送至 APOS。 API 不再需要明確呼叫註冊或取消註冊 API。

通知會使用序列佇列傳遞至 APO。 適用時,第一個通知會廣播所要求值的初始狀態(例如音訊端點音量)。 一旦audiodg.exe停止想要使用 APO 進行串流,通知就會停止。 APO 會在 UnlockForProcess 之後停止接收通知。 仍然需要同步處理 UnlockForProcess 和任何正式發行前小眾測試版通知。

實作 - 通知架構

若要利用通知架構,APO 會宣告其感興趣的通知。 沒有明確的註冊/取消註冊呼叫。 APO 的所有通知都會串行化,而且請務必不要封鎖通知回呼線程太久。

API 定義 - 通知架構

通知架構會實作新的 IAudioProcessingObjectNotifications 介面,可由用戶端實作,以註冊和接收 APO 端點和系統效果通知的常見音訊相關通知。

如需詳細資訊,請在下列頁面上尋找其他內容:

範例程序代碼 - Notifications Framework

此範例示範 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;
    }
}

下列程式代碼來自 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,讓 OS 完成其餘的工作。

提供者的定義如下:

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 都有自己的活動識別碼。 由於這會使用現有的追蹤記錄機制,因此現有的控制台工具可用來篩選這些事件,並實時顯示它們。 您可以使用追蹤記錄和 tracefmt 等現有工具,如軟體追蹤工具 - Windows 驅動程式中所述。 如需追蹤會話的詳細資訊,請參閱 使用控制 GUID 建立追蹤會話。

追蹤記錄事件不會標示為遙測,而且不會在 xperf 等工具中顯示為遙測提供者。

實作 - 記錄架構

記錄架構是以 ETW 追蹤所提供的記錄機制為基礎。 如需 ETW 的詳細資訊,請參閱 事件追蹤。 這不適用於記錄音訊數據,而是記錄通常記錄在生產環境中的事件。 記錄 API 不應該從即時串流線程使用,因為這些 API 可能會導致幫浦線程被 OS CPU 排程器預先清空。 記錄應該主要用於有助於偵錯欄位中經常發現的問題的事件。

API 定義 - 記錄架構

記錄架構引進 了 IAudioProcessingObjectLoggingService 介面,為 APOs 提供新的記錄服務。

如需詳細資訊,請參閱 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 線程的詳細資訊,請參閱多媒體類別排程器服務和即時工作佇列 API

API 定義 - 線程架構

線程架構引進 了 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 可支援應用程式使用者需要控制對串流的特定影響(例如深度雜訊抑制)的案例。 為了達成此目的,此架構會新增下列專案:

  • 要從 APO 查詢的新 API,以判斷是否可以啟用或停用音訊效果。
  • 新的 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 APOs。 本節說明執行這項操作的三種方式。

ADO 有三個插入點:預先混合轉譯、混合後轉譯和擷取。 每個邏輯裝置的音訊引擎都支援每個數據流的一個預先混合轉譯 APO 實例(轉譯 SFX)和一個混合後轉譯 APO (MFX)。 音訊引擎也支援在每個擷取數據流中插入的擷取 APO (capture SFX) 實例。 如需如何重複使用或包裝收件匣 API 的詳細資訊,請參閱 合併自定義和 Windows APOS

CAPX SFX 和 MFX API 可以透過下列三種方式之一重複使用。

使用 INF DDInstall 區段

使用 mssysfx。從 wdmaudio.inf 新增下列專案,從 wdmaudio.inf 複製FilesAndRegisterCapX。

   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 (Stream) 和 MFX (模式) 在 Windows 8.1 中被轉介為 LFX (local) 和 MFX 稱為 GFX (global)。 這些登錄項目會繼續使用先前的名稱。

裝置特定的註冊會使用 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 用於新的 API。

PKEY_FX_Association INF ex. HKR,"FX\\0",%PKEY_FX_Association%,,%KSNODETYPE_ANY%應該以 HKR,"FX\\0\\%WMALFXGFXAPO_Context%",%PKEY_FX_Association%,,%KSNODETYPE_ANY%取代 。

另請參閱

Windows 音訊處理物件