基于汉字编码特性构造函数实现的语音系统
摘要:基于汉字编码的特性,本文构造了一个函数,利用这个函数为每一个汉字建立索引,基于此建立了一个很小的基于文件形式的语音库;在程序里调用这个语音库来达到汉字语音输出的功能。同时给出了实现此语音库的方法、步骤及怎样调用此语音库,及相应的程序代码。
关键词:汉字编码, 函数, 语音系统
1.关于汉字映射序号函数 f
首先让我们来考查一下汉字的编码(我们这里只研究中国大陆的简体中文GB2312,下同)。
根据ANSI字符集[1],每一个汉字由两个字节构成,由一个十六进制数唯一表示,例如:‘黄’,其十六进制编码为:0xBBC6。我们可以看到在构成‘黄’字的这两个字节中,第一个字节表示的值为:0xBB,第二个字节为:0xC6。通过研究Windows系统中的字符映射表,我们可以得出以下命题:
命题一 在表示简体中文的这两个字节中,第一个字节的标识范围为:
0xB0~0xF7
,第二个字节的标识范围为:
0xA1~0xFE。
根据命题一,我们得出:
命题二 用这两个字节在此标识范围之内所能表示的汉字个数为:
(
0xF7
-
0xB0
+
1
)
×(
0xFE
-
0xA1
+
1
)
=72×94=6768。
事实上字符映射表里的简体汉字个数也为6768个。
于是我们定义了一个函数f:
定义一: P = f(Q) = (Q1 – 0xB0) × 94 + (Q2 – 0xA1)
其中:Q为任一汉字的十六进制编码,显然这里Q由两个字节构成,Q1为构成Q的两个字节中的第一个字节,Q2为第二个字节,所以:
0xB0≤Q1≤0xF7, 0xA1≤Q2≤0xFE
P = {p | 0 ≤t≤ 6767 且 p为整数}
不难证明此函数是可逆的。得出命题三:
命题三 任意给定一汉字,根据函数
f
,总可以找到一个唯一的数
p (p
∈
P)
,让
p
来代表这个汉字,反之,给定一数
p(p
∈
P)
,也总有一个唯一的汉字与之对应。
于是我们有f(‘啊’) = f(0xB0A1) = 0,又如f(‘黄’) = f(0xBBC6) = 1071,如此等等。
2.关于汉字拼音对照表
通过考查《新华字典》[2],我们为字符映射表里的6768个简体汉字建立了一个汉语拼音对照表。对照表里在每一个拼音的后面加上一个数字,以示声调,第一声用‘1’表示,第二声用‘2’,第三声用‘3’, 第四声用‘4’,平声用‘5’。例如:‘啊’→ a1,‘黄’→huang2。由于汉字里同音字比较多,所以6768个汉字拼音里实际上不同声调者只有1317个。 为了准备基本的语音素材,我们特地从电视台请了一位播音员录制了这1317个不同的发音。其中每一个发音是一个单独的wav型文件,其文件名就是其对应的汉语拼音,例如‘huang2’这个音的文件名字就是‘huang2’。
3.关于语音库的建立
语音库分为两个部份,第一部份存贮6768个汉字中的每一个汉字语音数据在库中的位置索引,第二部份存贮的是已经录制好的1317个wav文件的数据。事实上在库中并没有存贮汉字本身,因为根据前面得到的命题三,字符映射表里的每一个汉字都有一个唯一的数字与其对应,故这个语音库的第一部份,也就是索引部份,可以看作一个有6768个元素的数组----SndIdx[6768] (实际我们后面编程时也是这样实现的),例如SndIdx[1071]的值是‘黄’这个字的语音数据在库中的位置偏移量。因为1317个语音数据有10M之多,故索引数组的元素值远超过32767,为此数组的每一个元素占有4个字节的空间。因此索引大小为:4 × 6768 ≈25K 。
调用语音库时利用了两个缓冲区 :DWORD SndIdx[6768] 与char *m_szSndBuff,
m_szSndBuff = (char *)malloc( 语音库文件第二部份的大小 )。然后把语音库文件fsnd.dat的第一部份,也就是索引部份读入SndIdx;把第二部份,也就是语音数据部份读入m_szSndBuff。当需要语音输出的时候,例如要输出‘黄’的读音,程序根据定义一的函数f得出与该字对应的的编号为1071,于是得到该字的读音在缓冲区m_szSndBuff中的偏移量为SndIdx[1071],然后调用Windows的用于播放wav数据的API函数来输出该字的发音:
sndPlaySound( m_szSndBuff + SndIdx[1071] , SND_SYNC | SND_MEMORY )[3]。
4.关于系统的实现
首先我们来研究怎样生成这个语音系统。
语音系统的主体部份其实只是一个数据文件fsnd.dat,我们要做的只是怎样把这些索引与单个的汉字语音数据写到fsnd.dat中。对于6768个汉字的索引以及1317个不同的语音数据文件,我们构造了一个算法做这件事。
为此我们设计了一个临时数据库(sndDB),这个库里只有一张表(wordtable),原型是我们前面整理的汉语拼音对照表,正如我们前面所知,这个表有两个字段:汉字本身(在表里用 m_Word 表示)与其对应的拼音(在表里用 m_Sound 表示)。根据命题三,每一个汉字都与一个唯一的整数对应,因此6768个汉字在这张表里是按这些整数的从小到大的顺序来排列的。另外为了算法的实现,我们又加了两个额外的字段,一个是:bool m_Tag ,初始化为‘0’,这是一个标志,因为我们知道,汉字里同音字比较多,当对一个拼音对应的语音数据进行处理之后,这个语音数据在fsnd.dat中的偏移量就已确定,那么其它同音的汉字的对应语音数据的偏移量也是同一个值,此时就把这些同音字的这个标志置为‘1’,以后看到这个标志为1就跳到下一汉字。另一个额外的字段是:long m_sndIndex, 顾名思义,就是记录该汉字语音数据在此文件中的偏移量,初始为‘0’。又因为fsnd.dat是由两部份构成:索引部份与数据部份,为了方便,将这两部份分成两个文件来处理,然后再把包含语音数据的这个文件(totalsnd.dat)的全部内容写到fsnd.dat的后面。因为我们前面说过索引部份可看作一个数组,故在程序中我们又设计了一个数组:DWORD SndIdx[6768],来充当数据库与fsnd.dat的中转。以下是具体的程序部份:
………………
m_DbSnd.Open( "sndDB", false, false, "ODBC; UID = sa " ); //m_DbSnd为数据库对象
m_pSndSet = new CSndSet( &m_DbSnd ) ; //m_pSndSet为指向数据集的指针
m_pSndSet->Open ();//打开数据集
fsnd.Open( "fsnd.dat", CFile::modeCreate | CFile::modeWrite );//fsnd为一文件对象
DWORD idex = 0 ; //从第一个wav文件到第i个wav文件的长度的累加和
DWORD sndLen = 0; //记录第i个wav文件的长度
char pszFilePath[50] = "E:\\mywork\\hhw\\wav\\"; //语音文件位于该目录下
char *pFileKind = ".wav"; //语音文件是.wav文件
CFile snd; //代表具体汉字的语音文件对象
Cfile totalsnd( "totalsnd.dat" , CFile::modeCreate | CFile::modeWrite );//全部语音文件数据写入totalsnd.dat
CString strsnd; //记录从数据库里读出的汉字的汉语拼音
char *sndBuff = NULL; //为第i个wav文件数据开辟缓冲区
int i=0;
CString strSQL;//要执行的SQL查询、更新语句
while( !m_pSndSet->IsEOF() )//从数据库里的第一条记录读到最后一条
{
if( m_pSndSet->m_tag == TRUE ) //说明该字与前面某个字同音
{
SndIdx[i] = m_pSndSet->m_sndIndex ;
fsnd.Write(&SndIdx[i], sizeof(DWORD));//将该字发音数据所在位置写入文件
i++ ;
m_pSndSet->MoveNext ();
continue ;
}
strsnd = m_pSndSet->m_sound ;
strcat( pszFilePath , strsnd );//具体汉字语音数据文件名与其拼音是一致的
strcat( pszFilePath , pFileKind );//给出该字发音文件的完整路径
snd.Open( pszFilePath , CFile::modeRead | CFile::typeBinary ); //打开相应的发音文件
strcpy( pszFilePath, "E:\\mywork\\hhw\\wav\\" ); //初始化下个汉字发音文件的路径
//将与该字同音的汉字打上标志,并置发音索引为一样
strSQL.Format("update wordtable set m_tag=1,m_sndIndex=%d where m_sound='%s'", idex, strsnd );
m_DbSnd.ExecuteSQL( strSQL );
SndIdx[i] = idex ;
fsnd.Write( &SndIdx[i], sizeof(DWORD));//将该字发音数据所在位置写入文件
i++;
sndLen = snd.GetLength(); //得到该字发音文件的大小
idex += sndLen ; //紧接的与该汉字不同音的下一汉字的发音在文件中的偏移量
if( sndBuff != NULL )
{
free( sndBuff );
sndBuff = NULL;
}
sndBuff = ( char* )malloc( sndLen );//为该wav文件数据分配缓冲区
snd.ReadHuge( sndBuff, sndLen ) ; //
totalsnd.SeekToEnd();
totalsnd.WriteHuge( sndBuff, sndLen);// 把缓冲区语音数据写到语音数据文件
snd.Close();//关闭该读音的wav文件
m_pSndSet->Move( i ); //转向下一个汉字
}
totalsnd.Close();//关闭语音数据文件
//以读的且二进制的方式打开语音数据文件以把它的数据并到fsnd.dat里去
totalsnd.Open( "totalsnd.dat", CFile::modeRead | CFile::typeBinary );
sndLen = totalsnd.GetLength();
if( sndBuff != NULL )
{
free( sndBuff );
sndBuff = NULL;
}
sndBuff = ( char* )malloc( sndLen );
totalsnd.ReadHuge( sndBuff , sndLen );//把全部语音数据写到缓冲区里
totalsnd.Close();
fsnd.WriteHuge( sndBuff , sndLen ); //把全部语音数据写到索引部份后面
fsnd.Close();//fsnd.dat制作完毕,关闭文件对象
至此这个语音系统的主体部份—fsnd.dat已经完成。考虑到这个语音系统只是我们要开发的客户系统的一个外挂程序,于是把库文件的调用部份做成一个动态链接库,即一个dll文件。在这个dll文件中输出了一个类。这个类是这样定义的:
class CTextSnd
{
public:
CTextSnd();
virtual ~CTextSnd();
void Load(); //通过新开的一个线程调用LoadSnd,
void UnLoad( void );清空缓冲区数据。
BOOL LoadSnd();//打开fsnd.dat,载入声音数据到m_szSndBuff,索引数据到SndIdx[6768]
BOOL Play( char * szText );//szText是一段需要语音输出的文字,用Play(char* szText)来完成
private:
char * m_szSndBuff;
DWORD SndIdx[6768];
}
由于语音输出功能是在系统处理业务的时候才调用,故语音输出与这些业务的进行应该是并发的,为此单独开了一个线程来处理语音输出,这是一个全程函数:
DWORD WINAPI LoadSndThread(CTextSnd* ptr) { ptr->LoadSnd(); return 0; }
我们是这样调用这个全程函数的:
void CTextSnd::Load()
{
DWORD ThreadId;
CloseHandle(CreateThread(NULL,0,(unsigned long(_stdcall*)(void*))LoadSndThread,this,0,&ThreadId));
}
最后再给出用于最终输出语音的函数:BOOL CTextSnd::Play(char* szText )。
BOOL CTextSnd::Play( char * szText )//szText为一指针,指向要语音输出的那段话
{
if( m_szSndBuff == NULL) return FALSE;
DWORD dwSndPosition;
char * pT = szText;
while( *pT )
{
//(BYTE)pT[0]为汉字的第一个字节,(BYTE)pT[1] 为汉字的第二个字节
DWORD dwp = (( (BYTE)pT[0] - 0xb0 ) * 94 + (BYTE)pT[1] - 0xa1 ) ;
pT += 2;//转到下一个汉字
dwSndPosition = SndIdx[dwp] ;
sndPlaySound( m_szSndBuff + dwSndPosition, SND_SYNC | SND_MEMORY );[3]
}
return TRUE;
}
调用这个语音系统的时候,例如要输出一段话--- m_strWord的语音(Cstring m_strWord)。我们可以这样做:
CtextSnd *TextSnd = new CtextSnd() ;
TextSnd->Load();
LPTSTR p = m_strWord.GetBuffer( m_strWord.GetLength() );
TextSnd->Play( p );//播放语音
至此,我们这个利用汉字编码特性构造函数来实现的语音系统已经全部完成。值得一提的是,这个语音系统在云南某地下属各烟草公司的烟叶收购中得到应用,并收到了良好的效果。
参考文献
[1] Angelika Langer, Klaus Kreft 著,何渝等译标准C++输入输出流与本地化 北京:人民邮电出版社 2001
[2]、 新华字典 北京:商务印书馆 1998
[3]、 朱友芹等 新编Windows API 参考大全 电子工业出版社 2000
posted on 2006-08-01 10:37
汪杰 阅读(612)
评论(1) 编辑 收藏 引用