假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别

假期之不务正业——Qt+FFmpeg+百度api进行视频的语音识别

  • 一、前言
  • 二、FFmpeg进行音频提取和重采样
  • 三、对音频分段
  • 四、百度api调用
  • 五、Qt编程的一些补充
  • 六、结语

一、前言

现在语音识别技术逐渐发展,先有siri开个好头,现在有各种小度小爱什么的轮番上阵。王者荣耀有语音识别以后,祖安起来也省事多了。我看一些视频教程的时候,对一些讲的不错的,也有记笔记的习惯。可是每次都是把视频暂停,然后一句一句敲出word,说实话,也没见学习效果有多好,反而效率变得低到不行。想来想去,咱也不能一直停留在这么笨比的方式,总是想整点活。

其实网上就有一些提取字幕的、或是语音识别的应用,应该效果也不错(我没试),但是要钱(emmmmm)。所以暂时先放弃这个方案,而且如果自己做一个那不是快乐加倍?于是利用假期时间,自己找了一些资料借(chao)鉴(xi)了一下,也是算是自己从零开始做的垃圾。

先放一下目前做到的:
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第1张图片
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第2张图片
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第3张图片
我主要选择了4个B站的视频来测试运行结果,顺便一提,B站用手机端下载视频后,会在缓存文件里发现audio.m4s和vedio.m4s。实际上,FFmpeg可以直接打开m4s格式,因此如果仅仅是为了对音频进行处理,不需要将两个文件合流为一个(合流的方法也很简单,尤其是使用FFmpeg,可以直接百度)。

我选择的4个视频分别是冰冰vlog、卢本伟17张牌名场面、小潮院长的不要做挑战和吴恩达老师的机器学习课程,链接放在下文,我这里就夹带私货安利一波。下面是识别结果:
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第4张图片
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第5张图片
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第6张图片
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第7张图片

简单复盘一下:识别结果也算还可以,与英文相比,中文能够带上标点符号看起来更利落一些。显然,语速放慢,说话更标准时,识别效果更好(这不是废话吗)。可以看到小潮的不要做挑战的前面正经讲游戏规则时,识别结果还能接受,到后面整活了,识别结果驴唇不对马嘴。对于语速中规中矩的视频(尤其对于目的:视频教程),能有一些帮助;但如果是小视频(尤其节奏比较快的),那还是算了吧。

总体思路就是:Qt做个外壳,FFmpeg提取视频里的音频,百度api进行语音识别。由于百度开放的免费接口要求时长在1分钟以内,所以对于超过一分钟时长的音频,需要进行分段(顺便一提,免费接口使用量是中文普通话5w次,英文2w次)。下面对于各个部分的内容和遇到的(包括未处理完的)问题简单做一下记录。

以下是本实现主要参考资料的相关链接:
1、提供FFmpeg相关操作流程:
《使用 FFmpeg 进行音视频操作》,这个CSDN博客介绍了FFmpeg的主要模块、音视频解码与重采样等内容,主要都是文字介绍,具体代码实现也有一部分,有一定的参考价值(后面的记录仅写一些我的工作和问题吧,这个博客的内容不会转载的)。放下链接:
https://gitchat.csdn.net/activity/5d08d7d44ea36e699ecac739
2、提供百度API相关操作流程:
《Qt语音识别 | 百度语音识别应用》,这个B站视频介绍百度API的接口、使用Qt来调用百度API的方法,我的相关操作全部参考这个视频(因此后面的记录里代码部分不会太多,引用也经过老师同意),有兴趣的直接看视频吧。放下链接:
https://www.bilibili.com/video/BV19K411V79h

以下是上面效果展示的原视频链接:
1、【冰冰vlog.001】带大家看看每个冬天我必去的地方
https://www.bilibili.com/video/BV1vy4y1i7bS
2、【名场面】17张牌你能秒我?你能秒杀我?你今天17张牌把卢本伟秒了,我当场就把这个电脑屏幕吃掉!
https://www.bilibili.com/video/BV1W4411r7ue
3、不要“做”挑战 ?
https://www.bilibili.com/video/BV1x7411Z7VA
4、[中英字幕]吴恩达机器学习系列课程
https://www.bilibili.com/video/BV164411b7dx

