Música, efectos y narración sincronizados con la línea de tiempo. Declaras una pista con scene.media.audio(...) y la activas con scene.play([pista]). La vista previa la reproduce siguiendo el reloj de la escena (pausa, seek y velocidad incluidos) y la exportación a MP4 o WebM la mezcla con el video.

Pistas de audio

Archivos de sonido que empiezan en el cursor donde los activas.

FÁBRICAMediaLibrary.audioaudio(path: str, *, duration: Optional[float] = None, end: Optional[float] = None, volume: float = 1.0, fade_in: float = 0.0, fade_out: float = 0.0) -> Audio
Declara una pista validada. La declaración no cambia la línea de tiempo: scene.play([pista]) fija su inicio en el cursor absoluto de esa llamada. Rutas o tiempos inválidos lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pathstrobligatorioArchivo de audio; una ruta relativa usa la carpeta de assets, igual que imágenes y SVG.
durationfloat | NoneNoneRecorta la pista y la hace participar en la duración del lote. Sin ella, la pista suena de fondo sin alargar la línea de tiempo.
endfloat | NoneNoneCorta la pista en ese segundo del archivo sin alargar el lote: música de fondo que suena bajo las escenas siguientes y termina donde quieres. No se combina con duration.
volumefloat1.0Ganancia lineal.
fade_in / fade_outfloat0.0Fundidos de entrada y salida en segundos; fade_out necesita duration o end y termina justo en el corte.
Ejemplo
from gaanim import Scene

scene = Scene()
scene.assets.assets_dir("assets")

music = scene.media.audio("music.ogg", volume=0.35)
pop = scene.media.audio("pop.wav", duration=0.4, volume=0.8, fade_in=0.02)
scene.play([music])
scene.wait(1.5)
scene.play([pop])

scene.render()

Una narración grabada con duración fija alarga el lote y termina con un fundido:

narration = scene.media.audio(
    "narration.m4a",
    duration=7.5,
    volume=0.9,
    fade_in=0.15,
    fade_out=0.25,
)
scene.play([narration])

Música de fondo que dura 30 s y se funde al final, mientras la escena sigue programándose desde el mismo cursor:

musica = scene.media.audio("music.ogg", end=30.0, fade_out=1.5, volume=0.5)
scene.play([musica])  # no mueve el cursor: lo que sigue suena sobre la música
CLASEAudioscene.media.audio(...) -> Audio
Declaración de audio ligada a la escena que la creó. Solo se usa pasándola a scene.play.

Efectos de sonido anclados

Sonidos cortos que se colocan en la línea de tiempo sin scene.play. Usan el mismo mezclador que scene.media.audio: suenan en la vista previa (con pausa, seek y velocidad) y MP4/WebM los mezclan con el video. Un seek al segundo t oye lo mismo que la reproducción continua, porque cada efecto tiene un inicio absoluto fijado al componer la escena. Ninguno alarga la línea de tiempo ni mueve el cursor. Hay tres formas de anclarlos:

  • scene.media.sfx(...): en un segundo absoluto.
  • anim.sound(...): al inicio de una animación. Si la animación se mueve (un delay, su lugar en una sequence o un stagger), el sonido se mueve con ella.
  • Transition.*(..., sound=...): al inicio de la transición, que es el inicio del segmento al que entra.
MÉTODOMediaLibrary.sfxsfx(path: str, at: Optional[float] = None, *, volume: float = 1.0) -> None
Coloca un efecto que suena una vez, entero. Un archivo inexistente o un at o volume negativos o no finitos lanzan ValueError. Para que el sonido siga a una animación que puede moverse, usa Anim.sound.
Parámetros
NombreTipoPredeterminadoDescripción
pathstrobligatorioArchivo de audio; una ruta relativa usa la carpeta de assets.
atfloat | NoneNoneSegundo absoluto de inicio; sin él, el cursor actual (scene.cursor).
volumefloat1.0Ganancia lineal.
Devuelve: None — El efecto queda programado de inmediato.
Ejemplo
scene.play(title.animate.fade_in())
scene.media.sfx("whoosh.wav", at=scene.cursor)
scene.media.sfx("pop.wav", at=0.25, volume=0.6)
scene.wait(1)
MÉTODOAnim.soundsound(path: str, *, volume: float = 1.0, offset: float = 0.0) -> Anim
El sonido empieza cuando empieza la animación resuelta, incluidos su delay y su posición en composiciones, y suena una vez, entero, aunque la animación use repeat. Una ruta vacía, un volume negativo o no finito o un offset no finito lanzan ValueError al llamarlo; un archivo inexistente o un sonido que empezaría antes de 0 s lanzan ValueError desde scene.play, que entonces no programa nada.
Parámetros
NombreTipoPredeterminadoDescripción
pathstrobligatorioArchivo de audio; una ruta relativa usa la carpeta de assets.
volumefloat1.0Ganancia lineal.
offsetfloat0.0Segundos desde el inicio de la animación hasta el sonido; negativo lo adelanta.
Devuelve: Anim — Una copia con el efecto anclado; una segunda llamada lo reemplaza.
Ejemplo
title = scene.text("Efectos anclados", role="title")
dot = scene.geometry.circle(0.5).move_to(0, -2)
scene.play(title.animate.write().sound("typing.wav", volume=0.6))
# El pop suena 0.3 s después, con el círculo.
scene.play(dot.animate.grow_from_center().delay(0.3).sound("pop.wav"))

Una transición con sonido:

scene.segment("detalle", Transition.slide(0.5, "left", sound="whoosh.wav"))
scene.wait(1)

La vista previa reproduce varias pistas a la vez y aplica volume, fade_in y fade_out. MP4 usa AAC y WebM usa Opus. Las secuencias de imágenes, GIF y WebP animado rechazan las pistas porque esos formatos no transportan audio.

Audio de los videos

El audio embebido de un video sigue las mismas reglas que una pista.

clip = scene.media.video(...) solo declara el video y scene.play([clip]) activa juntos sus fotogramas y su audio; ambos se pausan, recorren y repiten con la línea de tiempo. audio=False silencia el video y volume fija su ganancia. Con video.segment(start=..., end=...), cada fragmento genera una pista finita: las pausas entre fragmentos son silenciosas aunque el último fotograma siga visible, y speed conserva el tono. Consulta Medios.

Audio que anima la escena

Los datos de una pista animan cualquier cosa. level, band y pulse devuelven un Computed del tiempo, entre 0 y 1, que se usa donde se acepta un número reactivo:

  • scale_to, opacity, move_to y rotate_to;
  • la cámara (bind_2d) y los uniforms de los shaders;
  • computed, scene.viz.readout y fill_level;
  • Updater.rotate y Updater.wiggle;
  • los colores, con Falloff.source y drive("fill", ...).

El archivo se analiza una sola vez: los WAV directamente y los demás formatos con FFmpeg. Cada señal consulta ese análisis donde suena la pista, según su inicio, así que un salto en la línea de tiempo o una exportación leen lo mismo que la reproducción. Antes de que la pista suene y después de que termine, las señales valen 0.

musica = scene.media.audio("assets/ritmo.ogg")
bajo = musica.band(20, 150, smoothing=0.5)
golpe = musica.pulse(decay=0.12)

logo = scene.geometry.star(5, 1, 0.45).fill(GOLD)
logo.scale_to(computed(lambda b: 0.8 + 0.5 * b, inputs=[bajo]))
logo.add_updater(Updater.wiggle(position=computed(lambda p: 0.15 * p, inputs=[golpe])))
puntos = scene.geometry.group([scene.geometry.circle(0.2).move_to(x, -2) for x in range(-3, 4)])
puntos.drive("fill", Falloff.source(bajo).gradient(BLUE, GOLD))

scene.play(musica)
scene.wait(musica.duration)
scene.render()

scene.play(musica) sin duration no alarga el lote: scene.wait(musica.duration) mantiene la escena mientras suena. El ejemplo completo, examples/audio/signals.py, está en el repositorio de Gaanim; no viene con el paquete instalado.

