前言
翻看其他博客,基本都是无损PCM,这是最简单的方法,我的上一篇博文(
https://blog.csdn.net/liangyuna8787/article/details/155987199?spm=1001.2014.3001.5502
)有详细讲解及实现代码,本篇介绍更常见的压缩格式mp4的音频录音,整体上多了编码等环节,比PCM无损格式要复杂一些。
功能讲解
延续上一篇博文的风格,方便做比较,流程有很多是相同的,不同的在编码等环节,下面会专门标注出增加(绿色 )和修改(蓝色)的内容。
环境搭建
参照我分享的第一篇ffmpeg库调用的博文,传送门:
【QT window】ffmpeg实现手动绘图(裁剪)、缩放、拍照,显示fps等功能
本篇涉及的ffmpeg接口
| 类别 | 函数名称 | 作用说明 | 所属模块 |
|---|---|---|---|
| 设备注册 | avdevice_register_all() |
注册所有输入/输出设备(如 dshow、v4l2、alsa 等 | libavdevice |
| 输入格式查找 | av_find_input_format(const char *short_name) |
查找指定名称的输入格式(如 "dshow") |
libavformat |
| 打开输入设备 | avformat_open_input(AVFormatContext **, const char *, AVInputFormat *, AVDictionary **) |
打开输入设备或文件,创建并初始化 AVFormatContext |
libavformat |
| 获取流信息 | avformat_find_stream_info(AVFormatContext *, AVDictionary **) |
读取媒体流信息(如采样率、声道数等) | libavformat |
| 编解码器操作 | avcodec_find_encoder(enum AVCodecID id) |
根据编码器 ID(如 AV_CODEC_ID_AAC)查找编码器 |
libavcodec |
| 编解码器操作 | avcodec_alloc_context3(const AVCodec *) |
分配编码器上下文 | libavcodec |
| 编解码器操作 | avcodec_open2(AVCodecContext *, const AVCodec *, AVDictionary **) |
打开编码器,应用配置参数 | libavcodec |
| 编解码器操作 | avcodec_parameters_from_context(AVCodecParameters *, const AVCodecContext *) |
将编码器上下文参数复制到流的 codecpar 中 |
libavcodec |
| 编解码器操作 | avcodec_send_frame(AVCodecContext *, const AVFrame *) |
向编码器发送原始音频帧 | libavcodec |
| 编解码器操作 | avcodec_receive_packet(AVCodecContext *, AVPacket *) |
从编码器接收编码后的数据包 | libavcodec |
| 编解码器操作 | avcodec_free_context(AVCodecContext **) |
释放编码器上下文及其内部资源 | libavcodec |
| 输出文件操作 | avformat_alloc_output_context2(AVFormatContext **, AVOutputFormat *, const char *, const char *) |
创建输出上下文(用于写入 MP4 等容器) | libavformat |
| 输出流创建 | avformat_new_stream(AVFormatContext *, const AVCodec *) |
为输出上下文添加新流 | libavformat |
| 文件 IO 打开 | avio_open(AVIOContext **, const char *, int) |
打开输出文件用于写入 | libavformat (libavio) |
| 文件 IO 关闭 | avio_closep(AVIOContext **) |
安全关闭并置空 AVIOContext |
libavformat (libavio) |
| 写入容器头 | avformat_write_header(AVFormatContext *, AVDictionary **) |
写入容器头部(如 MP4 的 moov box) | libavformat |
| 写入数据帧 | av_interleaved_write_frame(AVFormatContext *, AVPacket *) |
写入交错排列的数据包(自动处理时间戳排序) | libavformat |
| 写入容器尾 | av_write_trailer(AVFormatContext *) |
写入容器尾部信息(如更新 moov) | libavformat |
| 释放上下文 | avformat_free_context(AVFormatContext *) |
释放整个输出/输入上下文 | libavformat |
| 重采样(音频格式转换) | swr_alloc_set_opts2(SwrContext **, ...) |
分配并配置重采样上下文(新版 API) | libswresample |
| 重采样初始化 | swr_init(SwrContext *) |
初始化重采样器 | libswresample |
| 重采样 | swr_get_out_samples(SwrContext *, int in_samples) |
获取重采样后预计输出的样本数 | libswresample |
| 执行重采样 | swr_convert(SwrContext *, uint8_t **, int, const uint8_t **, int) |
执行实际重采样(格式/采样率/布局转换) | libswresample |
| 释放重采样 | swr_free(SwrContext **) |
释放重采样上下文 | libswresample |
| 内存与数据管理 | av_packet_alloc() |
分配 AVPacket 结构体 |
libavcodec |
| 释放数据包资源 | av_packet_unref(AVPacket *) |
解引用并清空 packet 数据 | libavcodec |
| 内存与数据管理 | av_packet_free(AVPacket **) |
释放 AVPacket 及其内部缓冲区 |
libavcodec |
| 内存与数据管理 | av_frame_alloc() |
分配 AVFrame 结构体 |
libavutil |
| 内存与数据管理 | av_frame_get_buffer(AVFrame *, int align) |
为 frame 分配数据缓冲区 | libavutil |
| 内存与数据管理 | av_frame_unref(AVFrame *) |
解引用并清空 frame 数据 | libavutil |
| 内存与数据管理 | av_samples_alloc(uint8_t ***, ..., enum AVSampleFormat, ...) |
分配 planar 音频样本缓冲区 | libavutil |
| 内存与数据管理 | av_freep(void *) |
安全释放内存并置指针为 NULL | libavutil |
| 辅助工具 | av_get_bytes_per_sample(enum AVSampleFormat) |
获取每采样的字节数(如 FLTP 为 4) | libavutil |
| 构建声道布局 | av_channel_layout_from_mask(AVChannelLayout *, uint64_t) |
从位掩码(如 AV_CH_LAYOUT_STEREO)构建声道布局 |
libavutil |
| 复制声道布局 | av_channel_layout_copy(AVChannelLayout *, const AVChannelLayout *) |
复制声道布局结构 | libavutil |
av_packet_rescale_ts(AVPacket *, AVRational src_tb, AVRational dst_tb) |
转换 packet 的时间戳(time base) | libavutil |
|
av_strerror(int errnum, char *errbuf, size_t errbuf_size) |
将 FFmpeg 错误码转为可读字符串 | libavutil |
过程讲解
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;//记录音频流索引供后续使用
}
3.3、若通道布局未指定,手动设置
av_channel_layout_from_mask(AVChannelLayout *, uint64_t),需要手动指定为立体声布局(AV_CH_LAYOUT_STEREO)
4、准备输出上下文
4.1、创建 mp4 输出上下文---change
avformat_alloc_output_context2(&outputCtx, oformat, format_name, filename) 显示指定mp4格式
- outputCtx:输出参数,分配好的
AVFormatContext* - oformat:指定输出格式(可为
NULL) - format_name:"mp4",指定为mp4格式
- filename:文件名(用于自动推断格式,文件名后缀名为
".mp4")
4.2、查找 AAC 编码器---mp4是aac编码的一种-----new
avcodec_find_encoder(AV_CODEC_ID_AAC),指定用AAC编码器
4.3、创建新的输出流-----change
avformat_new_stream(outputCtx, codec) 为输出上下文添加新流
- outputCtx:输出参数,分配好的
AVFormatContext* - codec:编码器,即4.2中返回的aac编码器
4.4、分配编码器上下文,并设置相关参数-----new
avcodec_alloc_context3(codec),alloc即是分配
codec:编码器,即4.2中返回的aac编码器
4.5、打开 AAC 编码器-----new
avcodec_open2(AVCodecContext *codecCtx, const AVCodec *codec, AVDictionary **opt)
- codecCtx:4.4的编码器的上下文
- codec:即4.2中返回的aac编码器
- opt:通常传
NULL
4.6、将编码器上下文参数复制到输出流中-----new
avcodec_parameters_from_context(AVCodecParameters *codecpar, const AVCodecContext *codecCtx)
- codecpar:输出流的codecpar
- codecCtx:4.4的编码器的上下文
5、文件IO建立
5.1、打开输出文件
avio_open(&outputCtx->pb, outputfilename, flags) 打开文件用于写入(底层 I/O)
- &outputCtx->pb:分配
AVIOContext*并赋值给outputCtx->pb - outputfilename: 文件路径(如
"output.mp4") - flags:打开模式,如
AVIO_FLAG_WRITE
6、写入文件头及初始化重采样器
6.1、写入文件头部信息
avformat_write_header(outputCtx, options),
- outputCtx:输出参数,分配好的
AVFormatContext* options: 写入选项(通常NULL)
6.2、配置并分配 SwrContext-----new
swr_alloc_set_opts2(SwrContext **, ...),设置采样器的上下文
6.3、初始化重采样器-----new
swr_init(SwrContext **)
7、录音主循环
7.1、读原始包
av_read_frame(inputCtx, &pkt),从inputCtx读取数据
7.2、过滤出音频流数据
pkt.stream_index == audioIndex
7.3 音频处理-----new
7.3.1、计算样本数、分配缓冲区、拷贝数据
av_get_bytes_per_sample
7.3.2、设置输入帧
av_channel_layout_copy
7.3.3、计算重采样后样本数
swr_get_out_samples
7.3.4、分配指针数组(每个通道一个指针)
calloc
7.3.5、分配实际音频数据缓冲区
av_samples_alloc
7.4、执行重采样---new
swr_convert
7.5、拆分成标准帧并编码
7.5.1、设置输出帧的基本属性
av_channel_layout_copy
7.5.2、为 outFrame 分配内存缓冲区
av_frame_get_buffer
7.5.3、计算单个采样的字节数
av_get_bytes_per_sample
7.5.4、拷贝 planar 格式数据
每个通道独立存储,需逐通道 memcpy
7.5.5、将构建好的音频帧送入 AAC 编码器
avcodec_send_frame
7.5.6、循环接收编码后的数据包
avcodec_receive_packet
7.6、写入到输出文件
av_interleaved_write_frame(outputCtx, &pkt),将数据包写入输出文件
7.7、释放数据包资源
av_packet_unref(&pkt),释放 AVPacket(原始压缩帧)
7.8、此循环持续直到用户停止录制
通过一个bool值作为开关,让程序跳出while循环即可
8、结束录制
8.1、刷新编码器---new
avcodec_send_frame,重复7.5.5的操作,将剩余的音频帧送入 AAC 编码器
8.2、获取剩余编码包--new
avcodec_receive_packet,接收剩余的编码数据
8.3、写入容器尾
av_write_trailer(outputCtx),ffmpeg根据输出容器格式(如 WAV、MP4、FLV 等)自动构建并写入该格式所需的"尾部"(trailer)数据,包括音频流的必要结束信息。
8.4、释放输入设备资源
avformat_close_input(&inputCtx),关闭输入设备并释放资源
8.5、关闭文件和设备
avio_closep(&outputCtx->pb) ,安全关闭文件句柄
8.6、释放输出上下文
avformat_free_context(outputCtx),释放输出上下文及其内部流、参数等
流程图
图片有点大,暂时上传不了。
源码
//mainwindow.h
// mainwindow.h(修改部分)
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
#include <QThread>
#include <atomic>
#include <cstring> // for memset (如果用 av_malloc_array + memset)
#include <cstdlib> // for calloc, free
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();
private:
// >>> 新增:AAC 编码所需上下文 <<<
AVFormatContext* m_inputCtx = nullptr;
AVFormatContext* m_outputCtx = nullptr;
AVCodecContext* m_codecCtx = nullptr;
SwrContext* m_swrCtx = nullptr;
AVStream* m_outStream = nullptr;
int m_audioStreamIndex = -1;
std::atomic<bool> m_stop{false};
int64_t m_pts = 0;
// >>> 移除旧的 cleanup 函数 <<<
void cleanup(); // 简化为无参
};
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() {
if (m_swrCtx) {
swr_free(&m_swrCtx);
m_swrCtx = nullptr;
}
if (m_codecCtx) {
//8.3、释放编解码器上下文---new
avcodec_free_context(&m_codecCtx);
m_codecCtx = nullptr;
}
if (m_inputCtx) {
//8.4、释放输入设备资源
avformat_close_input(&m_inputCtx);
m_inputCtx = nullptr;
}
if (m_outputCtx) {
if (!(m_outputCtx->oformat->flags & AVFMT_NOFILE)) {
//8.5、关闭文件和设备
avio_closep(&m_outputCtx->pb);
}
//8.6、释放输出上下文
avformat_free_context(m_outputCtx);
m_outputCtx = nullptr;
}
m_pts = 0;
}
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.mp4";
int ret = 0;
//2.打开输入设备
//2.1、找到DirectShow输入格式
const AVInputFormat* iformat = av_find_input_format("dshow");
//2.2、打开指定的音频设备(麦克风)
ret = avformat_open_input(&m_inputCtx, deviceName, iformat, nullptr);
if (ret < 0) {
qWarning() << "Open device failed";
cleanup();
return;
}
//2.3、获取设备流信息
ret = avformat_find_stream_info(m_inputCtx, nullptr);
if (ret < 0) {
qWarning() << "Find stream info failed";
cleanup();
return;
}
//3、查找音频流
m_audioStreamIndex = -1;
//3.1、遍历流查找音频流 (AVMEDIA_TYPE_AUDIO)
for (uint i = 0; i < m_inputCtx->nb_streams; ++i) {
//3.2、识别音频流类型
if (m_inputCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
m_audioStreamIndex = i;//记录音频索引
break;
}
}
if (m_audioStreamIndex == -1) {
qWarning() << "No audio stream found";
cleanup();
return;
}
//3.3、若通道布局未指定,手动设置为 AV_CH_LAYOUT_STEREO-----new
AVCodecParameters* inPar = m_inputCtx->streams[m_audioStreamIndex]->codecpar;
if (inPar->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC) {
av_channel_layout_from_mask(&inPar->ch_layout, AV_CH_LAYOUT_STEREO);//手动指定为立体声布局
}
qDebug() << "[Input] SampleRate:" << inPar->sample_rate
<< "Channels:" << inPar->ch_layout.nb_channels
<< "Format:" << inPar->format;
// ===== 初始化输出上下文和 AAC 编码器 =====
//4.输出文件准备
//4.1、创建 mp4 输出上下文---change
ret = avformat_alloc_output_context2(&m_outputCtx, nullptr, "mp4", outputFilename);
if (ret < 0 || !m_outputCtx) {
qWarning() << "Failed to create output context";
cleanup();
return;
}
//4.2、查找 AAC 编码器---mp4是aac编码的一种-----new
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!codec) {
qWarning() << "AAC encoder not found";
cleanup();
return;
}
//4.3、创建新的输出流
m_outStream = avformat_new_stream(m_outputCtx, codec);
if (!m_outStream) {
qWarning() << "Failed to create output stream";
cleanup();
return;
}
//4.4、分配编码器上下文,并设置相关参数-----new
m_codecCtx = avcodec_alloc_context3(codec);
m_codecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;
m_codecCtx->bit_rate = 128000;
m_codecCtx->sample_rate = inPar->sample_rate;
m_codecCtx->time_base = (AVRational){1, m_codecCtx->sample_rate};
av_channel_layout_copy(&m_codecCtx->ch_layout, &inPar->ch_layout);
//4.5、打开 AAC 编码器-----new
ret = avcodec_open2(m_codecCtx, codec, nullptr);
if (ret < 0) {
qWarning() << "Failed to open AAC encoder";
cleanup();
return;
}
//4.6、将编码器上下文参数复制到输出流中-----new
ret = avcodec_parameters_from_context(m_outStream->codecpar, m_codecCtx);
m_outStream->time_base = m_codecCtx->time_base;
//5.文件IO建立
if (!(m_outputCtx->oformat->flags & AVFMT_NOFILE)) {
//5.1打开输出文件
ret = avio_open(&m_outputCtx->pb, outputFilename, AVIO_FLAG_WRITE);
if (ret < 0) {
qWarning() << "Failed to open output file";
cleanup();
return;
}
}
//6、写入文件头及初始化重采样器
//6.1、写入文件头,使用 outputCtx 中已配置好的流信息来生成正确的头部
ret = avformat_write_header(m_outputCtx, nullptr);
if (ret < 0) {
qWarning() << "Failed to write header";
cleanup();
return;
}
// ===== 初始化重采样器 =====
//6.2、配置并分配 SwrContext-----new
ret = swr_alloc_set_opts2(
&m_swrCtx,
&m_codecCtx->ch_layout, m_codecCtx->sample_fmt, m_codecCtx->sample_rate,
&inPar->ch_layout, static_cast<AVSampleFormat>(inPar->format), inPar->sample_rate,
0, nullptr
);
if (ret < 0 || !m_swrCtx) {
qWarning() << "Failed to alloc resampler";
cleanup();
return;
}
//6.3、初始化重采样器-----new
swr_init(m_swrCtx);
// ===== 主循环 =====
AVPacket* pkt = av_packet_alloc();
AVFrame* inFrame = av_frame_alloc();
AVFrame* outFrame = av_frame_alloc();
uint8_t** tempBuffer = nullptr; // 用于存储重采样后的 planar 数据
int totalPackets = 0;
int audioFrameCount = 0;
m_pts = 0;
int frame_size = m_codecCtx->frame_size;
if (frame_size <= 0) frame_size = 1024; // AAC 通常需要 1024
qDebug() << "Encoder expected frame_size:" << frame_size;
qDebug() << "=== Starting main capture loop ===";
while (!m_stop.load()) {
//7.1、读原始包
ret = av_read_frame(m_inputCtx, pkt);
if (ret == AVERROR(EAGAIN)) continue;
if (ret == AVERROR_EOF) break;
if (ret < 0) break;
//7.2、过滤出音频流数据
if (pkt->stream_index == m_audioStreamIndex) {
audioFrameCount++;
qDebug() << ">>> Processing audio frame #" << audioFrameCount << "| size:" << pkt->size;
//7.3 音频处理
//7.3.1、计算样本数、分配缓冲区、拷贝数据
int bytes_per_sample = av_get_bytes_per_sample(static_cast<AVSampleFormat>(inPar->format));
int channels = inPar->ch_layout.nb_channels;
int nb_samples = pkt->size / (bytes_per_sample * channels);
if (nb_samples <= 0) {
av_packet_unref(pkt);
continue;
}
//7.3.2、设置输入帧
inFrame->nb_samples = nb_samples;
inFrame->format = inPar->format;
inFrame->sample_rate = inPar->sample_rate;
av_channel_layout_copy(&inFrame->ch_layout, &inPar->ch_layout); // 声道布局
ret = av_frame_get_buffer(inFrame, 0);
if (ret < 0) {
av_packet_unref(pkt);
continue;
}
memcpy(inFrame->data[0], pkt->data, pkt->size);
//7.3.3、计算重采样后样本数
int out_samples = swr_get_out_samples(m_swrCtx, nb_samples);
if (out_samples <= 0) {
av_frame_unref(inFrame);
av_packet_unref(pkt);
continue;
}
//7.3.4、分配指针数组(每个通道一个指针)
tempBuffer = (uint8_t**)calloc(channels, sizeof(uint8_t*));
if (!tempBuffer) {
qWarning() << "calloc tempBuffer failed";
av_frame_unref(inFrame);
av_packet_unref(pkt);
continue;
}
//7.3.5、分配实际音频数据缓冲区(由 FFmpeg 管理)
ret = av_samples_alloc(tempBuffer, nullptr, channels, out_samples,
m_codecCtx->sample_fmt, 0);
if (ret < 0) {
qWarning() << "av_samples_alloc failed";
free(tempBuffer);
tempBuffer = nullptr;
av_frame_unref(inFrame);
av_packet_unref(pkt);
continue;
}
//7.4、执行重采样---new
int converted = swr_convert(m_swrCtx, tempBuffer, out_samples,
(const uint8_t**)inFrame->data, nb_samples);
if (converted <= 0) {
av_freep(&tempBuffer[0]);
free(tempBuffer);
tempBuffer = nullptr;
av_frame_unref(inFrame);
av_packet_unref(pkt);
continue;
}
//7.5、拆分成标准帧(如 1024 samples)并编码,按照 frame_size进行切片处理
int offset = 0;
while (offset + frame_size <= converted) {
//7.5.1、设置输出帧的基本属性
outFrame->nb_samples = frame_size;
outFrame->format = m_codecCtx->sample_fmt;
outFrame->sample_rate = m_codecCtx->sample_rate;
av_channel_layout_copy(&outFrame->ch_layout, &m_codecCtx->ch_layout); // 声道布局
//7.5.2、为 outFrame 分配内存缓冲区
ret = av_frame_get_buffer(outFrame, 0);
if (ret < 0) {
offset += frame_size;
continue;
}
//7.5.3、计算单个采样的字节数(例如 FLTP 是 4 字节/采样)
int sample_size = av_get_bytes_per_sample(m_codecCtx->sample_fmt);
//7.5.4、拷贝 planar 格式数据:每个通道独立存储,需逐通道 memcpy
for (int ch = 0; ch < channels; ch++) {
memcpy(outFrame->data[ch],
tempBuffer[ch] + offset * sample_size,
frame_size * sample_size);
}
// 设置 PTS(Presentation Timestamp):用于同步和容器写入
outFrame->pts = m_pts;
m_pts += frame_size;
//7.5.5、将构建好的音频帧送入 AAC 编码器
ret = avcodec_send_frame(m_codecCtx, outFrame);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << " avcodec_send_frame failed:" << errbuf;
} else {
// 成功送帧后,尝试接收所有可输出的编码包(可能 0 个、1 个或多个)
AVPacket* enc_pkt = av_packet_alloc();
//7.5.6、循环接收编码后的数据包
while (avcodec_receive_packet(m_codecCtx, enc_pkt) == 0) {
enc_pkt->stream_index = m_outStream->index;
av_packet_rescale_ts(enc_pkt, m_codecCtx->time_base, m_outStream->time_base);
//7.6、写入到输出文件(自动处理 interleaving)
av_interleaved_write_frame(m_outputCtx, enc_pkt);
//7.7、释放数据包资源
av_packet_unref(enc_pkt);
totalPackets++;
}
av_packet_free(&enc_pkt);//释放packet
}
// 释放 outFrame 的 buffer 引用(下次循环会重新 av_frame_get_buffer)
av_frame_unref(outFrame);
offset += frame_size;// 移动偏移,处理下一帧
}
// 释放临时缓冲区
av_freep(&tempBuffer[0]); // 音频数据
free(tempBuffer); // 指针数组
tempBuffer = nullptr;
// 释放 inFrame 的 buffer 引用(下次循环会重新 av_frame_get_buffer)
av_frame_unref(inFrame);
}
//7.5.8、释放数据包资源
av_packet_unref(pkt);
}
//8.结束录制
//8.1、刷新编码器---new
avcodec_send_frame(m_codecCtx, nullptr);
AVPacket* flush_pkt = av_packet_alloc();
//8.2、获取剩余编码包--new
while (avcodec_receive_packet(m_codecCtx, flush_pkt) == 0) {
flush_pkt->stream_index = m_outStream->index;
av_packet_rescale_ts(flush_pkt, m_codecCtx->time_base, m_outStream->time_base);
av_interleaved_write_frame(m_outputCtx, flush_pkt);
av_packet_unref(flush_pkt);
totalPackets++;
}
av_packet_free(&flush_pkt);
//8.3、写入文件尾部信息
av_write_trailer(m_outputCtx);
// 清理
if (tempBuffer) {
av_freep(&tempBuffer[0]);
free(tempBuffer);
}
cleanup();
qDebug() << "=== Final Report ===";
qDebug() << "Audio frames processed:" << audioFrameCount;
qDebug() << "Total encoded packets:" << totalPackets;
}
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);
}