音频欠喂

335 阅读3分钟

音频欠喂(audio underrun / buffer underrun)指的是:

播放端消耗音频数据的速度 > 你提供(写入)数据的速度,导致音频缓冲区被“吃空”。一旦缓冲见底,声卡/音频管线没有可播放的样本,就会出现断续、爆音、卡顿、静音跳变等现象。


为什么会发生(生产者—消费者模型)

  • 消费者:系统播放链路(AudioTrack → AudioFlinger → HAL/DSP)。

  • 生产者:你的解码/合成线程,或播放器把PCM写入的线程。

    当生产者喂不及消费者(或缓冲太小不足以平滑抖动)就会欠喂。反之,缓冲写满而来不及读叫过喂/溢出(overrun)

常见诱因:

  • 线程调度不及时(GC 停顿、主线程/音频线程被阻塞、优先级过低)。
  • 解码或效果处理太重,单帧处理时间 > 写入周期
  • 缓冲设置过小(低延迟配置但没有算好安全余量)。
  • 网络抖动/丢包(流媒体),上游解码迟迟拿不到完整帧。
  • 采样率/通道/格式频繁重配导致短时停顿。
  • 蓝牙链路重传/切编解码器(SBC/AAC/LC3)瞬时卡顿。

在 Android/ExoPlayer 里怎么体现

  • AudioTrack 路径:你周期性 write() PCM 到内部环形缓冲。喂慢了就“欠喂”,日志常见 underrunobtainBuffer timed out 等。
  • ExoPlayer:由 MediaCodecAudioRenderer + DefaultAudioSink 驱动 AudioTrack。欠喂时控制台会有 AudioTrack underrun 警告,并可能听到断续;A/V 同步器会尝试拉齐,但连续欠喂仍会卡。

如何识别

  • 听感:咔哒、爆音、短暂静音、节奏抖。

  • 日志

    • AudioTrack: buffer underrun, AudioFlinger: not enough data 等。
    • ExoPlayer 的 AudioTrack underrun 警告/事件回调。
  • 指标:播放指针(AudioTrack.getPlaybackHeadPosition())在前进,而可用写入空间长时间为满;或 ExoPlayer 的 decoder counters 显示 underrun 次数上升。


解决与缓解思路

通用

  1. 加大缓冲(牺牲一些延迟换稳定):

    • 计算安全时长:bufferTime = bufferFrames / sampleRate。例如 48kHz 下 2400 帧≈50ms。通常总缓冲 100–250ms 比较稳。
  2. 稳定喂数据的节拍:用独立音频线程,避免在该线程里做 I/O、日志、繁重对象分配,减少 GC。

  3. 提升线程优先级:音频写入线程设为较高优先级(但别压死系统)。

  4. 处理前移到后台:重DSP/变速/混音放到工作线程,写入线程只做搬运。

  5. 匹配采样率:尽量与输出设备采样率一致,减少重采样压力/抖动。

  6. 网络流媒体:加大jitter buffer、预缓冲阈值,弱网时降码率/启用重传或 FEC。

Android AudioTrack 自己写 PCM

  • 使用 AudioTrack.getMinBufferSize() 作为起点,适当放大 2–4 倍
  • 优先 WRITE_BLOCKING 写入(阻塞直到有空间),避免自旋忙等:
audioTrack.write(pcm, 0, pcm.size, AudioTrack.WRITE_BLOCKING)
  • 避免在写线程里做磁盘/网络/复杂解码;解码完把整块 PCM 交给写线程。

  • 如需低延迟,使用 LOW_LATENCY 输出但仍保留最小安全余量(别把缓冲压得过小)。

ExoPlayer

  • 增大播放器缓冲:DefaultLoadControl.Builder() 调整

    setBufferDurationsMs(minBufferMs, maxBufferMs, minPlaybackStartMs, minPlaybackResumeMs)。弱网或重解码场景把 minBufferMs / minStartMs 适度提高。

  • Audio offload(硬件卸载)可用时启用,降低CPU占用、减少欠喂概率。

  • 避免把昂贵的业务逻辑(日志/DB/渲染)放在播放回调线程。

  • 监控 underrun 次数,达到阈值时 临时加大缓冲/降码率/暂停-预缓冲-再播


实用小公式(估算安全写入节拍)

  • 每次写入 N 帧,在 48kHz 下对应时间 Δt = N / 48000 秒;
  • 你需要保证连续在不大于 Δt 的周期内完成下一次写入(含解码时间)。
  • 例如每次写 1024 帧:Δt ≈ 21.3ms;若你的解码+搬运偶尔超过 21ms,就会飘红,建议要么减少每次处理成本,要么放大写入块/缓冲

一句话总结

音频欠喂 = 播放端“吃”得比你“喂”得快

治法:增大缓冲、稳定写入节拍、降低线程/解码压力、对网络做预缓冲;在 Android/ExoPlayer 下分别通过 AudioTrack 合理 buffer + 阻塞写LoadControl 提高预缓冲 等方式落地。