当前位置: 首页 > 工具软件 > SAPI++ > 使用案例 >

基于微软 SAPI 的 TTS 程序实现

东方俊明
2023-12-01

背景描述  

前几年因工作的需要,想在网上找一款 TTS(Text To Speech) 软件,用于将文字转换为特定格式(ALaw、8000Hz、8bit)的语音文件,但实在找不到合适的。最后想着,干脆自己写一个得了,成果就是本文所要描述的基于微软 SAPI的文字转语音的小程序。在开发的过程中,主要参考了 “鸡啄米”同学的博客,在此表示感谢。现把开发过程重新整理了下,希望能对有需要的同学提供帮助。

SAPI 概述

SAPI全称 The Microsoft Speech API。相关的SR和SS引擎位于Speech SDK开发包中,这个语音引擎支持多种语言的识别和朗读,包括英文、中文、日文等。

SAPI包括以下组件对象(接口):

(1)Voice Commands API。对应用程序进行控制,一般用于语音识别系统中。识别某个命令后,会调用相关接口是应用程序完成对应的功能。如果程序想实现语音控制,必须使用此组对象。

(2)Voice Dictation API。听写输入,即语音识别接口。

(3)Voice Text API。完成从文字到语音的转换,即语音合成。

(4)Voice Telephone API。语音识别和语音合成综合运用到电话系统之上,利用此接口可以建立一个电话应答系统,甚至可以通过电话控制计算机,最新版本为 TAPI 3.1。

(5)Audio Objects API。封装了计算机发音系统。

    我们要实现语音合成需要的是Voice Text API。

TTS开发程序举例

开发环境

操作系统:Windows 7 X64

IDE:Visual Studio 2013 Update 5(含Windows Kits 8.1)

开发语言:C++(& MFC)

语音包:Windows 7系统默认包含的Microsoft Lili - Chinese (China)和Microsoft Anna - English (United States)两个语音包,Microsoft Lili支持中英文混读。Windows XP系统或需要其他语音包可到微软官网下载。

示例代码

(1)包含头文件和lib库

使用SAPI语音引擎前需包含以下头文件和lib库:

#include "sapi.h"
#include "sphelper.h"
#pragma comment(lib, "sapi.lib")

(2)文字转 WAV文件示例

直接看代码,有疑问先保留,函数说明后续分解。

::CoInitialize(NULL);         // COM初始化

//获取ISpVoice接口
	if (FAILED(CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL/*CLSCTX_INPROC_SERVER*/, IID_ISpVoice, (void**)&m_pISpVoice)))
	{
		MessageBox(L"获取ISpVoice接口失败!", L"提示", MB_OK | MB_ICONWARNING);
		return;
	}

	//语音文件保存的目录
	this->CreateSaveDirectory();

	CString strChannel;
	CString strFormat;
	CString strSamplesPreSec;
	CString strBitsPerSample;
	m_ComboxChannel.GetLBText(m_ComboxChannel.GetCurSel(), strChannel);	
	m_ComboxFormat.GetLBText(m_ComboxFormat.GetCurSel(), strFormat);	
	m_ComboxSamplesPreSec.GetLBText(m_ComboxSamplesPreSec.GetCurSel(), strSamplesPreSec);	
	m_ComboxBitsPreSamples.GetLBText(m_ComboxBitsPreSamples.GetCurSel(), strBitsPerSample);

	//编码设置,此处只列举ALaw、ULaw和PCM
	//其他编码参见 mmreg.h 文件
	WORD wFormatTag = WAVE_FORMAT_ALAW;
	if (strFormat.CompareNoCase(_T("ALaw")) == 0)
	{
		wFormatTag = WAVE_FORMAT_ALAW;
	} 
	else if (strFormat.CompareNoCase(_T("ULaw")) == 0)
	{
		wFormatTag = WAVE_FORMAT_MULAW;
	}
	else
	{
		wFormatTag = WAVE_FORMAT_PCM;
	}

	int nChannel = strChannel.CompareNoCase(_T("单声道")) == 0 ? 1 : 2;	//声道设置
	int nSamplesPreSec = _ttoi(strSamplesPreSec.GetBuffer());			//采样率设置
	int nBitsPerSample = _ttoi(strBitsPerSample.GetBuffer());			//采样比特设置
	int nBlockAlign = (nChannel * nBitsPerSample) / 8;

	//此处很重要,录音文件的参数在此配置
	WAVEFORMATEX waveFormate;
	waveFormate.wFormatTag = wFormatTag;	//编码,WAVE_FORMAT_ALAW、WAVE_FORMAT_MULAW、WAVE_FORMAT_PCM...	
