八-1 讲了所有播放源把 AudioFrame 数组累加进 bus 的机制,但没讲这些数组从哪来。答案是 AudioStreamPlayback——每个发声的 stream 都有一个 playback 对象,它的唯一职责是回答一个问题:"从播放位置开始给我 p_frame_count 个采样,现在就要。"这篇讲这个契约怎么实现,以及流式播放(OGG/MP3)为什么不会在播放中途卡壳。
mix 契约:拉模式,不缓存,不预取
AudioServer 不主动管播放进度。每次 mix step(八-1,默认 512 帧 ≈ 10.7ms @ 48kHz)遍历到某个 playback 时,调它的 mix(AudioFrame *buffer, float rate, int frames),playback 就往 buffer 里写帧。写完这次调用的,下次调用就从新位置继续——状态全在 playback 对象里(当前帧偏移、循环点、音高重采样相位)。
这个设计的关键是"拉":消费端(混音器)按设备节奏要数据,生产端(解码器)被动响应。两者速度天然不同步——解码器解码一帧 OGG 可能只要 0.1ms,而消费端每 10.7ms 才来要 512 帧。如果每次 mix 调用都现解码,要么解码开销抖动直接变成爆音,要么 decode 延迟卡 deadline。环形缓冲就是插在两者之间的速度匹配器。
环形缓冲的结构与回填阈值
流式 playback(AudioStreamPlaybackOggVorbis 是代表)内部维护两块 buffer:一块是已解码的 AudioFrame 环形缓冲(默认能装几秒采样),一块是压缩数据的预读缓冲。运行状态只有两个指针:
write_pos ── 解码线程写到这里 read_pos ── mix() 从这里读
可用量 = (write_pos - read_pos + N) mod N N = 缓冲总帧数
mix 被调时从 read_pos 顺序拷出请求的数量,拷完 read_pos 前进。当可用量低于总容量的 1/3(约 1 秒,对应 stream_buffering 时间量级)时,playback 标记自己需要回填,解码在下一帧空闲时补满。选 1/3 是个权衡:太低(比如 1/10)遇到解码突发延迟(系统调度把解码线程抢走几十毫秒)就直接见底爆音;太高则解码线程频繁醒来做无用功,移动端的功耗看着难受。
解码本身在独立的解码线程(4.x 里 OGG/MP3 流式解码跑在 ResourceLoader 系的线程池上)跑,和音频线程不共用时间线——这正是"流式播放为什么不会卡"的答案:解码提前一秒以上完成,音频线程永远从现成的缓冲里取,两者通过环形缓冲解耦,各自的抖动被缓冲吸收。
顺序流与随机访问流
不是所有 stream 都走环形缓冲。两类实现走了不同的路:
全量加载流(AudioStreamWAV 的默认模式、小 OGG 的 load())把解码后的 PCM 整个放在内存里,mix 就是一块连续内存的逐帧拷贝,没有缓冲管理、没有解码线程、seek 是 O(1) 的指针跳转。代价是内存:3 分钟未压缩立体声 48kHz float32 是 $180 \times 48000 \times 2 \times 4 \approx 69\ \mathrm{MB}$——一首歌没问题,一百首 BGM 这样放内存就炸了。
流式流(AudioStreamOggVorbis、AudioStreamMP3 默认)就是上面讲的环形缓冲方案,内存占用恒定(几秒缓冲 + 预读窗口),代价是 seek 慢——OGG 的 seek 要先定位到前一个 keyframe 再顺序解码到目标位置,预填缓冲也要整个重灌,所以交互式音乐系统(八-11)里频繁跳轨会感到半秒左右的响应迟滞。
样本库流(AudioStreamInteractive、AudioStreamPlaylist,4.3+ 的新类型)介于两者之间:多个 clip 共享一个底层流,切 clip 不需要重新打开文件,但 clip 边界还是要等缓冲对齐。这是为分层音乐设计的(八-11 会展开)。
一个容易踩的坑:buffer 大小与内存的换算
调 stream_buffering(毫秒,默认 5000?不,实际默认是 500ms 量级)时,注意内存占用随采样率线性涨:
$$ \text{缓冲内存} = t_{\text{buffer}} \times f_s \times 2 \text{(声道)} \times 4 \text{(float)} $$
1 秒缓冲 @ 48kHz 立体声 ≈ 384KB,每路流一份。移动端同时开 8 路 BGM + 环境流,光缓冲就 3MB——不算大,但如果你的项目有几十路常驻流式源(比如每个 NPC 挂一路环境音),这个数要算进内存预算。另外 4.x 的 stream_buffering 调整只对新创建的 playback 生效,运行中改无效。
本文基于 Godot 4.2–4.4,源码 modules/vorbis/audio_stream_ogg_vorbis.cpp、scene/audio/audio_stream_player.cpp、servers/audio_server.cpp;环形缓冲阈值与线程模型以主线实现为准。