Qt+FFmpeg 实现 PCM 转 WAV

一、为什么 PCM 要转换为WAV

先记住核心结论:PCM 是裸的音频数据,只有原始数字信号,没有任何 "描述信息";而播放器要播放音频,必须知道「采样率、声道数、位深度」这些关键参数(比如采样率 44100Hz、单声道、32 位浮点)。

就像你拿到一堆散装零件(PCM),不知道它是拼汽车还是拼飞机;而 WAV 文件相当于给 PCM 套了个 "包装盒"(44 字节的文件头),盒子上写清楚了 "零件规格"(采样率、声道数等),播放器一看就知道怎么 "组装" 播放。

简单说:WAV = 44 字节的格式头 + PCM 裸数据,这也是我们实现 PCM 转 WAV 的核心思路。

二、从 0 实现 PCM 转 WAV

我们分 2 部分实现:先写「PCM 转 WAV 的核心函数」,再结合「麦克风采集 PCM + 转 WAV」的完整流程(带界面按钮控制)。

步骤 1:定义 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块:告诉播放器这是WAV文件
    uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'}; // 固定值
    uint32_t riffChunkDataSize; // 文件总大小-8字节(不用算,代码自动算)
    
    // 2. 固定标记:WAVE
    uint8_t format[4] = {'W', 'A', 'V', 'E'};
    
    // 3. fmt块:描述音频格式
    uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '}; // 固定值
    uint32_t fmtChunkDataSize = 16; // PCM格式固定为16
    uint16_t audioFormat = AUDIO_FORMAT_PCM; // 音频格式(PCM/浮点)
    uint16_t numChannels; // 声道数(1=单声道,2=立体声)
    uint32_t sampleRate; // 采样率(比如44100Hz)
    uint32_t byteRate; // 字节率=采样率*声道数*位深度/8(代码自动算)
    uint16_t blockAlign; // 一个样本的字节数=声道数*位深度/8(代码自动算)
    uint16_t bitsPerSample; // 位深度(比如16/32位)
    
    // 4. data块:标记PCM数据开始
    uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'}; // 固定值
    uint32_t dataChunkDataSize; // PCM数据的总大小(代码自动算)
} WAVHeader;

class FFmpegs {
public:
    FFmpegs();
    // PCM转WAV的核心静态函数(直接调用)
    static void pcm2wav(WAVHeader &header,const char *pcmFilename,const char *wavFilename);
};

#endif // FFMPEGS_H

步骤 2:实现 PCM 转 WAV 的核心函数

这个函数做 3 件事:计算 WAV 头参数 → 打开 PCM 文件读数据 → 写入 WAV 头 + PCM 数据到新文件:

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

FFmpegs::FFmpegs() {}

void FFmpegs::pcm2wav(WAVHeader &header,const char *pcmFilename,const char *wavFilename) {
    // 1. 计算WAV头的关键参数(不用记公式,直接用)
    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() << "PCM文件打开失败" << pcmFilename;
        return;
    }
    // 获取PCM数据大小(用于填充WAV头)
    header.dataChunkDataSize = pcmFile.size();
    // 计算RIFF块的大小(总文件大小-8)
    header.riffChunkDataSize = header.dataChunkDataSize + sizeof (WAVHeader) - 8;

    // 3. 打开WAV文件(只写,不存在则创建)
    QFile wavFile(wavFilename);
    if (!wavFile.open(QFile::WriteOnly)) {
        qDebug() << "WAV文件打开失败" << wavFilename;
        pcmFile.close();
        return;
    }

    // 4. 先写入WAV头(44字节)
    wavFile.write((const char *) &header, sizeof (WAVHeader));

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

    // 6. 关闭文件(必做!)
    pcmFile.close();
    wavFile.close();
    qDebug() << "PCM转WAV成功!WAV文件:" << wavFilename;
}

步骤 3:结合麦克风采集 PCM

如果想直接采集麦克风的 PCM 并转 WAV,用 Qt 线程实现(避免卡界面):

(1)音频采集线程头文件
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
(2)音频采集线程实现
cpp 复制代码
#include "audiothread.h"
#include <QDebug>
#include <QFile>
#include <QDateTime>
#include "ffmpegs.h"

// 引入FFmpeg的音频设备头文件(extern "C"必须加,否则编译报错)
extern "C" {
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}

// Windows麦克风配置(小白改这里:换成自己的麦克风名称)
#define FMT_NAME "dshow" // Windows音频采集格式
#define DEVICE_NAME "audio=麦克风阵列 (英特尔® 智音技术)" // 自己的麦克风名
#define FILEPATH "D:/" // 音频文件保存路径

AudioThread::AudioThread(QObject *parent) : QThread(parent) {
    // 线程结束后自动释放内存(小白不用管,固定写法)
    connect(this, &AudioThread::finished,this, &AudioThread::deleteLater);
}

