17、音频路由管理:扬声器与听筒切换、蓝牙耳机支持、音频焦点管理

音频路由管理,说白了就是让声音从正确的设备出来。你想想看,用户戴着蓝牙耳机开会,声音却从扬声器外放,这体验得多糟糕。我在做视频会议App时,音频路由这块踩过的坑,比视频渲染还多。今天咱们就把这块彻底讲透。

17.1 音频路由的核心概念

音频路由,本质上就是选择音频的输入输出设备。Android系统提供了多种音频设备:

  • 扬声器:外放,声音大,但容易产生回声
  • 听筒:贴近耳朵,私密性好
  • 有线耳机:3.5mm或Type-C接口
  • 蓝牙耳机:无线,支持A2DP和HFP协议
  • USB音频设备:外接麦克风、音箱等

WebRTC默认使用扬声器输出。但视频会议场景下,用户经常需要在扬声器和听筒之间切换。比如开会时想私聊,就得切到听筒。嗯,这里要注意,切换不是简单的setSpeakerphoneOn就能搞定。

17.2 扬声器与听筒切换

Android提供了AudioManager来管理音频路由。核心API就两个:

// 获取AudioManager
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

// 切换到扬声器
audioManager.setSpeakerphoneOn(true);

// 切换到听筒
audioManager.setSpeakerphoneOn(false);

看起来很简单对吧?但我告诉你,这里有个大坑。直接调用setSpeakerphoneOn(false),声音不一定从听筒出来。为什么?因为系统会优先选择当前可用的音频设备。如果插着耳机,声音还是从耳机出。

⚠️ 我曾经踩过的坑:

在华为某款手机上,调用setSpeakerphoneOn(false)后,声音既没从听筒出,也没从扬声器出,直接静音了。后来发现是系统音频策略的问题,需要配合setMode()一起使用。

正确的做法是:

private void switchToEarpiece() {
    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    // 先设置音频模式为通话模式
    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
    // 再关闭扬声器
    audioManager.setSpeakerphoneOn(false);
}

private void switchToSpeaker() {
    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
    audioManager.setSpeakerphoneOn(true);
}

我个人习惯在切换时,先判断当前音频设备状态:

public boolean isSpeakerOn() {
    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    return audioManager.isSpeakerphoneOn();
}

17.3 蓝牙耳机支持

蓝牙耳机是视频会议的标配。但蓝牙音频协议分两种:

协议 用途 延迟 质量
A2DP 音乐播放
HFP 通话

WebRTC通话时,系统会自动选择HFP协议。但问题来了——HFP的音频质量很差,采样率只有8kHz。我建议在视频会议中,如果用户使用蓝牙耳机,尽量用A2DP协议。但A2DP延迟高,不适合实时通话。这是个两难选择。

💡 我的经验:

在项目中,我采用动态切换策略。当用户说话时,用HFP保证低延迟;当用户听别人说话时,用A2DP保证音质。虽然实现复杂,但体验提升明显。

监听蓝牙设备连接状态:

private BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
            // 蓝牙耳机已连接
            Log.d(TAG, "蓝牙耳机已连接");
            // 自动切换到蓝牙
            switchToBluetooth();
        } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
            // 蓝牙耳机已断开
            Log.d(TAG, "蓝牙耳机已断开");
            // 自动切回扬声器
            switchToSpeaker();
        }
    }
};

注册广播接收器:

IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
context.registerReceiver(bluetoothReceiver, filter);

17.4 音频焦点管理

音频焦点,是Android系统用来协调多个应用同时播放音频的机制。说白了,就是谁有资格出声。视频会议App必须正确处理音频焦点,否则会出现声音冲突。

我记得有一次,用户反馈开会时突然没声音了。排查了半天,发现是微信语音消息抢了音频焦点。从那以后,我对音频焦点的处理格外小心。

请求音频焦点:

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioAttributes attributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
        .build();

AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(attributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(focusChangeListener)
        .build();

int result = audioManager.requestAudioFocus(focusRequest);

音频焦点变化监听:

private AudioManager.OnAudioFocusChangeListener focusChangeListener = 
    new AudioManager.OnAudioFocusChangeListener() {
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // 获得焦点,可以正常播放
                Log.d(TAG, "获得音频焦点");
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                // 永久失去焦点,应该暂停播放
                Log.d(TAG, "永久失去音频焦点");
                // 暂停通话或降低音量
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                // 暂时失去焦点,可以降低音量
                Log.d(TAG, "暂时失去音频焦点");
                // 降低音量
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 暂时失去焦点,可以降低音量(闪避)
                Log.d(TAG, "闪避模式");
                // 降低音量到20%
                break;
        }
    }
};
🔑 关键点:

视频会议App应该请求AUDIOFOCUS_GAIN,而不是AUDIOFOCUS_GAIN_TRANSIENT。因为通话是持续性的,不是临时播放。我见过不少App用错了焦点类型,导致通话过程中被其他App打断。

释放音频焦点:

audioManager.abandonAudioFocusRequest(focusRequest);

17.5 完整音频路由管理方案

下面是我在项目中使用的完整音频路由管理类核心逻辑:

public class AudioRouter {
    private AudioManager audioManager;
    private boolean isBluetoothConnected = false;
    
    public void init(Context context) {
        audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        // 设置通话模式
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        // 请求音频焦点
        requestAudioFocus();
        // 监听蓝牙状态
        registerBluetoothReceiver(context);
    }
    
    public void switchRoute(AudioRoute route) {
        switch (route) {
            case SPEAKER:
                audioManager.setSpeakerphoneOn(true);
                break;
            case EARPIECE:
                audioManager.setSpeakerphoneOn(false);
                break;
            case BLUETOOTH:
                // 蓝牙路由由系统自动管理
                audioManager.setSpeakerphoneOn(false);
                break;
        }
    }
    
    public enum AudioRoute {
        SPEAKER,
        EARPIECE,
        BLUETOOTH
    }
}

17.6 知识体系总览

音频路由管理涉及的知识点比较多,我画了一张图帮你理清思路:

音频路由管理知识体系 音频路由管理 扬声器与听筒切换 蓝牙耳机支持 音频焦点管理 setSpeakerphoneOn setMode A2DP协议 HFP协议 AUDIOFOCUS_GAIN 焦点变化监听 核心原则:用户选择优先,系统策略兜底 自动切换 + 手动控制 + 焦点协调 注意:不同厂商的音频策略有差异,需要充分测试 华为、小米、三星等机型在音频路由上各有"特色"

17.7 避坑指南

最后,分享几个我亲身踩过的坑:

  1. 不要依赖默认路由:不同厂商的默认音频路由策略不同。华为手机插耳机时,默认从耳机出;小米手机可能从扬声器出。一定要主动设置。
  2. 蓝牙切换要延迟:蓝牙连接成功后,不要立即切换音频路由。系统需要几百毫秒来建立音频通道。我一般延迟500ms再切换。
  3. 音频焦点要成对使用:请求了焦点一定要释放,否则其他App永远拿不到焦点。我曾经在某个版本中忘了释放,导致用户退出会议后,微信语音一直没声音。
  4. 测试要覆盖所有机型:音频路由是厂商定制最多的模块之一。我建议至少测试华为、小米、OPPO、vivo、三星这五家的主流机型。

音频路由管理,看似简单,实则细节很多。但只要掌握了核心原理,再加上充分的测试,就能给用户一个流畅的通话体验。


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