MÉTODOAudio.levellevel(*, smoothing: float = 0.0, range_db: Optional[float] = None) -> Computed
Envolvente de volumen (RMS), normalizada al percentil 99 del archivo para que unos pocos picos no aplasten el resto. En una pista masterizada las partes fuertes quedan todas cerca de 1; range_db las separa. Un smoothing fuera de rango o un range_db fuera de (0, 120] lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
smoothingfloat0.0En [0, 1): cada cuadro conserva esa parte del anterior, así que cerca de 1 cambia despacio.
range_dbfloat | NoneNoneMide en decibelios, como el oído: 0 a range_db dB por debajo de la parte fuerte y 1 en ella. Con 24, un drop se distingue de un verso más suave.
Devuelve: Computed — El volumen donde suena la pista: 0 en silencio, 1 en su parte fuerte.
MÉTODOAudio.bandband(low: float, high: float, *, smoothing: float = 0.0) -> Computed
El análisis llega hasta 11 kHz. Una banda con low >= high, negativa o por encima de ese rango lanza ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
low / highfloatobligatorioBanda de frecuencias en Hz: band(20, 150) sigue el bajo y band(2000, 8000), los platillos y las sibilantes.
smoothingfloat0.0Como en level.
Devuelve: Computed — La amplitud de la banda, de 0 a 1, cada banda respecto a su propia parte fuerte.
MÉTODOAudio.pulsepulse(decay: float = 0.15, *, low: Optional[float] = None, high: Optional[float] = None) -> Computed
La forma más directa de reaccionar al ritmo: scale_to(computed(lambda p: 1 + 0.2 * p, inputs=[musica.pulse()])). Un golpe es un onset: donde empieza un sonido (un bombo, una nota, una sílaba). Sin banda salta con cualquiera, y en música electrónica los platillos lo disparan sin parar; pulse(low=40, high=120) sigue solo el bombo. Un decay no positivo, solo uno de low/high o una banda inválida lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
decayfloat0.15Segundos en que el pulso cae a un tercio.
low / highfloat | NoneNoneSolo cuentan los golpes de esa banda, en Hz; los dos o ninguno.
Devuelve: Computed — 1 en cada golpe y bajando hasta 0.
MÉTODOAudio.beatsbeats(*, low: Optional[float] = None, high: Optional[float] = None) -> list[float]
Son onsets, no una rejilla de tempo; para la rejilla usa tempo. Suma musica.start cuando la pista ya sonó para obtener segundos de la línea de tiempo, por ejemplo para colocar marcas o animaciones en cada golpe.
Parámetros
NombreTipoPredeterminadoDescripción
low / highfloat | NoneNoneSolo los golpes de esa banda, en Hz, como en pulse.
Devuelve: list[float] — Segundos desde el inicio de la pista en que empieza un sonido, en orden.
Ejemplo
scene.play(musica)
golpes = [musica.start + t for t in musica.beats()]
bombos = [musica.start + t for t in musica.beats(low=40, high=120)]
MÉTODOAudio.tempotempo(*, min_bpm: float = 60.0, max_bpm: float = 200.0) -> AudioTempo
Estima un tempo estable: el periodo en que más se repiten los golpes, dando peso a los graves (bombo y bajo, 40–150 Hz), y la fase que pone los pulsos sobre esos golpes. confidence mide cuánto se repiten los golpes en la rejilla de ese tempo, a uno, dos o cuatro pulsos: un bombo a medio tiempo, cada dos pulsos, también da un pulso claro. Si el rango no contiene el tempo de la música, se devuelve el mejor pico dentro del rango con una confianza baja, nunca uno de sus extremos. El tempo y el pulso que se siente pueden estar en proporción 4:3 o 2:1, así que conviene comparar con el tempo publicado del tema. Así scene.tempo(t.bpm, offset=musica.start + t.offset) alinea wait_until(bar=...) con la música. Sigue la speed y el offset de la pista. Sirve para música con pulso estable; con confidence por debajo de 0.3 no hay un pulso claro. No detecta secciones (drops, estribillos): el volumen confunde la entrada del bombo con un drop, así que esas marcas se colocan a mano con analysis. Un rango fuera de 0 < min_bpm < max_bpm <= 400 lanza ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
min_bpm / max_bpmfloat60 / 200Rango en que se busca el tempo.
Devuelve: AudioTempo — bpm, offset (segundos desde el inicio de la pista hasta su primer pulso, menos de un pulso) y confidence, de 0 a 1.
Ejemplo
scene.play(musica)
pulso = musica.tempo()
scene.tempo(pulso.bpm, offset=musica.start + pulso.offset)
scene.wait_until(bar=2)
MÉTODOAudio.analysisanalysis() -> AudioData
Para colocar cortes a mano con datos reales. frame_rate cuadros por segundo (unos 43) y times, el segundo del archivo de cada cuadro; level, el volumen de 0 a 1, y level_db, en decibelios por debajo de la parte fuerte (0 en ella, −120 en silencio); spectrum, una fila por cuadro con la amplitud de 96 bandas logarítmicas (frequencies, en Hz), de 0 a 1 contra la parte fuerte de todo el archivo; band(low, high), la amplitud de una banda por cuadro, y onsets(low=, high=). Los tiempos son del archivo, antes del offset y la speed de la pista. Son listas de Python, listas para numpy si lo usas.
Devuelve: AudioData — El análisis del archivo como números.
Ejemplo
datos = musica.analysis()
bombo = datos.band(40, 120)
fuertes = [t for t, v in zip(datos.times, bombo) if v > 0.9]
PROPIEDADAudio.start / duration
start es el segundo de la línea de tiempo en que la pista empezó a sonar por última vez, o None antes de reproducirla. duration son los segundos que suena: su duration, o el archivo entero.
FÁBRICAFalloff.sourcesource(value: Computed) -> Falloff
Lleva las señales de audio a drive, que es como cambian los colores: drive("fill", Falloff.source(bajo).gradient(BLUE, GOLD)). Un Computed que lee un Parameter lanza ValueError. Ver Animaciones.
Parámetros
NombreTipoPredeterminadoDescripción
valueComputedobligatorioUn Computed que depende solo del tiempo: una señal de audio, scene.noise o scene.time.
Devuelve: Falloff — Su valor, el mismo para todos los miembros.