AudioThread::~AudioThread() {
    disconnect();
    requestInterruption(); // 结束线程
    quit();
    wait();
    qDebug() << "音频线程已释放";
}

void AudioThread::run() {
    qDebug() << "开始录音----------";

    // 1. FFmpeg初始化:获取麦克风输入格式
    AVInputFormat *fmt = av_find_input_format(FMT_NAME);
    if (!fmt) {
        qDebug() << "获取麦克风格式失败";
        return;
    }

    // 2. 打开麦克风设备
    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));
        qDebug() << "打开麦克风失败" << errbuf;
        return;
    }

    // 3. 生成文件名(按时间命名,避免重复)
    QString timeStr = QDateTime::currentDateTime().toString("MM_dd_HH_mm_ss");
    QString pcmFile = FILEPATH + timeStr + ".pcm"; // PCM文件
    QString wavFile = FILEPATH + timeStr + ".wav"; // WAV文件

    // 4. 打开文件,准备写入PCM数据
    QFile file(pcmFile);
    if (!file.open(QFile::WriteOnly)) {
        qDebug() << "PCM文件创建失败" << pcmFile;
        avformat_close_input(&ctx);
        return;
    }

    // 5. 循环采集麦克风数据,写入PCM文件
    AVPacket pkt; // FFmpeg数据包(存音频数据)
    while (!isInterruptionRequested()) { // 没收到"结束录音"指令就一直采
        ret = av_read_frame(ctx, &pkt); // 读取麦克风数据
        if (ret == 0) { // 读取成功
            file.write((const char *) pkt.data, pkt.size); // 写入PCM文件
        } else if (ret == AVERROR(EAGAIN)) { // 临时没数据,继续等
            continue;
        } else { // 采集出错,退出
            char errbuf[1024];
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "采集失败" << errbuf;
            break;
        }
    }

    // 6. 采集结束:关闭文件+设备
    file.close();
    avformat_close_input(&ctx);

    // 7. 获取麦克风的音频参数(自动获取,不用手动填!)
    AVStream *stream = ctx->streams[0];
    AVCodecParameters *params = stream->codecpar;
    WAVHeader header;
    header.sampleRate = params->sample_rate; // 采样率(比如44100)
    header.bitsPerSample = av_get_bits_per_sample(params->codec_id); // 位深度
    header.numChannels = params->channels; // 声道数
    // 判断是否是浮点型PCM(自动适配)
    if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
        header.audioFormat = AUDIO_FORMAT_FLOAT;
    }

    // 8. 调用核心函数:PCM转WAV
    FFmpegs::pcm2wav(header, pcmFile.toUtf8().data(), wavFile.toUtf8().data());

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

步骤 4:简单界面

(1)主窗口头文件
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
(2)主窗口实现
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
}

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

// 按钮点击事件:开始/结束录音
void MainWindow::on_audioButton_clicked() {
    if (!_audioThread) { // 还没开始录音
        _audioThread = new AudioThread(this);
        _audioThread->start(); // 启动线程(开始录音)
        // 线程结束后,按钮文字改回"开始录音"
        connect(_audioThread, &AudioThread::finished,[this]() {
            _audioThread = nullptr;
            ui->audioButton->setText("开始录音");
        });
        ui->audioButton->setText("结束录音");
    } else { // 已经在录音,点击结束
        _audioThread->requestInterruption(); // 停止线程
        _audioThread = nullptr;
        ui->audioButton->setText("开始录音");
    }
}

三、总结

  1. PCM 不能播:因为没有格式信息;WAV 能播:因为多了 44 字节的格式头;
  2. 核心逻辑:先算 WAV 头的参数(采样率、声道数等),再把 "头 + PCM 数据" 写入新文件;
  3. 线程采集:避免录音时界面卡死(Qt 的 QThread 必用);
  4. 自动适配参数:不用手动填采样率 / 声道数,从麦克风设备自动获取,更灵活。
相关推荐
訫悦2 小时前
体验在Qt中简单使用C++20的协程
qt·c++20·协程
eWidget3 小时前
Shell循环进阶:break/continue,循环嵌套与优化技巧
运维·开发语言·ffmpeg·运维开发
m0_635647484 小时前
Qt中使用opencv库imread函数读出的图片是空
开发语言·c++·qt·opencv·计算机视觉
少控科技4 小时前
QT新手日记034
开发语言·qt
凯子坚持 c4 小时前
Qt常用控件指南(5)
开发语言·数据库·qt
Knight_AL5 小时前
Java + FFmpeg 实现视频分片合并(生成 list.txt 自动合并)
java·ffmpeg·音视频
C++ 老炮儿的技术栈5 小时前
CMFCEditBrowseCtrl用法一例
c语言·开发语言·c++·windows·qt·visual studio code
掘根5 小时前
【jsonRpc项目】服务端的RpcRouter模块
开发语言·qt
幸福的达哥5 小时前
PyQt5多线程UI更新方法
python·qt·ui