前言
最近线上用户反馈了一些语音唤醒功能无法使用的问题(即无法通过语音来和应用交互),因此专门梳理了Android应用对音频焦点的处理逻辑并提出一些优化思路,下文是一次总结整理。
音频焦点出现的缘由、作用
为什么会有音频焦点的出现?两个或两个以上的 Android 应用可同时向同一输出流播放音频,系统会将所有音频流混合在一起。为了避免所有音乐应用同时播放,Android 引入了“音频焦点”的概念,一次只能有一个应用获得音频焦点。当应用需要输出音频时,它需要请求获得音频焦点,获得焦点后,就可以播放声音了。
不过,Android系统虽然可以对音频焦点进行分配,但不能强制让上一个占有音频焦点的应用程序放弃使用音频。即应用程序A正在前台使用语音唤醒功能的时候,应用B拨打了一个语音电话过来,这时Android系统会把音频焦点分配给应用B,但应用A的语音唤醒功能并没有被剥夺、仍然可以使用。那么应用B的语音电话功能就会受到影响。
这时就需要约定一个规则,当应用程序失去音频焦点的时候,就需要暂停使用音频或降低音频音量,把音频通道让给占有音频焦点的应用程序使用。如果应用违背上述规则,不释放音频通道,可能会引发大量客诉、流失掉很多用户。
音频焦点的使用核心
Android应用通过调用 requestAudioFocus()(请求音频焦点) 和 abandonAudioFocus()(放弃音频焦点) 来管理音频焦点。应用还必须为这两个方法注册监听器 AudioManager.OnAudioFocusChangeListener ,以便接收回调并管理自己的音量。
音频焦点具体的代码使用
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// 永久失去音频焦点
// 立即停止播放
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// 暂时性失去焦点
// 播放暂停
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// 暂时性失去焦点
// 声音变低,继续播放
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// 获取到音频焦点
// 声音恢复到正常音量,开始播放
}
}
};
int result = audioManager.requestAudioFocus(afChangeListener,
// 使用音频流
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 音频焦点请求成功,执行相应逻辑
} else {
// 音频焦点请求失败,执行相应逻辑
}
播放完成后,放弃音频焦点:
audioManager.abandonAudioFocus(afChangeListener);
使用中注意的点:
① 为 requestAudioFocus 方法传入的第3个参数:
- 如果计划在将来一段时间内播放音频,并且希望前一个持有音频焦点的应用停止播放,则应该请求永久性的音频焦点 (AUDIOFOCUS_GAIN)。
- 如果只希望在短时间内播放音频,并且希望前一个持有音频焦点的应用暂停播放,则应该请求暂时性的焦点 (AUDIOFOCUS_GAIN_TRANSIENT)。
- 如果只希望在短时间内播放音频,并允许前一个持有焦点的应用在降低音量的情况下继续播放,则应该请求“降低音量”的暂时性焦点(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 。这两个音频会混合到音频流中同时输出。
② AudioManager.OnAudioFocusChangeListener回调回来的音频焦点类型,如下:
- AUDIOFOCUS_GAIN:获取得到音频焦点。
- AUDIOFOCUS_LOSS:永久性失去音频焦点,后续不会再收到 AUDIOFOCUS_GAIN 回调。应用应立即暂停播放,此时其他应用会播放音频。
- AUDIOFOCUS_LOSS_TRANSIENT:暂时性失去音频焦点,应用应暂停播放。当抢占焦点的应用放弃焦点时、自己的应用可以收到 AUDIOFOCUS_GAIN 回调。
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:暂时性失去音频焦点,应用应降低音量。当抢占焦点的应用放弃焦点时、自己的应用可以收到 AUDIOFOCUS_GAIN 回调。
开发中遇到的问题 & 解决思路
大多数情况下,按照上述流程使用音频焦点是没有问题的。但线上有部分用户反馈无法使用语音唤醒功能(即无法通过语音来和应用交互),经过排查,发现有如下可优化的地方:
-
问题1:不是所有应用都会将音频焦点释放,有的应用也会不释放,比如手机系统的录音机。这看似违背最上面说的音频焦点管理规则,但录音机是有它特殊性的,不管它位于前台还是后台,都应该拥有录音的功能。这就会导致只要不杀死录音机应用进程,它就会一直占用音频焦点,导致其他应用无法使用音频焦点。或者是其他应用把原本属于本应用的音频焦点抢占了,也会导致本应用无法使用音频焦点,比如某些应用内的语音通话功能就会抢占音频焦点。
处理策略:当AudioManager.OnAudioFocusChangeListener监听器监听到音频焦点已失去时(AUDIOFOCUS_LOSS、AUDIOFOCUS_LOSS_TRANSIENT),就主动通过弹窗或toast提示用户【关闭其他应用或重启手机】,以期望达到让其他应用释放音频焦点的目的。
-
问题2:正常来说,当B应用把音频焦点释放后,A应用是可以重新获取到音频焦点的回调(AUDIOFOCUS_GAIN),但某些Android手机可能会有系统bug,即不会主动通知A应用音频焦点已经回来了,A应用无法拿到AUDIOFOCUS_GAIN这个回调,A应用也就无法再恢复播放音频。
处理策略:增加音频功能的调用时机。比如在切页或者做其他操作的时候,主动尝试使用语音唤醒的音频功能,而不是通过AudioManager.OnAudioFocusChangeListener监听器被动地等待回调。当主动尝试使用语音唤醒成功时,说明音频焦点已经回来了。