4、音频焦点管理:焦点请求与释放、焦点策略配置、多音区焦点冲突处理

音频焦点,说白了就是车载系统里各路音频的「发言权」。

我刚开始做车载项目时,觉得这玩意儿不就是个锁嘛,谁抢到谁用。后来发现,事情远没那么简单。尤其是在多音区场景下,前排听导航、后排看视频、第三排小孩在玩游戏——这要是焦点管理没做好,车里就乱成一锅粥了。

4.1 焦点请求与释放:谁有资格说话?

Android Automotive 的音频焦点机制,跟手机上的不太一样。手机上是「一个 App 独占」,车里是「多个音区共存」。但共存归共存,总得有个优先级。

焦点请求的核心,是 AudioManager.requestAudioFocus() 这个方法。每个音频流在播放前,都得先申请焦点。

// 典型的焦点请求示例
AudioAttributes attrs = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(attrs)
        .setOnAudioFocusChangeListener(focusChangeListener)
        .build();

int result = audioManager.requestAudioFocus(focusRequest);

这里有个关键点:AUDIOFOCUS_GAIN 表示「我要长期占用」。但导航提示音通常用 AUDIOFOCUS_GAIN_TRANSIENT,表示「我插个嘴,说完就走」。

重要: 释放焦点时,一定要调用 abandonAudioFocus()。我在项目中遇到过,某个第三方 App 申请了焦点后忘了释放,导致整个系统的导航音一直出不来。排查了半天,最后发现是焦点被「死锁」了。

4.2 焦点策略配置:系统级的「交通规则」

光靠 App 自觉申请焦点,肯定不行。系统得有硬性的策略配置。在 Android Automotive 里,焦点策略是通过 audio_policy_configuration.xml 来定义的。

这个文件里,最核心的是 focus 相关的配置。我习惯把它理解成「交通规则」——谁该让行,谁有优先权,都在这里写死。

<!-- audio_policy_configuration.xml 中的焦点策略片段 -->
<focus>
    <!-- 导航音优先级最高 -->
    <focusRule usage="AUDIO_USAGE_NAVIGATION" priority="100"/>
    <!-- 电话次之 -->
    <focusRule usage="AUDIO_USAGE_VOICE_COMMUNICATION" priority="90"/>
    <!-- 媒体音最低 -->
    <focusRule usage="AUDIO_USAGE_MEDIA" priority="10"/>
</focus>

你想想看,如果导航音和媒体音优先级一样,那导航播报时音乐还在大声放,用户根本听不清。所以,优先级必须拉开差距。

我的经验: 优先级数字不要设得太密集。我建议至少留 20 的间隔,方便后续扩展。比如导航 100、电话 80、媒体 60、系统提示音 40。这样万一要加个「紧急广播」,直接设 120 就行,不用改现有配置。

4.3 多音区焦点冲突处理:这才是真正的难点

单音区场景下,焦点管理其实挺简单的。但到了多音区,事情就复杂了。

举个例子:前排主驾在导航,后排乘客在看视频。这时候导航说「前方 300 米右转」,视频里的爆炸声也在响。按理说,导航音应该「压过」视频音。但问题是——后排乘客可能不想被导航打断。

这就是多音区焦点冲突的核心矛盾:同一个系统,不同音区,对焦点的诉求可能完全相反。

Android Automotive 的解决方案是「音区独立焦点」机制。每个音区有自己的焦点状态机,互不干扰。

// 多音区焦点请求示例
// 假设 zoneId = 2 表示后排音区
AudioManager zoneAudioManager = audioManager.createForZone(zoneId);

AudioFocusRequest rearFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(rearMediaAttrs)
        .setZoneId(zoneId)  // 指定音区
        .build();

int result = zoneAudioManager.requestAudioFocus(rearFocusRequest);

但这里有个坑:跨音区的焦点冲突怎么处理?

比如,前排导航音和后排视频音,虽然属于不同音区,但物理上它们共享同一个扬声器(或者部分扬声器)。这时候,系统得有个「全局仲裁者」。

注意: 我曾经在一个项目中,后排视频音把前排导航音给「覆盖」了。原因是后排音区的焦点策略配置了 ducking = true,但前排导航音没有配置 ducking。结果导航音被「闪避」了,用户完全听不到。后来我们在导航音的焦点策略里加了 ducking = false,问题才解决。

4.4 实战中的焦点冲突处理策略

根据我的经验,多音区焦点冲突处理,可以归纳为以下几种策略:

冲突类型 推荐策略 说明
导航 vs 媒体(同音区) 导航闪避媒体 导航音播放时,媒体音降低音量(ducking)
电话 vs 媒体(同音区) 电话暂停媒体 电话接通时,媒体音完全暂停
导航 vs 媒体(不同音区) 各自独立 只要扬声器不冲突,互不干扰
紧急广播 vs 所有音区 全局抢占 紧急广播拥有最高优先级,强制打断所有音区

嗯,这里要注意:「各自独立」的前提是扬声器不冲突。 如果前后排共用低音炮,那还是得做混音处理。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 焦点泄漏: App 退出时没释放焦点。我建议在 onDestroy()onPause() 里强制释放焦点,别指望 App 开发者自觉。
  • 焦点死锁: 两个音区互相等待对方释放焦点。这种情况通常发生在自定义焦点策略里。解决办法是加一个「超时机制」——如果某个音区 5 秒内没响应,系统强制回收焦点。
  • 焦点优先级反转: 低优先级的音频因为某些原因(比如后台服务)获得了焦点,导致高优先级的音频被阻塞。我曾经遇到过,一个音乐 App 的后台服务申请了 AUDIOFOCUS_GAIN,结果导航音一直出不来。后来我们在焦点策略里加了「前台优先」的规则,才解决。

总结一下: 音频焦点管理,说白了就是「谁说话,谁闭嘴」的问题。单音区简单,多音区复杂。核心是三点:明确的优先级、独立的音区状态机、以及全局的冲突仲裁机制。把这三点做好了,车载音频系统就不会出大乱子。

下一章,我们会聊聊音频路由的硬件抽象层实现。到时候我会分享一些 HAL 层的调试技巧,保证让你少走弯路。