二、FFmpeg进行音频提取和重采样

关于FFmpeg的介绍、使用,可以直接看前言的链接,或者找其他教程,这里也直接梳理一下我们需要做的事情和以及整个过程:
1.对于视频文件,需要解封装,即分离出音频流或者视频流或者其他乱七八糟的东西。得到音频流参数,如声道数、采样率、采样格式等等。
2.解封装后的音频流,再进行解码,得到音频的实际采样数据。
3.设置重采样参数,分配存储重采样的数据空间。对于重采样参数,需要配合百度API的要求:单声道、采样率16000Hz、16bit量化。
4.读取原数据,将重采样后得到的数据,并将数据写入文件,建议直接pcm文件,简单粗暴。
5.释放之前申请的资源。

对于这部分,我们可以考虑封装成一个类ExtractAudio(请不要吐槽我的命名品味,真不会),方便调用和后续的查看,最开始调试时我就是直接全写在一个函数里面的,省事是省事,但是太长了会看得累。以下是代码(.cpp)部分:

void ExtractAudio::init()
{
     
	//初始化参数
	in_nb_samples = 1024; //输入采样点数
	out_channel_layout = AV_CH_LAYOUT_MONO; //输出格式(声道数)
	out_sample_rate = SAMPLE_RATE; //输出采样率
	out_sample_fmt = AV_SAMPLE_FMT_S16; //输出样本格式
}


//打开文件函数,返回值为解封装上下文
AVFormatContext *ExtractAudio::open(QString inpath)
{
     
	av_register_all();//初始化封装库
	AVDictionary *opts = NULL;//参数设置
	AVFormatContext *format = avformat_alloc_context();//解封装上下文
	//QString转换为char数组
	QByteArray ba = inpath.toLocal8Bit();
	char* cpath = ba.data();
	//打开视频文件,参数3:0表示自动选择解封器,参数4:参数设置(比如rtsp的延时时间)
	int re = avformat_open_input(&format, (const char*)cpath, 0, &opts);
	if (re != 0)//打开失败
		return NULL;
	else
		return format;
}


//解码函数,返回值为解码器上下文
AVCodecContext *ExtractAudio::decodec(AVFormatContext *format)
{
     
	//获取流信息,不是所有的格式都需要调用
	//但是即便头已经获取过,这里再获取也没有问题
	//所以原则上每次都获取一下
	int re = avformat_find_stream_info(format, 0);//获取流信息
	if (re < 0)
		return NULL;

	//使用遍历的方法获取音视频流信息
	for (int i = 0; i < format->nb_streams; i++)
	{
     
		AVStream *as = format->streams[i];
		//音频
		if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
		{
     
			audioStream = i;
			break;
		}
	}
	
	//音频解码器打开
	AVCodec *acodec = avcodec_find_decoder(format->streams[audioStream]->codecpar->codec_id); //找到音频解码器
	if (!acodec) //没有找到音频解码器
		return false;

	AVCodecContext *avctx = avcodec_alloc_context3(acodec); //创建解码器上下文
	avcodec_parameters_to_context(avctx, format->streams[audioStream]->codecpar); //配置解码器上下文参数
	avctx->thread_count = 8; //解码线程数改为8
	re = avcodec_open2(avctx, 0, 0); //打开解码器上下文
	if (re != 0) //打开解码器失败
		return NULL;

	return avctx;
}


