利用讯飞语音识别技术开发离线语音控制系统(Windows平台)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sac761/article/details/52751370

本项目做出的产品是一个Windows下的增强现实系统,系统很庞大,产品功能已经基本完善,考虑到给用户带来更好的体验,故综合评估后采用讯飞语音识别方案进行嫁接。

项目介绍:

1)打开系统时启动语音识别,检测到用户说出关键词(如:上一步,下一步,结束等)时,系统自动进行相应的操作;

2)不需要按任何按键进行连续的语音识别控制,延迟为2秒左右;

3)可以识别词语,句子,中文,英文数字字母通吃,不限语速。并且语句结束判断机制很智能;

3)离线!离线!现在做一个基于windows的离线的语音识别系统在网络上还真没谁,讯飞自己也还没有推出这款SDK,我是在讯飞的离线命令词识别SDK开发包上开发出的介于两者之间功能的系统。

为什么用讯飞:

之前使用过语音识别开发版,用驻极体采集声音,通过串口传输信号来进行识别,只能讲开发版只适合用来做个课程设计啥的玩玩,做产品还是太LOW,识别率地下并且识别范围窄,貌似只能识别数字字母;之后调查过汉语语音识别库如Sphinx,Julius,百度语音等。去网上问,很多人推荐这些方案的,我没试过,不过一个SDK也只有自己正真开发过才知道好不好用强不强大,说的再牛逼也是在别人手上玩的转,到你这就不一定了。我用讯飞就是看上它的鲁棒,免费,毕竟是久经考验的平台,讯飞有一个开发者论坛,开发别人的SDK有个社区很重要,不然有问题没地方问。


项目开发经验与源码如下:

一、跑通讯飞离线命令词识别SDK

首先去讯飞官网下载相应SDK,要注册登录,绑定产品才能下载,下载后跑一跑给的demo,跑不通的话去讯飞的论坛找答案。

我用的demo是asr_record_demo,这个demo可以实现的功能是按下R键从mic中录制缓冲音频并按下S键后进行识别。

稍微看一看这个demo中的东西吧,把该删的东西删了,该修整的修整,玩转了demo才能进一步做移植。

二、文件分析与移植

把SDK包中的需要的.lib  .h  .c和所需要的其余文件拷贝到项目相应的文件夹去,最好是分个类,看起来比较清楚。它比较重要的几个文件有以下几个:

msc.lib   讯飞语音离线命令词识别所依赖的库(X64版本为msc_x64.lib)才15KB就能有如此强大的功能。

call.bnf    采用巴科斯范式语法规则的自定义语法库(call是根据自己需要命名的),用来编写语音识别过程中的语法规则,这个语法写的好语音识别更智能。

speech_recognizer.c  基于录音接口和MSC接口封装一个MIC录音识别的模块,SDK的主要文件

winrec.c  封装windows录音,Windows平台下SDK的主要文件


好了,既然demo已经知道怎么用了,把demo所依赖的所有文件和库照搬进自己的项目去,就可以在自己的项目中调用讯飞的函数了(注意头文件和库文件的路径问题)。

三、使用讲解

先看看我的语法文件

call.bnf:


巴科斯范式语法语法文件怎么写在讯飞的论坛里有很多资源自己去看吧,我写的比较简单,写得好的语法可以更加智能。


大致讲讲源码:

为了看起来简洁,我在speech_recognizer.h中做了一个类SR,把主文件中的一些函数定义和一些宏搬进去。

speech_recognizer.h:

class SR {
public:

	const char * ASR_RES_PATH = "fo|res/asr/common.jet";  //离线语法识别资源路径
#ifdef _WIN64
	const char * GRM_BUILD_PATH = "res/asr/GrmBuilld_x64";  //构建离线语法识别网络生成数据保存路径
#else
	const char * GRM_BUILD_PATH = "res/asr/GrmBuilld";  //构建离线语法识别网络生成数据保存路径
#endif
	const char * GRM_FILE = "call.bnf"; //构建离线识别语法网络所用的语法文件

	int build_grammar(UserData *udata);//构建语法
	int run_asr(UserData *udata);//启动引擎

};
speech_recognizer.c:

#pragma comment(lib,"../../lib/msc_x64.lib") //x64
#else
#pragma comment(lib,"XXXXXX/lib/msc.lib") //x86
#endif
这里的库目录要注意,用相对路径可能找不到文件,可以使用绝对路径

