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_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成功";

关键要点

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

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

关键要点

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

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初始化成功";

⚠️ 关键要点

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

错误示例

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 << "个采样";
}

关键要点

  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 - 计算数据字节数

函数原型

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

关键要点

  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格式并保存

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 系列教程将持续更新 🔥!

相关推荐
张晓~183399481214 小时前
碰一碰发视频 系统源码 /PHP 语言开发方案
开发语言·线性代数·矩阵·aigc·php·音视频·文心一言
先知后行。5 小时前
音视频ffmpeg
ffmpeg
雨之小17 小时前
RV1106+es8388音频采集和播放调试
音视频·rv1106·es8388
EasyCVR17 小时前
不止于“看”:视频汇聚平台EasyCVR视频监控系统功能特点详解
音视频
彷徨而立20 小时前
【FFmpeg】对比 d3d12va 、d3d11va、dxva2 这三种视频硬解方案
ffmpeg
来知晓1 天前
语音处理:音频移形幻影,为何大振幅信号也无声
开发语言·音视频
阿酷tony1 天前
开源项目:FlyCut Caption智能视频字幕裁剪工具
音视频·智能视频字幕裁剪·视频字幕裁剪
jjjxxxhhh1231 天前
【学习】USB摄像头 -> FFmpeg -> H264 -> AI模型
人工智能·学习·ffmpeg