21、音频焦点与多应用管理:AudioFocus 机制、混音策略、通话与媒体冲突处理

各位好,欢迎来到第21章。

今天聊的话题,我估计每个做过音频开发的兄弟都头疼过——多应用音频冲突

你想想看,手机里同时开着音乐App、导航、微信语音、来电铃声……谁该出声?谁该闭嘴?谁可以混音?谁必须暂停?

Android 用了一套叫 AudioFocus 的机制来管这事。说白了,就是给每个音频流发一个“通行证”,谁有焦点谁出声,没焦点的要么闭嘴,要么小声点。

21.1 AudioFocus 是什么?

AudioFocus 是 Android 系统提供的一套应用层音频资源协调机制。它不是内核级的强制策略,而是靠应用主动配合来实现的。

嗯,这里要注意:它不是强制性的。应用可以无视焦点变化,继续播放。但系统会通过回调告诉你“你该停一下了”,至于你停不停,那是你的事。

核心原则:同一时刻,只有一个应用可以持有“主动播放”的焦点。其他应用要么暂停,要么降低音量(duck)。

我在项目中遇到过一个问题:某个第三方音乐App完全不处理焦点丢失回调,导致导航语音和音乐同时炸响。用户投诉说“导航听不清”。最后我们只能通过 AudioPolicy 层面强制压低它的音量——这是后话了。

21.2 焦点请求与释放流程

一个标准的 AudioFocus 生命周期是这样的:

  1. 请求焦点:应用调用 requestAudioFocus(),传入 AudioAttributes 和焦点类型。
  2. 系统裁决:AudioService 根据当前焦点持有者、请求者的优先级,决定是否授予。
  3. 回调通知:如果焦点被授予,原持有者会收到 onAudioFocusChange() 回调。
  4. 释放焦点:播放结束后,调用 abandonAudioFocus()

代码示例,我习惯这样写:

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

AudioAttributes attrs = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();

int result = am.requestAudioFocus(
        focusChange -> {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    // 获得焦点,可以正常播放
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    // 永久失去焦点,应该暂停播放
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    // 暂时失去焦点,暂停播放
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    // 暂时失去焦点,但可以降低音量继续播放
                    break;
            }
        },
        attrs,
        AudioManager.AUDIOFOCUS_GAIN,
        0 // 无延迟
);

我的建议:不要只处理 AUDIOFOCUS_GAIN。很多开发者只关心“能不能播”,忽略了 LOSS 回调。结果就是焦点被抢了还在播,用户体验极差。

21.3 焦点类型详解

焦点类型决定了你的应用在冲突时的“霸道程度”。我整理了一张表:

焦点类型 含义 对原持有者的影响
AUDIOFOCUS_GAIN 长期持有焦点 原持有者收到 LOSS,应停止播放
AUDIOFOCUS_GAIN_TRANSIENT 临时持有(如提示音) 原持有者收到 LOSS_TRANSIENT,应暂停
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 临时持有,允许混音 原持有者收到 CAN_DUCK,降低音量即可

你想想看,导航语音用 GAIN_TRANSIENT 就很合理——它只是短暂播报,音乐应该暂停。而微信语音通话用 GAIN 就对了,因为它要长期占用。

21.4 混音策略:什么时候可以一起出声?

不是所有冲突都必须你死我活。有些场景下,两个应用可以同时出声,这就是混音策略

Android 的混音决策主要看两点:

  • Usage 类型:比如 USAGE_MEDIA 和 USAGE_NAVIGATION 可以混音,但 USAGE_VOICE_COMMUNICATION 和 USAGE_MEDIA 不行。
  • 焦点类型:如果新请求是 MAY_DUCK,原持有者只需要降低音量,不需要暂停。

我记得有一次做车载系统,导航和音乐混音的需求特别多。用户希望导航播报时音乐声音变小,播报完恢复。这就是典型的 duck 场景。

实现上,你只需要在 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 回调里把音量降到 20%,然后在 AUDIOFOCUS_GAIN 时恢复即可。

注意:不要自己实现 duck 逻辑时用固定音量值。不同设备的扬声器灵敏度不同。我建议用 AudioManager 的 getStreamMaxVolume() 做比例计算。

21.5 通话与媒体冲突处理

通话是最高优先级的音频活动,没有之一。

当来电响起时,系统会强制所有媒体应用失去焦点。这个过程是 AudioService 内部处理的,应用层只需要响应回调即可。

但有个坑:通话结束后的焦点恢复

我曾经遇到一个 bug:通话结束后,音乐App没有自动恢复播放。原因是 App 只监听了 AUDIOFOCUS_LOSS,没有监听 AUDIOFOCUS_GAIN 的恢复信号。

正确的做法是:

// 在 onAudioFocusChange 中
if (focusChange == AUDIOFOCUS_GAIN) {
    // 检查是否是通话结束后的恢复
    if (wasPausedForCall) {
        resumePlayback();
        wasPausedForCall = false;
    }
}

另外,通话中的媒体播放也是常见需求。比如用户想一边打电话一边听音乐。这其实是不允许的——通话时媒体流会被静音。但如果你做的是 VoIP 应用,可以通过 setSpeakerphoneOn(true) 来绕过,不过这会牺牲通话质量。

21.6 系统级 AudioPolicy 配置

如果你有系统权限,可以修改 audio_policy_configuration.xml 来定制混音规则。比如:

<!-- 允许导航和媒体混音 -->
<mixPort name="primary output" role="source">
    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
</mixPort>

但说实话,我建议非必要不动这个文件。改错了可能导致整个音频系统崩溃。我见过有人把通话和媒体的路由混在一起,结果打电话时音乐从听筒里冒出来……

21.7 避坑指南

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

  • 不要假设焦点一定会被授予。 返回值可能是 AUDIOFOCUS_REQUEST_FAILED,尤其是低端设备上。
  • 释放焦点要及时。 播放结束后不释放,其他应用永远拿不到焦点。用户会以为手机坏了。
  • 多实例场景要小心。 如果你的 App 有多个播放器实例,每个实例都应该独立请求焦点,而不是共用一个。
  • 测试时别忘了耳机。 插拔耳机会触发音频路由切换,焦点状态可能被重置。

一句话总结:AudioFocus 不是强制锁,而是君子协定。你的 App 配合得好,用户就舒服;配合得不好,用户就骂你。

21.8 核心逻辑流程图

下面这张图展示了 AudioFocus 的完整裁决流程,从请求到回调,再到混音决策:

应用A 请求焦点 AudioService 裁决 是否有焦点持有者? 无持有者 授予 有持有者 检查请求类型 GAIN → 强制抢占 TRANSIENT → 暂停原持有者 MAY_DUCK → 降低音量 回调原持有者 onAudioFocusChange() 新应用获得焦点,开始播放 图:AudioFocus 裁决流程

这张图把整个流程串起来了。你从左上角开始看,应用A请求焦点,AudioService 先检查有没有人占着茅坑。没有就直接给,有的话根据请求类型决定怎么处理原持有者。

我个人觉得,理解这张图比背 API 重要得多。API 可以查文档,但流程逻辑错了,查文档也救不了你。


好了,这一章就到这里。音频焦点这东西,说难不难,说简单也不简单。关键是你得站在系统角度想问题——你不是一个人在播,你只是众多应用中的一个。学会谦让,才能共赢。