17、USB音频设备:USB Audio Class驱动、USB设备枚举与配置、USB音频流管理、热插拔处理
好,咱们今天聊聊USB音频设备。说实话,这个模块在AudioFlinger里属于那种「平时不显山露水,一出问题就让人头大」的部分。我最早接触USB音频是在做车载系统的时候,用户插上U盘听歌,结果声音断断续续,还时不时爆音。排查到最后,发现是USB音频流的同步方式没选对。
嗯,咱们今天就把这块彻底讲透。从驱动层到应用层,把USB音频的整个链路理清楚。
17.1 USB Audio Class驱动:谁在管这件事?
先问一个问题:你插一个USB耳机到手机上,系统怎么知道这是个音频设备?
答案就是USB Audio Class(UAC)驱动。这是Linux内核里专门负责识别和管理USB音频设备的模块。Android直接继承了这个驱动框架。
UAC驱动分两个版本:
- UAC 1.0:老协议,带宽有限,最高支持96kHz/24bit。胜在兼容性好,很多老设备都支持。
- UAC 2.0:新协议,支持到384kHz/32bit,甚至DSD。延迟更低,功能更丰富。
我个人习惯在调试时先看一眼内核日志,确认设备被识别成了哪个版本:
// 查看USB音频设备信息
adb shell dmesg | grep -i "usb audio"
// 输出示例
[ 1234.5678] usb 1-1: new high-speed USB device number 5 using xhci_hcd
[ 1234.6789] usb 1-1: New USB device found, idVendor=0x1234, idProduct=0x5678
[ 1234.6790] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 1234.6791] usb 1-1: Product: USB Audio Device
[ 1234.6792] usb 1-1: Manufacturer: Generic
[ 1234.7890] input: USB Audio Device as /devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.3/input/input20
[ 1234.7891] usbcore: registered new interface driver snd-usb-audio
看到 snd-usb-audio 这个驱动加载成功,说明内核层已经认了。接下来就看AudioFlinger怎么跟它打交道了。
dmesg 看一眼驱动有没有报错,能省一半的排查时间。
17.2 USB设备枚举与配置:AudioFlinger怎么知道你是谁?
驱动加载成功后,USB设备会通过控制传输(Control Transfer)向主机报告自己的能力。这个过程叫「枚举」。说白了就是设备跟主机说:「我是谁,我能干啥,你需要怎么用我。」
枚举的核心是描述符(Descriptor)。USB音频设备会提供以下几类描述符:
| 描述符类型 | 作用 | 我遇到过的问题 |
|---|---|---|
| 设备描述符 | 厂商ID、产品ID、版本号 | 有些山寨设备填的ID是假的,导致驱动匹配不上 |
| 配置描述符 | 供电方式、接口数量 | 自供电设备如果供电不足,枚举会失败 |
| 接口描述符 | 音频功能类型(播放/录音/控制) | 有些设备把播放和录音放在同一个接口,处理起来要小心 |
| 端点描述符 | 传输类型、最大包大小、轮询间隔 | 等时传输的包大小算错了,声音就会卡顿 |
在AudioFlinger里,USB设备的枚举最终会映射成一个 AudioHwDevice 对象。这个过程大致是这样的:
// AudioFlinger中USB设备枚举的简化流程
// 1. HAL层检测到USB设备插入
// 2. 调用 loadAudioInterface() 加载对应的so库
// 3. 通过 openOutputStream() 创建音频流
// 4. 设置采样率、通道数、格式等参数
status_t AudioFlinger::openOutput(...) {
// 这里会调用到USB HAL的open_output_stream
// 如果返回失败,AudioFlinger会尝试fallback到其他设备
...
}
嗯,这里要注意一点:USB设备的配置不是一次性的。如果用户拔掉再插上,或者换了个USB口,整个枚举过程会重新走一遍。所以代码里一定要处理好状态重置。
17.3 USB音频流管理:数据是怎么跑起来的?
枚举完了,配置好了,接下来就是真正的音频数据传输了。USB音频流用的是等时传输(Isochronous Transfer)。为什么不用批量传输?因为音频对实时性要求高,等时传输能保证固定的带宽和间隔。
等时传输有两种模式:
- 自适应模式(Adaptive):设备根据主机的数据速率调整自己的时钟。说白了就是「你发多少我收多少,你慢我也慢」。
- 异步模式(Asynchronous):设备有自己的时钟,通过反馈端点告诉主机「你该发快一点」或「你该发慢一点」。这是目前主流的方式,延迟更低。
我在项目中遇到过一个问题:某款USB DAC只支持异步模式,但我们的代码默认用了自适应模式。结果声音时快时慢,像磁带卡住了一样。后来在 AudioStreamOut 的配置里强制指定了异步模式才解决。
来看看AudioFlinger里USB音频流的数据路径:
// USB音频数据流路径(简化版)
// App -> AudioFlinger MixerThread -> AudioStreamOut -> USB HAL -> USB设备
// 关键代码:AudioStreamOut::write()
ssize_t AudioStreamOut::write(const void* buffer, size_t bytes) {
// 1. 检查设备是否还在(热插拔处理)
// 2. 将数据分割成USB包(每个包大小由端点描述符决定)
// 3. 通过URB(USB Request Block)提交给USB控制器
// 4. 等待传输完成回调
...
}
这里有个性能关键点:USB包的大小和数量直接影响延迟。包越小,延迟越低,但CPU开销越大。包越大,延迟越高,但更省CPU。我一般建议在调试时先用默认值,然后根据实际场景微调。
17.4 热插拔处理:插拔之间,系统在做什么?
USB音频设备最大的特点就是「随时可能被拔掉」。你想想看,用户正听着歌呢,突然把USB耳机拔了,系统该怎么办?
AudioFlinger的热插拔处理分为三个层面:
- 内核层:USB控制器检测到设备断开,触发
disconnect事件,驱动清理资源。 - HAL层:通过
adev->callback通知AudioFlinger「设备没了」。 - AudioFlinger层:停止当前音频流,切换到默认输出设备(比如扬声器)。
我曾经踩过一个坑:某款手机在拔掉USB耳机后,系统没有自动切换到扬声器。排查发现是HAL层的回调没有正确实现。具体来说,audio_hw_device 的 init_check 函数返回了错误的值,导致AudioFlinger以为设备还在。
正确的处理逻辑应该是这样的:
// HAL层热插拔回调示例
static int adev_open(const hw_module_t* module, const char* name,
hw_device_t** device) {
// ... 初始化代码 ...
// 注册热插拔监听
usb_audio_register_callback(usb_device_connected_cb, usb_device_disconnected_cb);
return 0;
}
static void usb_device_disconnected_cb(void* context) {
// 通知AudioFlinger设备断开
struct audio_hw_device* adev = (struct audio_hw_device*)context;
adev->callback(NOTIFY_DISCONNECT, ...);
}
在AudioFlinger这边,收到断开通知后会做这些事情:
- 关闭对应的
AudioStreamOut和AudioStreamIn - 从设备列表中移除该USB设备
- 触发音频策略管理器重新选择输出设备
- 如果当前有音频正在播放,自动切换到扬声器或蓝牙
17.5 一张图看懂USB音频全流程
说了这么多,咱们用一张SVG图把整个流程串起来。从设备插入到声音播放,再到拔掉设备,每一步都清晰可见。
这张图把整个流程分成了四个阶段:设备插入、配置注册、数据传输、热插拔处理。每个阶段都有对应的模块负责,环环相扣。
好了,USB音频这块的核心内容就这些。从驱动到枚举,从流管理到热插拔,每个环节都有它的坑和技巧。下次遇到USB音频问题,你应该知道从哪里下手了。