2009年12月15日 星期二

The Voronoi Diagram of Curved Objects.pdf

Robotic Mapping A Survey.pdf

An Algorithm for Planning Collision-Free Paths Amon Polyedral Obstacles.pdf

An Introduction to Mapping and Localization

2009年11月27日 星期五

在 Windows 環境下使用 SAPI 開發語音程式(四):語音合成

之前的文章描述怎麼使用 SAPI 達到語音辨識,相較之下,語音合成
(文字轉語音, TTS )就簡單了許多。

一樣,還是一個 .hpp 和一個 .cpp 檔。


// SpeechSynthesizer.hpp
#ifndef _SPEECHSYNTHESIZER_HPP
#define _SPEECHSYNTHESIZER_HPP

#pragma warning(disable:4995)

#include < sapi.h >
#include < sphelper.h >
#include < string >
#include < cstring >


using namespace std;

class SpeechSynthesizer
{
public:
SpeechSynthesizer();
void initSAPI();
void setSpeakerMike();
void setSpeakerSam();
void setSpeakerMary();
void setVoiceRate(int rate);
void setVoiceVolume(int vol);
void setVoicePitch(int pitch);
void setVoiceOutputWavFile(string filename);
void setVoiceOutputDefault();
void speak(string str);
void readTextFile(string filename);


// protected:
CComPtr < ISpVoice > cpVoice;
int _pitch;
};

#endif

2009年11月20日 星期五

[轉錄] 《Top 10 Traits of a Rockstar Software Engineer》:明星程式設計師必備的十項特質

本文轉錄自「猴子靈藥」

原文出處:Top 10 Traits of a Rockstar Software Engineer

這是一篇很有意思的短文。文中條列出不多不少、總共十項優秀軟體工程師所應具備的特質,並且很微妙地將軟體工程師比喻成搖滾明星。你是公司的主管嗎?按照這些特質尋找人才就對了!你是在學的學生嗎?按照這十項特質的方向努力學習就沒錯了!

在這十個特質中,我認為最關鍵、同時也是寫得最為貼切的莫過於第一點:Loves to Code


##CONTINUE##
1. 真心喜愛程式 (Loves to Code)

程式設計,是一種發自於內心、不求回報的付出 (Labor of Love)。如同任何的職業一樣,唯有具備滿滿的熱情,才能完成真正偉大的事情。一般人的誤解,常認為撰寫程式是一種機械化,或者純然科學化的行為。事實上,最棒的軟體工程師是工匠 (Craftman),能夠將能量、巧思以及創造力注入每一行的程式碼當中。優秀的工程師,知道程式碼區塊何時被琢磨至完美的程度,也知道在大型的系統中,這些區塊何時會如同謎題般巧妙地拼湊組合起來。熱愛撰寫程式的工程師所獲得的喜悅感,就像是作曲家完成一首交響樂所感受到的狂喜;而也正是這種興奮感以及成就感,使優秀的程式設計者們真心熱愛程式設計。


我個人非常、非常地喜歡以上整段的敘述。Labor of Love 是一個非常棒的形容詞,幾乎將我內心最深層的感動,完整無缺地表達了出來。是否有時會覺得累、覺得倦,或是覺得不知所做為何?不妨回頭找找自己最初的本心吧。


2. 把事情完成 (Gets Things Done)

有些技術人喜歡只說不做,而優秀的工程師是會真正去做事的人。有些人為了找出最佳的方法解決問題,會花費數週的時間設計出複雜且多餘的系統架構與函式庫;真正優秀的程式設計者應該問自己:什麼才是解決問題最容易的途徑?


請記得我們身處現實世界中,而非傳說中的理想境界,沒有所謂的完美解決方案存在。做為程式設計者,我們所應當盡力去做的事情,就是利用手邊既有的各種資源,以最有效率的方式完成交派的任務。如果不能夠把事情完成,再神妙的構思與設計都只能活在白日夢,以及那些不著邊際的大話裡。


3. 持續地重構程式 (Continuously Refactors Code)

