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 知识体系总览
音频路由管理涉及的知识点比较多,我画了一张图帮你理清思路:
17.7 避坑指南
最后,分享几个我亲身踩过的坑:
- 不要依赖默认路由:不同厂商的默认音频路由策略不同。华为手机插耳机时,默认从耳机出;小米手机可能从扬声器出。一定要主动设置。
- 蓝牙切换要延迟:蓝牙连接成功后,不要立即切换音频路由。系统需要几百毫秒来建立音频通道。我一般延迟500ms再切换。
- 音频焦点要成对使用:请求了焦点一定要释放,否则其他App永远拿不到焦点。我曾经在某个版本中忘了释放,导致用户退出会议后,微信语音一直没声音。
- 测试要覆盖所有机型:音频路由是厂商定制最多的模块之一。我建议至少测试华为、小米、OPPO、vivo、三星这五家的主流机型。
音频路由管理,看似简单,实则细节很多。但只要掌握了核心原理,再加上充分的测试,就能给用户一个流畅的通话体验。