Implementando “Text Blips” (Pseudo-Speech) na Web com Web Audio API
O charme do som procedural
Se você já jogou Undertale, Celeste ou até mergulhou em atmosferas narrativas como MiSide, sabe que identidade de personagem passa muito pelo ritmo da fala. Na web, subir dezenas de .wav por caractere é custo de banda, latência e manutenção.
A abordagem que usei aqui foi síntese de áudio procedural em tempo real com Web Audio API. Resultado: controle fino de pitch, timbre e envelope sem depender de assets externos.
---
1) Arquitetura da solução
Para um sistema de diálogo de nível produção, eu separo em três blocos:
- OscillatorNode: gera a onda base (sine, square, triangle, sawtooth).
- GainNode: controla envelope de volume (ataque/decay) para evitar click/clipping.
- Timing Engine: sincroniza som com efeito typewriter e pausas semânticas.
Essa divisão facilita ajuste de identidade sonora sem reescrever lógica de render de texto.
---
2) Desafios reais de trincheira
2.1 Autoplay policy (bloqueio de contexto de áudio)
Browsers modernos bloqueiam AudioContext sem gesto do usuário.
Correção operacional: criar/resumir contexto dentro de clique explícito (botão de iniciar diálogo, por exemplo).
2.2 Efeito “metralhadora”
Tocar exatamente o mesmo blip para toda letra deixa a experiência mecânica.
Correção: randomização leve de frequência (±15~20Hz) por caractere.
2.3 Estalos no início/fim (click artifacts)
Sem envelope, você ouve transiente seco no start/stop.
Correção: ataque rápido + decay exponencial curto via GainNode.
---
3) Implementação otimizada (com cleanup de memória)
<div id="dialog-box" style="font-family: 'Courier New', monospace; min-height: 50px;"></div>
<button id="btn-start">Iniciar Diálogo</button>
<script>
let audioCtx;
// Inicialização segura do contexto
const initAudio = () => {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
};
/**
* Gera o blip sonoro
* @param {number} freq - Frequência base em Hz
* @param {string} type - Tipo da onda (sine, square, triangle, sawtooth)
*/
function playBlip(freq = 440, type = 'sine') {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
// Randomização sutil para evitar som robótico
osc.frequency.setValueAtTime(freq + (Math.random() * 40 - 20), audioCtx.currentTime);
// Envelope para eliminar clique: ataque + decay curto
gain.gain.setValueAtTime(0, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.06);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.06);
// Cleanup para evitar acúmulo de nós em diálogos longos
osc.onended = () => {
gain.disconnect();
osc.disconnect();
};
}
async function typeWriter(text, freq, wave) {
const el = document.getElementById('dialog-box');
el.textContent = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
el.textContent += char;
// Toca em tudo exceto espaço
if (char !== ' ') {
playBlip(freq, wave);
}
// Pausas semânticas para respiração natural
let delay = 50;
if (char === ',') delay = 200;
if ('.!?'.includes(char)) delay = 500;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
document.getElementById('btn-start').addEventListener('click', () => {
initAudio();
typeWriter('Olá, Percio! Isso é áudio procedural rodando em tempo real.', 220, 'triangle');
});
</script>
---
4) Estratégia de identidade sonora (branding de personagem)
No design de áudio, não é “som por som”. É identidade.
| Perfil | Pitch (Hz) | Waveform | Característica |
|---|---|---|---|
| Herói | 200–300 | triangle | equilibrado, amigável |
| NPC feminina | 400–550 | sine | suave, limpo |
| Robô/antagonista | 80–150 | square | áspero, harmônicos agressivos |
Além disso, costumo variar também:
- duração do blip por emoção (curto = urgência, longo = calma);
- ganho máximo por personagem;
- intervalo base do typewriter para mudar “cadência de fala”.
---
5) Hardening técnico (segurança, performance e UX)
5.1 CSP e fontes externas
Se você migrar para samples externos no futuro, ajuste CSP (connect-src, media-src) para domínios confiáveis. Sem isso, produção pode bloquear carregamento silenciosamente.
5.2 Performance mobile
OscillatorNode é leve, mas diálogo contínuo sem cleanup gera ruído de GC. Por isso o onended desconectando nós não é opcional em sessão longa.
5.3 UX corporativo
Sempre incluir controle de mute global. O que é imersão para game pode ser atrito em escritório.
Exemplo rápido:
let muted = false;
function playBlipSafe(freq, type) {
if (muted) return;
playBlip(freq, type);
}
5.4 Acessibilidade
Para acessibilidade, não dependa só de áudio para transmitir estado narrativo. Mantenha texto e sinais visuais equivalentes.
---
6) Checklist de produção
- [ ]
AudioContextinicializado por gesto do usuário - [ ] Envelope aplicado para eliminar clicks
- [ ] Randomização de pitch habilitada
- [ ] Pausas por pontuação implementadas
- [ ] Cleanup de nós (
disconnect) noonended - [ ] Mute global disponível
- [ ] CSP revisada se houver mídia remota
---
Conclusão
Pseudo-speech na web é um exemplo clássico de como UX de alto nível pode ser entregue com stack nativa, sem payload desnecessário e com controle criativo total.
Com Web Audio API, você transforma diálogo em experiência — e, tecnicamente, mantém o sistema leve, previsível e pronto para escalar.
Este post está licenciado sob CC BY-NC.
Comentários
Participe da discussão abaixo.
Comentários ainda não configurados. Adicione as opções do Cusdis em /assets/json/config/blog-comments-config.json.