21、音频焦点与多应用管理:AudioFocus 机制、混音策略、通话与媒体冲突处理
各位好,欢迎来到第21章。
今天聊的话题,我估计每个做过音频开发的兄弟都头疼过——多应用音频冲突。
你想想看,手机里同时开着音乐App、导航、微信语音、来电铃声……谁该出声?谁该闭嘴?谁可以混音?谁必须暂停?
Android 用了一套叫 AudioFocus 的机制来管这事。说白了,就是给每个音频流发一个“通行证”,谁有焦点谁出声,没焦点的要么闭嘴,要么小声点。
21.1 AudioFocus 是什么?
AudioFocus 是 Android 系统提供的一套应用层音频资源协调机制。它不是内核级的强制策略,而是靠应用主动配合来实现的。
嗯,这里要注意:它不是强制性的。应用可以无视焦点变化,继续播放。但系统会通过回调告诉你“你该停一下了”,至于你停不停,那是你的事。
核心原则:同一时刻,只有一个应用可以持有“主动播放”的焦点。其他应用要么暂停,要么降低音量(duck)。
我在项目中遇到过一个问题:某个第三方音乐App完全不处理焦点丢失回调,导致导航语音和音乐同时炸响。用户投诉说“导航听不清”。最后我们只能通过 AudioPolicy 层面强制压低它的音量——这是后话了。
21.2 焦点请求与释放流程
一个标准的 AudioFocus 生命周期是这样的:
- 请求焦点:应用调用
requestAudioFocus(),传入 AudioAttributes 和焦点类型。 - 系统裁决:AudioService 根据当前焦点持有者、请求者的优先级,决定是否授予。
- 回调通知:如果焦点被授予,原持有者会收到
onAudioFocusChange()回调。 - 释放焦点:播放结束后,调用
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 先检查有没有人占着茅坑。没有就直接给,有的话根据请求类型决定怎么处理原持有者。
我个人觉得,理解这张图比背 API 重要得多。API 可以查文档,但流程逻辑错了,查文档也救不了你。
好了,这一章就到这里。音频焦点这东西,说难不难,说简单也不简单。关键是你得站在系统角度想问题——你不是一个人在播,你只是众多应用中的一个。学会谦让,才能共赢。