Saltar al contenido principal

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

VOICEVOX Song
Notas de implementación al construir una app cliente que llama a la API del motor 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

VOICEVOX Song
El editor oficial de VOICEVOX arranca desde la propia app el ejecutable del motor incluido o especificado, y lo gestiona con base en su configuración y en 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:
MotorEjemplo
Motor VOICEVOX oficialhttp://127.0.0.1:50021
Motor versión Laravelhttp://127.0.0.1:50513
El motor oficial tiene una función de ajuste automático de puerto: si el 50021 está en uso, arranca en el primer puerto libre a partir del 50022.
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.
APIUso
GET /engine_manifestObtener uuid, nombre del motor, frame_rate, sampling rate por defecto, etc.
GET /versionObtener la versión para visualización y comprobación de compatibilidad
GET /singersObtener la lista de estilos de canto
GET /singer_info?speaker_uuid=...&resource_format=...Obtener iconos e información adicional
Lo importante: en los proyectos y pistas guarda 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:
  1. El usuario arranca el motor primero.
  2. En la app se introduce la URL.
  3. La app obtiene la metainformación y la registra.
  4. Al renderizar, se resuelve la URL a partir del engineId registrado.
Con este enfoque puedes tratar en la misma abstracción el motor oficial, el motor versión Laravel, un motor en Docker o un motor en otro host. Si no se puede conectar, indica claramente la siguiente acción que debe tomar el usuario, como «arranca el motor y vuelve a conectar». Nota para Xcode: en SwiftUI conviene tener una pantalla independiente de «Conexión al motor» con un 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ámetro speaker. 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:
{
  "singer": {
    "engineId": "engine-uuid",
    "styleId": 6000
  }
}
Esta estructura encaja bien con datos compatibles con .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:
ElementoCampos principales
Songtpqn, tempos, timeSignatures, tracks, trackOrder
Trackname, singer, notes, gain, pan, solo, mute
Noteid, position, duration, noteNumber, lyric
Tempoposition, bpm
TimeSignaturemeasureNumber, beats, beatType
Solo al renderizar conviertes ticks a segundos usando el mapa de tempo y calculas 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
AudioQueryengineId, frame rate, tempo, Score, ajuste de rango
F0AudioQuery, edición de pitch, posición de la frase
VolumeAudioQuery, F0, valores que afectan a la generación de volumen
Voice/WAVQuery de síntesis, style ID final
En la implementación inicial puedes invalidar por completo el audio ya renderizado de la pista modificada. Para poder añadir después invalidación diferencial por frase, construye las claves de caché con un hash de la entrada. Nota para Xcode: guardar la caché de renderizado en un 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ónMomento en que se aplica
keyRangeAdjustmentKey al generar el Score, y shift de pitch tras generar F0
volumeRangeAdjustmentArray de volumen antes de /frame_synthesis
Edición de pitchArray F0 tras /sing_frame_f0
Edición de volumenArray volume tras /sing_frame_volume
Edición de timing de fonemasFrameAudioQuery.phonemes
Definir pronto qué edición invalida qué caché evita que el resultado del renderizado se rompa cuando después añadas funciones de edición en la UI. Nota para Xcode: no dispersen la aplicación de las ediciones de parámetros por el ViewModel; concéntrala en una lógica pura tipo 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 con AVAudioEngine, 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.
EstadoAviso a mostrar
Motor no registradoRegistrar la URL en la pantalla de conexión al motor
Motor no arrancadoArrancar el motor y volver a conectar
engineId del proyecto no registradoAñadir la URL del motor correspondiente
Pista sin cantante asignadoElegir un estilo de cantante registrado
Audio obsoleto tras editarSe necesita rerenderizar
RenderizandoDebe poderse cancelar
Si agrupas todo esto como «no se puede reproducir», la causa queda oculta. Distingue estados y muestra en la UI la siguiente acción. Nota para Xcode: en el ViewModel de SwiftUI, si conviertes 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 y engineId tienen roles distintos.
ValorCiclo de vidaUso
URLCambia según el entorno de cada usuarioDestino de las llamadas API
engineIdIdentificador propio de la implementación/modelo del motorReferencia dentro del proyecto
styleIdIdentificador del estilo dentro del motorUsarlo como parámetro speaker
No incrustes la URL en el archivo del proyecto: guarda 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:
ObjetivoQué comprobar
Normalización de URLQue las URLs con barra final o con path se tratan como se espera
Registro del motorQue se llaman /engine_manifest, /version, /singers, /singer_info y se puede construir el modelo a guardar
I/O de .vvprojConsistencia entre trackOrder y tracks, manejo de versiones antiguas o futuras
Conversión tick/secondQue la conversión que atraviesa cambios de tempo es reversible
Generación de ScoreSilencios, frame_length, letras por defecto, ajuste de rango
Plan de renderizadoDetectar 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
Nota para Xcode: si usas un 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:
1

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.
2

Lista de estilos de cantante y asignación

Muestra la lista de estilos y asigna engineId + styleId a las pistas.
3

Modelo basado en ticks

Fija primero el modelo Song/Track/Note basado en ticks.
4

Cliente de API de 4 pasos

Implementa la generación del Score y el cliente de la API de canciones en 4 pasos.
5

Renderizado por frases

Introduce el renderizado por frase y su caché.
6

Función de reproducción

Permite reproducir los WAV renderizados en el timeline.
7

Criterio multipista

Unifica el criterio solo/mute/gain/pan en escenarios multipista.
8

Exportación

Añade exportación de WAV y de stems.
9

Funciones de edición

Añade edición de pitch, volumen y timing de fonemas.
10

Funciones avanzadas

Avanza hacia funciones más avanzadas, como la compatibilidad de lectura/escritura de .vvproj o la exportación a vídeo.
En lugar de perseguir desde el principio todas las funciones del editor oficial, es mejor consolidar primero el registro de URL, la asignación de cantante, la API de 4 pasos y los estados de rerenderizado; así es más fácil dar soporte tanto al motor oficial como al motor versión Laravel.

Enlaces relacionados

Última modificación el 13 de julio de 2026