> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 引擎 API 集成应用开发指南 - VOICEVOX for Laravel

> 从实现视角整理开发调用 VOICEVOX 引擎 API 的客户端应用所需的 URL 注册、Song 合成流水线、缓存、多引擎设计等要点。

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

<Frame caption="Song 应用">
  <img src="https://mintcdn.com/invokable/BODEyCSwaMBfQXfp/images/laravel-voicevox/song-app-1.png?fit=max&auto=format&n=BODEyCSwaMBfQXfp&q=85&s=91a6f894c9a5c128517f125e1afaa5ce" alt="VOICEVOX Song" width="3106" height="1646" data-path="images/laravel-voicevox/song-app-1.png" />
</Frame>

编写调用 VOICEVOX 引擎 API 的客户端应用时的实现笔记。\
本文基于 macOS 用 Song 应用的实现记录，并非将官方 VOICEVOX 编辑器的内部结构原样移植，而是整理作为外部应用如何稳定地使用引擎 API 所需的要点。

为了让 Xcode/Swift 以外的环境也能参考，先介绍通用方针，然后在各条目结尾附上面向 Xcode 的补充说明。

<Info>
  本页基于 [原文指南](https://github.com/invokable/laravel-voicevox/blob/main/docs/develop/voicevox-engine-api-app-guide.md) 完整整理，不省略内容。
</Info>

## 1. 先通过 URL 注册引擎

<Frame caption="引擎连接设置">
  <img src="https://mintcdn.com/invokable/-Erq4G4RzGqd8RU2/images/laravel-voicevox/song-app-2.png?fit=max&auto=format&n=-Erq4G4RzGqd8RU2&q=85&s=f8676c5ab7b58f87d87c2585da87ff58" alt="VOICEVOX Song" width="2024" height="1248" data-path="images/laravel-voicevox/song-app-2.png" />
</Frame>

官方 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，而应保存 `engineId` 与 `styleId`。URL 因用户环境而异，而 `engineId` 对应 `/engine_manifest` 的 `uuid`，也便于与 `.vvproj` 的 `singer.engineId` 保持一致。

**Xcode 补充：** 在 Song 应用中，将 `baseURL`、`engineId`、`name`、`frameRate`、`defaultSamplingRate`、`version`、`singers` 保存在 `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 作为选项。

保存的值只需以下两项即可。

```jsonc theme={null}
{
  "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` 的读写。

最基本的结构如下。

| 元素            | 主要字段                                                       |
| ------------- | ---------------------------------------------------------- |
| Song          | `tpqn`, `tempos`, `timeSignatures`, `tracks`, `trackOrder` |
| Track         | `name`, `singer`, `notes`, `gain`, `pan`, `solo`, `mute`   |
| Note          | `id`, `position`, `duration`, `noteNumber`, `lyric`        |
| Tempo         | `position`, `bpm`                                          |
| TimeSignature | `measureNumber`, `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 个阶段。

```mermaid theme={null}
sequenceDiagram
  participant App as 客户端<br>应用
  participant Engine as VOICEVOX<br>引擎

  App->>Engine: POST /sing_frame_audio_query?speaker=6000
  Engine-->>App: FrameAudioQuery
  App->>Engine: POST /sing_frame_f0?speaker=6000
  Engine-->>App: f0[]
  App->>Engine: POST /sing_frame_volume?speaker=6000
  Engine-->>App: volume[]
  App->>Engine: POST /frame_synthesis?speaker=track.styleId
  Engine-->>App: WAV
```

`/sing_frame_audio_query`、`/sing_frame_f0`、`/sing_frame_volume` 使用 singing teacher 的 style ID `6000`，只有最后的 `/frame_synthesis` 才使用实际所选歌手的 `styleId`。这一约定在官方引擎和 Laravel 版引擎中都是一致的。

各 API 请求中，关键是不要破坏 `Score` 与 `FrameAudioQuery` 的对应关系。`Score.notes[].frame_length` 根据引擎的 `frame_rate` 计算，静音用 `key: null`、`lyric: ""` 表示。

**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、音域调整 |
| F0         | AudioQuery、音高编辑、phrase 位置      |
| Volume     | AudioQuery、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 上可以用 `AVAudioEngine`、`AVAudioPlayerNode`、`AVAudioMixerNode` 构建播放图。导出应准备与普通播放分开的离线渲染路径，并共用 WAV 编码。播放中如果发生编辑导致已渲染音频失效，应停止播放并明确显示状态。

## 10. UI 要区分"未注册"、"未指派"、"未渲染"

在 VOICEVOX 引擎 API 集成应用中，失败原因可以分为多种。

| 状态                 | 应显示的指引        |
| ------------------ | ------------- |
| 引擎未注册              | 在引擎连接界面注册 URL |
| 引擎未启动              | 启动引擎后重新连接     |
| 项目的 `engineId` 未注册 | 额外注册对应的引擎 URL |
| 轨道未指派歌手            | 选择已注册的歌手风格    |
| 编辑后音频已过期           | 需要重新渲染        |
| 渲染中                | 允许取消          |

如果这些全部混在一起显示为"无法播放"，用户就无法判断原因。应区分状态，并在 UI 上呈现下一步操作。

**Xcode 补充：** 在 SwiftUI 的 ViewModel 中，将 `renderingState` 设为 `idle`、`rendering`、`rendered`、`stale`、`failed` 之类的 enum，便于统一控制按钮 disabled 与状态显示。错误消息不仅在状态栏，也应可通过 Alert 显示。

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

多引擎支持时最容易混淆的是 URL 与 `engineId` 的角色差异。

| 值          | 生命周期        | 用法              |
| ---------- | ----------- | --------------- |
| URL        | 因用户环境而异     | 用作 API 调用地址     |
| `engineId` | 引擎实现/模型侧的标识 | 用作项目内的引用        |
| `styleId`  | 引擎内的风格标识    | 用作 `speaker` 参数 |

项目文件中不要嵌入 URL，而应保存 `engineId` 与 `styleId`。在应用设置侧保存 `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/O  | trackOrder 与 tracks 的一致性，旧/未来版本的处理                                   |
| tick/second 转换 | 跨越速度变化时可以来回转换                                                        |
| Score 生成       | 静音、frame\_length、默认歌词、音域调整                                           |
| 渲染计划           | 在发起 API 调用前检测出未注册引擎、未指派歌手、engineId 不匹配                               |
| 缓存             | 同输入可复用，编辑后失效                                                         |

**Xcode 补充：** 使用替换了 `URLProtocol` 的 `URLSessionConfiguration.ephemeral`，就能在没有真实 HTTP 服务器的情况下测试 `URLSession` 客户端。将渲染器通过 protocol 注入 API，`actor` 的缓存状态提供快照获取方法便于验证。

## 推荐的实现顺序

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

<Steps>
  <Step title="引擎 URL 注册与元信息保存">
    注册 URL，并将从 `/engine_manifest`、`/version` 获取的元信息一并保存。
  </Step>

  <Step title="歌手风格列表与指派">
    展示歌手风格列表，并将 `engineId + styleId` 指派给轨道。
  </Step>

  <Step title="以 tick 为基准的模型">
    先确定以 tick 为基准的 Song/Track/Note 模型。
  </Step>

  <Step title="四阶段 API 客户端">
    实现 `Score` 生成与四阶段 Song API 客户端。
  </Step>

  <Step title="按 phrase 拆分的渲染">
    引入按 phrase 渲染与缓存机制。
  </Step>

  <Step title="播放功能">
    使渲染完成的 WAV 能在时间轴上播放。
  </Step>

  <Step title="多轨道判定">
    统一多轨道的 solo/mute/gain/pan 判定。
  </Step>

  <Step title="导出">
    追加 WAV 导出与 Stem 导出功能。
  </Step>

  <Step title="编辑功能">
    追加音高、音量、音素时长的编辑功能。
  </Step>

  <Step title="进阶功能">
    进入 `.vvproj` 兼容读写、视频导出等进阶功能。
  </Step>
</Steps>

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

## 相关链接

* [VOICEVOX Core for PHP 包页面](/zh/packages/voicevox-core-php)
* [引擎 API 模式：Talk](/zh/packages/laravel-voicevox/engine-talk)
* [引擎 API 模式：Song](/zh/packages/laravel-voicevox/engine-song)
* [Score 与 Note 详解](/zh/packages/laravel-voicevox/song-score-note)
* [.vvproj 文件规范](/zh/packages/laravel-voicevox/vvproj)


## Related topics

- [VOICEVOX for Laravel](/zh/packages/laravel-voicevox/index.md)
- [开始使用 - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/getting-started.md)
- [引擎 API 模式：Talk（文本语音合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/engine-talk.md)
- [引擎 API 模式：Song（歌声合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/engine-song.md)
- [Laravel AI SDK 集成 - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/ai-sdk.md)
