本文說明高可用性渲染器 (HAR) 中的鈴聲播放功能。Audio Crate 會向 HAR 應用程式公開 AudioManager,該應用程式可控制鈴聲播放。
為維持低延遲,播放執行緒會在應用程式的整個生命週期中執行,並在沒有音訊播放時閒置及產生。
術語
- 資產
AudioAsset與可播放的音訊有關。資產通常稱為「應用程式執行階段中的資產」。- 裝置
AudioDevice是指用於播放音訊的獨立匯流排。裝置是與系統存取硬體相關的最精細單位。在標準 SDVM 實作中,AudioDevice是指單一進階 Linux 音效架構 (ALSA) PCM。- 串流
- 在裝置上播放資產的執行個體。從排定時間到完成、取消或發生錯誤為止,串流都會持續存在。
元件
圖 1 顯示鈴聲的元件圖:
圖 1. 元件圖。
音訊裝置和 PCM
音訊硬體設定遵循標準 HAR 平台抽象層設計,並包含在 har-platform-api 中。
HAR Audio Crate 會定義 AudioDevice 的新結構,該結構會定義影響內部 HAR Audio Crate 和播放作業的所有資料結構欄位。AudioDevice 也會使用泛型包裝潛在的平台專屬額外參數。如果是 tinyalsa,PlatformAudioDevice 則包含 ALSA PCM 的描述元和屬性。
/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.
AudioDevice<PlatformAudioDevice> {
/// Internal HAR Identifier for the device.
AudioDeviceID,
/// The size (in bytes) for chunks of audio data to stream to the device.
ChunkSize,
/// Properties necessary to control volume (details in "Mixer control" section).
VolumeControl,
/// Properties necessary to control spatialization (details in "Mixer control"
/// section).
SpatialControl,
/// Platform specific data for the AudioDevice.
/// E.g. ALSA properties and reference to opened PCM.
PlatformAudioDevice
}
/// Elaboration of the previously mentioned VolumeControl
VolumeControl {
/// Identifier for the control used to change volume.
ControlID,
/// Mapping between Decibel and control values. (see Mixer control section)
VolumeOutputIndex
}
音訊素材資源
本節說明如何設定及實作音訊素材資源。
設定
初始 HAR 音訊實作支援靜態設定的音訊資產。JSON 設定會定義可用的素材資源,以及定義為 WAV 檔案的素材資源。
此外,實作作業也支援透過更通用的資產實作作業合成及串流音訊資產,並接受用於產生音訊資料的函式。
導入作業
使用兩個不同的建構函式 (AudioAsset 和 AudioStream) 實作資產。
AudioAsset 可定義資產的靜態屬性,以及與資產相關的潛在內部資料容器。AudioAsset AudioStream 可衍生出 AudioAsset AudioStream,這是資產的單一可串流執行個體。AudioStream 包含與單一串流播放相關的內部狀態。
/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.
/// Static properties and definition of an Asset.
AudioAsset {
/// Perform optional initialization steps, e.g. load bytes from file into memory.
/// Can also define lazy loading, to load data at first playback instead.
fn initialize(LazyLoad);
/// Create a new AudioStream from the asset.
fn create_stream() -> AudioStream;
/// More functions for metadata etc. of the asset.
...
}
/// Single streamable instance of an AudioAsset
AudioStream {
/// Gets the next bytes to play from the Asset together with if the current chunk of
/// bytes contains any control signals (e.g. fade-out).
fn get_playback(num_bytes: usize) -> ([u8], ControlSignals);
/// Gets playback Mode details used to handle special states of playback
/// e.g. when a chime gets is interrupted and put in "fade-out" mode.
fn playback_mode() -> PlaybackMode;
/// [0.0, 1.0] indication of how much of the stream was played.
fn progress() -> f32;
/// Reset the stream, e.g. if it should play again.
fn reset();
/// Time of which the stream was created.
fn created_at() -> Instant;
/// Additional metadata etc. for the stream.
...
}
播放鈴聲
本節說明播放鈴聲的 API 和程序。單一鈴聲播放稱為「串流」。
串流的生命週期
圖 2 說明串流的生命週期:
圖 2. 串流播放和事件。
圖 2 說明這些步驟:
播放:排定要播放的串流。
優先順序:播放優先順序會決定是否要:
- 立即播放鈴聲 (在第一個位元組時啟動事件)
- 稍後播放鈴聲 (已暫停或繼續的活動)
- 降低電鈴優先順序 (已取消的活動)
混音器控制項:視需要根據設定的行為更新混音器控制項。
寫入位元組:將一組位元組寫入
AudioDevice。更多資料:如果串流有更多資料,請返回步驟 2。
重複:如要重複播放串流,請重設並返回步驟 2 (重新啟動活動)。
已完成:串流已順利完成 (
FinishedSuccessfully事件)。
你隨時可以暫停、繼續或停止通話,中斷鈴聲。
鈴聲優先順序
這項邏輯會設定鈴聲優先順序:
覆寫播放模式。舉例來說,在淡出模式中,鈴聲一律會獲得最高優先順序,直到淡出完成為止。
指定的優先順序。
如果優先順序相同,系統會先播放較新的鈴聲。
如果鈴聲的優先順序相同,系統會使用 enum 值例項化 AudioManager。
API
事件
如果鈴聲開始時提供事件管道,HAR Audio 會在播放期間發出多個事件。支援的事件如以下範例所示:
/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.
StreamBehaviors<PlatformStreamBehaviors> {
/// What should happen if the stream is interrupted for a higher priority stream.
/// e.g. pause-and-resume or cancel, will also define preference for fade-out.
OverrunBehavior,
/// Urgency, if interrupted streams are allowed to "fade-out", or if the stream should
/// urgently disrupt any other playback.
Optional<Urgency>,
/// Priority for the stream (or minimum if not specified).
Optional<StreamPriority>
/// Descriptor if a stream should be played on repeat.
Optional<RepeatBehavior>
/// Volume, if the stream should play at a specific volume.
Optional<Volume>
/// Spatialization, if the stream should play with specific spatialization.
Optional<Spatialization>
/// Optional generic for future expandability of the API, or pass-through of platform
/// specific Stream Behaviors
Optional<PlatformStreamBehaviors>
}
/// Plays a chime on specified device with given behaviors. StreamEvents are delivered
/// using the provided event transmitter. This method won't wait for any events.
fn play(AudioDeviceID, AssetID, StreamBehaviors, Option<EventTransmitter>) -> StreamController
/// Object used to control a Stream.
StreamController {
/// Gets the current state/metadata of a stream (e.g. ID, progress, playback_state).
fn metadata() -> StreamMetadata
/// Stops the stream.
fn stop()
/// Pauses a given stream, if the specified duration expires the stream is cancelled.
/// Timeout is required to make sure there are no paused streams left indefinitely
/// pending resumption.
fn pause(TimeoutDuration)
/// Resumes a paused stream.
fn resume()
/// Updates the spatialization of a playing stream.
fn set_spatialization(Spatialization)
/// Updates the volume of a playing stream.
fn set_volume(Volume)
}
混音器控制
本節說明如何控制音量和空間化。
音量
HAR 會以毫貝為單位,一致地定義音量。har-platform-api Crate
會處理從毫貝到控制信號的轉換。
毫貝與硬體輸出功率之間的關係為對數,且不同硬體和音箱設定的差異極大。因此,請在 AudioDevice (音訊裝置和 PCM) 設定中提供值之間的設定,且轉換必須在呼叫平台層之前進行。
因此,PAL API 中的實作會定義兩個函式。
fn set_volume_millibel(AudioDeviceID, Millibel) {
/// Default implementation with conversion using DeviceConfig.
}
fn set_volume_control(AudioDeviceID, ControlValue);
set_volume_millibel 的預設實作方式會使用為 AudioDevice 提供的設定,包括一組用於參照毫貝的鍵/值組合,將毫貝轉換為控制值,然後使用轉換後的值呼叫 set_volume_control 函式。
這項設計提供預設值,並允許後續實作項目覆寫預設對應。
圖 3. HAR 音訊流程。
空間化
Audio API 會公開功能,用來控制音訊資料應在哪個空間區域播放。這些參數會傳遞至 PAL 層,並使用硬體控制項套用至下游。選項定義為 PAL API 的一部分,如下所示:
/// NOTE: The following code is a sample definition to help understanding, it is not a
/// representation of the final code/implementation.
enum Spatialization {
Front,
FrontLeft,
FrontRight,
Center, // No spatialization
Rear,
RearLeft,
RearRight,
Right,
Left
}
混音器控制層級
您可以為資產和串流定義音量和空間化。如果定義串流優先順序,串流會覆寫資產定義的控制項。
執行緒管理
音訊管理工具會為每個 AudioDevice 例項維護一個執行緒。每個執行緒都會獨立運作。AudioManager 與播放執行緒之間的互動會使用依優先順序排序的共用串流佇列。
ALSA 呼叫會使用輪詢的 ASYNC 寫入,判斷資料何時消化。
圖 4. 執行緒管理序列。
輪詢期間的控制信號
等待音效卡消化位元組時,可以發出控制信號。例如變更音訊的淡出或空間化效果。輪詢以取得音訊裝置狀態的設定層級為 AudioManager,或預設為 1 毫秒。每個輪詢週期結束後,播放執行緒會摘要並發出任何時間控制指令。
緩衝區管理
為盡量縮短中斷延遲時間,寫入裝置的緩衝區大小會保持在較小範圍。使用 TinyALSA 做為預設值時,緩衝區空間會設為與 startup_threshold 參數相同。TinyALSA 會將分配到的整個裝置緩衝區除以 2,做為預設值。
串流中斷
如果串流中斷,串流會維持執行緒優先順序,直到寫入至卡的資料排空為止。因此,中斷和新串流之間會有一段轉換期。
舉例來說,如果 HAR 中的音訊樣本使用:
- 大小為 3,072
- 48,000 的費率
- 樣本大小為 2
待處理緩衝區會計算為 3,072 和 6,144 個影格,因此中斷延遲時間為 64 到 128 毫秒。實際運作時,緩衝區會較小。
錯誤管理和風險
本節說明如何管理錯誤和潛在風險。
過時的串流和佇列資源不足
由於 AudioStream 可以暫停,且只能從最高優先順序的 AudioStream 執行個體播放,因此可能會發生佇列不斷增長,導致低優先順序的串流無法播放。
為避免發生這種情況,每個佇列的大小上限都可設定。超過這個值時,系統會捨棄優先順序最低的串流。
監控與快訊
在實際工作環境中,安全監控器會追蹤音訊功能,確保播放作業如預期進行。
AudioManager 會監控與延遲時間相關的內部統計資料,以及定義記錄效能的旗標。設定這些門檻後,系統會在下列情況為所有偵錯版本產生警告記錄:
- 排定播放時間與開始播放之間的時間長度超過
x毫秒。 - (適用於未中斷的串流) 資產長度和播放時間的差異超過
y%。
裝置已封鎖
音訊裝置一律有小機率會停止回應,舉例來說,如果系統中的其他程序分配並寫入音訊裝置,就會發生這種情況。由於播放作業會在個別執行緒中非同步執行,且鈴聲可以排隊稍後播放,因此對呼叫應用程式而言,這完全是透明的。
為偵測此情況,系統會在排定播放新鈴聲時進行執行緒健康狀態檢查,如果播放執行緒的佇列已填滿,且在過去一秒內未消化任何新位元組,就會傳回錯誤。
為因應日後需求,您可能需要嘗試重新啟動 / 開啟裝置,但就初始實作而言,錯誤不應隱藏。
程式碼結構
大致來說,與鈴聲播放相關的程式碼存在於下列 Crate 中:
CRATE: display-safety/crates/(harry-app|harry)
現有的 HAR 應用程式,會發出鈴聲呼叫。
NEW CRATE: display-safety/crates/audio
新功能:Crate 可管理音訊控制和播放 (大部分功能都在這裡)。
CRATE: display-safety/crates/har-platform-api/audio
PAL,包括音訊所需的所有系統呼叫。
CRATE: display-safety/crates/har-platform-(android|linux)/audio
使用 TinyALSA 呼叫 tinyalsa-rs 進行播放。初始解決方案未實作 QNX 支援,隨著支援的平台越來越多,這項功能也會隨之擴展。
TINYALSA PAL: display-safety/crates/tinyalsa-audio
TinyALSA 專用的播放程式碼。Android 和 Linux 平台實作項目會使用這項資訊。
CRATE: display-safety/crates/tinyalsa-rs
TinyALSA C 實作項目的 Rust 繫結
Rust 實作詳細資料
以下是幾項具體的實作詳情:
- 所有 API 函式都會傳回
Result<X, AudioError>,其中X是 () 或傳回值。 - 沒有任何 API 函式標示為
unsafe。 - 互斥和同步機制是內部機制,不會在
AudioManagerAPI 中公開。
擁有權模式和 AudioManager
應用程式與音訊系統的所有互動,都是透過
AudioManager或AudioManager傳回的物件進行。AudioManager是執行緒安全。AudioManager會在 HARry 應用程式中執行個體化一次,而Moved則會為了讓Looper擁有所有權而執行個體化。AudioManager會使用tokio_util::CancellationToken權杖管理已啟動的播放執行緒,確保執行緒終止,並在AudioManager為Dropped時釋放資源。AudioManager不會明確禁止建立多個執行個體。如果存在多個執行個體,系統會以warn層級記錄。
共同擁有權
許多物件的共用擁有權已包裝並同步處理,且具有專屬存取權。這些機制不會在 AudioManager API 中公開,而是音訊和 PAL 實作的內部機制。
AudioDevice- 開啟的每個硬體參照 (例如 TinyALSA PCM) 都具有專屬存取權。請參閱「SMP 設計」。AudioStream執行個體排定播放時間後,就會取得專屬存取權,因為這些執行個體可由應用程式控制,同時由播放執行緒存取。播放執行緒不會在播放期間保留鎖定,但會建立下一個要播放緩衝區的不變快照,且不會考慮變更,直到下一個緩衝區消化為止。
每個播放執行緒都有播放佇列,以及
AudioManager和播放執行緒之間的共用參照。因此,執行緒需要專屬的變異存取權。沒有串流的執行緒會進入閒置狀態,並使用
Condvar變數接收喚醒事件,以便在偵測到新資料時喚醒。這個機制具有共用擁有權。
依附元件
Crate 和音訊 Crate 的設計宗旨,是減少對未獲准在 Android 來源樹狀結構中建構的 Crate 的依附元件。請參閱這份所含Crate清單。
Android 和 Linux 的下游平台實作項目取決於 TinyALSA 和現有的顯示器安全 tinyalsa-rs Crate。
品質屬性
可靠性
雖然音訊播放功能對安全至關重要,但這項設計並未涵蓋安全監控的實作方式。請另外進行這項作業,以驗證硬體和正式環境中的音訊播放穩定性。
擴充性
每個裝置一個執行緒的方法,是為了擴展到不同的硬體設定。由於每個執行緒主要處於閒置狀態、等待資料或等待裝置消化寫入的資料,因此不應對處理器造成負擔,也不會對系統造成效能密集型負擔。
設計決策是只將資料播放至單一裝置,加上所有後續輸出控制的混音器控制指令,可確保音訊硬體處理確切輸出,並應能因應未來的系統擴充。
延遲時間
延遲是音訊系統的關鍵,因此在實作後,系統延遲會定義一組服務等級目標 (SLO)。如要持續監控延遲健康狀態,請在所有偵錯版本中,監控系統記錄中未達到定義 SLO 的項目。
對於正式版,監控資料會傳遞至音訊實作項目外部的某些系統,而不是依賴記錄檔。
測試和測試策略
這些 Crate 和音訊 Crate 的設計都涵蓋測試範圍。我們新增了模擬平台實作,確認所有功能都經過測試。
硬體和繫結的複雜性,使得平台實作無法進行廣泛的測試涵蓋範圍。我們提供範例實作,方便您在硬體和 Cuttlefish 模擬器上手動測試解決方案。
說明文件
Audio crates/audio 中的 README.md 檔案說明如何使用 AudioManager。crates/audio/examples 包含以下範例:
- 實作平台。
- 建立
AudioManager的執行個體。 - 播放「
WavAsset」。 - 重複播放自訂函式素材資源。
- 記錄播放事件。