撰寫程式,與雕刻非常相像。就像藝術家會不斷地改善自己的創作作品,程式設計者也會持續性地改造自己的程式碼,只為了以最好的方法達到需求的目標。


不要變成老舊程式碼的奴隸。當這些程式碼是由其他人撰寫的時候,你或許可以輕易地推卸責任或者怪罪於別人;但是在多數的情況下,當這些可惡的程式碼,是由你自己所撰寫的時候,才是最令自己捶胸頓足、欲哭無淚的時候。請拿出細心、耐心與愛心,勇敢地挑戰那些殘破不堪的老舊程式碼吧。


4. 使用設計模式 (Uses Design Patterns)

所謂的模式 (Pattern),是不斷重現在自然界與人類行為中的各種情境以及機制;而軟體工程也不例外。優秀的工程師能夠辨認出系統中所使用的設計模式,並且善加利用各種設計模式,同時也不受制於它們。


設計模式是前人智慧的結晶,幫助我們解決重複出現的類似設計難題,同時也成為程式設計者之間的溝通橋樑;但請記得,它們絕對不是程式設計中的萬靈藥:不要為了使用設計模式而使用設計模式;設計模式並不能使原來就很差勁的程式碼變得比較高明。


5. 撰寫測試 (Writes Tests)

有經驗的程式設計師,總是能夠瞭解撰寫測試程式碼的價值所在。測試的存在,能夠證明撰寫完成的系統運作無誤,並且確保過去曾經發生過的臭蟲問題不會再次重現。


為了進行測試而撰寫多餘的、與功能無關的程式碼?專案的進度怎麼辦?還有許多功能項目需要完成?所有的理由都是忽略撰寫測試程式碼的好理由。直到被臭蟲痛咬一口之前都是。花費心力在關鍵的程式碼區塊中撰寫測試,將能夠為你節省下難以計數的除錯時間;但很遺憾地,就我所知,目前台灣的業界並沒有撰寫測試程式碼的風氣,仍然亟待改進。


6. 善用既存程式碼 (Leverages Existing Code)

重新發明輪子一直都是軟體產業中的大問題。優秀的工程師會專注於三種不可或缺的復用 (Reuse) 層面:第一,使用同儕已經撰寫好並且經過測試的系統架構;第二,善用第三方團體所提供的函式庫;最後,則是利用某些網路服務所提供的便利功能。正確地善用既存的程式碼,才能使程式設計者專注於真正重要的任務上,也就是應用程式本身。


不要再寫第一千零一個 Linked List 類別了!不使用其他人撰寫的元件,堅持所有的功能都要由自己親手完成,究竟是自大、自爽、自衛還是自慰?請搞清楚自己的目的、專案的目標,以及核心關鍵的任務。


7. 專注於可用性 (Focuses on Usability)

好程式設計師專注於使用者。無論使用者是事業體或者個人,無論程式設計者為消費性軟體公司或者投資銀行工作,專注的焦點同樣在於可用性。優秀的程式設計者會非常努力地工作,只為了使系統更加簡單並且更為容易使用。他們無時無刻都會想到使用者,不會撰寫出錯綜複雜只有怪咖能夠理解的系統。


這是一項經常被忽略的重要特質。有時候,程式設計者寫得太開心太入迷,往往會忘了撰寫出來的程式,是需要交給其他使用者使用的東西。對於程式設計者來說,使用者的角色其實存在於許多不同的面向中,包括專案中的主程式企畫設計者,以及遊戲成品的玩家,都是開發過程中需要「常在我心」的使用者。


8. 撰寫可維護的程式碼 (Writes Maintainable Code)

工程師界的小秘密:撰寫好程式碼或者壞程式碼,所花費的時間一樣多!紀律良好的工程師,會從第一行程式碼就開始思考維護性以及程式碼未來的演化。絕對沒有任何理由寫出醜惡的程式碼、橫跨數個頁面的函式,或者帶有稀奇古怪名稱的變數。每一字、每一句、每一行的程式碼,都應該恰如其份地展示出它們原先擁有的意涵。


