背景

语音合成(Text To Speech,TTS)可以实现网页文章的阅读。通过它,我们可以不需要看文字,就可以听新闻、听小说。

目前国内的主流的语音合成解决方案无非是以下几种:

  1. 科大讯飞
  2. Google
  3. 云服务(阿里云、腾讯云……)

上述的方案,音质不错,语音包选择多。但是都有一定的免费配额或者语言的限制(如科大讯飞的试用语音包),没有完全免费的方案。

开源免费的方案的效果比较差,而且需要利用自己的服务器资源,需要考虑额外的成本。

那有没有什么免费可以使用的方案呢?还是有的!

这里就要搬出来 Web API 里面的 SpeechSynthesis。这个接口是浏览器原生支持的语音合成。

基本上现在主流的现代浏览器都支持这个接口。具体的浏览器支持情况可以查阅上面链接里的文档。

如何使用?

下面就是一个简单例子:

1
2
3
4
const ss = new window.SpeechSynthesisUtterance();
ss.text = "你好,世界!";
ss.lang = "zh-CN";
window.speechSynthesis.speak(ss);

SpeechSynthesisUtterance 是配置语音合成内容和参数的类,有以下参数可以配置:

  • lang:文本所属语言,默认使用 <html> 里面的 lang
  • pitch:音调(0-2.0),默认是 1
  • rate:播放速率(0.1-10.0),默认是 1
  • text:需要说出来的文本
  • voice:语音包名称
  • volume:音量大小(0-1.0),默认是 1

同时可以配置以下事件:

  • onerror:出现错误
  • onstart:开始播放
  • onboundary:句子结束时触发
  • onend:播放完成
  • onmark:SSML 的 mark 标记触发
  • onpause:被暂停播放
  • onresume:被恢复播放

speechSynthesis 类实现语音合成的方法:

  • SpeechSynthesis.getVoices():获取全部可以播放的语音包(含语言)
  • SpeechSynthesis.speak():按 SpeechSynthesisUtterance 配置播放合成语音
  • SpeechSynthesis.cancel():移除 SpeechSynthesis 播放队列里面的全部待播放语音
  • SpeechSynthesis.pause():暂停播放某个语音
  • SpeechSynthesis.resume():恢复播放某个语音

具体的文档可以查看 MDN。

以 macOS 下的 Google Chrome 为例,通过 window.speechSynthesis.getVoices() 可以获取全部语音包,下面是一部分的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
[{
"voiceURI": "Ting-Ting",
"name": "Ting-Ting",
"lang": "zh-CN",
"localService": true,
"default": true
}, {
"voiceURI": "Google 普通话(中国大陆)",
"name": "Google 普通话(中国大陆)",
"lang": "zh-CN",
"localService": false,
"default": false
}]

上述数组是 SpeechSynthesisVoice 的实例数组,SpeechSynthesisVoice 的参数如下:

  • voiceURI:语音合成服务的URL
  • name:语音包名称
  • lang:语言(符合BCP 47规范)
  • localService:是本地语言包(离线)还是远程语言包(网络)
  • default:是否为默认的语音包

通过观察,可以发现,Google Chrome 在 macOS 下可以使用的语音包有 macOS 自带的本地语音包和谷歌提供的在线语音包。如苹果的「Ting-Ting」和谷歌的「Google 普通话(中国大陆)」。

在构建 SpeechSynthesisUtterance 的时候,传入支持的语音包的 langvoice (对应 SpeechSynthesisVoicename)就可以选择语音包了。

通过这个接口,我们可以给使用 Google Chrome 的浏览器用户提供来自谷歌的语音合成功能。

探索 SpeechSynthesis 的原理

下面,我们通过 Chromium 的源代码探索 Google Chrome 的 SpeechSynthesis 是怎么实现的。

Chromium 里面的 SpeechSynthesis 类都是在 speech 模块 中实现的。其中,该模块包含 语音识别(speechRecognizer)和 语音合成 (SpeechSynthesis)两大功能的实现。

看语音合成的头文件就大概知道有哪些功能:

语音包怎么来的?

我们可以先看 tts_controller_impl 里面的 GetVoices 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void TtsControllerImpl::GetVoices(BrowserContext* browser_context,
std::vector<VoiceData>* out_voices) {
TtsPlatform* tts_platform = GetTtsPlatform(); // 获取操作系统相关的 TTS
if (tts_platform) { // 当前平台存在 TTS
// Ensure we have all built-in voices loaded. This is a no-op if already
// loaded.
tts_platform->LoadBuiltInTtsEngine(browser_context); // 加载本地的 TTS
if (tts_platform->PlatformImplAvailable())
tts_platform->GetVoices(out_voices);
}

if (browser_context) {
TtsControllerDelegate* delegate = GetTtsControllerDelegate();
if (delegate && delegate->GetTtsEngineDelegate()) // 浏览器自带的 TTS
delegate->GetTtsEngineDelegate()->GetVoices(browser_context, out_voices);
}
}

简单来说,SpeechSynthesis 的语音包来自两大地方——操作系统自带浏览器自带。所以我们才发现 window.speechSynthesis.getVoices() 拿到的语音包存在一部分是 Apple 提供的,一部分是 Google 提供的。

本地语音包的实现

下面分开看不同的语音包是怎么调用的,先看操作系统自带的。

观察源代码,我们发现 tts_platform_impl.h 的实现是跟操作系统有关,不同的操作系统下的实现不一样:

  • tts_android.cc:安卓本地语音的实现,本质上是通过JNI调用 android.speech.tts.TextToSpeech具体实现点我查看,该接口与安卓系统「语音引擎」设置有关,原生安卓系统是 Pico TTS,国内大多是科大讯飞引擎
  • tts_fuchsia.cc:Fuchsia 本地语音的实现
  • tts_linux.cc:Linux 本地语音的实现,本质上是使用 libspeechd.so.2
  • tts_mac.mm:macOS 本地语音的实现,本质上是调用 NSSpeechSynthesizerDelegate,集成了 Apple 的语音包

