13、音频焦点与并发:AudioFocus机制、多应用音频混合、低延时场景下的焦点处理

音频焦点,说白了就是系统里的一套“礼貌规则”。

你想想看,手机同时只能出一个声音。要是导航、音乐、游戏、通话一起抢喇叭,那不乱套了?Android 用 AudioFocus 来管这件事。谁拿到焦点,谁出声;丢了焦点的,要么暂停,要么降低音量。

但在低延时场景下,这套规则有时候会“好心办坏事”。我遇到过好几次,游戏里刚开麦,背景音乐突然被掐断,玩家直接骂街。嗯,今天我们就来把这套机制掰开揉碎,看看怎么在低延时需求下优雅地处理音频并发。

核心观点: AudioFocus 不是性能问题,而是策略问题。低延时场景下,焦点处理的核心是“快”和“稳”——快速响应焦点变化,稳定控制音频流状态。

13.1 AudioFocus 机制:谁说了算?

AudioFocus 本质上是一个“令牌”。应用通过 requestAudioFocus() 去申请,系统根据优先级和当前焦点持有者来决定给不给。

焦点类型有几种:

  • AudioManager.AUDIOFOCUS_GAIN:长期持有焦点,比如音乐播放器。
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:短暂持有,比如通知音、导航提示。
  • AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:短暂持有,但允许其他应用降低音量继续播放(俗称“闪避”)。

焦点丢失的回调也很关键:

  • onAudioFocusChange(AUDIOFOCUS_LOSS):永久丢失,通常需要暂停播放。
  • onAudioFocusChange(AUDIOFOCUS_LOSS_TRANSIENT):短暂丢失,暂停后等恢复。
  • onAudioFocusChange(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK):短暂丢失,降低音量即可。
个人习惯: 我在项目中一般把焦点回调封装成一个独立的 Listener,而不是直接写在 Activity 或 Service 里。这样方便单元测试,也方便在多个音频源之间切换。

13.2 多应用音频混合:谁的声音更大?

Android 从 8.0 开始引入了 音频策略(AudioPolicy),系统可以同时让多个应用发声,但通过混音器(AudioFlinger)来调整每个流的音量、采样率、通道数。

低延时场景下,混音器可能会引入额外的延迟。为什么呢?因为混音需要把多个音频流的数据按时间对齐,然后叠加。如果某个流的采样率不一致,还得做重采样——这玩意儿很耗时间。

我建议你在多应用并发时,尽量统一采样率。比如所有音频流都用 48000Hz,避免重采样带来的延迟抖动。

场景 推荐采样率 延迟影响
音乐 + 导航 48000Hz 低(无需重采样)
游戏 + 语音 48000Hz 低(建议使用 AAudio)
通话 + 媒体 16000Hz / 48000Hz 中(需要混音策略)
避坑指南: 我曾经在一个直播项目中,发现两个应用同时使用 OpenSL ES 播放,结果混音后出现了明显的“爆音”。排查了半天,发现是其中一个应用没有正确设置 SL_ANDROID_KEY_STREAM_TYPE,导致 AudioFlinger 把它当成了不同的流类型,混音策略出了问题。记住:流类型(STREAM_MUSIC、STREAM_VOICE_CALL 等)一定要明确指定。

13.3 低延时场景下的焦点处理:快进快出

低延时应用(比如实时语音、游戏音效)对焦点的要求很特殊:

  • 不能因为焦点丢失而卡顿。
  • 不能因为焦点申请而增加延迟。
  • 不能因为焦点恢复而出现“啪”的一声(pop noise)。

我一般会这样做:

  1. 预申请焦点:在音频流启动前 100ms 就申请焦点,而不是等到播放那一刻。这样可以避免焦点申请阻塞音频线程。
  2. 使用 TRANSIENT_MAY_DUCK:对于游戏音效,尽量用这个类型。这样即使丢了焦点,也只是降低音量,不会暂停。玩家体验会好很多。
  3. 焦点丢失时静音,但不释放资源:低延时场景下,释放和重新初始化音频设备很耗时。我建议焦点丢失时只做静音处理,保留音频流和缓冲区。等焦点恢复时,直接取消静音,延迟可以控制在 1ms 以内。
关键代码片段: 焦点丢失时静音,不释放资源
// 焦点变化回调
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_LOSS:
            // 永久丢失,暂停播放
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // 短暂丢失,静音但不释放
            setMute(true);
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // 降低音量
            setVolume(0.3f);
            break;
        case AudioManager.AUDIOFOCUS_GAIN:
            // 恢复焦点,取消静音
            setMute(false);
            setVolume(1.0f);
            break;
    }
}

你可能会问:为什么不直接暂停?因为暂停再恢复,中间涉及音频设备重新打开、缓冲区重新填充,延迟可能高达 50ms 以上。对于实时语音来说,50ms 的延迟已经能被人耳感知到了。

13.4 知识体系:一张图看懂音频焦点与并发

下面这张 SVG 图,我把本章的核心逻辑画了出来。你可以看到焦点申请、混音、低延时处理这三条线是如何交织的。

音频焦点与并发核心逻辑 应用A(游戏) 应用B(音乐) 应用C(语音) AudioFocus 管理器(系统服务) 音频策略(AudioPolicy) AudioFlinger 混音器(统一采样率、混音输出) 低延时 注:低延时场景下,焦点丢失时建议静音而非释放资源,以减少恢复延迟。

13.5 实战建议:如何优雅地处理焦点?

最后,我总结几条实战经验:

  • 不要在主线程申请焦点requestAudioFocus() 虽然是异步的,但回调可能发生在主线程。如果你在音频线程里直接调用,可能会造成线程阻塞。我习惯在音频线程初始化时,提前通过 Handler 在主线程申请焦点。
  • 使用 AudioFocusRequest.Builder:Android 8.0 之后推荐用这个 API,可以设置延迟播放、暂停时是否闪避等参数。比老版的 requestAudioFocus() 更灵活。
  • 测试多应用并发:别只在单应用环境下测试。打开音乐播放器、导航、游戏同时运行,看看焦点切换是否流畅。我曾经在测试时发现,某个应用在焦点丢失后没有及时静音,导致两个声音叠加,用户体验极差。
一个小技巧: 如果你做的是游戏引擎,建议把焦点管理封装到引擎的音频模块里。这样所有游戏音效都走同一套焦点逻辑,不会出现“某个音效突然不响了”的诡异问题。

嗯,关于音频焦点与并发,今天就聊到这里。记住:焦点不是枷锁,而是工具。用好了,多应用共存也能很丝滑。


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