Skip to main content

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

VOICEVOX Song

Song 應用

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

1. 一開始就以 URL 註冊引擎

VOICEVOX Song

引擎連線設定

官方 VOICEVOX 編輯器會由應用啟動內建或指定的引擎執行檔,並以引擎設定或 engine_manifest.json 為前提管理。相較之下,外部 client 應用先「註冊已啟動的 HTTP API 伺服器 URL」的方式更好處理。 代表性的 URL 如下。 官方引擎有自動 port 調整功能,若 50021 已被使用會以 50022 之後空著的 port 啟動。
Laravel 版若未指定 --port=50513 而以預設的 8000 啟動,會以 8001 之後啟動,但指定 port 時調整功能會被停用。
註冊時不要僅保存 URL 字串,而是作為連線確認至少呼叫下列 API,並將取得的 metadata 一併保存。 重點是,專案或 track 不要保存 URL,而是 engineIdstyleId。URL 因使用者環境不同而異,但 engineId 對應 /engine_manifestuuid,也容易對應到 .vvprojsinger.engineId Xcode 補充: Song 應用中,在 RegisteredEngine 保存 baseURLengineIdnameframeRatedefaultSamplingRateversionsingers,並存入 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 管理。特別是使用非官方引擎的實作時,以指定引擎執行檔位置來啟動的方式較不契合。 首先固定為以下運作方式較為安全。
  1. 使用者先啟動引擎。
  2. 於應用輸入 URL。
  3. 應用取得 metadata 並註冊。
  4. 渲染時從已註冊的 engineId 解析為 URL。
此方式下,官方引擎、Laravel 版引擎、Docker 上的引擎、其他 host 的引擎都能以相同的抽象處理。無法連線時明確告知使用者「請啟動引擎後再連線」等下一步操作。 Xcode 補充: SwiftUI 中將「引擎連線」畫面獨立,放上 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 請求中,重要的是不破壞 ScoreFrameAudioQuery 的對應。Score.notes[].frame_length 從引擎的 frame_rate 計算,靜音以 key: nulllyric: "" 表示。 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 上可用 AVAudioEngineAVAudioPlayerNodeAVAudioMixerNode 建立播放 graph。輸出時另外準備一條 offline 渲染路徑,將 WAV 編碼共用。播放中若編輯導致已渲染音訊失效,應停止播放並明確標示狀態。

10. UI 區分「未註冊、未指派、未渲染」

在 VOICEVOX 引擎 API 整合應用中,失敗原因可分為以下數種。 若都合併為「無法播放」則使用者無法得知原因。應分開狀態,並在 UI 顯示下一步操作。 Xcode 補充: SwiftUI 的 ViewModel 中將 renderingState 定為 idlerenderingrenderedstalefailed 之類的 enum,可讓按鈕的 disabled 控制與狀態顯示一致。錯誤訊息除了狀態列,也應可顯示在 alert。

11. 多引擎中將「URL」與「engineId」的存續期分開

多引擎支援時容易混淆的是 URL 與 engineId 的角色不同。 專案檔案中不要嵌入 URL,而是保存 engineIdstyleId。應用設定端持有 engineId -> URL 的對應。這樣即使從他人接收專案,只要在自己的環境中以 URL 註冊相同引擎即可渲染。 Xcode 補充:Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) }) 於渲染前從 engineId 解析為 RegisteredEngine。找不到時不應發送 HTTP 請求,而是事先以「歌手的引擎未註冊」失敗。

12. 測試分開 HTTP client、模型轉換、渲染計畫

由於一直啟動 VOICEVOX 引擎本體測試較重,一般自動測試中對 HTTP 進行 mock,確認應用端的請求產生與狀態轉移。 優先測試的項目如下。 Xcode 補充: 使用替換 URLProtocolURLSessionConfiguration.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 相容讀寫或影片輸出等進階功能。
與其從一開始就追求官方編輯器的所有功能,不如先固化 URL 註冊、歌手指派、4 階段 API、重新渲染狀態,這樣不論對官方引擎或 Laravel 版引擎都容易對齊。

相關連結

最後修改於 2026年8月2日