【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);
}
相关推荐
七夜zippoe1 天前
Spring Data JPA原理与实战 Repository接口的魔法揭秘
java·ffmpeg·事务·jpa·repository
Benny的老巢1 天前
n8n工作流中FFmpeg 视频截取失败排查:文件路径和参数顺序错误解决方案
chrome·ffmpeg·音视频
RockWang.2 天前
【配置】FFmpeg配置环境ubuntu踩坑记录。
ffmpeg
王者鳜錸2 天前
Java使用FFmpeg获取音频文件时长:完整实现与原理详解
java·开发语言·ffmpeg·音频时长
桃杬2 天前
用现代 C++ 封装 FFmpeg:从摄像头采集到 H.264 编码的完整实践
c++·ffmpeg·h.264
cvcode_study3 天前
FFmpeg 工具基础
ffmpeg
1nv1s1ble3 天前
记录一个`ffmpeg`的`swscale`库crash的例子
ffmpeg
CodeOfCC3 天前
C++ 实现ffmpeg解析hls fmp4 EXT-X-DISCONTINUITY并支持定位
开发语言·c++·ffmpeg·音视频
Java程序员 拥抱ai3 天前
SpringBoot + FFmpeg + Redis:视频转码、截图、水印异步处理平台搭建
spring boot·redis·ffmpeg
给算法爸爸上香4 天前
yolo tensorrt视频流检测软解码和硬解码
yolo·ffmpeg·视频编解码·tensorrt·nvcodec