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() 中,根據識別結果產生不同動作