//音频重采样初始化函数,返回值为音频重采样上下文
SwrContext *ExtractAudio::initswr(AVCodecContext *avctx, uint8_t **out_data)
{
     
	//设置音频重采样
	SwrContext *swr = swr_alloc();
	in_channel_layout = avctx->channel_layout;
	in_sample_rate = avctx->sample_rate;
	in_sample_fmt = avctx->sample_fmt;

	av_opt_set_int(swr, "in_channel_layout", in_channel_layout, 0);
	av_opt_set_int(swr, "out_channel_layout", out_channel_layout, 0);
	av_opt_set_int(swr, "in_sample_rate", in_sample_rate, 0);
	av_opt_set_int(swr, "out_sample_rate", out_sample_rate, 0);
	av_opt_set_sample_fmt(swr, "in_sample_fmt", in_sample_fmt, 0);
	av_opt_set_sample_fmt(swr, "out_sample_fmt", out_sample_fmt, 0);
	swr_init(swr);
	if (!swr_is_initialized(swr))
		return NULL;

	//计算转换样本的数量:避免缓存
	//确保输出缓冲区至少包含所有转换后的输入样本
	out_nb_samples = av_rescale_rnd(in_nb_samples, out_sample_rate, in_sample_rate, AV_ROUND_UP);
	//缓冲区将直接写入原始音频文件,无需对齐
	out_nb_channels = av_get_channel_layout_nb_channels(out_channel_layout);
	int re = av_samples_alloc_array_and_samples(&out_data, &out_linesize, out_nb_channels,
		out_nb_samples, out_sample_fmt, 0);
	if (re < 0)
		return NULL;

	return swr;
}


//音频重采样函数,返回值为输出缓冲区的字节数
//返回值为0时,未找到音频流或暂无音频流,可继续执行函数
//返回值为-1时,重采样失败,应中断
int ExtractAudio::resample(AVFormatContext *format, AVCodecContext *avctx, 
	SwrContext *swr, uint8_t **out_data, AVFrame *frame, AVPacket *pkt)
{
     
	if (pkt->stream_index != audioStream) //判断是否为音频流
		return 0;

	//解码一帧音频
	int gotFrame;
	if (avcodec_decode_audio4(avctx, frame, &gotFrame, pkt) < 0)
		return -1;
	if (!gotFrame)
		return 0;

	//重采样
	int frame_count = swr_convert(swr,
		out_data, out_nb_samples, //输出
		(const uint8_t **)frame->data, in_nb_samples  //输入
	);
	if (frame_count < 0)
		return -1;

	out_bufsize = av_samples_get_buffer_size(&out_linesize, out_nb_channels, frame_count, out_sample_fmt, 1);
	av_packet_unref(pkt);//释放,引用计数-1,为0释放空间
	av_frame_unref(frame);
	return out_bufsize;
}


// 释放空间函数
void ExtractAudio::clear(AVFormatContext *format, AVCodecContext *avctx, 
	SwrContext *swr, AVFrame *frame, AVPacket *pkt)
{
     
	//结束,释放空间
	avformat_close_input(&format);
	avcodec_close(avctx);
	swr_free(&swr);
	av_frame_free(&frame);
	av_packet_free(&pkt);
	av_free(frame);
	av_free(pkt);
}

但是这里虽然代码上释放了,占用空间并没有释放。我自己测试如果打开了一个2G的视频,即便将整个过程都跑完,引用计数也减了,free函数也用了,2G内存还是占着,吐血。所以每次感觉视频大小差不多了,就可以把应用关了重开吧。

三、对音频分段

得到重采样完的数据之后,就可以进行分段处理了。对于短语音识别,时长不能超过1分钟,我这里采用的方法就是,在从每段音频第30s处开始,一直到第60s前,计算1s以内采样值(绝对值)之和,和最小的地方,是我认为这个人声说话的停顿处。有几点补充就是,一是采样率已经默认好是16000Hz;二是每两次求和间的步进,我暂时默认为是0.01s,比如求完了第30s—第31s的和,下一次就求30.01s—31.01s的和。当然这个步进是可以进行变化的,但是个人认为没有必要使步进太小,计算次数变多后很慢(我做过步进是一个采样点的尝试,速度非常非常的慢)。

当然这个方法肯定并不是最优的,对于有BGM的视频来说,可能人不在说话,背景音乐还是有的,从一句话中间给掐断的可能性不是没有。另一个是参数的设置,这里面有很多参数是需要根据视频的情况的调整的,包括比如上面说的从第30s开始,可以换成别的数字;再比如计算1s以内的采样值之和,如果视频的节奏比较快(像小潮的一些视频)或者说话人语速感人,也可以调整;或者是步进等其他参数。但是我觉得我这里设置的参数还算中规中矩,也可以不变。对于这一部分,我们封装为SeparatePCM类。以下是代码(.cpp)部分:

#include "SeparatePCM.h"

#include <qdir.h>