上述代码通过不同的操作系统下的本地语音合成调用方法完成了本地语音包的调用,此处不着墨过多,有兴趣的小伙伴可以看源代码深入了解。

在线语音包的实现

浏览器自带的在线语音包是通过 TtsEngineDelegate 类封装的。这个东西在 Chromium 里面是由 tts_extension 实现的。因此,在线的语音包是以插件的形式存在于 Chrome 里面。

我们可以查阅 tts_engine_extension_api.ccGetVoicesInternal 实现,发现在线语音包的获取方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Get the voices for an extension, checking the preferences first
// (in case the extension has ever called UpdateVoices in the past),
// and the manifest second.
std::unique_ptr<std::vector<extensions::TtsVoice>> GetVoicesInternal(
content::BrowserContext* context,
const extensions::Extension* extension) {
// 首先尝试从扩展的prefs里面拿到保存好的语音包数据集
auto* extension_prefs = extensions::ExtensionPrefs::Get(context);
const base::ListValue* voices_data = nullptr;
// kPrefTtsVoices = "tts_voices"
// 从 tts_voices 字段读取一个语音包列表
if (extension_prefs->ReadPrefAsList(extension->id(), kPrefTtsVoices,
&voices_data)) {
const char* error = nullptr;
return ValidateAndConvertToTtsVoiceVector(
extension, *voices_data,
/* return_after_first_error = */ false, &error);
}

// Fall back on the extension manifest.
auto* manifest_voices = extensions::TtsVoices::GetTtsVoices(extension);
if (manifest_voices)
return std::make_unique<std::vector<extensions::TtsVoice>>(
*manifest_voices);
return std::make_unique<std::vector<extensions::TtsVoice>>();
}

} // namespace

大概锁定到这个在线语音包的扩展在 pref 里面存储语音包的信息。

那么我们可以去 chrome://prefs-internals/ 找一下这个扩展。然后就会惊奇发现到这个隐藏的插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
"neajdppkdcdipfabeoofebfddakdcjhd": {
"active_permissions": {
"api": ["systemPrivate", "ttsEngine"],
"explicit_host": ["https://www.google.com/*"],
"manifest_permissions": []
},
"events": ["ttsEngine.onPause", "ttsEngine.onResume", "ttsEngine.onSpeak", "ttsEngine.onStop"],
"manifest": {
"background": {
"persistent": false,
"scripts": ["tts_extension.js"]
},
"description": "Component extension providing speech via the Google network text-to-speech service.",
"manifest_version": 2,
"name": "Google Network Speech",
"permissions": ["systemPrivate", "ttsEngine", "https://www.google.com/"],
"tts_engine": {
"voices": [{
"event_types": ["start", "end", "error"],
"gender": "female",
"lang": "zh-CN",
"remote": true,
"voice_name": "Google 普通话(中国大陆)"
}, {
"event_types": ["start", "end", "error"],
"gender": "female",
"lang": "zh-HK",
"remote": true,
"voice_name": "Google 粤語(香港)"
}, {
"event_types": ["start", "end", "error"],
"gender": "female",
"lang": "zh-TW",
"remote": true,
"voice_name": "Google 國語(臺灣)"
}]
},
"version": "1.0"
}
}
}

你会发现这里的 voiceswindow.speechSynthesis.getVoices() 里面的谷歌的语音包一模一样,所以就是这个插件负责了 Google Chrome 的在线语音包。

Google Network Speech 这个扩展的源代码可以在 Chromium 的项目里面找到,源代码在此自取

本质上,这个在线语音包调用的是 Google Cloud 的语音合成模块。

接口是 https://www.google.com/speech-api/v2/synthesize

谷歌中国 www.google.cn 也是有这个接口可以使用的(明显是为了解决国内用户无法调用语音合成模块的问题)。

Query 参数分别是:

  • key: Google API Key,请到 Google Developers Console 的凭据页面申请,并开通文字到语音的接口权限
  • text: 需要合成语音的文本,对应 SpeechSynthesisUtterance.text
  • lang: 语言,对应 SpeechSynthesisUtterance.lang
  • name: 语音包名称,对应 SpeechSynthesisUtterance.name(可选)
  • speed: 播放速率,对应 SpeechSynthesisUtterance.rate(可选)
  • pitch: 音调,对应 SpeechSynthesisUtterance.pitch(可选)
  • enc: 默认是 mpeg
  • client: 默认是 chromium

每个用户每个月可以享受400万字符的免费配额(一个中文字也算一个字符),配额超过后每100万字符收4美元。

关于这个服务的更多介绍,可以查看 Google Cloud 上的介绍信息

总结

  1. 对于谷歌浏览器的用户,我们可以使用 window.speechSynthesis 享受原生的免费语音合成
  2. 对于其他浏览器的场景,我们可以使用 Google Cloud 的语音合成模块实现语音合成,有一定的免费配额
  3. 针对苹果用户,苹果系统自带了各种离线语音包,可以免费使用
  4. 针对安卓用户,安卓系统设置的语音包决定了 window.speechSynthesis 的能力,如华为内置的科大讯飞离线语音包

除非注明,麦麦小家文章均为原创,转载请以链接形式标明本文地址。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

本文地址:https://blog.micblo.com/2020/02/21/Chromium-%E8%AF%AD%E9%9F%B3%E5%90%88%E6%88%90%E6%8E%A5%E5%8F%A3-SpeechSynthesis-%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90/