waveFormate.nChannels = nChannel;	//采样声道数,对于单声道音频设置为1,立体声设置为2
	waveFormate.nSamplesPerSec = nSamplesPreSec; //采样率,8.0 kHz, 11.025 kHz, 22.05 kHz, and 44.1 kHz
	waveFormate.nAvgBytesPerSec = nSamplesPreSec * nBlockAlign;	//每秒的数据率,就是每秒能采集多少字节的数据,nSamplesPerSec × nBlockAlign
	waveFormate.nBlockAlign = nBlockAlign; //一个块的大小,采样bit的字节数乘以声道数, (nChannels × wBitsPerSample) / 8
	waveFormate.wBitsPerSample = nBitsPerSample; //8 or 16,采样比特  8bits/次
	waveFormate.cbSize = 0;						//一般为0

	CComPtr<ISpStream> cpISpStream;
	CComPtr<ISpStreamFormat> cpISpStreamFormat;
	CSpStreamFormat spStreamFormat;
	m_pISpVoice->GetOutputStream(&cpISpStreamFormat); 
	spStreamFormat.AssignFormat(cpISpStreamFormat);

	HRESULT hResult = SPBindToFile(this->GetFileDirectory().AllocSysString(),
		SPFM_CREATE_ALWAYS,
		&cpISpStream,
		&spStreamFormat.FormatId(),
		&waveFormate/*spStreamFormat.WaveFormatExPtr()*/);
	if (SUCCEEDED(hResult))
	{
		//语音包、音量、语速设置
		this->SetVoiceParam();

		m_pISpVoice->SetOutput(cpISpStream, TRUE);
		m_pISpVoice->Speak(m_EditContent.AllocSysString(), SPF_DEFAULT, NULL);
		MessageBox(_T("保存 WAV 文件成功!"), _T("提示"), MB_OK);

		ShellExecute(NULL, _T("open"), m_strFileDir, NULL, NULL, SW_SHOWNORMAL);
	}
	else
	{
		MessageBox(_T("保存 WAV 文件失败!"), _T("提示"), MB_OK | MB_ICONWARNING);
	}

	m_pISpVoice->Release();
::CoUninitialize(); 

代码解释

SAPI是架构在COM基础上的,因此在使用语音引擎之前需进行COM初始化:

::CoInitialize(NULL);

当然,使用结束后记得释放资源:

::CoUninitialize();

 

COM初始化后,调用函数

STDAPI CoCreateInstance(

REFCLSID rclsid, //创建的Com对象的类标识符(CLSID)

LPUNKNOWN pUnkOuter, //指向接口IUnknown的指针

DWORD dwClsContext, //运行可执行代码的上下文

REFIID riid, //创建的Com对象的接口标识符

LPVOID * ppv //用来接收指向Com对象接口地址的指针变量

)

来创建ISpVoice对象。获取到ISpVoice接口对象以后,我们就可以通过pSpVoice指针调用SAPI接口了。

 

需要输出自定义格式WAV文件的,下面是关键点,请留意!!!

WAV文件的格式可通过结构体WAVEFORMATEX来设置,WAVEFORMATEX 各字段解释如下:

typedef struct tWAVEFORMATEX

{

    WORD        wFormatTag;        //编码,取WAVE_FORMAT_ALAW\WAVE_FORMAT_MULAW\WAVE_FORMAT_PCM...等值

    WORD        nChannels;          //采样声道数,对于单声道音频设置为1,立体声设置为2

    DWORD       nSamplesPerSec;     //采样率,8.0 kHz, 11.025 kHz, 22.05 kHz, and 44.1 kHz等

    DWORD       nAvgBytesPerSec;    / //每秒的数据率,就是每秒能采集多少字节的数据(nSamplesPerSec × nBlockAlign)

    WORD        nBlockAlign;        /一个块的大小,采样bit的字节数乘以声道数, (nChannels × wBitsPerSample) / 8

    WORD        wBitsPerSample;     //8 or 16,采样比特  8bits/次

    WORD        cbSize;            //一般为0

} WAVEFORMATEX, *PWAVEFORMATEX, NEAR *NPWAVEFORMATEX, FAR *LPWAVEFORMATEX;

 

