非AI生成,觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
目录
3. 采样格式转换:从 float 到 int16 的那些坑
4.3 FFmpeg 6 的 AVChannelLayout:表达能力大升级
6. swr_convert() 为什么返回的不是"一进一出"
如果你第一次接触 FFmpeg 的音频部分,十有八九是被 swresample(或者老一点的 libswresample)这个词劝退的。文档里写着"音频重采样",但到底"重"了个啥?是把 44.1kHz 变成 48kHz 就叫重采样,还是里面另有乾坤?
今天咱们把这层窗户纸捅破。内容从 1 开始,一层一层剥。
1. 先说结论:重采样 = 三个维度的对齐
音频重采样干的事情,本质上是在三个维度上把"源音频的格式"对齐到"目标环境要求的格式":
| 维度 | 是什么 | 典型场景 |
|---|---|---|
| 采样率 | 每秒多少个采样点 | 44.1kHz CD → 48kHz 声卡 |
| 采样格式 | 每个采样点用多少位、什么类型存储 | float planar → int16 交错 |
| 声道布局 | 几个声道、怎么排布 | 5.1 → 立体声(下混) |
任何一个维度对不上,下游(SDL/ALSA/编码器/硬件)就可能罢工。重采样就是那个万能适配器。
类比:就像视频的 swscale 把 YUV420P 1920×1080 变成 RGB24 1280×720,音频重采样是"在时间轴 + 数值精度 + 空间布局"三个方向上同时做变换。
2. 采样率变换:不是删点也不是复制,是"重新算一遍"
2.1 为什么不能简单地丢点或补点
假设你有个 44.1kHz 的音频,想变成 48kHz:
-
天真想法 A:每隔 44.1 个点删一个 → 会引入周期性失真(一听就是"机器声")
-
天真想法 B:每个点复制一份 → 音调变高,速度变快
真实的声音是连续的模拟信号,采样率是你在时间轴上"拍照"的频率。从一种拍照频率变到另一种,需要做数字信号处理意义上的重采样:
原始采样点 → 插值成连续信号 → 按新频率重新采样 → 新采样点
2.2 FFmpeg 怎么做:插值 + 低通滤波
FFmpeg 的重采样核心是一个多相滤波器(polyphase FIR filter):
-
上采样(Upsample):在原始采样点之间插入零值,把采样率提到 LCM(44100, 48000) = 7056000 Hz(理论上)
-
低通滤波:滤掉插入零值引入的高频镜像,防止混叠(aliasing)
-
下采样(Downsample):每隔若干点取一个,降到目标采样率
实际上 FFmpeg 用多相分解技巧,把这三步合并成一步,效率极高。你选的算法(SWS_BILINEAR / SWS_SINC / SWS_LANCZOS 等)决定了 FIR 滤波器的长度和形状:
// swr 初始化时指定的 quality
av_opt_set_int(swr, "quality", 5, 0); // 0=poor..10=best
| Quality | 实际含义 | 适用场景 |
|---|---|---|
| 0–2 | 最近邻/线性插值,速度快,音质差 | 实时监控、语音对讲 |
| 3–5 | 中等长度 FIR,平衡 | 普通播放器(默认) |
| 6–8 | 长 FIR,音质好,CPU 高 | 音乐播放、后期制作 |
| 9–10 | 极长 FIR,接近无损 | 母带处理、离线转码 |
2.3 延迟是不可避免的
重采样滤波器是有"宽度"的,意味着它需要未来的采样点才能算出当前输出。这引入了固有延迟(通常几十个采样点):
输入: [x₀] [x₁] [x₂] [x₃] ...
输出: [y₀] [y₁] ...
↑ 这里才有第一个有效输出
这就是为什么 swr_convert()在 flush 阶段还会吐出一些采样点——滤波器尾巴里的能量必须刷出来,否则结尾会少一截声音。
3. 采样格式转换:从 float 到 int16 的那些坑
3.1 为什么 AAC 解码器爱吐 FLTP
AAC 解码器默认输出 AV_SAMPLE_FMT_FLTP(float planar):
-
每个采样点是 32-bit float,范围
[-1.0, +1.0] -
每个声道一个独立的 buffer(planar)
-
精度高、溢出不可能、DSP 算法友好
但你的声卡大概率要吃 S16(16-bit signed integer,交错):
-
每个采样点 16-bit,范围
[-32768, 32767] -
左右声道交错存放:
L R L R L R...
3.2 转换过程发生了什么
float -0.5 → (-0.5) × 32767 = -16383.5 → 取整 → -16384
float +0.999 → 0.999 × 32767 = 32764.2 → 取整 → 32764
float +1.0 → 1.0 × 32767 = 32767(削顶!)
问题来了:float 理论上可以到 ±1.0,但 int16 的 +32767 对应 +0.99997,所以 FFmpeg 内部会做一点点headroom 预留,防止削顶失真。同时:
-
舍入方式(rounding):就近取整 / 向零取整 / 随机抖动(dither)
-
溢出钳位(clamp):确保不会超出目标格式的范围
3.3 Dither:你可能没听过但一直在享受的技术
从高位深往低位深转(比如 float 32-bit → int16),量化误差会变成可听见的高频失真。FFmpeg 可以选择开启 dither(抖动):
原始信号 + 低幅度随机噪声 → 量化 → 噪声能量分散到全频段(听感更自然)
av_opt_set_int(swr, "dither_method", SWR_DITHER_TRIANGULAR, 0);
大多数播放器不关心这个(因为 float→int16 的动态范围已经够大),但做音乐制作的人会非常在意。
4. 声道重映射:从 5.1 到立体声不是"丢掉后面四个"
4.1 下混(Downmix)的数学
5.1 声道布局是:FL FR FC LFE BL BR(前左、前右、中置、低音、后左、后右)
变成立体声时,FFmpeg 做的事情是加权混合:
L' = FL×gain₁ + FC×gain₂ + BL×gain₃ + LFE×gain₄
R' = FR×gain₁ + FC×gain₂ + BR×gain₃ + LFE×gain₄
增益系数不是随便定的,遵循 ITU-R BS.775 或 Dolby 规范:
-
中置声道贡献约 -3dB(因为离两只耳朵距离差不多)
-
环绕声道贡献约 -6dB(因为来自侧面/后方)
-
LFE(超低音)通常不参与下混(立体声喇叭再现不了 120Hz 以下的能量,留着反而会让小喇叭过载)
4.2 上混(Upmix)和缺失声道的猜测
从立体声变 5.1 更 tricky,因为信息不够。常见策略:
-
左右声道直接复制到 FL/FR
-
中置 =
(L + R) / 2再衰减 -
环绕 = 用 HRTF 或相位差算法从立体声中"提取"环境声
-
LFE = 低通滤波后的单声道
FFmpeg 的 pan滤镜可以精细控制这个过程,但 swresample 内置的下混/上混已经能满足大多数播放器需求。
4.3 FFmpeg 6 的 AVChannelLayout:表达能力大升级
以前声道布局是个 64-bit mask(AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT ...),最多描述十几个声道。FFmpeg 6 的 AVChannelLayout结构体可以描述:
-
Ambisonics(全景声)
-
22.2(NHK 超级多声道)
-
任意自定义布局
AVChannelLayout layout;
av_channel_layout_from_string(&layout, "5.1");
// 或
av_channel_layout_from_string(&layout, "hexagonal"); // 6声道六边形
重采样时会自动做正确的重映射,你不用手写混合矩阵。
5. 三个维度同时变换:重采样的完整流水线
当你同时改变采样率、格式、声道数时,FFmpeg 内部大致是这样安排的:
输入 AVFrame (FLTP, 48kHz, 5.1)
│
▼
┌─────────────────────────────────────┐
│ ① 声道重映射(5.1 → stereo) │ ← 先降维,减少后续计算量
│ ② 采样率变换(48k → 44.1k) │ ← 计算量最大的步骤
│ ③ 格式转换(FLTP → S16) │ ← 最后做,float 运算精度高
└─────────────────────────────────────┘
│
▼
输出 buffer (S16, 44.1kHz, stereo)
为什么要按这个顺序? 因为:
-
先降声道数 → 后面每个步骤处理的数据量更小
-
采样率变换在 float 域做 → 精度最高,失真最小
-
格式转换放最后 → 输出就是下游要的格式
6. swr_convert() 为什么返回的不是"一进一出"
这是新手最困惑的点:
int out_samples = swr_convert(swr,
&out_buf, out_nb_samples,
(const uint8_t**)in_buf, in_nb_samples);
你送进去 1024 个采样点,出来的不一定是 1024 个。原因:
| 情况 | 输出采样数 | 原因 |
|---|---|---|
| 升采样(44.1k→48k) | 多于输入 | 时间轴拉长,采样点变多 |
| 降采样(48k→44.1k) | 少于输入 | 时间轴压缩,采样点变少 |
| 滤波器延迟期 | 0 或很少 | 还没攒够历史数据 |
| Flush 阶段 | 少量 | 滤波器尾巴的能量 |
所以正确的用法是循环抽,直到 swr_convert()返回 0:
// 正常处理
while (have_input) {
int consumed = swr_convert(swr, &out, out_nb, &in, in_nb);
// consumed = 实际消耗的输入采样数
// 返回值 = 实际输出的采样数
}
// flush
while (swr_convert(swr, &out, out_nb, NULL, 0) > 0) {
// 刷干净
}
7. 延迟补偿:音画同步的隐形杀手
重采样引入的延迟,如果不补偿,会导致声音比画面慢几毫秒到几十毫秒。
FFmpeg 提供了查询延迟的 API:
int64_t delay = swr_get_delay(swr, 48000); // 以 48kHz 为时间基的延迟采样数
这个 delay的含义是:当前输入的第 N 个采样点,要再过 delay个输出采样后才会出现在输出端。
音画同步时,你应该把这个延迟算进音频时钟:
音频时钟 = 当前播放位置 + 重采样延迟(秒)
否则你的视频会"领先"音频一点点,长时间播放后越来越明显。
8. 常见翻车现场速查
| 现象 | 根因 | 解法 |
|---|---|---|
| 声音像"电话音",闷 | 采样率没变对,或低通太狠 | 检查 in/out 采样率,提高 quality |
| 只有左声道有声 | 声道布局没设对,或 planar/interleaved 搞混 | 确认 AVChannelLayout,确认 data[] 指针个数 |
| 爆音/咔嚓声 | 格式转换溢出(float→int 削顶) | 降低输入增益,或确认没超范围 |
| 开头几毫秒没声音 | 重采样滤波器延迟,没 flush | 结尾记得 swr_convert(swr, ..., NULL, 0) |
| 结尾被截断 | flush 没做完整 | 循环 flush 直到返回 0 |
| 音量忽大忽小 | 5.1→stereo 下混增益没归一化 | 用 swresample 内置下混,别手写权重 |
| 锁屏恢复后音频破音 | swr 上下文没重建,或设备采样率变了 | 设备丢失时 free swr,重建时重新 alloc |
9. 性能热点在哪里
重采样是音频流水线里计算量第二大的环节(仅次于解码)。热点通常在这几个地方:
-
FIR 滤波器的卷积运算(采样率变换的核心)
-
格式转换的乘加和钳位(每个采样点都要过)
-
声道混合的矩阵乘法(多声道下混)
优化方向:
-
降低 quality(播放器用 3–5 足够)
-
避免不必要的重采样(如果源和目标格式一致,直接 bypass)
-
批量处理(攒够一帧再送,减少函数调用开销)
-
硬件加速(部分平台支持音频 DSP,但 FFmpeg 主要靠 CPU)
10. 一句话总结 + 脑内流程图
重采样 = 把音频从"源的物理形态"翻译成"目的地能听懂的语言"。 采样率是时间轴的密度,采样格式是每个点的精度,声道布局是空间的分布。三者任意一个不匹配,就需要 swresample 来做这个翻译工作。
AVFrame (FLTP, 48kHz, 5.1)
│
▼
┌──────────────────────────────┐
│ SwrContext │
│ 声道重映射(矩阵混合) │
│ 采样率变换(FIR 插值) │
│ 格式转换(float→int + dither)│
└──────────────────────────────┘
│
▼
PCM Buffer (S16, 44.1kHz, stereo)
│
▼
SDL / ALSA / 编码器
3006

被折叠的 条评论
为什么被折叠?



