FFmepg-- 38-ffplay源码-缓冲区 audio_buf调试

文章目录

      • [采样率(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帧(剩余)
相关推荐
会思考的猴子2 小时前
UE5 C++ 笔记 GameplayAbilitySystem人物角色
c++·笔记·ue5
ht巷子2 小时前
Qt:信号与槽
开发语言·c++·qt
千里马-horse2 小时前
Checker Tool
c++·node.js·napi
北辰水墨2 小时前
【算法篇】单调栈的学习
c++·笔记·学习·算法·单调栈
lxmyzzs2 小时前
【硬核部署】在 RK3588上部署毫秒级音频分类算法
人工智能·分类·音视频
惆怅客1232 小时前
在 vscode 中断点调试 ROS2 C++ 的办法
c++·vscode·调试·ros 2
眠りたいです2 小时前
Docker:镜像的运行实体-Docker Container
java·运维·c++·docker·容器·eureka
ComputerInBook2 小时前
C++ 标准提供的 thread (线程)之 join() 函数示例(windows平台)
c++·线程·join函数
快乐的划水a2 小时前
嵌入式时间测量方法总结
c++·stm32·单片机