audio_ostream - 文本到语音的 ostream






4.88/5 (25投票s)
一篇关于如何使用 ostream 接口将文本转语音添加到应用程序的文章
引言
在这篇文章中,我将向您展示如何为您的程序添加文本转语音 (TTS) 功能。
您将能够使用熟悉的标准 ostream
语法,基本上用一行代码就能完成。
此外,我还将展示如何使用开源 C++ 工具使您的代码更简短(我所有的代码不到 50 行)、更可靠、更健壮,并且比原始 API 更通用。
我将展示什么
- 如何为您的程序添加简单的 TTS。
- 简单使用 COMSTL 和各种其他 STLSoft 组件。
- 一个如何使用 boost::iostreams 的简单示例
背景
我最近不得不在一个程序(在 Windows 上运行)中添加音频输出。
Microsoft 的 SAPI SDK 提供了一个 COM 接口,可以通过该接口使用 SAPI 的 TTS 引擎朗读宽字符字符串。Code Project 有许多文章解释了如何使用 SAPI,其复杂程度各不相同。那么为什么还要再写一篇呢?
嗯,我想要的一些附加功能在那些文章中并没有实现。
- 尽量减少或不进行 COM 操作。理想情况下,它应该能在最简单的控制台应用程序中工作。
- 对除宽字符外的其他类型进行完全(透明)支持。例如
char*
、std::string
甚至int
、float
等。 - 直观(或至少熟悉)的语法
为了实现这些目标,我开发了 audio_ostream
。
audio_ostream
是一个功能齐全的 std::ostream
,它支持任何具有 operator<<()
的类型。
您可以根据需要创建任意数量的 audio_ostream
,它们可以并行工作。
为了处理 COM 问题,我使用了出色的 COMSTL 库,它负责处理所有微妙而脆弱的 COM 复杂问题,例如(初始化)、资源(分配)和引用计数等。
boost::iostreams
用于以很少的样板代码编写工作量,提供完整的 std::ostream
支持。
由于 boost::iostreams
和 COMSTL 都是纯头文件库,所以我决定我的类也是纯头文件。这个决定的一个小代价是,SAPI 头文件将被包含在任何使用 audio_ostream
的文件中。
使用代码
使用代码再简单不过了
#include "audiostream.hpp"
using namespace std;
using namespace audiostream;
int main()
{
audio_ostream aout;
aout << "Hello World!" << endl;
// some more code...
return 0;
}
这个小程序会,顾名思义,说 "Hello World!"。
音频流是异步的,所以程序会继续运行,即使文本正在被朗读(这就是为什么有 // some more code...
这一行,以便它能完成朗读)。这在概念上类似于 std::ostream
如何缓冲结果,直到内部缓冲区满,然后才显示文本。
使用该类
#include
包含audiostream.hpp
头文件。- 创建一个
audio_ostream
(或waudio_ostream
)实例 - 像使用任何
std::ostream
一样使用该流。
这就是开始使用该类所需的所有操作。
先决条件
为了让代码编译和运行,您需要 3 个库
- 对于 TTS 引擎,您需要安装 Microsoft Speech SDK(我使用的是 5.1 版)。
- 对于 COMSTL,您需要 STLSoft 库(您需要 STLSoft 版本 1.9.1 beta 44 或更高版本)。
- Boost Iostreams 库。您可以在 这里 下载 Boost。
相应地设置您的编译器和链接器路径(Boost 和 STLSOft 都是纯头文件)。
高级用法
可以使用 SAPI 文本转语音 (TTS) XML 标签更改语音的性别、语速、语言以及更多参数。
只需将相关的 XML 标签插入流中即可进行更改。可能的 XML 标签的完整列表可以在 这里 找到。
例如
audio_ostream aout;
// Select a male voice.
aout << "<voice required='Gender=Male'>Hello World!" << endl;
aout << "Five hundred milliseconds of silence" << flush <<
"<silence msec='500'/> just occurred." << endl;
出于某种原因,XML 标签必须是 SAPI 朗读字符串中的第一个项目,前面不能有任何文本。像示例中那样刷新流,以方便实现这一点。
您还可以调用 SetRate()
并传入 [-10,10] 之间的值来控制语音的语速。
神奇之处
核心类
代码的核心是 audio_sink
类
template < class SinkType >
class audio_sink: public SinkType
{
public:
audio_sink()
{
// Initialize the COM libraries
static comstl::com_initializer coinit;
// Get SAPI Speech COM object
HRESULT hr;
if(FAILED(hr = comstl::co_create_instance(CLSID_SpVoice, _pVoice)))
throw comstl::com_exception(
"Failed to create SpVoice COM instance",hr);
}
// speak a character string
std::streamsize write(const char* s, std::streamsize n)
{
// make a null terminated string.
std::string str(s,n);
// convert to wide character and call the actual speak method.
return write(winstl::a2w(str), str.size());
}
// speak a wide character string
std::streamsize write(const wchar_t* s, std::streamsize n)
{
// make a null terminated wstring.
std::wstring str(s,n);
// The actual COM call to Speak.
_pVoice->Speak(str.c_str(), SPF_ASYNC, 0);
return n;
}
// Set the speech speed.
void setRate(long n) { _pVoice->SetRate(n); }
private:
// COM object smart pointer.
stlsoft::ref_ptr< ISpVoice > _pVoice;
};
这个小类里有很多东西。让我们逐一剖析。
COMSTL、stlsoft::ref_ptr<> 和 ISpVoice
该类的唯一成员是 stlsoft::ref_ptr< ISpVoice > _pVoice
。
这是智能指针,它将为我们处理所有 COM 相关的事情。STLSoft 类 stlsoft::ref_ptr<> 提供 RAII 安全的引用计数接口 (RCI) 处理。具体来说,它非常适合处理 COM 对象。
我们正在使用 ISpVoice
接口。来自 Microsoft 的 网站
ISpVoice
接口使应用程序能够执行文本合成操作。应用程序可以通过此接口朗读字符串和文本文件,或播放音频文件。所有这些都可以同步或异步完成。
在构造函数中,我们首先通过 comstl::com_initializer
初始化 COM 使用。这只发生一次(因为它是一个静态对象),并且不再需要我们担心。为了初始化 _pVoice
,我们使用 CLSID_SpVoice
ID 调用 comstl::co_create_instance()
。如果一切顺利,我们现在就持有 ISpVoice
对象句柄。所有引用计数问题都将由 stlsoft::ref_ptr<>
处理。如果调用失败,将抛出 comstl::com_exception
异常,并且类实例将不会被创建。
要朗读一些文本,我们只需要用宽字符字符串调用 _pVoice->Speak()
。
要“朗读文本”,我们只需要用宽字符字符串调用 _pVoice->Speak()
。
然而,我们希望支持其他字符类型,如 char*
、std::string
等。事实上,我们希望支持任何可以通过 operator<<()
转换为字符串或宽字符串的类型。
Boost Iostreams
boost::iostreams 可以轻松创建标准的 C++ 流和流缓冲区,用于访问新的源和接收器。从 网站 转述
Sink 提供对给定类型字符序列的写访问。Sink 可以通过定义一个名为 write
的成员函数来公开此序列,该函数通过 boost::iostreams::write
函数间接调用,由 Iostreams 库调用。
有两个预定义的 sink:boost::iostreams::sink
和 boost::iostreams::wsink
,分别用于写入窄字符和宽字符字符串。
为了使我们的类成为 Sink 并获得其所有功能,我们只需从这些类之一派生我们的类(取决于我们想要窄字符还是宽字符输出)。因此,audio_sink
是一个派生自其模板参数的模板类。
要使用我们的 sink 并创建一个具体的 ostream
,我们需要使用 boost::iostreams::stream
类。
支持类是 audio_ostream_t
template < class SinkType >
class audio_ostream_t: public boost::iostreams::stream< SinkType >,
public SinkType
{
public:
audio_ostream_t()
{
// Connect to Sink
open(*this);
}
};
typedef audio_ostream_t< audio_sink< boost::iostreams::sink > >
audio_ostream ;
typedef audio_ostream_t< audio_sink< boost::iostreams::wsink > >
waudio_ostream;
此类允许我们将 sink 和 stream 对象组合成一个单一的实体。
派生自 boost::iostreams::stream
使我们拥有了所有的 ostream
功能。此流对象需要用 sink 对象实例进行初始化。因此,我们也从 SinkType
(模板参数)派生,并用 *this
初始化 boost::iostreams::stream
。从 SinkType
派生的另一个好处是它允许我们直接访问 sink 对象。直接访问允许我们,例如,直接访问 SetRate()
方法来更改语音速度。
朗读文本
boost::iostreams
机制将负责所有类型转换和 ostream
语法。最终,audio_sink::write
将被调用。尽管我们同时提供了窄字符和宽字符字符串 ostream
,但 SAPI 只支持宽字符字符串。此外,Sink 的 write()
方法接受非空终止的字符串以及要从流中使用的字符数。
为了解决这两个问题,我们将连续流+大小转换为一个空终止的(w)字符串,使用适当的 std::(w)string
构造函数。
为了朗读窄字符字符串,我们调用宽 write
版本,使用 STLSoft 的 winstl::a2w()
来轻松地从窄转换为宽。winstl::a2w()
将负责任何必需的临时缓冲区的分配和释放,以及转换本身。
可能的扩展
在实现了我的设计目标后,一些可能的扩展浮现在脑海中。
通过使用区域设置进行语言选择,进一步扩展 ostream
支持可能会很有趣。将一些 XML 标签包装成 ostream
操纵符,将提供更自然(或至少更熟悉)的语法。当然,类似的扩展可以将 SAPI 语音识别接口转换为 istream
,但这又是另一回事了。
支持同步(阻塞)语音也可能是可取的。
修订历史
- 2007 年 3 月 30 日 通过使用
wchar_t
而非unsigned short
,修复了代码以便在 MSVS 2005 上编译和运行。
感谢 Jochen Berteld 指出问题,感谢 Matthew Wilson 指出解决方案。