不要總是認為以後、未來或者某一天,一定會有機會回頭改寫那些從前寫不好的程式碼,因而和自己做出妥協,寫出只是暫時堪用的程式碼。事實上,不遵守紀律的程式撰寫方式,不僅難以節省開發的時程,更無法順利推動專案的進度。重構的觀念與程序並不是偷懶的藉口,也不能拯救一個病入膏肓的系統架構。維持良好的寫作風格、命名規則以及嚴謹的設計架構,都是非常重要的基本守則。


9. 能夠以任何程式語言撰寫程式 (Can Code in Any Language)

優秀的程式設計師或許會有個人喜愛的程式語言,但從不固執迷信於其中。在很多的情境中,程式語言的重要性往往不如那些伴隨程式語言而來的函式庫。優秀的程式設計者能夠體認這項事實,並且願意去學習新的程式語言、新的函式庫以及新的方法以建造出更好的程式系統。


對於知識,要求知若渴;對於自己,要能虛懷若谷。保持開放的心態,對新鮮的事物保持孩子般的好奇心;而不是像個「大人」般被冷漠的態度與嘲諷的言語佔據內心,困守在象牙塔中而不自知。電腦科學與軟體程式設計領域的進展飛快無比,不止要從書本中獲取知識,更要儘可能地從網路、研討會,甚至身邊的同儕,學到那些經過真實歷練的經驗與智慧。


10. 瞭解基礎的電腦科學 (Knows Basic Computer Science)

優秀的工程師需要紮實的基礎。也許你沒有資訊科系的學位,但你不能不認識其中的基礎知識:資料結構與演算法。明星級的程式設計師不但需要瞭解,更要能夠內化這些基本知識,因為擁有這些知識基礎,將能夠幫助我們在軟體系統中做出正確的設計決定。


在 90% 的狀況中,我們不會需要使用複雜可怕的資料結構或令人畏懼的演算法,但是請至少先瞭解其中最基本首要的部分。什麼時候該用 vector?什麼時候可以用 list?如果使用 deque 的話有什麼差別?應該優先考慮執行效能,或者優先考慮記憶體空間,甚至是未來擴充的彈性?不同的資料結構與演算法之間,有沒有不同的取捨?招式是死的,用的人是活的,能夠順應局勢見招拆招,才是好本事!

以上,就是為了成為超級星光大道的 Super Star Programmer 所需具備的十項基本特質。看完上述十點特質之後,是不是覺得好像還少了點什麼?是不是有某個很重要的特質沒有被列入其中?還有什麼樣的態度、能力或特徵,是你認為做為一位優秀的程式設計者所不可或缺的呢?歡迎提出來討論喔~ ^_^

2009年11月14日 星期六

2009年11月8日 星期日

ICP ( Iterative Closest Point )演算法實作( C 語言)


ICP ( Iterative Closest Point )演算法是用來將兩群點集合( point set )
對齊的演算法,這是我在去年修資工系王傑智老師的課「機器人知覺
與學習」學到的。


簡單解釋:

假設有兩個點集合 P 和 Q ,其中 P 和 Q 裡面的點數目未必一樣。
如果我知道 P 裡面的每個點都能在 Q 裡面找到相對應的點,另外我
假設經過一次平移和旋轉,可以讓 P 和 Q 之間的誤差最小(誤差的
算法:加總 P 裡面的所有點和 Q 中相對應的點之間的距離),那麼
一定可以經由計算得知所需的平移和旋轉量是多少。

##CONTINUE##不過,以上的敘述在現實應用上有個大問題,就是怎樣得知 P 、 Q
裡面的點的對應關係呢?

在這個演算法中,就是武斷的認定 Q 中「最近距離」的點就是對應
點,接著平移旋轉後,用新的點集合 P' 再迭代一次,得到 P'' ,
就這樣迭代到誤差收斂為止。

以下是我自己寫的 C 程式碼,因為學弟想要把它實驗在 DSP 裡面,
用 C 語言是比較直接的作法。