#ifdef _WIN64
<pre name="code" class="cpp">static int update_format_from_sessionparam(const char * session_para, WAVEFORMATEX *wavefmt)
{
	char *s;
	/*if ((s = strstr(session_para, "sample_rate"))) {
		if (s = strstr(s, "=")) {
			s = skip_space(s);
			if (s && *s) {
				wavefmt->nSamplesPerSec = atoi(s);
				wavefmt->nAvgBytesPerSec = wavefmt->nBlockAlign * wavefmt->nSamplesPerSec;
			}
		}
		else
			return -1;
	}
	else {
		return -1;
	}*/

	return 0;
}

 
 如果加到讯飞提供的demo使用的是VS2010如果放到VS2015中就会出一些语法上的错,自己看着改一改,比如上面这个函数,是更新用户词典用的,我不需要但是又存在语法错误,就把它的内容直接注释掉得了。 
 

我的项目的主文件中:

首先定义几个宏:

UserData asr_data;//语音识别用户配置
speech_rec asr;//麦克风输入存储结构体
SR sr;//语音识别实体
string SPEAKER="";//用于缓存语音识别内容
在程序初始化处配置如下:

openVoiceRecognizer = true;//语音识别开关
	if (openVoiceRecognizer)
	{
		const char *login_config = "appid = XXXXX"; //登录参数
		int ret = 0;


		ret = MSPLogin(NULL, NULL, login_config); //第一个参数为用户名,第二个参数为密码,传NULL即可,第三个参数是登录参数
		if (MSP_SUCCESS != ret) {
			printf("登录失败:%d\n", ret);
			openVoiceRecognizer = false;
		}


		memset(&asr_data, 0, sizeof(UserData));
		printf("构建离线识别语法网络...\n");
		ret = sr.build_grammar(&asr_data);  //第一次使用某语法进行识别,需要先构建语法网络,获取语法ID,之后使用此语法进行识别,无需再次构建
		if (MSP_SUCCESS != ret) {
			printf("构建语法调用失败!\n");
			openVoiceRecognizer = false;
		}


		while (1 != asr_data.build_fini)
			_sleep(300);
		if (MSP_SUCCESS != asr_data.errcode)
			_sleep(300);
		printf("离线识别语法网络构建完成,开始识别...\n");
		ret = sr.run_asr(&asr_data);//预启动语音识别引擎
	}
在初始化这里我修改了 sr.run_asr(&asr_data);这个函数,我把它调用的sr_start_listening(&asr);函数抽离出来,不让它启动识别引擎而只让它预启动,把所有结构体,变量先初始化待命。

void recognize_mic(const char* session_begin_params)  //根据自己项目需要写的语音识别预热函数
{
	int errcode;
	HANDLE helper_thread = NULL;

	struct speech_rec_notifier recnotifier = {
		on_result,
		on_speech_begin,
		on_speech_end
	};
	errcode = sr_init(&asr, session_begin_params, SR_MIC, DEFAULT_INPUT_DEVID, &recnotifier);
	if (errcode) {
		printf("speech recognizer init failed\n");
		return;
	}

	/*	while (1) {
			errcode = sr_start_listening(&asr);//我把它调用的sr_start_listening(&asr);函数抽离出来,不让它启动识别引擎而只让它预启动,把所有结构体,变量先初始化待命。
		}

	exit:
		sr_uninit(&asr);*/
}

int SR::run_asr(UserData *udata)
{
	char asr_params[MAX_PARAMS_LEN] = { NULL };
	const char *rec_rslt = NULL;
	const char *session_id = NULL;
	const char *asr_audiof = NULL;
	FILE *f_pcm = NULL;
	char *pcm_data = NULL;
	long pcm_count = 0;
	long pcm_size = 0;
	int last_audio = 0;
	int aud_stat = MSP_AUDIO_SAMPLE_CONTINUE;
	int ep_status = MSP_EP_LOOKING_FOR_SPEECH;
	int rec_status = MSP_REC_STATUS_INCOMPLETE;
	int rss_status = MSP_REC_STATUS_INCOMPLETE;
	int errcode = -1;
	int aud_src = 0;

	//离线语法识别参数设置
	_snprintf(asr_params, MAX_PARAMS_LEN - 1,         //<span style="font-family: Arial, Helvetica, sans-serif;">离线语法识别参数根据自己的需要进行更改</span>

		"engine_type = local, \
		asr_res_path = %s, sample_rate = %d, \
		grm_build_path = %s, local_grammar = %s, ",
		sr.ASR_RES_PATH,
		SAMPLE_RATE_16K,
		sr.GRM_BUILD_PATH,
		udata->grammar_id
		);

	recognize_mic(asr_params);
	return 0;
}
而后是三个语音识别的中间过程和结果处理的函数:

