音频焦点

在启动逻辑声音流之前,应用应使用将用于其逻辑声音流的同一个音频属性来请求获得音频焦点。虽然我们建议发送此类焦点请求,但系统不会强制要求发送。有些应用可能会明确跳过发送请求的步骤,以实现特定行为(例如,在通话期间故意播放声音)。

为此,您应将焦点视为间接控制播放和消除播放冲突的一种方式,而不是作为主要的音频控制机制;也就是说,车辆不应依赖于焦点系统来操作音频子系统。

焦点交互

为了支持 AAOS 的需求,系统会根据请求的 CarAudioContext 和当前焦点持有者的 CarAudioContext 之间的预定义交互来处理音频焦点请求。交互类型分以下三种:独占、拒绝和并发。

独占交互

在独占交互中,一次只允许一个应用持有焦点。因此,在传入的焦点请求被授予焦点的同时,现有的焦点持有者会失去焦点。例如,用户在现有应用中播放音乐时启动新的音乐应用。由于这两个应用都在播放媒体内容,因此一次只允许其中一个应用持有焦点。结果就是,新启动的应用发出的焦点请求将会返回 AUDIOFOCUS_REQUEST_GRANTED;而当前在播放的应用将收到焦点更改事件,该事件中的丢失状态与所发请求的类型相对应。这是 Android 中最常见的交互模型。

拒绝交互

在拒绝交互中,传入的请求一律会遭到拒绝。尝试在通话过程中播放音乐就是拒绝交互的一个示例。在这个例子中,如果拨号器正为某个通话持有音频焦点,而另一个应用要请求焦点来播放音乐,则音乐应用发出的请求会收到 AUDIOFOCUS_REQUEST_FAILED 响应。由于焦点请求遭拒,因此系统不会向当前焦点持有者分派任何类型的焦点丢失事件。

并发交互

AAOS 最独特的地方就是并发交互。在这种交互模式下,请求音频焦点的车载应用可与其他应用同时持有焦点。若要实现并发交互,必须满足以下条件。即:

如果满足上述条件,焦点请求将返回 AUDIOFOCUS_REQUEST_GRANTED,而当前焦点持有者的焦点不会发生任何变化。不过,如果当前焦点持有者选择接收闪避事件或在闪避时暂停,则会失去焦点,就像独占交互一样。

处理并发声音流

虽然并发交互适用于许多实用应用,但原始设备制造商 (OEM) 必须在硬件级别跨输出设备实现混音和闪避。因此,强烈建议仅将 CarAudioContext 路由到无法与其同时播放的 CarAudioContext 的同一输出设备。通过为并发声音流提供单独的输出设备,HAL 便可在混音之前对其中一个声音流进行闪避;或者将物理声音流路由到车辆中的其他音响设备。如果在 Android 中对逻辑声音流进行混音,则其增益将保持不变,并会作为同一物理声音流的一部分来提供。

例如,如果同时提供导航提示音和媒体播放声音,媒体声音流的增益会暂时降低(闪避),以便用户能更清楚地听到导航提示。或者,导航声音流会被路由到驾驶员身旁的音响设备,媒体则在驾驶舱的其余音响设备中继续播放。

交互矩阵

下表显示了由 CarAudioService 定义的交互矩阵。行内容和列内容分别表示当前焦点持有者和传入请求的 CarAudioContext

我们来看一个例子:如果音乐媒体应用目前正持有音频焦点,而导航应用要请求获得焦点,那么通过该矩阵便能知道,这两个交互可以同时进行(假设满足并发交互的其他条件)。

由于并发交互的缘故,可能会存在多个焦点持有者。在这种情况下,系统会将传入的焦点请求与当前的各个焦点持有者进行比较,然后决定应用哪种交互。此时,最保守的交互会胜出(先是拒绝交互,然后是独占交互,最后是并发交互)。

下表罗列了传入焦点请求的 CarAudioContext(列)与现有焦点持有者的上下文(行)之间的焦点交互。每个单元格表示两种上下文的预期交互类型,其中:

  • R 代表拒绝交互
  • E 代表独占交互
  • C 代表并发交互

音频焦点交互

图 1. 音频焦点交互

Android 11 中引入了一项新的用户设置,可让用户更改导航与通话之间的交互行为。设置后,android.car.KEY_AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL 会将传入的焦点请求 NAVIGATION 与当前焦点持有者 CALL 之间的交互从并发变为拒绝。因此,当用户不想让导航提示中断通话时,就可以启用此设置。这是为用户保留的,并可动态设置,以便让后续焦点请求遵循新的设置值。

可延迟的音频焦点

在 Android 11 中,AAOS 开始支持请求获得可延迟的音频焦点。这样一来,当非瞬态焦点请求与当前焦点持有者交互通常会遭到拒绝时,前者可以延迟。一旦焦点的变化导致延迟的请求可以获得焦点,系统就会将焦点授予该请求。

