godot-demo-projects 实战:基于 DisplayServer 的文本转语音(TTS)Demo 完整解析
godot-demo-projects 实战:基于 DisplayServer 的文本转语音(TTS)Demo 完整解析
本指南以仓库 audio/text_to_speech 下的官方 TTS 演示为核心,逐层拆解 Godot 引擎通过 DisplayServer 单例暴露的 tts_*() 方法族:如何枚举操作系统提供的语音、如何提交朗读任务(utterance)、如何通过回调信号跟踪朗读进度,以及如何用 GDScript 实现语速/音调/音量调节、暂停/恢复/打断与多语言朗读。读完本文,你将掌握一套可直接复用的文本转语音接入方案,并理解其底层调用链与回调事件模型。
演示运行界面:左侧为语音列表(Name/Language 双列)与朗读控制区,右侧日志面板实时记录 utterance 生命周期事件,顶部状态灯随朗读状态变色。
项目概览与运行方式
该演示位于仓库的 audio/text_to_speech 目录,是一个独立的 Godot 工程,仅包含 4 个核心文件:
- control.tscn:主界面场景(语音列表、控制按钮、滑杆、日志面板);
- voice_list.gd:全部 TTS 交互逻辑;
- project.godot:工程配置(语言、渲染器、TTS 开关);
- README.md:官方说明文档。
从 project.godot 可以看到工程元信息:项目名为 Text-to-speech demo,标签为 accessibility / demo / gui / official,主场景为 res://control.tscn,config/features 标注的引擎版本为 4.7,说明这是一个面向无障碍(accessibility)场景的官方演示。运行方式很简单:用 Godot 4.x 打开该目录下的 project.godot,直接运行主场景即可。
工作原理:DisplayServer 的 tts_*() 方法族
根据 README.md,整个演示的核心机制是调用 DisplayServer 单例的 tts_*() 方法,完成三件事:
- 枚举语音信息:查询操作系统已安装的 TTS 语音及其属性;
- 提交朗读任务:把文本与参数发送给操作系统级 TTS API;
- 接收回调信号:监听朗读过程中产生的事件并做出响应。
值得注意的是,TTS 并不走 Godot 的音频总线,而是直接对接操作系统/平台的语音合成能力,因此「可用语音」完全取决于运行平台(截图中的 macOS 环境检测到了 48 个语音)。这也是 project.godot 中必须显式开启 audio/general/text_to_speech=true 的原因——它控制着引擎是否启用文本转语音支持。
语音枚举 API
voice_list.gd 在 _ready() 中调用:
vs = DisplayServer.tts_get_voices()
tts_get_voices() 返回 Array<a href="https://link.gitcode.com/i/890cf9bbc8e963ef45f313fe76a78e48" target="_blank">Dictionary],每个字典包含三个字段,正是 [voice_list.gd 中 Tree 两列展示的内容:
| 键 | 含义 | 演示中的用途 |
|---|---|---|
name |
语音显示名称(如 Alex、Alice) | Tree 第 0 列 |
id |
语音唯一标识 | 存入 TreeItem 元数据,作为 tts_speak 的 voice_id 参数 |
language |
语音所属语言(如 en-US、it-IT) | Tree 第 1 列 |
另外,voice_list.gd 中的 Demo 按钮还使用了按语言筛选的便捷接口:
var vc: PackedStringArray
vc = DisplayServer.tts_get_voices_for_language("en")
if not vc.is_empty():
...
DisplayServer.tts_speak("Beware the Jabberwock, my son!", vc[0], ...)
tts_get_voices_for_language(lang) 直接返回指定语言(如 "en"、"es"、"ru")的 voice_id 数组(PackedStringArray),省去遍历过滤的步骤,适合"我要说某种语言"的场景。
朗读与参数控制
Speak 与 Interrupt 按钮最终都归结为一次 tts_speak() 调用,区别只在于最后一个参数。见 voice_list.gd:
DisplayServer.tts_speak(
$Utterance.text, # 要朗读的文本
$Tree.get_selected().get_metadata(0), # 语音 id(来自列表选中项)
$HSliderVolume.value, # 音量
$HSliderPitch.value, # 音调
$HSliderRate.value, # 语速
id, # utterance 唯一标识
false # 是否打断当前朗读
)
参数顺序与含义如下:
| 参数 | 来源控件 | 取值范围(来自 control.tscn) |
|---|---|---|
text |
Utterance(TextEdit) |
任意文本,默认示例为 "Beware the Jabberwock, my son! ..." |
voice_id |
Tree 选中项的元数据 | 来自 tts_get_voices() 返回的 id 字段 |
volume |
HSliderVolume |
最小 1,默认 50(百分比) |
pitch |
HSliderPitch |
0 至 2.0,步长 0.05,默认 1.0 |
rate |
HSliderRate |
0.1 至 10.0,步长 0.05,默认 1.0(exp_edit = true 指数刻度) |
utterance_id |
脚本维护的自增计数器 id |
整数,用于在回调中区分任务 |
interrupt |
固定值 | false(Speak)/ true(Interrupt) |
打断式朗读(Interrupt)与普通朗读的唯一差别就是 interrupt 为 true,此时新任务会立即抢占当前正在播放的朗读。此外还有两个查询状态的方法,voice_list.gd 在 _process() 中每帧调用它们来刷新状态灯:
$ButtonPause.button_pressed = DisplayServer.tts_is_paused()
if DisplayServer.tts_is_speaking():
$ColorRect.color = Color(0.9, 0.3, 0.1) # 正在朗读:红色
else:
$ColorRect.color = Color(1, 1, 1) # 空闲:白色
停止、暂停与恢复则对应三个独立方法(voice_list.gd):
DisplayServer.tts_stop() # 停止并丢弃所有排队任务
DisplayServer.tts_pause() # 暂停当前朗读
DisplayServer.tts_resume() # 从暂停处恢复
回调事件模型
朗读是异步的,调用 tts_speak() 后无法同步得知进度,因此必须注册回调。voice_list.gd 在启动时一次性注册了四类事件:
DisplayServer.tts_set_utterance_callback(DisplayServer.TTS_UTTERANCE_STARTED, _on_utterance_start)
DisplayServer.tts_set_utterance_callback(DisplayServer.TTS_UTTERANCE_ENDED, _on_utterance_end)
DisplayServer.tts_set_utterance_callback(DisplayServer.TTS_UTTERANCE_CANCELED, _on_utterance_error)
DisplayServer.tts_set_utterance_callback(DisplayServer.TTS_UTTERANCE_BOUNDARY, _on_utterance_boundary)
四类事件与回调签名、在演示中的用途如下:
| 事件常量 | 回调签名 | 演示中的行为 |
|---|---|---|
TTS_UTTERANCE_STARTED |
(ut_id: int) |
在日志追加 "Utterance N started." |
TTS_UTTERANCE_ENDED |
(ut_id: int) |
将整段文本渲染为黄色高亮并记日志,同时从 ut_map 中移除该任务 |
TTS_UTTERANCE_CANCELED |
(ut_id: int) |
清空高亮、记日志 "canceled/failed.",并移除任务 |
TTS_UTTERANCE_BOUNDARY |
(pos: int, ut_id: int) |
在 pos 位置用 BBCode 高亮当前读到的字符,实现"跟读"效果 |
边界回调是演示中最直观的反馈——它把朗读文本实时分段渲染到 RichTextLabel 上。见 voice_list.gd:
func _on_utterance_boundary(pos: int, ut_id: int) -> void:
$RichTextLabel.text = "[bgcolor=yellow][color=black]" \
+ ut_map[ut_id].substr(0, pos) + "[/color][/bgcolor]" \
+ ut_map[ut_id].substr(pos, -1)
由于 control.tscn 中 RichTextLabel 开启了 bbcode_enabled = true,这里 [bgcolor=yellow] 与 [color=black] 标签即可生效,朗读进行到哪里,哪里就被实时标黄(截图顶部 "The jaws that" 即处于该状态)。
任务生命周期与日志设计
演示用一个 ut_map: Dictionary 以 utterance_id 为键保存待朗读文本(voice_list.gd),回调通过 ut_id 反查文本,从而正确渲染高亮与日志:
- 提交任务时:
ut_map<a href="https://link.gitcode.com/i/e8dbdb7884d5052def674d71f047a2dc" target="_blank">id] = $Utterance.text存入文本,随后id += 1自增([voice_list.gd); - 开始朗读时:记录 started 事件;
- 朗读结束/取消时:
ut_map.erase(ut_id)清理任务,避免字典无限增长(voice_list.gd)。
右侧 Log 面板(control.tscn)是一个只读 TextEdit,逐行追加所有事件,相当于把异步回调"序列化"成可读的生命周期时间线;截图中可见 "utterance 0 started … utterance 7 ended" 的完整 8 段播报记录,Clear 按钮对应 _on_button_clear_log_pressed(voice_list.gd)负责清空日志。
多语言 Demo:用 tts_get_voices_for_language 播报经典童谣
Demo 按钮(voice_list.gd)演示了多语言朗读的完整套路:对 "en"、"es"、"ru" 三种语言分别调用 tts_get_voices_for_language(),若该语言存在语音(not vc.is_empty()),就取第一个语音连续朗读两行《Jabberwocky》童谣译文,例如:
vc = DisplayServer.tts_get_voices_for_language("ru")
if not vc.is_empty():
ut_map[id] = "О, бойся Бармаглота, сын!"
ut_map[id + 1] = "Он так свирлеп и дик!"
DisplayServer.tts_speak("О, бойся Бармаглота, сын!", vc[0], $HSliderVolume.value, $HSliderPitch.value, $HSliderRate.value, id)
DisplayServer.tts_speak("Он так свирлеп и дик!", vc[0], $HSliderVolume.value, $HSliderPitch.value, $HSliderRate.value, id + 1)
id += 2
由于每个 utterance 都使用独立且连续的自增 id,两行文本会各自触发独立的 started / ended 回调,日志面板因此能完整区分每一句的播报过程。
语音列表过滤:Name / Language 双条件筛选
界面顶部提供 Name 与 Language 两个过滤输入框(control.tscn),其 text_changed 信号都连接到 _on_LineEditFilterName_text_changed(control.tscn)。过滤逻辑见 voice_list.gd:
$Tree.clear()
var root: TreeItem = $Tree.create_item()
for v in vs:
if (
$LineEditFilterName.text.is_empty() or $LineEditFilterName.text.to_lower() in v["name"].to_lower()
) and (
$LineEditFilterLang.text.is_empty() or $LineEditFilterLang.text.to_lower() in v["language"].to_lower()
):
var child: TreeItem = $Tree.create_item(root)
child.set_text(0, v["name"])
child.set_metadata(0, v["id"])
child.set_text(1, v["language"])
其要点包括:以 _ready() 中缓存的 vs 为数据源、大小写不敏感的包含匹配、空输入表示"不过滤"、每次输入即时重建 Tree。首次启动时则遍历全部语音建树,并默认选中第一项(voice_list.gd),保证用户不手动选择也能直接点击 Speak。
工程配置要点
project.godot 中与 TTS 演示直接相关的配置:
[application]
config/name="Text-to-speech demo"
config/description="This is a demo showing text-to-speech support."
config/tags=PackedStringArray("accessibility", "demo", "gui", "official")
run/main_scene="res://control.tscn"
[audio]
general/text_to_speech=true
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
audio/general/text_to_speech=true:TTS 功能的全局开关,缺失或关闭时tts_*()调用将不可用;run/main_scene="res://control.tscn":演示主界面,即本文解析的 Control 场景;- Compatibility 渲染器:README 标注 Renderer 为 Compatibility,与
rendering_method="gl_compatibility"对应,保证 TTS 界面在低端/兼容环境同样可运行(语音合成本身与渲染器无关); - 窗口拉伸模式:
canvas_items+expand,配合场景中基于锚点的布局,窗口缩放时 UI 自适应。
从演示到实战:可复用的接入要点
综合源码,接入 Godot 文本转语音时只需记住五个步骤:
- 在
project.godot中开启audio/general/text_to_speech=true; - 用
DisplayServer.tts_get_voices()枚举语音,向用户展示name/language,保留id; - 用
DisplayServer.tts_speak(text, voice_id, volume, pitch, rate, utterance_id, interrupt)提交任务,自主维护 utterance_id 并建立 id → 文本的映射; - 用
DisplayServer.tts_set_utterance_callback()注册四类事件回调,在TTS_UTTERANCE_BOUNDARY中实现逐字高亮等进度反馈; - 用
tts_stop()/tts_pause()/tts_resume()/tts_is_speaking()/tts_is_paused()管理播放状态。
需要特别留意的是:可用语音、事件粒度甚至部分行为都取决于操作系统与平台的 TTS 实现。本演示通过"先枚举、空则跳过"(如 Demo 按钮中的 is_empty() 判断)的方式对平台差异做了优雅降级——你的生产代码也应在语音缺失、任务被取消(TTS_UTTERANCE_CANCELED)时给出合理反馈,而不是假设朗读必然成功。这是从 voice_list.gd 的完整实现中可以提炼出的最实用经验。
