Qt+FFmpeg 实现录音程序(pcm转wav)

本文会带大家从零理解一个基于 Qt (做界面)+ FFmpeg(采集音频)的简易录音程序,核心功能是:点击界面按钮开始 / 结束录音,实时显示录音时长,最终将麦克风采集的音频保存为 WAV 文件。即使你是刚接触 Qt 和 FFmpeg 的小白,也能看懂每一行核心代码的作用~

先搞懂基础概念

  • PCM:原始音频数据(无压缩、无封装),可以理解为 "音频裸数据",直接播放不了,需要封装成 WAV/MP3 等格式。
  • WAV:一种音频格式,本质是 "WAV 文件头 + PCM 数据",播放器能通过文件头识别音频参数(采样率、声道数等),再播放 PCM 数据。
  • 采样率:每秒采集的音频样本数(比如 44100Hz,就是每秒采 44100 次),数值越高音质越好。
  • 位深度:每个音频样本的二进制位数(比如 16 位、32 位),位数越高音质越好。
  • 声道数:单声道(1)/ 立体声(2)等。
  • Qt 线程:避免录音的 "耗时操作" 卡死界面,所以把录音逻辑放到子线程中。
文件 核心作用
ffmpegs.h/ffmpegs.cpp 定义 WAV 文件头结构、封装 PCM 转 WAV 的工具函数
audiothread.h/audiothread.cpp 录音线程类,用 FFmpeg 采集麦克风音频,写入 WAV 文件
mainwindow.h/mainwindow.cpp Qt 主窗口(界面),处理 "开始 / 结束录音" 按钮、显示录音时长
main.cpp 程序入口,初始化 FFmpeg 设备、启动 Qt 界面

1. 基础封装:WAV 文件头与 PCM 转 WAV(ffmpegs.h/ffmpegs.cpp)

ffmpegs.h:定义 WAV 文件头

WAV 文件必须有固定格式的 44 字节头,里面存着音频的关键参数,先看结构体定义:

cpp 复制代码
#ifndef FFMPEGS_H
#define FFMPEGS_H

#include <stdint.h>

// 音频格式标记:1=PCM(整数),3=浮点PCM
#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3

// WAV文件头(固定44字节)
typedef struct {
    // 1. RIFF块:标识文件类型
    uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'}; // 固定值"RIFF"
    uint32_t riffChunkDataSize; // 文件总长度-8(RIFF块的总大小)

    // 2. 格式标识:固定"WAVE"
    uint8_t format[4] = {'W', 'A', 'V', 'E'};

    // 3. fmt块:音频参数
    uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '}; // 固定"fmt "
    uint32_t fmtChunkDataSize = 16; // PCM格式固定16
    uint16_t audioFormat = AUDIO_FORMAT_PCM; // 音频编码(1=PCM,3=浮点)
    uint16_t numChannels; // 声道数(1=单声道,2=立体声)
    uint32_t sampleRate; // 采样率(比如44100)
    uint32_t byteRate; // 字节率=采样率×blockAlign
    uint16_t blockAlign; // 一个样本的总字节数=位深度×声道数/8
    uint16_t bitsPerSample; // 位深度(比如16、32)

    // 4. data块:PCM数据区
    uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'}; // 固定"data"
    uint32_t dataChunkDataSize; // PCM数据的总长度
} WAVHeader;

// 工具类:封装PCM转WAV的静态函数
class FFmpegs {
public:
    FFmpegs();
    static void pcm2wav(WAVHeader &header, const char *pcmFilename, const char *wavFilename);
};

#endif // FFMPEGS_H

解读 :这个结构体就是 WAV 文件的 "说明书",播放器靠这些参数解析 PCM 数据。比sampleRate告诉播放器 "每秒要播放 44100 个样本",bitsPerSample告诉播放器 "每个样本用 32 位表示"。

ffmpegs.cpp:PCM 转 WAV 的实现