// icp.h

#ifndef _ICP_H
#define _ICP_H

typedef struct Point2D
{
double x;
double y;
} Point2D;

double p2p_length(Point2D p, Point2D q);

Point2D find_closest_point(Point2D target, Point2D* refPset, int size);

void icp_step(double* dx, double* dy, double* dth, Point2D* tgtPset, int t_size, Point2D* refPset, int r_size);

double icp(double* dx, double* dy, double* dth, Point2D* tgtPset, int t_size, Point2D* refPset, int r_size);

#endif


// icp.c

#include " icp.h "
#include < math.h >
#include < stdlib.h >
#include < stdio.h >

double p2p_length(Point2D p, Point2D q)
{
return sqrt(pow(p.x - q.x, 2)+pow(p.y - q.y, 2));
}

Point2D find_closest_point(Point2D target, Point2D* refPset, int size)
{
Point2D ret;
double min_distance;
int i;

ret = refPset[0];
min_distance = p2p_length(target, refPset[0]);
for(i=1; i < size; i++){
if(p2p_length(target, refPset[i]) < min_distance){
ret = refPset[i];
min_distance = p2p_length(target, refPset[i]);
}
}
return ret;
}

void icp_step(double* dx, double* dy, double* dth, Point2D* tgtPset, int t_size, Point2D* refPset, int r_size)
{
double sum_bx_times_ay;
double sum_by_times_ax;
double sum_bx_times_ax;
double sum_by_times_ay;
double mean_bx;
double mean_by;
double mean_ax;
double mean_ay;

Point2D* a = (Point2D*)malloc(t_size*sizeof(Point2D));
// accroding to paper, a is the closest point set
// according to paper, b is the target point set (tgtPset in this function)

int i;

// find closest point set
for(i=0; i < t_size; i++){
a[i] = find_closest_point(tgtPset[i], refPset, r_size);
}

// initialize dx, dy, dth
*dx = 0;
*dy = 0;
*dth = 0;
mean_bx = 0;
mean_by = 0;
mean_ax = 0;
mean_ay = 0;

for(i=0; i < t_size; i++){
mean_bx += tgtPset[i].x;
mean_by += tgtPset[i].y;
mean_ax += a[i].x;
mean_ay += a[i].y;
}

mean_bx = mean_bx / t_size;
mean_by = mean_by / t_size;
mean_ax = mean_ax / t_size;
mean_ay = mean_ay / t_size;

sum_bx_times_ay = 0;
sum_by_times_ax = 0;
sum_bx_times_ax = 0;
sum_by_times_ay = 0;

for(i=0; i < t_size; i++){
sum_bx_times_ay += (tgtPset[i].x - mean_bx)*(a[i].y - mean_ay);
sum_by_times_ax += (tgtPset[i].y - mean_by)*(a[i].x - mean_ax);
sum_bx_times_ax += (tgtPset[i].x - mean_bx)*(a[i].x - mean_ax);
sum_by_times_ay += (tgtPset[i].y - mean_by)*(a[i].y - mean_ay);
}

*dth = atan2(sum_bx_times_ay - sum_by_times_ax, sum_bx_times_ax + sum_by_times_ay);
*dx = mean_ax - ((mean_bx * cos(*dth)) - (mean_by * sin(*dth)));
*dy = mean_ay - ((mean_bx * sin(*dth)) + (mean_by * cos(*dth)));

}

