You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌入式设备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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:24:56