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同时申请焦点时,系统会按优先级裁决。优先级怎么定?看UsageContentType。比如导航的Usage是USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,它的优先级天然高于音乐播放器的USAGE_MEDIA

冲突发生时,系统会:

  1. 通知当前持有焦点的App:你失去焦点了(通过onAudioFocusChange()回调)。
  2. 根据失去焦点的类型,App需要做出响应:暂停、闪避、或者忽略。
  3. 新App获得焦点,开始播放。

我举个例子。你在听网易云音乐,突然微信视频电话来了。这时候:

  • 微信申请AUDIOFOCUS_GAIN_TRANSIENT(短暂焦点)
  • 系统通知网易云:你失去焦点,类型是AUDIOFOCUS_LOSS_TRANSIENT
  • 网易云应该暂停播放
  • 微信通话结束后释放焦点
  • 系统通知网易云:焦点回来了(AUDIOFOCUS_GAIN
  • 网易云恢复播放

你看,这个流程环环相扣。任何一个环节没处理好,用户体验就崩了。

避坑指南:我曾经遇到一个App,在收到AUDIOFOCUS_LOSS_TRANSIENT后没有暂停播放,而是继续出声。结果用户打电话时,背景音乐还在响——直接被用户投诉到应用商店下架。所以,失去焦点时必须静音,这是底线。

22.3 多实例音频路由

多实例音频路由,说白了就是「多个音频流怎么走」。Android从5.0开始引入了AudioRouting接口,允许App指定音频输出到哪个设备。比如:

  • 音乐走蓝牙耳机
  • 导航走车载扬声器
  • 电话走听筒

这个功能在车载和IoT设备上特别重要。我记得做智能音箱项目时,要求「语音助手从顶部扬声器出声,音乐从底部扬声器出声」——这就是典型的多实例路由。

实现方式是通过AudioAttributesAudioDeviceInfo配合:

// 创建音频属性,指定用途
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知识体系,你可以把它当作思维导图来用:

AudioFocus 核心机制 焦点类型 GAIN(永久) GAIN_TRANSIENT(短暂) GAIN_TRANSIENT_MAY_DUCK(闪避) 冲突处理 LOSS(永久失去)→ 停止 LOSS_TRANSIENT → 暂停 LOSS_TRANSIENT_CAN_DUCK → 闪避 多实例路由 AudioAttributes 指定用途 setPreferredDevice 指定设备 fallback 到默认设备 三层防御:系统机制 + 状态机 + 业务策略 AudioFocus 不是硬件锁,是软件层面的播放权限仲裁 所有App必须遵守焦点协议,否则会被系统静音 申请成功 冲突仲裁 焦点丢失 恢复播放

22.6 实战中的几个关键点

最后,我分享几个实战中容易踩的坑:

  • 不要重复申请焦点:如果已经持有焦点,再次申请会导致回调混乱。我习惯用一个boolean hasFocus标记来防止重复申请。
  • 释放焦点要及时:播放结束后必须调用abandonAudioFocus()。否则其他App永远拿不到焦点。
  • 闪避音量要合理MAY_DUCK场景下,音量降到20%-30%比较合适。降太多用户听不清,降太少起不到提示作用。
  • 测试要覆盖所有场景:电话进来、闹钟响起、通知弹出、语音助手唤醒……每个场景都要测。我当年就是漏测了「闹钟+音乐」的组合,结果上线后用户反馈闹钟响时音乐还在播。

嗯,关于AudioFocus就讲这么多。这个机制看似简单,但要做好其实不容易。核心就一句话:尊重焦点,及时响应,状态清晰。做到这三点,多音频管理就不会出大问题。


公众号:蓝海资料掘金营,微信deep3321