Visualizar el audio

Para dibujar el sonido, una pista da una señal por banda de frecuencia (spectrum) o por instante de su pasado reciente (waveform), y scene.viz.equalizer las dibuja. Cada parte se elige o se reemplaza: la forma de los elementos, su disposición y sus colores. Como son señales normales, también sirven para dibujar a mano o en un shader.

MÉTODOAudio.spectrumspectrum(bands: int = 32, *, low: float = 40.0, high: Optional[float] = None, smoothing: float = 0.0, normalize: Literal["band", "global"] = "band") -> list[Computed]
Reparte el rango en escala logarítmica, como el oído percibe el tono. Con "band", igual que band, los agudos también se mueven aunque suenen menos; con "global" las barras conservan la forma real del espectro (graves altos, agudos bajos). Un rango inválido u otro normalize lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
bandsint32Bandas, de 1 a 256.
low / highfloat40 / NoneRango en Hz; high es por defecto la frecuencia más alta del análisis, unos 11 kHz.
smoothingfloat0.0Suavizado de cada banda, en [0, 1).
normalizestr"band""band" mide cada banda contra su propio máximo; "global", todas contra la más fuerte.
Devuelve: list[Computed] — Una señal de 0 a 1 por banda, de la más grave a la más aguda.
MÉTODOAudio.waveformwaveform(points: int = 64, *, span: float = 2.0, low: Optional[float] = None, high: Optional[float] = None, smoothing: float = 0.0) -> list[Computed]
El punto i lee la pista span * (1 - i / (points - 1)) segundos atrás, así que los valores avanzan hacia el primer punto mientras suena, como el trazo de una grabadora. Valores fuera de rango lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pointsint64Instantes, de 2 a 512.
spanfloat2.0Segundos que abarca, hacia atrás desde el presente.
low / highfloat | NoneNoneSigue esa banda en lugar del volumen; los dos o ninguno.
smoothingfloat0.0Suavizado, en [0, 1).
Devuelve: list[Computed] — El volumen en points instantes de los últimos span segundos, el más antiguo primero.
MÉTODOAudio.samplessamples(points: int = 256, *, span: float = 0.05) -> list[Computed]
La onda misma, con signo, contra el pico del archivo: un osciloscopio. waveform sigue el volumen, de 0 a 1; samples dibuja la forma de las notas con un span de unas centésimas de segundo. Valores fuera de rango lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pointsint256Instantes, de 2 a 1024.
spanfloat0.05Segundos que abarca, hacia atrás desde el presente.
Devuelve: list[Computed] — La onda del sonido en esos instantes, de −1 a 1, la más antigua primero.
Ejemplo
onda = musica.samples(200, span=0.03)
osciloscopio = scene.geometry.polyline(
    [(-6 + 12 * i / 199, computed(lambda v: 1.5 * v, inputs=[s])) for i, s in enumerate(onda)]
).stroke(GOLD, 0.03)
scene.play(musica)
scene.wait(3)
scene.render()
FÁBRICAVisualization.equalizerequalizer(values: Sequence[float | Parameter | Variable | Computed], *, shape: Literal["bar", "capsule", "dot", "line", "area"] | Callable[[Scene, int, int], Drawable] = "bar", layout: Literal["row", "mirror", "radial"] | Callable[[int, int], tuple[float, float, float]] = "row", center: tuple[float, float] = (0.0, 0.0), width: float = 8.0, height: float = 2.0, radius: float = 1.5, gap: float = 0.25, thickness: Optional[float] = None, min_length: float = 0.04, start_angle: float = 1.5707963267948966, fill: Optional[Paint | Sequence[Paint] | FalloffColor | Callable[[int, int], Paint]] = None, value_colors: Optional[Sequence[ColorLike]] = None, stretch: bool = True) -> Drawable
Una lista vacía, una forma o disposición desconocida o tamaños inválidos lanzan ValueError; una función que no devuelve lo esperado, TypeError.
Parámetros
NombreTipoPredeterminadoDescripción
valuesSequence[ScalarSource]obligatorioSeñales de 0 a 1: spectrum(), waveform(), Parameter, Computed o números. Lo que sale de ese rango se recorta.
shapestr | Callable"bar""bar", "capsule" (extremos redondos), "dot" (en la punta), "line" (una curva por las puntas) o "area" (esa curva rellena hasta la base). O una función shape(scene, index, count) que devuelve cualquier objeto, que se centra en su lugar, se gira a lo largo y se estira a su longitud.
layoutstr | Callable"row""row" (crece hacia arriba desde la base de una caja de width × height), "mirror" (crece hacia los dos lados de una línea media) o "radial" (alrededor de un círculo de radius, desde start_angle en sentido horario, hacia fuera). O una función layout(index, count) que devuelve (x, y, ángulo): dónde empieza cada elemento y en qué dirección crece, en radianes.
fillPaint | list | FalloffColor | CallableNoneUna pintura, una lista que se repite, una rampa Falloff sobre el grupo (Falloff.index().gradient(...)) o fill(index, count).
value_colorsSequence[ColorLike] | NoneNoneColorea cada elemento según su propio valor a lo largo de estos colores. Requiere señales que solo dependen del tiempo, como las de audio.
center / width / height / radius—(0, 0) / 8 / 2 / 1.5Dónde y de qué tamaño. Cada elemento mide de min_length (0.04) a height.
gap / thicknessfloat0.25 / NoneFracción de cada hueco que queda vacía, o un grosor fijo.
stretchboolTrueCon una forma propia, False la escala entera en lugar de estirarla, para iconos que deben conservar su proporción.
Devuelve: Drawable — Un grupo con un miembro por valor, en orden: objetos normales que aceptan cualquier estilo, efecto o animación.
Ejemplo
import math
from gaanim import BLUE, CORAL, GOLD, Falloff, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
bars = scene.viz.equalizer(music.spectrum(32, smoothing=0.3), center=(-4, 1.5), width=6.5,
                           fill=Falloff.index().gradient(BLUE, GOLD))
