音訊鈴聲

本文說明高可用性渲染器 (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 也會使用泛型包裝潛在的平台專屬額外參數。如果是 tinyalsaPlatformAudioDevice 則包含 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 檔案的素材資源。

此外,實作作業也支援透過更通用的資產實作作業合成及串流音訊資產,並接受用於產生音訊資料的函式。

導入作業

使用兩個不同的建構函式 (AudioAssetAudioStream) 實作資產。

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 說明這些步驟:

  1. 播放:排定要播放的串流。

  2. 優先順序:播放優先順序會決定是否要:

    • 立即播放鈴聲 (在第一個位元組時啟動事件)
    • 稍後播放鈴聲 (已暫停或繼續的活動)
    • 降低電鈴優先順序 (已取消的活動)
  3. 混音器控制項:視需要根據設定的行為更新混音器控制項。

  4. 寫入位元組:將一組位元組寫入 AudioDevice

  5. 更多資料:如果串流有更多資料,請返回步驟 2。

  6. 重複:如要重複播放串流,請重設並返回步驟 2 (重新啟動活動)。

  7. 已完成:串流已順利完成 (FinishedSuccessfully 事件)。

你隨時可以暫停、繼續或停止通話,中斷鈴聲。

鈴聲優先順序

這項邏輯會設定鈴聲優先順序:

  1. 覆寫播放模式。舉例來說,在淡出模式中,鈴聲一律會獲得最高優先順序,直到淡出完成為止。

  2. 指定的優先順序。

  3. 如果優先順序相同,系統會先播放較新的鈴聲。

如果鈴聲的優先順序相同,系統會使用 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 函式。

這項設計提供預設值,並允許後續實作項目覆寫預設對應。

HAR 音訊流程

圖 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
  • 互斥和同步機制是內部機制,不會在 AudioManager API 中公開。

擁有權模式和 AudioManager

  • 應用程式與音訊系統的所有互動,都是透過 AudioManagerAudioManager 傳回的物件進行。

  • AudioManager 是執行緒安全。

  • AudioManager 會在 HARry 應用程式中執行個體化一次,而 Moved 則會為了讓 Looper 擁有所有權而執行個體化。

  • AudioManager 會使用 tokio_util::CancellationToken 權杖管理已啟動的播放執行緒,確保執行緒終止,並在 AudioManagerDropped 時釋放資源。

  • 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 檔案說明如何使用 AudioManagercrates/audio/examples 包含以下範例:

  • 實作平台。
  • 建立 AudioManager 的執行個體。
  • 播放「WavAsset」。
  • 重複播放自訂函式素材資源。
  • 記錄播放事件。