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。我一般建议在调试时先用默认值,然后根据实际场景微调。

核心要点: USB音频流的延迟 = 包大小 / 采样率 × 缓冲区数量。举个例子,如果包大小是192字节,采样率48kHz,缓冲区数量是2,那么延迟就是 192 / 48000 × 2 ≈ 8ms。这个值对大多数场景来说是可以接受的。

17.4 热插拔处理:插拔之间,系统在做什么?

USB音频设备最大的特点就是「随时可能被拔掉」。你想想看,用户正听着歌呢,突然把USB耳机拔了,系统该怎么办?

AudioFlinger的热插拔处理分为三个层面:

  1. 内核层:USB控制器检测到设备断开,触发 disconnect 事件,驱动清理资源。
  2. HAL层:通过 adev->callback 通知AudioFlinger「设备没了」。
  3. AudioFlinger层:停止当前音频流,切换到默认输出设备(比如扬声器)。

我曾经踩过一个坑:某款手机在拔掉USB耳机后,系统没有自动切换到扬声器。排查发现是HAL层的回调没有正确实现。具体来说,audio_hw_deviceinit_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这边,收到断开通知后会做这些事情:

  • 关闭对应的 AudioStreamOutAudioStreamIn
  • 从设备列表中移除该USB设备
  • 触发音频策略管理器重新选择输出设备
  • 如果当前有音频正在播放,自动切换到扬声器或蓝牙
注意: 热插拔处理中最容易出问题的是竞态条件。比如用户快速插拔两次,或者拔掉的同时还有音频数据在传输。我建议在代码里加一个「去抖」机制,比如收到断开事件后延迟100ms再执行清理,避免误触发。

17.5 一张图看懂USB音频全流程

说了这么多,咱们用一张SVG图把整个流程串起来。从设备插入到声音播放,再到拔掉设备,每一步都清晰可见。

USB音频设备完整工作流程 1. 设备插入 2. 内核枚举 3. UAC驱动加载 4. HAL层配置 5. AudioFlinger注册 6. 音频流创建 7. 音频数据传输 8. 等时传输 9. USB设备播放 10. 设备拔出 11. 断开通知 12. 切换到默认设备 蓝色箭头:正常流程 | 红色箭头:热插拔流程

这张图把整个流程分成了四个阶段:设备插入、配置注册、数据传输、热插拔处理。每个阶段都有对应的模块负责,环环相扣。

好了,USB音频这块的核心内容就这些。从驱动到枚举,从流管理到热插拔,每个环节都有它的坑和技巧。下次遇到USB音频问题,你应该知道从哪里下手了。


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