#define SAMPLE_RATE 16000

SeparatePCM::SeparatePCM()
{
     
	//初始化
	//创建一个新缓冲文件夹,用于保存分段后的每一段音频数据
	QDir *folder = new QDir;
	folderStr = "D:\\temp\\temp\\";
	bool exist = folder->exists(folderStr);
	if (!exist)
	{
     
		folder->mkdir(folderStr);
	}
	delete folder;

	//音频处理相关系数初始化
	sample_rate = SAMPLE_RATE;
	sample_amount = 60 * sample_rate; //60s内的样点总数
	start = 0; //每次分段时的第0s的位置
	position = 0; //当前位置
	best_position = 0; //判断的最佳静音段位置
	now_sum = 0; //初始分段的采样点值之和
	number = 1; //初始分段序号

	//下面的参数可以根据实际情况进行调整
	step = 0.01 * sample_rate; //步进,这里设定为0.01s,可以根据实际情况调整
	threshold_len_silence = 1 * sample_rate; //判断为静音段的默认时长,这里设定为1s,可以根据实际情况调整
	start_position = (long)sample_amount / 6 * 3; //开始分段的位置,这里设定为第30s,可以根据实际情况调整
}


SeparatePCM::~SeparatePCM()
{
     
}


//打开文件函数,返回打开文件是否成功
bool SeparatePCM::open(QString inpath)
{
     
	filePath = inpath;
	QByteArray ba = filePath.toLocal8Bit();
	char* path = ba.data();
	//获取文件的指针
	FILE *file = fopen((const char*)path, "rb");
	if (!file)
		return false;
	//把指针移动到文件的结尾 ,获取文件长度
	fseek(file, 0, SEEK_END);
	//获取文件长度
	fileLength = ftell(file);
	//关闭文件
	fclose(file);
	return true;
}


//音频文件分段处理函数
void SeparatePCM::execute()
{
     
	// 打开文件
	QByteArray ba = filePath.toLocal8Bit();
	char* path = ba.data();
	FILE *file = fopen((const char*)path, "rb");

	//定义数组长度
	long bufferSize = fileLength / 2;
	
	//判断音频时长是否够60s
	if (bufferSize < sample_amount)
	{
     
		//音频文件时长不足60s,不需要分段
		outpath = folderStr + pcmStr.arg(1);
		QFile::copy(filePath, outpath);
		fclose(file);
		return;
	}

	//设置读取文件存储区
	short *fileBuffer = new short[bufferSize];
	//读文件
	fread(fileBuffer, sizeof(short), bufferSize, file);

	//对超过60s音频文件进行分段
	short max_value = 0; //音频文件采样值的最大值(绝对值)
	for (long i = 0; i < bufferSize; i++)
	{
     
		if (abs(fileBuffer[i]) > max_value)
			max_value = abs(fileBuffer[i]);
	}
	
	//记录分段中最小的采样点值之和,初始值设定大一些方便后续更新
	min_sum = (long)threshold_len_silence * max_value; 
	//分段数据缓冲区
	short *cutfileBuffer = new short[sample_amount];
	
	//循环执行音频分段,直到剩一段时长<60s
	while (true)
	{
     
		//从分段的位置开始,间隔步长,遍历寻找分段点
		for (position = start_position + start; position < (long)sample_amount + start - 1; position += step)
		{
     
			//计算默认静音时长下的采样值的和
			for (int i = 0; i < threshold_len_silence; i++)
			{
     
				now_sum = now_sum + (long)abs(fileBuffer[position - i]);
			}
			//判断是否最小
			if (now_sum < min_sum)
			{
     
				min_sum = now_sum;
				//best_position = position - threshold_len_silence / 2;
				best_position = position - (long)threshold_len_silence / 2;
			}
			now_sum = 0;
		}
		//复制数据并把结果写入文件
		copyData_and_writeFile(fileBuffer, cutfileBuffer, best_position - start + 1);
	
		//判断剩下的数据是否还需要分段(若剩下的数据不足60s,直接导出即可)
		start = best_position + 1;
		number++;
		if (start > bufferSize - sample_amount)
		{
     
			//复制数据并把结果写入文件
			copyData_and_writeFile(fileBuffer, cutfileBuffer, bufferSize - start + 1);
			break;
		}
		//为下次分段初始化
		now_sum = 0;
		min_sum = (long)threshold_len_silence * max_value;
	}
	delete[] cutfileBuffer;
	delete[] fileBuffer;

	fclose(file);

	//删除提取的音频文件
	QFile fileTemp(filePath);
	fileTemp.remove();
	fileTemp.close();
}