核心逻辑:先写 WAV 文件头,再把 PCM 裸数据追加到文件里,同时计算文件头的关键参数:

cpp 复制代码
#include "ffmpegs.h"
#include <QFile>
#include <QDebug>

FFmpegs::FFmpegs() {}

void FFmpegs::pcm2wav(WAVHeader &header, const char *pcmFilename, const char *wavFilename) {
    // 1. 计算音频参数(字节率、块对齐)
    header.blockAlign = header.bitsPerSample * header.numChannels >> 3; // 等价于 /8
    header.byteRate = header.sampleRate * header.blockAlign;

    // 2. 打开PCM文件(读裸数据)
    QFile pcmFile(pcmFilename);
    if (!pcmFile.open(QFile::ReadOnly)) {
        qDebug() << "文件打开失败" << pcmFilename;
        return;
    }
    // PCM数据总长度 = PCM文件的大小
    header.dataChunkDataSize = pcmFile.size();
    // RIFF块大小 = PCM数据长度 + WAV头长度 - 8(减去riffChunkId和riffChunkDataSize的长度)
    header.riffChunkDataSize = header.dataChunkDataSize + sizeof (WAVHeader) - 8;

    // 3. 打开WAV文件(写数据)
    QFile wavFile(wavFilename);
    if (!wavFile.open(QFile::WriteOnly)) {
        qDebug() << "文件打开失败" << wavFilename;
        pcmFile.close();
        return;
    }

    // 4. 先写WAV文件头
    wavFile.write((const char *) &header, sizeof (WAVHeader));

    // 5. 循环读取PCM数据,写入WAV文件
    char buf[1024];
    int size;
    while ((size = pcmFile.read(buf, sizeof (buf))) > 0) {
        wavFile.write(buf, size);
    }

    // 6. 关闭文件
    pcmFile.close();
    wavFile.close();
}

解读:这个函数的作用是 "给裸的 PCM 数据套上 WAV 的壳",输入一个 PCM 文件,输出一个能直接播放的 WAV 文件。

2. 核心逻辑:录音线程(audiothread.h/audiothread.cpp)

录音操作是 "耗时操作",如果直接在界面线程执行,界面会卡死,所以我们把录音逻辑放到 Qt 的QThread子线程中。

audiothread.h:线程类声明
cpp 复制代码
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H

#include <QThread>

class AudioThread : public QThread {
    Q_OBJECT
private:
    void run(); // 线程启动后执行的函数(核心录音逻辑)
    bool _stop = false;

public:
    explicit AudioThread(QObject *parent = nullptr);
    ~AudioThread();
    void setStop(bool stop);
signals:
    void timeChanged(unsigned long long ms); // 发送录音时长的信号(给界面显示)
};

#endif // AUDIOTHREAD_H

解读run()是线程的 "入口",线程启动后会自动执行这个函数;timeChanged信号用来给界面传录音时长(比如录了 5 秒,就发 5000ms)。

audiothread.cpp:录音核心逻辑

核心步骤:初始化 FFmpeg 设备 → 打开麦克风 → 循环采集音频数据 → 写入 WAV 文件 → 实时计算录音时长 → 结束后修正 WAV 文件头。

cpp 复制代码
#include "audiothread.h"
#include <QDebug>
#include <QFile>
#include <QDateTime>
#include "ffmpegs.h"

// 引入FFmpeg的C语言接口(必须extern "C",否则编译报错)
extern "C" {
#include <libavdevice/avdevice.h> // 设备相关
#include <libavformat/avformat.h> // 格式相关
#include <libavutil/avutil.h>     // 工具函数(错误处理)
}

// 跨平台配置:Windows用dshow采集麦克风,macOS用avfoundation
#ifdef Q_OS_WIN
#define FMT_NAME "dshow" // Windows音频采集格式
#define DEVICE_NAME "audio=麦克风阵列 (英特尔® 智音技术)" // 你的麦克风设备名(需修改!)
#define FILEPATH "D:/" // WAV文件保存路径
#else
#define FMT_NAME "avfoundation" // macOS/iOS采集格式
#define DEVICE_NAME ":0" // macOS默认麦克风
#define FILEPATH "/Users/mj/Desktop/"
#endif

