C++實現TTS文字語音朗讀Microsoft Speech SDK

轉自http://www.zhimax.com/article/vc/ttsvoice.html

一. TTS概述

隨着語音技術的發展,微軟也推出了相應的語音開發工具,即Microsoft Speech SDK,這個SDK中包含了語音應用設計接口(SAPI)、微軟的連續語音識別引擎(MCSR)以及微軟的語音合成(TTS)引擎等等。它其中的 TTS(text-to-speech)引擎可以用於實現語音合成,我們通過TTS引擎可以分析文本內容並且將其朗讀出。實現TTS技術的方法有很多種, 現在主要採用三種:連詞技術、語音合成技術、子字連接技術。目前的5.1版本的SDK一共可以支持3種語言的識別 (英語,漢語和日語)以及2種語言的合成(英語和漢語)。其中還包括對於低層控制和高度適應性的直接語音管理、訓練嚮導、事件、語法編譯、資源、語音識別 (SR)管理以及TTS管理等強大的設計接口。

二. 實現原理

以下是SpeechAPI的總體結構:

從圖中我們可以看出語音引擎則通過DDI層(設備驅動接口)和SAPI(SpeechAPI)進行交互,應用程序通過API層和SAPI通信。通過使用這些API,用戶可以快速開發在語音識別或語音合成方面應用程序。

應用程序使用ISpVoice接口來控制TTS,通過調用其中的Speak方法可以朗讀出文本內容,通過調用SetVoice / GetVoice方法(在.NET中已經轉變成Voice屬性)來獲取或設置朗讀的語音,而通過調用GetVolume / SetVolume、GetRate / SetRate等方法(在.NET中已經轉變成Volume和Rate屬性)來獲取或設置朗讀的音量和語速。

功能強大之處在於TTS能識別XML標記,通過給文本加上XML標記,我們讓TTS朗讀出更加符合語言閱讀習慣的句子。例如:

l 用於設置文本朗讀的音量;

l 、 分別用於設置文本朗讀的絕對速度和相對速度;

l 、 分別用於設置文本朗讀的絕對語調和相對語調;

l 在他們之間的句子被視爲強調;

l 可以將單詞逐個字母的拼寫出來;

l 表示停止發聲,並保持500微秒;

l 02/03/07 可以按要求朗讀出日期

l 用於設置朗讀所用的語言,其中409表示使用英語,804表示使用漢語,而411表示日語。

三. 軟件的開發

1.首先開發得需要Microsoft Speech SDK的支持,以下是下載地址

http://msdn.microsoft.com/code/sample.asp?url=/msdn-files/027/000/781/msdncompositedoc.xml
Speech SDK 5.1 (68 MB)
http://download.microsoft.com/download/speechSDK/SDK/5.1/WXP/EN-US/speechsdk51.exe
5.1 Language Pack (81.5 MB)
http://download.microsoft.com/download/speechSDK/SDK/5.1/WXP/EN-US/speechsdk51LangPack.exe
Redistributables (128 MB)
http://download.microsoft.com/download/speechSDK/SDK/5.1/WXP/EN-US/speechsdk51MSM.exe
Documentation (2.28 MB)
http://download.microsoft.com/download/speechSDK/SDK/5.1/WXP/EN-US/sapi.chm

2.下載後,執行安裝

下載完畢後首先安裝SpeechSDK51.exe,然後安裝中文語言補丁包SpeechSDK51LangPack,然後展開
speechsdk51MSM.exe,這些都是自解壓文件,解壓後執行相應的setup程序到你要的目錄,默認C:\Microsoft Speech SDK 5.1.對應的開發參考手冊爲sapi.chm,詳細描述了各個函數的細節等.

3.VC的環境配置

在應用SDK的開發前當然得需要對工程環境進行配置,我用的是VS2003(其他情況類似),配置的過程如下:

工具->選項->項目->VC++目錄,在"顯示以下內容的目錄"下拉框中選擇"包含目錄"項,添加一項C:\Program Files\Microsoft Speech SDK 5.1\Include到目錄中去。再選擇"庫文件"項,添加一項C:\Program Files\Microsoft Speech SDK 5.1\Lib\i386到目錄中去.