//复制数据并将其写入文件
//参数:文件存储区指针、分段数据缓冲区指针、数据长度
void SeparatePCM::copyData_and_writeFile(short *fileBuffer, short *cutfileBuffer, int len_cut)
{
     
	short *pfile = NULL; //设置原文件读取指针
	//复制数据
	pfile = fileBuffer + start;
	memcpy(cutfileBuffer, pfile, len_cut * 2);
	//把结果写入文件
	outpath = folderStr + pcmStr.arg(number);
	QByteArray qba = outpath.toLocal8Bit();
	char *cpath = qba.data();
	FILE *cfile = fopen((const char*)cpath, "wb");
	fwrite(cutfileBuffer, sizeof(short), len_cut, cfile);
	fclose(cfile);
}

四、百度api调用

这里也不再多说,请全部参考上文的B站视频吧,代码也不放了,基本是一模一样的。唯一的区别是我加上了“中文”或者“英文”的判断,在url里改变pid=1537或者1737。在这基础上,封装成了一个WriteText类。以下是代码(.cpp)部分:

#include "WriteText.h"
#include "Speech.h"
#include <qdir.h>
#include <qfile.h>
#include <qiodevice.h>


WriteText::WriteText()
{
     
}


WriteText::~WriteText()
{
     
}


void WriteText::execute(QString fileName, int id)
{
     
	QFile file(fileName);
	file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append);

	//开始识别
	//可以获取文件夹路径下的所有文件信息
	QStringList filter;
	//文件筛选,可以置为空,获取所有文件信息
	filter << QString("*.pcm");
	//找到分段后的缓冲文件夹
	QString folderStr = "D:\\temp\\temp\\";
	//获取文件夹信息,并初始化需要识别的文件
	QDir dir(folderStr);
	dir.setNameFilters(filter);
	QFileInfoList fileInfoList = dir.entryInfoList(filter);
	int dir_count = fileInfoList.count();
	QString pcmFileName("%1.pcm");
	QString fullFileName;

	for (int i = 0; i < dir_count; i++)
	{
     
		//遍历文件夹内的所有文件
		fullFileName = folderStr + pcmFileName.arg(i + 1);
		//利用百度api进行音频识别
		Speech m_speech;
		QString str = m_speech.speechIdentify(fullFileName, id);
		//将结果写入文件中
		QTextStream txtStream(&file);
		txtStream << str << "\n";

		//删除缓存的音频分段文件
		QFile fileTemp(fullFileName);
		fileTemp.remove();
		fileTemp.close();
	}
	file.close();

	//删除保存分段音频的缓存文件夹
	dir.removeRecursively();
}

另外在提醒一点就是,调用api之前,一定要先确保自己的免费额度已经领取(如下图),否则调用api失败的同时貌似还占用了次数(我也不太清楚),反正就是算是个坑吧,我就找了半天错误,查了好久才发现是这里出错了QAQ,错误码3304。
假期之不务正业—— Qt+FFmpeg+百度api进行视频的语音识别_第8张图片

五、Qt编程的一些补充

1、Qt在打开文件时,可能面对一些带有中文的字符串,我的方法是在需要支持中文的cpp最开始进行以下声明:

//设置UTF-8编码以支持中文
#if defined(_MSC_VER) && (_MSC_VER >= 1600)    
# pragma execution_character_set("utf-8")    
#endif

然后在构造函数里添加:

//设置中文编码
QTextCodec *codec = QTextCodec::codecForName("GBK");
QTextCodec::setCodecForLocale(codec);

即可。
当然GBK是windows系统下的,如果跨平台的话还需要找其他编码。

