Guía de desarrollo de apps con integración de la Engine API de VOICEVOX

Con base en la implementación de una app de canciones para macOS, en lugar de trasplantar tal cual la arquitectura interna del editor oficial de VOICEVOX, se organizan los aspectos necesarios para usar la API del motor de forma estable como aplicación externa. Para que también sea aplicable fuera de Xcode/Swift, primero se describen las directrices generales y al final de cada apartado hay notas específicas para Xcode.
Esta página documenta sin omisiones, con base en la guía original.
1. Registra primero el motor por URL

engine_manifest.json. En cambio, para una app cliente externa es más manejable registrar primero «la URL del servidor de API HTTP ya en ejecución».
URL representativas:
| Motor | Ejemplo |
|---|---|
| Motor VOICEVOX oficial | http://127.0.0.1:50021 |
| Motor versión Laravel | http://127.0.0.1:50513 |
La versión Laravel, si no indicas
--port=50513 y arrancas con el 8000 por defecto, también arrancará en el 8001 y siguientes, pero si especificas el puerto, esa función de ajuste se deshabilita.
Al registrar, no guardes solo la cadena de URL: como comprobación de conexión llama al menos a las siguientes APIs y guarda la metainformación obtenida junto con la URL.
| API | Uso |
|---|---|
GET /engine_manifest | Obtener uuid, nombre del motor, frame_rate, sampling rate por defecto, etc. |
GET /version | Obtener la versión para visualización y comprobación de compatibilidad |
GET /singers | Obtener la lista de estilos de canto |
GET /singer_info?speaker_uuid=...&resource_format=... | Obtener iconos e información adicional |
engineId y styleId, no la URL. La URL cambia según el entorno del usuario, pero engineId corresponde al uuid de /engine_manifest y encaja bien con singer.engineId de .vvproj.
Nota para Xcode: en la app de canciones, RegisteredEngine mantiene baseURL, engineId, name, frameRate, defaultSamplingRate, version y singers, y se guarda en UserDefaults. Si normalizas la URL (barra final, query, fragment) antes de usarla, puedes tratar http://127.0.0.1:50021/ y http://127.0.0.1:50021 como iguales. Si en una app de macOS usas conexiones HTTP locales, revisa también los ajustes del sandbox y App Transport Security.
2. No te encargues del «arranque del motor» como la app oficial
Si desde el principio intentas implementar la misma gestión de arranque del motor que el editor oficial en una app externa, tendrás que ocuparte del arranque de procesos por sistema operativo, binarios incluidos, conflictos de puertos, actualizaciones y gestión de logs. Sobre todo si se usan implementaciones distintas al motor oficial, el enfoque de especificar la ruta del ejecutable y arrancarlo encaja mal. Lo seguro es fijar de entrada este flujo:- El usuario arranca el motor primero.
- En la app se introduce la URL.
- La app obtiene la metainformación y la registra.
- Al renderizar, se resuelve la URL a partir del
engineIdregistrado.
TextField para la URL, botones de Registrar / Reconectar, presets para las versiones oficial y Laravel, y una lista de motores ya registrados. Durante la conexión asíncrona muestra un ProgressView; para los errores de conexión, no expongas directamente el texto de LocalizedError, mejor precede con una explicación orientada al usuario.
3. Deja elegir cantante/estilo tras registrar el motor
En la API de VOICEVOX, la voz que se usa para la síntesis final se decide por el style ID que se pasa en el parámetrospeaker. Como en las canciones cada pista tiene su propio estilo de cantante, en la UI expande singers desde los motores registrados y usa el nombre del cantante, el nombre del estilo y el style ID como opciones.
Con guardar estos dos valores es suficiente:
.vvproj y también con configuraciones multi-engine. Si al abrir un proyecto el engineId correspondiente no está registrado, muéstralo como «motor no registrado» e invita a registrar la URL.
Nota para Xcode: en Swift, haz que la pista tenga Singer(engineId: String, styleId: Int) y para visualizar aplana la información desde RegisteredEngine.singers a un array tipo SingerStyleOption orientado a ViewModel. Cuando se cambie la asignación de cantante, invalida el audio ya renderizado.
4. Si buscas compatibilidad con .vvproj, usa un modelo de canción basado en ticks
Los datos de canción del editor de VOICEVOX guardan notas y tempo en base a ticks. La API del motor requiere finalmente longitudes de frame, pero mantener el modelo de edición interno en ticks encaja mejor con el piano roll, cambios de tempo, compases, Undo/Redo y la lectura/escritura de .vvproj.
Estructura mínima:
| Elemento | Campos principales |
|---|---|
| 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 |
frame_length según el frame_rate del motor.
Nota para Xcode: al leer y escribir JSON estilo .vvproj con Codable, separa tracks como Dictionary indexado por ID y trackOrder como array del orden de visualización. Antes de guardar, valida duplicados en trackOrder, IDs inexistentes y pistas sin ordenar; así evitas que la UI se rompa tras la carga.
5. Implementa la síntesis de canciones como una API de 4 pasos
La renderización de canciones tiene más pasos que la de talk (/audio_query → /synthesis). El pipeline básico es de 4 pasos:
En /sing_frame_audio_query, /sing_frame_f0 y /sing_frame_volume se usa el style ID 6000 del singing teacher, y solo en la última /frame_synthesis se usa el styleId del cantante realmente seleccionado. Este valor puede tratarse del mismo modo tanto en el motor oficial como en el motor versión Laravel.
En las peticiones de cada API es importante no romper la correspondencia entre Score y FrameAudioQuery. Calcula Score.notes[].frame_length a partir del frame_rate del motor y representa el silencio como key: null, lyric: "".
Nota para Xcode: en Swift crea un protocol como VoicevoxEngineSongAPI y usa un cliente URLSession como implementación; es más fácil de testear. Para los endpoints que devuelven JSON, pon Content-Type / Accept en application/json; la respuesta de /frame_synthesis trátala como Data binaria WAV. Cualquier respuesta HTTP fuera del rango 2xx conviértela en un error que incluya el código y el cuerpo.
6. Divide por frases y cachea
Si sintetizas toda la canción de una vez cada vez, con un pequeño cambio se regeneran todas las pistas y la UI se ralentiza. Como en el editor oficial, es más manejable un diseño que agrupe las notas contiguas como frases y divida por silencios. Cachea las siguientes fases por frase:| Caché | Qué incluir en la entrada |
|---|---|
| AudioQuery | engineId, frame rate, tempo, Score, ajuste de rango |
| F0 | AudioQuery, edición de pitch, posición de la frase |
| Volume | AudioQuery, F0, valores que afectan a la generación de volumen |
| Voice/WAV | Query de síntesis, style ID final |
actor facilita la gestión de estado bajo Swift Concurrency. Cuando arranque un nuevo renderizado, incrementa un número de generación y descarta las respuestas de API antiguas. Combinando Task.checkCancellation() con la comprobación de generación, gestionas tanto la cancelación como el rerenderizado.
7. Decide el frame rate y los silencios a partir de la metainformación del motor
ScoreNote.frame_length se calcula a partir de los segundos y el frame_rate. No incrustes el frame_rate como valor fijo: obténlo de /engine_manifest al registrar y guárdalo.
En la síntesis de canciones, añade silencios al principio y al final de la frase. Si el silencio inicial es demasiado corto, la generación de fonemas puede volverse inestable, así que equilibra en la implementación entre «el silencio real», «una negra» y «los ticks calculados a partir de un mínimo de segundos». También añade un silencio corto al final y, si es necesario, aplica un fade-out al último tramo de silencio.
Además, corrige la longitud del frame para que el redondeo no la deje en 0 o menos. En notas cortas o justo tras un cambio de tempo se dan fácilmente 0 frames por error de redondeo.
Nota para Xcode: si conviertes tickToSecond / secondToTick en funciones puras y las testeas, podrás usar la misma conversión en el piano roll, el cabezal de reproducción, el renderizado y la exportación de vídeo. Al resultado de frameLength = Int(round(seconds * frameRate)), garantízale al menos 1 frame trasladando la diferencia a las notas adyacentes.
8. Distingue en qué punto se aplica cada edición de parámetros: «antes de generar» o «antes de sintetizar»
En canciones, cada edición (rango, volumen general, pitch, volumen, timing de fonemas) afecta a una fase distinta.| Edición | Momento en que se aplica |
|---|---|
keyRangeAdjustment | Key al generar el Score, y shift de pitch tras generar F0 |
volumeRangeAdjustment | Array de volumen antes de /frame_synthesis |
| Edición de pitch | Array F0 tras /sing_frame_f0 |
| Edición de volumen | Array volume tras /sing_frame_volume |
| Edición de timing de fonemas | FrameAudioQuery.phonemes |
SongParameterEditApplicator, es más fácil de testear. Corrige o convierte en error los accesos fuera de rango y los volúmenes negativos.
9. Reproducción, mezcla y exportación deben usar el mismo criterio de pista
Tras renderizar, coloca los WAV por frase en el timeline para reproducir. En escenarios multipista, si el criterio de solo/mute/gain/pan varía entre reproducción normal, exportación de WAV total y exportación de stems, el comportamiento se vuelve difícil de anticipar para el usuario. Como regla común: si hay al menos una pista en solo, sale solo esa; si no hay ninguna en solo, salen las pistas que no estén en mute. La exportación de stems saca la pista objetivo de forma aislada, separadamente del estado normal de solo/mute. Nota para Xcode: en macOS puedes construir el grafo de reproducción conAVAudioEngine, AVAudioPlayerNode y AVAudioMixerNode. Para exportar, prepara una ruta de renderizado offline distinta de la reproducción normal y unifica la codificación WAV. Si durante la reproducción se produce una edición que invalida el audio renderizado, detén la reproducción y muestra el estado.
10. Distingue en la UI entre «no registrado», «no asignado» y «sin renderizar»
En una app que se integra con la API del motor VOICEVOX, los motivos de fallo se dividen en varios casos.| Estado | Aviso a mostrar |
|---|---|
| Motor no registrado | Registrar la URL en la pantalla de conexión al motor |
| Motor no arrancado | Arrancar el motor y volver a conectar |
engineId del proyecto no registrado | Añadir la URL del motor correspondiente |
| Pista sin cantante asignado | Elegir un estilo de cantante registrado |
| Audio obsoleto tras editar | Se necesita rerenderizar |
| Renderizando | Debe poderse cancelar |
renderingState en un enum con idle, rendering, rendered, stale, failed, es más fácil mantener coherente el control de disabled de los botones y la visualización de estado. Los mensajes de error deberían poder mostrarse no solo en la barra de estado, sino también en alertas.
11. En multi-engine, separa los ciclos de vida de «URL» y de «engineId»
En multi-engine, lo que más confusión genera es que URL yengineId tienen roles distintos.
| Valor | Ciclo de vida | Uso |
|---|---|---|
| URL | Cambia según el entorno de cada usuario | Destino de las llamadas API |
engineId | Identificador propio de la implementación/modelo del motor | Referencia dentro del proyecto |
styleId | Identificador del estilo dentro del motor | Usarlo como parámetro speaker |
engineId y styleId. En la configuración de la app mantén la correspondencia engineId -> URL. Así, aunque recibas el proyecto de otra persona, con registrar la URL del mismo motor en tu entorno podrás renderizar.
Nota para Xcode: antes de renderizar, resuelve engineId a RegisteredEngine con algo como Dictionary(uniqueKeysWithValues: registeredEngines.map { ($0.engineID, $0) }). Si no lo encuentras, no lances la petición HTTP y falla previamente con «el motor del cantante no está registrado».
12. En los tests, separa cliente HTTP, conversión de modelos y plan de renderizado
Mantener el motor VOICEVOX arrancado siempre para los tests es pesado, así que en los tests automatizados normales mockea HTTP y comprueba la generación de peticiones y las transiciones de estado del lado de la app. Elementos que conviene testar con prioridad:| Objetivo | Qué comprobar |
|---|---|
| Normalización de URL | Que las URLs con barra final o con path se tratan como se espera |
| Registro del motor | Que se llaman /engine_manifest, /version, /singers, /singer_info y se puede construir el modelo a guardar |
I/O de .vvproj | Consistencia entre trackOrder y tracks, manejo de versiones antiguas o futuras |
| Conversión tick/second | Que la conversión que atraviesa cambios de tempo es reversible |
| Generación de Score | Silencios, frame_length, letras por defecto, ajuste de rango |
| Plan de renderizado | Detectar antes de llamar a la API motor no registrado, cantante sin asignar y engineId no coincidente |
| Caché | Se reutiliza con la misma entrada e invalida tras editar |
URLSessionConfiguration.ephemeral con URLProtocol reemplazado, puedes testear un cliente URLSession sin un servidor HTTP real. Inyecta la API en el renderer mediante protocolos y, para el estado de caché en un actor, prepara un método que devuelva snapshots para facilitar la verificación.
Orden de implementación recomendado
Si empiezas por la configuración mínima, este orden es adecuado:Registro de URL del motor y guardado de metainformación
Registra la URL y guarda junto a ella la metainformación obtenida de
/engine_manifest y /version.Lista de estilos de cantante y asignación
Muestra la lista de estilos y asigna
engineId + styleId a las pistas.Cliente de API de 4 pasos
Implementa la generación del
Score y el cliente de la API de canciones en 4 pasos.