4.其他準備項

基礎的配置已經完成,那麼接下來的工作就是要包含編譯的頭文件了,所以先將頭文件和庫文件包含進來

#include
#include
#include

#pragma comment(lib,"ole32.lib") //CoInitialize CoCreateInstance需要調用ole32.dll
#pragma comment(lib,"sapi.lib") //sapi.lib在SDK的lib目錄,必需正確配置

具體其他函數所需要的頭文件可參考sapi.chm手冊.

5.源文件修改項

看上去上面的部分配置完成後就大功告成了,其實還不全是,當你編譯時就會出錯:
c:\program files\microsoft speech sdk 5.1\include\sphelper.h(769) : error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
c:\program files\microsoft speech sdk 5.1\include\sphelper.h(1419) : error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
c:\program files\microsoft speech sdk 5.1\include\sphelper.h(2373) : error C2065: 'psz' : undeclared identifier
c:\program files\microsoft speech sdk 5.1\include\sphelper.h(2559) : error C2440: 'initializing' : cannot convert from 'CSpDynamicString' to 'SPPHONEID *'
No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
c:\program files\microsoft speech sdk 5.1\include\sphelper.h(2633) : error C2664: 'wcslen' : cannot convert parameter 1 from 'SPPHONEID *' to 'const wchar_t *'
Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
Speech代碼編寫時間太早,語法不嚴密。而VS2003(及以上)對於語法檢查非常嚴格,導致編譯無法通過。修改頭文件中的以下行即可正常編譯:
Line 769
修改前: const ulLenVendorPreferred = wcslen(pszVendorPreferred);
修改後: const unsigned long ulLenVendorPreferred = wcslen(pszVendorPreferred);
Line 1418
修改前: static CoMemCopyWFEX(const WAVEFORMATEX * pSrc, WAVEFORMATEX ** ppCoMemWFEX)
修改後: static HRESULT CoMemCopyWFEX(const WAVEFORMATEX * pSrc, WAVEFORMATEX ** ppCoMemWFEX)
Line 2372
修改前: for (const WCHAR * psz = (const WCHAR *)lParam; *psz; psz++) {}
修改後: const WCHAR * psz; for (psz = (const WCHAR *)lParam; *psz; psz++) {}
Line 2559
修改前: SPPHONEID* pphoneId = dsPhoneId;
修改後: SPPHONEID* pphoneId = (SPPHONEID*)((WCHAR *)dsPhoneId);
Line 2633
修改前: pphoneId += wcslen(pphoneId) + 1;
修改後: pphoneId += wcslen((const wchar_t *)pphoneId) + 1;
好了,編譯通過,下面可以正式編寫程序了。
6.SAPI實現TTS(Text to Speech)
1. 首先要初始化語音接口,一般有兩種方式:
ISpVoice* pVoice;
::CoInitialize(NULL);
HRESULT hr =CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL,IID_ISpVoice, (void **)&pVoice);
然後就可以使用這個指針調用SAPI函數了,例如
pVoice->SetVolume(50);//設置音量
pVoice->Speak(str.AllocSysString(),SPF_ASYNC,NULL);

另外也可以使用如下方式:
CComPtr m_cpVoice;
HRESULT hr = m_cpVoice.CoCreateInstance(CLSID_SpVoice );
在下面的例子中都用這個m_cpVoice變量。CLSID_SpVoice的定義位於sapi.h中。
2. 獲取/設置輸出頻率。

SAPI朗讀文字的時候,可以採用多種頻率方式輸出聲音,比如:8kHz 8Bit Mono、8kHz 8BitStereo、44kHz 16BitStereo等,在音調上有所差別。具體可以參考sapi.h。