// 构造函数:线程销毁时自动回收资源
AudioThread::AudioThread(QObject *parent) : QThread(parent) {
    connect(this, &AudioThread::finished, this, &AudioThread::deleteLater);
}

// 析构函数:确保线程安全退出
AudioThread::~AudioThread() {
    disconnect(); // 断开信号槽
    requestInterruption(); // 请求中断线程
    quit(); // 退出事件循环
    wait(); // 等待线程结束
    qDebug() << this << "析构(内存被回收)";
}

// 线程核心逻辑:录音的所有操作都在这里
void AudioThread::run() {
    qDebug() << this << "开始录音----------";

    // 1. 获取FFmpeg输入格式(Windows=dshow,macOS=avfoundation)
    AVInputFormat *fmt = av_find_input_format(FMT_NAME);
    if (!fmt) {
        qDebug() << "获取输入格式失败" << FMT_NAME;
        return;
    }

    // 2. 初始化FFmpeg格式上下文(操作设备的"句柄")
    AVFormatContext *ctx = nullptr;
    // 打开麦克风设备
    int ret = avformat_open_input(&ctx, DEVICE_NAME, fmt, nullptr);
    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf)); // 解析FFmpeg错误码
        qDebug() << "打开麦克风失败" << errbuf;
        return;
    }

    // 3. 准备WAV文件(文件名=当前时间,避免重复)
    QString filename = FILEPATH + QDateTime::currentDateTime().toString("MM_dd_HH_mm_ss") + ".wav";
    QFile file(filename);
    if (!file.open(QFile::WriteOnly)) {
        qDebug() << "打开WAV文件失败" << filename;
        avformat_close_input(&ctx); // 失败时关闭设备
        return;
    }

    // 4. 获取音频参数(从FFmpeg设备中读取采样率、声道数等)
    AVStream *stream = ctx->streams[0]; // 音频流
    AVCodecParameters *params = stream->codecpar; // 音频参数

    // 初始化WAV文件头
    WAVHeader header;
    header.sampleRate = params->sample_rate; // 采样率(比如44100)
    header.bitsPerSample = av_get_bits_per_sample(params->codec_id); // 位深度
    header.numChannels = params->channels; // 声道数
    // 判断音频格式:浮点PCM还是整数PCM
    if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
        header.audioFormat = AUDIO_FORMAT_FLOAT;
    }
    // 计算字节率、块对齐
    header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
    header.byteRate = header.sampleRate * header.blockAlign;

    // 5. 先写入空的WAV文件头(后续会修正参数)
    file.write((char *) &header, sizeof (WAVHeader));

    // 6. 循环采集音频数据(核心!)
    AVPacket *pkt = av_packet_alloc(); // 音频数据包(存储采集到的PCM数据)
    while (!isInterruptionRequested()) { // 没收到"结束录音"指令就一直采集
        // 从麦克风读取一帧音频数据
        ret = av_read_frame(ctx, pkt);

        if (ret == 0) { // 读取成功
            // 把采集到的PCM数据写入WAV文件
            file.write((const char *) pkt->data, pkt->size);

            // 计算录音时长:总字节数 / 字节率 = 秒数 → 转毫秒
            header.dataChunkDataSize += pkt->size;
            unsigned long long ms = 1000.0 * header.dataChunkDataSize / header.byteRate;
            emit timeChanged(ms); // 发送时长信号(给界面显示)

            av_packet_unref(pkt); // 释放数据包(必须!否则内存泄漏)
        } else if (ret == AVERROR(EAGAIN)) { // 临时没数据,继续等
            continue;
        } else { // 采集出错,退出循环
            char errbuf[1024];
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "采集音频失败" << errbuf;
            break;
        }
    }

    // 7. 修正WAV文件头(关键!之前写的是空值,现在补全)
    // 移动文件指针到dataChunkDataSize的位置,写入真实的PCM数据长度
    file.seek(sizeof (WAVHeader) - sizeof (header.dataChunkDataSize));
    file.write((char *) &header.dataChunkDataSize, sizeof (header.dataChunkDataSize));
    // 移动文件指针到riffChunkDataSize的位置,写入真实的RIFF块大小
    file.seek(sizeof (header.riffChunkId));
    header.riffChunkDataSize = file.size() - 8;
    file.write((char *) &header.riffChunkDataSize, sizeof (header.riffChunkDataSize));

    // 8. 释放资源(必须!否则内存泄漏)
    av_packet_free(&pkt); // 释放数据包
    file.close(); // 关闭WAV文件
    avformat_close_input(&ctx); // 关闭麦克风设备

    qDebug() << this << "录音结束----------";
}

