Diseño de sonido
La mayoría de los gráficos en movimiento hechos con IA son silenciosos. El sonido es lo que hace que una pieza se sienta producida.
El motor sfx/ sintetiza cada sonido en Python puro de la biblioteca estándar: sin muestras, sin licencias, sin instalaciones, la misma salida en cada ejecución. Lee el mismo JSON de línea de tiempo que la imagen, por lo que el sonido no puede desincronizarse.
Cómo encaja
| Archivo | Rol |
|---|---|
sfx/synth.py | generadores: cada uno devuelve muestras mono a 48 kHz |
sfx/mix.py | Mix: coloca sonidos en el tiempo, masteriza a un objetivo de sonoridad, escribe WAV |
sfx/loudness.py | Sonoridad integrada BS.1770 (coincide con ebur128 de ffmpeg) |
sfx/cues/<video>.py | una hoja de referencia: el diseño de sonido de un vídeo |
python3 -m sfx --list # generadores con su propósito
python3 -m sfx sfx/cues/<video>.py # renderizar → WAV masterizado
Escribir una hoja de referencia para un vídeo nuevo
Crea sfx/cues/<video>.py:
from sfx.synth import click, pop, whoosh, chime, riser, impact
TIMELINE = "timeline.json" # el mismo archivo que lee la composición
OUT = "public/sfx.wav"
TARGET_LUFS = -14.0 # opcional, este es el valor por defecto
def score(mix, tl):
s1 = tl["s1"]
mix.put(s1["title"], whoosh(0.4, 500, 2400, peak=0.45), 0.12)
for i, t in enumerate(s1["items"]):
mix.put(t, pop(900 + i * 150, 400), 0.3, pan=-0.4 + i * 0.3)
mix.put(t, samples, gain, pan):tsiempre proviene detl, nunca un número escrito. Los desplazamientos desde un beat (s1["exit"] + 0.05) están bien.mix.typing(start, end, chars): una pulsación de tecla humanizada por carácter.mix.put_stereo(t, left, right, gain): para fuentes estéreo comopad().- La masterización es automática: el motor recorta suavemente y ajusta la ganancia para alcanzar
TARGET_LUFS, aumentando la excursión solo lo necesario para mantener el pico verdadero ≤ −2 dBFS (espacio para el exceso de AAC, por lo que el MP4 se mantiene por debajo de −1 dBTP). Una mezcla compuesta principalmente de golpes cortos con silencio entre ellos no puede alcanzar −14 sin comprimir; el motor entonces sale más bajo y lo indica. Soluciónalo con sonido sostenido (un pad, colas más largas), no forzándolo. - Añade un script
"sfx"enpackage.jsonque apunte a la nueva hoja de referencia, o ejecútala directamente.
sfx/cues/effort.py es un ejemplo real completo: 128 sonidos más un pad de fondo.
Evento visual → sonido
| En pantalla | Generador | Notas |
|---|---|---|
| Escritura | mix.typing() | fluctuación ±4–6 ms, ganancia aleatoria |
| Revelación de palabra / línea | whoosh() + blip() al aterrizar | corto, aireado |
| Insignia, chip, llamada aparecen | pop() | eleva el tono a lo largo de una secuencia |
| Control se ajusta a un paso | click() + blip() en tono | |
| Boceto / dibujo | scratch() | |
| Acumulación | riser() | termina exactamente en el golpe; un gran riser por vídeo |
| Éxito / llegada | chime() | acorde en tono |
| Transición principal | whoosh() al entrar, impact() en el golpe | un impacto por vídeo |
| Debajo de todo | pad() con un envelope() | redúcelo bajo la transición principal |
Acentúa, no narres: no todo movimiento necesita un sonido. El movimiento continuo (deriva, grano, rotación) permanece en silencio.
Niveles y espacio
- Golpes principales: ganancia 0.25–0.35. Sonidos de apoyo: 0.08–0.2. Pad: ~0.03, reducido ~65% bajo la transición principal.
- El paneo sigue la posición en pantalla (−1 izquierda … +1 derecha). Las secuencias barren el campo estéreo.
- Elige una tonalidad y mantén cada sonido tonal en ella; el tono sube con el progreso y se resuelve en el remate. (En el ejemplo effort, el pad y las notas del slider están en Re mayor, pero los pops de la escena 3 y el chime siguen en Do mayor. No copies eso).
Añadir un generador
Escribe una función en sfx/synth.py que devuelva una lista de flotantes a SR. Pon su propósito en la primera línea del docstring; --list lo mostrará. Usa el random a nivel de módulo para ruido, de modo que los renderizados sigan siendo reproducibles. Construye sobre bandnoise() para cualquier cosa aireada.
Verificar
npm run sfx # o python3 -m sfx sfx/cues/<video>.py
npm run wave # out/wave.png + sonoridad del WAV
npm run build
node scripts/wave.mjs out/effort.mp4 # sonoridad de lo que realmente publicarás
En out/wave.png, cada línea coral es un beat de la línea de tiempo. Cada transitorio debe situarse sobre una línea o justo después. Un golpe entre líneas significa un tiempo codificado o un WAV desactualizado. Pasa --timeline para un vídeo con su propio archivo de línea de tiempo.
Opcional: audio generado
Para locuciones o sonidos que no puedas sintetizar, el MCP de ElevenLabs (consulta .mcp.json.example) puede generar archivos en public/. Colócalos con <Audio src={staticFile(...)} /> dentro de un <Sequence from={beat * FPS}>.