可以使用如下代碼獲取當前的頻率配置:
CComPtr cpStream;
HRESULT hrOutputStream =m_cpVoice->GetOutputStream(&cpStream);
if (hrOutputStream ==S_OK)
{
CSpStreamFormat Fmt;
hr = Fmt.AssignFormat(cpStream);
if (SUCCEEDED(hr))
{
SPSTREAMFORMAT eFmt = Fmt.ComputeFormatEnum();
}
}
SPSTREAMFORMAT 是一個ENUM類型,定義位於sapi.h中,這樣eFmt就保存了獲得的當前頻率設置值。每一個值對應了不同的頻率設置。

通過如下代碼設置當前朗讀頻率:
CComPtr m_cpOutAudio; //聲音輸出接口
SpCreateDefaultObjectFromCategoryId( SPCAT_AUDIOOUT,&m_cpOutAudio ); //創建接口

SPSTREAMFORMAT eFmt = SPSF_8kHz8BitMono; //SPSF_8kHz 8Bit Mono這個參數可以參考sapi.chm手冊

CSpStreamFormat Fmt;
Fmt.AssignFormat(eFmt);
if (m_cpOutAudio )
{
hr = m_cpOutAudio->SetFormat(Fmt.FormatId(), Fmt.WaveFormatExPtr() );
}
else hr = E_FAIL;

if(SUCCEEDED( hr ) )
{
m_cpVoice->SetOutput( m_cpOutAudio, FALSE );
}
3. 獲取/設置播放所用語音。

引擎中所用的語音數據文件一般保存在SpeechEngines下的spd或者vce文件中。安裝sdk後,在註冊表中保存了可用的語音,比如英文的男/女,簡體中文的男音等。位置是:
HKEY_LOCAL_MACHINE\Software\Microsoft\Speech\Voices\Tokens
SAPI的缺點是不能支持中英文混讀,在朗讀中文的時候,遇到英文,只能逐個字母讀出。所以需要程序自己進行語音切換。

(1) 可以採用如下的函數把當前SDK支持的語音填充在一個組合框中:
// SAPI5helper function in sphelper.h

CWnd* m_wnd = GetDlgItem(IDC_COMBO_VOICES);
HWND hWndCombo = m_wnd->m_hWnd; //組合框句柄
HRESULT hr =SpInitTokenComboBox( hWndCombo , SPCAT_VOICES );
這個函數是通過IEnumSpObjectTokens接口枚舉當前可用的語音接口,把接口的說明文字添加到組合框中,並且把接口的指針作爲LPARAM 保存在組合框中。
一定要記住最後程序退出的時候,釋放組合框中保存的接口:
SpDestroyTokenComboBox( hWndCombo );
這個函數的原理就是逐個取得combo裏面每一項的LPARAM數據,轉換成IUnknown接口指針,然後調用Release函數。
(2) 當組合框選擇變化的時候,可以用下面的函數獲取用戶選擇的語音:
ISpObjectToken* pToken = SpGetCurSelComboBoxToken( hWndCombo );

(3) 用下面的函數獲取當前正在使用的語音:
CComPtr pOldToken;
HRESULT hr = m_cpVoice->GetVoice( &pOldToken);
(4) 當用戶選擇的語音和當前正在使用的不一致的時候,用下面的函數修改:
if(pOldToken != pToken)
{
// 首先結束當前的朗讀,這個不是必須的。
HRESULT hr = m_cpVoice->Speak( NULL,SPF_PURGEBEFORESPEAK, 0);
if (SUCCEEDED (hr) )
hr = m_cpVoice->SetVoice( pToken );
}
(5) 也可以直接使用函數SpGetTokenFromId獲取指定voice的Token指針,例如:
WCHAR pszTokenId[] =L"HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Speech\\Voices\\Tokens\\MSSimplifiedChineseVoice";
SpGetTokenFromId(pszTokenID , &pChineseToken);
4 開始/暫停/恢復/結束當前的朗讀

