您的位置:首页 > 其它

TTS实现文字语音朗读

2010-04-15 11:50 525 查看
一. 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;

}

Powered by Zoundry Raven
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: