메인 콘텐츠로 건너뛰기

VOICEVOX 엔진 API 연동 앱 개발 가이드

VOICEVOX Song
VOICEVOX 엔진 API를 호출하는 클라이언트 앱을 만들 때의 구현 메모입니다.
macOS용 송 앱의 구현 기록을 바탕으로, 공식 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_manifestuuid, 엔진 이름, frame_rate, 기본 샘플링 레이트 등을 조회
GET /version표시·호환성 확인용 버전을 조회
GET /singers노래 스타일 목록을 조회
GET /singer_info?speaker_uuid=...&resource_format=...아이콘이나 추가 정보를 조회
중요한 것은 프로젝트나 트랙에는 URL이 아니라 engineIdstyleId를 저장하는 것입니다. URL은 사용자 환경마다 달라지지만, engineId/engine_manifestuuid에 대응하며, .vvprojsinger.engineId에도 맞추기 쉽습니다. Xcode용 보충: 송 앱에서는 RegisteredEnginebaseURL, engineId, name, frameRate, defaultSamplingRate, version, singers를 보관하고, UserDefaults에 저장합니다. URL은 끝의 슬래시, 쿼리, 프래그먼트를 정규화한 뒤 사용하면 http://127.0.0.1:50021/http://127.0.0.1:50021을 동일하게 다룰 수 있습니다. macOS 앱에서 HTTP의 로컬 연결을 사용하는 경우에는 샌드박스나 App Transport Security 설정도 확인합니다.

2. 공식 앱과 같은 “엔진 시작”까지 떠안지 않는다

외부 앱에서 처음부터 공식 에디터와 같은 엔진 시작 관리를 구현하려고 하면, OS별 프로세스 시작, 동봉 바이너리, 포트 충돌, 업데이트, 로그 관리까지 다뤄야 합니다. 특히 공식 엔진 이외의 구현을 사용하는 경우, 엔진 실행 파일 위치를 지정해 시작하는 방식은 궁합이 좋지 않습니다. 우선은 다음 운영으로 고정하는 것이 안전합니다.
  1. 사용자가 엔진을 먼저 시작합니다.
  2. 앱에서 URL을 입력합니다.
  3. 앱이 메타 정보를 가져와 등록합니다.
  4. 렌더링 시에는 등록된 engineId에서 URL로 해결합니다.
이 방식이라면 공식 엔진, Laravel판 엔진, Docker 상의 엔진, 다른 호스트의 엔진을 같은 추상화로 다룰 수 있습니다. 연결할 수 없는 경우에는 “엔진을 시작한 뒤 다시 연결해 주세요”처럼, 사용자가 다음에 취할 작업을 명시합니다. Xcode용 보충: SwiftUI에서는 “엔진 연결” 화면을 독립시키고, TextField에 URL, 등록 / 재연결 버튼, 공식/Laravel판 프리셋 버튼, 등록된 엔진 목록을 두면 운영하기 쉽습니다. 비동기 연결 중에는 ProgressView를 표시하고, 연결 오류는 LocalizedError 문구를 그대로 표시하기보다는 사용자용 설명을 앞에 두면 좋습니다.

3. 가수·스타일은 엔진 등록 후에 선택하게 한다

VOICEVOX API에서는 최종적으로 음성 생성에 사용하는 목소리가 speaker 쿼리 파라미터에 전달하는 style ID로 결정됩니다. 송에서는 트랙마다 가수 스타일을 갖기 때문에 UI에서는 등록된 엔진에서 singers를 펼쳐 가수명·스타일명·style ID를 선택지로 만듭니다. 저장하는 값은 다음 두 가지면 충분합니다.
{
  "singer": {
    "engineId": "engine-uuid",
    "styleId": 6000
  }
}
이 구조로 해 두면 .vvproj 호환 송 데이터와 맞추기 쉽고 멀티엔진에도 대응하기 쉽습니다. 프로젝트를 열었을 때 해당 engineId가 미등록이라면 “미등록 엔진”으로 표시하고 URL 등록을 유도합니다. Xcode용 보충: Swift에서는 Singer(engineId: String, styleId: Int)를 트랙에 갖게 하고, 표시용으로는 RegisteredEngine.singers에서 SingerStyleOption과 같은 ViewModel용 배열로 평탄화하면 좋습니다. 가수 할당을 변경했다면 렌더링된 음성은 무효화합니다.

