Qt+FFmpeg 实现音频重采样

前言

本文面向编程小白,用最通俗的语言讲解如何基于 Qt+FFmpeg 实现音频重采样(简单说:把音频文件的采样率 / 采样格式 / 声道数转换成目标规格)。全程避开复杂术语,只讲核心逻辑,代码可直接运行~

3 个核心概念

概念 通俗解释
采样率 每秒采集的音频 "样本数",比如 44100Hz = 每秒采 44100 个样本,数值越高音质越细腻
采样格式 每个音频样本的 "存储格式",比如 S16(16 位整数)、F32(32 位浮点)
声道数 单声道(MONO,1 个声道)、立体声(STEREO,2 个声道)
音频重采样 把音频的采样率 / 采样格式 / 声道数,转换成目标规格的过程(本文核心)

整体流程

  1. 用 Qt 做一个带按钮的简单界面;
  2. 点击按钮后,启动一个独立线程(避免界面卡死);
  3. 线程中调用 FFmpeg 的音频重采样接口,把指定的 PCM 音频文件转换成新格式;
  4. 生成转换后的新音频文件。

环境准备

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

核心逻辑

  1. 初始化变量:定义输入 / 输出文件、缓冲区参数;
  2. 创建重采样上下文:告诉 FFmpeg "输入格式" 和 "输出格式";
  3. 创建缓冲区:用来存放读取的音频数据和转换后的音频数据;
  4. 打开文件:准备读原始文件、写新文件;
  5. 循环处理:读一点数据→转格式→写一点数据,直到文件读完;
  6. 释放资源:FFmpeg 的资源要手动释放,避免内存泄漏。

运行步骤

  1. 把代码中的文件路径(比如D:/44100_s16le_2.pcm)换成自己的 PCM 文件路径;
  2. 配置 Qt 工程,链接 FFmpeg 的swresample库;
  3. 编译运行程序,点击界面上的按钮;
  4. 去指定路径(比如 D 盘)查看转换后的 PCM 文件。

核心总结

  1. 为啥用线程?→ 避免界面卡死,让 "界面操作" 和 "音频处理" 并行;
  2. FFmpeg 核心?→ swr_alloc_set_opts(配置参数)、swr_init(初始化)、swr_convert(重采样);
  3. 内存管理?→ FFmpeg 的缓冲区 / 上下文要手动释放,Qt 线程要安全退出;
  4. 核心流程?→ 读原始 PCM→转格式→写新 PCM。

常见问题

  1. 运行报错 "找不到 FFmpeg 库"→ 检查 Qt 工程的库链接路径;
  2. 转换后文件没声音→ 检查原始 PCM 文件路径是否正确、格式参数是否匹配;
  3. 界面卡死→ 确认音频处理逻辑在独立线程中执行。

如果这篇文章帮到你,欢迎点赞收藏~有问题评论区交流,小白也能学会音频重采样✨

相关推荐
专注echarts研发20年4 小时前
如何实现 QLabel 的 Click 事件?Qt 富文本超链接优雅方案
开发语言·qt
发哥来了5 小时前
主流AI视频生成模型商用化能力评测:三大核心维度对比分析
大数据·人工智能·音视频
小小码农Come on5 小时前
QT控件之QTabWidget使用
开发语言·qt
Li_Zhi_Yao5 小时前
linux下qt快速搭建环境
linux·运维·qt
发哥来了5 小时前
《AI图生视频技术深度剖析:原理、应用与发展趋势》
人工智能·音视频
从此不归路5 小时前
Qt5 进阶【12】JSON/XML 数据协议处理:与后端/配置文件的对接
xml·开发语言·c++·qt·json
艾莉丝努力练剑5 小时前
【QT】信号与槽
linux·开发语言·c++·人工智能·windows·qt·qt5
轩情吖5 小时前
Qt的窗口(二)
开发语言·c++·qt·qdialog·对话框·桌面级开发
誰能久伴不乏17 小时前
【Qt实战】工业级多线程串口通信:从底层协议设计到完美收发闭环
linux·c++·qt