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

发布时间:2026/10/20 10:43:58
【QT window】ffmpeg实现录音功能之无损格式--PCM
前言无损即完整保真未做处理的数据文件文件比较大未经过压缩开发的流程相对简单。下一篇会分享AAC-mp4格式的录音功能功能讲解UI只有两个按钮功能比较简单这里想要是展示功能的流程方便与其他压缩格式的进行对比。环境搭建我的上一篇博文包含有传送门【QT window】ffmpeg实现手动绘图裁剪、缩放、拍照显示fps等功能本篇涉及的ffmpeg接口类别函数作用说明所属模块设备注册avdevice_register_all()注册所有输入/输出设备如 dshow、v4l2、alsa 等libavdevice输入格式查找av_find_input_format(dshow)获取指定名称的输入格式如 DirectShowlibavformat打开输入设备avformat_open_input(inputCtx, device, iformat, opts)打开音频/视频捕获设备如麦克风libavformat获取流信息avformat_find_stream_info(inputCtx, opts)读取并解析设备/文件中的流元数据libavformat流操作inputCtx-nb_streamsinputCtx-streams[i]-codecpar-codec_type遍历流识别音频流AVMEDIA_TYPE_AUDIOlibavformat输出上下文创建avformat_alloc_output_context2(outputCtx, NULL, NULL, output.wav)根据文件扩展名自动选择输出格式如 WAVlibavformat输出流创建avformat_new_stream(outputCtx, NULL)为输出上下文添加新流libavformat编解码参数复制avcodec_parameters_copy(dst, src)将输入音频参数采样率、通道数等复制到输出流libavcodec文件 IO 打开avio_open(outputCtx-pb, output.wav, AVIO_FLAG_WRITE)打开文件用于写入底层 I/Olibavformat写入容器头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_NOFILElibavformat释放上下文avformat_free_context(outputCtx)释放输出上下文及其内部流、参数等libavformat过程讲解1、初始化1.1、注册所有设备avdevice_register_all()注册所有设备下文有专门的打开音频设备和判断音频数据的环节不需要特意的只检测音频设备。因为avdevice_register_all执行很快ffmpeg未提供专门只检测某类设备的接口。2、打开输入设备2.1、找到设备av_find_input_format(dshow)获取指定名称的输入格式如 DirectShow2.2、打开设备avformat_open_input(inputCtx, device, iformat, opts)通过device指定的音频路径/描述打开音频设备inputCtx指向AVFormatContext*的指针。函数会分配内存并赋值。device设备路径或文件路径如audio麦克风或你的 GUID 字符串iformat指定输入格式(av_find_input_format的返回值)opts 可选字典参数如设置采样率、缓冲区大小等通常传NULL2.3、获取设备流信息avformat_find_stream_info(inputCtx, opts)读取并解析设备/文件中的流元数据inputCtx指向AVFormatContext*的指针opts每个流的选项字典通常传NULL3、查找音频流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) 根据文件扩展名自动选择输出格式如 WAVoutputCtx输出参数分配好的AVFormatContext*oformat指定输出格式可为NULLformat_name格式名如wav可为NULLfilename文件名用于自动推断格式本篇为PCM格式文件名后缀名为.wav4.2、创建新的输出流avformat_new_stream(outputCtx, c) 为输出上下文添加新流outputCtx输出参数分配好的AVFormatContext*c编码器转封装时传NULL返回新分配的AVStream*其codecpar需后续填充见4.34.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/OoutputCtx-pb分配AVIOContext*并赋值给outputCtx-pboutputfilename 文件路径如output.wavflags打开模式如AVIO_FLAG_WRITE6、写入文件头6.1、写入WAV文件头部信息avformat_write_header(outputCtx, options)outputCtx输出参数分配好的AVFormatContext*options: 写入选项通常NULL7、录音主循环7.1、初始化升级包av_init_packet(AVPacket *pkt)初始化AVPacket设 dataNULL, size07.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::atomicbool 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(audiodevice_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); }