铃声

此内容介绍了高可用性渲染器 (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 派生出 `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. 确定优先级: 播放优先级决定是否执行以下操作:

    • 立即播放电铃(第一个字节的 started 事件)
    • 稍后播放电铃(paused 或 resumed 事件)
    • 降低电铃的优先级(canceled 事件)
  3. 混音器控制: 如果需要,请根据配置的行为更新混音器控制。

  4. 写入字节: 将字节块写入 AudioDevice

  5. 更多数据: 如果流有更多数据,请返回第 2 步。

  6. 重复: 如果应重复流,请重置并返回第 2 步(restarted 事件)。

  7. 已完成: 流已成功完成(FinishedSuccessfully 事件)。

您可以随时使用暂停、恢复或停止调用来中断电铃。

电铃优先级

此逻辑设置电铃优先级:

  1. 播放模式替换。例如,淡出模式下的电铃始终被授予最高优先级,直到淡出完成。

  2. 指定的优先级。

  3. 如果优先级相同,则最近的电铃先播放。

当电铃的优先级相同时,AudioManager 会使用 enum 值进行实例化。

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 监控特定于延迟时间的内部统计信息以及定义日志记录性能的标志。设置这些阈值后,当出现以下情况时,系统会为所有调试 build 生成警告日志:

  • 安排播放和开始播放之间的时间超过 x 毫秒。
  • (对于未中断的流)资产长度和播放时间相差超过 y%。

设备已屏蔽

音频设备始终存在无响应的小风险,例如,如果它由系统中的另一个进程分配和写入。 鉴于播放在单独的线程中异步运行,并且电铃可以排队稍后播放,因此这对调用应用完全透明。

为检测到这一点,每当安排播放新电铃时,系统都会进行线程健康检查,如果播放线程的队列已填充,并且在过去一秒内未消化任何新字节,则返回错误。

出于未来的目的,可能需要尝试重启 / 打开设备,但对于初始实现,错误不应不可见。

代码结构

概括来讲,与电铃播放相关的代码存在于以下 crate 中:

CRATE: display-safety/crates/(harry-app|harry)

现有的 HAR 应用,用于发出播放电铃的调用。

NEW CRATE: display-safety/crates/audio

NEW:用于管理音频控制和播放的 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

  • 所有应用与音频系统的交互都通过 AudioManager 或从 AudioManager 返回的对象进行。

  • 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)。为持续监控延迟时间的健康状况,系统日志中的监控不会满足所有调试 build 中定义的 SLO。

对于生产版本,监控数据会传递给音频实现之外的某些系统,而不是依赖于日志。

测试和测试策略

Crate 和音频 crate 均采用测试覆盖率设计。我们添加了一个模拟平台实现,以确认所有功能都经过测试。

硬件和绑定的复杂性使得平台实现无法进行广泛的测试覆盖。我们提供了示例实现,以便在硬件和 Cuttlefish 模拟器上手动测试解决方案。

文档

Audio crates/audio 中的 README.md 文件介绍了如何使用 AudioManagercrates/audio/examples 包含以下示例:

  • 实现平台。
  • 创建 AudioManager 的实例。
  • 播放 WavAsset
  • 重复播放自定义函数资产。
  • 记录播放事件。