void AudioThread::setStop(bool stop) {
    _stop = stop;
}

重点解读

  • avformat_open_input:打开麦克风设备,FFmpeg 会自动识别设备的音频参数(采样率、位深度等)。
  • av_read_frame:循环读取麦克风的音频数据,每次读一帧,存到AVPacket里。
  • file.write(pkt->data, pkt->size):把采集到的 PCM 数据写入 WAV 文件。
  • timeChanged信号:实时计算录音时长并传给界面,让界面显示 "00:05.1" 这种格式。
  • 最后修正 WAV 文件头:因为采集前不知道 PCM 数据的总长度,所以先写空值,采集结束后再补真实值。

3. 界面逻辑:Qt 主窗口(mainwindow.h/mainwindow.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

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_audioButton_clicked(); // 按钮点击槽函数
    void onTimeChanged(unsigned long long ms); // 接收时长信号的槽函数

private:
    Ui::MainWindow *ui; // 界面控件
    AudioThread *_audioThread = nullptr; // 录音线程指针
};
#endif // MAINWINDOW_H
mainwindow.cpp:界面逻辑实现
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTime>

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
    onTimeChanged(0); // 初始化时长显示(00:00.0)
}

MainWindow::~MainWindow() {
    delete ui;
}

// 更新录音时长显示:把毫秒转成"分:秒.毫秒"格式
void MainWindow::onTimeChanged(unsigned long long ms) {
    QTime time(0, 0, 0, 0);
    QString text = time.addMSecs(ms).toString("mm:ss.z"); // 格式:00:05.1
    ui->timeLabel->setText(text.left(7)); // 只显示前7位(比如00:05.123 → 00:05.1)
}

// "开始/结束录音"按钮点击事件
void MainWindow::on_audioButton_clicked() {
    if (!_audioThread) { // 点击"开始录音"
        // 1. 创建并启动录音线程
        _audioThread = new AudioThread(this);
        _audioThread->start();

        // 2. 连接信号槽:线程的时长信号 → 界面的显示函数
        connect(_audioThread, &AudioThread::timeChanged, this, &MainWindow::onTimeChanged);

        // 3. 线程结束后重置指针、恢复按钮文字
        connect(_audioThread, &AudioThread::finished, [this]() {
            _audioThread = nullptr;
            ui->audioButton->setText("开始录音");
        });

        // 4. 修改按钮文字
        ui->audioButton->setText("结束录音");
    } else { // 点击"结束录音"
        // 中断录音线程
        _audioThread->requestInterruption();
        _audioThread = nullptr;
        // 恢复按钮文字
        ui->audioButton->setText("开始录音");
    }
}

解读

  • on_audioButton_clicked:按钮点击后判断是 "开始" 还是 "结束":
    • 开始:创建录音线程、启动、连接信号槽(线程传时长给界面)。
    • 结束:中断线程、重置指针、恢复按钮文字。
  • onTimeChanged:把线程传来的毫秒数转成 "00:05.1" 这种友好的格式,显示在界面的标签上。

