前言
本文面向编程小白,用最通俗的语言讲解如何基于 Qt+FFmpeg 实现音频重采样(简单说:把音频文件的采样率 / 采样格式 / 声道数转换成目标规格)。全程避开复杂术语,只讲核心逻辑,代码可直接运行~
3 个核心概念
| 概念 | 通俗解释 |
|---|---|
| 采样率 | 每秒采集的音频 "样本数",比如 44100Hz = 每秒采 44100 个样本,数值越高音质越细腻 |
| 采样格式 | 每个音频样本的 "存储格式",比如 S16(16 位整数)、F32(32 位浮点) |
| 声道数 | 单声道(MONO,1 个声道)、立体声(STEREO,2 个声道) |
| 音频重采样 | 把音频的采样率 / 采样格式 / 声道数,转换成目标规格的过程(本文核心) |
整体流程
- 用 Qt 做一个带按钮的简单界面;
- 点击按钮后,启动一个独立线程(避免界面卡死);
- 线程中调用 FFmpeg 的音频重采样接口,把指定的 PCM 音频文件转换成新格式;
- 生成转换后的新音频文件。
环境准备
- Qt:随便一个版本(比如 Qt 5.15),用来做界面和线程管理;
- FFmpeg :编译好的库(重点包含
swresample模块,音频重采样专用); - PCM 文件 :测试用的无压缩音频文件(本文用
44100_s16le_2.pcm,格式:44100 采样率、16 位整数、立体声)。
完整代码
目录结构
├── main.cpp // 程序入口
├── mainwindow.h/.cpp // 主界面(带按钮)
├── audiothread.h/.cpp// 音频处理线程(避免界面卡死)
├── ffmpegs.h/.cpp // FFmpeg重采样核心逻辑
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
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "audiothread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
// 按钮点击的响应函数
void on_audioButton_clicked();
private:
// 界面控件(比如按钮)的管理对象
Ui::MainWindow *ui;
// 音频线程对象(避免界面卡死)
AudioThread *_audioThread = nullptr;
};
#endif // MAINWINDOW_H
3. mainwindow.cpp(主窗口实现)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow) {
// 初始化界面(加载Qt设计师画的按钮)
ui->setupUi(this);
}
MainWindow::~MainWindow() {
// 销毁界面控件,释放内存
delete ui;
}
void MainWindow::on_audioButton_clicked() {
// 创建音频线程对象
_audioThread = new AudioThread(this);
// 启动线程(执行AudioThread的run函数)
_audioThread->start();
}
4. audiothread.h(音频线程头文件)
cpp
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H
#include <QThread>
class AudioThread : public QThread {
Q_OBJECT
private:
// 线程启动后自动执行的函数(核心逻辑写这)
void run();
public:
explicit AudioThread(QObject *parent = nullptr);
~AudioThread();
};
#endif // AUDIOTHREAD_H
5. audiothread.cpp(音频线程实现)
cpp
#include "audiothread.h"
#include <QDebug>
#include "ffmpegs.h"
AudioThread::AudioThread(QObject *parent) : QThread(parent) {
// 线程结束后自动释放内存(Qt内存管理小技巧)
connect(this, &AudioThread::finished, this, &AudioThread::deleteLater);
}
AudioThread::~AudioThread() {
// 断开所有连接
disconnect();
// 要求线程安全退出
requestInterruption();
quit();
wait();
qDebug() << this << "析构(内存被回收)";
}
void AudioThread::run() {
// 定义转换规则:44100立体声16位 → 48000单声道浮点 → 48000单声道32位 → 还原成原始格式
ResampleAudioSpec ras1;
ras1.filename = "D:/44100_s16le_2.pcm"; // 原始文件路径
ras1.sampleFmt = AV_SAMPLE_FMT_S16; // 16位整数格式
ras1.sampleRate = 44100; // 44100采样率
ras1.chLayout = AV_CH_LAYOUT_STEREO; // 立体声
ResampleAudioSpec ras2;
ras2.filename = "D:/48000_f32le_1.pcm"; // 转换后文件1
ras2.sampleFmt = AV_SAMPLE_FMT_FLT; // 浮点格式
ras2.sampleRate = 48000; // 48000采样率
ras2.chLayout = AV_CH_LAYOUT_MONO; // 单声道
ResampleAudioSpec ras3;
ras3.filename = "D:/48000_s32le_1.pcm"; // 转换后文件2
ras3.sampleFmt = AV_SAMPLE_FMT_S32; // 32位整数格式
ras3.sampleRate = 48000; // 48000采样率
ras3.chLayout = AV_CH_LAYOUT_MONO; // 单声道
ResampleAudioSpec ras4 = ras1;
ras4.filename = "D:/44100_s16le_2_new.pcm"; // 最终还原的文件
// 调用FFmpeg重采样函数,一步步转换
FFmpegs::resampleAudio(ras1, ras2);
FFmpegs::resampleAudio(ras2, ras3);
FFmpegs::resampleAudio(ras3, ras4);
}
6. ffmpegs.h(FFmpeg 核心头文件)
cpp
#ifndef FFMPEGS_H
#define FFMPEGS_H
extern "C" {
#include <libavformat/avformat.h>
}
// 封装音频规格的结构体(方便传参)
typedef struct {
const char *filename; // 文件路径
int sampleRate; // 采样率
AVSampleFormat sampleFmt; // 采样格式
int chLayout; // 声道布局(单声道/立体声)
} ResampleAudioSpec;
class FFmpegs {
public:
FFmpegs();
// 重载的重采样函数(传结构体更方便)
static void resampleAudio(ResampleAudioSpec &in, ResampleAudioSpec &out);
// 核心重采样函数(传单个参数)
static void resampleAudio(const char *inFilename,
int inSampleRate,
AVSampleFormat inSampleFmt,
int inChLayout,
const char *outFilename,
int outSampleRate,
AVSampleFormat outSampleFmt,
int outChLayout);
};
#endif // FFMPEGS_H
7. ffmpegs.cpp(FFmpeg 重采样核心实现)
cpp
#include "ffmpegs.h"
#include <QDebug>
#include <QFile>
extern "C" {
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}
// 错误信息封装(小白不用抠,复制就行)
#define ERROR_BUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf));
FFmpegs::FFmpegs() {}
// 重载函数:调用核心重采样函数
void FFmpegs::resampleAudio(ResampleAudioSpec &in, ResampleAudioSpec &out) {
resampleAudio(in.filename, in.sampleRate, in.sampleFmt, in.chLayout,
out.filename, out.sampleRate, out.sampleFmt, out.chLayout);
}
// 核心重采样函数
void FFmpegs::resampleAudio(const char *inFilename,
int inSampleRate,
AVSampleFormat inSampleFmt,
int inChLayout,
const char *outFilename,
int outSampleRate,
AVSampleFormat outSampleFmt,
int outChLayout) {
// ===== 1. 初始化变量 =====
QFile inFile(inFilename); // 输入文件
QFile outFile(outFilename); // 输出文件
// 输入缓冲区相关
uint8_t **inData = nullptr; // 输入缓冲区指针
int inLinesize = 0; // 输入缓冲区大小
int inChs = av_get_channel_layout_nb_channels(inChLayout); // 输入声道数
int inBytesPerSample = inChs * av_get_bytes_per_sample(inSampleFmt); // 输入单个样本大小
int inSamples = 1024; // 输入缓冲区样本数
int len = 0; // 读取文件的字节数
// 输出缓冲区相关
uint8_t **outData = nullptr;// 输出缓冲区指针
int outLinesize = 0; // 输出缓冲区大小
int outChs = av_get_channel_layout_nb_channels(outChLayout); // 输出声道数
int outBytesPerSample = outChs * av_get_bytes_per_sample(outSampleFmt); // 输出单个样本大小
// 计算输出缓冲区样本数(按采样率比例缩放,向上取整)
int outSamples = av_rescale_rnd(outSampleRate, inSamples, inSampleRate, AV_ROUND_UP);
int ret = 0; // FFmpeg函数返回值
// ===== 2. 创建并初始化重采样上下文 =====
// 创建上下文(配置输入/输出参数)
SwrContext *ctx = swr_alloc_set_opts(nullptr,
// 输出参数
outChLayout, outSampleFmt, outSampleRate,
// 输入参数
inChLayout, inSampleFmt, inSampleRate,
0, nullptr);
if (!ctx) {
qDebug() << "创建重采样上下文失败";
goto end; // 跳转到释放资源的位置
}
// 初始化上下文
ret = swr_init(ctx);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "初始化重采样上下文失败:" << errbuf;
goto end;
}
// ===== 3. 创建输入/输出缓冲区 =====
// 创建输入缓冲区
ret = av_samples_alloc_array_and_samples(&inData, &inLinesize, inChs, inSamples, inSampleFmt, 1);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "创建输入缓冲区失败:" << errbuf;
goto end;
}
// 创建输出缓冲区
ret = av_samples_alloc_array_and_samples(&outData, &outLinesize, outChs, outSamples, outSampleFmt, 1);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "创建输出缓冲区失败:" << errbuf;
goto end;
}
// ===== 4. 打开文件 =====
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "打开输入文件失败:" << inFilename;
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "打开输出文件失败:" << outFilename;
goto end;
}
// ===== 5. 循环读取→重采样→写入 =====
while ((len = inFile.read((char *) inData[0], inLinesize)) > 0) {
// 计算实际读取的样本数
inSamples = len / inBytesPerSample;
// 核心:调用FFmpeg重采样函数
ret = swr_convert(ctx, outData, outSamples, (const uint8_t **) inData, inSamples);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "重采样失败:" << errbuf;
goto end;
}
// 把转换后的数据写入输出文件
outFile.write((char *) outData[0], ret * outBytesPerSample);
}
// 处理缓冲区残留的样本(避免数据丢失)
while ((ret = swr_convert(ctx, outData, outSamples, nullptr, 0)) > 0) {
outFile.write((char *) outData[0], ret * outBytesPerSample);
}
// ===== 6. 释放资源 =====
end:
// 关闭文件
inFile.close();
outFile.close();
// 释放输入缓冲区
if (inData) {
av_freep(&inData[0]);
}
av_freep(&inData);
// 释放输出缓冲区
if (outData) {
av_freep(&outData[0]);
}
av_freep(&outData);
// 释放重采样上下文
swr_free(&ctx);
}
核心逻辑:
- 初始化变量:定义输入 / 输出文件、缓冲区参数;
- 创建重采样上下文:告诉 FFmpeg "输入格式" 和 "输出格式";
- 创建缓冲区:用来存放读取的音频数据和转换后的音频数据;
- 打开文件:准备读原始文件、写新文件;
- 循环处理:读一点数据→转格式→写一点数据,直到文件读完;
- 释放资源:FFmpeg 的资源要手动释放,避免内存泄漏。
运行步骤
- 把代码中的文件路径(比如
D:/44100_s16le_2.pcm)换成自己的 PCM 文件路径; - 配置 Qt 工程,链接 FFmpeg 的
swresample库; - 编译运行程序,点击界面上的按钮;
- 去指定路径(比如 D 盘)查看转换后的 PCM 文件。
核心总结
- 为啥用线程?→ 避免界面卡死,让 "界面操作" 和 "音频处理" 并行;
- FFmpeg 核心?→
swr_alloc_set_opts(配置参数)、swr_init(初始化)、swr_convert(重采样); - 内存管理?→ FFmpeg 的缓冲区 / 上下文要手动释放,Qt 线程要安全退出;
- 核心流程?→ 读原始 PCM→转格式→写新 PCM。
常见问题
- 运行报错 "找不到 FFmpeg 库"→ 检查 Qt 工程的库链接路径;
- 转换后文件没声音→ 检查原始 PCM 文件路径是否正确、格式参数是否匹配;
- 界面卡死→ 确认音频处理逻辑在独立线程中执行。
如果这篇文章帮到你,欢迎点赞收藏~有问题评论区交流,小白也能学会音频重采样✨