文章目录
-
-
- [采样率(Sample Rate)](#采样率(Sample Rate))
- [每帧样本数(Samples per Frame)](#每帧样本数(Samples per Frame))
- [总字节数的计算(重采样后,S16 格式)](#总字节数的计算(重采样后,S16 格式))
- [补充:为什么 SDL 回调请求 2048 字节?](#补充:为什么 SDL 回调请求 2048 字节?)
- [解码输出的 AVFrame](#解码输出的 AVFrame)
- [SDL 设备实际支持格式(is->audio_tgt)](#SDL 设备实际支持格式(is->audio_tgt))
- 重采样后数据大小计算
- 缓冲区变量
- [第一次 SDL 回调发生](#第一次 SDL 回调发生)
- [第二次 SDL 回调发生(约 21.3ms 后)](#第二次 SDL 回调发生(约 21.3ms 后))
- [第三次 SDL 回调发生](#第三次 SDL 回调发生)
- 循环往复
- 补充说明:边界情况处理
- 总结:数据流与状态变迁
-
缓冲区 audio_buf 调试过程
采样率(Sample Rate)
定义:每秒钟对音频信号进行多少次采样。
单位:Hz(赫兹)
例子:48,000 Hz 表示每秒采集 48,000 个样本(每个声道)。
作用:决定音频的频率响应范围(根据奈奎斯特采样定理,最高可还原频率为采样率的一半,即 24 kHz)。
注意:采样率是时间维度上的密度,和"一帧有多少样本"无关。
每帧样本数(Samples per Frame)
定义:一次处理/传输的音频块中包含多少个采样点(每个声道)。
例子:1024 样本/帧 表示这一帧音频中,左声道有 1024 个 float 值,右声道也有 1024 个 float 值(因为是立体声)。
作用:影响延迟和处理效率。帧越大,延迟越高但 CPU 开销可能更低。
关键点:这是"批量处理"的单位,和采样率无直接关系,但结合采样率可以算出一帧持续多长时间:
帧时长= 1024 samples 48000 samples/sec ≈ 21.33 ms \frac{1024 \text{ samples}}{48000 \text{ samples/sec}} ≈ 21.33 \text{ ms} 48000 samples/sec1024 samples≈21.33 ms
总字节数的计算(重采样后,S16 格式)
重采样后的格式是 AV_SAMPLE_FMT_S16(packed signed 16-bit integer),立体声(2 声道),每帧仍是 1024 个样本(每声道)。
每个样本占多少字节?
S16:16 位 = 2 字节。
每帧总样本数是多少?
立体声:2 声道 × 1024 样本/声道 = 2048 个样本(注意:这里是"样本总数",不是"每声道样本数")。
总字节数 = 样本总数 × 每样本字节数
2048 samples × 2 bytes/sample = 4096 bytes
所以 4096 字节是正确的。
补充:为什么 SDL 回调请求 2048 字节?
SDL 音频回调函数每次被调用时,会要求提供 len 字节的 PCM 数据(这里是 2048 字节)。
2048 字节(S16, stereo)对应多少样本?
2048 bytes 2 bytes/sample × 2 channels = 512 samples per channel \frac{2048 \text{ bytes}}{2 \text{ bytes/sample} × 2 \text{ channels}} = 512 \text{ samples per channel} 2 bytes/sample×2 channels2048 bytes=512 samples per channel
一帧 1024 样本(每声道)可满足两次 SDL 回调(因为 1024 ÷ 512 = 2)。
解码输出的 AVFrame
格式:AV_SAMPLE_FMT_FLTP(planar float)
采样率:48,000 Hz
声道数:2(立体声)
每帧样本数:1024(每个声道 1024 个 float 样本)
SDL 设备实际支持格式(is->audio_tgt)
格式:AV_SAMPLE_FMT_S16(packed signed 16-bit int)
采样率:48,000 Hz
声道数:2
无需改变采样率或声道,但需格式转换(FLTP → S16)
重采样后数据大小计算
每样本 2 字节(S16)
总字节数 = 1024 samples × 2 channels × 2 bytes = 4096 bytes
SDL 回调每次请求:len = 2048 bytes(即每次要 2048 字节 PCM 数据)
缓冲区变量
is->audio_buf_size
含义:当前 audio_buf 缓冲区中有效 PCM 数据的总字节数。
单位:字节(bytes)
何时设置:
每次调用 audio_decode_frame() 成功后;
该函数完成解码 + 重采样,并将结果写入 is->audio_buf;
然后将实际写入的字节数赋值给 is->audio_buf_size。
📌 举例:
若重采样输出 1024 个 stereo S16 样本,则:
audio_buf_size = 1024 × 2(声道) × 2(字节/S16) = 4096
is->audio_buf_index
含义:下一次从 audio_buf 中读取数据的起始位置(偏移量)。
单位:字节(bytes)
初始值:每次填入新数据后被重置为 0
变化方式:
在 sdl_audio_callback 中,每拷贝一段数据到 SDL,就增加相应的字节数;
当 audio_buf_index >= audio_buf_size 时,表示当前缓冲区已读完,需要加载下一帧。
全局状态变量初始值(在播放开始前)
c
is->audio_buf = NULL; // 尚未分配
is->audio_buf_size = 0;
is->audio_buf_index = 0;
第一次 SDL 回调发生
SDL 调用回调
c
sdl_audio_callback(is, stream, 2048);
检查缓冲区是否可用
c
if (is->audio_buf_index >= is->audio_buf_size) // 0 >= 0 → true
缓冲区为空,需要填充新数据。
调用 audio_decode_frame(is)
从音频帧队列 sampq 中取出一帧(FLTP, 48kHz, 2ch, 1024 samples)
检查格式:frame.format != is->audio_src.fmt(因为 audio_src 初始等于 audio_tgt,即 S16) → 需要重采样
创建 SwrContext(FLTP → S16,其他参数相同)
调用 swr_convert(...),输出 4096 字节 S16 PCM 数据
分配/扩容 is->audio_buf 至至少 4096 字节
将重采样结果写入 is->audio_buf
设置:
c
is->audio_buf_size = 4096;
is->audio_buf_index = 0; // 重置读指针
从 audio_buf 拷贝数据到 SDL 的 stream
c
len1 = min(4096 - 0, 2048) = 2048
memcpy(stream, is->audio_buf + 0, 2048);
更新状态
c
is->audio_buf_index = 0 + 2048 = 2048;
// len -= 2048 → 0,退出 while 循环
第一次回调结束
已播放前 2048 字节(即前 512 个 stereo 样本)
audio_buf 还剩 2048 字节未读
第二次 SDL 回调发生(约 21.3ms 后)
48kHz 立体声下,2048 字节 = 2048 / (2×2) = 512 samples → 播放时长 = 512 / 48000 ≈ 10.7ms
两次回调间隔约 10~20ms,取决于 SDL 缓冲设置
SDL 调用回调
c
sdl_audio_callback(is, stream, 2048);
检查缓冲区
c
is->audio_buf_index = 2048
is->audio_buf_size = 4096
2048 < 4096 → 缓冲区还有数据!跳过 audio_decode_frame
直接拷贝剩余数据
c
len1 = min(4096 - 2048, 2048) = 2048
memcpy(stream, is->audio_buf + 2048, 2048);
更新状态
c
is->audio_buf_index = 2048 + 2048 = 4096;
第二次回调结束
播放完剩下的 2048 字节(后 512 个 stereo 样本)
整个音频帧(1024 samples)已全部送出
audio_buf_index == audio_buf_size → 下次回调将触发新帧加载
第三次 SDL 回调发生
SDL 调用回调
c
sdl_audio_callback(is, stream, 2048);
检查缓冲区
c
is->audio_buf_index (4096) >= is->audio_buf_size (4096) → true
缓冲区耗尽,需要新数据。
再次调用 audio_decode_frame(is)
从队列取下一帧(假设同样是 FLTP, 48kHz, 2ch, 1024 samples)
此时 is->audio_src 已更新为上一帧的格式(FLTP...),而新帧格式相同 → 无需重建 SwrContext(复用已有上下文)
swr_convert 输出新的 4096 字节 S16 数据
覆盖写入 is->audio_buf(或原地重用内存)
重置:
c
is->audio_buf_size = 4096;
is->audio_buf_index = 0;
拷贝前 2048 字节
c
memcpy(stream, is->audio_buf + 0, 2048);
is->audio_buf_index = 2048;
第三次回调结束
开始播放第二帧的前半部分
流程回到与第一次回调相同的状态
循环往复
只要播放不停止,这个"填满 → 分两次读完 → 再填满"的过程就会持续下去。
补充说明:边界情况处理
情况 1:一帧小于回调需求(如只有 1000 字节)
第一次回调:拷贝全部 1000 字节;
仍缺 1048 字节 → 继续循环,再次调用 audio_decode_frame 取下一帧;
从新帧中拷贝剩余 1048 字节;
→ 一次回调可能消耗多个音频帧
情况 2:队列为空(如刚 seek 或 EOF)
audio_decode_frame 返回 < 0;
ffplay 填充静音(silence_buf,全零);
避免回调无数据导致爆音或崩溃。
总结:数据流与状态变迁
| 回调次数 | 触发动作 | audio_buf_index 变化 | 数据来源 |
|---|---|---|---|
| 1 | 首次填充帧 → 重采样 | 0 → 2048 | 第1帧(重采样后) |
| 2 | 直接读剩余 | 2048 → 4096 | 第1帧(剩余) |
| 3 | 填充新帧 → 重采样(复用上下文) | 0 → 2048 | 第2帧(重采样后) |
| 4 | 读剩余 | 2048 → 4096 | 第2帧(剩余) |