Design de som
A maioria dos motion graphics feitos por IA são silenciosos. O som é o que faz uma peça parecer produzida.
O motor sfx/ sintetiza cada som em Python puro stdlib: sem samples, sem licenças, sem instalações, mesma saída em cada execução. Ele lê o mesmo JSON de linha do tempo que a imagem, então o som não pode sair de sincronia.
Como tudo se encaixa
| Arquivo | Função |
|---|---|
sfx/synth.py | geradores: cada um retorna amostras mono a 48 kHz |
sfx/mix.py | Mix: posiciona sons no tempo, masteriza para um alvo de loudness, escreve WAV |
sfx/loudness.py | Loudness integrada BS.1770 (corresponde ao ebur128 do ffmpeg) |
sfx/cues/<video>.py | uma folha de dicas: o design de som de um vídeo |
python3 -m sfx --list # geradores com o que cada um faz
python3 -m sfx sfx/cues/<video>.py # renderiza → WAV masterizado
Escrevendo uma folha de dicas para um novo vídeo
Crie sfx/cues/<video>.py:
from sfx.synth import click, pop, whoosh, chime, riser, impact
TIMELINE = "timeline.json" # o mesmo arquivo que a composição lê
OUT = "public/sfx.wav"
TARGET_LUFS = -14.0 # opcional, este é o padrão
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):tsempre vem detl, nunca um número digitado. Deslocamentos a partir de uma batida (s1["exit"] + 0.05) são aceitáveis.mix.typing(start, end, chars): uma tecla humanizada por caractere.mix.put_stereo(t, left, right, gain): para fontes estéreo comopad().- A masterização é automática: o motor faz soft-clipping e ajusta o ganho para atingir
TARGET_LUFS, aumentando o drive apenas o necessário para manter o pico verdadeiro ≤ −2 dBFS (espaço para overshoot do AAC, para que o MP4 fique abaixo de −1 dBTP). Uma mixagem com maioria de hits curtos e silêncio entre eles não consegue atingir −14 sem esmagamento; o motor então sai mais baixo e avisa. Corrija com som sustentado (um pad, caudas mais longas), não forçando. - Adicione um script
"sfx"empackage.jsonapontando para a nova folha de dicas, ou execute diretamente.
sfx/cues/effort.py é um exemplo real completo: 128 sons mais um pad de fundo.
Evento visual → som
| Na tela | Gerador | Notas |
|---|---|---|
| Digitação | mix.typing() | jitter ±4–6 ms, ganho aleatório |
| Revelação de palavra / linha | whoosh() + blip() no pouso | curto, arejado |
| Badge, chip, callout aparecem | pop() | aumenta o tom em uma sequência |
| Controle se encaixa em um passo | click() + blip() na tonalidade | |
| Esboço / desenho | scratch() | |
| Acúmulo | riser() | termina exatamente no hit; um riser grande por vídeo |
| Sucesso / chegada | chime() | acorde na tonalidade |
| Transição de herói | whoosh() na entrada, impact() no hit | um impact por vídeo |
| Debaixo de tudo | pad() com um envelope() | abaixe sob a transição de herói |
Acentue, não narre: nem todo movimento precisa de som. Movimento contínuo (deriva, grão, rotação) permanece silencioso.
Níveis e espaço
- Hits de herói: ganho 0,25–0,35. Sons de suporte: 0,08–0,2. Pad: ~0,03, abaixado ~65% sob a transição de herói.
- Pan segue a posição na tela (−1 esquerda … +1 direita). Sequências varrem o campo estéreo.
- Escolha uma tonalidade e mantenha todos os sons tonais nela; o tom sobe com o progresso e resolve no payoff. (No exemplo effort, o pad e as notas do slider são em Ré maior, mas os pops e chime da cena 3 ainda estão em Dó maior. Não copie isso.)
Adicionando um gerador
Escreva uma função em sfx/synth.py que retorna uma lista de floats em SR. Coloque para que serve na primeira linha do docstring; --list mostra. Use o random de nível de módulo para ruído, para que as renderizações permaneçam reproduzíveis. Use bandnoise() para qualquer coisa arejada.
Verificar
npm run sfx # ou python3 -m sfx sfx/cues/<video>.py
npm run wave # out/wave.png + loudness do WAV
npm run build
node scripts/wave.mjs out/effort.mp4 # loudness do que você realmente vai postar
Em out/wave.png, cada linha coral é uma batida da linha do tempo. Cada transitório deve estar sobre uma linha ou um pouco depois. Um hit entre linhas significa um tempo codificado ou um WAV desatualizado. Passe --timeline para um vídeo com seu próprio arquivo de linha do tempo.
Opcional: áudio gerado
Para narração ou sons que você não pode sintetizar, o MCP do ElevenLabs (veja .mcp.json.example) pode gerar arquivos em public/. Coloque-os com <Audio src={staticFile(...)} /> dentro de um <Sequence from={beat * FPS}>.