4. .vvproj 호환을 고려한다면 tick 기준의 송 모델로 한다

VOICEVOX 에디터의 송 데이터는 음표나 템포를 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. 송 합성은 4단계 API로 구현한다

송 렌더링은 토크의 /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: null, lyric: ""으로 표현합니다. Xcode용 보충: Swift에서는 VoicevoxEngineSongAPI와 같은 protocol을 만들고 구현체를 URLSession 클라이언트로 하면 테스트하기 쉽습니다. JSON을 반환하는 엔드포인트에서는 Content-Type / Acceptapplication/json으로 하고, /frame_synthesis의 응답은 WAV 바이너리 Data로 다룹니다. HTTP 2xx 이외에는 상태 코드와 응답 본문을 포함한 오류로 처리합니다.

6. 프레이즈 단위로 분할하여 캐시한다

송 전체를 매번 한 번에 합성하면, 약간 편집한 것만으로도 모든 트랙이 재생성되어 UI 응답성이 나빠집니다. 공식 에디터와 마찬가지로, 연속된 노트 그룹을 프레이즈로 나누고 쉼표로 분할하는 설계가 다루기 쉽습니다. 프레이즈마다 다음 단계를 캐시합니다.
캐시입력에 포함할 것
AudioQuery엔진 ID, frame rate, 템포, Score, 음역 조정
F0AudioQuery, 피치 편집, 프레이즈 위치
VolumeAudioQuery, F0, 볼륨 생성에 영향을 주는 값
Voice/WAV합성용 Query, 최종 style ID
초기 구현에서는 편집이 들어오면 대상 트랙의 렌더링된 음성을 통째로 무효화해도 좋습니다. 나중에 프레이즈 단위 차분 무효화를 추가할 수 있도록 입력 해시로 캐시 키를 만듭니다. Xcode용 보충: actor에 렌더링 캐시를 갖게 하면 Swift Concurrency 상에서 상태 관리하기 쉽습니다. 새로운 렌더링이 시작되면 세대 번호를 진행시키고, 오래된 API 응답이 돌아와도 채택하지 않습니다. Task.checkCancellation()과 세대 체크를 결합하면 취소와 재렌더링 양쪽을 다룰 수 있습니다.

7. 프레임 레이트와 쉼표를 엔진 메타 정보에서 결정한다

ScoreNote.frame_length는 초 수와 frame_rate에서 계산합니다. frame_rate를 고정값으로 임베드하지 말고 등록 시 /engine_manifest에서 획득해 저장합니다. 송 합성에서는 프레이즈 앞뒤에 쉼표를 넣습니다. 앞쪽 쉼표가 너무 짧으면 음소 생성이 불안정해지는 경우가 있으므로, 구현에서는 “실제 쉼표”, “4분음표”, “최저 초 수로부터 역산한 tick”의 균형을 잡습니다. 뒤쪽에도 짧은 무음을 추가하고, 필요하면 마지막 무음 구간을 페이드아웃합니다. 또, 프레임 길이는 반올림 결과가 0 이하가 되지 않도록 보정합니다. 짧은 노트나 템포 변경 직후에는 반올림 오차로 0 프레임이 발생하기 쉽습니다. Xcode용 보충: tickToSecond / secondToTick을 순수 함수로 만들어 테스트해 두면 피아노 롤, 재생 헤드, 렌더링, 동영상 출력에서 같은 변환을 사용할 수 있습니다. frameLength = Int(round(seconds * frameRate))의 결과는 인접 노트에 차분을 넘기는 등으로 최저 1프레임을 보장합니다.

8. 파라미터 편집은 “생성 전”과 “합성 전” 어디에 적용되는지를 나눈다

