嵌入式设备ALSA LIB音频播放:snd_pcm_writei用法与问题排查
snd_pcm_writei() 正确用法及Underrun问题解决方案
问题背景
开发基于kernel-5.10的嵌入式音频设备,采用ALSA-LIB实现音频播放功能,对snd_pcm_writei()的输入参数设置、返回值处理逻辑存在困惑,同时遇到快速启停播放时的underrun问题。
初始代码与疑问
初始实现按系统获取的periodsize分块发送音频数据:
/* Get periodsize from system */ snd_pcm_hw_params_get_period_size(hw_params, &periodsize, NULL); left = frames; sent = 0; while (left > 0) { sent = (left > periodsize) ? periodsize : left; rc = snd_pcm_writei(pcm_handle, buf, sent); if (rc < 0) { if (rc == -EAGAIN) { usleep(1000); } rc = snd_pcm_recover(pcm_handle, rc, 0); if (rc < 0) { snd_pcm_prepare(pcm_handle); } } else if (rc == 0) { /* How to handle this case ?*/ } else { /* Samples less than were sent to audio-card */ left -= rc; buf += rc * frame_size; } }
测试中frames为820,periodsize为400,疑问:
- 按
periodsize分块发送是否正确? - 是否可以直接将820帧数据传入
snd_pcm_writei()播放?snd_pcm_writei(pcm_handle, buf, frames);
更新后的测试代码与现象
更新后的测试代码:
#ifdef TEST_APLAY frame_size = chan * 2; buf = databuf; left = data_frames; sent = 0; while (left > 0) { sent = (left > periodsize) ? periodsize : left; rc = snd_pcm_writei(hah->pcm_handle, buf, sent); if (rc == -EAGAIN || (rc >= 0 && (size_t)rc < sent)) { snd_pcm_wait(hah->pcm_handle, 10); } else if (rc == -EPIPE) { snd_pcm_recover(hah->pcm_handle, rc, 0); //// snd_pcm_prepare(hah->pcm_handle); } else if (rc < 0) { break; } if (rc > 0) { left -= rc; buf += rc * frame_size; } } #endif
在Ubuntu-20.04 X86_64虚拟机(AC97声卡)上快速启停播放10次,出现大量underrun:
Playing/stoping quickly for times, pid: 3168 Got end of media, breaking Got end of media, breaking ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred Got end of media, breaking ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred Got end of media, breaking ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred Got end of media, breaking
解决方案与正确用法
1. 参数设置与分块逻辑
- 直接传入820帧是允许的,但分块按
periodsize处理更稳妥:- 阻塞模式下,
snd_pcm_writei()会阻塞直到所有请求的帧被写入,但系统负载高时可能返回部分帧数;非阻塞模式下更易出现部分写入的情况。 - 分块处理能更精细控制缓冲区状态,避免一次性写入过多导致的阻塞或异常,同时便于处理部分写入的场景。
- 阻塞模式下,
- 无论分块与否,核心是必须根据返回的实际写入帧数(rc>0时)更新剩余帧数和缓冲区指针,不能假设请求的帧数全部被写入。
2. 返回值的正确处理
- rc == -EAGAIN:非阻塞模式下缓冲区无可用空间,应调用
snd_pcm_wait(pcm_handle, timeout)等待缓冲区就绪,而非固定时长的usleep,snd_pcm_wait会精准等待到有空间或超时。 - rc == -EPIPE:发生underrun(播放端缓冲区空),调用
snd_pcm_recover()即可,该函数会自动重置PCM状态至可播放状态,无需额外调用snd_pcm_prepare(),除非recover返回错误。 - rc == 0:极少出现,代表未写入任何帧,通常是PCM状态异常,可尝试
snd_pcm_wait后重试,或检查PCM设备状态。 - rc > 0:实际成功写入的帧数,必须用此值更新
left和buf的偏移(left -= rc; buf += rc * frame_size;),继续循环处理剩余数据。
3. Underrun问题解决措施
快速启停导致underrun的核心原因是PCM设备状态切换不及时、缓冲区数据残留或供应不足,可通过以下方式解决:
- 停止播放时正确收尾:调用
snd_pcm_drain(pcm_handle)等待所有已写入的数据播放完成,再进行停止或重置操作,避免设备处于异常状态。 - 启动前确保设备就绪:每次启动播放前,调用
snd_pcm_prepare(pcm_handle)重置PCM设备状态,确保缓冲区处于初始可用状态。 - 调整缓冲区参数:通过
hw_params增大buffer_size或periodsize,提升缓冲区的容错能力,减少快速启停时的断流风险。 - 复用PCM句柄:避免频繁打开/关闭PCM设备,尽量复用已配置好的句柄,减少状态切换开销。
- 选择合适的工作模式:如果是简单的播放场景,优先使用阻塞模式,无需手动处理等待逻辑,降低underrun概率。
内容的提问来源于stack exchange,提问作者wangt13
相关产品推荐
相关产品推荐

