FFmepg-- 37-ffplay源码- 播放器中音频输出模块

文章目录

整体架构:

音频输出模型: 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->frameis->audio_src(当前源格式);
    • 若与 is->audio_tgt(目标格式)不一致,则初始化 SwrContext(libswresample);
  • 执行重采样(若需要):

    • 使用 swr_convert 将数据转为目标格式;
    • 若启用了音频同步(变速播放),通过 synchronize_audio 调整样本数,并调用 swr_set_compensation 实现"软补偿";
  • 更新音频时钟:

    c 复制代码
    audio_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* is
  • stream: 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* is
  • stream: 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 调用;

  • 动作:

    c 复制代码
    memcpy(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 填入下一帧。
相关推荐
EasyCVR2 小时前
编辑器分发RTSP地址接入到视频汇聚平台EasyCVR离线原因排查
编辑器·音视频
kkk_皮蛋2 小时前
WebRTC 视频编码基础 (VP8/VP9/H.264/AV1)
音视频·webrtc·vp8
科技小E3 小时前
EasyGBS助力平安乡村搭建无线视频联网监控系统
音视频
程序猿小郑3 小时前
Quill 编辑器自定义视频模块:将 iframe 替换为 video 标签
编辑器·音视频
线束线缆组件品替网3 小时前
TE Linx RF 物联网射频模块的 RF 线缆连接设计思路
数码相机·物联网·测试工具·电脑·音视频·pcb工艺
EasyCVR3 小时前
视频融合平台EasyCVR赋能旅游景区构建全场景可视化监控新体系
音视频·旅游
EasyGBS3 小时前
EasyGBS扩展市场:视频监控系统的“应用商店”,拖入安装、即装即用!
音视频
八月的雨季 最後的冰吻3 小时前
FFmepg-- 38-ffplay源码-缓冲区 audio_buf调试
c++·ffmpeg·音视频
lxmyzzs4 小时前
【硬核部署】在 RK3588上部署毫秒级音频分类算法
人工智能·分类·音视频