Todo reel do Instagram do Navistron tem trilha, e nenhuma delas veio de banco de música. São geradas por um script de 75 linhas em Node.js que escreve um arquivo WAV direto, amostra por amostra, sem nenhuma dependência — nem biblioteca de áudio, nem ffmpeg para sintetizar, nem sample baixado. Este tutorial mostra como fazer isso: sintetizar onda quadrada e triângulo, montar arpejo, bumbo e caixa, criar um riser que explode em um impacto e, no fim, escrever o cabeçalho WAV na mão.
A motivação é prática: música de banco tem licença, e licença gera dúvida quando o vídeo vai para o Instagram. Gerar a trilha por código resolve o problema na origem — o áudio é seu, é determinístico (o mesmo comando gera o mesmo arquivo) e muda com um parâmetro. O pipeline completo de renderização dos reels, que consome esse WAV, está no artigo sobre como transformar HTML em imagem e vídeo com Playwright.
A ideia: um array de amostras
Áudio digital é uma lista de números. A 44.100 amostras por segundo, um trecho de 14 segundos são 617.400 números entre -1 e 1. Sintetizar é preencher esse array; salvar é converter cada número para 16 bits e colar um cabeçalho na frente. Em Node.js, o esqueleto é este:
const SR = 44100; const N = Math.floor(DUR * SR); const buf = new Float32Array(N);
A partir daí, um laço percorre i de 0 a N, calcula o instante t = i / SR e soma no buffer tudo o que deve soar naquele instante: melodia, baixo, percussão, efeitos. Não há mixer, não há trilhas separadas — é uma soma de valores, e o resultado é a mixagem.
Osciladores: quadrada e triângulo em uma linha cada
O som 8-bit vem das formas de onda simples que os chips antigos conseguiam gerar. Duas bastam:
const square = (ph) => (ph % 1) < 0.5 ? 1 : -1;
const tri = (ph) => 1 - 4 * Math.abs(Math.round(ph - 0.25) - (ph - 0.25));
Ambas recebem uma fase, que é simplesmente o tempo multiplicado pela frequência: square(t * f) produz uma onda quadrada de f hertz. Para converter nota musical em frequência, a fórmula MIDI resolve: 440 * Math.pow(2, (m - 69) / 12), onde 69 é o lá de 440 Hz. Com isso, a nota 72 vira dó, 76 vira mi, e transpor a música inteira é somar um número a todas as notas.
Harmonia: quatro acordes e um arpejo
A trilha usa a progressão mais batida do mundo em tom menor — Am, G, F, E — porque ela soa bem em qualquer contexto e não exige teoria nenhuma. Cada acorde é um array de notas MIDI, e o baixo é a fundamental uma oitava e meia abaixo:
const chords = [[69,72,76,81], [67,71,74,79], [65,69,72,77], [64,68,71,76]];
const bassNotes = [45, 43, 41, 40];
A melodia não toca acordes: toca arpejo, uma nota por semicolcheia, subindo e descendo na sequência 0-1-2-3-2-1-0-2. Isso dá a sensação de movimento típica do chiptune sem nenhuma polifonia real. A duração da semicolcheia sai do andamento: 60 / bpm / 4. A 132 BPM, cada nota dura 113 milissegundos.
O que faz a nota soar como nota, e não como um zumbido contínuo, é o envelope: Math.exp(-inStep * 18), onde inStep é o tempo decorrido desde o início daquela semicolcheia. A amplitude cai exponencialmente, o ataque é instantâneo, e o resultado é um "plim" seco. Trocar o 18 por 6 alonga a nota; por 40, encurta.
Percussão sem sample: bumbo e caixa
Bumbo é uma senoide cuja frequência despenca. Começa em 150 Hz e cai para 40 em uma fração de segundo, com a amplitude caindo junto:
const fk = 40 + 110 * Math.exp(-inBeat * 30);
Caixa é ainda mais simples: ruído branco com envelope curto, ou seja, números aleatórios multiplicados por uma exponencial decrescente. O gerador de aleatórios é um LCG com semente fixa, e não Math.random(), justamente para o arquivo sair idêntico a cada execução — a mesma regra de determinismo que vale para a animação dos reels e para a geração procedural dos meteoros dentro do jogo.
| Elemento | Como é sintetizado | Envelope |
|---|---|---|
| Melodia (lead) | Onda quadrada + oitava acima a 25% | exp(-t·18), por semicolcheia |
| Baixo | Triângulo com saturação (tanh) | exp(-t·6), por colcheia |
| Bumbo | Senoide com frequência de 150 a 40 Hz | exp(-t·14), tempos 1 e 3 |
| Caixa | Ruído branco (LCG determinístico) | exp(-t·28), tempos 2 e 4 |
| Riser | Ruído com amplitude crescendo ao quadrado | rampa até o impacto |
| Impacto | Senoide grave com queda de frequência | exp(-t·6), 0,6 s |
O truque que faz o vídeo funcionar: riser e impacto
Um reel de 14 segundos tem um instante em que o número grande trava na tela. O áudio precisa marcar esse instante, ou a peça inteira parece desalinhada. A solução tem duas partes. Antes do impacto, um riser: ruído branco cuja amplitude cresce ao quadrado, o que dá aquela sensação de tensão subindo. No impacto, um hit grave, uma senoide de 135 Hz caindo para 55 em meio segundo.
Junto vem um ducking manual: nos 350 milissegundos seguintes ao impacto, todo o resto da mixagem é multiplicado por um ganho que sobe de 0,35 a 1. Isso abre espaço para o hit sem precisar de compressor. Os três números — instante do impacto, início do riser e duração do fade final — são parâmetros do script, então cada reel ajusta a trilha ao seu próprio roteiro passando um JSON: {"bpm":126,"transpose":-5,"hit":5.2,"riserStart":3.0}.
Escrever o WAV na mão
O formato WAV não comprimido é um cabeçalho de 44 bytes seguido das amostras. Antes de escrever, duas etapas: normalizar e converter. Normalizar é achar o pico do buffer e multiplicar tudo por 0.89 / pico, o que deixa a trilha logo abaixo do máximo sem distorcer. Converter é passar de float (-1 a 1) para inteiro de 16 bits (-32768 a 32767) com writeInt16LE.
O cabeçalho tem campos fixos e campos calculados. Os fixos: as marcas RIFF, WAVE, fmt e data, o tamanho do bloco fmt (16), o formato PCM (1) e o número de canais (1, mono). Os calculados: taxa de amostragem, bytes por segundo (taxa vezes 2), alinhamento de bloco (2), bits por amostra (16) e os dois tamanhos em bytes — o do arquivo menos 8 e o dos dados. Escrever os 44 bytes com Buffer.alloc e concatenar com os dados fecha o arquivo.
É a parte que parece intimidante e é a mais mecânica: nenhum campo depende da música, só do formato. Depois disso, ffmpeg junta o WAV aos quadros do vídeo, e a mesma ideia de escrever o arquivo binário na mão vale para qualquer formato simples.
O que isso custa
Gerar 14 segundos de áudio assim leva menos de um segundo de CPU, porque são 617 mil iterações de aritmética simples. O arquivo sai com cerca de 1,2 MB, que o ffmpeg depois converte para AAC de 96 kbps dentro do MP4. E o script inteiro cabe em 75 linhas sem nenhum npm install, o que significa que ele roda igual em qualquer máquina com Node e não quebra quando uma dependência muda de versão — a mesma filosofia de poucas dependências que o Navistron usa no Next.js e que orienta a otimização do jogo: zero web fonts, zero texturas, zero pacote que não se pague.
Para quem quiser adaptar: troque os acordes por outra progressão, mude o BPM e ajuste o envelope da melodia. Esses três parâmetros já dão trilhas completamente diferentes. Se a ideia for usar em um jogo em vez de um vídeo, o mesmo raciocínio funciona no navegador com a Web Audio API, com a diferença de que lá você preenche um AudioBuffer em vez de um arquivo — o Navistron em si não usa áudio, mas a técnica é a mesma. Os números que aparecem em cada reel, aliás, saem todos da telemetria pública e do ranking — trilha, animação e dado são gerados pelo mesmo pipeline, todo dia.
FAQ
Dá para gerar áudio em Node.js sem biblioteca?
Sim. Áudio PCM é só uma lista de números: você preenche um array com as amostras, converte para 16 bits e escreve um cabeçalho WAV de 44 bytes na frente. Não é preciso nenhuma dependência para isso.
Como se faz uma onda quadrada em JavaScript?
Comparando a fase com 0,5: se a parte fracionária do tempo vezes a frequência for menor que 0,5, o valor é 1; senão, -1. Uma linha resolve, e é essa onda que dá o som característico de 8 bits.
Por que a trilha precisa ser determinística?
Porque o vídeo é renderizado várias vezes durante o ajuste. Usando um gerador de aleatórios com semente fixa em vez de Math.random(), o mesmo comando produz exatamente o mesmo arquivo, e dá para comparar duas versões sem ruído de diferença aleatória.
Qual o tamanho de um WAV desses?
Cerca de 1,2 MB para 14 segundos em mono, 44,1 kHz e 16 bits — o cálculo é taxa vezes 2 bytes vezes a duração. Depois de convertido para AAC dentro do MP4, sobram alguns quilobytes por segundo.
Essa música pode ser usada no Instagram sem problema de direitos?
Ela é gerada por código e não usa nenhum sample de terceiros, então não há licença de terceiros envolvida. É a razão principal de sintetizar em vez de baixar de um banco de músicas.
