【QT window】ffmpeg实现录音功能之无损格式--PCM

前言

无损即完整保真未做处理的数据文件,文件比较大,未经过压缩,开发的流程相对简单。(下一篇会分享AAC-mp4格式的录音功能)

功能讲解

UI只有两个按钮,功能比较简单,这里想要是展示功能的流程,方便与其他压缩格式的进行对比。

环境搭建

我的上一篇博文包含有,传送门:

【QT window】ffmpeg实现手动绘图(裁剪)、缩放、拍照,显示fps等功能

本篇涉及的ffmpeg接口

类别 函数 作用说明 所属模块
设备注册 avdevice_register_all() 注册所有输入/输出设备(如 dshow、v4l2、alsa 等) libavdevice
输入格式查找 av_find_input_format("dshow") 获取指定名称的输入格式(如 DirectShow) libavformat
打开输入设备 avformat_open_input(&inputCtx, device, iformat, opts) 打开音频/视频捕获设备(如麦克风) libavformat
获取流信息 avformat_find_stream_info(inputCtx, opts) 读取并解析设备/文件中的流元数据 libavformat
流操作 inputCtx->nb_streams inputCtx->streams[i]->codecpar->codec_type 遍历流,识别音频流(AVMEDIA_TYPE_AUDIO libavformat
输出上下文创建 avformat_alloc_output_context2(&outputCtx, NULL, NULL, "output.wav") 根据文件扩展名自动选择输出格式(如 WAV) libavformat
输出流创建 avformat_new_stream(outputCtx, NULL) 为输出上下文添加新流 libavformat
编解码参数复制 avcodec_parameters_copy(dst, src) 将输入音频参数(采样率、通道数等)复制到输出流 libavcodec
文件 IO 打开 avio_open(&outputCtx->pb, "output.wav", AVIO_FLAG_WRITE) 打开文件用于写入(底层 I/O) libavformat
写入容器头 avformat_write_header(outputCtx, NULL) 写入 WAV/MP4 等容器的文件头 libavformat
数据包处理 av_init_packet(&pkt) av_read_frame(inputCtx, &pkt) av_packet_unref(&pkt) 初始化、读取、释放 AVPacket(原始压缩帧) libavcodec / libavformat
写入数据帧 av_interleaved_write_frame(outputCtx, &pkt) 将数据包写入输出文件(自动处理交错) libavformat
写入容器尾 av_write_trailer(outputCtx) 写入文件尾部(如更新文件大小) libavformat
资源释放 avformat_close_input(&inputCtx) 关闭输入设备并释放资源 libavformat
文件 IO 关闭 avio_closep(&outputCtx->pb) 安全关闭文件句柄(仅当格式非 AVFMT_NOFILE libavformat
释放上下文 avformat_free_context(outputCtx) 释放输出上下文及其内部流、参数等 libavformat

过程讲解

1、初始化

1.1、注册所有设备

avdevice_register_all(),注册所有设备,下文有专门的打开音频设备和判断音频数据的环节,不需要特意的只检测音频设备。

因为avdevice_register_all执行很快,ffmpeg未提供专门只检测某类设备的接口。

2、打开输入设备

2.1、找到设备

av_find_input_format("dshow"),获取指定名称的输入格式(如 DirectShow)

2.2、打开设备

avformat_open_input(&inputCtx, device, iformat, opts),通过device指定的音频路径/描述打开音频设备

  • inputCtx:指向 AVFormatContext* 的指针。函数会分配内存并赋值。
  • device:设备路径或文件路径(如 "audio=麦克风" 或你的 GUID 字符串);
  • iformat:指定输入格式(av_find_input_format的返回值);
  • opts: 可选字典参数(如设置采样率、缓冲区大小等),通常传 NULL;
2.3、获取设备流信息

avformat_find_stream_info(inputCtx, opts),读取并解析设备/文件中的流元数据

  • inputCtx:指向 AVFormatContext* 的指针
  • opts:每个流的选项字典(通常传 NULL

3、查找音频流

3.1、遍历所有可用流

通过inputCtx->nb_streams遍历所有的可用流

复制代码
for (uint i = 0; i < inputCtx->nb_streams; i++) {
    ....
}
3.2、识别音频流类型

通过AVMEDIA_TYPE_AUDIO识别出音频流,并且记录音频流索引audioIndex,在数据采集中用到

复制代码
if (inputCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
    audioIndex = i; break;//记录音频流索引供后续使用
}

4、准备输出上下文

4.1、创建WAV格式的输出上下文

avformat_alloc_output_context2(&outputCtx, oformat, NULL, filename) 根据文件扩展名自动选择输出格式(如 WAV)

  • outputCtx:输出参数,分配好的 AVFormatContext*
  • oformat:指定输出格式(可为 NULL
  • format_name:格式名(如 "wav"),可为 NULL
  • filename:文件名(用于自动推断格式,本篇为PCM格式,文件名后缀名为 ".wav"
4.2、创建新的输出流

avformat_new_stream(outputCtx, c) 为输出上下文添加新流

  • outputCtx:输出参数,分配好的 AVFormatContext*
  • c:编码器(转封装时传 NULL

返回新分配的 AVStream*,其 codecpar 需后续填充(见4.3)

4.3、复制输入音频参数到输出流

avcodec_parameters_copy(dst, src)将输入音频参数(采样率、通道数等)复制到输出流

  • dst:输出流的codecpar(4.2返回值的codecpar)
  • src:输入流的codecpar(inputCtx->streams[audioIndex]->codecpar)

5、文件IO建立

5.1、打开输出文件

avio_open(&outputCtx->pb, outputfilename, flags) 打开文件用于写入(底层 I/O)

  • &outputCtx->pb:分配 AVIOContext* 并赋值给 outputCtx->pb
  • outputfilename: 文件路径(如 "output.wav"
  • flags:打开模式,如 AVIO_FLAG_WRITE

6、写入文件头

6.1、写入WAV文件头部信息

avformat_write_header(outputCtx, options),

  • outputCtx:输出参数,分配好的 AVFormatContext*
  • options: 写入选项(通常 NULL

7、录音主循环

7.1、初始化升级包

av_init_packet(AVPacket *pkt),初始化 AVPacket(设 data=NULL, size=0)

7.2、循环读取音频数据包

av_read_frame(inputCtx, &pkt),从inputCtx读取数据

7.3、过滤出音频流数据

通过判断是否为3.2中识别到的音频索引,来指定读取音频数据

复制代码
if (pkt.stream_index == audioIndex) {

}                                 
7.4、写入到输出文件

av_interleaved_write_frame(outputCtx, &pkt),将数据包写入输出文件

7.5、释放数据包资源

av_packet_unref(&pkt),释放 AVPacket(原始压缩帧)

7.6、此循环持续直到用户停止录制

通过一个bool值作为开关,让程序跳出while循环即可

8、结束录制

8.1、写入文件尾部信息

av_write_trailer(outputCtx),ffmpeg根据输出容器格式(如 WAV、MP4、FLV 等)自动构建并写入该格式所需的"尾部"(trailer)数据,包括音频流的必要结束信息。

8.2、释放输入设备资源

avformat_close_input(&inputCtx),关闭输入设备并释放资源

8.3、关闭文件和设备

avio_closep(&outputCtx->pb) ,安全关闭文件句柄

8.4、释放输出上下文

avformat_free_context(outputCtx),释放输出上下文及其内部流、参数等

流程图

源码

复制代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include <QThread>
#include <atomic>

extern "C" {
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
}

class RecorderThread : public QThread {
    Q_OBJECT
public:
    explicit RecorderThread(QObject *parent = nullptr);
    virtual ~RecorderThread() = default;  // 添加这行
    void run() override;
    void stop();
    void cleanup(AVFormatContext*& inputCtx,AVFormatContext*& outputCtx);

private:
    std::atomic<bool> m_stop{false};
};

class MainWindow : public QMainWindow {
    Q_OBJECT

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

private slots:
    void onStartClicked();
    void onStopClicked();

private:
    QPushButton *startBtn;
    QPushButton *stopBtn;
    RecorderThread *recorderThread = nullptr;
    bool isRecording = false;
};

#endif // MAINWINDOW_H

//mainwindow.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QWidget>
#include <QApplication>
#include <QDebug>

// 初始化 FFmpeg(全局一次)
static void initFFmpeg() {
    //1.初始化FFmpeg‌
    avdevice_register_all();//注册所有输入设备
}

RecorderThread::RecorderThread(QObject *parent)
    : QThread(parent) {}

void RecorderThread::stop() {
    m_stop = true;
    wait(); // 等待线程结束
}

void RecorderThread::cleanup(AVFormatContext*& inputCtx,AVFormatContext*& outputCtx){
    //8.2、释放输入设备资源
    if (inputCtx) avformat_close_input(&inputCtx);//释放输入设备资源
    if (outputCtx) {
        if (!(outputCtx->oformat->flags & AVFMT_NOFILE))
            //8.3、关闭文件和设备
            avio_closep(&outputCtx->pb);//关闭文件IO
        //8.4、释放输出上下文
        avformat_free_context(outputCtx);//清理输出上下文
    }
}

void RecorderThread::run() {
    const char* deviceName = R"(audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{60D84350-EE0D-4AD4-BDA1-9714DE1AB548})";
    const char* outputFilename = "output.wav";

    AVFormatContext* inputCtx = nullptr;
    AVFormatContext* outputCtx = nullptr;
    //‌2.打开输入设备
    //2.1、找到DirectShow输入格式
    const AVInputFormat* iformat = av_find_input_format("dshow");//查找DirectShow输入格式
    //2.2、打开指定的音频设备(麦克风)
    int ret = avformat_open_input(&inputCtx, deviceName, iformat, nullptr);
    if (ret < 0) { qWarning() << "Open device failed"; cleanup(inputCtx,outputCtx); return; }
    //2.3、获取设备流信息
    ret = avformat_find_stream_info(inputCtx, nullptr);
    if (ret < 0) { qWarning() << "Find stream failed"; cleanup(inputCtx,outputCtx); return; }

    int audioIndex = -1;
    //3、查找音频流
    //3.1、遍历流查找音频流 (AVMEDIA_TYPE_AUDIO)
    for (uint i = 0; i < inputCtx->nb_streams; i++) {
        //3.2、识别音频流类型
        if (inputCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audioIndex = i; break;//记录音频
        }
    }
    if (audioIndex == -1) { qWarning() << "No audio stream"; cleanup(inputCtx,outputCtx); return; }

    //4.‌输出文件准备
    //4.1、创建 WAV 输出上下文
    ret = avformat_alloc_output_context2(&outputCtx, nullptr, nullptr, outputFilename);
    if (ret < 0) { qWarning() << "Alloc output failed"; cleanup(inputCtx,outputCtx); return; }
    //4.2、创建新的输出流
    AVStream* outStream = avformat_new_stream(outputCtx, nullptr);
    //4.3、复制输入音频参数到输出流
    avcodec_parameters_copy(outStream->codecpar, inputCtx->streams[audioIndex]->codecpar);

    //5.‌文件IO建立
    if (!(outputCtx->oformat->flags & AVFMT_NOFILE)) {
        //5.1打开输出文件
        ret = avio_open(&outputCtx->pb, outputFilename, AVIO_FLAG_WRITE);
        if (ret < 0) { qWarning() << "Open output file failed"; cleanup(inputCtx,outputCtx); return; }
    }
    //6、写入文件头
    //6.1、写入WAV文件头,使用 outputCtx 中已配置好的流信息来生成正确的头部
    avformat_write_header(outputCtx, nullptr);

    //7.数据采集和录音写入循环
    AVPacket pkt;
    int count = 0;
    while (!m_stop.load()) {
        //7.1、初始化升级包
        av_init_packet(&pkt);
        pkt.data = nullptr;
        pkt.size = 0;
        //7.2、循环读取音频数据包
        ret = av_read_frame(inputCtx, &pkt);// 从设备读取音频数据包AVPacket
        if (ret < 0) break;
        //7.3、过滤出音频流数据
        if (pkt.stream_index == audioIndex) {
            pkt.stream_index = 0;// 重置流索引
            //7.4、写入到输出文件
            av_interleaved_write_frame(outputCtx, &pkt);//写入WAV文件
            count++;
            if (count % 100 == 0) qDebug() << "Wrote packets:" << count;
        }
        //7.5、释放数据包资源
        av_packet_unref(&pkt);
    }
    //8、结束录制
    //8.1、写入文件尾部信息
    av_write_trailer(outputCtx);//写入文件尾
    cleanup(inputCtx,outputCtx);
    qDebug() << "Recording finished. Total packets:" << count;
}

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent) {
    initFFmpeg();

    QWidget *central = new QWidget(this);
    setCentralWidget(central);

    startBtn = new QPushButton("开始录音", this);
    stopBtn = new QPushButton("结束录音", this);
    stopBtn->setEnabled(false);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(startBtn);
    layout->addWidget(stopBtn);
    central->setLayout(layout);

    connect(startBtn, &QPushButton::clicked, this, &MainWindow::onStartClicked);
    connect(stopBtn, &QPushButton::clicked, this, &MainWindow::onStopClicked);
}

MainWindow::~MainWindow() {
    if (recorderThread) {
        recorderThread->stop();
        delete recorderThread;
    }
}

void MainWindow::onStartClicked() {
    if (isRecording) return;
    recorderThread = new RecorderThread(this);
    recorderThread->start();
    isRecording = true;
    startBtn->setEnabled(false);
    stopBtn->setEnabled(true);
}

void MainWindow::onStopClicked() {
    if (!isRecording) return;
    if (recorderThread) {
        recorderThread->stop();
        delete recorderThread;
        recorderThread = nullptr;
    }
    isRecording = false;
    startBtn->setEnabled(true);
    stopBtn->setEnabled(false);
}
相关推荐
止礼6 小时前
FFmpeg8.0.1 源代码的深入分析
ffmpeg
小曾同学.com7 小时前
音视频中的“透传”与“DTS音频”
ffmpeg·音视频·透传·dts
vivo互联网技术7 小时前
数字人动画云端渲染方案
前端·ffmpeg·puppeteer·web3d
止礼8 小时前
FFmpeg8.0.1 编解码流程
ffmpeg
qs70169 小时前
c直接调用FFmpeg命令无法执行问题
c语言·开发语言·ffmpeg
止礼9 小时前
FFmpeg8.0.1 Mac环境 CMake本地调试配置
macos·ffmpeg
简鹿视频1 天前
视频转mp4格式具体作步骤
ffmpeg·php·音视频·实时音视频