1. 项目背景与核心需求
最近在开发一个需要通过蓝牙连接第三方硬件设备的安卓应用,硬件设备会实时传输声音数据到手机端,应用需要接收并解析这些数据。这个需求在医疗监护、工业检测、智能家居等领域都很常见。比如心电监护仪通过蓝牙发送生理信号,或者智能门铃传输实时音频流。
整个流程可以拆解为四个关键环节:蓝牙设备发现与配对、建立GATT连接、配置数据传输通道、实时音频流解析。每个环节都有不少技术细节需要特别注意,尤其是安卓蓝牙API的版本差异和权限管理。
2. 蓝牙协议栈与硬件准备
2.1 蓝牙协议选择
蓝牙4.0以上的BLE(低功耗蓝牙)是最常用的方案,但需要注意经典蓝牙和BLE在音频传输上的差异:
- 经典蓝牙:支持A2DP协议(高质量音频)和HFP协议(语音通话质量)
- BLE:需要通过自定义服务传输原始音频数据,带宽较低但功耗更优
如果硬件设备已经固定,需要确认其使用的蓝牙协议。我最近做的一个助听器项目就遇到这个问题——硬件厂商使用的是BLE 5.0,但传输的是16kHz采样的音频流,这对BLE的吞吐量是个挑战。
2.2 硬件设备配置
硬件端需要预先设置好:
- 蓝牙服务UUID(如00001101-0000-1000-8000-00805F9B34FB)
- 音频特征值UUID(用于数据传输)
- 音频格式(PCM、ADPCM等)
- 传输参数(采样率、位深、声道数)
建议让硬件工程师提供完整的蓝牙服务规范文档。曾经有个项目因为没确认这些参数,导致后期要重写整个音频解析模块。
3. 安卓端实现步骤
3.1 权限与特性声明
在AndroidManifest.xml中添加:
xml复制<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- 安卓6.0+需要 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
注意:从Android 12开始,还需要声明BLUETOOTH_CONNECT和BLUETOOTH_SCAN权限,且需要动态申请。
3.2 设备扫描与连接
使用BluetoothAdapter启动设备扫描:
kotlin复制val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
val bleScanner = bluetoothAdapter?.bluetoothLeScanner
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(SERVICE_UUID))
.build()
)
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bleScanner?.startScan(filters, settings, scanCallback)
扫描到设备后,通过BluetoothDevice.connectGatt()建立连接。这里有个关键点:在Android 7.0之后,建议使用TRANSPORT_LE参数:
kotlin复制device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
3.3 GATT服务发现与配置
连接成功后,在BluetoothGattCallback中处理服务发现:
kotlin复制override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID)
audioCharacteristic = service.getCharacteristic(AUDIO_CHARACTERISTIC_UUID)
// 启用通知
gatt.setCharacteristicNotification(audioCharacteristic, true)
// 配置客户端特征配置描述符(CCC)
val cccd = audioCharacteristic.getDescriptor(CCC_DESCRIPTOR_UUID)
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd)
}
}
4. 音频数据接收与处理
4.1 数据接收回调
在GattCallback中实现onCharacteristicChanged处理数据:
kotlin复制override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
when (characteristic.uuid) {
AUDIO_CHARACTERISTIC_UUID -> {
val audioData = characteristic.value
audioProcessor.onDataReceived(audioData)
}
}
}
4.2 音频流解析
根据硬件使用的音频格式进行解析。以16位单声道PCM为例:
kotlin复制fun processPCMData(data: ByteArray) {
val samples = ShortArray(data.size / 2)
ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples)
// 后续处理(FFT、播放等)
audioTrack.write(samples, 0, samples.size)
}
如果硬件使用压缩格式(如ADPCM),需要先解码。我曾经使用Android的MediaCodec来处理G.711编码的音频:
kotlin复制val codec = MediaCodec.createDecoderByType("audio/g711-alaw")
codec.configure(format, null, null, 0)
codec.start()
// 在数据回调中
val inputBufferId = codec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferId >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferId)
inputBuffer.put(audioData)
codec.queueInputBuffer(inputBufferId, 0, audioData.size, presentationTimeUs, 0)
}
5. 性能优化与问题排查
5.1 传输稳定性优化
BLE连接常见问题及解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 频繁断开 | 信号干扰 | 调整MTU大小(gatt.requestMtu(247)) |
| 数据丢失 | 传输速率过高 | 降低采样率或增加数据包间隔 |
| 延迟高 | 连接间隔过长 | 修改连接参数(gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)) |
5.2 音频处理优化
- 双缓冲技术:准备两个缓冲区交替接收和处理数据
- 预处理线程:避免在主线程执行FFT等耗时操作
- 动态采样率适配:根据设备性能自动调整
kotlin复制// 环形缓冲区实现示例
class AudioBuffer(capacity: Int) {
private val buffer = ShortArray(capacity)
private var head = 0
private var tail = 0
fun write(data: ShortArray) {
// 实现线程安全的写入逻辑
}
fun read(dest: ShortArray): Int {
// 实现线程安全的读取逻辑
}
}
6. 实测经验与避坑指南
-
设备兼容性问题:
- 某些厂商的BLE实现在服务发现后不会自动发送通知,需要手动读取特征值
- 华为/荣耀设备可能需要特殊处理连接参数
-
权限陷阱:
- Android 10+需要在后台时保持BLUETOOTH_SCAN权限
- 某些设备即使授予了位置权限,扫描仍可能失败
-
音频同步问题:
- 建议在数据包中添加时间戳
- 使用Android的AudioTrack时注意设置正确的播放模式(MODE_STREAM)
-
内存泄漏预防:
kotlin复制override fun onDestroy() { gatt?.disconnect() gatt?.close() // 必须调用close释放资源 super.onDestroy() } -
调试技巧:
- 使用nRF Connect等工具验证硬件端配置
- 在开发者选项中开启"蓝牙HCI日志"抓包分析
这个方案已经成功应用在多个医疗设备项目中,实测在三星S10、Pixel 5等设备上可以实现<50ms的音频延迟。关键是要充分测试不同厂商的设备,特别是国内品牌的定制ROM可能会有特殊行为。