要朗讀的文字必須位於寬字符串中,所以從文本框中讀取的字符串類型CString必須轉換成爲WCHAR型,如下(m_strText爲文本框變量):
CString strSpeak;
m_strText.GetWindowText(strSpeak);
WCHAR wChar[256];
memset(wChar ,0,256);
MultiByteToWideChar( CP_ACP , 0 , strSpeak , strSpeak.GetLength() , wChar , 256);
這樣就將文本框中的字符串strSpeak轉化爲WCHAR型的wChar變量中了.
開始朗讀的代碼:
hr =m_cpVoice->Speak( wChar, SPF_ASYNC |SPF_IS_NOT_XML, 0 );
如果要解讀一個XML文本,用:
hr =m_cpVoice->Speak( wChar, SPF_ASYNC |SPF_IS_XML, 0 );

暫停的代碼: m_cpVoice->Pause();
恢復的代碼: m_cpVoice->Resume();
結束的代碼:(上面的例子中已經給出了)
hr =m_cpVoice->Speak( NULL, SPF_PURGEBEFORESPEAK,0);
5 跳過部分朗讀的文字

在朗讀的過程中,可以跳過部分文字繼續後面的朗讀,代碼如下:
ULONG ulGarbage = 0;
WCHAR szGarbage[] =L"Sentence";
hr =m_cpVoice->Skip( szGarbage, SkipNum,&ulGarbage );
SkipNum是設置要跳過的句子數量,值可以是正/負。
根據sdk的說明,目前SAPI僅僅支持SENTENCE這個類型。SAPI是通過標點符號來區分句子的。
6 播放WAV文件。SAPI可以播放WAV文件,這是通過ISpStream接口實現的:

CComPtr cpWavStream;
WCHAR szwWavFileName[NORM_SIZE] = L"";

USES_CONVERSION;
wcscpy( szwWavFileName, T2W(szAFileName ) );//從ANSI將WAV文件的名字轉換成寬字符串

//使用sphelper.h 提供的這個函數打開wav 文件,並得到一個 IStream 指針
hr = SPBindToFile(szwWavFileName, SPFM_OPEN_READONLY, &cpWavStream);
if( SUCCEEDED( hr ) )
{
m_cpVoice->SpeakStream( cpWavStream, SPF_ASYNC, NULL);//播放WAV文件
}
7 將朗讀的結果保存到wav文件
TCHARszFileName[256];//假設這裏面保存着目標文件的路徑
USES_CONVERSION;
WCHAR m_szWFileName[MAX_FILE_PATH];
wcscpy( m_szWFileName,T2W(szFileName) );//轉換成寬字符串

//創建一個輸出流,綁定到wav文件
CSpStreamFormat originalFmt;
CComPtr cpWavStream;
CComPtr cpOldStream;
HRESULT hr =m_cpVoice->GetOutputStream(&cpOldStream );
if (hr == S_OK) hr =OriginalFmt.AssignFormat(cpOldStream);
else hr =E_FAIL;
// 使用sphelper.h中提供的函數創建 wav文件
if (SUCCEEDED(hr))
{
hr = SPBindToFile( m_szWFileName, SPFM_Create_ALWAYS,&cpWavStream,&OriginalFmt.FormatId(),OriginalFmt.WaveFormatExPtr() );
}
if( SUCCEEDED( hr ) )
{
//設置聲音的輸出到 wav 文件,而不是speakers
m_cpVoice->SetOutput(cpWavStream, TRUE);
}
//開始朗讀
m_cpVoice->Speak( wChar, SPF_ASYNC |SPF_IS_NOT_XML, 0 );

//等待朗讀結束
m_cpVoice->WaitUntilDone( INFINITE );
cpWavStream.Release();

//把輸出重新定位到原來的流
m_cpVoice->SetOutput( cpOldStream, FALSE );
8 設置朗讀音量和速度
m_cpVoice->SetVolume((USHORT)hpos); //設置音量,範圍是 0 -100
m_cpVoice->SetRate(hpos); //設置速度,範圍是 -10 - 10
9 設置SAPI通知消息。