2、整个流程执行下来速度不算慢,但是也需要等待,这个时候肯定是要把运算的流程放入运算线程里面防止界面卡死。创建自定义线程类MyThread,继承于QThread,重写run函数,并定义bool值判断线程结束与否。先放代码:
MyThread.h:

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QFileInfo>
#include <QMessageBox>
#include <QTextCodec>
#include <QFile>

#include "ExtractAudio.h"
#include "SeparatePCM.h"
#include "WriteText.h"

class QString;

class MyThread : public QThread
{
     
	Q_OBJECT
public:
	MyThread();

	void setMessage(const QStringList &message);
	void setLanguage(int id);
	void stop();

protected:
	void run();

	void extracrAudio(QString strInPath, QString strOutPath); //提取音频并重采样
	QString separatePCM(QString strInPath); //音频分段
	void writeText(QString strInPath); //语音识别并将结果写入txt

private:
	QStringList str_path_list; //待处理的视频文件列表
	int languageId; //传入语种id
	volatile bool m_Stopped;

signals:
	void updateProgress(int);
	void updateLabel(QString);
};

#endif // MYTHREAD_H

MyThread.cpp:

#include "mythread.h"
#include <iostream>
using namespace std;

//设置UTF-8编码以支持中文
#if defined(_MSC_VER) && (_MSC_VER >= 1600)    
# pragma execution_character_set("utf-8")    
#endif

MyThread::MyThread()
{
     
	m_Stopped = false;

	//设置中文编码
	QTextCodec *codec = QTextCodec::codecForName("GBK");
	QTextCodec::setCodecForLocale(codec);
}

void MyThread::setMessage(const QStringList &message)
{
     
	str_path_list = message;
}

void MyThread::setLanguage(int id)
{
     
	languageId = id;
}

void MyThread::stop()
{
     
	m_Stopped = true;
}

void MyThread::run()
{
     
	while (!m_Stopped)
	{
     
		//doSomething
		QString strShowLabel;
		for (int i = 0; i < str_path_list.size(); i++)
		{
     
			QString inPath = str_path_list[i]; //单个输入文件路径
			QFileInfo fileInfo = QFileInfo(inPath); //获取输入文件信息
			QString file_name = fileInfo.fileName(); //输入文件名
			QString fileSuffix = fileInfo.suffix(); //输入文件后缀

			strShowLabel = "正在处理:" + file_name; 
			emit updateLabel(strShowLabel);

			QString outPcmName = file_name.replace(fileSuffix, "pcm"); //输出pcm文件名
			QString outPcmPath = "D:\\temp\\" + outPcmName; //输出pcm路径
			QString outTextName = file_name.replace("pcm", "txt"); //输出txt文件名
			QString outTextPath = "D:\\temp\\" + outTextName; //输出txt路径

			//下面这一段是处理步骤
			extracrAudio(inPath, outPcmPath); //提取音频并重采样
			QString temppath = separatePCM(outPcmPath); //音频分段,并获取缓冲文件夹
			writeText(outTextPath); //音频识别,并将结果写入txt中
			cout << endl;

			int v = 100 * (i + 1) / str_path_list.size();
			emit updateProgress(v);
		}
		str_path_list.clear();
		strShowLabel = tr("处理结束!");
		emit updateLabel(strShowLabel);
	}		

	m_Stopped = false;
}