double icp(double* dx, double* dy, double* dth, Point2D* tgtPset, int t_size, Point2D* refPset, int r_size)
{
double error = 0;
double pre_err = 0;
double step_dx, step_dy, step_dth;
double tmp_x, tmp_y;
Point2D* step_tgtPset = (Point2D*)malloc(t_size*sizeof(Point2D));

int i, iter_count;

for(i=0; i < t_size; i++){
step_tgtPset[i] = tgtPset[i];
}

*dx = 0;
*dy = 0;
*dth = 0;
iter_count = 0;
do{
iter_count++;

icp_step(&step_dx, &step_dy, &step_dth, step_tgtPset, t_size, refPset, r_size);

pre_err = error;
error = 0;
for(i=0; i < t_size; i++){
tmp_x = (step_tgtPset[i].x * cos(step_dth)) - (step_tgtPset[i].y * sin(step_dth)) + (step_dx);
tmp_y = (step_tgtPset[i].x * sin(step_dth)) + (step_tgtPset[i].y * cos(step_dth)) + (step_dy);
step_tgtPset[i].x = tmp_x;
step_tgtPset[i].y = tmp_y;

error += (pow(step_tgtPset[i].x - refPset[i].x, 2) + pow(step_tgtPset[i].y - refPset[i].y, 2));
}
error /= t_size;

*dx += step_dx;
*dy += step_dy;
*dth += step_dth;

// just for debug
printf("iter[%d]: err = %lf, dx = %lf, dy = %lf, dth = %lf\n", iter_count, error, *dx, *dy, *dth);
system("pause");
//
} while(fabs(error - pre_err) > 0.00001);

return error;
}


// test_main.c
// just for debug and test

#include "icp.h"
#include < stdlib.h >
#include < math.h >
#include < stdio.h >

double randomNumber(int hi) //the correct random number generator for [0,hi]
{
// scale in range [0,1)
double scale;
scale = (double)rand();
scale /= (double)(RAND_MAX);
// return range [0,hi]
return (scale*hi); // implicit cast and truncation in return
}
void GenerateRandomPointSet(Point2D* pSet, int p_size)
{
int i;
for(i=0; i < p_size; i++){
pSet[i].x = randomNumber(1000);
pSet[i].y = randomNumber(1000);
}
}

void MovePointSet(Point2D* pSet, int p_size, double dx, double dy)
{
int i;
for(i=0; i < p_size; i++){
pSet[i].x += dx;
pSet[i].y += dy;
}
}

void RotatePointSet(Point2D* pSet, int p_size, double dth)
{
int i;
double tmp_x, tmp_y;
for(i=0; i < p_size; i++){
tmp_x = pSet[i].x * cos(dth) - pSet[i].y * sin(dth);
tmp_y = pSet[i].x * sin(dth) + pSet[i].y * cos(dth);
pSet[i].x = tmp_x;
pSet[i].y = tmp_y;
}
}

int main()
{
Point2D tgtPset[1000];
Point2D refPset[1000];
double error;
double dx, dy, dth;

int i;

GenerateRandomPointSet(tgtPset, 1000);

for(i=0; i < 1000; i++){
refPset[i] = tgtPset[i];
}

MovePointSet(tgtPset, 1000, 10, 12);
RotatePointSet(tgtPset, 1000, 0.02);

error = icp(&dx, &dy, &dth, tgtPset, 1000, refPset, 1000);

printf("final result: err = %lf, dx = %lf, dy = %lf, dth = %lf\n", error, dx, dy, dth);
system("pause");
return 0;
}

最後補充幾點值得注意的事情:
1. 這程式碼本身是最單純的 ICP ,完全沒有任何別的加速技巧。
2. 所謂的加速技巧主要有二,其一是如何 down sampling ,因為ICP 需要的運算量很大,如果不做 down sampling ,需要 real time 的 ICP 大概就掛了。
3. 另一加速技巧在於改善點集合的資料結構,其中最有名的適合 ICP 演算法的資料結構是 K-D tree ,因為它的資料格式和距離有關,用 K-D tree 可以在尋找對應點的時候大幅降低運算時間。

2009年11月1日 星期日

在 Windows 環境下使用 SAPI 開發語音程式(三):類別核心架構


接下來針對 SpeechRecognizer 這個 class 做一個介紹。

首先注意到它的 member functions 有 void thrRun(void*) 、
void activate() 、 void deactivate() ,以及 member variable
有 bool _active ,表示我希望程式執行的時候,這個物件是可以持續
跑的(參見 A Running Object 這篇文章)。