SAPI在朗讀的過程中,會給指定窗口發送消息,窗口收到消息後,可以主動獲取SAPI的事件,根據事件的不同,用戶可以得到當前SAPI的一些信 息,比如正在朗讀的單詞的位置,當前的朗讀口型值(用於顯示動畫口型,中文語音的情況下並不提供這個事件)等等。要獲取SAPI的通知,首先要註冊一個消 息:
m_cpVoice->SetNotifyWindowMessage( hWnd,WM_TTSAPPCUSTOMEVENT, 0, 0 );
這個代碼一般是在主窗口初始化的時候調用,hWnd是主窗口(或者接收消息的窗口)句柄。WM_TTSAPPCUSTOMEVENT是用戶自定義消息。在窗口響應WM_TTSAPPCUSTOMEVENT消息的函數中,通過如下代碼獲取sapi的通知事件:

CSpEvent event; // 使用這個類,比用 SPEVENT結構更方便

while(event.GetFrom(m_cpVoice) == S_OK )
{
switch( event.eEventId )
{
...
}
}

eEventID有很多種,比如SPEI_START_INPUT_STREAM表示開始朗讀,SPEI_END_INPUT_STREAM表示朗讀結束等。
可以根據需要進行判斷使用。

7.總結

還有一些關於xml的支持可以參考sapi.chm幫助手冊,感謝網絡原作提供的資源,有iwaswzq,yaooo等。

先看一個入門的例子:

#include <sapi.h>

#pragma comment(lib,"ole32.lib") //CoInitialize CoCreateInstance需要調用ole32.dll
#pragma comment(lib,"sapi.lib") //sapi.lib在SDK的lib目錄,必需正確配置
int main(int argc, char* argv[])
{
ISpVoice * pVoice = NULL;

//COM初始化:
if (FAILED(::CoInitialize(NULL)))
return FALSE;

//獲取ISpVoice接口:
HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice);
if( SUCCEEDED( hr ) )
{
hr = pVoice->Speak(L"Hello world", 0, NULL);
pVoice->Release();
pVoice = NULL;
}

//千萬不要忘記:
::CoUninitialize();
return TRUE;
}


短短20幾行代碼就實現了文本語音轉換,夠神奇吧。SDK提供的SAPI是基於COM封裝的,無論你是否熟悉COM,只要按部就班地用 CoInitialize(), CoCreateInstance()獲取IspVoice接口就夠了,需要注意的是初始化COM後,程序結束前一定要用 CoUninitialize()釋放資源。

   IspVoice接口主要函數

   上述程序的流程是獲取IspVoice接口,然後用ISpVoice::Speak()把文本輸出爲語音,可見,程序的核心就是IspVoice接口。除 了Speak外IspVoice接口還有許多成員函數,具體用法請參考SDK的文檔。下面擇要說一下幾個主要函數的用法: HRESULT Speak(const WCHAR *pwcs,DWORD dwFlags,ULONG *pulStreamNumber);

   功能:就是speak了

   參數:

    *pwcs 輸入的文本字符串,必需爲Unicode,如果是ansi字符串必需先轉換爲Unicode。

    dwFlags 用來標誌Speak的方式,其中SPF_IS_XML 表示輸入文本含有XML標籤,這個下文會講到。

    PulStreamNumber 輸出,用來獲取去當前文本輸入的等候播放隊列的位置,只有在異步模式纔有用。

HRESULT Pause ( void );
HRESULT Resume ( void );

   功能:一看就知道了。

HRESULT SetRate(long RateAdjust );
HRESULT GetRate(long *pRateAdjust);

   功能:設置/獲取播放速度,範圍:-10 to 10

HRESULT SetVolume(USHORT usVolume);
HRESULT GetVolume(USHORT *pusVolume);

   功能:設置/獲取播放音量,範圍:0 to 100

HRESULT SetSyncSpeakTimeout(ULONG msTimeout);
HRESULT GetSyncSpeakTimeout(ULONG *pmsTimeout);

   功能:設置/獲取同步超時時間。由於在同步模式中,電泳Speak後程序就會進入阻塞狀態等待Speak返回,爲免程序長時間沒相應,應該設置超時時間,msTimeout單位爲毫秒。

