2009年10月24日 星期六

在 Windows 環境下使用 SAPI 開發語音程式(二): 程式碼概觀


我個人在 coding 的時候有個癖好,就是喜歡極簡風。所以當我要寫
一個軟體模組,我都會盡量堅持一個 .cpp 和一個 .hpp 。

先把 code 秀出來好了。

// SpeechRecognizer.hpp
#ifndef _SPEECHRECOGNIZER_HPP
#define _SPEECHRECOGNIZER_HPP

#include < sphelper.h >
#include < windows.h >

class SpeechRecognizer
{
public:
void activate();
void deactivate();
static void thrRun(void*);

void initSAPI();
void cleanupSAPI();
void executeCommand(ISpPhrase *pPhrase);
void exitError(LPTSTR lpszFunction);
//protected:
// for running
bool _active;

// for SAPI
CComPtr< ISpRecoContext > _cpRecoCtxt;
CComPtr< ISpRecoGrammar > _cpGrammar;
CComPtr< ISpRecognizer > _cpEngine;
};
#endif

##CONTINUE##

// SpeechRecognizer.cpp

#include "SpeechRecognizer.hpp"
#include < process.h >
#include < iostream >

using namespace std;

void SpeechRecognizer::activate()
{
initSAPI();
_active = true;
_beginthread(SpeechRecognizer::thrRun, 0, this);
}

void SpeechRecognizer::deactivate()
{
_active = false;
cleanupSAPI();
}

void SpeechRecognizer::thrRun(void* ptr)
{
SpeechRecognizer* pSR = (SpeechRecognizer*)ptr;

HRESULT hr;
while(pSR->_active){
hr = pSR->_cpRecoCtxt->WaitForNotifyEvent(100);
if(hr == 0){
CSpEvent event;
event.GetFrom(pSR->_cpRecoCtxt);
pSR->executeCommand(event.RecoResult());
}
}
_endthread();
}

void SpeechRecognizer::initSAPI()
{
if(FAILED(::CoInitialize(NULL))){ exitError(TEXT("init COM Failed!")); }

HRESULT hr;

// create a recognition engine
hr = _cpEngine.CoCreateInstance(CLSID_SpSharedRecognizer);
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpEngine.CoCreateInstance")); }

// create the command recognition context
hr = _cpEngine->CreateRecoContext( &_cpRecoCtxt );
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpEngine->CreateRecoContext")); }

// Let SR know that window we want it to send event information to, and using
// what message
hr = _cpRecoCtxt->SetNotifyWin32Event();
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpRecoCtxt->SetNotifyWin32Event")); }

// Tell SR what types of events interest us. Here we only care about command
// recognition.
hr = _cpRecoCtxt->SetInterest(SPFEI(SPEI_RECOGNITION), SPFEI(SPEI_RECOGNITION));
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpRecoCtxt->SetInterest")); }

// Load our grammar, which is the compiled form of simple.xml bound into this executable as a
hr = _cpRecoCtxt->CreateGrammar(0, &_cpGrammar);
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpRecoCtxt->CreateGrammar")); }

hr = _cpGrammar->LoadCmdFromFile(L"test.xml", SPLO_DYNAMIC);
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpCmdGrammar->LoadCmdFromFile")); }

// Set rules to active, we are now listening for commands
hr = _cpGrammar->SetRuleState(NULL, NULL, SPRS_ACTIVE);
if(FAILED(hr)){ cleanupSAPI(); exitError(TEXT("_cpGrammar->SetRuleState")); }
}

void SpeechRecognizer::cleanupSAPI()
{
// Release grammar, if loaded
if (_cpGrammar){
_cpGrammar.Release();
}
// Release recognition context, if created
if (_cpRecoCtxt){
_cpRecoCtxt->SetNotifySink(NULL);
_cpRecoCtxt.Release();
}
// Release recognition engine instance, if created
if (_cpEngine){
_cpEngine.Release();
}
CoUninitialize();
}