ring = scene.viz.equalizer(music.spectrum(40), shape="dot", layout="radial", center=(4, 1.5),
                           radius=1.2, height=0.8, fill=[CORAL, GOLD])
trace = scene.viz.equalizer(music.waveform(96, span=2), shape="line", layout="mirror",
                            center=(0, -2.2), width=12, height=1.6)
scene.play(music)
scene.wait(music.duration)
scene.render()

Las mismas señales llegan a un shader como uniforms de PostProcess.shader. Un shader no indexa campos, así que el ejemplo genera una función que elige la banda:

from gaanim import PostProcess, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
bands = music.spectrum(16, smoothing=0.3)
pick = "\n".join(f"    if (i == {i}u) {{ return gaanim_uniforms.b{i}; }}" for i in range(16))
scene.canvas.post = PostProcess.shader(f"""
fn band(i: u32) -> f32 {{
{pick}
    return 0.0;
}}
fn gaanim_post(uv: vec2<f32>, resolution: vec2<f32>, time: f32) -> vec4<f32> {{
    let column = min(u32(uv.x * 16.0), 15u);
    if (1.0 - uv.y < 0.3 * band(column)) {{
        return vec4<f32>(0.2, 0.8, 0.95, 1.0);
    }}
    return gaanim_scene(uv);
}}
""", uniforms={f"b{i}": band for i, band in enumerate(bands)})
scene.play(music)
scene.wait(music.duration)
scene.render()

