FFmpeg 核心 API 系列:音频重采样 SwrContext 完全指南(新API版本)
📅 更新时间:2025年10月18日
🏷️ 标签:FFmpeg | 多媒体处理 | 音视频编程 | C/C++ | 音频处理 | 重采样
文章目录
- [📖 前言](#📖 前言)
- [🎯 为什么需要音频重采样?](#🎯 为什么需要音频重采样?)
-
- [1. 音频的三大属性](#1. 音频的三大属性)
-
- [📊 采样率(Sample Rate)](#📊 采样率(Sample Rate))
- [📊 采样格式(Sample Format)](#📊 采样格式(Sample Format))
- [📊 声道数(Channels)](#📊 声道数(Channels))
- [2. Planar vs Packed(重要概念!)](#2. Planar vs Packed(重要概念!))
- [3. 音频转换对比表](#3. 音频转换对比表)
- [🔧 核心 API 详解(新版)](#🔧 核心 API 详解(新版))
-
- [API 1️⃣:`swr_alloc` - 创建音频转换器](#API 1️⃣:
swr_alloc
- 创建音频转换器) - [API 2️⃣:`av_opt_set_int` - 设置整数选项](#API 2️⃣:
av_opt_set_int
- 设置整数选项) - [API 3️⃣:`av_opt_set_sample_fmt` - 设置采样格式](#API 3️⃣:
av_opt_set_sample_fmt
- 设置采样格式) - [API 4️⃣:`av_opt_set_chlayout` - 设置声道布局](#API 4️⃣:
av_opt_set_chlayout
- 设置声道布局) - [API 5️⃣:`swr_init` - 初始化转换器](#API 5️⃣:
swr_init
- 初始化转换器) - [API 6️⃣:`av_rescale_rnd` - 计算输出采样数](#API 6️⃣:
av_rescale_rnd
- 计算输出采样数) - [API 7️⃣:`av_samples_alloc_array_and_samples` - 分配音频缓冲区](#API 7️⃣:
av_samples_alloc_array_and_samples
- 分配音频缓冲区) - [API 8️⃣:`swr_convert` - 执行音频转换](#API 8️⃣:
swr_convert
- 执行音频转换) - [API 9️⃣:`av_samples_get_buffer_size` - 计算数据字节数](#API 9️⃣:
av_samples_get_buffer_size
- 计算数据字节数) - [API 🔟:`swr_free` - 释放转换器](#API 🔟:
swr_free
- 释放转换器)
- [API 1️⃣:`swr_alloc` - 创建音频转换器](#API 1️⃣:
- [🔄 完整转换流程](#🔄 完整转换流程)
- [💻 完整示例代码](#💻 完整示例代码)
- [⚠️ 常见错误与注意事项](#⚠️ 常见错误与注意事项)
-
- 错误1:采样格式从流参数获取
- [错误2:忘记调用 swr_init()](#错误2:忘记调用 swr_init())
- 错误3:输出缓冲区太小
- [错误4:释放了 linesize](#错误4:释放了 linesize)
- 错误5:混淆采样数和字节数
- [🎓 验证结果](#🎓 验证结果)
-
- [方法1:使用 FFplay 播放](#方法1:使用 FFplay 播放)
- [📋 总结](#📋 总结)
📖 前言
回顾前四个阶段,我们已经能够:
- 阶段一 :打开文件 → 得到
AVFormatContext
和AVStream
- 阶段二 :查找解码器 → 配置并打开
AVCodecContext
- 阶段三 :读取并解码 → 得到音视频
AVFrame
- 阶段四 :视频格式转换 → YUV 转 RGB(使用
SwsContext
)
现在我们已经能处理视频了,但音频呢?解码出来的音频帧格式通常不能直接播放!就像视频需要从YUV转RGB一样,音频也需要格式转换。
本阶段的核心任务:
解码后的音频帧(AVFrame)→ 格式转换(SwrContext)→ PCM数据 → 播放或保存
这就是音频播放的关键一步!
🎯 为什么需要音频重采样?
1. 音频的三大属性
在理解重采样之前,先搞清楚音频的三大核心属性:
📊 采样率(Sample Rate)
定义:每秒采样的次数
常见值:
44100 Hz
- CD音质标准48000 Hz
- 专业音频标准16000 Hz
- 语音通话8000 Hz
- 电话质量
类比:就像视频的帧率,采样率越高,音质越好
📊 采样格式(Sample Format)
定义:每个采样点的数据类型和存储方式
常见格式:
格式 | 数据类型 | 每采样字节数 | 存储方式 | 说明 |
---|---|---|---|---|
AV_SAMPLE_FMT_S16 |
16位整数 | 2 | Packed | 交错存储 |
AV_SAMPLE_FMT_S16P |
16位整数 | 2 | Planar | 平面存储 |
AV_SAMPLE_FMT_FLT |
32位浮点 | 4 | Packed | 交错存储 |
AV_SAMPLE_FMT_FLTP |
32位浮点 | 4 | Planar | 最常见 |
📊 声道数(Channels)
定义:音频的声道数量
常见配置:
1
- 单声道(Mono)2
- 立体声(Stereo)6
- 5.1环绕声
2. Planar vs Packed(重要概念!)
这是音频处理中最容易混淆的概念:
Packed(交错格式)
左右声道数据交错存储在一起:
[L1][R1][L2][R2][L3][R3]...
存储位置:frame->data[0](只用一个平面)
Planar(平面格式)
左右声道数据分开存储:
左声道:[L1][L2][L3][L4]... → frame->data[0]
右声道:[R1][R2][R3][R4]... → frame->data[1]
存储位置:每个声道一个平面
为什么需要转换?
FFmpeg解码输出 → 通常是 FLTP(浮点数Planar)
↓
需要转换
↓
音频设备播放 → 需要 S16(整数Packed)
不转换的后果:
- 直接播放会有噪音或无声
- 声道错乱
- 播放速度异常
3. 音频转换对比表
转换类型 | 示例 | 用途 |
---|---|---|
采样率转换 | 48000Hz → 44100Hz | 适配播放设备 |
格式转换 | FLTP → S16 | 从浮点转整数 |
声道转换 | 立体声 → 单声道 | 节省带宽 |
存储方式转换 | Planar → Packed | 适配播放API |
🔧 核心 API 详解(新版)
API 1️⃣:swr_alloc
- 创建音频转换器
函数原型
cpp
struct SwrContext* swr_alloc(void);
参数说明
- 无参数
返回值
- 成功:返回
SwrContext*
指针 - 失败:返回
NULL
作用
创建一个空的音频转换器对象,类似于视频转换中的 SwsContext
。
基本用法
cpp
SwrContext* swr_ctx = swr_alloc();
if(!swr_ctx) {
qDebug() << "创建SwrContext失败";
return -1;
}
qDebug() << "创建SwrContext成功";
关键要点
- 只是创建空对象 :还需要用后续API设置参数!!!
- 不能直接使用 :必须调用
swr_init()
初始化后才能用 - 只需创建一次:可以重复用于多帧转换
API 2️⃣:av_opt_set_int
- 设置整数选项
函数原型
cpp
int av_opt_set_int(void *obj, const char *name, int64_t val, int search_flags);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
obj |
要设置的对象 | swr_ctx |
name |
选项名称 | "in_sample_rate" / "out_sample_rate" |
val |
要设置的值 | 采样率(如 44100 ) |
search_flags |
搜索标志 | 0 (默认) |
返回值
- 成功:
>= 0
- 失败:
< 0
(负数错误码)
作用
设置整数类型的音频参数,主要用于设置采样率!!!
基本用法
cpp
// 设置输入采样率(从解码器获取)
av_opt_set_int(swr_ctx, "in_sample_rate", audio_ctx->sample_rate, 0);
// 设置输出采样率(目标值)
av_opt_set_int(swr_ctx, "out_sample_rate", 44100, 0);
关键要点
- 输入采样率来源 :从
audio_ctx->sample_rate
获取 - 输出采样率选择 :常用
44100
或48000
- 顺序无关:先设置输入还是输出都可以
API 3️⃣:av_opt_set_sample_fmt
- 设置采样格式
函数原型
cpp
int av_opt_set_sample_fmt(void *obj, const char *name,
enum AVSampleFormat fmt, int search_flags);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
obj |
要设置的对象 | swr_ctx |
name |
选项名称 | "in_sample_fmt" / "out_sample_fmt" |
fmt |
采样格式 | AV_SAMPLE_FMT_S16 等 |
search_flags |
搜索标志 | 0 |
返回值
- 成功:
>= 0
- 失败:
< 0
作用
设置音频的采样格式(整数/浮点、Planar/Packed)。
基本用法
cpp
// 设置输入格式(从解码器获取)
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", audio_ctx->sample_fmt, 0);
// 设置输出格式(S16是最常用的播放格式)
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
常用采样格式表
格式常量 | 说明 | 适用场景 |
---|---|---|
AV_SAMPLE_FMT_S16 |
16位整数Packed | 播放器输出 |
AV_SAMPLE_FMT_S16P |
16位整数Planar | 音频处理 |
AV_SAMPLE_FMT_FLTP |
32位浮点Planar | 解码器常见输出 |
AV_SAMPLE_FMT_FLT |
32位浮点Packed | 音频处理 |
⚠️ 重要注意事项
采样格式必须从解码器上下文获取,不能从流参数获取!
cpp
// ❌ 错误:从流参数获取
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt",
(AVSampleFormat)audio_stream->codecpar->format, 0); // 可能不准确
// ✅ 正确:从解码器上下文获取
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt",
audio_ctx->sample_fmt, 0); // 准确的格式
原因:
audio_stream->codecpar->format
是文件中存储的格式audio_ctx->sample_fmt
是解码器实际输出的格式- 解码器可能会转换格式!
API 4️⃣:av_opt_set_chlayout
- 设置声道布局
函数原型
cpp
int av_opt_set_chlayout(void *obj, const char *name,
const AVChannelLayout *layout, int search_flags);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
obj |
要设置的对象 | swr_ctx |
name |
选项名称 | "in_chlayout" / "out_chlayout" |
layout |
声道布局指针 | &audio_ctx->ch_layout |
search_flags |
搜索标志 | 0 |
返回值
- 成功:
>= 0
- 失败:
< 0
作用
设置音频的声道布局(单声道、立体声、环绕声等)。
基本用法
cpp
// 设置输入声道布局(从解码器获取)
av_opt_set_chlayout(swr_ctx, "in_chlayout", &audio_ctx->ch_layout, 0);
// 设置输出声道布局(立体声)
AVChannelLayout out_layout = AV_CHANNEL_LAYOUT_STEREO;
av_opt_set_chlayout(swr_ctx, "out_chlayout", &out_layout, 0);
// 或者单声道
AVChannelLayout out_layout = AV_CHANNEL_LAYOUT_MONO;
av_opt_set_chlayout(swr_ctx, "out_chlayout", &out_layout, 0);
常用声道布局
常量 | 声道数 | 说明 |
---|---|---|
AV_CHANNEL_LAYOUT_MONO |
1 | 单声道 |
AV_CHANNEL_LAYOUT_STEREO |
2 | 立体声(最常用) |
AV_CHANNEL_LAYOUT_5POINT1 |
6 | 5.1环绕声 |
如何获取声道数
cpp
// 从声道布局获取声道数
int channels = audio_ctx->ch_layout.nb_channels;
qDebug() << "声道数:" << channels;
API 5️⃣:swr_init
- 初始化转换器
函数原型
cpp
int swr_init(struct SwrContext *s);
参数说明
参数 | 说明 |
---|---|
s |
SwrContext指针 |
返回值
- 成功:
0
- 失败:
< 0
(负数错误码)
作用
初始化SwrContext,让之前设置的参数生效。
基本用法
cpp
int result = swr_init(swr_ctx);
if(result < 0) {
qDebug() << "SwrContext初始化失败";
return -1;
}
qDebug() << "SwrContext初始化成功";
⚠️ 关键要点
- 必须调用 :不调用无法使用转换器!!!!
- 调用时机:所有参数设置完成后
- 只需调用一次:初始化后可重复使用
- 相当于"确认配置":就像按下"应用"按钮
错误示例:
cpp
SwrContext* swr_ctx = swr_alloc();
av_opt_set_int(swr_ctx, "in_sample_rate", 48000, 0);
// 忘记调用 swr_init()
swr_convert(swr_ctx, ...); // ❌ 会失败或崩溃
API 6️⃣:av_rescale_rnd
- 计算输出采样数
函数原型
cpp
int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd);
参数说明
参数 | 说明 | 含义 |
---|---|---|
a |
输入值 | 输入采样数 |
b |
乘数 | 输出采样率 |
c |
除数 | 输入采样率 |
rnd |
取整方式 | AV_ROUND_UP |
返回值
- 计算结果:
(a * b) / c
作用
计算采样率变化后的输出采样数。
为什么需要这个函数?
原因:采样率变化时,采样数也要相应变化!
示例:
输入:1024个采样,48000Hz
输出:?个采样,44100Hz
计算:1024 * 44100 / 48000 = 941个采样
基本用法
cpp
// 计算输出采样数
int out_samples = av_rescale_rnd(
frame->nb_samples, // 输入采样数
44100, // 输出采样率
audio_ctx->sample_rate, // 输入采样率
AV_ROUND_UP // 向上取整
);
qDebug() << "输入" << frame->nb_samples << "采样";
qDebug() << "输出" << out_samples << "采样";
取整方式
常量 | 说明 | 适用场景 |
---|---|---|
AV_ROUND_UP |
向上取整 | 分配缓冲区(推荐) |
AV_ROUND_DOWN |
向下取整 | 精确计算 |
AV_ROUND_ZERO |
向零取整 | 一般不用 |
为什么用 AV_ROUND_UP
?
- 分配缓冲区时,宁可多分配一点
- 避免缓冲区不够导致崩溃!!!
API 7️⃣:av_samples_alloc_array_and_samples
- 分配音频缓冲区
函数原型
cpp
int av_samples_alloc_array_and_samples(
uint8_t ***audio_data, // 输出:缓冲区指针数组
int *linesize, // 输出:每个平面的大小
int nb_channels, // 声道数
int nb_samples, // 采样数
enum AVSampleFormat sample_fmt, // 采样格式
int align // 对齐字节数
);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
audio_data |
指向指针数组的指针(三级指针) | &out_data |
linesize |
输出每个平面的字节大小 | &out_linesize |
nb_channels |
声道数 | 1 (单声道)或 2 (立体声) |
nb_samples |
采样数 | av_rescale_rnd() 计算的值 |
sample_fmt |
采样格式 | AV_SAMPLE_FMT_S16 |
align |
内存对齐 | 0 (默认) |
返回值
- 成功:返回分配的总字节数
- 失败:
< 0
(负数错误码)
作用
一次性分配音频数据缓冲区,自动处理Planar/Packed格式。
基本用法
cpp
uint8_t** out_data = nullptr;
int out_linesize = 0;
int ret = av_samples_alloc_array_and_samples(
&out_data, // 注意是 &out_data
&out_linesize,
2, // 2声道(立体声)
out_samples, // 采样数
AV_SAMPLE_FMT_S16, // S16格式
0 // 默认对齐
);
if(ret < 0) {
qDebug() << "分配音频缓冲区失败";
return -1;
}
qDebug() << "分配了" << ret << "字节缓冲区";
这个函数做了什么?
1. 分配 audio_data 数组(指针数组)
2. 分配实际的音频数据内存
3. 自动计算需要的内存大小
4. 处理Planar/Packed格式的差异
Packed格式(如S16):
- 只有 out_data[0] 有数据
- 所有声道交错存储在一起
Planar格式(如S16P):
- out_data[0] 存第一声道
- out_data[1] 存第二声道
⚠️ 内存释放注意
cpp
// 使用完后必须释放
if(out_data) {
av_freep(&out_data[0]); // 释放数据
av_freep(&out_data); // 释放指针数组
}
API 8️⃣:swr_convert
- 执行音频转换
函数原型
cpp
int swr_convert(
struct SwrContext *s, // 转换器上下文
uint8_t **out, // 输出缓冲区
int out_count, // 输出缓冲区能容纳的采样数
const uint8_t **in, // 输入数据
int in_count // 输入采样数
);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
s |
SwrContext指针 | swr_ctx |
out |
输出缓冲区 | out_data |
out_count |
输出缓冲区容量(采样数) | out_samples |
in |
输入数据 | (const uint8_t**)frame->data |
in_count |
输入采样数 | frame->nb_samples |
返回值
- 成功:返回实际输出的采样数
- 失败:
< 0
(负数错误码)
作用
执行音频格式转换,是整个重采样的核心函数。
基本用法
cpp
int converted_samples = swr_convert(
swr_ctx, // 转换器
out_data, // 输出到这里
out_samples, // 输出容量
(const uint8_t**)frame->data, // 输入数据
frame->nb_samples // 输入采样数
);
if(converted_samples < 0) {
qDebug() << "音频转换失败";
} else {
qDebug() << "转换了" << converted_samples << "个采样";
}
关键要点
- 返回值是采样数:不是字节数!
- 输出采样数可能不同:因为采样率可能变化
- 可以多次调用:同一个转换器可以重复使用
- 输入数据来自AVFrame :
frame->data
和frame->nb_samples
转换过程示意
输入AVFrame:
- 48000Hz
- FLTP格式(浮点Planar)
- 1024个采样
- 2声道
↓
swr_convert
↓
输出PCM数据:
- 44100Hz
- S16格式(整数Packed)
- 941个采样
- 2声道
API 9️⃣:av_samples_get_buffer_size
- 计算数据字节数
函数原型
cpp
int av_samples_get_buffer_size(
int *linesize, // 输出:每行大小(可填NULL)
int nb_channels, // 声道数
int nb_samples, // 采样数
enum AVSampleFormat sample_fmt, // 采样格式
int align // 对齐
);
参数说明
参数 | 说明 | 常用值 |
---|---|---|
linesize |
输出每行字节数 | nullptr (不需要) |
nb_channels |
声道数 | 2 |
nb_samples |
采样数 | converted_samples |
sample_fmt |
采样格式 | AV_SAMPLE_FMT_S16 |
align |
对齐 | 1 (不对齐) |
返回值
- 成功:返回总字节数
- 失败:
< 0
作用
计算音频数据占用的字节数,用于写入文件或播放。
基本用法
cpp
// 计算转换后的数据大小(字节)
int data_size = av_samples_get_buffer_size(
nullptr, // 不需要linesize
2, // 2声道
converted_samples, // 转换后的采样数
AV_SAMPLE_FMT_S16, // S16格式
1 // 不对齐
);
qDebug() << "数据大小:" << data_size << "字节";
// 写入文件
fwrite(out_data[0], 1, data_size, pcm_file);
计算公式
S16格式(2字节/采样):
总字节数 = 声道数 × 采样数 × 2
示例:
cpp
// 2声道,1024采样,S16格式
data_size = 2 × 1024 × 2 = 4096字节
快速计算方法
如果你知道格式,也可以手动计算:
cpp
// 方法1:使用API(推荐,自动处理对齐)
int size = av_samples_get_buffer_size(nullptr, 2, 1024, AV_SAMPLE_FMT_S16, 1);
// 方法2:手动计算(简单场景)
int size = 2 * 1024 * 2; // 声道数 × 采样数 × 字节/采样
推荐使用API,因为它会自动处理:
- 不同格式的字节数
- 内存对齐
- Planar/Packed差异
API 🔟:swr_free
- 释放转换器
函数原型
cpp
void swr_free(struct SwrContext **s);
参数说明
参数 | 说明 |
---|---|
s |
SwrContext指针的指针(二级指针) |
返回值
- 无返回值
作用
释放SwrContext及其内部资源。
基本用法
cpp
swr_free(&swr_ctx);
// 之后 swr_ctx 会变成 NULL
关键要点
- 传入二级指针 :
&swr_ctx
,不是swr_ctx
- 自动置NULL:释放后指针会被设为NULL
- 调用时机:程序结束前调用
- 必须调用:避免内存泄漏
🔄 完整转换流程
标准流程图
┌─────────────────────┐
│ 打开文件和解码器 │ ← 阶段一、二
└──────────┬──────────┘
↓
┌─────────────────────┐
│ swr_alloc() │ ← 创建转换器
└──────────┬──────────┘
↓
┌─────────────────────┐
│ av_opt_set_int() │ ← 设置采样率
│ av_opt_set_sample_fmt│ ← 设置采样格式
│ av_opt_set_chlayout()│ ← 设置声道布局
└──────────┬──────────┘
↓
┌─────────────────────┐
│ swr_init() │ ← 初始化(必须!)
└──────────┬──────────┘
↓
┌────────┐
┌──│ 循环 │──┐
│ └────────┘ │
│ ↓ │
│ ┌─────────────────────┐
│ │ av_read_frame() │ ← 读取packet
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │ avcodec_send_packet │ ← 发送给解码器
│ │avcodec_receive_frame│ ← 接收frame
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │ av_rescale_rnd() │ ← 计算输出采样数
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │av_samples_alloc_... │ ← 分配输出缓冲区
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │ swr_convert() │ ← 执行转换
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │av_samples_get_buffer│ ← 计算字节数
│ │ fwrite() │ ← 写入文件
│ └──────────┬──────────┘
│ ↓
│ ┌─────────────────────┐
│ │ av_freep() │ ← 释放缓冲区
│ │ av_frame_unref() │ ← 释放frame引用
│ └──────────┬──────────┘
│ ↓
└────────────┘
↓
┌─────────────────────┐
│ swr_free() │ ← 释放转换器
│ 释放其他资源 │
└─────────────────────┘
💻 完整示例代码
目标:打开视频文件,解码音频流,转换为PCM格式并保存
cpp
#include "mainwindow.h"
#include<QDebug>
#include <QApplication>
#include<stdio.h>
extern "C"{
#include<libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswresample/swresample.h>
#include <libavutil/samplefmt.h>
#include <libavutil/channel_layout.h>
}
int result=0;
int main()
{
QString path="D:/桌面/视频录制/搞笑生气猫_爱给网_aigei_com.mp4";
// ========== 阶段一:打开文件 ==========
AVFormatContext* avfmctx=nullptr;
result=avformat_open_input(&avfmctx,path.toUtf8().data(),nullptr,nullptr);
if(result<0)
{
qDebug()<<"avformat_open_input is error";
return 0;
}
qDebug()<<"avformat_open_input is success";
// ========== 阶段二:找音频流 ==========
result=av_find_best_stream(avfmctx,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
if(result<0)
{
qDebug()<<"av_find_best_stream is error";
return 0;
}
qDebug()<<"av_find_best_stream is success";
int Audio_index=result;
AVStream * Audio_stream=avfmctx->streams[Audio_index];
qDebug()<<"this Audio_stream index is "<<Audio_index;
// ========== 阶段三:打开解码器 ==========
const AVCodec* decodec=avcodec_find_decoder(Audio_stream->codecpar->codec_id);
if(!decodec)
{
qDebug()<<"decodec is not find";
return 0;
}
qDebug()<<"decodec is find, name is "<<decodec->name;
// 给解码器分配上下文
AVCodecContext* avcodec_ctx=avcodec_alloc_context3(decodec);
result=avcodec_parameters_to_context(avcodec_ctx,Audio_stream->codecpar);
if(result<0)
{
qDebug()<<"avcodec_parameters_to_context is error";
return 0;
}
qDebug()<<"avcodec_parameters_to_context is success";
// 打开解码器
result=avcodec_open2(avcodec_ctx,decodec,nullptr);
if(result<0)
{
qDebug()<<"avcodec_open2 is error";
return 0;
}
qDebug()<<"avcodec_open2 is success";
// ========== 打印音频信息 ==========
qDebug()<<"音频流信息---------";
qDebug()<<"采样率:"<<Audio_stream->codecpar->sample_rate;
qDebug()<<"声道数:"<<Audio_stream->codecpar->ch_layout.nb_channels;
// ⚠️ 注意:采样格式要从解码器上下文获取,不是从流参数!
qDebug()<<"采样格式:"<<av_get_sample_fmt_name(avcodec_ctx->sample_fmt);
// ========== 阶段五:创建SwrContext(新API)==========
SwrContext* swr_ctx=swr_alloc();
if(!swr_ctx)
{
qDebug()<<"创建一个空SwrContext失败";
return 0;
}
qDebug()<<"创建一个空SwrContext成功";
// 采样率设置
av_opt_set_int(swr_ctx, "in_sample_rate", Audio_stream->codecpar->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", 44100, 0); // 输出:44.1kHz
// 采样格式设置
// ⚠️ 重要:必须从解码器上下文获取,不是从流参数!
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", avcodec_ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); // 输出:16位整数
// 声道布局设置
AVChannelLayout out_layout = AV_CHANNEL_LAYOUT_MONO; // 输出:单声道
av_opt_set_chlayout(swr_ctx, "in_chlayout", &Audio_stream->codecpar->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &out_layout, 0);
// 初始化(必须调用!)
result=swr_init(swr_ctx);
if(result<0)
{
qDebug()<<"SwrContext初始化失败";
return 0;
}
qDebug()<<"SwrContext初始化成功";
// ========== 打开PCM文件 ==========
FILE* pcm_file = fopen("E:/output.pcm", "wb");
if(!pcm_file) {
qDebug() << "无法创建 PCM 文件";
return 0;
}
qDebug() << "PCM 文件创建成功: E:/output.pcm";
// ========== 读取、解码、转换 ==========
AVPacket* packet=av_packet_alloc();
AVFrame* frame=av_frame_alloc();
int total_frame=0; // 只读200帧
while(av_read_frame(avfmctx,packet)==0 && total_frame<200)
{
// 只找音频流
if(packet->stream_index!=Audio_index)
{
av_packet_unref(packet);
continue;
}
// 将packet发给解码器
if(avcodec_send_packet(avcodec_ctx,packet)==0)
{
// 从解码器读取frame
while(avcodec_receive_frame(avcodec_ctx,frame)==0 && total_frame<200)
{
total_frame++;
// 计算输出采样数(因为采样率可能变化)
int out_samples=av_rescale_rnd(
frame->nb_samples,
44100,
Audio_stream->codecpar->sample_rate,
AV_ROUND_UP
);
uint8_t **out_data = nullptr; // 输出数据指针
int out_linesize = 0; // 每个平面的大小
// 为音频数据分配内存缓冲区
int ret = av_samples_alloc_array_and_samples(
&out_data, // 音频数据指针(二级指针的地址)
&out_linesize, // 每个平面大小的地址
1, // 声道数(单声道)
out_samples, // 采样数
AV_SAMPLE_FMT_S16, // 采样格式(S16)
0 // 对齐(0表示默认对齐)
);
if(ret < 0) {
qDebug() << "分配音频缓冲区失败";
av_frame_unref(frame);
continue;
}
// 使用 swr_convert 进行音频转换
int converted_samples = swr_convert(
swr_ctx, // 重采样上下文
out_data, // 输出缓冲区
out_samples, // 输出采样数
(const uint8_t**)frame->data, // 输入数据
frame->nb_samples // 输入采样数
);
if(converted_samples > 0) {
// 计算实际数据大小(字节)
// S16格式:每个采样2字节,单声道
int data_size = converted_samples * 1 * 2;
// 写入PCM文件
fwrite(out_data[0], 1, data_size, pcm_file);
qDebug() << "第" << total_frame << "帧,转换了" << converted_samples
<< "个采样,写入" << data_size << "字节";
}
// 释放分配的缓冲区
if(out_data) {
av_freep(&out_data[0]);
av_freep(&out_data);
}
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
qDebug()<<"总共读取了"<<total_frame<<"帧";
// ========== 关闭PCM文件 ==========
if(pcm_file) {
fclose(pcm_file);
qDebug() << "PCM 文件已保存: E:/output.pcm";
}
// ========== 回收资源 ==========
swr_free(&swr_ctx);
avformat_close_input(&avfmctx);
avcodec_free_context(&avcodec_ctx);
av_packet_free(&packet);
av_frame_free(&frame);
return 0;
}
输出结果示例
avformat_open_input is success
av_find_best_stream is success
this Audio_stream index is 1
decodec is find, name is aac
avcodec_parameters_to_context is success
avcodec_open2 is success
音频流信息---------
采样率: 48000
声道数: 2
采样格式: fltp
创建一个空SwrContext成功
SwrContext初始化成功
PCM 文件创建成功: E:/output.pcm
第 1 帧,转换了 941 个采样,写入 1882 字节
第 2 帧,转换了 941 个采样,写入 1882 字节
第 3 帧,转换了 941 个采样,写入 1882 字节
...
第 200 帧,转换了 941 个采样,写入 1882 字节
总共读取了 200 帧
PCM 文件已保存: E:/output.pcm
⚠️ 常见错误与注意事项
错误1:采样格式从流参数获取
cpp
// ❌ 错误:从流参数获取采样格式
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt",
(AVSampleFormat)audio_stream->codecpar->format, 0);
// ✅ 正确:从解码器上下文获取
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt",
audio_ctx->sample_fmt, 0);
原因:
audio_stream->codecpar->format
是文件存储的格式audio_ctx->sample_fmt
是解码器实际输出的格式- 解码器可能会转换格式,两者可能不同!
实际案例:
文件中:AAC编码(compressed)
解码器输出:FLTP格式(解码后)
错误2:忘记调用 swr_init()
cpp
// ❌ 错误:忘记初始化
SwrContext* swr_ctx = swr_alloc();
av_opt_set_int(swr_ctx, "in_sample_rate", 48000, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
// 忘记调用 swr_init()
swr_convert(swr_ctx, ...); // ❌ 会失败或崩溃
// ✅ 正确:必须初始化
SwrContext* swr_ctx = swr_alloc();
av_opt_set_int(swr_ctx, "in_sample_rate", 48000, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
swr_init(swr_ctx); // ✅ 必须调用
swr_convert(swr_ctx, ...);
症状:
- 程序崩溃
- 转换返回负数错误码
- 输出的PCM文件全是噪音
错误3:输出缓冲区太小
cpp
// ❌ 错误:没有考虑采样率变化
int out_samples = frame->nb_samples; // 假设输出和输入一样
// ✅ 正确:用 av_rescale_rnd 计算
int out_samples = av_rescale_rnd(
frame->nb_samples,
44100, // 输出采样率
audio_ctx->sample_rate, // 输入采样率
AV_ROUND_UP // 向上取整
);
原因:
- 采样率从48kHz转44.1kHz,采样数会变化
- 缓冲区不够会导致数据丢失或崩溃
错误4:释放了 linesize
cpp
uint8_t** out_data = nullptr;
int out_linesize = 0;
av_samples_alloc_array_and_samples(&out_data, &out_linesize, ...);
// ❌ 错误:linesize 是整数,不需要释放
av_freep(&out_linesize); // 错误!
// ✅ 正确:只释放 out_data
av_freep(&out_data[0]);
av_freep(&out_data);
原因:
out_data
指向动态分配的内存 → 需要释放out_linesize
只是一个整数变量 → 不需要释放
错误5:混淆采样数和字节数
cpp
// ❌ 错误:把采样数当成字节数
int converted_samples = swr_convert(...);
fwrite(out_data[0], 1, converted_samples, file); // 错误!
// ✅ 正确:计算字节数
int converted_samples = swr_convert(...);
int data_size = av_samples_get_buffer_size(
nullptr, channels, converted_samples, AV_SAMPLE_FMT_S16, 1
);
fwrite(out_data[0], 1, data_size, file); // 正确
原因:
swr_convert
返回的是采样数,不是字节数- S16格式:每个采样2字节
- 需要用
av_samples_get_buffer_size
转换
🎓 验证结果
方法1:使用 FFplay 播放
bash
# 格式:ffplay -f s16le -ar 采样率 -ac 声道数 文件名
ffplay -f s16le -ar 44100 -ac 1 output.pcm
参数说明:
-f s16le
:16位有符号整数,小端序-ar 44100
:采样率44100Hz-ac 1
:1声道(单声道)
如果是立体声:
bash
ffplay -f s16le -ar 44100 -ac 2 output.pcm
📋 总结
核心流程回顾
1. swr_alloc() ← 创建转换器
2. av_opt_set_int() ← 设置采样率
3. av_opt_set_sample_fmt() ← 设置采样格式(从解码器上下文获取)
4. av_opt_set_chlayout() ← 设置声道布局
5. swr_init() ← 初始化(必须!)
6. while(解码循环)
7. av_rescale_rnd() ← 计算输出采样数
8. av_samples_alloc_...() ← 分配输出缓冲区
9. swr_convert() ← 执行转换
10. av_samples_get_buffer_size ← 计算字节数
11. fwrite() ← 写入文件
12. av_freep() ← 释放缓冲区
13. swr_free() ← 释放转换器
关键API对比
API | 功能 | 调用时机 | 返回值 |
---|---|---|---|
swr_alloc |
创建转换器 | 一次 | SwrContext* |
av_opt_set_int |
设置采样率 | 初始化前 | 0成功 |
av_opt_set_sample_fmt |
设置采样格式 | 初始化前 | 0成功 |
av_opt_set_chlayout |
设置声道布局 | 初始化前 | 0成功 |
swr_init |
初始化 | 设置参数后(必须) | 0成功 |
av_rescale_rnd |
计算采样数 | 每帧转换前 | 采样数 |
swr_convert |
执行转换 | 每帧 | 输出采样数 |
av_samples_get_buffer_size |
计算字节数 | 转换后 | 字节数 |
swr_free |
释放转换器 | 程序结束 | 无 |
资源释放清单
资源 | 分配函数 | 释放函数 |
---|---|---|
音频转换器 | swr_alloc() |
swr_free() |
音频缓冲区 | av_samples_alloc_array_and_samples() |
av_freep(&data[0]) + av_freep(&data) |
Packet | av_packet_alloc() |
av_packet_free() |
Frame | av_frame_alloc() |
av_frame_free() |
解码器上下文 | avcodec_alloc_context3() |
avcodec_free_context() |
格式上下文 | avformat_open_input() |
avformat_close_input() |
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多 FFmpeg 系列教程将持续更新 🔥!