文章目录
-
-
- 整体架构:
- [SDL 音频回调函数注册与调用机制](#SDL 音频回调函数注册与调用机制)
-
- [注册时机:audio_open() 函数](#注册时机:audio_open() 函数)
- 注册动作:SDL_OpenAudioDevice()
- 回调调用机制
- 注册流程
- 注册动作:SDL_OpenAudioDevice()
- 回调调用机制
- [缓冲区 audio_buf 设计](#缓冲区 audio_buf 设计)
-
整体架构:
音频输出模型: ffplay 使用 SDL(Simple DirectMedia Layer) 实现音频播放,其音频输出是被动式回调驱动的:
SDL 启动后会定期调用注册的回调函数 sdl_audio_callback;
回调函数负责向 SDL 提供指定长度的 PCM 数据;
ffplay 内部维护一个中间缓冲区 audio_buf,用于桥接解码后的音频帧(AVFrame)与 SDL 所需的数据格式/长度。
关键问题 :
AVFrame 中的音频数据格式(采样率、声道数、样本格式等)通常与 SDL 设备支持的格式不一致;且每次回调所需的数据量固定,而 AVFrame 的大小可变。因此需要:
- 重采样(格式/参数转换)
- 缓冲区管理(
audio_buf)
打开音频设备(audio_open)
在 stream_component_open 中调用 audio_open;
尝试以期望参数(来自解码器上下文 AVCodecContext)打开 SDL 音频设备;
若失败,自动降级尝试其他采样率/声道数组合;
最终将实际打开的设备参数保存到 is->audio_tgt(目标格式);
返回值为硬件缓冲区大小(spec.size),用于后续时钟同步。
注意 :SDL 2.0 不支持 planar 格式(如 AV_SAMPLE_FMT_S16P),只支持 packed(如 AV_SAMPLE_FMT_S16),因此 ffplay 统一使用 AUDIO_S16SYS。
音频回调函数(sdl_audio_callback)
SDL 调用此函数请求 len 字节的音频数据;
函数从 is->audio_buf 中拷贝数据到 stream;
若 audio_buf 已耗尽,则调用 audio_decode_frame 重新填充;
支持音量控制(muted / audio_volume);
更新音频时钟 audclk,考虑:
- 硬件缓冲区延迟(
2 * audio_hw_buf_size) - 当前未写入的缓冲区(
audio_write_buf_size)
时钟更新公式:
c
is->audio_clock - (2 * hw_buf + write_buf) / bytes_per_sec
获取音频帧并重采样(audio_decode_frame)
⚠️ 注意:该函数不执行解码,仅从 sampq(音频 Frame 队列)取帧并处理格式转换。
主要步骤:
-
从队列取帧:跳过
serial不匹配的帧(seek 后丢弃旧帧); -
计算原始数据大小:
av_samples_get_buffer_size; -
判断是否需要重采样:
- 比较
af->frame与is->audio_src(当前源格式); - 若与
is->audio_tgt(目标格式)不一致,则初始化SwrContext(libswresample);
- 比较
-
执行重采样(若需要):
- 使用
swr_convert将数据转为目标格式; - 若启用了音频同步(变速播放),通过
synchronize_audio调整样本数,并调用swr_set_compensation实现"软补偿";
- 使用
-
更新音频时钟:
caudio_clock = pts + nb_samples / sample_rate表示当前帧结束时间。
音频重采样(Resampling)细节
- 使用 FFmpeg 的
libswresample库; - 重采样触发条件:
- 格式不同(sample format)
- 声道布局/数量不同
- 采样率不同
- 样本数被同步逻辑修改(如变速播放)
- 重采样输出缓冲区动态分配(
av_fast_malloc); - 补偿机制用于处理非整数倍变速导致的样本数变化。
关键数据结构
| 变量 | 说明 |
|---|---|
is->audio_tgt |
SDL 设备实际支持的音频参数(目标格式) |
is->audio_src |
当前音频帧的原始参数(源格式),初始等于 audio_tgt |
is->audio_buf |
供 SDL 回调读取的 PCM 缓冲区(已重采样) |
is->audio_buf_size |
缓冲区总字节数 |
is->audio_buf_index |
下次读取位置 |
is->swr_ctx |
重采样上下文(SwrContext) |
总结:
打开音频设备:用 SDL 初始化播放器,自动适配设备支持的格式(如采样率、声道数);
重采样音频数据:当解码出来的音频格式和设备不匹配时,用 libswresample 转换成设备能播的格式;
按需提供音频数据:SDL 会不断回调一个函数,这个函数从缓冲区里取出已重采样的 PCM 数据喂给声卡。
音频不是直接播放原始帧,而是通过 重采样 + 缓冲区管理 来适配硬件;
sdl_audio_callback 是核心回调,驱动整个播放流程;
SwrContext 的动态创建与补偿机制 保证了不同格式、变速播放下的流畅输出。
SDL 音频回调函数注册与调用机制
注册时机:audio_open() 函数
在 ffplay 的 audio_open() 函数中,通过构造 SDL_AudioSpec 结构体注册回调函数:
c
SDL_AudioSpec wanted_spec;
wanted_spec.freq = avctx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = avctx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = sdl_audio_callback; // 注册回调函数
wanted_spec.userdata = is; // 传递播放器状态指针
注册动作:SDL_OpenAudioDevice()
通过 SDL_OpenAudioDevice() 完成实际注册:
c
audio_dev = SDL_OpenAudioDevice(
NULL,
0,
&wanted_spec,
&spec,
SDL_AUDIO_ALLOW_ANY_CHANGE
);
- SDL 内部保存回调函数指针和
userdata - 成功打开设备后启动独立音频线程
回调调用机制
SDL 音频线程伪代码:
c
while (playing) {
Uint8 buffer[spec.size];
sdl_audio_callback(userdata, buffer, spec.size); // 触发回调
send_to_audio_hardware(buffer, spec.size);
}
参数说明:
userdata: 注册时传入的VideoState* isstream: SDL 内部缓冲区指针len: 需填充的字节数
注册流程
text
ffplay 初始化
↓
调用 stream_component_open()
↓
调用 audio_open()
↓
构造 SDL_AudioSpec 设置回调
↓
调用 SDL_OpenAudioDevice()
↓
SDL 保存回调指针
↓
启动音频线程
↓
定期调用 sdl_audio_callback()
↓
填充 PCM 数据至 audio_buf
```### SDL 音频回调函数注册与调用机制
#### 注册时机:audio_open() 函数
在 ffplay 的 `audio_open()` 函数中,通过构造 `SDL_AudioSpec` 结构体注册回调函数:
```c
SDL_AudioSpec wanted_spec;
wanted_spec.freq = avctx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = avctx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = sdl_audio_callback; // 注册回调函数
wanted_spec.userdata = is; // 传递播放器状态指针
注册动作:SDL_OpenAudioDevice()
通过 SDL_OpenAudioDevice() 完成实际注册:
c
audio_dev = SDL_OpenAudioDevice(
NULL,
0,
&wanted_spec,
&spec,
SDL_AUDIO_ALLOW_ANY_CHANGE
);
- SDL 内部保存回调函数指针和
userdata - 成功打开设备后启动独立音频线程
回调调用机制
SDL 音频线程伪代码:
c
while (playing) {
Uint8 buffer[spec.size];
sdl_audio_callback(userdata, buffer, spec.size); // 触发回调
send_to_audio_hardware(buffer, spec.size);
}
参数说明:
userdata: 注册时传入的VideoState* isstream: SDL 内部缓冲区指针len: 需填充的字节数
缓冲区 audio_buf 设计
为什么需要缓冲区?
核心矛盾:生产者和消费者节奏不一致
| 角色 | 行为 | 特点 |
|---|---|---|
| 生产者(解码 + 重采样线程) | 产出 AVFrame → 重采样为 PCM 块 | 每帧大小可变(如 1024、512 样本),处理速度受 CPU 影响 |
| 消费者(SDL 音频回调线程) | 每次请求固定字节数(如 4096 字节) | 节奏严格由硬件决定(如每 23ms 调用一次) |
如果没有缓冲区:
- 回调时若解码未完成 → 卡顿或爆音;
- 一帧太大 → 无法一次性塞进回调的 len;
- 一帧太小 → 一次回调需拼接多帧,逻辑复杂。
缓冲区的作用:
- 解耦:让生产和消费独立运行;
- 平滑:吸收帧大小和回调长度的不匹配;
- 防断流:提前准备好数据,避免回调时"等解码"。
ffplay 使用单块动态缓冲区 + 读写指针实现:
c
typedef struct VideoState {
uint8_t *audio_buf; // 指向 PCM 数据缓冲区(重采样后)
unsigned int audio_buf_size; // 缓冲区总字节数
unsigned int audio_buf_index; // 当前读取位置(消费者指针)
// ...
} VideoAssistant;
分配方式:av_fast_malloc(&is->audio_buf, &is->audio_buf_size, needed_size)
- 避免频繁 malloc/free,提升性能;
- 自动扩容(当重采样输出变大时)。
生命周期:整个播放过程复用同一块缓冲区。
数据流向:从哪里放入?从哪里取出?
放入缓冲区(生产)
- 时机:在
sdl_audio_callback中发现audio_buf_index >= audio_buf_size(即缓冲区耗尽); - 调用:
audio_decode_frame(is); - 动作:
- 从音频帧队列
sampq取出一个 AVFrame; - 若格式不匹配,用
swr_convert重采样; - 将结果写入
is->audio_buf; - 设置
is->audio_buf_size = 输出字节数; - 重置
is->audio_buf_index = 0。
- 从音频帧队列
放入的是:已重采样、格式统一(S16)、声道数适配后的 PCM 原始数据。
从缓冲区取出(消费)
-
时机:每次
sdl_audio_callback(void *userdata, Uint8 *stream, int len)被 SDL 调用; -
动作:
cmemcpy(stream, is->audio_buf + is->audio_buf_index, copy_len); is->audio_buf_index += copy_len; -
目标:填满 SDL 要求的
len字节(可能跨多次audio_buf填充)。
取出的是:原始 PCM 字节流(S16 格式),直接送给声卡播放。
假设:
- 解码帧:FLTP, 48kHz, 2ch, 1024 samples → 重采样后仍为 S16, 48kHz, 2ch
audio_buf被填入:1024 × 2 × 2 = 4096 字节(S16 立体声)- SDL 回调每次要 2048 字节
则:
- 第一次回调:取 0~2047 字节;
- 第二次回调:取 2048~4095 字节;
- 第三次回调:发现
audio_buf_index == 4096,于是调用audio_decode_frame填入下一帧。