Para más detalle, audio= le da al shader el espectrograma entero de la pista sin gastar uniforms (usa uno, gaanim_audio_time, reservado). El shader llama a:

  • gaanim_audio_spectrum(x, ago): el espectro en x, de 0 (20 Hz) a 1 (unos 11 kHz) en escala logarítmica, cada frecuencia de 0 a 1 contra su propio máximo, tal como sonaba hace ago segundos;
  • gaanim_audio_level(ago): el volumen;
  • gaanim_audio_band(low, high, ago): la media entre dos frecuencias en Hz;
  • gaanim_audio_x(hz): dónde cae una frecuencia en la escala de x.

Leer varios ago dibuja la historia del espectro: un espectrograma que avanza mientras suena. Todas valen 0 mientras la pista no suena. Los paquetes .gaanim guardan los datos una sola vez. También sirve en shader_effect.

from gaanim import PostProcess, Scene
scene = Scene(frame=(16, 9), background="#0b1020")
music = scene.media.audio("assets/ritmo.ogg")
scene.canvas.post = PostProcess.shader("""
fn gaanim_post(uv: vec2<f32>, resolution: vec2<f32>, time: f32) -> vec4<f32> {
    // Izquierda: hace 3 s; derecha: ahora. Abajo los graves.
    let energy = gaanim_audio_spectrum(1.0 - uv.y, 3.0 * (1.0 - uv.x));
    let heat = vec3<f32>(energy, energy * energy, 0.3 + 0.7 * energy * energy * energy);
    return vec4<f32>(mix(gaanim_scene(uv).rgb, heat, energy), 1.0);
}
""", audio=music)
scene.play(music)
scene.wait(music.duration)
scene.render()

Narración

Gaanim graba tu voz sin salir del editor y la sincroniza con la escena, sin editor de video externo. Hay dos formas de trabajar:

  • Voz en off: la voz marca el ritmo. Cada bloque scene.voiceover(...) tiene su propia toma y el script espera a marcas con nombre en lugar de llevar duraciones fijas. Puedes regrabar un bloque sin tocar los demás.
  • Toma en vivo: presentas la escena hablando. Cada scene.stop() posterior a scene.live_take(...) se convierte en una pausa tan larga como la que hiciste al grabar.

Las tomas viven en narration/ dentro de la carpeta de assets: la de scene.assets.load_project(...) o, sin esa llamada, la del proyecto al que pertenece el script (el gaanim.toml más cercano hacia arriba). Fuera de un proyecto, o si un proyecto anterior ya tiene narration/ junto al script y no en sus assets, van junto al script: narration/<clave>.wav y, al lado, narration/<clave>.json con las marcas. Al grabar o editar esos archivos, el editor recarga la escena con los tiempos nuevos. render() y la exportación alargan el timeline para que ninguna toma quede cortada, y MP4/WebM mezclan las tomas como cualquier otra pista.

El guion

El texto que lees no tiene que vivir en el código. Escríbelo en narration/script.md con cualquier editor: cada encabezado ## clave contiene el texto de scene.voiceover("clave"), y un encabezado con el nombre de una diapositiva, sus notas del orador (ver El guion de la charla). Gaanim lo carga automáticamente; para usar otro archivo, llama antes a scene.narration_script("guion.md"). Al guardar el guion, el editor recarga la escena con las duraciones estimadas nuevas.

# Mi video sobre derivadas

## intro
Hoy vemos qué es una derivada: la pendiente de una curva en un punto.

### nota para mí: sonreír, sin prisa

## tangente
Movemos el punto por la recta y leemos su pendiente, que no cambia.
<!-- aquí respiro antes del cierre -->

Un # título cierra el bloque anterior; los encabezados ### son notas para ti que no se leen, los comentarios HTML se ignoran y una línea [stop] o [stop nombre] marca dónde avanzas, y la voz no la lee. Las líneas seguidas se unen y una línea en blanco separa párrafos. Un ## vacío o repetido (sin distinguir mayúsculas) produce ValueError. El texto de un bloque se busca en este orden: el argumento text, su sección del guion y las notas del segmento; con un guion cargado, una clave sin sección emite un UserWarning. El icono del guion en la cabecera del panel crea el archivo o le añade las secciones de los bloques y las diapositivas que faltan, y las marcas de sus stops, y lo abre en el editor de GAANIM_EDITOR en lo primero que añadió. En una toma en vivo, el subtítulo del editor muestra el tramo del guion que toca decir, con ▶ y el nombre del stop donde toca avanzar (ver El prompter).

