VOICEVOX 引擎 API 整合應用開發指南

Song 應用
以 macOS 用 song 應用的實作紀錄為基礎,不將官方 VOICEVOX 編輯器的內部結構原樣移植,而是整理作為外部應用穩定使用引擎 API 所需的項目。 為了讓 Xcode/Swift 以外的環境也能使用,先寫下通用方針,並在各項目結尾附上 Xcode 的補充說明。
本頁基於 原文指南 文件化,內容並未省略。
1. 一開始就以 URL 註冊引擎

引擎連線設定
engine_manifest.json 為前提管理。相較之下,外部 client 應用先「註冊已啟動的 HTTP API 伺服器 URL」的方式更好處理。
代表性的 URL 如下。
官方引擎有自動 port 調整功能,若 50021 已被使用會以 50022 之後空著的 port 啟動。
Laravel 版若未指定
--port=50513 而以預設的 8000 啟動,會以 8001 之後啟動,但指定 port 時調整功能會被停用。
註冊時不要僅保存 URL 字串,而是作為連線確認至少呼叫下列 API,並將取得的 metadata 一併保存。
重點是,專案或 track 不要保存 URL,而是
engineId 與 styleId。URL 因使用者環境不同而異,但 engineId 對應 /engine_manifest 的 uuid,也容易對應到 .vvproj 的 singer.engineId。
Xcode 補充: Song 應用中,在 RegisteredEngine 保存 baseURL、engineId、name、frameRate、defaultSamplingRate、version、singers,並存入 UserDefaults。URL 先將結尾斜線、query、fragment 正規化後再使用,可將 http://127.0.0.1:50021/ 與 http://127.0.0.1:50021 視為相同。macOS 應用使用 HTTP 本地連線時,也請確認 sandbox 與 App Transport Security 的設定。
2. 不要像官方應用一樣抱著「引擎啟動」
若要以外部應用一開始就實作與官方編輯器相同的引擎啟動管理,需要處理 OS 別的程序啟動、內建 binary、port 衝突、更新、log 管理。特別是使用非官方引擎的實作時,以指定引擎執行檔位置來啟動的方式較不契合。 首先固定為以下運作方式較為安全。- 使用者先啟動引擎。
- 於應用輸入 URL。
- 應用取得 metadata 並註冊。
- 渲染時從已註冊的
engineId解析為 URL。
TextField 讓輸入 URL、註冊 / 重新連線 按鈕、官方/Laravel 版預設按鈕與已註冊引擎清單,運作起來會較順利。非同步連線中顯示 ProgressView,連線錯誤時建議先加上對使用者的說明,而非直接顯示 LocalizedError 的訊息。
3. 歌手/Style 由使用者於註冊引擎後選擇
在 VOICEVOX 的 API 中,最終音訊產生使用的聲音由傳給speaker query 參數的 style ID 決定。Song 中每個 track 都持有歌手 style,因此 UI 上會展開已註冊引擎的 singers,並將歌手名、style 名、style ID 作為選項。
保存的值有以下 2 個即可。
.vvproj 相容的 song 資料,也易於支援多引擎。開啟專案時若對應的 engineId 未註冊,顯示為「未註冊引擎」並提示註冊 URL。
Xcode 補充: Swift 中讓 track 持有 Singer(engineId: String, styleId: Int),顯示時可從 RegisteredEngine.singers 平坦化為 SingerStyleOption 等 ViewModel 用陣列。更改歌手指派時,已渲染的音訊應失效。
4. 考慮 .vvproj 相容時採用以 tick 為基準的 song 模型
VOICEVOX 編輯器的 song 資料以 tick 為基準保存音符與 tempo。引擎 API 本身最終需要 frame length,但應用內的編輯模型採用 tick 基準對於 piano roll、tempo 變更、拍號、Undo/Redo、.vvproj 讀寫較為友善。
最基本的結構如下。
只在渲染時使用 tempo map 將 tick 轉為秒,並依引擎的
frame_rate 計算 frame_length。
Xcode 補充: 用 Codable 讀寫 .vvproj 風格 JSON 時,tracks 使用以 ID 為 key 的 Dictionary,trackOrder 分開為顯示順序陣列。保存前檢查 trackOrder 重複、不存在的 ID、未排序的 track,可避免載入後 UI 崩壞。
5. Song 合成以 4 階段 API 實作
Song 的渲染比 Talk 的/audio_query → /synthesis 多出許多步驟。基本 pipeline 分為以下 4 階段。
/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 client 進行,便於測試。回傳 JSON 的 endpoint 中將 Content-Type / Accept 設為 application/json,而 /frame_synthesis 的回應以 WAV binary 的 Data 處理。HTTP 非 2xx 時應包含狀態碼與回應內容的錯誤。
6. 以片段(phrase)為單位分割並快取
若每次都將整首歌一次合成,只要小幅編輯就會導致全 track 重新產生,UI 反應變差。與官方編輯器類似,將連續音符群分為 phrase 並以休止符分割的設計較易處理。 每個 phrase 快取以下各階段。
初期實作中,當有編輯時可直接使該 track 的渲染音訊全部失效。設計 cache key 時以輸入 hash 建立,方便日後加入以 phrase 為單位的差異失效。
Xcode 補充: 讓
actor 持有渲染快取,便於在 Swift Concurrency 上進行狀態管理。新渲染開始時世代編號往前推進,即便舊 API 回應返回也不採用。搭配 Task.checkCancellation() 與世代檢查,可同時處理取消與重新渲染。
7. 從引擎 metadata 決定 frame rate 與休止
ScoreNote.frame_length 由秒數與 frame_rate 計算。不要將 frame_rate 寫死為固定值,而是註冊時從 /engine_manifest 取得後保存。
Song 合成中,phrase 開頭與結尾要插入休止符。開頭休止太短時音素生成可能不穩定,實作時要在「實際的休止」「四分音符」「從最低秒數反推的 tick」之間取得平衡。結尾也加入短的無音,必要時對最後無音區間 fade out。
此外,frame length 若因四捨五入變成 0 以下要進行修正。短音符或 tempo 變更後,容易因四捨五入誤差產生 0 frame。
Xcode 補充: 將 tickToSecond / secondToTick 設計為純函式並測試後,可於 piano roll、播放頭、渲染、影片輸出中使用相同的轉換。frameLength = Int(round(seconds * frameRate)) 的結果建議將差值移至相鄰音符等方式,以保證最低 1 frame。
8. 參數編輯依「產生前」與「合成前」分開
Song 中,音域、音量、pitch、volume、音素 timing 編輯分別影響不同階段。
儘早決定哪個編輯會使哪些快取失效,日後追加 UI 編輯功能時,渲染結果就不易崩壞。
Xcode 補充: 參數編輯的套用不要散落在 ViewModel,建議集中在
SongParameterEditApplicator 之類的純邏輯中,便於測試。陣列越界存取與負值音量應明確修正或錯誤化。
9. 播放、mix、輸出使用相同的 track 判定
渲染後將 phrase 的 WAV 排放在 timeline 上播放。多 track 支援時,若一般播放、整體 WAV 輸出、stem 輸出中 solo/mute/gain/pan 判定不同,對使用者而言行為會難以預期。 作為共用規則:只要有 1 個 solo track 就只輸出 solo track;沒有 solo 時輸出未 mute 的 track。Stem 輸出時單獨輸出目標 track,與一般 solo/mute 狀態分開考量。 Xcode 補充: macOS 上可用AVAudioEngine、AVAudioPlayerNode、AVAudioMixerNode 建立播放 graph。輸出時另外準備一條 offline 渲染路徑,將 WAV 編碼共用。播放中若編輯導致已渲染音訊失效,應停止播放並明確標示狀態。
10. UI 區分「未註冊、未指派、未渲染」
在 VOICEVOX 引擎 API 整合應用中,失敗原因可分為以下數種。
若都合併為「無法播放」則使用者無法得知原因。應分開狀態,並在 UI 顯示下一步操作。
Xcode 補充: SwiftUI 的 ViewModel 中將
renderingState 定為 idle、rendering、rendered、stale、failed 之類的 enum,可讓按鈕的 disabled 控制與狀態顯示一致。錯誤訊息除了狀態列,也應可顯示在 alert。
11. 多引擎中將「URL」與「engineId」的存續期分開
多引擎支援時容易混淆的是 URL 與engineId 的角色不同。
專案檔案中不要嵌入 URL,而是保存
engineId 與 styleId。應用設定端持有 engineId -> URL 的對應。這樣即使從他人接收專案,只要在自己的環境中以 URL 註冊相同引擎即可渲染。
Xcode 補充: 用 Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) }) 於渲染前從 engineId 解析為 RegisteredEngine。找不到時不應發送 HTTP 請求,而是事先以「歌手的引擎未註冊」失敗。
12. 測試分開 HTTP client、模型轉換、渲染計畫
由於一直啟動 VOICEVOX 引擎本體測試較重,一般自動測試中對 HTTP 進行 mock,確認應用端的請求產生與狀態轉移。 優先測試的項目如下。
Xcode 補充: 使用替換
URLProtocol 的 URLSessionConfiguration.ephemeral,不需實際 HTTP 伺服器即可測試 URLSession client。渲染器透過 protocol 注入 API,actor 的快取狀態提供傳回快照的方法便於驗證。
建議實作順序
從最小構成開始的話,以下順序較佳。1
引擎 URL 註冊與 metadata 保存
註冊 URL,並將從
/engine_manifest 或 /version 取得的 metadata 一併保存。2
歌手 style 一覽與指派
顯示歌手 style 清單,將
engineId + styleId 指派給 track。3
以 tick 為基準的模型
先固化以 tick 為基準的 Song/Track/Note 模型。
4
4 階段 API client
實作
Score 生成與 4 階段 song API client。5
以 phrase 分割的渲染
引入以 phrase 為單位的渲染與快取。
6
播放功能
讓已渲染的 WAV 能在 timeline 上播放。
7
多 track 判定
統一多 track 的 solo/mute/gain/pan 判定。
8
輸出
加入 WAV 輸出與 stem 輸出。
9
編輯功能
加入 pitch、volume、音素 timing 編輯。
10
進階功能
進一步發展
.vvproj 相容讀寫或影片輸出等進階功能。