4. 程序入口:main.cpp

cpp 复制代码
#include "mainwindow.h"
#include <QApplication>
#include "ffmpegs.h"

extern "C" {
#include <libavdevice/avdevice.h> // FFmpeg设备
}

int main(int argc, char *argv[]) {
    // 注册FFmpeg所有设备(必须!否则找不到麦克风)
    avdevice_register_all();

    // 启动Qt应用
    QApplication a(argc, argv);
    MainWindow w;
    w.show(); // 显示主窗口
    return a.exec(); // 进入Qt事件循环
}

解读avdevice_register_all()是 FFmpeg 的 "设备注册",必须调用,否则 FFmpeg 不认识麦克风设备;QApplication a是 Qt 程序的核心,负责管理界面、事件循环。

运行前必做的配置

  1. 修改麦克风设备名 :在audiothread.cpp中,DEVICE_NAME要改成你自己的麦克风名称(Windows 用户):

    • 打开命令行,执行ffmpeg -list_devices true -f dshow -i dummy,会列出所有音频设备,复制你的麦克风名称替换即可。
  2. 配置 FFmpeg 环境

    • 下载对应平台的 FFmpeg 库(包含libavdevice/libavformat等)。

    • 在 Qt 工程文件(.pro)中配置 FFmpeg 的头文件路径、库文件路径。

    • 示例.pro 配置(Windows):

      cpp 复制代码
      INCLUDEPATH += D:/ffmpeg/include
      LIBS += -LD:/ffmpeg/lib -lavdevice -lavformat -lavutil -lavcodec
  3. Qt 界面设计

    • 在 Qt Designer 中拖一个按钮(audioButton)、一个标签(timeLabel)。
    • 按钮文字默认 "开始录音",标签默认显示 "00:00.0"。

程序运行流程

  1. 启动程序 → 界面显示 "开始录音" 按钮和 "00:00.0" 时长。
  2. 点击 "开始录音" → 创建录音线程 → FFmpeg 打开麦克风 → 循环采集音频数据 → 写入 WAV 文件 → 实时发送时长信号给界面。
  3. 点击 "结束录音" → 中断线程 → 修正 WAV 文件头 → 关闭麦克风和文件 → 线程销毁 → 按钮恢复 "开始录音"。
  4. 最终在D:/(Windows)或桌面(macOS)生成以当前时间命名的 WAV 文件,可直接用播放器播放。

总结

这个程序的核心是:

  • Qt 线程:避免录音卡死界面。
  • FFmpeg:跨平台采集麦克风的 PCM 音频数据。
  • WAV 封装:把 PCM 裸数据套上 WAV 文件头,变成可播放的音频文件。

重点理解 "线程为什么要用""WAV 文件头的作用""FFmpeg 采集音频的基本步骤",先跑通代码,再逐行拆解,就能快速掌握 Qt+FFmpeg 音频采集的核心逻辑~

相关推荐
喜欢喝果茶.2 小时前
QOverload<参数列表>::of(&函数名)信号槽
开发语言·qt
wjhx2 小时前
QT中对蓝牙权限的申请,整理一下
java·数据库·qt
踏过山河,踏过海2 小时前
【qt-查看对应的依赖的一种方法】
qt·visual studio
C++ 老炮儿的技术栈3 小时前
VS2015 + Qt 实现图形化Hello World(详细步骤)
c语言·开发语言·c++·windows·qt
C++ 老炮儿的技术栈5 小时前
Qt Creator中不写代如何设置 QLabel的颜色
c语言·开发语言·c++·qt·算法
ae_zr21 小时前
QT动态编译应用后,如何快速获取依赖
开发语言·qt
LYOBOYI1231 天前
qml的对象树机制
c++·qt
菜鸟小芯1 天前
Qt Creator 集成开发环境下载安装
开发语言·qt
牵牛老人1 天前
Qt中集成 MQTT 来实现物联网通信:从原理到实战全解析
开发语言·qt·物联网