转载:http://billxia.diandian.com/post/2012-12-23/40049402032
在Windows下,使用Microsoft Speech API(简称为SAPI)可以很简单高效的实现语音识别,关于如何使用SAPI实现语音识别的文章请参见MVP尹成的博客 :
微软语音识别语音朗读技术
VC++基于微软语音引擎开发语音识别总结
而Speech SDK安装后有一个Samples文件夹,里面有C++/C#/VB的示例代码可以参考。
现在我想把基于SAPI的语音识别转移到我的QT的项目里,也就是在QT里调用微软的SAPI来实现语音识别。这个想法是很简单的,但要实现的话,却充满了阻碍。我首先查了一下QT调用Win API的可能性,发现这是完全可以的;接下来就着手来实现了。
首次还是封装一个Speech Recognition的引擎类SREngine,其头文件和源文件分别如下:
/****************************************************
SREngine类,将MS SAPI的语音识别引擎封装,用于语音识别
SREngine.h
****************************************************/
#ifndef SRENGINE_H
#define SRENGINE_H
#include <QString.h>
#include <QMessageBox>
// Microsoft Speech API
#undef UNICODE
#include <sapi.h>
#include <sphelper.h>
#include <spuihelp.h>
#include <comdef.h>
#define UNICODE
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#define WM_RECOEVENT WM_USER+100
#define GID_SRCMD_CN 1234
#define MYGRAMMARID 101
class SREngine
{
public:
SREngine();
~SREngine();
public:
//speech varibale
CComPtr <ISpRecognizer> m_cpRecognizer;
CComPtr <ISpRecoContext> m_cpRecoContext;
CComPtr <ISpRecoGrammar> m_cpCmdGrammar;
//audio variable
CComPtr <ISpAudio> m_cpAudio;
public:
HRESULT SetRuleState(const WCHAR * pszRuleName, const WCHAR *pszValue, BOOL fActivate);
HRESULT LoadCmdFromFile(QString XMLFileName);
HRESULT InitializeSapi(WId hWnd, UINT Msg);
};
#endif // SRENGINE_H
/*****************************************************************
SREngine.cpp
*****************************************************************/
#include "SREngine.h"
SREngine::SREngine()
{
}
SREngine::~SREngine()
{
}
HRESULT SREngine::InitializeSapi(WId hWnd, UINT Msg)
{
HRESULT hr = S_OK;
//FOR ONE NOT FOR ALL
/* 独享模式的配置 */
hr = m_cpRecognizer.CoCreateInstance( CLSID_SpInprocRecognizer); //独享模式
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Create recognizer error", MB_OK);
return hr;
}
hr = SpCreateDefaultObjectFromCategoryId(SPCAT_AUDIOIN, &m_cpAudio); //建立默认的音频输入对象
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Create default audio object error", MB_OK);
return hr;
}
hr = m_cpRecognizer ->SetInput(m_cpAudio, TRUE); //设置识别引擎输入源
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error setINPUT", MB_OK);
return hr;
}
hr = m_cpRecognizer->CreateRecoContext(&m_cpRecoContext); //创建识别上下文接口
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error CreateRecoContext", MB_OK);
return hr;
}
hr = m_cpRecoContext->SetNotifyWindowMessage(hWnd, Msg, 0, 0); //设置识别消息,即将Msg消息绑定到hWnd这个窗体上,如果识别出了结果就会产生Msg这个消息,并会emit到hWnd这个窗体
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error SetNotifyWindowMessage", MB_OK);
return hr;
}
const ULONGLONG ullInterest = SPFEI(SPEI_SOUND_START) | SPFEI(SPEI_SOUND_END) |
SPFEI(SPEI_PHRASE_START) | SPFEI(SPEI_RECOGNITION) |
SPFEI(SPEI_FALSE_RECOGNITION) | SPFEI(SPEI_HYPOTHESIS) |
SPFEI(SPEI_INTERFERENCE) | SPFEI(SPEI_RECO_OTHER_CONTEXT) |
SPFEI(SPEI_REQUEST_UI) | SPFEI(SPEI_RECO_STATE_CHANGE) |
SPFEI(SPEI_PROPERTY_NUM_CHANGE) | SPFEI(SPEI_PROPERTY_STRING_CHANGE);
hr = m_cpRecoContext->SetInterest(ullInterest, ullInterest); //设置感兴趣的事件
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error set interest", MB_OK);
}
return hr;
}
HRESULT SREngine::LoadCmdFromFile(QString XMLFileName)
{
HRESULT hr = S_OK;
if(!m_cpCmdGrammar)
{
hr = m_cpRecoContext ->CreateGrammar(MYGRAMMARID, &m_cpCmdGrammar); //命令式(command and control---C&C)
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error Creategammar", MB_OK);
return hr;
}
WCHAR wszXMLFile[256] = L"";
XMLFileName.toWCharArray(wszXMLFile); //ASNI TO UNICODE
//LAOD RULE FROME XML FILE
hr = m_cpCmdGrammar->LoadCmdFromFile(wszXMLFile, SPLO_DYNAMIC);
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "Error LoadCmdFromFile", MB_OK);
return hr;
}
}
return hr;
}
HRESULT SREngine::SetRuleState(const WCHAR * pszRuleName, const WCHAR *pszValue, BOOL fActivate)
{
HRESULT hr = S_OK;
if(fActivate)
{
hr = m_cpCmdGrammar ->SetRuleState(pszRuleName, NULL, SPRS_ACTIVE);
}
else
{
hr = m_cpCmdGrammar ->SetRuleState(pszRuleName, NULL, SPRS_INACTIVE);
}
return hr;
}
在MainWindow的ui上添加一个“开启”按钮,然后给它添加槽函数:
void MainWindow:: on_pushButtonStart_clicked()
{
HRESULT hr = m_SREngine.InitializeSapi(this->winId(), WM_RECOEVENT); //初始化SAPI
if(FAILED(hr))
{
return;
}
QString grammarFileName = "../SpeechGrammar.xml";
hr = m_SREngine.LoadCmdFromFile(grammarFileName); //创建语法规则
if(FAILED(hr))
{
return;
}
/* 激活语音控制 */
hr = m_SREngine.SetRuleState(NULL, NULL, SPRS_ACTIVE);
if(FAILED(hr))
{
QMessageBox::information(NULL, "Error", "SetRuleState Active Error!", MB_OK);
return;
}
setWindowTitle("Sound Start");
ui->pushButtonStart->setEnabled(false);
}
实现的时候,一个比较棘手的问题是,如何将MFC的消息机制用QT来取代。我首先想到的当然是用信号槽机制来实现,但是这里完全跟信号槽对不上号啊,套不进去啊!!!尝试了半天,在google和baidu上搜啊搜,找到了很少比较相关的资料。没办法我就加QT技术的QQ群,问群里大牛,但可能我没描述清楚,还是解决不了。
最后,还是google搜,通过搜QT和WinAPI混合编程,找到了一两个比较好的结果,里面有提到用winEvent来截获窗体的消息,于是看到希望了,哈哈哈。我的winEvent函数如下:
bool MainWindow::winEvent(MSG* pMsg, long* result)
{
setWindowTitle("Control - Debug: winEvent");
if(pMsg->message == WM_RECOEVENT)
{
*result = this->OnRecoEvent(); //OnRecoEvent函数是具体的处理过程,其中可获取识别结果并对不同的结果做相应的处理
}
return false;
}
这里的winEvent函数就相当于WinAPI里的WinProc函数,但不需要用信号槽来显式的将信号和槽connect起来,只要有该窗体有消息产生,它就会执行。这样就将消息机制实现了,消息从SAPI的函数里产生,发送到了MainWindow窗体,再在MainWindow的winEvent函数里截获该消息进行相应的处理。
该实例的所有源文件已上传到GitHub:https://github.com/ibillxia/Demo/tree/master/QtSAPIDemo