void on_result(const char *result, char is_last)   //根据自己的需要写结果处理
{
	char *p = "上一步";
	char *pq = "下一步";
	char *q = "扫频仪操作演示";
	char *end1 = "结束";
	char *end2 = "退出";
	if (strstr(result, p)) {
		SPEAKER = "上一步";	
	}
	else if (strstr(result, pq)) {
		SPEAKER = "下一步";
	}
	else if (strstr(result, q)) {
		SPEAKER = "扫频仪操作演示";
	}
	else if (strstr(result,end1)|| strstr(result, end2)) {
		SPEAKER = "退出";
	}
	cout << SPEAKER << endl;
}
void on_speech_begin()
{
	if (g_result)
	{
		free(g_result);
	}
	g_result = (char*)malloc(BUFFER_SIZE);
	g_buffersize = BUFFER_SIZE;
	memset(g_result, 0, g_buffersize);

	printf("Start Listening...\n");
}
void on_speech_end(int reason)
{
	if (reason == END_REASON_VAD_DETECT)
		printf("\nSpeaking done \n");
	else
		printf("\nRecognizer error %d\n", reason);
}
最后就是在自己项目哪里需要语音识别就在哪里抛出缓冲线程启动识别引擎:

if (openVoiceRecognizer) {
		
		sr_start_listening(&asr);//抛出缓冲线程进行语音识别

		if (SPEAKER=="下一步") {
			keyPressed(' ');
		}
		else if (SPEAKER == "上一步") {
			keyPressed('b');
		}
		else if (SPEAKER == "扫频仪操作演示") {
			keyPressed('1');
		}
		else if (SPEAKER=="退出") {
			std::exit(0);
		}
		SPEAKER = "";
	}

上面代码中的关键就是sr_start_listening(&asr);这个函数,前面也说了这是从run_asr()调用的方法中抽离出来的,抽离出来后run_asr()就变成了预热函数,只需在程序初始化的时候调用它后面的语音识别就不要重复调用了,节省资源。

必须要说说这个关键函数sr_start_listening(&asr);

这是讯飞这款SDK中不需要动的最后一个封装好的函数,里面的东西不要动,前面的东西配置好一调用它就可以进行语音识别了,这个函数中的东西是这款SDK的精华,实现过程很复杂不需要管,但是要记住它的几个特性:

1)一调用它就相当于抛出了一个带缓冲的新线程,这个线程独立进行语音的识别可以不干扰项目的主循环的进行。

2)这个线程寿命是自动的,程序一开始启动到这里调用这个函数后启动线程,当程序的主循环又回来这里不会重复启动这个线程,而是该线程识别完用户的语音判断语句结束机制触发后才待命,等待下一次循环的启动。这句话有些费解,用我的项目解释下:我的项目在update()函数中调用sr_start_listening(&asr); 我的update()函数每100毫秒循环一次,第一次循环启动这个函数,抛出线程进行语音识别,语音识别用了10秒结束,在这10秒过程中update()循环了100次,但是只启动该函数一次,第101次循环的时候就可以第二次启动该函数了,同时在上面代码片中我用SPEAKER转存的字符串的判断条件已经成立,就可以进行相应的操作了,在这里我触发了不同的按键来代替各种操作。

3)举个栗子,我在update()中可以在每一次循环时画一帧图片的方式实现一个视频的播放,而在这里面调用语音引擎sr_start_listening(&asr);,在引擎识别的过程中会不会中断视频的播放或者视频出现卡帧或迟滞呢?答案是不会,上面也解释了,这个函数抛出一个独立的线程,不影响主函数的循环。所以可以用这种方法实现用语音控制视频的播放:快进、后退、暂停、截屏、全屏、音量加大、切换到高清......是不是很酷!

上面的几个源码文件我打了个包,下载看可能更清楚:

http://download.csdn.net/detail/sac761/9647029

四、总结

我用这个方案实现了离线的语音控制系统,语音识别率达到百分之九十以上,而且词语句子中英文通吃,实时性强,系统鲁棒。

畅想一下用这个方法加方案开发这几个产品:

语音控制的媒体播放器,功能在上面也讲了,很酷的!

语音控制的PPT遥控器

语音控制的AR/VR系统

语音交互智能眼镜

.............





猜你喜欢

转载自blog.csdn.net/sac761/article/details/52751370