本文說明了 ACX 串流與緩衝,這些對於無故障的音訊體驗至關重要。 它描述了驅動程式如何傳達串流狀態並管理串流緩衝區。 如需常見 ACX 音訊術語的清單和 ACX 簡介,請參閱 ACX 音訊類別延伸模組概觀。
ACX 串流類型
AcxStream 代表特定電路硬體上的音訊串流。 AcxStream 可以聚合一個或多個類似 AcxElements 的物件。
ACX 支援兩種串流類型。 第一種串流類型,RT 封包串流,允許你分配 RT 封包,並用它們來傳輸音訊資料到或從裝置硬體傳輸,同時進行串流狀態轉換。 第二種串流類型, 基本串流,僅支援串流狀態轉換。
在單一電路端點中,該電路是一個串流電路,產生一個 RT 封包串流。 若兩個或以上迴路連接以建立端點,端點內的第一條迴路即為串流迴路,並產生 RT 封包串流。 連接的電路會建立基本串流,以接收與串流狀態轉換相關的事件。
如需詳細資訊,請參閱 ACX 物件摘要中的 ACX 資料流程。 串流的 DDI 定義在 acxstreams.h 標頭中。
ACX 串流通訊堆疊
ACX 串流有兩種通訊方式。 其中一條通訊路徑控制串流行為。 例如,像 Start、Create 和 Alllocate 這類指令,使用標準的 ACX 通訊。 ACX 框架使用 IO 佇列,並透過佇列傳遞 WDF 請求。 佇列行為透過事件回調與 ACX 函式隱藏於實際驅動程式程式碼之外。 驅動程式也有機會預先處理所有 WDF 請求。
第二條更有趣的通訊路徑處理音訊串流訊號。 訊號傳遞是指告訴驅動程式封包何時準備好並接收資料,以及驅動程式何時完成封包處理。
串流訊號的主要需求:
- 支援無故障播放
- 低延遲
- 任何必要的鎖定都僅限於相關的串流
- 便於驅動程式開發者使用
若要與驅動程式通訊以發出串流狀態訊號,ACX 會使用具有共用緩衝區和直接 IRP 呼叫的事件。 以下將介紹這些技術。
共用緩衝區
共享緩衝區與事件由驅動程式與用戶端通訊。 事件和共享緩衝區確保客戶端不需要等待或輪詢。 用戶端可以決定繼續串流所需的一切,同時減少或消除直接 IRP 呼叫的需求。
裝置驅動程式會使用共用緩衝區,與用戶端通訊要從哪個封包轉譯或擷取到哪個封包。 此共享緩衝區包含最後完成封包的封包計數(以一為基礎)及完成時間的 QPC(QueryPerformanceCounter)值。 對於裝置驅動程式,必須透過呼叫 AcxRtStreamNotifyPacketComplete 來標示這些資訊。 當裝置驅動程式呼叫 AcxRtStreamNotifyPacketComplete 時,ACX 框架會更新共享緩衝區中的新封包數量與 QPC,並發出與用戶端共享的事件,表示用戶端可以讀取新的封包數量。
直接 IRP 呼叫
直接 IRP 呼叫是從用戶端與驅動程式進行通訊。
用戶端可以隨時向裝置驅動程式請求目前封包計數,或告知目前封包計數。 這些請求呼叫 EvtAcxStreamGetCurrentPacket 與 EvtAcxStreamSetRenderPacket 裝置驅動程式事件處理程式。 用戶端也可以請求目前的擷取封包,該封包會呼叫 EvtAcxStreamGetCapturePacket 裝置驅動程式事件處理程序。
與 PortCls 的相似之處
ACX 使用的直接 IRP 呼叫與共享緩衝區的組合,類似於 PortCls 通訊緩衝區完成處理的方式。
為防止故障,驅動程式必須確保不做任何需要存取同時用於流控制路徑的鎖的操作。
支援大緩衝區以支援低功耗播放
為了降低播放時的功耗,請縮短 APU 在高功率狀態下停留的時間。 由於一般音訊播放使用 10 毫秒緩衝區,APU 會保持活躍狀態。 ACX 驅動程式可宣告支援較大緩衝區,約 1–2 秒,讓 APU 進入較低功耗狀態。
在現有的串流模式中,卸載播放支援低功耗播放。 音訊驅動程式透過在波形濾波器上顯示端點的 AudioEngine 節點來表示對卸載播放的支援。 AudioEngine 節點提供一種方法來控制 DSP 引擎,這個引擎在驅動程式中用於從大型緩衝區中轉譯音訊並進行所需的處理。
AudioEngine 節點提供以下功能:
- 音訊引擎描述指定音訊堆疊中波形濾波器的接腳,這些接腳提供卸載、迴路回放支援(以及主機播放支援)。
- 緩衝區大小範圍告訴音訊堆疊可支援的最小與最大緩衝區大小。 播放。 緩衝區大小範圍可以根據系統活動動態變更。
- 格式支援,包括支援的格式、目前的裝置混合格式和裝置格式。
- 音量,包括擴充支援,因為緩衝區大後軟體音量不會有反應。
- 回送保護告知驅動程式,如果一或多個卸載的資料流包含受保護的內容,則將 AudioEngine 的回送針腳靜音。
- 全域 FX 狀態,以在 AudioEngine 上啟用或停用 GFX。
當你在卸載腳位建立串流時,串流支援音量、本地效果和迴路保護。
使用 ACX 進行低功耗播放
ACX 架構會使用相同的模型進行低功率播放。 驅動程式會為主機、卸載和回送串流處理建立三個個別的 ACXPIN 物件,以及一個 ACXAUDIOENGINE 元素,以描述這些針腳怎麼用於主機、卸載和回送。 驅動程式在建立電路時,會將針腳和 ACXAUDIOENGINE 元素添加至 ACXCIRCUIT。
卸載的串流建立
驅動程式也會在為卸載而建立的串流中加入 ACXAUDIOENGINE 元素,以便控制音量、靜音和峰值計量。
串流圖
此圖顯示多堆疊 ACX 驅動程式。
每個 ACX 驅動程式控制音訊硬體的不同部分,這些部分可能來自不同廠商。 ACX 提供相容的核心串流介面,使應用程式無需更改即可運行。
串流腳位
每個ACXcircuit至少有一個接收引腳和一個源引腳。 這些腳位由 ACX 框架用來呈現電路與音訊堆疊之間的連接。 針對渲染電路,輸入端針可用來控制從電路建立之任何串流的渲染行為。 針對擷取電路,接收針腳可用來控制從電路建立的任何資料流程的擷取行為。
ACXPIN 是用來控制音訊路徑中串流的物件。 串流 ACXCIRCUIT 負責在電路建立時為端點音訊路徑建立適當的 ACXPIN 物件,並將 ACXPIN 註冊給 ACX。 ACXCIRCUIT 只負責產生電路的渲染或擷取腳位。 ACX 框架建立了連接並與電路通訊所需的另一個腳位。
串流電路
當端點由單一線路組成時,該線路就是串流線路。
當一個端點由一個或多個裝置驅動程式建立的多個電路組成時,描述該組建端點的 ACXCOMPOSITETEMPLATE 會決定連接這些電路的具體順序。 端點中的第一個線路是端點的串流線路。
串流線路應該使用 AcxRtStreamCreate 來建立 RT 封包串流,來回應 EvtAcxCircuitCreateStream。 使用 AcxRtStreamCreate 建立的 ACXSTREAM 允許串流電路驅動程式分配用於串流的緩衝區,並根據客戶端與硬體需求控制串流流程。
端點中的下列線路應該使用 AcxStreamCreate 來建立基本数据流,以便回應 EvtAcxCircuitCreateStream。 以下電路使用 AcxStreamCreate 建立的 ACXSTREAM 物件,允許驅動程式根據串流狀態變化(如暫停或執行)來配置硬體。
串流 ACXCIRCUIT 會接收第一個建立串流的請求。 要求包括裝置、引腳和資料格式(包括模式)。
音訊路徑中的每個 ACXCIRCUIT 會建立一個 ACXSTREAM 物件,代表該電路的串流實例。 ACX 框架將 ACXSTREAM 物件串連在一起,類似於連結 ACXCIRCUIT 物件的方式。
上下游電路
串流建立從串流電路開始,並按照電路連線的順序轉送至每個下游電路。 連線在通訊等於 AcxPinCommunicationNone 的橋接插針之間建立。 如果驅動程式在電路建立時未新增橋接腳位,則 ACX 框架會為電路建立一個或多個橋接腳位。
對於每個以串流電路開始的迴路,AcxPinTypeSource 橋接腳會連接到下一個下游迴路。 最終電路有一個端點腳位來描述音訊輸出設備(例如該端點是麥克風或喇叭,以及插孔是否已連接)。
對於串流電路後的每個電路,AcxPinTypeSink 橋接腳會連接到下一個上游電路。
串流格式協商
驅動程式透過將每種模式所支援的格式新增至用於使用 AcxPinAssignModeDataFormatList 和 AcxPinGetRawDataFormatList 建立資料流的 ACXPIN,以公告支援的資料流格式。 針對多線路端點,ACXSTREAMBRIDGE 可用來協調 ACX 線路之間的模式和格式支援。 串流電路所產生的串流 ACXPIN 決定了端點支援的串流格式。 下列電路所使用的格式是由端點中前一個電路的橋接針腳所決定。
預設情況下,ACX 框架會在多電路端點的每個迴路之間建立 ACXSTREAMBRIDGE。 預設的 ACXSTREAMBRIDGE 在將串流建立請求轉發到下游電路時,使用 RAW 模式預設的橋接腳位格式。 若上行電路的橋接腳位沒有格式,則使用原始串流格式。 如果下行電路的連接腳位不支援所使用的格式,串流建立就會失敗。
如果裝置線路正在執行資料流格式變更,裝置驅動程式應該將下游格式添加到下游接口。
串流創建
串流建立的第一個步驟是建立端點音訊路徑中每個 ACXCIRCUIT 的 ACXSTREAM 實例。 ACX 呼叫每個迴路的 EvtAcxCircuitCreateStream。 ACX 從頭電路開始,依序呼叫每個電路的 EvtAcxCircuitCreateStream,最後以尾端電路結束。 若要反轉資料流程橋接器的順序,可以在 ACX_STREAM_BRIDGE_CONFIG_FLAGS 中定義的 AcxStreamBridgeInvertChangeStateSequence 旗標中進行指定。 所有電路建立串流物件後,串流物件會處理串流邏輯。
串流建立請求會被發送到在主機電路建立過程中藉由呼叫 EvtAcxCircuitCreateStream 所指定的 PIN,此 PIN 是主機電路拓撲生成的一部分所產生的。
串流線路是一開始處理串流建立要求的上游線路。
- 它會更新 ACXSTREAM_INIT 結構,並指派 AcxStreamCallbacks 和 AcxRtStreamCallbacks。
- 它會使用 AcxRtStreamCreate 建立 ACXSTREAM 物件
- 它會建立任何數據流專屬的元素(例如 ACXVOLUME 或 ACXAUDIOENGINE)
- 它會將元素新增至 ACXSTREAM 物件
- 它會將建立的 ACXSTREAM 物件傳回至 ACX 架構
然後,ACX 會將流建立轉送至下游線路。
- 它會更新 ACXSTREAM_INIT 結構體,並賦予 AcxStreamCallbacks 參數。
- 它會使用 AcxStreamCreate 建立 ACXSTREAM 物件
- 它會建立任何串流特定元素
- 它會將元素新增至 ACXSTREAM 物件
- 它會將建立的 ACXSTREAM 物件傳回至 ACX 架構
音訊路徑中線路之間的通訊通道會使用 ACXTARGETSTREAM 物件。 每個迴路都能存取其前方迴路與後方迴路的 IO 佇列,該迴路位於端點音訊路徑中。 端點音訊路徑是線性且雙向的。 ACX 框架負責實際的 IO Queue 處理。
建立 ACXSTREAM 物件時,每個電路都可以將 Context 資訊新增至 ACXSTREAM 物件,以儲存和追蹤資料流程的專用數據。
渲染串流範例
在由三個電路組成的端點音頻路徑上建立渲染流:DSP、編解碼器和放大器。 DSP 線路運作為串流線路,並提供 EvtAcxPinCreateStream 處理常式。 DSP 電路同時也作為濾波電路:根據串流模式與配置,可對音訊資料進行訊號處理。 編解碼器電路代表 DAC,提供音訊接收器功能。 AMP 電路代表 DAC 和揚聲器之間的模擬硬件。 AMP電路可能會處理插孔檢測或其他端點硬體詳細資訊。
- AudioKSE 會呼叫 NtCreateFile 來建立串流。
- 此過程會經過 ACX 篩選,最後以呼叫 DSP 電路的 EvtAcxPinCreateStream(包含腳位、資料格式(含模式)及裝置資訊作結。
- DSP電路會驗證資料格式資訊,以確保它可以處理建立的串流。
- DSP 電路會建立 ACXSTREAM 物件來代表資料流。
- DSP 線路會配置私人內容結構,並將它與 ACXSTREAM 產生關聯。
- DSP 線路會將執行流程傳回至 ACX 架構,然後呼叫端點音訊路徑中的下一個線路,即 CODEC 線路。
- 編解碼器電路會驗證資料格式資訊,以確認它可以處理轉譯資料。
- 編解碼器線路會配置私人內容結構,並將它與 ACXSTREAM 產生關聯。
- 編解碼器電路會將自身作為資料流接收器新增到 ACXSTREAM。
- 編解碼器線路會將執行流程傳回給 ACX 架構,然後呼叫端點音訊路徑中的下一個線路,即 AMP 線路。
- AMP 線路會配置私人內容結構,並將其與 ACXSTREAM 相關聯。
- AMP電路將執行流程傳回ACX框架。 至此,串流建立完成。
大型緩衝串流
在 ACXCIRCUIT 的 ACXAUDIOENGINE 元件指定卸載的 ACXPIN 上建立大容量緩衝串流。
為了支援卸載串流,裝置驅動程式在串流電路建立時應執行以下操作:
- 建立主機、卸載和回送 ACXPIN 物件,並將其新增至 ACXCIRCUIT。
- 建立 ACXVOLUME、ACXMUTE 和 ACXPEAKMETER 元素。 這些不會直接加入 ACXCIRCUIT。
- 初始化 ACX_AUDIOENGINE_CONFIG結構,指派 HostPin、OffloadPin、LoopbackPin、VolumeElement、MuteElement 和 PeakMeterElement 物件。
- 建立 ACXAUDIOENGINE 元素。
驅動程式在 Offload 腳位建立串流時,也需執行類似步驟,以新增 ACXSTREAMAUDIOENGINE 元素。
串流資源分配
ACX 的串流模型是以封包為基礎,支援串流的一或兩個封包。 串流電路的渲染或擷取 ACXPIN 接收到分配資料流中所需記憶體資料包的請求。 若要支援重新平衡,配置的記憶體必須是系統記憶體,而不是對應至系統的裝置記憶體。 驅動程式可以使用現有的 WDF 函式來執行分配,並回傳一組指標指向緩衝區分配。 若驅動程式需要單一連續區塊,則可將兩個封包分配為單一緩衝區。 第二個封包有 WdfMemoryDescriptorTypeInvalid ,第二個封包的偏移量進入第一個封包描述的緩衝區。
若分配單個封包,驅動程式必須分配一個頁面對齊的緩衝區,其長度必須是頁面的倍數。 單一封包的偏移量也必須為 0。 ACX 框架將此封包連續兩次映射為使用者模式:
|資料包 0 |資料包 0 |
這使得 GetBuffer 能夠回傳一個指標,指向一個連續的記憶體緩衝區,該緩衝區可以從緩衝區的末端延伸到起始,而無需應用程式處理記憶體存取的包裝。
若分配兩個封包,則會映射為使用者模式:
|資料包 0 |資料包 1 |
使用初始 ACX 封包串流時,一開始只會配置兩個封包。 配置與映射完成後,客戶端虛擬記憶體映射在串流生命週期內保持有效且不會改變。 串流中有一個事件,表示兩個封包都已完成。 ACX 框架也有一個共享緩衝區,用來溝通哪個封包完成了事件。
當 PacketCount=1 時,如果應用程式要求 10 毫秒的資料,音訊堆疊會向驅動程式發送單一 10 毫秒緩衝區的請求(不會將緩衝區大小加倍)。
驅動程式會分配一個至少 10 毫秒長度的頁面對齊緩衝區。 對於每個 48k 2ch 2 位元組的取樣串流,可分配的最小定時器驅動緩衝區為 1,024 個樣本(一頁記憶體),等於 21.333 毫秒。 對於每個取樣串流 48k 8ch 2 位元組,可分配的最小定時器驅動緩衝區為 512 個樣本(一頁記憶體),約 10.667 毫秒。 對於每個取樣串流 48k 6ch 2 位元組,最小的計時器驅動緩衝區仍為 1,024 個樣本(三頁記憶體,以確保取樣點末端與緩衝區末端對齊),即 21.333 毫秒。
ACX 框架會將這個頁面對齊的緩衝區接連兩次映射到使用者模式程序中。 使用者模式程序可以將最多一個緩衝區的資料寫入使用者模式的映射,從緩衝區的任意位置開始,無需進行任何包裝。
驅動程式在從系統記憶體讀取整個封包後呼叫 NotifyPacketComplete,讓系統知道可以將下一個音訊封包寫入封包的緩衝區。
NotifyPacketComplete 和最後一個封包樣本被渲染之間會有延遲。 延遲以EvtAcxStreamGetHwLatency的結果表示。
乒乓緩衝器
可以使用乒乓緩衝區,其中一個緩衝區被讀取(ping),另一個緩衝區被填充(pong)。 這樣一來,一個緩衝區可以被處理,另一個緩衝區則收集下一組資料。 在 ACX 中,當緩衝區被填滿時,驅動程式會內部負責切換。 ping 緩衝區填滿後,會收到註冊回撥通知。 在回調過程中,會取得處理中的緩衝區位址,並重新提交緩衝區。 同時,pong 緩衝區在背景中收集資料。 此機制確保資料處理持續且不中斷。
對於乒乓緩衝區,請求的封包大小是單一緩衝區(ping 或 pong 型),封包數量為二。
當兩個封包共用單一緩衝區時,請按照 EVT_ACX_STREAM_ALLOCATE_RTPACKETS 回調函式中的描述來配置第二個封包。 第一個封包描述的緩衝區部分(記憶體、偏移量與長度)稱為 ping 緩衝區,而第二個封包描述的部分(無記憶體顯示緩衝區與第一個封包共享,加上指向第一個封包後緩衝區的偏移量)稱為 pong 緩衝區。
在封包標頭中加入額外資訊
對於 ping/pong 事件驅動的串流(封包計數 = 2),只能在封包的開頭新增額外的資訊,例如用於日誌或簿記。 對於只有一個封包的計時器驅動串流,封包必須完全對齊頁面(起點與終點皆在頁面邊界),因為封包會被映射兩次進入使用者模式。
在這種情況下,應用程式可以將第一個映射結束後寫入第二個映射,第二個映射先在系統緩衝區末端寫入,再寫入同一系統緩衝區的起始。
單一分配的緩衝區必須進行頁面對齊,因為虛擬記憶體映射到使用者模式是逐頁進行的。
定時器驅動緩衝區
ACX 中的定時器驅動緩衝區可透過維持精確的時序與同步,確保音訊體驗無故障。 對於 ACX 中以計時器驅動的緩衝區:
- 用戶端會根據 EvtAcxStreamGetPresentationPosition 的值來判斷可寫入多少幀。
- 每次處理緩衝區時,呈現位置需要更新多次。 用戶端會從它最後寫入的位置開始,透過驅動程式回報的位置(應該是硬體自上次查詢該位置以來所消耗的資料)寫入緩衝區。
- 位置越細緻,遇到故障的機率就越低。
- 在計時器驅動的緩衝區中,DSP 不能在更新位置前就用掉整個緩衝區。
- 在計時器驅動模式下,驅動程式可以將一個計時器驅動的緩衝區拆分成多個 DSP 緩衝區,隨著 DSP 在每個緩衝區中運作時更新位置(例如,一個 20 毫秒的計時器驅動緩衝區拆分成 10 個 2 毫秒緩衝區,在定時器驅動模式下表現相當良好)。
大型緩衝流的封包大小
當公開大型緩衝區的支援時,驅動程式還會提供一個回調,用於判斷大型緩衝區播放中封包大小的下限和上限。
流緩衝區分配的封包大小是根據最小值和最大值來確定的。
由於最小與最大緩衝區大小可能具有揮發性,若最小與最大緩衝區大小有變動,驅動程式可能會失敗封包配置呼叫。
指定 ACX 緩衝區限制
若要指定 ACX 緩衝區條件約束,ACX 驅動程式可以使用 KS/PortCls 屬性設定 - KSAUDIO_PACKETSIZE_CONSTRAINTS2 和 KSAUDIO_PACKETSIZE_PROCESSINGMODE_CONSTRAINT 結構。
下列程式碼範例示範如何針對不同的訊號處理模式設定 WaveRT 緩衝區的緩衝區大小限制。
//
// Describe buffer size constraints for WaveRT buffers
// Note: 10msec for each of the Modes is the default system behavior.
//
static struct
{
KSAUDIO_PACKETSIZE_CONSTRAINTS2 TransportPacketConstraints; // 1
KSAUDIO_PACKETSIZE_PROCESSINGMODE_CONSTRAINT AdditionalProcessingConstraints[4]; // + 4 = 5
} DspR_RtPacketSizeConstraints =
{
{
10 * HNSTIME_PER_MILLISECOND, // 10 ms minimum processing interval
FILE_BYTE_ALIGNMENT, // 1 byte packet size alignment
0, // no maximum packet size constraint
5, // 5 processing constraints follow
{
STATIC_AUDIO_SIGNALPROCESSINGMODE_RAW, // constraint for raw processing mode
0, // NA samples per processing frame
10 * HNSTIME_PER_MILLISECOND, // 100000 hns (10ms) per processing frame
},
},
{
{
STATIC_AUDIO_SIGNALPROCESSINGMODE_DEFAULT, // constraint for default processing mode
0, // NA samples per processing frame
10 * HNSTIME_PER_MILLISECOND, // 100000 hns (10ms) per processing frame
},
{
STATIC_AUDIO_SIGNALPROCESSINGMODE_COMMUNICATIONS, // constraint for movie communications mode
0, // NA samples per processing frame
10 * HNSTIME_PER_MILLISECOND, // 100000 hns (10ms) per processing frame
},
{
STATIC_AUDIO_SIGNALPROCESSINGMODE_MEDIA, // constraint for default media mode
0, // NA samples per processing frame
10 * HNSTIME_PER_MILLISECOND, // 100000 hns (10ms) per processing frame
},
{
STATIC_AUDIO_SIGNALPROCESSINGMODE_MOVIE, // constraint for movie movie mode
0, // NA samples per processing frame
10 * HNSTIME_PER_MILLISECOND, // 100000 hns (10ms) per processing frame
},
}
};
DSP_DEVPROPERTY結構用於儲存約束。
typedef struct _DSP_DEVPROPERTY {
const DEVPROPKEY *PropertyKey;
DEVPROPTYPE Type;
ULONG BufferSize;
__field_bcount_opt(BufferSize) PVOID Buffer;
} DSP_DEVPROPERTY, PDSP_DEVPROPERTY;
並創建了這些結構的陣列。
const DSP_DEVPROPERTY DspR_InterfaceProperties[] =
{
{
&DEVPKEY_KsAudio_PacketSize_Constraints2, // Key
DEVPROP_TYPE_BINARY, // Type
sizeof(DspR_RtPacketSizeConstraints), // BufferSize
&DspR_RtPacketSizeConstraints, // Buffer
},
};
稍後在 EvtCircuitCompositeCircuitInitialize 函式中,AddPropertyToCircuitInterface 協助程式函式可用來將介面屬性陣列新增至線路。
// Set RT buffer constraints.
//
status = AddPropertyToCircuitInterface(Circuit, ARRAYSIZE(DspC_InterfaceProperties), DspC_InterfaceProperties);
AddPropertyToCircuitInterface 輔助函式會接受線路的 AcxCircuitGetSymbolicLinkName,然後呼叫 IoGetDeviceInterfaceAlias 來尋找線路所使用的音訊介面。
然後,SetDeviceInterfacePropertyDataMultiple 函式會呼叫 IoSetDeviceInterfacePropertyData 函式 來修改裝置介面屬性的目前值 - ACXCIRCUIT 音訊介面上的 KS 音訊屬性值。
PAGED_CODE_SEG
NTSTATUS AddPropertyToCircuitInterface(
_In_ ACXCIRCUIT Circuit,
_In_ ULONG PropertyCount,
_In_reads_opt_(PropertyCount) const DSP_DEVPROPERTY * Properties
)
{
PAGED_CODE();
NTSTATUS status = STATUS_UNSUCCESSFUL;
UNICODE_STRING acxLink = {0};
UNICODE_STRING audioLink = {0};
WDFSTRING wdfLink = AcxCircuitGetSymbolicLinkName(Circuit);
bool freeStr = false;
// Get the underline unicode string.
WdfStringGetUnicodeString(wdfLink, &acxLink);
// Make sure there is a string.
if (!acxLink.Length || !acxLink.Buffer)
{
status = STATUS_INVALID_DEVICE_STATE;
DrvLogError(g_BthLeVDspLog, FLAG_INIT,
L"AcxCircuitGetSymbolicLinkName failed, Circuit: %p, %!STATUS!",
Circuit, status);
goto exit;
}
// Get the audio interface.
status = IoGetDeviceInterfaceAlias(&acxLink, &KSCATEGORY_AUDIO, &audioLink);
if (!NT_SUCCESS(status))
{
DrvLogError(g_BthLeVDspLog, FLAG_INIT,
L"IoGetDeviceInterfaceAlias failed, Circuit: %p, symbolic link name: %wZ, %!STATUS!",
Circuit, &acxLink, status);
goto exit;
}
freeStr = true;
// Set specified properties on the audio interface for the ACXCIRCUIT.
status = SetDeviceInterfacePropertyDataMultiple(&audioLink, PropertyCount, Properties);
if (!NT_SUCCESS(status))
{
DrvLogError(g_BthLeVDspLog, FLAG_INIT,
L"SetDeviceInterfacePropertyDataMultiple failed, Circuit: %p, symbolic link name: %wZ, %!STATUS!",
Circuit, &audioLink, status);
goto exit;
}
status = STATUS_SUCCESS;
exit:
if (freeStr)
{
RtlFreeUnicodeString(&audioLink);
freeStr = false;
}
return status;
}
串流狀態變更
當串流狀態發生變化時,該串流端點音訊路徑中的每個串流物件都會收到來自 ACX 框架的通知事件。 發生此情況的順序取決於狀態變更和資料流的順序。
對於從較不活躍狀態轉為較活躍狀態的渲染串流,串流電路(負責註冊 SINK)會先接收事件。 當電路處理完事件後,端點音訊路徑中的下一個電路會接收該事件。
對於從較活躍狀態轉換到較不活躍狀態的渲染串流,串流電路會最後接收事件。
對於從較不活躍狀態轉為較活躍狀態的擷取串流,串流電路最後接收事件。
對於從較活躍狀態切換到較不活躍狀態的擷取串流,串流電路會先接收事件。
排序是 ACX 框架的預設設定。 驅動程式在建立新增至串流電路的 ACXSTREAMBRIDGE 時,透過設定在 ACX_STREAM_BRIDGE_CONFIG_FLAGS 中定義的 AcxStreamBridgeInvertChangeStateSequence,以請求相反的行為。
串流音訊資料
建立串流並分配適當的緩衝區後,串流會處於暫停狀態,等待串流開始。 當用戶端將串流置於播放狀態時,ACX 框架會呼叫所有與串流相關的 ACXSTREAM 物件,以表示串流狀態已進入播放狀態。 接著將 ACXPIN 置於播放狀態,資料開始流動。
渲染音訊資料
建立串流並分配資源後,應用程式會呼叫串流的 Start 開始播放。 應用程式應在開始串流前呼叫 GetBuffer/ReleaseBuffer,以確保第一個開始播放的封包有有效的音訊資料。
客戶端首先預加載緩衝區。 當用戶端呼叫 ReleaseBuffer 時,這會被轉換成在 AudioKSE 的一次呼叫,然後進一步呼叫到 ACX 層,在活動中的 ACXSTREAM 上進而呼叫 EvtAcxStreamSetRenderPacket。 此特性包含封包索引(以零為基礎),以及(如適當)包含當前封包串流末端位元組偏移的 EOS 旗標。
串流電路完成封包後,會觸發緩衝區完成通知,釋放等待用渲染音訊資料填補下一個封包的用戶端。
支援計時器驅動串流模式,並透過呼叫驅動程式的 EvtAcxStreamAllocateRtPackets 回撥時,使用 PacketCount 值 1 來表示。
擷取音訊資料
當串流運行時,來源電路會將擷取封包填入音訊資料。 第一個封包填滿後,來源電路會將封包釋放給 ACX 框架。 此時,ACX 框架會發出串流通知事件的訊號。
串流通知發出後,客戶端可以發送 KSPROPERTY_RTAUDIO_GETREADPACKET ,取得已擷取封包的索引(零為基礎)。 當用戶端發送 GETCAPTUREPACKET 時,驅動程式可以假設所有先前的封包都已處理並可填寫。
針對連拍擷取,源電路可在呼叫 GETREADPACKET 後立即將新的封包釋放至 ACX 架構。
用戶端也可以使用 KSPROPERTY_RTAUDIO_PACKETVREGISTER 來取得資料流 RTAUDIO_PACKETVREGISTER 結構的指標。 ACX 框架會在訊號表示封包完成前更新此結構。
舊版 KS (核心串流) 行為
有時候,例如當一個驅動程式實作突發擷取(如關鍵字偵測器)時,你需要使用傳統核心的串流封包處理行為,而不是 PacketVRegister。 若要使用先前基於封包的行為,驅動程式會針對 KSPROPERTY_RTAUDIO_PACKETVREGISTER 回傳 STATUS_NOT_SUPPORTED。
下列範例示範如何在 ACXSTREAM 的 AcxStreamInitAssignAcxRequestPreprocessCallback 中執行此動作。 欲了解更多資訊,請參閱 AcxStreamDispatchAcxRequest。
Circuit_EvtStreamRequestPreprocess(
_In_ ACXOBJECT Object,
_In_ ACXCONTEXT DriverContext,
_In_ WDFREQUEST Request)
{
ACX_REQUEST_PARAMETERS params;
PCIRCUIT_STREAM_CONTEXT streamCtx;
streamCtx = GetCircuitStreamContext(Object);
// The driver would define the pin type to track which pin is the keyword pin.
// The driver would add this to the driver-defined context when the stream is created.
// The driver would use AcxStreamInitAssignAcxRequestPreprocessCallback to set
// the Circuit_EvtStreamRequestPreprocess callback for the stream.
if (streamCtx && streamCtx->PinType == CapturePinTypeKeyword)
{
if (IsEqualGUID(params.Parameters.Property.Set, KSPROPSETID_RtAudio) &&
params.Parameters.Property.Id == KSPROPERTY_RTAUDIO_PACKETVREGISTER)
{
status = STATUS_NOT_SUPPORTED;
outDataCb = 0;
WdfRequestCompleteWithInformation(Request, status, outDataCb);
return;
}
}
(VOID)AcxStreamDispatchAcxRequest((ACXSTREAM)Object, Request);
}
溪流位置
ACX 框架會呼叫 EvtAcxStreamGetPresentationPosition 回調來取得目前的串流位置。 目前的串流位置包含 PlayOffset 和 WriteOffset。
WaveRT 串流模型允許音訊驅動程式向用戶端暴露硬體位置暫存器。 ACX 串流模型不支援暴露硬體暫存器,因為這會阻止重新平衡的發生。
每當串流電路完成一個封包時,會呼叫 AcxRtStreamNotifyPacketComplete ,並以以零為基礎的封包索引及盡可能接近封包完成的 QPC 值(例如,中斷服務例程可以計算 QPC 值)。 用戶端可透過 KSPROPERTY_RTAUDIO_PACKETVREGISTER 取得此資訊,回傳一個包含 CompletedPacketCount、CompletedPacketQPC 及結合兩者的值的指標(以便用戶端確認 CompletedPacketCount 與 CompletedPacketQPC 是否來自同一封包)。
數據流狀態轉換
建立資料流程之後,ACX 會使用下列回呼將資料流程轉換成不同的狀態:
- EvtAcxStreamPrepareHardware 會將串流從 AcxStreamStateStop 狀態轉換到 AcxStreamStatePause 狀態。 驅動程式在接收到 EvtAcxStreamPrepareHardware 時,應該保留如 DMA 引擎之類的必要硬體。
- EvtAcxStreamRun 將串流從 AcxStreamStatePause 狀態轉換到 AcxStreamStateRun 狀態。
- EvtAcxStreamPause 將串流從 AcxStreamStateRun 狀態轉換到 AcxStreamStatePause 狀態。
- EvtAcxStreamReleaseHardware 將串流從 AcxStreamStatePause 狀態轉換到 AcxStreamStateStop 狀態。 驅動程式應該在收到 EvtAcxStreamReleaseHardware 時釋放必要的硬體,例如 DMA 引擎。
串流可能會在收到 EvtAcxStreamReleaseHardware 回調後,接收到 EvtAcxStreamPrepareHardware 回調。 這會將串流轉換回 AcxStreamStatePause 狀態。
EvtAcxStreamAllocateRtPackets 的封包分配通常在第一次呼叫 EvtAcxStreamPrepareHardware 之前進行。 分配的封包通常在最後一次呼叫 EvtAcxStreamReleaseHardware 後,透過 EvtAcxStreamFreeRtPackets 釋放。 這種排序並不保證。
不會使用 AcxStreamStateAcquire 狀態。 ACX 消除了驅動程式必須具備獲取狀態的需求,因為這個狀態是在準備硬體(EvtAcxStreamPrepareHardware)和釋放硬體(EvtAcxStreamReleaseHardware)回調中隱含的。
大型緩衝流與卸載引擎支援
ACX 使用 ACXAUDIOENGINE 元素來指定 ACXPIN,以處理卸載資料流的建立,以及處理卸載資料流音量、靜音和峰值電平狀態所需的各種元素。 這類似於 WaveRT 驅動程式中現有的音訊引擎節點。
串流關閉處理程序
當用戶端關閉串流時,驅動程式會先接收 EvtAcxStreamPause 和 EvtAcxStreamReleaseHardware,然後 ACX 框架會刪除 ACXSTREAM 物件。 驅動程式可以在呼叫 AcxStreamCreate 時,在 WDF_OBJECT_ATTRIBUTES結構 中提供標準 WDF EvtCleanupCallback 專案,以執行 ACXSTREAM 的最終清除。 當框架嘗試刪除物件時,WDF 會呼叫 EvtCleanupCallback。 不要使用 EvtDestroyCallback,因為它只有在所有物件引用都釋放後才會被呼叫,且是不確定的。
如果 EvtAcxStreamReleaseHardware 尚未清理與 ACXSTREAM 物件相關的系統記憶體資源,驅動程式應在 EvtCleanupCallback 中清理與 ACXSTREAM 物件相關的資源。
驅動程式不應該在客戶端請求前清理支援串流的資源。
不會使用 AcxStreamStateAcquire 狀態。 ACX 消除了驅動程式必須具備獲取狀態的需求,因為這個狀態被隱含在準備硬體(EvtAcxStreamPrepareHardware)和釋放硬體(EvtAcxStreamReleaseHardware)回呼中。
串流意外移除與無效化
若驅動程式判定串流無效(例如插頭拔出),電路會關閉所有串流。
串流記憶體清理
數據流資源的處置可以在驅動程序的數據流內容清除中完成(而非終結)。 不要將任何共享的資源的釋放操作放在物件的銷毀回調函數中。 此指引適用於所有 ACX 物件。
銷毀回調是在最後一個引用消失後被呼叫,這是不可預測的。
一般而言,當控制碼關閉時,資料流的清理回呼會被呼叫。 一個例外是當驅動程式在回調時建立串流。 如果 ACX 在從建立串流操作返回前未能將此串流加入其串流橋接器,該串流會被非同步取消,且當前執行緒會向建立串流客戶端回傳錯誤。 此時串流不應有任何記憶體分配。 如需詳細資訊,請參閱 EVT_ACX_STREAM_RELEASE_HARDWARE 回呼。
串流記憶體清理序列
串流緩衝區是系統資源,只有當使用者模式用戶端關閉串流的句柄時才應該釋放它。 緩衝區(與裝置的硬體資源不同)的壽命與資料流的控制代碼相同。 當客戶端關閉 handle 時,ACX 會呼叫串流物件的清理回調,然後當物件的參考計數歸零時,再呼叫串流物件的刪除回調。
當驅動建立 stream-obj 但 create-stream 回調失敗時,ACX 可能會將 STREAM obj 刪除延後給工作項目。 為了防止關閉 WDF 執行緒發生死結,ACX 會將刪除延遲至不同的執行程。 為避免此行為的任何可能副作用(即延遲釋放資源),驅動程式可以在資料流建立過程中返回錯誤之前,先行釋放已配置的資料流資源。
當 ACX 叫用 EVT_ACX_STREAM_FREE_RTPACKETS 回呼時,驅動程式必須釋放音訊緩衝區。 當使用者關閉串流控制碼時,將會觸發此回調。
由於 RT 緩衝區是在使用者模式下映射的,緩衝區的壽命與句柄的壽命相同。 驅動程式不應該在 ACX 調用此回調函數前釋放音訊緩衝區。
EVT_ACX_STREAM_FREE_RTPACKETS回呼 應在 EVT_ACX_STREAM_RELEASE_HARDWARE回呼 之後呼叫,並在 EvtDeviceReleaseHardware 之前結束。
此回呼可能發生在驅動程式處理 WDF 釋放硬體回呼後,這是因為使用者模式的客戶端能長時間保留其控制代碼。 駕駛不應該等這些把手消失。 此動作會產生0x9f DRIVER_POWER_STATE_FAILURE錯誤檢查碼。 如需詳細資訊,請參閱 EVT_WDF_DEVICE_RELEASE_HARDWARE 回呼函式。
這段來自範例 ACX 驅動程式的 EvtDeviceReleaseHardware 程式碼,展示了呼叫 AcxDeviceRemoveCircuit 然後釋放串流硬體記憶體的範例。
RETURN_NTSTATUS_IF_FAILED(AcxDeviceRemoveCircuit(Device, devCtx->Render));
RETURN_NTSTATUS_IF_FAILED(AcxDeviceRemoveCircuit(Device, devCtx->Capture));
// NOTE: Release streaming h/w resources here.
CSaveData::DestroyWorkItems();
CWaveReader::DestroyWorkItems();
總結:
- WDF 裝置釋放硬體:釋放裝置的硬體資源。
- AcxStreamFreeRtPackets:釋放或解放與控制代碼相關的音訊緩衝區。
欲了解更多管理 WDF 與電路物件的資訊,請參閱 ACX WDF 驅動程式生命週期管理。
串流 DDI
串流結構
ACX_RTPACKET結構
此結構代表單一已配置的封包。 PacketBuffer 可以是 WDFMEMORY 控制碼、MDL 或緩衝區。 它有一個關聯的初始化函數 ACX_RTPACKET_INIT。
ACX_STREAM_CALLBACKS
此結構會識別串流至 ACX 架構的驅動程式回呼。 這個結構是 ACX_PIN_CONFIG 結構的一部分。
串流回呼
EvtAcxStreamAllocateRtPackets
EvtAcxStreamAllocateRtPackets 事件會告知驅動程式配置 RtPackets 以供串流處理。 對於事件驅動的串流,AcxRtStream 會接收到 PacketCount = 2,或以計時器為基礎的串流,PacketCount = 1。 如果驅動程式針對這兩個封包使用單一緩衝區,則第二個 RtPacketBuffer 應該具有類型 = WdfMemoryDescriptorTypeInvalid 的 WDF_MEMORY_DESCRIPTOR,其 RtPacketOffset 與第一個封包結尾對齊(packet[2].RtPacketOffset = packet[1].RtPacketOffset + packet[1].RtPacketSize)。
EvtAcxStreamFreeRtPackets
EvtAcxStreamFreeRtPackets 事件會告知驅動程序釋放先前呼叫 EvtAcxStreamAllocateRtPackets 中所配置的 RtPackets。 包含來自該呼叫的相同封包。
EvtAcxStreamGetHw延遲
EvtAcxStreamGetHwLatency 事件會告知驅動程式為此數據流的特定線路提供數據流延遲(整體延遲將是不同線路的延遲總和)。 FifoSize 以位元組為單位,延遲以 100 納秒為單位。
EvtAcxStreamSetRenderPacket
EvtAcxStreamSetRenderPacket 事件會告知驅動程式用戶端剛剛釋放的封包。 如果沒有問題,此封包應該是 (CurrentRenderPacket + 1),其中 CurrentRenderPacket 是驅動程式目前串流的來源封包。
旗標可以是 0 或 KSSTREAM_HEADER_OPTIONSF_ENDOFSTREAM = 0x200,表示 Packet 是數據流中的最後一個封包,而 EosPacketLength 是封包的有效長度。 欲了解更多資訊,請參閱 KSSTREAM_HEADER 結構體中的OptionsFlags。
驅動程式在封包渲染時持續增加 CurrentRenderPacket,而非調整 CurrentRenderPacket 以匹配此值。
EvtAcxStreamGetCurrentPacket
EvtAcxStreamGetCurrentPacket 告訴驅動程式指示哪個數據包(從零開始計數)目前正在被渲染給硬體,或正由擷取硬體填補。
EvtAcxStreamGetCapturePacket
EvtAcxStreamGetCapturePacket 告訴驅動程式標示最近被填滿的封包(零為基礎),包括驅動程式開始填充封包時的 QPC 值。
EvtAcxStreamGetPresentationPosition
EvtAcxStreamGetPresentationPosition 會告知驅動程式指出目前位置,以及計算目前位置時的 QPC 值。
串流狀態事件
以下 API 管理 ACXSTREAM 的串流狀態。
- EVT_ACX_STREAM_PREPARE_HARDWARE
- EVT_ACX_STREAM_RELEASE_HARDWARE
- EVT_ACX_STREAM_RUN
- EVT_ACX_STREAM_PAUSE
串流 ACX API 應用程式介面
AcxStreamCreate
AcxStreamCreate 會建立 ACX 串流,用來控制串流行為。
AcxRtStreamCreate
AcxRtStreamCreate 會建立一個 ACX 串流,可用來控制串流行為,並處理封包配置以及傳達串流狀態。
AcxRtStreamNotifyPacketComplete
驅動程式會在封包完成時呼叫此 ACX API。 封包完成時間與零基封包索引被納入以提升用戶端效能。 ACX 框架會設定與串流相關的任何通知事件。