godot-demo-projects 实战:基于 DisplayServer 的文本转语音(TTS)Demo 完整解析

原创2026-10-02 22:11:101,455 阅读
文章标签:示例工程

godot-demo-projects 实战:基于 DisplayServer 的文本转语音(TTS)Demo 完整解析

本指南以仓库 audio/text_to_speech 下的官方 TTS 演示为核心,逐层拆解 Godot 引擎通过 DisplayServer 单例暴露的 tts_*() 方法族:如何枚举操作系统提供的语音、如何提交朗读任务(utterance)、如何通过回调信号跟踪朗读进度,以及如何用 GDScript 实现语速/音调/音量调节、暂停/恢复/打断与多语言朗读。读完本文,你将掌握一套可直接复用的文本转语音接入方案,并理解其底层调用链与回调事件模型。

Text-to-Speech 演示界面

演示运行界面:左侧为语音列表(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_*() 方法,完成三件事:

  1. 枚举语音信息:查询操作系统已安装的 TTS 语音及其属性;
  2. 提交朗读任务:把文本与参数发送给操作系统级 TTS API;
  3. 接收回调信号:监听朗读过程中产生的事件并做出响应。

值得注意的是,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 文本转语音时只需记住五个步骤:

  1. 在 project.godot 中开启 audio/general/text_to_speech=true;
  2. 用 DisplayServer.tts_get_voices() 枚举语音,向用户展示 name / language,保留 id;
  3. 用 DisplayServer.tts_speak(text, voice_id, volume, pitch, rate, utterance_id, interrupt) 提交任务,自主维护 utterance_id 并建立 id → 文本的映射;
  4. 用 DisplayServer.tts_set_utterance_callback() 注册四类事件回调,在 TTS_UTTERANCE_BOUNDARY 中实现逐字高亮等进度反馈;
  5. 用 tts_stop() / tts_pause() / tts_resume() / tts_is_speaking() / tts_is_paused() 管理播放状态。

需要特别留意的是:可用语音、事件粒度甚至部分行为都取决于操作系统与平台的 TTS 实现。本演示通过"先枚举、空则跳过"(如 Demo 按钮中的 is_empty() 判断)的方式对平台差异做了优雅降级——你的生产代码也应在语音缺失、任务被取消(TTS_UTTERANCE_CANCELED)时给出合理反馈,而不是假设朗读必然成功。这是从 voice_list.gd 的完整实现中可以提炼出的最实用经验。

登录后查看全文
godot-demo-projects