Grabar desde el editor

Abre el panel con el botón de micrófono de la barra de reproducción (o More controls → Script & narration). El panel se acopla a la derecha (ver El panel Script & narration); al final lista la toma en vivo y cada bloque de voz en off con su estado: Not recorded (duración estimada del texto) o Recorded, su nivel y de dónde sale su texto. Mientras grabas, un subtítulo abajo de la vista previa muestra el texto que toca decir y, debajo, el reloj, el medidor de nivel y las teclas.

  • Record posiciona la escena al inicio del bloque y abre el micrófono predeterminado durante una cuenta atrás de tres segundos que sirve de prueba de nivel (no se guarda). Después graba mientras la escena avanza, con el texto del bloque en el subtítulo y la siguiente marca resaltada. Pulsa Espacio al decir cada marca, Enter para guardar y Esc para descartar.
  • Record live take recarga la escena con sus paradas originales y graba mientras presentas. La reproducción se detiene en cada parada; → o Espacio continúa, Enter termina y Esc descarta. Con Also dictate the script with Scribe, Scribe de Zoom transcribe a la vez lo que dices y lo escribe en el guion (ver Dictar el guion con Scribe).
  • Listen reproduce el bloque desde su inicio.
  • Level lleva una toma ya grabada al nivel configurado.

Durante la grabación el audio del preview se silencia para que la toma anterior no se cuele en el micrófono.

Nivel de la voz

Al grabar, el medidor del subtítulo muestra los picos del micrófono en dBFS. La zona verde, entre −18 y −6 dBFS, es la ideal. Entre −6 y −3 dBFS el medidor pasa a ámbar, y por encima de −3 dBFS el borde del subtítulo se pone rojo con el aviso «Too loud!»: una palabra más fuerte podría saturar, y la saturación no se corrige después. Si algún tramo satura, el aviso lo cuenta hasta el final de la toma; si durante cuatro segundos nada llega a −30 dBFS, avisa de que la voz está muy baja.

Cada toma nueva se nivela al guardarla a −16 LUFS integrados, el nivel recomendado para voz en video online (YouTube reproduce a −14 y la televisión usa −23). FFmpeg mide la toma y le aplica una ganancia constante, con un pico real máximo de −1.5 dBTP y un filtro paso alto a 80 Hz. La grabación original se conserva en narration/.originals/. En Settings → Recording (Ctrl+,) puedes cambiar el nivel final, y avisa si eliges más de −14 LUFS o menos de −20, o desactivar la nivelación. Las tomas sin nivelar aparecen marcadas en el panel.

Whisper (opcional)

En lugar de pulsar Espacio, whisper.cpp puede detectar cuándo dices cada palabra. Instala whisper-cli y un modelo ggml-*.bin, e indícalos en Settings → Transcription (vacío busca whisper-cli en el PATH). Transcribe guarda en el JSON de la toma el tiempo de cada palabra; también puede hacerse automáticamente al guardar cada toma. Requiere FFmpeg. Para que una marca se resuelva por transcripción, su nombre debe ser la palabra o frase que dices; se ignoran mayúsculas, tildes y puntuación.

Cómo se resuelven las marcas

Cada marca se busca, en este orden: la marca pulsada al grabar (o escrita a mano en el JSON), la transcripción de Whisper y, por último, su posición proporcional dentro del texto del bloque. Las marcas se buscan después de la anterior, así que una palabra repetida corresponde a su siguiente aparición; pedir otra vez el mismo nombre devuelve el mismo tiempo. Una marca que no se encuentra emite un UserWarning y no espera.

Sin toma grabada, la duración se estima del texto a unas 150 palabras por minuto, de modo que puedes componer y revisar la escena antes de grabar.

El JSON es editable: mover un número retima la escena sin tocar el código.

{
  "version": 1,
  "markers": { "derivada": 2.4, "pendiente": 5.1 },
  "words": [{ "text": "Hoy", "start": 0.12, "end": 0.3 }],
  "holds": [],
  "loudness": -16.0
}

markers guarda segundos desde el inicio de la toma, words la transcripción, holds los segundos de cada pausa de una toma en vivo y loudness el nivel en LUFS al que se niveló.

API de narración

Los bloques de voz en off, sus marcas y la toma en vivo.

