16、蓝牙音频(A2DP/HFP)开发:A2DP Profile连接管理、音频焦点处理、HFP通话控制、SCO连接
各位同学,今天我们来聊聊蓝牙音频开发。说实话,这部分是蓝牙开发里最"接地气"的——毕竟用户每天听歌、打电话,体验好不好全看这里。A2DP和HFP这两个Profile,一个管音乐,一个管通话,是蓝牙音频的左右护法。
我个人习惯把蓝牙音频开发分成两条线:一条是数据流(音频怎么传),一条是控制流(连接怎么管、焦点怎么抢)。今天咱们重点讲控制流,数据流部分后面章节会细说。
16.1 A2DP Profile连接管理
A2DP,全称Advanced Audio Distribution Profile,说白了就是"蓝牙听歌协议"。它负责把手机上的立体声音频流,通过蓝牙传到耳机或音箱上。
连接管理这块,我建议你重点关注三个状态:连接建立、连接断开、连接重连。Android系统里,A2DP的连接状态是通过BluetoothA2dp这个代理类来监控的。
16.1.1 连接状态回调
先看一段核心代码,这是监听A2DP连接状态的广播接收器:
// 注册广播接收器
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
registerReceiver(mA2dpReceiver, filter);
// 接收器实现
private final BroadcastReceiver mA2dpReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE,
BluetoothA2dp.STATE_DISCONNECTED);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
switch (state) {
case BluetoothA2dp.STATE_CONNECTED:
// 连接成功,可以开始音频传输
Log.d("A2DP", "设备已连接: " + device.getName());
break;
case BluetoothA2dp.STATE_DISCONNECTED:
// 连接断开,需要清理资源
Log.d("A2DP", "设备已断开: " + device.getName());
break;
case BluetoothA2dp.STATE_CONNECTING:
// 连接中,显示加载状态
break;
}
}
}
};
嗯,这里要注意:EXTRA_STATE和EXTRA_PREVIOUS_STATE两个参数配合使用,可以判断状态变化的方向。比如从STATE_CONNECTING到STATE_CONNECTED,说明连接成功了。
16.1.2 主动发起连接与断开
主动连接A2DP设备,需要先获取BluetoothA2dp代理:
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothA2dp a2dp = (BluetoothA2dp) adapter.getProfileProxy(
context,
mProfileListener,
BluetoothProfile.A2DP
);
// 连接设备
a2dp.connect(device);
// 断开设备
a2dp.disconnect(device);
你想想看,为什么有时候调用connect()没反应?大概率是因为设备不在已配对列表里,或者蓝牙没打开。我建议你在调用前先检查device.getBondState()是否为BOND_BONDED。
16.2 音频焦点处理
音频焦点,说白了就是"谁有资格出声"。Android系统里,多个应用可以同时请求音频焦点,但只有拿到焦点的应用才能播放音频。蓝牙音频场景下,焦点处理尤其重要——你总不希望耳机里同时传出音乐和导航语音吧?
16.2.1 请求音频焦点
Android从8.0开始推荐使用AudioFocusRequest来请求焦点:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChangeListener)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 成功获取焦点,开始播放
}
AUDIOFOCUS_GAIN而不是AUDIOFOCUS_GAIN_TRANSIENT。因为音乐播放是持续性的,临时焦点会导致频繁的焦点切换,影响体验。
16.2.2 焦点变化处理
当其他应用(比如电话)抢走焦点时,你的应用需要做出响应:
private final AudioManager.OnAudioFocusChangeListener focusChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
// 永久失去焦点,停止播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时失去焦点,暂停播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可以降低音量继续播放(比如导航语音)
setVolume(0.3f);
break;
case AudioManager.AUDIOFOCUS_GAIN:
// 重新获得焦点,恢复播放
resumePlayback();
break;
}
}
};
为什么会区分LOSS_TRANSIENT和LOSS_TRANSIENT_CAN_DUCK?前者是"你闭嘴",后者是"你小声点"。我记得有一次做车载蓝牙项目,导航语音和音乐同时播放,就是因为没正确处理CAN_DUCK,结果导航说话时音乐完全停了,用户投诉说"导航一说话歌就断"。
16.3 HFP通话控制
HFP(Hands-Free Profile)是蓝牙通话的核心协议。它负责把手机的通话音频路由到蓝牙耳机,同时支持接听、挂断、音量调节等控制命令。
16.3.1 通话状态监听
HFP的通话状态变化,通过BluetoothHeadset的广播来监听:
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(mHfpReceiver, filter);
private final BroadcastReceiver mHfpReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
switch (state) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
// SCO连接已建立,通话音频开始传输
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
// SCO连接断开,通话结束
break;
}
}
}
};
ACTION_AUDIO_STATE_CHANGED)和连接状态(ACTION_CONNECTION_STATE_CHANGED)是两回事。连接状态表示HFP服务是否建立,音频状态表示SCO链路是否激活。我曾经见过有人把这两个搞混,结果通话时音频死活出不来。
16.3.2 接听与挂断电话
通过HFP接听或挂断电话,需要调用BluetoothHeadset的方法:
BluetoothHeadset headset = (BluetoothHeadset) adapter.getProfileProxy(
context,
mProfileListener,
BluetoothProfile.HEADSET
);
// 接听电话
headset.acceptCall(device);
// 挂断电话
headset.rejectCall(device);
嗯,这里有个坑:acceptCall()和rejectCall()只能在SCO连接建立后调用。如果SCO还没建立,调用会返回false。我建议你在调用前先检查getAudioState(device)是否为STATE_AUDIO_CONNECTED。
16.4 SCO连接
SCO(Synchronous Connection Oriented)链路,是蓝牙通话音频的传输通道。它和A2DP的ACL链路不同,SCO是同步的、面向连接的,专门用于语音传输。
16.4.1 SCO连接的生命周期
SCO连接的建立和断开,通常由系统自动管理。但有些场景下,你需要手动控制:
// 手动建立SCO连接
BluetoothHeadset headset = ...;
headset.startVoiceRecognition(device); // 这会触发SCO连接
// 手动断开SCO连接
headset.stopVoiceRecognition(device); // 这会断开SCO连接
你想想看,为什么有时候startVoiceRecognition()会失败?大概率是因为设备不支持语音识别,或者SCO资源被占用了。我记得有一次做对讲机App,多个应用同时请求SCO,结果系统直接拒绝了所有请求。后来我们加了一个SCO请求队列,才解决了这个问题。
16.4.2 SCO与A2DP的共存
这是蓝牙音频开发里最头疼的问题之一。SCO和A2DP共享蓝牙射频资源,当SCO激活时,A2DP的带宽会被压缩,导致音质下降甚至卡顿。
| 场景 | A2DP状态 | SCO状态 | 用户体验 |
|---|---|---|---|
| 听音乐 | 激活 | 未激活 | 正常音质 |
| 打电话 | 暂停 | 激活 | 通话正常 |
| 音乐+通话 | 降级 | 激活 | 音质下降 |
a2dp.setCodecConfigPreference(device, codecConfig)。
16.5 知识体系总览
下面这张图,是我梳理的蓝牙音频开发核心逻辑。你可以把它当作一个"地图",开发时对照着看,不容易迷路。
这张图把蓝牙音频开发分成了两条主线:左边是A2DP(音乐),右边是HFP(通话)。底部是两者共存时的资源竞争问题。开发时,你心里要时刻装着这张图——遇到问题先定位是哪个环节出的问题。
好了,这一章的内容就到这里。A2DP和HFP是蓝牙音频的两大支柱,理解它们的连接管理和音频控制逻辑,是后续开发的基础。下一章我们会深入A2DP的音频编码协商,看看SBC、AAC、LDAC这些编码到底是怎么工作的。
公众号:蓝海资料掘金营,微信deep3321