前言
测试环境:
- ffmpeg的shared版本
- windows环境
- qt5.12
ffmpeg录制音频为wav文件,思路和录制成pcm相同,关键思路是在打开文件之初,先加上wav文件头,后续再写入音频数据的二进制信息即可
完整代码:(我这里是将功能封装在线程里,其中还考虑了线程的关闭问题)
AudioRecordWavThread.h
#ifndef AUDIORECORDWAVTHREAD_H
#define AUDIORECORDWAVTHREAD_H
#include <QThread>
#include "pcmtowavthread.h"
class AudioRecordWavThread : public QThread {
Q_OBJECT
private:
void run();
bool _stop = false;
public:
explicit AudioRecordWavThread(QObject *parent = nullptr);
~AudioRecordWavThread();
void setStop(bool stop);
signals:
void timeChanged(unsigned long long ms);
};
#endif // AUDIORECORDWAVTHREAD_H
.h文件中包含pcmtowavthread.h是为了使用其内部的WAVHeader结构体
WAVHeader结构体如下
#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3
// WAV文件头(44字节)
typedef struct {
// RIFF chunk的id
uint8_t riffChunkId[4] = {
'R', 'I', 'F', 'F'};
// RIFF chunk的data大小,即文件总长度减去8字节
uint32_t riffChunkDataSize;
// "WAVE"
uint8_t format[4] = {
'W', 'A', 'V', 'E'};
/* fmt chunk */
// fmt chunk的id
uint8_t fmtChunkId[4] = {
'f', 'm', 't', ' '};
// fmt chunk的data大小:存储PCM数据时,是16
uint32_t fmtChunkDataSize = 16;
// 音频编码,1表示PCM,3表示Floating Point
uint16_t audioFormat = AUDIO_FORMAT_PCM;
// 声道数
uint16_t numChannels;
// 采样率
uint32_t sampleRate;
// 字节率 = sampleRate * blockAlign
uint32_t byteRate;
// 一个样本的字节数 = bitsPerSample * numChannels >> 3
uint16_t blockAlign;
// 位深度
uint16_t bitsPerSample;
/* data chunk */
// data chunk的id
uint8_t dataChunkId[4] = {
'd', 'a', 't', 'a'};
// data chunk的data大小:音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
uint32_t dataChunkDataSize;
} WAVHeader;
AudioRecordWavThread.cpp
#include "audiorecordwavthread.h"
#include <QDebug>
#include <QFile>
#include <QDateTime>
extern "C" {
// 设备
#include <libavdevice/avdevice.h>
// 格式
#include <libavformat/avformat.h>
// 工具(比如错误处理)
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
}
#ifdef Q_OS_WIN
// 格式名称
#define FMT_NAME "dshow"
// 设备名称
#define DEVICE_NAME "audio=耳机 (Baseus Bowie E8 Hands-Free AG Audio)"
// PCM文件名
#define FILEPATH "E:/media/"
#else
#define FMT_NAME "avfoundation"
#define DEVICE_NAME ":0"
#define FILEPATH "/Users/mj/Desktop/"
#endif
AudioRecordWavThread::AudioRecordWavThread(QObject *parent) : QThread(parent) {
// 当监听到线程结束时(finished),就调用deleteLater回收内存
connect(this, &AudioRecordWavThread::finished,
this, &AudioRecordWavThread::deleteLater);
}
AudioRecordWavThread::~AudioRecordWavThread() {
// 断开所有的连接
disconnect();
// 内存回收之前,正常结束线程
requestInterruption();
// 安全退出
quit();
wait();
qDebug() << this << "析构(内存被回收)";
}
// 当线程启动的时候(start),就会自动调用run函数
// run函数中的代码是在子线程中执行的
// 耗时操作应该放在run函数中
void AudioRecordWavThread::run() {
qDebug() << this << "开始执行----------";
// 获取输入格式对象
const AVInputFormat *fmt = av_find_input_format(FMT_NAME);
if (!fmt) {
qDebug() << "获取输入格式对象失败" << FMT_NAME;
return;
}
// 格式上下文(将来可以利用上下文操作设备)
AVFormatContext *ctx = nullptr;
// 打开设备
int ret = avformat_open_input(&ctx, DEVICE_NAME, fmt, nullptr);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "打开设备失败" << errbuf;
return;
}
// 打印一下录音设备的参数信息
// showSpec(ctx);
// 文件名
QString filename = FILEPATH;
filename += QDateTime::currentDateTime().toString("MM_dd_HH_mm_ss");
filename += ".wav";
QFile file(filename);
// 打开文件
if (!file.open(QFile::WriteOnly)) {
qDebug() << "文件打开失败" << filename;
// 关闭设备
avformat_close_input(&ctx);
return;
}
// 获取输入流
AVStream *stream = ctx->streams[0];
// 获取音频参数
AVCodecParameters *params = stream->codecpar;
// 写入WAV文件头
WAVHeader header;
header.sampleRate = params->sample_rate;
// 2
header.bitsPerSample = av_get_bits_per_sample(params->codec_id);
header.numChannels = params->channels;
if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
header.audioFormat = AUDIO_FORMAT_FLOAT;
}
header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
header.byteRate = header.sampleRate * header.blockAlign;
// header.dataChunkDataSize = 0;
file.write((char *) &header, sizeof (WAVHeader));
// 数据包
AVPacket *pkt = av_packet_alloc();
while (!isInterruptionRequested()) {
// 不断采集数据
ret = av_read_frame(ctx, pkt);
if (ret == 0) {
// 读取成功
// 将数据写入文件
file.write((const char *) pkt->data, pkt->size);
// 计算录音时长
header.dataChunkDataSize += pkt->size;
unsigned long long ms = 1000.0 * header.dataChunkDataSize / header.byteRate;
emit timeChanged(ms);
// 释放资源
av_packet_unref(pkt);
} else if (ret == AVERROR(EAGAIN)) {
// 资源临时不可用
continue;
} else {
// 其他错误
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_read_frame error" << errbuf << ret;
break;
}
}
// qDebug() << file.size() << header.dataChunkDataSize;
// int size = file.size();
// 写入dataChunkDataSize
// header.dataChunkDataSize = size - sizeof (WAVHeader);
file.seek(sizeof (WAVHeader) - sizeof (header.dataChunkDataSize));
file.write((char *) &header.dataChunkDataSize, sizeof (header.dataChunkDataSize));
// 写入riffChunkDataSize
header.riffChunkDataSize = file.size()
- sizeof (header.riffChunkId)
- sizeof (header.riffChunkDataSize);
file.seek(sizeof (header.riffChunkId));
file.write((char *) &header.riffChunkDataSize, sizeof (header.riffChunkDataSize));
// 释放资源
av_packet_free(&pkt);
// 关闭文件
file.close();
// 关闭设备
avformat_close_input(&ctx);
qDebug() << this << "正常结束----------";
}
void AudioRecordWavThread::setStop(bool stop) {
_stop = stop;
}
注意:该代码对可能出现的问题都进行了一定的优化,读者使用时只需关注关键代码
线程调用
void MainWindow::on_pushButton_record_to_wav_clicked()
{
if (!audioRecordWavThread) {
// 点击了“开始录音”
// 开启线程
audioRecordWavThread = new AudioRecordWavThread(this);
audioRecordWavThread->start();
connect(audioRecordWavThread, &AudioRecordWavThread::timeChanged,
this, &MainWindow::onTimeChanged);
connect(audioRecordWavThread, &AudioThread::finished,
[this]() {
// 线程结束
audioRecordWavThread = nullptr;
ui->pushButton_record_to_wav->setText("音频录制为wav");
});
// 设置按钮文字
ui->pushButton_record_to_wav->setText("结束录音");
} else {
// 点击了“结束录音”
// 结束线程
// _audioThread->setStop(true);
audioRecordWavThread->requestInterruption();
audioRecordWavThread = nullptr;
// 设置按钮文字
ui->pushButton_record_to_wav->setText("音频录制为wav");
}
}
其中槽函数
void onTimeChanged(unsigned long long ms);
的具体实现为
void MainWindow::onTimeChanged(unsigned long long ms) {
QTime time(0, 0, 0, 0);
QString text = time.addMSecs(ms).toString("mm:ss.zz");
ui->label_record_wav_time->setText(text.left(5));
}
在主函数的构造方法中还要初始化一下时间
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 初始化libavdevice并注册所有输入和输出设备
avdevice_register_all();
// 初始化时间
onTimeChanged(0);
}
注意:.h文件中提前声明了以下全局变量
AudioRecordWavThread *audioRecordWavThread = nullptr;
注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用
码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方