22. 音频焦点与多音频管理:AudioFocus机制、多实例音频路由、冲突处理
各位好,今天我们来聊聊音频焦点。说实话,这个机制在Android多媒体框架里,属于那种「平时不太注意,一出问题就头大」的模块。我早年做车载系统时,就遇到过导航语音和音乐播放打架的情况——导航说「前方300米右转」,音乐直接把它盖过去了。嗯,后来查了一圈,就是AudioFocus没处理好。
音频焦点,说白了就是一套「谁有资格出声」的仲裁机制。你想想看,手机同时只能播放一路有意义的声音(除非做混音),那当多个App都想发声时,谁说了算?就是AudioFocus。
22.1 AudioFocus的核心概念
AudioFocus不是硬件资源,它是个软件层面的锁。App在播放前必须先申请焦点,申请成功后才能出声。焦点可以被抢占、可以释放、也可以被延迟授予。
我个人习惯把焦点分为三种类型:
- 短暂焦点(AUDIOFOCUS_GAIN_TRANSIENT):适合短提示音,比如消息通知。它会暂停其他播放器。
- 短暂可闪避焦点(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK):适合导航语音。它不暂停音乐,只是把音乐音量降低(俗称「闪避」)。
- 永久焦点(AUDIOFOCUS_GAIN):适合音乐播放器。它会完全抢占之前的焦点。
这里有个关键点:焦点申请是异步的。你调用requestAudioFocus()后,系统会通过回调告诉你结果。我曾经见过新手直接忽略回调,以为申请了就一定能播——结果在某些场景下焦点被拒绝,声音死活出不来。
核心原则:永远不要假设焦点申请成功。必须监听OnAudioFocusChangeListener回调,根据结果决定是否开始播放。
22.2 焦点冲突的处理流程
当两个App同时申请焦点时,系统会按优先级裁决。优先级怎么定?看Usage和ContentType。比如导航的Usage是USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,它的优先级天然高于音乐播放器的USAGE_MEDIA。
冲突发生时,系统会:
- 通知当前持有焦点的App:你失去焦点了(通过
onAudioFocusChange()回调)。 - 根据失去焦点的类型,App需要做出响应:暂停、闪避、或者忽略。
- 新App获得焦点,开始播放。
我举个例子。你在听网易云音乐,突然微信视频电话来了。这时候:
- 微信申请
AUDIOFOCUS_GAIN_TRANSIENT(短暂焦点) - 系统通知网易云:你失去焦点,类型是
AUDIOFOCUS_LOSS_TRANSIENT - 网易云应该暂停播放
- 微信通话结束后释放焦点
- 系统通知网易云:焦点回来了(
AUDIOFOCUS_GAIN) - 网易云恢复播放
你看,这个流程环环相扣。任何一个环节没处理好,用户体验就崩了。
避坑指南:我曾经遇到一个App,在收到AUDIOFOCUS_LOSS_TRANSIENT后没有暂停播放,而是继续出声。结果用户打电话时,背景音乐还在响——直接被用户投诉到应用商店下架。所以,失去焦点时必须静音,这是底线。
22.3 多实例音频路由
多实例音频路由,说白了就是「多个音频流怎么走」。Android从5.0开始引入了AudioRouting接口,允许App指定音频输出到哪个设备。比如:
- 音乐走蓝牙耳机
- 导航走车载扬声器
- 电话走听筒
这个功能在车载和IoT设备上特别重要。我记得做智能音箱项目时,要求「语音助手从顶部扬声器出声,音乐从底部扬声器出声」——这就是典型的多实例路由。
实现方式是通过AudioAttributes和AudioDeviceInfo配合:
// 创建音频属性,指定用途
AudioAttributes attrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
// 获取目标设备
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioDeviceInfo targetDevice = findDeviceByName(devices, "Speaker");
// 创建播放器并指定路由
AudioTrack track = new AudioTrack.Builder()
.setAudioAttributes(attrs)
.setAudioFormat(format)
.setBufferSizeInBytes(bufferSize)
.build();
track.setPreferredDevice(targetDevice);
这里有个坑:setPreferredDevice()是「建议性」的,不是强制性的。如果目标设备不可用,系统会fallback到默认设备。所以一定要做fallback处理。
22.4 冲突处理的实战策略
在实际项目中,冲突处理不能只靠系统机制。我总结了一套「三层防御」策略:
| 层级 | 机制 | 说明 |
|---|---|---|
| 第一层 | AudioFocus系统机制 | 系统自动仲裁,App响应回调 |
| 第二层 | App内部状态机 | 管理自己的播放状态,防止重复申请 |
| 第三层 | 业务层策略 | 比如「导航语音优先」「来电强制暂停」 |
第一层是基础,但不够。为什么?因为系统只告诉你「焦点变化了」,具体怎么做还得你自己定。比如:
- 收到
AUDIOFOCUS_LOSS:必须停止播放,释放资源。 - 收到
AUDIOFOCUS_LOSS_TRANSIENT:暂停播放,但保留播放位置。 - 收到
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:降低音量到20%,继续播放。
第二层是状态机。我习惯用enum定义播放状态:
enum PlayState {
IDLE, // 空闲
REQUESTING, // 正在申请焦点
PLAYING, // 正常播放
PAUSED, // 被暂停
DUCKING // 被闪避
}
每个状态转换都对应一个焦点事件。比如从PLAYING到PAUSED,必须是因为收到了AUDIOFOCUS_LOSS_TRANSIENT。这样可以避免状态混乱。
个人经验:我建议在焦点回调里不要直接操作UI或执行耗时操作。回调是在binder线程里触发的,直接更新UI会引发崩溃。正确的做法是:把焦点事件post到主线程Handler里处理。
22.5 知识体系总览
下面这张图是我自己整理的AudioFocus知识体系,你可以把它当作思维导图来用:
22.6 实战中的几个关键点
最后,我分享几个实战中容易踩的坑:
- 不要重复申请焦点:如果已经持有焦点,再次申请会导致回调混乱。我习惯用一个
boolean hasFocus标记来防止重复申请。 - 释放焦点要及时:播放结束后必须调用
abandonAudioFocus()。否则其他App永远拿不到焦点。 - 闪避音量要合理:
MAY_DUCK场景下,音量降到20%-30%比较合适。降太多用户听不清,降太少起不到提示作用。 - 测试要覆盖所有场景:电话进来、闹钟响起、通知弹出、语音助手唤醒……每个场景都要测。我当年就是漏测了「闹钟+音乐」的组合,结果上线后用户反馈闹钟响时音乐还在播。
嗯,关于AudioFocus就讲这么多。这个机制看似简单,但要做好其实不容易。核心就一句话:尊重焦点,及时响应,状态清晰。做到这三点,多音频管理就不会出大问题。
公众号:蓝海资料掘金营,微信deep3321