大家好!今天给大家分享一个零基础也能看懂的实战案例:用 Qt 结合 FFmpeg,把原始的 PCM 音频文件编码成常用的 AAC 格式。本文会从 "是什么、怎么做、代码怎么理解" 三个维度拆解,保证小白也能跟上~
一、先搞懂基础概念
在写代码前,先花 2 分钟搞懂核心概念,不然代码看了也白看:
- PCM:音频的 "原始素颜数据",是声卡 / 麦克风采集到的最原始的音频数据,体积大、不压缩,几乎所有播放器都能解析,但文件体积超大。
- AAC:我们平时听歌、刷视频的主流音频格式(比如 MP4 里的音频),是经过压缩编码的,体积小、音质损失少。
- FFmpeg:音视频处理的 "瑞士军刀",是一套开源的音视频编解码库,几乎所有音视频软件(比如抖音、B 站客户端)底层都用到了它。
- Qt:跨平台的 C++ 图形界面库,我们这里用它做界面(一个按钮触发编码)+ 线程管理(避免编码卡住界面)。
简单说,我们要做的事:把 "原始素颜" 的 PCM,通过 FFmpeg 编码成 "精致压缩" 的 AAC。
二、整体代码结构
我们的项目一共 7 个核心文件,先看整体分工,心里有个数:
| 文件 | 作用 | 小白理解版 |
|---|---|---|
| main.cpp | Qt 程序入口 | 启动程序,显示主窗口 |
| mainwindow.h/.cpp | 主窗口界面 | 放一个按钮,点击按钮触发音频编码 |
| audiothread.h/.cpp | 音频编码线程 | 把编码逻辑放到子线程(避免卡界面) |
| ffmpegs.h/.cpp | FFmpeg 核心编码逻辑 | 真正做 "PCM 转 AAC" 的核心代码 |
三、逐文件拆解
接下来逐个文件解析,重点讲 "核心逻辑",无关代码直接跳过~
1. 程序入口:main.cpp
cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
// Qt应用程序对象,管理程序的资源
QApplication a(argc, argv);
// 创建主窗口
MainWindow w;
// 显示主窗口
w.show();
// 进入Qt的事件循环(简单说:让程序一直运行,等待用户操作)
return a.exec();
}
小白解读:这是所有 Qt 程序的 "标配入口",作用就是启动程序、显示窗口,没难度~
2. 主窗口:mainwindow.h/.cpp
mainwindow.h(头文件)
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <audiothread.h> // 引入音频线程头文件
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; } // Qt设计师生成的界面类
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT // Qt的信号槽机制必须加这个宏
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
// 按钮点击的槽函数(按钮叫audioButton)
void on_audioButton_clicked();
private:
Ui::MainWindow *ui; // 界面对象(对应UI设计师里的按钮、输入框等)
AudioThread *_audioThread = nullptr; // 音频编码线程对象
};
#endif // MAINWINDOW_H
mainwindow.cpp(实现文件)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
// 构造函数:创建主窗口时调用
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow) {
// 初始化UI界面(加载设计师画的窗口)
ui->setupUi(this);
}
// 析构函数:窗口关闭时调用
MainWindow::~MainWindow() {
delete ui; // 释放界面资源
}
// 按钮点击后执行的函数
void MainWindow::on_audioButton_clicked() {
// 创建音频编码线程(父对象是MainWindow,自动管理内存)
_audioThread = new AudioThread(this);
// 启动线程(会执行AudioThread的run()函数)
_audioThread->start();
}
小白解读:
- 我们在 Qt 设计师里拖了一个叫
audioButton的按钮,点击它就会执行on_audioButton_clicked(); - 为什么要创建线程?因为编码音频需要时间,如果直接在主线程执行,窗口会 "卡住不动",子线程专门做编码,界面依然能操作。
3. 音频线程:audiothread.h/.cpp
audiothread.h(头文件
cpp
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H
#include <QThread> // Qt的线程类
class AudioThread : public QThread {
Q_OBJECT
private:
// 线程的核心函数:线程启动后会执行run()
void run();
public:
explicit AudioThread(QObject *parent = nullptr);
~AudioThread();
};
#endif // AUDIOTHREAD_H
audiothread.cpp(实现文件)
cpp
#include "audiothread.h"
#include <QDebug>
#include "ffmpegs.h" // 引入FFmpeg编码类
// 构造函数
AudioThread::AudioThread(QObject *parent) : QThread(parent) {
// 线程结束后,自动释放内存(Qt的小技巧,避免内存泄漏)
connect(this, &AudioThread::finished, this, &AudioThread::deleteLater);
}
// 析构函数:线程销毁时调用
AudioThread::~AudioThread() {
disconnect(); // 断开所有信号槽连接
requestInterruption(); // 请求线程中断
quit(); // 退出线程事件循环
wait(); // 等待线程完全结束
qDebug() << this << "析构(内存被回收)";
}
// 线程执行的核心逻辑:PCM转AAC的参数配置+调用编码函数
void AudioThread::run() {
// 配置输入的PCM参数(小白重点看注释!)
AudioEncodeSpec in;
in.filename = "D:/in.pcm"; // 输入的PCM文件路径
in.sampleRate = 44100; // 采样率:44100Hz(CD级音质,最常用)
in.sampleFmt = AV_SAMPLE_FMT_S16; // 采样格式:16位整数(PCM最常用格式)
in.chLayout = AV_CH_LAYOUT_STEREO; // 声道布局:立体声(左右声道)
// 调用FFmpeg的编码函数,输出AAC文件到D:/out.aac
FFmpegs::aacEncode(in, "D:/out.aac");
}
小白解读:
- 线程的核心是
run()函数,里面定义了 "要编码的 PCM 文件路径" 和 "编码参数",然后调用FFmpegs::aacEncode()做实际编码; - 采样率、采样格式、声道布局是音频的三大核心参数,必须和输入的 PCM 文件一致,否则编码出来的音频会 "杂音" 或 "无声";
- 析构函数里的代码是 Qt 线程的 "标准收尾操作",避免线程没结束就销毁导致崩溃。
4. 核心编码逻辑:ffmpegs.h/.cpp
这部分是 FFmpeg 的核心,也是小白最容易懵的地方
ffmpegs.h(头文件)
cpp
#ifndef FFMPEGS_H
#define FFMPEGS_H
// 引入FFmpeg的C语言头文件(extern "C"是为了兼容C++)
extern "C" {
#include <libavformat/avformat.h>
}
// 定义PCM输入参数的结构体(把参数打包,方便传递)
typedef struct {
const char *filename; // PCM文件路径
int sampleRate; // 采样率
AVSampleFormat sampleFmt; // 采样格式
int chLayout; // 声道布局
} AudioEncodeSpec;
class FFmpegs {
public:
FFmpegs();
// 静态函数:不用创建对象就能调用,直接FFmpegs::aacEncode()
static void aacEncode(AudioEncodeSpec &in, const char *outFilename);
};
#endif // FFMPEGS_H
ffmpegs.cpp(核心实现)
先看整体流程,再拆细节:
cpp
#include "ffmpegs.h"
#include <QDebug>
#include <QFile>
// 引入FFmpeg的编解码、工具类头文件
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}
// 错误信息宏(小白不用纠结,就是把FFmpeg的错误码转成文字)
#define ERROR_BUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf));
FFmpegs::FFmpegs() {}
// 辅助函数:检查编码器是否支持指定的采样格式
static int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt) {
const enum AVSampleFormat *p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) {
if (*p == sample_fmt) return 1; // 支持
p++;
}
return 0; // 不支持
}
// 核心编码函数:把PCM数据编码成AAC,写入文件
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, QFile &outFile) {
// 1. 把PCM帧(frame)发送到编码器
int ret = avcodec_send_frame(ctx, frame);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "发送PCM数据到编码器失败:" << errbuf;
return ret;
}
// 2. 循环从编码器取出编码后的AAC数据
while (true) {
ret = avcodec_receive_packet(ctx, pkt);
// 两种正常情况:需要更多PCM数据 / 编码完成
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) { // 编码出错
ERROR_BUF(ret);
qDebug() << "获取AAC数据失败:" << errbuf;
return ret;
}
// 3. 把编码后的AAC数据写入文件
outFile.write((char *) pkt->data, pkt->size);
// 4. 释放AAC数据包的资源(FFmpeg的内存必须手动释放!)
av_packet_unref(pkt);
}
}
// 主编码函数:PCM转AAC的完整流程
void FFmpegs::aacEncode(AudioEncodeSpec &in, const char *outFilename) {
// ===== 第一步:初始化变量 =====
QFile inFile(in.filename); // 输入PCM文件
QFile outFile(outFilename); // 输出AAC文件
int ret = 0; // FFmpeg函数的返回值(小于0就是出错)
// FFmpeg核心对象(小白记作用途就行):
AVCodec *codec = nullptr; // 编码器(AAC编码器)
AVCodecContext *ctx = nullptr; // 编码器上下文(管理编码器的所有参数)
AVFrame *frame = nullptr; // 存放PCM数据的帧
AVPacket *pkt = nullptr; // 存放AAC数据的包
// ===== 第二步:获取AAC编码器 =====
// 用libfdk_aac编码器(音质好、兼容性强)
codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
qDebug() << "找不到AAC编码器!";
return;
}
// 检查编码器是否支持我们的PCM采样格式(这里是16位整数)
if (!check_sample_fmt(codec, in.sampleFmt)) {
qDebug() << "编码器不支持该采样格式!";
return;
}
// ===== 第三步:创建并配置编码器上下文 =====
ctx = avcodec_alloc_context3(codec); // 分配上下文内存
if (!ctx) {
qDebug() << "创建编码器上下文失败!";
return;
}
// 设置编码器参数(和输入PCM的参数一致)
ctx->sample_rate = in.sampleRate; // 采样率
ctx->sample_fmt = in.sampleFmt; // 采样格式
ctx->channel_layout = in.chLayout; // 声道布局
ctx->bit_rate = 32000; // 比特率(越高音质越好,文件越大)
ctx->profile = FF_PROFILE_AAC_HE_V2; // AAC编码规格(HE-V2是高效压缩)
// 打开编码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "打开编码器失败:" << errbuf;
goto end; // 跳转到结尾释放资源
}
// ===== 第四步:创建PCM帧(frame)和AAC包(pkt) =====
frame = av_frame_alloc(); // 分配frame内存
if (!frame) {
qDebug() << "创建PCM帧失败!";
goto end;
}
// 设置frame的参数(和编码器一致)
frame->nb_samples = ctx->frame_size; // 每一帧的样本数(编码器决定)
frame->format = ctx->sample_fmt; // 采样格式
frame->channel_layout = ctx->channel_layout; // 声道布局
// 为frame分配实际的内存缓冲区(存放PCM数据)
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "为PCM帧分配缓冲区失败:" << errbuf;
goto end;
}
pkt = av_packet_alloc(); // 分配AAC包内存
if (!pkt) {
qDebug() << "创建AAC包失败!";
goto end;
}
// ===== 第五步:打开文件 =====
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "打开PCM文件失败:" << in.filename;
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "打开AAC文件失败:" << outFilename;
goto end;
}
// ===== 第六步:读取PCM数据,编码成AAC =====
// 循环读取PCM文件数据到frame的缓冲区
while ((ret = inFile.read((char *) frame->data[0], frame->linesize[0])) > 0) {
// 如果读取的字节数不够一帧,调整有效样本数(避免编码冗余数据)
if (ret < frame->linesize[0]) {
int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format); // 每个样本的字节数
int ch = av_get_channel_layout_nb_channels(frame->channel_layout); // 声道数
frame->nb_samples = ret / (bytes * ch); // 实际有效的样本数
}
// 调用编码函数,把PCM转AAC并写入文件
if (encode(ctx, frame, pkt, outFile) < 0) {
goto end;
}
}
// ===== 第七步:刷新编码器(处理最后一批数据) =====
encode(ctx, nullptr, pkt, outFile);
// ===== 结尾:释放资源 =====
end:
// 关闭文件
inFile.close();
outFile.close();
// 释放FFmpeg对象(必须释放,否则内存泄漏)
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&ctx);
qDebug() << "编码完成!";
}
小白核心解读(不用记代码,记流程):
- 找编码器 :FFmpeg 提供了多个 AAC 编码器,我们选
libfdk_aac(音质最好); - 配参数:告诉编码器 "输入的 PCM 是 44100 采样率、16 位格式、立体声";
- 读 PCM:把硬盘上的 PCM 文件读入内存;
- 送编码器:把 PCM 数据传给 FFmpeg 编码器;
- 取 AAC:从编码器取出编码后的 AAC 数据;
- 写文件:把 AAC 数据写入硬盘;
- 清资源:FFmpeg 的内存必须手动释放,否则会内存泄漏。
四、运行效果
- 准备一个 PCM 文件(放到
D:/in.pcm,参数要和代码里的 44100、16 位、立体声一致); - 点击 Qt 窗口的
audioButton按钮; - 等待几秒,
D:/out.aac就会生成,用播放器打开就能听到和原 PCM 一样的声音,但文件体积小了很多!
五、小白常见问题 & 解决技巧
- 编译报错 "找不到 avcodec.h" :FFmpeg 的头文件路径没配对,在 Qt 的
.pro文件里加INCLUDEPATH += FFmpeg的头文件路径; - 链接报错 "找不到 avcodec.lib" :在
.pro文件里加LIBS += -LFFmpeg的库路径 -lavcodec -lavutil; - 编码后 AAC 无声:PCM 参数和代码里的不一致(比如采样率不是 44100),或者 PCM 文件本身是错的;
- 窗口卡住 :编码逻辑没放到子线程,必须像
AudioThread那样放到 QThread 里。
六、总结
今天我们实现了 "PCM 转 AAC" 的核心功能,小白重点掌握:
- 音频编码的核心流程(找编码器→配参数→读数据→编码→写文件);
- Qt 线程的使用(避免卡界面);
- FFmpeg 的核心对象(编码器、上下文、帧、包)的作用。