现在语音识别技术逐渐发展,先有siri开个好头,现在有各种小度小爱什么的轮番上阵。王者荣耀有语音识别以后,祖安起来也省事多了。我看一些视频教程的时候,对一些讲的不错的,也有记笔记的习惯。可是每次都是把视频暂停,然后一句一句敲出word,说实话,也没见学习效果有多好,反而效率变得低到不行。想来想去,咱也不能一直停留在这么笨比的方式,总是想整点活。
其实网上就有一些提取字幕的、或是语音识别的应用,应该效果也不错(我没试),但是要钱(emmmmm)。所以暂时先放弃这个方案,而且如果自己做一个那不是快乐加倍?于是利用假期时间,自己找了一些资料借(chao)鉴(xi)了一下,也是算是自己从零开始做的垃圾。
先放一下目前做到的:
我主要选择了4个B站的视频来测试运行结果,顺便一提,B站用手机端下载视频后,会在缓存文件里发现audio.m4s和vedio.m4s。实际上,FFmpeg可以直接打开m4s格式,因此如果仅仅是为了对音频进行处理,不需要将两个文件合流为一个(合流的方法也很简单,尤其是使用FFmpeg,可以直接百度)。
我选择的4个视频分别是冰冰vlog、卢本伟17张牌名场面、小潮院长的不要做挑战和吴恩达老师的机器学习课程,链接放在下文,我这里就夹带私货安利一波。下面是识别结果:
简单复盘一下:识别结果也算还可以,与英文相比,中文能够带上标点符号看起来更利落一些。显然,语速放慢,说话更标准时,识别效果更好(这不是废话吗)。可以看到小潮的不要做挑战的前面正经讲游戏规则时,识别结果还能接受,到后面整活了,识别结果驴唇不对马嘴。对于语速中规中矩的视频(尤其对于目的:视频教程),能有一些帮助;但如果是小视频(尤其节奏比较快的),那还是算了吧。
总体思路就是: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的介绍、使用,可以直接看前言的链接,或者找其他教程,这里也直接梳理一下我们需要做的事情和以及整个过程:
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);
}
这里也不再多说,请全部参考上文的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。
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就不放出来了,弄了一个半成品再放出来就觉得很惭愧。
计划以后每年都利用各种假期的时间集合起来,做个小东西,同时更新一下这个系列,做什么方向就看自己的脑洞和心情,反正是假期不务正业时间,如果有好的想法也欢迎一起学习一起做。