void SpeechRecognizer::executeCommand(ISpPhrase *pPhrase)
{
SPPHRASE* pElements;
if(SUCCEEDED(pPhrase->GetPhrase(&pElements))){
switch(pElements->Rule.ulId){
case 1:
cout << "robot" << endl;
break;
case 2:
cout << "hello" << endl;
break;
default:
;
}
}
}
void SpeechRecognizer::exitError(LPTSTR lpszFunction)
{
// Retrieve the system error message for the last-error code
LPVOID lpMsgBuf;
LPVOID lpDisplayBuf;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
// Display the error message and exit the process
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
(lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR));
StringCchPrintf((LPTSTR)lpDisplayBuf,
LocalSize(lpDisplayBuf) / sizeof(TCHAR),
TEXT("%s failed with error %d: %s"),
lpszFunction, dw, lpMsgBuf);
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);

LocalFree(lpMsgBuf);
LocalFree(lpDisplayBuf);
ExitProcess(dw);
}




跟一般程式不同的地方在於, SAPI 需要「文法」,才能真正擁有語音辨識的能力。

通常關於文法需要由另一個 .xml 檔案提供。



robot
hello

2009年10月23日 星期五

在 Windows 環境下使用 SAPI 開發語音程式(一): SAPI 安裝與環境設定

很早前就想寫這系列文章,現在總算可以如願。

由於研究上的需要(機器人需要語音能力),我需要開發一套軟體
模組給機器人用。

##CONTINUE##

在我之前的學長們都是沿用一套叫做 VR-Stamp 的 IC 和它的開發
板來實現語音功能,但是我發現在國外的機器人競賽中,語音處理
好像是很平常的功能,不太像是要另外買什麼硬體開發板來做(而
且 VR-Stamp 開發板要價將近 20,000 ,一般研究根本無法負擔)

於是我做了點功課,就發現了 SAPI (Microsoft Speech API)。

無奈這套 API 雖然號稱功能強大,但是它的說明文件相當不友善,
我翻遍網路都找不到比較有系統性的資料,最後只好自己看著文件
,還有好不容易跑起來的教學範例,抄一段猜一行的拼湊出我需要
的程式。

首先,下載 SAPI 來安裝。(XP 下好像最高支援到 5.1 ,至於 5.3
以上要 Vista 才有支援)

接著就是第一個會遇到的問題,我差點因此而放棄,那就是 tutorial
裡面的範例完全無法執行,會出現「 SAPI failed to initialize.
The application will now shut down. 」的錯誤訊息。

找了很久才發現問題的主因在於語言。範例的預設語音辨識引擎是
英文,但是我的 XP 是中文版。需要先進入「控制台→語音」,把
「語系」改成「Microsoft English Recognizer 5.1」即可。

接著就會發現可以正常執行 coffee0 這個 tutorial example 了。
但可能怎麼講電腦都沒有反應,原因在於語音資料庫需要先經過訓
練,不然辨識度一定很低。也是一樣進入「控制台→語音」,然後
點選「訓練設定檔」,接下來就會有一段訓練的過程(記得要先裝
麥克風),照做即可。

之後再開啟 coffee0 ,對麥克風說「 go to the store 」,應該
就會有反應了。

(待續)

2009年10月11日 星期日

A Running Instance

約莫一年前我為了寫一個控制機器人的程式,
需要有個在背景不斷運作的物件,
但是又不想用 MFC ,只想沿用過去習慣的 C++ empty project ,
加上 windows.h 、 process.h 這兩個 header file 。

研究了一陣子,終於有了點成果。而且還意外發現,
有間專門開發機器人行動平台的公司開放的 open source 裡面,這個部份和我的作法一模一樣。

假設我打算產生一個 thread 讓 Runner 這個 class 的物件不斷運作,
 // Runner.hpp 
#include < windows.h >
#include < process.h >
class Runner
{
public:
Runner();
void activate();
static void thrRun(void*);
void deactivate();
protected:
bool _activate;
}



##CONTINUE##





其中最關鍵的部份就在於 _beginthread 這個 function 的第一個參數不能接受 class member function ,因為在 compile 的過程中, class 的 member function 都會多一個 this 的 parameter ,但是 _beginthread 的第一個 parameter 只接受 function prototype 長得像

void function (void*) 這樣的 function ,

解決之道就在於宣告一個 static 的 function 來避開 this 的問題,然後再把物件的 pointer 當參數傳進去。關於這個問題的資料不少,這裡就不贅述了。

當我想要讓一個 Runner 物件在背景不斷跑,可以在 main 中這樣呼叫: