1. 程式人生 > >利用訊飛語音識別技術開發離線語音控制系統(Windows平臺)

利用訊飛語音識別技術開發離線語音控制系統(Windows平臺)

本專案做出的產品是一個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);,在引擎識別的過程中會不會中斷視訊的播放或者視訊出現卡幀或遲滯呢?答案是不會,上面也解釋了,這個函式丟擲一個獨立的執行緒,不影響主函式的迴圈。所以可以用這種方法實現用語音控制視訊的播放:快進、後退、暫停、截圖、全屏、音量加大、切換到高清......是不是很酷!

上面的幾個原始碼檔案我打了個包,下載看可能更清楚:

四、總結

我用這個方案實現了離線的語音控制系統,語音識別率達到百分之九十以上,而且詞語句子中英文通吃,實時性強,系統魯棒。

暢想一下用這個方法加方案開發這幾個產品:

語音控制的媒體播放器,功能在上面也講了,很酷的!

語音控制的PPT遙控器

語音控制的AR/VR系統

語音互動智慧眼鏡

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