Voltar para blog

Implementando “Text Blips” (Pseudo-Speech) na Web com Web Audio API

08/03/2026 · 3 min · Frontend Engineering

Compartilhar

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:

  1. OscillatorNode: gera a onda base (sine, square, triangle, sawtooth).
  2. GainNode: controla envelope de volume (ataque/decay) para evitar click/clipping.
  3. 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.

PerfilPitch (Hz)WaveformCaracterística
Herói200–300triangleequilibrado, amigável
NPC feminina400–550sinesuave, limpo
Robô/antagonista80–150squareáspero, harmônicos agressivos

Além disso, costumo variar também:

---

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

---

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.

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.