MÉTODOScene.voiceovervoiceover(key: str, *, text: Optional[str] = None, volume: float = 1.0) -> Voiceover
Empieza un bloque en el cursor y devuelve un Voiceover, usable con with. Una toma grabada (.wav, .flac, .mp3, .m4a, .aac, .ogg u .opus) suena desde aquí y fija la duración del bloque; si no existe, la duración se estima del texto. Al salir del with se espera el resto de la toma. Claves inválidas o repetidas, volumen negativo o tomas ilegibles lanzan ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
keystrobligatorioNombre de la toma: letras, dígitos, -, _ o .. Es el nombre de archivo dentro de narration/.
textstr | NoneNoneTexto del bloque: teleprompter y estimación de tiempos. Sin él se usa la sección ## key del guion y, si no existe, las notes del segmento activo.
volumefloat1.0Ganancia lineal de la toma.
Ejemplo
from gaanim import Scene

scene = Scene()
scene.segment("derivada", notes="Hoy vemos la derivada y su pendiente")
titulo = scene.text("La derivada", role="title").move_to(0, 2.5)
curva = scene.geometry.circle(1.5)
with scene.voiceover("intro") as vo:
    scene.play([titulo.animate.write()])
    vo.wait_until("derivada")
    scene.play([curva.animate.create()], duration=vo.until("pendiente"))
print(f"grabada: {vo.recorded} · {vo.duration:.1f} s · termina en {vo.end:.1f} s")
scene.render()
grabada: False · 3.2 s · termina en 3.2 s
MÉTODOVoiceover.wait_untilwait_until(marker: str) -> None
Avanza el cursor hasta la marca. Si la marca ya quedó atrás o no existe, no espera. Usarlo en un bloque cerrado lanza ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
markerstrobligatorioNombre de la marca: palabra o frase del guion, o el nombre pulsado al grabar.
MÉTODOVoiceover.untiluntil(marker: str) -> float
Segundos desde el cursor hasta la marca, nunca negativos. Úsalo como duration para que una animación termine justo en la marca.
Ejemplo
vo = scene.voiceover("cierre", text="Y eso es todo por hoy")
scene.play([logo.animate.fade_in()], duration=vo.until("todo"))
vo.finish()
MÉTODOVoiceover.finishfinish() -> None
Espera el resto de la toma y cierra el bloque; repetirlo no hace nada. Es lo que hace el with al salir.
PROPIEDADVoiceover.key / text / start / duration / end / remaining / recordedkey: str · text: str | None · start: float · duration: float · end: float · remaining: float · recorded: bool
Nombre de la toma, texto del bloque, segundos absolutos de inicio y fin, duración (medida o estimada), segundos que faltan desde el cursor y si hay un archivo grabado (False mientras la duración sea estimada).
MÉTODOScene.narration_scriptnarration_script(path) -> None
Sin esta llamada se carga narration/script.md si existe. Las secciones con el nombre de una diapositiva son sus notas del orador y sustituyen a notes=. Llámala antes de los bloques que la usan; las diapositivas la leen la llames donde la llames. Un archivo ilegible o un ## vacío o repetido producen ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
pathstrobligatorioArchivo Markdown; una ruta relativa se busca en la carpeta de assets, o junto al script si la escena no tiene assets.
Devuelve: None — Los bloques que empiezan después y todas las diapositivas leen su texto de este guion.
Ejemplo
scene.narration_script("guion.md")
with scene.voiceover("intro") as vo:
    vo.wait_until("derivada")
MÉTODOScene.live_takelive_take(key="live", *, volume=1.0) -> None
Empieza la toma en el cursor. Con la toma grabada, suena desde aquí y cada stop() posterior espera lo que duró tu pausa, así que el timeline se reproduce de corrido en sincronía con tu voz. Sin toma, las paradas siguen siendo interactivas. Una segunda toma en vivo, una clave inválida o usada por una voz en off, o una toma ilegible producen ValueError.
Parámetros
NombreTipoPredeterminadoDescripción
keystr"live"Nombre de la toma dentro de narration/.
volumefloat1.0Ganancia lineal de la toma.
Devuelve: None — Declara la única toma en vivo de la escena.
Ejemplo
scene.live_take("clase")
scene.segment("intro", notes="Presentamos el problema")
scene.play([titulo.animate.write()])
scene.stop()
scene.segment("idea", notes="La idea clave es…")
scene.play([diagrama.animate.fade_in()])
scene.stop()
scene.render()