延迟音频焦点请求的规则

  • 仅限非瞬态请求 - 如上所述,只能针对非瞬态声源发出延迟请求。这是为了避免在相关声音播放很长时间后出现瞬态声音。
  • 一次只能延迟一项请求 - 如果在已有一项延迟请求的情况下,又发出另一项延迟请求,则最初发送的延迟请求将收到 AUDIOFOCUS_LOSS 更改事件,新发送的请求将收到 AUDIOFOCUS_REQUEST_DELAYED 的同步响应。
  • 延迟请求必须具有 OnAudioFocusChangeListener - 请求延迟后,监听器将用于在请求最终获得焦点 (AUDIOFOCUS_GAIN) 或稍后被拒绝 (AUDIOFOCUS_LOSS) 时通知请求者。

请求可延迟的焦点

若要构建可以延迟的请求,请使用 AudioFocusRequest.Builder#setAcceptsDelayedFocusGain

mMediaWithDelayedFocusListener = new MediaWithDelayedFocusListener();

mDelayedFocusRequest = new AudioFocusRequest
     .Builder(AudioManager.AUDIOFOCUS_GAIN)
     .setAudioAttributes(mMusicAudioAttrib)
     .setOnAudioFocusChangeListener(mMediaWithDelayedFocusListener)
     .setForceDucking(false)
     .setWillPauseWhenDucked(false)
     .setAcceptsDelayedFocusGain(true)
     .build();

然后,在发出请求时,处理 AUDIOFOCUS_REQUEST_DELAYED 响应:

int delayedFocusRequestResults = mAudioManager.requestAudioFocus(mDelayedFocusRequest);
if (delayedFocusRequestResults == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// start audio playback
return;
}
if (delayedFocusRequestResults == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
     // audio playback delayed to audio focus listener
     return;
}

在请求获得延迟后,焦点监听器负责处理焦点变更:

private final class MediaWithDelayedFocusListener implements
OnAudioFocusChangeListener {
       @Override
       public void onAudioFocusChange(int focusChange) {
           synchronized (mLock) {
               switch (focusChange) {
                   case AudioManager.AUDIOFOCUS_GAIN:
                        // Start focus playback
                   case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                        // Pause media transiently
                   case AudioManager.AUDIOFOCUS_LOSS:
                        // Stop media

多音频区焦点管理

对于具有多个音频区的车辆,系统会为每个音频区单独管理音频焦点。因此,对一个音频区的请求不会考虑其他音频区中的焦点持有者,也不会导致其他音频区中的焦点持有者失去焦点。因此,主驾驶舱与后座娱乐系统的焦点可分开管理,从而避免某个音频区的焦点变更时另一个音频区的播放也随之中断。

所有应用的焦点均由 CarAudioService 自动管理。焦点请求的音频区是根据所关联的 UserIdUID 来确定的。如需了解详情,请参阅音频路由

同时从多个音频区请求音频

如果应用需要同时在多个音频区播放音频,就必须在软件包中包含 AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID,以针对各个音频区请求焦点。

//Create attribute with bundle and AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID
Bundle bundle = new Bundle();
bundle.putInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID,
               zoneId);

AudioAttributes attributesWithZone = new AudioAttributes.Builder()
     .setUsage(AudioAttributes.USAGE_MEDIA)
     .addBundle(bundle)
     .build();

//Create focus request using built attributesWithZone

此软件包参数允许请求者替换自动音频区映射,以改用指定的音频区 ID。因此,可使用此应用针对不同的音频区发出单独的请求。

HAL 音频焦点

从 Android 11 开始,HAL 可以代表外部声音流请求焦点。尽管这些 API 是可选的,但强烈建议您使用,以便让外部声音能够更顺利地融入 Android 生态系统,进而提供更加流畅的用户体验。

请记住,HAL 仍负责就应当优先处理的声音发出最终调用。因此,无论 HAL 是否被授予音频焦点,都应播放紧急声音和对保障安全至关重要的声音,即使 HAL 失去音频焦点,也应继续在适当的情况下播放这些声音。此规则也适用于法规要求的任何声音。

同样地,为了确保人们能够清楚地听到,HAL 仍会在播放紧急声音或对保障安全至关重要的声音时,适时主动将 Android 声音流静音。

2.0 版 AudioControl

2.0 版 AudioControl HAL 引入了几个新的 API:

API 用途
IAudioControl#registerFocusListener 向 AudioControl HAL 注册 IFocusListener 的实例。此监听器可让 HAL 请求和放弃音频焦点。HAL 应提供 ICloseHandle 实例,以供 Android 用于取消注册该监听器。
IAudioControl#onAudioFocusChange 向 HAL 发送通知,告知由 HAL 通过 IFocusListener 发出的焦点请求的状态变化。这包括对初始焦点请求的响应。
IFocusListener#requestAudioFocus 代表 HAL 针对指定的用法、音频区 ID 和焦点增益类型请求焦点。
IFocusListener#abandonAudioFocus 针对指定的用法和音频区 ID,放弃现有的 HAL 焦点请求。

HAL 可以同时拥有多个焦点请求,但每个用法与音频区 ID 的配对仅限一个请求。请注意,Android 会假定,HAL 将在请求发出后针对用法立即开始播放声音,并在放弃焦点之前继续执行同样的操作。

registerFocusListener 之外,这些请求均为 oneway,以确保 Android 在处理焦点请求时不会延迟 HAL。在播放对保障安全至关重要的声音前,HAL 不应等待获得焦点。尽管我们建议在适当的时候让 HAL 通过 IAudioControl#onAudioFocusChange 来监听和响应音频焦点的变化,但这是一项可选操作。