Qt+FFmpeg 实现 PCM 音频转 AAC 编码

大家好!今天给大家分享一个零基础也能看懂的实战案例:用 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() << "编码完成!";
}

小白核心解读(不用记代码,记流程)

  1. 找编码器 :FFmpeg 提供了多个 AAC 编码器,我们选libfdk_aac(音质最好);
  2. 配参数:告诉编码器 "输入的 PCM 是 44100 采样率、16 位格式、立体声";
  3. 读 PCM:把硬盘上的 PCM 文件读入内存;
  4. 送编码器:把 PCM 数据传给 FFmpeg 编码器;
  5. 取 AAC:从编码器取出编码后的 AAC 数据;
  6. 写文件:把 AAC 数据写入硬盘;
  7. 清资源:FFmpeg 的内存必须手动释放,否则会内存泄漏。

四、运行效果

  1. 准备一个 PCM 文件(放到D:/in.pcm,参数要和代码里的 44100、16 位、立体声一致);
  2. 点击 Qt 窗口的audioButton按钮;
  3. 等待几秒,D:/out.aac就会生成,用播放器打开就能听到和原 PCM 一样的声音,但文件体积小了很多!

五、小白常见问题 & 解决技巧

  1. 编译报错 "找不到 avcodec.h" :FFmpeg 的头文件路径没配对,在 Qt 的.pro文件里加INCLUDEPATH += FFmpeg的头文件路径
  2. 链接报错 "找不到 avcodec.lib" :在.pro文件里加LIBS += -LFFmpeg的库路径 -lavcodec -lavutil
  3. 编码后 AAC 无声:PCM 参数和代码里的不一致(比如采样率不是 44100),或者 PCM 文件本身是错的;
  4. 窗口卡住 :编码逻辑没放到子线程,必须像AudioThread那样放到 QThread 里。

六、总结

今天我们实现了 "PCM 转 AAC" 的核心功能,小白重点掌握:

  • 音频编码的核心流程(找编码器→配参数→读数据→编码→写文件);
  • Qt 线程的使用(避免卡界面);
  • FFmpeg 的核心对象(编码器、上下文、帧、包)的作用。
相关推荐
xmRao5 小时前
Qt+FFmpeg 实现录音程序(pcm转wav)
qt·ffmpeg
喜欢喝果茶.5 小时前
QOverload<参数列表>::of(&函数名)信号槽
开发语言·qt
wjhx6 小时前
QT中对蓝牙权限的申请,整理一下
java·数据库·qt
踏过山河,踏过海6 小时前
【qt-查看对应的依赖的一种方法】
qt·visual studio
C++ 老炮儿的技术栈6 小时前
VS2015 + Qt 实现图形化Hello World(详细步骤)
c语言·开发语言·c++·windows·qt
C++ 老炮儿的技术栈9 小时前
Qt Creator中不写代如何设置 QLabel的颜色
c语言·开发语言·c++·qt·算法
ae_zr1 天前
QT动态编译应用后,如何快速获取依赖
开发语言·qt
LYOBOYI1231 天前
qml的对象树机制
c++·qt
菜鸟小芯1 天前
Qt Creator 集成开发环境下载安装
开发语言·qt