我打算在執行的時候,用 activate() 讓 SpeechRecognizer 物件跑起
來,然後這個物件就不斷地聽有沒有使用者的命令。如果聽到有定義
的命令,就執行相應的動作。

接著就是為了使用 SAPI ,另外需要的 member functions 和
member variables 。

首先是 initSAPI() ,

##CONTINUE##

void SpeechRecognizer::initSAPI()
{
// 首先要初始化 COM 元件
if(FAILED(::CoInitialize(NULL))){
exitError(TEXT("init COM Failed!"));
}

HRESULT hr;
// 初始化語音識別引擎
hr = _cpEngine.CoCreateInstance(CLSID_SpSharedRecognizer);
if(FAILED(hr)){
cleanupSAPI();
exitError(TEXT("_cpEngine.CoCreateInstance"));
}

// 初始化語音識別內容
hr = _cpEngine->CreateRecoContext( &_cpRecoCtxt );
if(FAILED(hr)){
cleanupSAPI();
exitError(TEXT("_cpEngine->CreateRecoContext"));
}

// 設定當識別事件發生的時候,是用什麼方式通知程式
// 絕大多數的說明文件都是採用 Win32 Message 來實作,
// 但我不想被 Win32 API 的那套框架限制住,
// 所以我用 NotyfyWin32Event 的方式
hr = _cpRecoCtxt->SetNotifyWin32Event();
if(FAILED(hr)){
cleanupSAPI();
exitError(TEXT("_cpRecoCtxt->SetNotifyWin32Event"));
}

// SAPI 有自己定義一些事件,像是識別成功、識別失敗
// 或是語音剛開始、語音結束等等
// 要先告訴 SAPI 哪些事件是我們有興趣的
hr = _cpRecoCtxt->SetInterest(SPFEI(SPEI_RECOGNITION), SPFEI(SPEI_RECOGNITION));
if(FAILED(hr)){
cleanupSAPI();
exitError(TEXT("_cpRecoCtxt->SetInterest"));
}

// 初始化文法(Grammar)
hr = _cpRecoCtxt->CreateGrammar(0, &_cpGrammar);
if(FAILED(hr)){
cleanupSAPI();
exitError(TEXT("_cpRecoCtxt->CreateGrammar"));
}

// 載入定義好的文法檔案,一個 xml 檔
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"));
}
}
當 SAPI 的初始化和設定都完成以後,接下來就要定義不同的識別結
果要有哪些不同的反應。這部份定義在 executeCommand() 裡面,另
外對照文法的 xml 檔比較容易看懂。

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:
cout << "The action of RULE " << pElements->Rule.ulId << " is not define." << endl;
;
}
}
}







robot




hello




renbot




Chien Hao



應該不難猜,當辨識成功之後,結果最後會傳到 executeCommand()
這個 function 中。其中 Rule.ulId 就是在文法檔中定義的 ID 。

cleanSAPI 顧名思義,當我們要結束程式前,需要先釋放 COM 還有 SAPI
的資源,這裡就不贅述了。

最後是很關鍵的 thrRun():

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

HRESULT hr;
while(pSR->_active){
// 等待識別結果,有結果立刻返回
// 沒有結果的話,每 100 ms 返回一次
hr = pSR->_cpRecoCtxt->WaitForNotifyEvent(100);

// 返回後,檢查是否有識別結果
if(hr == 0){
// 有結果的話,呼叫 executeCommand()
// 並且將結果當參數傳過去
CSpEvent event;
event.GetFrom(pSR->_cpRecoCtxt);
pSR->executeCommand(event.RecoResult());
}
}
_endthread();
}
看以上程式碼以後,稍微歸納基本流程:
1. 初始化 COM 元件和 SAPI
2. 物件啟動,等待 Win32 event
3. 一旦有 event 出現,呼叫 executeCommand() ,並且將結果當參數傳入
4. 在 executeCommand() 中,根據識別結果產生不同動作

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 中這樣呼叫:

2009年9月20日 星期日

Hello World