深入解析:FFmpeg 核心 API 系列:音频重采样 SwrContext 完全指南(新API版本)

FFmpeg 核心 API 系列:音频重采样 SwrContext 完全指南(新API版本)
更新时间:2025年10月18日
️ 标签:FFmpeg | 多媒体处理 | 音视频编程 | C/C++ | 音频处理 | 重采样

文章目录


前言

回顾前四个阶段,我们已经能够:

  • 阶段一:打开文件 → 得到 AVFormatContextAVStream
  • 阶段二:查找解码器 → 配置并打开 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_S1616位整数2Packed交错存储
AV_SAMPLE_FMT_S16P16位整数2Planar平面存储
AV_SAMPLE_FMT_FLT32位浮点4Packed交错存储
AV_SAMPLE_FMT_FLTP32位浮点4Planar最常见

声道数(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成功";

关键要点

  1. 只是创建空对象还需要用后续API设置参数!!!
  2. 不能直接使用:必须调用 swr_init() 初始化后才能用
  3. 只需创建一次:可以重复用于多帧转换

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);

关键要点

  1. 输入采样率来源:从 audio_ctx->sample_rate 获取
  2. 输出采样率选择:常用 4410048000
  3. 顺序无关:先设置输入还是输出都可以

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_S1616位整数Packed播放器输出
AV_SAMPLE_FMT_S16P16位整数Planar音频处理
AV_SAMPLE_FMT_FLTP32位浮点Planar解码器常见输出
AV_SAMPLE_FMT_FLT32位浮点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_MONO1单声道
AV_CHANNEL_LAYOUT_STEREO2立体声(最常用)
AV_CHANNEL_LAYOUT_5POINT165.1环绕声

如何获取声道数

// 从声道布局获取声道数
int channels = audio_ctx->ch_layout.nb_channels;
qDebug() << "声道数:" << channels;

API 5️⃣:swr_init - 初始化转换器

函数原型

int swr_init(struct SwrContext *s);

参数说明

参数说明
sSwrContext指针

返回值

  • 成功:0
  • 失败:< 0(负数错误码)

作用

初始化SwrContext,让之前设置的参数生效。

基本用法

int result = swr_init(swr_ctx);
if(result < 0) {
qDebug() << "SwrContext初始化失败";
return -1;
}
qDebug() << "SwrContext初始化成功";

⚠️ 关键要点

  1. 必须调用不调用无法使用转换器!!!!
  2. 调用时机:所有参数设置完成后
  3. 只需调用一次:初始化后可重复使用
  4. 相当于"确认配置":就像按下"应用"按钮

错误示例

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                // 输入采样数
);

参数说明

参数说明常用值
sSwrContext指针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 << "个采样";
}

关键要点

  1. 返回值是采样数:不是字节数!
  2. 输出采样数可能不同:因为采样率可能变化
  3. 可以多次调用:同一个转换器可以重复使用
  4. 输入数据来自AVFrameframe->dataframe->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,                    // 不需要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

示例:

// 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);

参数说明

参数说明
sSwrContext指针的指针(二级指针)

返回值

  • 无返回值

作用

释放SwrContext及其内部资源。

基本用法

swr_free(&swr_ctx);
// 之后 swr_ctx 会变成 NULL

关键要点

  1. 传入二级指针&swr_ctx,不是 swr_ctx
  2. 自动置NULL:释放后指针会被设为NULL
  3. 调用时机:程序结束前调用
  4. 必须调用:避免内存泄漏

完整转换流程

标准流程图

┌─────────────────────┐
│  打开文件和解码器   │  ← 阶段一、二
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│   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)
                                {
                                // 从解码器读取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:采样格式从流参数获取

// ❌ 错误:从流参数获取采样格式
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)
Packetav_packet_alloc()av_packet_free()
Frameav_frame_alloc()av_frame_free()
解码器上下文avcodec_alloc_context3()avcodec_free_context()
格式上下文avformat_open_input()avformat_close_input()

如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多 FFmpeg 系列教程将持续更新 !

posted on 2025-11-19 16:39  ljbguanli  阅读(0)  评论(0)    收藏  举报