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 填入下一帧。
相关推荐
神奇霸王龙7 小时前
国产音乐视频 Prompt 三段式屠夫榜:5 个国产视频模型实测对比
人工智能·ai·prompt·音视频·agent·ai编程·agi
神奇霸王龙7 小时前
AI视频Prompt结构化实战指南:可灵/万相/豆包
人工智能·ai·prompt·aigc·音视频·ai编程
SEONIB_Explorer10 小时前
跨境电商新手零门槛教程:AI 一键生成 TikTok/Ins 投放短视频,5 步 60 秒出片
人工智能·音视频·跨境电商·视频制作·veonib
运营小白13 小时前
电商产品链接自动生成视频广告:从“信息搬运“到“链接进、视频出“的工程化思路
大数据·人工智能·aigc·音视频·veonib·电商视频广告·产品链接解析
AI创界者16 小时前
【AI视频生成】ComfyUI + LTX-Video 2.3 整合包:超低显存实现高帧率AI视频渲染与工作流详解
人工智能·aigc·音视频
开开心心就好19 小时前
Word双击预览图片插件弥补Word功能缺失
人工智能·python·智能手机·ocr·电脑·word·音视频
Yeauty21 小时前
你那条 ffmpeg 命令,一键翻成 Rust builder 代码
开发语言·rust·ffmpeg
tedcloud1231 天前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
潜创微科技1 天前
8K HDMI 2.1 多源切换芯片方案,一颗芯片搞定长线不黑屏
音视频
音视频牛哥2 天前
一款优秀的视频直播软件应具备哪些核心功能?结合 SmartMediaKit 谈实时音视频系统的能力边界
音视频·视频直播·低延迟rtsp播放器·gb28181接入·rtsp服务器·低延迟rtmp播放器·鸿蒙next rtsp