前言
无损即完整保真未做处理的数据文件,文件比较大,未经过压缩,开发的流程相对简单。(下一篇会分享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);
}