跳转到主要内容

VOICEVOX 引擎 API 集成应用开发指南

VOICEVOX Song
编写调用 VOICEVOX 引擎 API 的客户端应用时的实现笔记。
本文基于 macOS 用 Song 应用的实现记录,并非将官方 VOICEVOX 编辑器的内部结构原样移植,而是整理作为外部应用如何稳定地使用引擎 API 所需的要点。
为了让 Xcode/Swift 以外的环境也能参考,先介绍通用方针,然后在各条目结尾附上面向 Xcode 的补充说明。
本页基于 原文指南 完整整理,不省略内容。

1. 先通过 URL 注册引擎

VOICEVOX Song
官方 VOICEVOX 编辑器会由应用侧启动内置或指定的引擎可执行文件,并以引擎设置和 engine_manifest.json 为前提进行管理。而作为外部客户端应用,采用”先注册已启动的 HTTP API 服务器的 URL”的方式反而更好处理。 典型的 URL 如下。
引擎示例
官方 VOICEVOX 引擎http://127.0.0.1:50021
Laravel 版引擎http://127.0.0.1:50513
官方引擎具备端口自动调整功能,如果 50021 被占用会在 50022 之后寻找空闲端口启动。
Laravel 版如果不指定 --port=50513 而以默认 8000 启动,也会在 8001 之后寻找端口;但一旦指定端口,则该调整功能被禁用。
注册时不要只保存 URL 字符串,而应作为连接检查至少调用以下 API,并将获取到的元信息一并保存。
API用途
GET /engine_manifest获取 uuid、引擎名称、frame_rate、默认采样率等
GET /version获取用于显示与兼容性检查的版本
GET /singers获取歌唱风格列表
GET /singer_info?speaker_uuid=...&resource_format=...获取图标与附加信息
关键在于:项目或轨道中不应保存 URL,而应保存 engineIdstyleId。URL 因用户环境而异,而 engineId 对应 /engine_manifestuuid,也便于与 .vvprojsinger.engineId 保持一致。 Xcode 补充: 在 Song 应用中,将 baseURLengineIdnameframeRatedefaultSamplingRateversionsingers 保存在 RegisteredEngine 中,并保存到 UserDefaults。URL 在使用前对末尾斜杠、查询串、片段进行规范化,即可将 http://127.0.0.1:50021/http://127.0.0.1:50021 视为等同。在 macOS 应用中通过 HTTP 进行本地连接时,还需要检查 sandbox 与 App Transport Security 的设置。

2. 不要把公司应用式的”引擎启动”揽在自己身上

如果外部应用从一开始就想实现与官方编辑器一样的引擎启动管理,就得处理各操作系统的进程启动、内置二进制、端口冲突、更新、日志管理等。特别是使用官方引擎之外的实现时,指定引擎可执行文件位置并启动的方式很难对上号。 一开始最稳妥的做法是固定为以下流程。
  1. 用户先启动引擎。
  2. 在应用中输入 URL。
  3. 应用获取元信息并注册。
  4. 渲染时通过已注册的 engineId 解析为 URL。
采用这种方式,官方引擎、Laravel 版引擎、Docker 上的引擎、其他主机上的引擎都能以相同抽象处理。当无法连接时,应像”请先启动引擎再重新连接”这样,明确告知用户下一步该做什么。 Xcode 补充: 在 SwiftUI 中,将”引擎连接”界面独立出来,放置 URL 的 TextField注册 / 重新连接 按钮、官方/Laravel 版的预设按钮、已注册引擎列表,便于操作。异步连接时显示 ProgressView,连接错误与其直接显示 LocalizedError 的文案,不如加上面向用户的说明作为前缀。

3. 引擎注册后再让用户选歌手/风格

在 VOICEVOX 的 API 中,最终用于合成的声音由传给 speaker 查询参数的 style ID 决定。Song 每条轨道都有歌手风格,因此 UI 上应从已注册引擎展开 singers,将歌手名、风格名、style ID 作为选项。 保存的值只需以下两项即可。
{
  "singer": {
    "engineId": "engine-uuid",
    "styleId": 6000
  }
}
这种结构便于与 .vvproj 兼容的 Song 数据配合,也便于支持多引擎。打开项目时若对应的 engineId 未注册,可显示为”未注册的引擎”,并提示用户去注册 URL。 Xcode 补充: 在 Swift 中让轨道持有 Singer(engineId: String, styleId: Int),用于显示时可从 RegisteredEngine.singers 拍平为像 SingerStyleOption 这样供 ViewModel 使用的数组。切换歌手指派时,应使已渲染的音频失效。

4. 若考虑 .vvproj 兼容,采用以 tick 为基准的 Song 模型