HRESULT SetOutput(IUnknown *pUnkOutput,BOOL fAllowFormatChanges);

   功能:設置輸出,下文會講到用SetOutput把Speak輸出問WAV文件。

   這些函數的返回類型都是HRESULT,如果成功則返回S_OK,錯誤有各自不同的錯誤碼。

   使用XML

   個人認爲這個TTS api功能最強大之處在於能夠分析XML標籤,通過XML標籤設置音量、音調、延長、停頓,幾乎可以使輸出達到自然語音效果。前面已經提過,把Speak 參數dwFlags設爲SPF_IS_XML,TTS引擎就會分析XML文本,輸入文本並不需要嚴格遵守W3C的標準,只要含有XML標籤就行了,下面舉 個例子: ……

pVoice->Speak(L"<VOICE REQUIRED=''NAME=Microsoft Mary''/>volume<VOLUME LEVEL=''100''>turn up</VOLUME>", SPF_IS_XML, NULL);
……
<VOICE REQUIRED=''NAME=Microsoft Mary''/>

   標籤把聲音設爲Microsoft Mary,英文版SDK中一共含有3種聲音,另外兩種是Microsoft Sam和Microsoft Mike。 ……

<VOLUME LEVEL=''100''>

   把音量設爲100,音量範圍是0~100。

   另外:標誌音調(-10~10): <PITCH MIDDLE="10">text</PITCH>

   注意:" 號在C/C++中前面要加 \ ,否則會出錯。

    標誌語速(-10~10): <RATE SPEED="-10">text</RATE>

    逐個字母讀: <SPELL>text</SPELL>

    強調: <EMPH>text</EMPH>

    停頓200毫秒(最長爲65,536毫秒): <SILENCE MSEC="200" />

    控制發音: <PRON SYM = ''h eh - l ow 1''/>

   這個標籤的功能比較強,重點講一下:所有的語言發音都是由基本的音素組成,拿中文發音來說,拼音是組成發音的最基本的元素,只要知道漢字的拼音,即使不知 道怎麼寫,我們可知道這個字怎麼都,對於TTS引擎來說,它不一定認識所有字,但是你把拼音對應的符號(SYM)給它,它就一定能夠讀出來,而英語發音則 可以用音標表示,''h eh - l ow 1''就是hello這個單詞對應的語素。至於發音與符號SYM具體對應關係請看SDK文檔中的Phoneme Table。

   再另外,數字、日期、時間的讀法也有一套規則,SDK中有詳細的說明,這裏不說了(懶得翻譯了),下面隨便拋個例子: <context ID = "date_ ymd">1999.12.21</context>

   會讀成 "December twenty first nineteen ninety nine"

   XML標籤可以嵌套使用,但是一定要遵守XML標準。XML標籤確實好用,效果也不錯,但是……缺點:一個字―――"煩",如果給一大段文字加標籤,簡直痛不欲生。

   把文本語音輸出爲WAV文件

#include <sapi.h>

#include <sphelper.h>

#pragma comment(lib,"ole32.lib")
#pragma comment(lib,"sapi.lib")

int main(int argc, char* argv[])
{
ISpVoice * pVoice = NULL;
if (FAILED(::CoInitialize(NULL)))
return FALSE;

HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL,
IID_ISpVoice, (void **)&pVoice);
if( SUCCEEDED( hr ) )
{
CComPtr<ISpStream> cpWavStream;
CComPtr<ISpStreamFormat> cpOldStream;
CSpStreamFormat originalFmt;
pVoice->GetOutputStream( &cpOldStream );
originalFmt.AssignFormat(cpOldStream);
hr = SPBindToFile( L"D:\\output.wav",SPFM_Create_ALWAYS,
&cpWavStream,&OriginalFmt.FormatId(),
originalFmt.WaveFormatExPtr() );
if( SUCCEEDED( hr ) )
{
pVoice->SetOutput(cpWavStream,TRUE);
WCHAR WTX[] = L"<VOICE REQUIRED=''NAME=Microsoft Mary''/>text to wave";
pVoice->Speak(WTX, SPF_IS_XML, NULL);
pVoice->Release();
pVoice = NULL;
}
}

::CoUninitialize();
return TRUE;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章