PyAudio读取USB Audio 2.0流每1024字节微帧后1ms延迟问题排查
背景
我已将自带集成USB控制器的SoC开发板配置为符合USB Audio 2.0规范的单声道8位PCM麦克风,采样频率48KHz,通过单个同步IN端点实现异步音频流传输接口。我将其接入Raspberry Pi 3B+,使用PyAudio读取音频流并存储为WAV文件。
为进行测试,我通过该接口传输存储在SoC RAM中的正弦信号,信号周期为1024个采样点(占用空间1024字节)。
异常现象(bInterval=1,125微秒轮询周期)
当设备端点配置为支持125微秒轮询周期(即bInterval=1)时,录制的WAV文件在每传输1024字节后会出现空白段:
放大后可观察到空白段时长约为1毫秒:
我了解到,支持125微秒轮询周期(bInterval=1)的USB 2.0高速同步端点每微帧可传输1024字节,USB Audio设备每微帧可发送1024字节数据,而USB全速模式下bInterval为1毫秒。目前看起来USB主机控制器仅每1毫秒发送一次同步IN令牌(即轮询),而非125微秒,因此Raspberry Pi上匹配采样频率和设备bInterval配置的音频流接口需要等待后续音频数据到达。
正常现象(bInterval=4,1毫秒轮询周期)
当我重新编程SoC开发板,将其配置为bInterval=4的USB Audio 2.0设备(即支持1毫秒轮询周期)时,传输的正弦信号没有异常,1024个采样点传输完成后无空白/静音段。这符合预期,因为结合PyAudio流配置和端点配置,音频应用(PyAudio流)预期每1毫秒收到1024采样点的帧。
放大效果:
疑问
请问USB控制器无法每125微秒发送IN令牌的原因属于以下哪一种?
- 是PyAudio本身的问题:即它不支持125微秒微帧的IN令牌请求?
- 与操作系统上其他进程导致的延迟有关?
- 是ALSA硬件抽象层的底层问题?
- 是硬件问题?(尽管Raspberry Pi 3B+的端口和控制器符合USB 2.0规范)
是否有方案可实现PyAudio在125微秒轮询间隔下的无间断音频流传输?否则端点1毫秒的轮询间隔会使可达到的吞吐量降低8倍。
*注意:我并不限定使用Raspberry Pi作为USB主机,如果有在其他硬件上使用PyAudio、通过配置为125微秒间隔的USB端点实现音频输入流录制的成功案例,也请告知。
测试代码
import pyaudio import wave form_1 = pyaudio.paUInt8 # 8-bit resolution chans = 1 # 1 channel samp_rate = 48000 # 48kHz sampling rate chunk = 4096 record_secs = 1 # seconds to record dev_index = 2 # device index found by p.get_device_info_by_index(ii) wav_output_filename = 'sinad.wav' # name of .wav file audio = pyaudio.PyAudio() # create pyaudio instantiation # create pyaudio stream stream = audio.open(format = form_1,rate = samp_rate,channels = chans, \ input_device_index = dev_index,input = True, \ frames_per_buffer=chunk) print("recording") frames = [] # loop through stream and append audio chunks to frame array for ii in range(0,int((samp_rate/chunk)*record_secs)): data = stream.read(chunk) frames.append(data) print("finished recording") # stop the stream, close it, and terminate the pyaudio instantiation stream.stop_stream() stream.close() audio.terminate() # save the audio frames as .wav file wavefile = wave.open(wav_output_filename,'wb') wavefile.setnchannels(chans) wavefile.setsampwidth(audio.get_sample_size(form_1)) wavefile.setframerate(samp_rate) wavefile.writeframes(b''.join(frames)) wavefile.close()
问题原因定位
首先可以排除PyAudio本身的问题,PyAudio是基于PortAudio的上层封装,本身不直接控制USB总线的令牌调度,USB令牌的发送完全由操作系统内核的USB主机控制器驱动、ALSA层的USB Audio类驱动负责。
你遇到的问题核心原因优先级从高到低排序:
- 树莓派3B+的USB主机控制器驱动限制:树莓派3B+搭载的博通BCM2837内置DWC2 USB控制器,默认配置为全速优先,高速同步端点的微帧调度默认未开启,官方内核默认的调度粒度为1ms,不会按125us微帧触发同步传输,属于硬件默认配置问题。
- ALSA配置问题:默认ALSA的USB Audio驱动默认的最大传输周期配置为1ms,会自动合并多个微帧的数据包到1ms块返回。
- 代码参数配置不合理:你设置的
frames_per_buffer=4096对应的缓冲时长约为85ms,远大于125us的轮询间隔,也会导致驱动合并数据包。
修复方案
- 先修改树莓派内核启动参数:编辑
/boot/cmdline.txt,在末尾追加dwc2.utmi_hibernate=0 dwc2.dma_enable=1,保存重启,开启DWC2控制器的高优先级DMA调度和微帧支持。 - 修改ALSA配置:编辑
/etc/modprobe.d/alsa-base.conf,添加一行options snd-usb-audio nrpacks=0,保存后重新加载snd-usb-audio模块或者重启系统,关闭ALSA的自动批量打包功能,让驱动按设备实际的bInterval调度。 - 修改代码中的
frames_per_buffer参数为128,对应约2.6ms的缓冲,和125us的轮询间隔匹配。 - 如果需要更高的吞吐量,也可以换用x86平台的USB 2.0以上控制器,主流x86的EHCI/XHCI控制器默认原生支持125us微帧的同步端点调度,不需要额外修改配置就可以正常工作,仅需要调整缓冲块大小就可以实现无空白传输。
内容的提问来源于stack exchange,提问作者Ali Rahman