송에서는 음역·성량·피치·볼륨·음소 타이밍 편집이 각각 다른 단계에 영향을 줍니다.
편집반영 타이밍
keyRangeAdjustmentScore 생성 시의 키, F0 생성 후의 피치 시프트
volumeRangeAdjustment/frame_synthesis 전의 볼륨 배열
피치 편집/sing_frame_f0 후의 F0 배열
볼륨 편집/sing_frame_volume 후의 volume 배열
음소 타이밍 편집FrameAudioQuery.phonemes
어떤 편집이 어떤 캐시를 무효화하는지 미리 결정해 두면, 나중에 UI 편집 기능을 추가해도 렌더링 결과가 무너지기 어렵습니다. Xcode용 보충: 파라미터 편집의 적용은 ViewModel에 흩어놓지 말고 SongParameterEditApplicator와 같은 순수 로직에 몰아 넣으면 테스트하기 쉽습니다. 배열의 범위 밖 접근이나 음의 볼륨은 명시적으로 보정·오류화합니다.

9. 재생·믹스·내보내기는 같은 트랙 판정을 사용한다

렌더링 후에는 프레이즈별 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에서는 renderingStateidle, rendering, rendered, stale, failed와 같은 enum으로 하면 버튼의 disabled 제어와 상태 표시를 일관되게 하기 쉽습니다. 오류 메시지는 상태 바뿐만 아니라 얼럿에도 표시할 수 있게 해 둡니다.

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 엔진 본체를 항상 실행해서 테스트하는 것은 무겁기 때문에, 일반적인 자동 테스트에서는 HTTP를 목킹하고 앱 측의 요청 생성과 상태 전이를 확인합니다. 우선적으로 테스트하고 싶은 항목은 다음과 같습니다.
대상확인할 것
URL 정규화끝의 슬래시나 경로 있는 URL이 예상대로 처리됨
엔진 등록/engine_manifest, /version, /singers, /singer_info를 호출해 저장용 모델을 만들 수 있음
.vvproj I/OtrackOrder와 tracks의 정합성, 오래된/미래 버전의 취급
tick/second 변환템포 변경을 넘나드는 변환이 왕복 가능
Score 생성쉼표, frame_length, 기본 가사, 음역 조정
렌더링 계획미등록 엔진, 가수 미설정, engineId 불일치를 API 호출 전에 검출
캐시같은 입력으로 재사용되고, 편집 후에 무효화됨
Xcode용 보충: URLProtocol을 교체한 URLSessionConfiguration.ephemeral을 사용하면 실제 HTTP 서버 없이 URLSession 클라이언트를 테스트할 수 있습니다. 렌더러는 protocol을 통해 API를 주입하고, actor의 캐시 상태는 스냅샷을 반환하는 메서드를 준비하면 검증하기 쉽습니다.

구현 순서 추천

최소 구성부터 시작한다면 다음 순서가 좋습니다.
1

엔진 URL 등록과 메타 정보 저장

URL을 등록하고, /engine_manifest이나 /version에서 획득한 메타 정보와 함께 저장합니다.
2

가수 스타일 목록과 할당

가수 스타일 목록을 표시하고, 트랙에 engineId + styleId를 할당합니다.
3

tick 기준 모델

tick 기준의 Song/Track/Note 모델을 먼저 굳힙니다.
4

4단계 API 클라이언트

Score 생성과 4단계 송 API 클라이언트를 구현합니다.
5

프레이즈 분할 렌더링

프레이즈 단위 렌더링과 캐시를 도입합니다.
6

재생 기능

렌더링된 WAV를 타임라인 상에서 재생할 수 있게 합니다.
7

멀티트랙 판정

멀티트랙의 solo/mute/gain/pan 판정을 통일합니다.
8

내보내기

WAV 내보내기와 stem 내보내기를 추가합니다.
9

편집 기능

피치·볼륨·음소 타이밍 편집을 추가합니다.
10

확장 기능

.vvproj 호환 읽기/쓰기나 동영상 출력 등의 확장 기능으로 진행합니다.
처음부터 공식 에디터의 모든 기능을 쫓기보다는 URL 등록·가수 할당·4단계 API·재렌더링 상태를 먼저 굳혀 두면, 공식 엔진에도 Laravel판 엔진에도 맞추기 쉽습니다.

관련 링크

마지막 수정일 2026년 7월 13일