VOICEVOX 编辑器的 Song 数据是以 tick 为基准来存储音符与速度的。虽然引擎 API 本身最终需要帧长,但应用内部的编辑模型采用 tick 基准更适合钢琴卷帘、速度变化、拍号、Undo/Redo,以及 .vvproj 的读写。 最基本的结构如下。
元素主要字段
Songtpqn, tempos, timeSignatures, tracks, trackOrder
Trackname, singer, notes, gain, pan, solo, mute
Noteid, position, duration, noteNumber, lyric
Tempoposition, bpm
TimeSignaturemeasureNumber, beats, beatType
仅在渲染时使用速度表将 tick 转换为秒,并按引擎的 frame_rate 计算 frame_length Xcode 补充: 使用 Codable 读写类似 .vvproj 的 JSON 时,tracks 建议用以 ID 为键的 Dictionary,trackOrder 作为显示顺序数组单独分开。保存前对 trackOrder 中的重复、不存在的 ID、未排序的轨道进行校验,可以避免读入后 UI 出问题。

5. Song 合成实现为四阶段 API

Song 的渲染流程比 Talk 的 /audio_query/synthesis 多。基本流水线为以下 4 个阶段。 /sing_frame_audio_query/sing_frame_f0/sing_frame_volume 使用 singing teacher 的 style ID 6000,只有最后的 /frame_synthesis 才使用实际所选歌手的 styleId。这一约定在官方引擎和 Laravel 版引擎中都是一致的。 各 API 请求中,关键是不要破坏 ScoreFrameAudioQuery 的对应关系。Score.notes[].frame_length 根据引擎的 frame_rate 计算,静音用 key: nulllyric: "" 表示。 Xcode 补充: 在 Swift 中可以创建像 VoicevoxEngineSongAPI 这样的 protocol,将实现替换为 URLSession 客户端便于测试。对返回 JSON 的端点将 Content-Type / Accept 设为 application/json/frame_synthesis 的响应作为 WAV 二进制 Data 处理。HTTP 非 2xx 应作为包含状态码和响应体的错误抛出。

6. 按 phrase 拆分并进行缓存

如果每次都对整首 Song 一次性合成,稍作编辑就要重新生成所有轨道,UI 响应会变差。像官方编辑器那样,将连续音符视为 phrase,并按休止符切分的设计更合适。 对每个 phrase 分别缓存以下阶段。
缓存输入中应包含的内容
AudioQuery引擎 ID、frame rate、速度、Score、音域调整
F0AudioQuery、音高编辑、phrase 位置
VolumeAudioQuery、F0、影响音量生成的值
Voice/WAV合成用 Query、最终 style ID
初期实现可以在有编辑时直接使目标轨道的已渲染音频整体失效。为后续实现按 phrase 差分失效做准备,可用输入的哈希作为缓存 key。 Xcode 补充:actor 持有渲染缓存,可以在 Swift Concurrency 中方便地管理状态。开始新的渲染时推进 generation 编号,旧的 API 响应即使返回也不采纳。将 Task.checkCancellation() 与 generation 检查结合起来,即可同时应对取消与重渲染。

7. 帧率与静音时长根据引擎元信息决定

ScoreNote.frame_length 通过秒数与 frame_rate 计算得出。frame_rate 不要写死为固定值,而应在注册时从 /engine_manifest 获取并保存。 Song 合成时会在 phrase 首尾插入静音。若首静音过短,音素生成可能不稳定,因此实现中应在”实际静音”、“四分音符”、“从最低秒数反算得到的 tick”之间取得平衡。末尾也追加短暂的静音,必要时对末尾静音区间做淡出。 此外,帧长在取整后应校正使其不小于等于 0。较短的音符或紧跟着速度变化时,容易因取整误差产生 0 帧。 Xcode 补充:tickToSecond / secondToTick 设为纯函数并写好测试,就能在钢琴卷帘、播放头、渲染、视频输出中复用同一转换。frameLength = Int(round(seconds * frameRate)) 的结果应通过将差值传递给相邻音符等方式,保证至少 1 帧。

8. 参数编辑按”生成前”与”合成前”的作用点分开

在 Song 中,音域、音量、音高、音量、音素时长的编辑会分别影响不同阶段。
编辑生效时机
keyRangeAdjustment生成 Score 时的 key,以及 F0 生成后的 pitch shift
volumeRangeAdjustment/frame_synthesis 之前的音量数组
音高编辑/sing_frame_f0 之后的 F0 数组
音量编辑/sing_frame_volume 之后的 volume 数组
音素时长编辑FrameAudioQuery.phonemes
尽早确定哪种编辑会使哪些缓存失效,之后即使增加 UI 编辑功能,渲染结果也不易出问题。 Xcode 补充: 参数编辑的应用不要散落在各个 ViewModel 中,而是集中到像 SongParameterEditApplicator 这样的纯逻辑中,便于测试。对数组越界访问、负音量等要显式修正或抛错。