前面都是准备工作,函数SPBindToFile将音频流绑定到指定的文件,原型如下:

  HRESULT SPBindToFile( LPCWSTR pFileName, SPFILEMODE eMode, ISpStream ** ppStream, const GUID * pFormatId = NULL, const WAVEFORMATEX * pWaveFormatEx = NULL, ULONGLONG ullEventInterest = SPFEI_ALL_EVENTS)

pFileName将流绑定到的文件的文件名;

  eMode:用于定义文件打开模式;打开音频文件时,该文件必须为SPFM_OPEN_READONLY或SPFM_CREATE_ALWAYS,否则调用将失败;

  ppStream:ISpStream指针的地址;如果函数成功,则使用新创建的ISpStream接口填充该值;

  pFormatId:与流关联的数据格式标识符;可以为NULL(默认值),并且格式将由提供的wave文件确定(如果文件具有'.wav'扩展名)。如果不是,则假定该文件是文本文件;

  pWaveFormatEx:包含wave文件格式信息的WAVEFORMATEX结构;如果guidFormatId为SPDFID_WaveFormatEx,则必须指向有效的 WAVEFORMATEX结构;对于其他格式,它应该为NULL;

  ullEventInterest:所需事件的类型为SPEVENTENUM的标志。

 

在输出WAV文件前,需要选择语音库,通过函数SpEnumTokens来枚举所有语音库:

   IEnumSpObjectTokens *m_pIEnumSpObjectTokens

    SpEnumTokens(SPCAT_VOICES, NULL, NULL, &m_pIEnumSpObjectTokens)

语音库保存在 pIEnumSpObjectTokens 里,可通过函数Item来选择需要的语音库:

   ISpObjectToken      *m_pISpObjectToken;

   m_pIEnumSpObjectTokens->Item(Index, &m_pISpObjectToken);

通过函数HRESULT SetVolume(USHORT    usVolume)来设置使用的语音库:

    ISpVoice            *m_pISpVoice;

    m_pISpVoice->SetVoice(m_pISpObjectToken);

注意,如果使用不支持中文的语音库来操作中文字符串,WAV 文件将会输出失败。

 

通过函数HRESULT SetRate( long   RateAdjust)来设置朗读速度,取值范围:-10到10。

    m_pISpVoice->SetRate(m_SliderVoiceSpeed.GetPos() - 10);

 

通过函数HRESULT SetVolume(USHORT usVolume)设置音量,范围:0到100。

    m_pISpVoice->SetVolume(100 - m_SliderVoiceSize.GetPos());

 

将音频流输出到WAV文件:

     HRESULT SetOutput(IUnknown *pUnkOutput,BOOL fAllowFormatChanges);

    HRESULT Speak(LPCWSTR *pwcs, DWORD dwFlags, ULONG *pulStreamNumber)

   pwcs:待转换的字符串

   dwFlags:是用于控制朗读方式的标志,具体意义可以查看文档中的枚举 SPEAKFLAGS。SPF_DEFAULT表示使用默认设置,包括同步朗读的设置。异步朗读可以设置成 SPF_ASYNC。同步朗读表示读完string中的内容,speak函数才会返回,而异步朗读则将字符串送进去就返回,不会阻塞。

   pulStreamNumber:转换WAV文件时值为NULL;当用于朗读时为输出参数,它指向本次朗读请求对应的当前输入流编号,每次朗读一个字符串时都会有一个流编号返回,异步朗读时使用。

 

ISpVoice接口使用完毕后也要记得释放:

m_pISpVoice->Release();

朗读接口实现参见 Demo 的 void CTTSTestDlg::OnBnClickedButtonSpeak() 函数。

Demo下载:https://download.csdn.net/download/psy361212/12035482

 类似资料: