前言
前面有两篇使用纯ffmpeg库实现了PCM和MP4录音功能,几乎展示了所有涉及的流程(ffmpeg是一个C库,比较原生)。QT也有封装好的多媒体模块,能让流程变简单很多,这也是本篇的目的。
效果图

关联博文
【QT window】ffmpeg实现手动绘图(裁剪)、缩放、拍照,显示fps等功能
【QT window】ffmpeg实现录音功能之无损格式--PCM
【QT window】ffmpeg实现录音功能之AAC格式--mp4
ffmpeg库的引入请看第一篇
功能讲解
PCM录音
multimedia多媒体模块因为没有集成其他音频编解码模块,默认是支持无损格式录音的,使用QFile+QIODevice+QAudioInput即可实现,流程太简单,这里就不罗列流程了,直接看下面代码的注释即可。
//开始录音
void MainWindow::startRecording(){
//1.创建PCM/WAV文件
m_pcmFile = new QFile(fileName);
if (!m_pcmFile->open(QIODevice::WriteOnly)) {
QMessageBox::critical(this, "错误", "无法创建 WAV 文件");
delete m_pcmFile;
m_pcmFile = nullptr;
return;
}
//2.写入44字节的占位头(后续会替换为真实的WAV头)
m_pcmFile->write(QByteArray(44, 0));
//3.创建音频输入对象并开始录音
m_audioInput = new QAudioInput(device, m_audioFormat, this);
m_audioDevice = m_audioInput->start(); // 开始录音,返回音频设备对
if (!m_audioDevice) {
QMessageBox::critical(this, "错误", "无法开始录音");
// 清理资源
m_pcmFile->close();
delete m_pcmFile;
m_pcmFile = nullptr;
delete m_audioInput;
m_audioInput = nullptr;
return;
}
//4.连接音频数据就绪信号,当有音频数据可读时触发
connect(m_audioDevice, &QIODevice::readyRead, this, [this]() {
QByteArray data = m_audioDevice->readAll(); // 读取所有可用的音频数据
if (!data.isEmpty()) {
//4.1直接写入PCM数据到文件
m_pcmFile->write(data);
}
});
}
//结束录音
void MainWindow::stopRecording(){
// 停止音频输入
if (m_audioInput) {
m_audioInput->stop();
delete m_audioInput;
m_audioInput = nullptr;
}
//5.需要写入正确的文件头
qint64 dataSize = m_pcmFile->size() - 44; // 计算实际的音频数据大小
if (dataSize > 0) {
m_pcmFile->seek(0); // 回到文件开头
writeWavHeader(*m_pcmFile, m_audioFormat, dataSize); // 写入正确的WAV头
}
m_pcmFile->close();
delete m_pcmFile;
m_pcmFile = nullptr;
}
static void writeWavHeader(QFile &file, const QAudioFormat &format, qint64 dataSize)
{
// 断言检查格式要求:必须是小端序、有符号整型、16位采样
Q_ASSERT(format.byteOrder() == QAudioFormat::LittleEndian);
Q_ASSERT(format.sampleType() == QAudioFormat::SignedInt);
Q_ASSERT(format.sampleSize() == 16);
QByteArray header;
QDataStream ds(&header, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::LittleEndian); // WAV文件使用小端序
// RIFF header - WAV文件的标准头结构
ds.writeRawData("RIFF", 4); // 文件标识
ds << quint32(36 + dataSize); // 整个文件大小 = 36字节头 + 音频数据大小
ds.writeRawData("WAVE", 4); // 文件类型标识
// fmt subchunk - 格式描述块
ds.writeRawData("fmt ", 4); // 格式块标识
ds << quint32(16); // 格式块大小(固定16字节)
ds << quint16(1); // 音频格式(1表示PCM)
ds << quint16(format.channelCount()); // 声道数
ds << quint32(format.sampleRate()); // 采样率
ds << quint32(format.sampleRate() * format.channelCount() * format.sampleSize() / 8); // 字节率
ds << quint16(format.channelCount() * format.sampleSize() / 8); // 块对齐
ds << quint16(format.sampleSize()); // 位深度
// data subchunk - 数据块
ds.writeRawData("data", 4); // 数据块标识
ds << quint32(dataSize); // 音频数据大小
file.write(header); // 将头信息写入文件
}
因为不需要编码,直接在4.1步骤的写入PCM数据到文件即可,然后关闭录音时,想要第5部,写入一个文件头。
AAC-MP4录音
我写了一个ffmpeg处理MP4的AACRecorder类,下面先提供AACRecorder类的代码(不讲解了),再提供主框架MainWindow的MP4录音流程
//aacrecorder.h
#ifndef AACRECORDER_H
#define AACRECORDER_H
#include <QObject>
#include <QString>
#include <QMutex>
#include <vector>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libswresample/swresample.h>
}
class AACRecorder : public QObject
{
Q_OBJECT
public:
explicit AACRecorder(QObject *parent = nullptr);
~AACRecorder();
bool initialize(const QString &outputFile, int sampleRate, int channels, int bitrate);
void writeAudioData(const uint8_t *data, int size);
void close();
bool isInitialized() const { return m_initialized; }
private:
bool initEncoder();
bool initResampler();
bool encodeFrame(AVFrame *frame);
void cleanup();
bool encodeAudioFrame(const uint8_t *data, int samples);
QString m_outputFile;
// FFmpeg相关变量
AVFormatContext *m_formatCtx = nullptr;
AVCodecContext *m_codecCtx = nullptr;
AVStream *m_stream = nullptr;
SwrContext *m_swrCtx = nullptr;
// 音频参数
int m_sampleRate = 44100;
int m_channels = 2;
int m_bitrate = 128000;
AVSampleFormat m_inputFormat = AV_SAMPLE_FMT_S16;
// 状态
bool m_initialized = false;
int64_t m_pts = 0;
QMutex m_mutex;
// 输入缓冲区
std::vector<uint8_t> m_inputBuffer;
};
#endif
//aacrecorder.cpp
#include "aacrecorder.h"
#include <QDebug>
#include <QFileInfo>
#include <QDir>
AACRecorder::AACRecorder(QObject *parent)
: QObject(parent)
{
// 注册所有编解码器
avformat_network_init();
}
AACRecorder::~AACRecorder()
{
close();
}
bool AACRecorder::initialize(const QString &outputFile, int sampleRate, int channels, int bitrate)
{
QMutexLocker locker(&m_mutex);
qDebug() << "=== AACRecorder 初始化开始 ===";
if (m_initialized) {
close();
}
m_outputFile = outputFile;
m_sampleRate = sampleRate;
m_channels = channels;
m_bitrate = bitrate;
qDebug() << "初始化编码器参数:";
qDebug() << " 文件:" << outputFile;
qDebug() << " 采样率:" << sampleRate;
qDebug() << " 声道数:" << channels;
qDebug() << " 比特率:" << bitrate;
// 检查声道数是否有效
if (channels != 1 && channels != 2) {
qWarning() << "不支持的声道数:" << channels;
return false;
}
// 检查文件路径是否有效
QFileInfo fileInfo(outputFile);
QDir dir = fileInfo.absoluteDir();
if (!dir.exists()) {
qDebug() << "创建目录:" << dir.absolutePath();
if (!dir.mkpath(".")) {
qWarning() << "无法创建目录:" << dir.absolutePath();
return false;
}
}
// 初始化编码器
if (!initEncoder()) {
cleanup();
return false;
}
// 初始化重采样器
if (!initResampler()) {
cleanup();
return false;
}
m_initialized = true;
m_pts = 0;
m_inputBuffer.clear();
qDebug() << "=== AACRecorder 初始化完成 ===";
return true;
}
bool AACRecorder::initEncoder()
{
int ret = 0;
// 创建MP4格式上下文
ret = avformat_alloc_output_context2(&m_formatCtx, nullptr, "mp4", m_outputFile.toUtf8().constData());
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "无法创建MP4容器:" << errorBuf;
return false;
}
// 查找AAC编码器
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!codec) {
qWarning() << "未找到AAC编码器";
return false;
}
// 创建音频流
m_stream = avformat_new_stream(m_formatCtx, codec);
if (!m_stream) {
qWarning() << "无法创建音频流";
return false;
}
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
qWarning() << "无法分配编码器上下文";
return false;
}
// 设置编码参数
m_codecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;
m_codecCtx->bit_rate = m_bitrate;
m_codecCtx->sample_rate = m_sampleRate;
m_codecCtx->time_base = (AVRational){1, m_sampleRate};
// 设置声道布局
AVChannelLayout channel_layout;
if (m_channels == 1) {
av_channel_layout_default(&channel_layout, 1);
} else {
av_channel_layout_default(&channel_layout, 2);
}
av_channel_layout_copy(&m_codecCtx->ch_layout, &channel_layout);
av_channel_layout_uninit(&channel_layout);
// 打开编码器
ret = avcodec_open2(m_codecCtx, codec, nullptr);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "无法打开编码器:" << errorBuf;
return false;
}
// 复制编码器参数到流
ret = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
if (ret < 0) {
qWarning() << "无法复制编码器参数到流";
return false;
}
// 设置流的time_base
m_stream->time_base = m_codecCtx->time_base;
// 打开输出文件(FFmpeg统一管理文件操作)
ret = avio_open(&m_formatCtx->pb, m_outputFile.toUtf8().constData(), AVIO_FLAG_WRITE);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "FFmpeg无法打开输出文件:" << errorBuf;
return false;
}
// 写入文件头
ret = avformat_write_header(m_formatCtx, nullptr);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "无法写入文件头:" << errorBuf;
return false;
}
qDebug() << "MP4编码器初始化成功";
return true;
}
bool AACRecorder::initResampler()
{
if (m_swrCtx) {
swr_free(&m_swrCtx);
m_swrCtx = nullptr;
}
// 设置输入声道布局
AVChannelLayout in_ch_layout;
if (m_channels == 1) {
av_channel_layout_default(&in_ch_layout, 1);
} else {
av_channel_layout_default(&in_ch_layout, 2);
}
// 设置输出声道布局(与编码器一致)
AVChannelLayout out_ch_layout;
if (m_channels == 1) {
av_channel_layout_default(&out_ch_layout, 1);
} else {
av_channel_layout_default(&out_ch_layout, 2);
}
// 创建重采样器
int ret = swr_alloc_set_opts2(&m_swrCtx,
&out_ch_layout, // 输出声道布局
m_codecCtx->sample_fmt, // 输出格式
m_codecCtx->sample_rate, // 输出采样率
&in_ch_layout, // 输入声道布局
m_inputFormat, // 输入格式
m_sampleRate, // 输入采样率
0, nullptr);
av_channel_layout_uninit(&in_ch_layout);
av_channel_layout_uninit(&out_ch_layout);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "无法设置重采样器选项:" << errorBuf;
return false;
}
ret = swr_init(m_swrCtx);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "无法初始化重采样器:" << errorBuf;
swr_free(&m_swrCtx);
m_swrCtx = nullptr;
return false;
}
qDebug() << "重采样器初始化成功";
return true;
}
void AACRecorder::writeAudioData(const uint8_t *data, int size)
{
QMutexLocker locker(&m_mutex);
if (!m_initialized || !data || size <= 0) {
return;
}
// 将新数据添加到缓冲区
size_t oldSize = m_inputBuffer.size();
m_inputBuffer.resize(oldSize + size);
memcpy(m_inputBuffer.data() + oldSize, data, size);
// 计算缓冲区中的总样本数
int bytes_per_sample = av_get_bytes_per_sample(m_inputFormat);
int total_samples = m_inputBuffer.size() / (m_channels * bytes_per_sample);
// 编码器期望的帧大小
int frame_size = m_codecCtx->frame_size;
if (frame_size <= 0) {
frame_size = 1024; // 默认值
}
// 处理缓冲区中的数据,直到不足一个完整的帧
while (total_samples >= frame_size) {
// 编码一个完整的帧
if (!encodeAudioFrame(m_inputBuffer.data(), frame_size)) {
qWarning() << "编码音频帧失败";
break;
}
// 移除已处理的数据
int processed_bytes = frame_size * m_channels * bytes_per_sample;
if (processed_bytes < m_inputBuffer.size()) {
// 将剩余数据移到缓冲区开头
memmove(m_inputBuffer.data(),
m_inputBuffer.data() + processed_bytes,
m_inputBuffer.size() - processed_bytes);
}
m_inputBuffer.resize(m_inputBuffer.size() - processed_bytes);
total_samples = m_inputBuffer.size() / (m_channels * bytes_per_sample);
}
}
bool AACRecorder::encodeAudioFrame(const uint8_t *data, int samples)
{
if (!data || samples <= 0) {
return false;
}
// 分配输入帧
AVFrame *in_frame = av_frame_alloc();
if (!in_frame) {
qWarning() << "无法分配输入帧";
return false;
}
in_frame->nb_samples = samples;
in_frame->format = m_inputFormat;
in_frame->sample_rate = m_sampleRate;
// 设置输入声道布局
AVChannelLayout in_ch_layout;
if (m_channels == 1) {
av_channel_layout_default(&in_ch_layout, 1);
} else {
av_channel_layout_default(&in_ch_layout, 2);
}
int ret = av_channel_layout_copy(&in_frame->ch_layout, &in_ch_layout);
av_channel_layout_uninit(&in_ch_layout);
if (ret < 0) {
qWarning() << "无法设置输入帧声道布局";
av_frame_free(&in_frame);
return false;
}
// 分配输入帧缓冲区
ret = av_frame_get_buffer(in_frame, 0);
if (ret < 0) {
qWarning() << "无法分配输入帧缓冲区";
av_frame_free(&in_frame);
return false;
}
// 复制数据到输入帧
if (av_sample_fmt_is_planar(m_inputFormat)) {
for (int ch = 0; ch < m_channels; ch++) {
int bytes_per_sample = av_get_bytes_per_sample(m_inputFormat);
int channel_size = samples * bytes_per_sample;
memcpy(in_frame->data[ch], data + ch * channel_size, channel_size);
}
} else {
int total_size = samples * m_channels * av_get_bytes_per_sample(m_inputFormat);
memcpy(in_frame->data[0], data, total_size);
}
// 分配输出帧
AVFrame *out_frame = av_frame_alloc();
if (!out_frame) {
qWarning() << "无法分配输出帧";
av_frame_free(&in_frame);
return false;
}
out_frame->nb_samples = samples;
out_frame->format = m_codecCtx->sample_fmt;
out_frame->sample_rate = m_codecCtx->sample_rate;
// 复制输出声道布局
ret = av_channel_layout_copy(&out_frame->ch_layout, &m_codecCtx->ch_layout);
if (ret < 0) {
qWarning() << "无法设置输出帧声道布局";
av_frame_free(&in_frame);
av_frame_free(&out_frame);
return false;
}
// 重采样
ret = swr_convert_frame(m_swrCtx, out_frame, in_frame);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "重采样失败:" << errorBuf;
av_frame_free(&in_frame);
av_frame_free(&out_frame);
return false;
}
out_frame->pts = m_pts;
m_pts += samples;
// 编码帧
bool success = encodeFrame(out_frame);
// 清理
av_frame_free(&in_frame);
av_frame_free(&out_frame);
return success;
}
bool AACRecorder::encodeFrame(AVFrame *frame)
{
if (!frame) {
int ret = avcodec_send_frame(m_codecCtx, nullptr);
if (ret < 0 && ret != AVERROR_EOF) {
qWarning() << "发送刷新帧失败,错误代码:" << ret;
return false;
}
} else {
int ret = avcodec_send_frame(m_codecCtx, frame);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "发送帧到编码器失败:" << errorBuf;
return false;
}
}
AVPacket *pkt = av_packet_alloc();
if (!pkt) {
qWarning() << "无法分配AVPacket";
return false;
}
bool success = true;
while (true) {
int ret = avcodec_receive_packet(m_codecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "接收编码包失败:" << errorBuf;
success = false;
break;
}
// 写入到MP4容器
pkt->stream_index = m_stream->index;
av_packet_rescale_ts(pkt, m_codecCtx->time_base, m_stream->time_base);
ret = av_interleaved_write_frame(m_formatCtx, pkt);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_make_error_string(errorBuf, AV_ERROR_MAX_STRING_SIZE, ret);
qWarning() << "写入帧失败:" << errorBuf;
}
av_packet_unref(pkt);
}
av_packet_free(&pkt);
return success;
}
void AACRecorder::close()
{
QMutexLocker locker(&m_mutex);
if (!m_initialized) {
return;
}
// 处理缓冲区中剩余的数据
if (!m_inputBuffer.empty()) {
int bytes_per_sample = av_get_bytes_per_sample(m_inputFormat);
int remaining_samples = m_inputBuffer.size() / (m_channels * bytes_per_sample);
if (remaining_samples > 0) {
encodeAudioFrame(m_inputBuffer.data(), remaining_samples);
}
m_inputBuffer.clear();
}
// 刷新编码器
if (m_codecCtx) {
encodeFrame(nullptr);
// 写入文件尾
if (m_formatCtx) {
av_write_trailer(m_formatCtx);
}
}
// 关闭文件
if (m_formatCtx && m_formatCtx->pb) {
avio_closep(&m_formatCtx->pb);
}
cleanup();
m_initialized = false;
qDebug() << "AAC编码器已关闭";
}
void AACRecorder::cleanup()
{
if (m_swrCtx) {
swr_free(&m_swrCtx);
m_swrCtx = nullptr;
}
if (m_codecCtx) {
avcodec_free_context(&m_codecCtx);
m_codecCtx = nullptr;
}
if (m_formatCtx) {
avformat_free_context(m_formatCtx);
m_formatCtx = nullptr;
}
// 清空缓冲区
m_inputBuffer.clear();
m_inputBuffer.shrink_to_fit();
m_pts = 0;
}
下面是主框架MainWindow的MP4录音流程
void MainWindow::startRecording(){
//1.初始化AAC编码器
if (!m_recorder->initialize(fileName,
m_audioFormat.sampleRate(),
m_audioFormat.channelCount(),
ui->cbBitrates->currentText().toInt())) {
QMessageBox::critical(this, "错误", "初始化 AAC 编码器失败");
return;
}
//2.创建音频输入对象并开始录音
m_audioInput = new QAudioInput(device, m_audioFormat, this);
m_audioDevice = m_audioInput->start(); // 开始录音,返回音频设备对象
if (!m_audioDevice) {
QMessageBox::critical(this, "错误", "无法开始录音");
// 清理资源
m_recorder->close();
delete m_audioInput;
m_audioInput = nullptr;
return;
}
/3.连接音频数据就绪信号,当有音频数据可读时触发
connect(m_audioDevice, &QIODevice::readyRead, this, [this]() {
QByteArray data = m_audioDevice->readAll(); // 读取所有可用的音频数据
if (!data.isEmpty()) {
//4.将PCM数据编码为AAC
m_recorder->writeAudioData((const uint8_t*)data.constData(), data.size());
}
});
}
void MainWindow::stopRecording(){
//5.关闭AAC编码器
m_recorder->close(); //
}
5个步骤,因为需要音频编码,在读取到PCM数据之后,调用自定义的编码函数进行封装。
项目源码可以在资源中下载。