9. 播放、混合、导出使用同一套轨道判定

渲染完成后,将各 phrase 的 WAV 放到时间轴上进行播放。多轨道场景下,若普通播放、整体 WAV 导出、Stem 导出中 solo/mute/gain/pan 的判定不一致,对用户来说行为难以预测。 作为通用规则:只要存在任一 solo 轨道,就只输出 solo 轨道;不存在 solo 时则输出未 mute 的轨道。Stem 导出时单独输出目标轨道,与普通的 solo/mute 状态分开处理。 Xcode 补充: 在 macOS 上可以用 AVAudioEngineAVAudioPlayerNodeAVAudioMixerNode 构建播放图。导出应准备与普通播放分开的离线渲染路径,并共用 WAV 编码。播放中如果发生编辑导致已渲染音频失效,应停止播放并明确显示状态。

10. UI 要区分”未注册”、“未指派”、“未渲染”

在 VOICEVOX 引擎 API 集成应用中,失败原因可以分为多种。
状态应显示的指引
引擎未注册在引擎连接界面注册 URL
引擎未启动启动引擎后重新连接
项目的 engineId 未注册额外注册对应的引擎 URL
轨道未指派歌手选择已注册的歌手风格
编辑后音频已过期需要重新渲染
渲染中允许取消
如果这些全部混在一起显示为”无法播放”,用户就无法判断原因。应区分状态,并在 UI 上呈现下一步操作。 Xcode 补充: 在 SwiftUI 的 ViewModel 中,将 renderingState 设为 idlerenderingrenderedstalefailed 之类的 enum,便于统一控制按钮 disabled 与状态显示。错误消息不仅在状态栏,也应可通过 Alert 显示。

11. 多引擎场景下将”URL”与”engineId”的生命周期分开

多引擎支持时最容易混淆的是 URL 与 engineId 的角色差异。
生命周期用法
URL因用户环境而异用作 API 调用地址
engineId引擎实现/模型侧的标识用作项目内的引用
styleId引擎内的风格标识用作 speaker 参数
项目文件中不要嵌入 URL,而应保存 engineIdstyleId。在应用设置侧保存 engineId -> URL 的映射。这样即使收到他人的项目文件,只要在自己的环境中把同一引擎按 URL 注册,就能渲染。 Xcode 补充: 在渲染前通过类似 Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) }) 的方式从 engineId 解析出 RegisteredEngine。若未找到则不发起 HTTP 请求,而提前失败为”歌手对应的引擎未注册”。

12. 测试将 HTTP 客户端、模型转换、渲染计划分开

始终启动 VOICEVOX 引擎本体进行测试成本较高,因此在常规的自动化测试中 mock HTTP,只验证应用侧的请求生成与状态迁移。 优先测试的项目如下。
目标需确认的内容
URL 规范化末尾斜杠或带路径的 URL 是否按预期处理
引擎注册能调用 /engine_manifest/version/singers/singer_info 并构建保存用模型
.vvproj I/OtrackOrder 与 tracks 的一致性,旧/未来版本的处理
tick/second 转换跨越速度变化时可以来回转换
Score 生成静音、frame_length、默认歌词、音域调整
渲染计划在发起 API 调用前检测出未注册引擎、未指派歌手、engineId 不匹配
缓存同输入可复用,编辑后失效
Xcode 补充: 使用替换了 URLProtocolURLSessionConfiguration.ephemeral,就能在没有真实 HTTP 服务器的情况下测试 URLSession 客户端。将渲染器通过 protocol 注入 API,actor 的缓存状态提供快照获取方法便于验证。

推荐的实现顺序

如果从最小构成开始,推荐以下顺序。
1

引擎 URL 注册与元信息保存

注册 URL,并将从 /engine_manifest/version 获取的元信息一并保存。
2

歌手风格列表与指派

展示歌手风格列表,并将 engineId + styleId 指派给轨道。
3

以 tick 为基准的模型

先确定以 tick 为基准的 Song/Track/Note 模型。
4

四阶段 API 客户端

实现 Score 生成与四阶段 Song API 客户端。
5

按 phrase 拆分的渲染

引入按 phrase 渲染与缓存机制。
6

播放功能

使渲染完成的 WAV 能在时间轴上播放。
7

多轨道判定

统一多轨道的 solo/mute/gain/pan 判定。
8

导出

追加 WAV 导出与 Stem 导出功能。
9

编辑功能

追加音高、音量、音素时长的编辑功能。
10

进阶功能

进入 .vvproj 兼容读写、视频导出等进阶功能。
与其一开始就追求官方编辑器的全部功能,不如先把 URL 注册、歌手指派、四阶段 API、重新渲染状态这些做扎实,这样对官方引擎与 Laravel 版引擎都能兼容良好。

相关链接

最后修改于 2026年7月13日