FFmpeg 核心 API 系列:音频重采样 SwrContext 完全指南(新API版本)
FFmpeg 核心 API 系列:音频重采样 SwrContext 完全指南(新API版本)
📅 更新时间:2025年10月18日
🏷️ 标签:FFmpeg | 多媒体处理 | 音视频编程 | C/C++ | 音频处理 | 重采样
文章目录
- 📖 前言
- 🎯 为什么需要音频重采样?
- 1. 音频的三大属性
- 📊 采样率(Sample Rate)
- 📊 采样格式(Sample Format)
- 📊 声道数(Channels)
- 2. Planar vs Packed(重要概念!)
- Packed(交错格式)
- Planar(平面格式)
- 为什么需要转换?
- 3. 音频转换对比表
- 🔧 核心 API 详解(新版)
- API 1️⃣:`swr_alloc` - 创建音频转换器
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 关键要点
- API 2️⃣:`av_opt_set_int` - 设置整数选项
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 关键要点
- API 3️⃣:`av_opt_set_sample_fmt` - 设置采样格式
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 常用采样格式表
- ⚠️ 重要注意事项
- API 4️⃣:`av_opt_set_chlayout` - 设置声道布局
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 常用声道布局
- 如何获取声道数
- API 5️⃣:`swr_init` - 初始化转换器
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- ⚠️ 关键要点
- API 6️⃣:`av_rescale_rnd` - 计算输出采样数
- 函数原型
- 参数说明
- 返回值
- 作用
- 为什么需要这个函数?
- 基本用法
- 取整方式
- API 7️⃣:`av_samples_alloc_array_and_samples` - 分配音频缓冲区
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 这个函数做了什么?
- ⚠️ 内存释放注意
- API 8️⃣:`swr_convert` - 执行音频转换
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 关键要点
- 转换过程示意
- API 9️⃣:`av_samples_get_buffer_size` - 计算数据字节数
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 计算公式
- 快速计算方法
- API 🔟:`swr_free` - 释放转换器
- 函数原型
- 参数说明
- 返回值
- 作用
- 基本用法
- 关键要点
- 🔄 完整转换流程
- 标准流程图
- 💻 完整示例代码
- 输出结果示例
- ⚠️ 常见错误与注意事项
- 错误1:采样格式从流参数获取
- 错误2:忘记调用 swr_init()
- 错误3:输出缓冲区太小
- 错误4:释放了 linesize
- 错误5:混淆采样数和字节数
- 🎓 验证结果
- 方法1:使用 FFplay 播放
- 📋 总结
- 核心流程回顾
- 关键API对比
- 资源释放清单
📖 前言
回顾前四个阶段,我们已经能够:
- 阶段一:打开文件 → 得到
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
- 创建音频转换器
函数原型
struct SwrContext* swr_alloc(void);
参数说明
- 无参数
返回值
- 成功:返回
SwrContext*
指针 - 失败:返回
NULL
作用
创建一个空的音频转换器对象,类似于视频转换中的 SwsContext
。
基本用法
SwrContext* swr_ctx = swr_alloc();
if(!swr_ctx) {qDebug() << "创建SwrContext失败";return -1;
}
qDebug() << "创建SwrContext成功";
关键要点
- 只是创建空对象:还需要用后续API设置参数!!!
- 不能直接使用:必须调用
swr_init()
初始化后才能用 - 只需创建一次:可以重复用于多帧转换
API 2️⃣:av_opt_set_int
- 设置整数选项
函数原型
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
(负数错误码)
作用
设置整数类型的音频参数,主要用于设置采样率!!!
基本用法
// 设置输入采样率(从解码器获取)
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
- 设置采样格式
函数原型
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)。
基本用法
// 设置输入格式(从解码器获取)
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 | 音频处理 |
⚠️ 重要注意事项
采样格式必须从解码器上下文获取,不能从流参数获取!
// ❌ 错误:从流参数获取
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
- 设置声道布局
函数原型
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
作用
设置音频的声道布局(单声道、立体声、环绕声等)。
基本用法
// 设置输入声道布局(从解码器获取)
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环绕声 |
如何获取声道数
// 从声道布局获取声道数
int channels = audio_ctx->ch_layout.nb_channels;
qDebug() << "声道数:" << channels;
API 5️⃣:swr_init
- 初始化转换器
函数原型
int swr_init(struct SwrContext *s);
参数说明
参数 | 说明 |
---|---|
s | SwrContext指针 |
返回值
- 成功:
0
- 失败:
< 0
(负数错误码)
作用
初始化SwrContext,让之前设置的参数生效。
基本用法
int result = swr_init(swr_ctx);
if(result < 0) {qDebug() << "SwrContext初始化失败";return -1;
}
qDebug() << "SwrContext初始化成功";
⚠️ 关键要点
- 必须调用:不调用无法使用转换器!!!!
- 调用时机:所有参数设置完成后
- 只需调用一次:初始化后可重复使用
- 相当于"确认配置":就像按下"应用"按钮
错误示例:
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
- 计算输出采样数
函数原型
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个采样
基本用法
// 计算输出采样数
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
- 分配音频缓冲区
函数原型
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格式。
基本用法
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] 存第二声道
⚠️ 内存释放注意
// 使用完后必须释放
if(out_data) {av_freep(&out_data[0]); // 释放数据av_freep(&out_data); // 释放指针数组
}
API 8️⃣:swr_convert
- 执行音频转换
函数原型
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
(负数错误码)
作用
执行音频格式转换,是整个重采样的核心函数。
基本用法
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
- 计算数据字节数
函数原型
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
作用
计算音频数据占用的字节数,用于写入文件或播放。
基本用法
// 计算转换后的数据大小(字节)
int data_size = av_samples_get_buffer_size(nullptr, // 不需要linesize2, // 2声道converted_samples, // 转换后的采样数AV_SAMPLE_FMT_S16, // S16格式1 // 不对齐
);qDebug() << "数据大小:" << data_size << "字节";// 写入文件
fwrite(out_data[0], 1, data_size, pcm_file);
计算公式
S16格式(2字节/采样):
总字节数 = 声道数 × 采样数 × 2
示例:
// 2声道,1024采样,S16格式
data_size = 2 × 1024 × 2 = 4096字节
快速计算方法
如果你知道格式,也可以手动计算:
// 方法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
- 释放转换器
函数原型
void swr_free(struct SwrContext **s);
参数说明
参数 | 说明 |
---|---|
s | SwrContext指针的指针(二级指针) |
返回值
- 无返回值
作用
释放SwrContext及其内部资源。
基本用法
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格式并保存
#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){// 从解码器读取framewhile(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:采样格式从流参数获取
// ❌ 错误:从流参数获取采样格式
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()
// ❌ 错误:忘记初始化
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:输出缓冲区太小
// ❌ 错误:没有考虑采样率变化
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
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:混淆采样数和字节数
// ❌ 错误:把采样数当成字节数
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 播放
# 格式:ffplay -f s16le -ar 采样率 -ac 声道数 文件名
ffplay -f s16le -ar 44100 -ac 1 output.pcm
参数说明:
-f s16le
:16位有符号整数,小端序-ar 44100
:采样率44100Hz-ac 1
:1声道(单声道)
如果是立体声:
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 系列教程将持续更新 🔥!