//提取音频并重采样
void MyThread::extracrAudio(QString strInPath, QString strOutPath)
{
     
	//申请输出空间,先按照最大需求量申请
	uint8_t **out_data;
	int GroupSize = 1; //外层size
	int innerSize = 60 * 16000 * 2; //内层size,60s*16000Hz*2Bytes*1channel
	int maxbufferSize = 0;
	out_data = (uint8_t**)malloc(sizeof(uint8_t*)*GroupSize);
	for (int i = 0; i < GroupSize; i++)
	{
     
		out_data[i] = (uint8_t*)malloc(sizeof(uint8_t)*innerSize);
	}

	ExtractAudio ea; //创建对象
	ea.init(); //初始化

	AVFormatContext *format = ea.open(strInPath); //打开文件
	if (!format)
	{
     
		QMessageBox::warning(NULL, "提示", "打开文件失败!");
		return;
	}
	cout << "Open file successed!" << endl;

	AVCodecContext *avctx = ea.decodec(format);; //解码
	if (!avctx)
	{
     
		QMessageBox::about(NULL, "提示", "解码失败!");
		return;
	}
	cout << "Decodec successed!" << endl;

	SwrContext *swr = ea.initswr(avctx, out_data); //音频重采样初始化
	if (!swr)
	{
     
		QMessageBox::about(NULL, "提示", "音频重采样初始化失败!");
		return;
	}
	cout << "Initswr successed!" << endl;

	AVFrame *frame = av_frame_alloc(); //malloc AVFrame 并初始化
	AVPacket *pkt = av_packet_alloc(); //malloc AVPacket 并初始化
	int bufferSize = 0; //输出缓冲区的字节数
	//创建写出的pcm文件
	QFile outFile(strOutPath);
	outFile.open(QIODevice::WriteOnly);
	//读取数据
	while (av_read_frame(format, pkt) >= 0)
	{
     
		// 重采样并获取输出字节数
		bufferSize = ea.resample(format, avctx, swr, out_data, frame, pkt);

		if (bufferSize > 0) //有重采样的数据,写入文件中
			outFile.write((const char*)out_data[0], bufferSize);
		else if (bufferSize == 0) //暂无重采样的数据,继续执行
			continue;
		else //重采样出现错误,停止执行
		{
     
			QMessageBox::about(NULL, "提示", "音频重采样失败!");
			break;
		}
	}
	outFile.close();

	ea.clear(format, avctx, swr, frame, pkt); //释放空间
	cout << "ExtracrAudio Finish!" << endl;

	//释放空间
	for (int i = 0; i < GroupSize; i++)
	{
     
		free(out_data[i]);
	}
	free(out_data);
}


//音频分段
QString MyThread::separatePCM(QString strInPath)
{
     
	SeparatePCM sp; //创建对象
	bool flag = sp.open(strInPath); //打开文件
	if (!flag)
	{
     
		QMessageBox::warning(NULL, "提示", "打开音频文件失败!");
		return NULL;
	}
	sp.execute(); //音频分段
	return sp.folderStr;
	cout << "SeparatePCM Finish!" << endl;
}


//语音识别并将结果写入txt
void MyThread::writeText(QString strInPath)
{
     
	WriteText wt; //创建对象
	wt.execute(strInPath, languageId); //执行
	cout << "WriteText Finish!" << endl;
}

线程函数里,两个信号void updateProgress(int)和void updateLabel(QString)用来更新界面的进度条和便签。在MyThread里面发送信号后,在界面连接信号和槽:

connect(&m_thread, SIGNAL(updateProgress(int)), this, SLOT(updateProgress(int)));
connect(&m_thread, SIGNAL(updateLabel(QString)), this, SLOT(updateLabel(QString)));

其中信号是MyThread的信号(signals),槽是界面的槽(slots)。
而如果界面向线程发送参数的话,直接调用线程里的函数。例如在界面中有两个单选按钮来提供选择“中文”或是“英文”的功能,并且将这两个合并成一个组合:

// 设置单选按钮组合
groupButton = new QButtonGroup(this);
groupButton->addButton(ui.rbtn_Chinese, 0);
groupButton->addButton(ui.rbtn_English, 1);
ui.rbtn_Chinese->setChecked(true); //默认选择中文

在点击开始按钮时,我们就需要判断选择了哪个单选按钮,并把结果传递给运算线程:

int id = groupButton->checkedId();
m_thread.setLanguage(id);

上述的void setLanguage(int id)是线程类里的一个公共函数,直接在界面里面调用即可。把界面所确定的文件列表传递给线程类也是同理。

六、结语

内容差不多就这些了,也都是一些很新手的东西,非常欢迎大佬们给出一些好的建议(尤其是FFmpeg释放内存那里,能连带解决方案就更好了),demo就不放出来了,弄了一个半成品再放出来就觉得很惭愧。

计划以后每年都利用各种假期的时间集合起来,做个小东西,同时更新一下这个系列,做什么方向就看自己的脑洞和心情,反正是假期不务正业时间,如果有好的想法也欢迎一起学习一起做。

你可能感兴趣的:(qt,ffmpeg,语音识别)