修改UAC配置参数后无法正常通信

如果修改了UAC的配置后出现UAC无法正常通信的问题,可以通过修改设备的idVendor和idProduct来解决,只有设备的idVendor和idProduct发生改变Windows才会重新读取设备的配置描述符。

Kernel 5.4内核配置某些参数后UAC通信异常

例如在44100采样率、4channel、32bit参数下Device到Host端通信异常,而Host端到Device端通信正常。 问题的原因是由于Linux的UAC驱动计算的每次传输的usb包大小不正确,导致USB传输格式不匹配出现了通信异常。详细原因如下:

  • 该参数下每秒钟的数据量为44100 * 4 * 4 = 705600
  • USB每秒传输一千个包,则每个包的大小为 705600 / 1000 = 705.6
  • Linux下UAC驱动的做法是每次传输705字节数据,当余数足够多的时候传输705+16=721字节,然后依次循环下去。但是这种方式存在一个问题就是705不是frames大小的整数倍,即705 / (4 * 4) = 44.0625,那么USB每次传输的数据不能完全送到声卡,要等下一次的数据到来后拼成一个完整的frame才能送到声卡,这样显然是非常麻烦的,且会造成更大的延迟
  • Windows的做法是每次传输704字节,然后剩余的数据积攒到一帧的数据量后下次传输704 + (4 * 4)= 720字节
  • 可以看到Linux和Windows每次传输和接收的大小都不一样,导致了两则无法正常通信,显然Linux的计算方法是错误的,Linux驱动中也没有自己拼接数据的逻辑,这个就是Linux下计算usb包大小的一个Bug。

Linux上游社区已经发现并解决了这个Bug,最终计算到的大小与Windows的大小相同,计算方法如下:

pkt_size=frame_sizesampling_rate1000

即如下代码:

1
pkt_size = frame_size * (sampling_rate / 1000) // 704 = 16 * (44100 / 1000) = 16 * 44

UAC1 无法支持48K、4channel、32bit问题

drivers/usb/gadget/function/u_uac1.h:

1
#define UAC1_OUT_EP_MAX_PACKET_SIZE	200

这里闲置了usb每个包的大小不能超过200字节,我们可以计算一下数据量:

对于48K、4channel、32bit每秒钟的数据量为:48000_4_4 = 768,000Byte

USB每秒发送1000次数据,每次最大3个数据包,则每个包大小需要768000 / 1000 / 3 = 256。这里的宏定义将每个包的大小限制在200了,则带宽不够,需要放开这个限制,将宏大小改为1023就可以了。

💡 驱动中为什么默认设置为200呢,其实是为了保证usb低速设备可以正常使用才设置这么低的,对于usb低速设备1.5Mbit/s = 1.5 * 1024 *1024 / 8 / 1000 = 196.如果usb支持全速或者高速可以将这个限制放开。

UAC2无法支持大于192K、4channel、32bit参数问题

drivers/usb/gadget/function/f_uac2.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static struct usb_endpoint_descriptor hs_epout_desc = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bmAttributes = USB_ENDPOINT_XFER_ISOC | USB_ENDPOINT_SYNC_ADAPTIVE,
/* .wMaxPacketSize = DYNAMIC */
.bInterval = 4,
};
...
static struct usb_endpoint_descriptor hs_epin_desc = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bmAttributes = USB_ENDPOINT_XFER_ISOC | USB_ENDPOINT_SYNC_SYNC,
/* .wMaxPacketSize = DYNAMIC */
.bInterval = 4,
};

这里bInterval是控制UAC发包频率的,USB全速总线1ms发送一次数据,USB高速总线可以125us发送一次数据,这里的bInterval就是用来控制UAC多久发送一次数据的,如果bInterval=1,则UAC数据包的间隔就是125us211如果bInterval=4则UAC发送间隔为125us241=1ms。驱动中默认是1ms发送一次数据,则最大的数据量就是1000 * 1024 * 3 = 3072000 = 192K * 4channel * 4Byte。

要解决这个问题只需要将bInterval改为1即可。

💡 bInterval=1具有更低的延迟,更大的带宽,但是对于大部分场景没必要设置这么高的频率,1ms就可以满足要求。

各系统UAC音量调节支持情况

Windows10/11 Android 11(基于一加6) ios Macos
UAC1 Disable FU 通过调节音频数据幅值调节音量 通过调节音频数据幅值调节音量 不支持调节音量(音量滑块变灰不可拖动) 同ios(未做验证)
UAC1 Enable FU 支持控制音量;不支持同步设备音量 通过调节音频数据幅值调节音量 支持控制音量;支持同步设备音量 同ios(未做验证)
UAC2 Disable FU 通过调节音频数据幅值调节音量 通过调节音频数据幅值调节音量 不支持调节音量(音量滑块变灰不可拖动) 同ios(未做验证)
UAC2 Enable FU 支持控制音量;支持同步设备音量 通过调节音频数据幅值调节音量 支持控制音量;支持同步设备音量 同ios(未做验证)

💡 1. 网上查到的信息Android是支持Feature Unit的,实测发现插入android设备的时候有读取feature unit描述符,并且获取和设置了一次音量值,当device端修改了音量值,这个信息也有被android读走,但是android的音频组件应该没有使用这一套机制,请教了android那边的人,他们说android调节uac音量都是直接调节音频文件幅值实现的。实测也是这样,android上调节音量,device录到的文件音频幅值就是被修改了的。android也不能同步device的音量。
2. windows系统 UAC1 Enable FU后,是不支持中断端点的,也就是windows可以通过FU控制设备音量,但是设备主动修改音量无法同步到Host。
3. Macos UAC1 Enable FU后,Device只能主动同步一次音量到Host,之后中断端点的数据不会被读取。

关于windows系统UAC1对FU支持情况的疑问

Windows 的UAC2对Function的支持情况有明确说明:

UAC2的Feature Unit是明确支持Interrupt的。但是没有查到UAC1对Function的支持情况。实际测试发现Windows是可以正确识别Feature unit端点描述符的,如下:

这里面的信息与Spec里都能一一对应上,端点描述符没有任何问题,但是测试发现Windows不会去读取Interrupt端点数据,抓包发现没有任何该端点的读取操作,那么实测得到的UAC1对Feature支持情况是:

  • 支持:
    • GET CUR
    • SET CUR
    • GET RANGE
  • 不支持:
    • Interrupt

supperpowered latency test 测uac latency有跳变

latency.png

是由于arecord | aplay之间的的管道缓存了一些数据导致的。可通过如下手段来验证:

不停的重启test app当测得的latency很大时,按照上图步骤操作,将pipe里的数据清空后再次点击test,latency就会恢复到最初的42ms附近。

latency的主要影响因素:

  1. codec是否有开启dapm,当开启dapm后codec可能会进入休眠状态再唤醒会引入一定的延迟,大小取决于唤醒速度
  2. usb端的延迟非常小可以认为是us级别的
  3. 线程调度引入的latency,这个一般再几十us到几个ms不等,取决于系统性能和负载
  4. 主要latency取决于buffer缓存,hw buffer以及应用层开的buffer或者pipe管道缓存

这是在公版上通过如下命令测得的数据

arecord -D hw:2,0 -c 2 -f S16_LE -r 48000 --period-size=64 --buffer-size=512 |aplay -D hw:2,0 -f S16_LE -r 48000 --period-size=64 --buffer-size=512

buffer size=512,对应的缓存latency为10ms,加上手机端的缓存64frames(1.4ms)以及整个链路上其他的缓存19ms这个值基本上是符合实际情况的。