Skip to content

Web Audio API para jogos

Um bom áudio transforma um jogo. Um jogo sem som parece sem vida. A Web Audio API oferece efeitos sonoros de baixa latência, reprodução de música e até áudio espacial 3D, tudo no navegador.

Experimente (clique em um botão para ouvir sons sintetizados):

O problema da reprodução automática

Antes de qualquer coisa, você precisa entender as restrições de reprodução automática. Os navegadores bloqueiam o áudio até que o usuário interaja com a página. Isso é algo positivo (ninguém quer sites disparando sons ao carregar), mas significa que você precisa inicializar o áudio em resposta a um clique, toque ou pressionamento de tecla.

js
let audioCtx = null

function initAudio() {
  if (!audioCtx) {
    audioCtx = new AudioContext()
  }
  if (audioCtx.state === 'suspended') {
    audioCtx.resume()
  }
  return audioCtx
}

// Inicializa na primeira interação
document.addEventListener('click', () => initAudio(), { once: true })
document.addEventListener('keydown', () => initAudio(), { once: true })

Se você tentar reproduzir áudio antes disso, a operação falhará silenciosamente ou o AudioContext permanecerá suspenso. Isso pega muitos desenvolvedores de surpresa.

Carregando sons

Os arquivos de áudio precisam ser decodificados antes da reprodução. Busque o arquivo, decodifique-o e mantenha o buffer disponível para reutilização:

js
async function loadSound(url) {
  const response = await fetch(url)
  const arrayBuffer = await response.arrayBuffer()
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)
  return audioBuffer
}

A decodificação leva tempo, então carregue todos os sons durante uma tela de carregamento, não quando precisar reproduzi-los.

Reproduzindo sons

Para reproduzir um som, crie uma fonte de buffer, conecte-a por meio de um nó de ganho para controlar o volume e inicie-a:

js
function playSound(buffer, volume = 1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const gainNode = audioCtx.createGain()
  gainNode.gain.value = volume
  
  source.connect(gainNode)
  gainNode.connect(audioCtx.destination)
  
  source.start(0)
  return source
}

As fontes de buffer só podem ser usadas uma vez. Depois que uma fonte termina de tocar, você não pode reiniciá-la. Crie uma nova a cada reprodução. Isso pode parecer um desperdício, mas é assim que a API funciona, e os navegadores são otimizados para isso.

Um gerenciador de sons

Para um jogo de verdade, reúna tudo em uma classe gerenciadora:

js
class SoundManager {
  constructor() {
    this.ctx = null
    this.sounds = new Map()
    this.masterGain = null
  }
  
  init() {
    this.ctx = new AudioContext()
    this.masterGain = this.ctx.createGain()
    this.masterGain.connect(this.ctx.destination)
  }
  
  async load(name, url) {
    const response = await fetch(url)
    const buffer = await this.ctx.decodeAudioData(await response.arrayBuffer())
    this.sounds.set(name, buffer)
  }
  
  play(name, volume = 1) {
    const buffer = this.sounds.get(name)
    if (!buffer) return
    
    const source = this.ctx.createBufferSource()
    source.buffer = buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.masterGain)
    source.start(0)
    
    return source
  }
  
  setMasterVolume(v) {
    this.masterGain.gain.value = v
  }
}

O nó de ganho mestre permite implementar um controle deslizante de volume global. Todos os sons passam por ele.

Música de fundo

A música precisa ser reproduzida em loop e oferecer a possibilidade de ser interrompida ou desaparecer gradualmente:

js
class MusicPlayer {
  constructor(ctx, masterGain) {
    this.ctx = ctx
    this.masterGain = masterGain
    this.currentTrack = null
    this.gain = ctx.createGain()
    this.gain.connect(masterGain)
  }
  
  play(buffer, volume = 0.5) {
    this.stop()
    
    this.currentTrack = this.ctx.createBufferSource()
    this.currentTrack.buffer = buffer
    this.currentTrack.loop = true
    this.currentTrack.connect(this.gain)
    this.gain.gain.value = volume
    this.currentTrack.start(0)
  }
  
  stop() {
    if (this.currentTrack) {
      this.currentTrack.stop()
      this.currentTrack = null
    }
  }
  
  fadeOut(duration = 1) {
    this.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + duration)
  }
}

O método linearRampToValueAtTime cria transições suaves em vez de cortes abruptos.

Áudio espacial

Para jogos 3D, você pode posicionar os sons no espaço. Eles ficam mais baixos à medida que se afastam do ouvinte:

js
function playSpatialSound(buffer, x, y, z) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  
  const panner = audioCtx.createPanner()
  panner.panningModel = 'HRTF'
  panner.distanceModel = 'inverse'
  panner.refDistance = 1
  panner.maxDistance = 100
  panner.positionX.value = x
  panner.positionY.value = y
  panner.positionZ.value = z
  
  source.connect(panner)
  panner.connect(audioCtx.destination)
  source.start(0)
}

function updateListener(x, y, z, fx, fy, fz) {
  const listener = audioCtx.listener
  listener.positionX.value = x
  listener.positionY.value = y
  listener.positionZ.value = z
  listener.forwardX.value = fx
  listener.forwardY.value = fy
  listener.forwardZ.value = fz
  listener.upX.value = 0
  listener.upY.value = 1
  listener.upZ.value = 0
}

Chame updateListener a cada quadro usando a posição da câmera ou do jogador. O panner ajusta automaticamente o volume e a panoramização estéreo com base na distância e na direção.

Sons de disparos rápidos

Para sons reproduzidos muitas vezes por segundo (tiros, passos), você não quer que instâncias sobrepostas se acumulem. Um padrão simples de pool ajuda:

js
class SoundPool {
  constructor(ctx, buffer, size = 8) {
    this.sources = []
    this.index = 0
    this.ctx = ctx
    this.buffer = buffer
    this.size = size
  }
  
  play(volume = 1) {
    const source = this.ctx.createBufferSource()
    source.buffer = this.buffer
    
    const gain = this.ctx.createGain()
    gain.gain.value = volume
    
    source.connect(gain)
    gain.connect(this.ctx.destination)
    source.start(0)
    
    this.index = (this.index + 1) % this.size
  }
}

O pool limita quantas instâncias do mesmo som podem ser reproduzidas simultaneamente. Quando você atinge o limite, a mais antiga é substituída.

Formatos de áudio

Formatos diferentes funcionam melhor em situações diferentes:

MP3 funciona em todos os lugares e é uma boa opção para música. OGG Vorbis oferece melhor qualidade com o mesmo tamanho de arquivo. O Safari resistiu por muito tempo, mas adicionou reprodução nativa de Ogg Vorbis no Safari 18.4, então todos os principais navegadores atuais agora oferecem suporte. Ainda é recomendável disponibilizar MP3 como alternativa se você precisa oferecer suporte a versões mais antigas do Safari. AAC/M4A funciona bem em dispositivos Apple. WAV não tem compressão e gera arquivos grandes, mas é decodificado instantaneamente, o que é bom para efeitos sonoros curtos quando o tamanho do arquivo não é um problema. WebM/Opus oferece a melhor relação entre qualidade e tamanho, mas só funciona em navegadores modernos.

Para garantir compatibilidade máxima, ofereça alternativas:

js
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'

Truques úteis

Variação de tom faz com que sons repetidos pareçam menos robóticos:

js
function playWithPitchVariation(buffer, variance = 0.1) {
  const source = audioCtx.createBufferSource()
  source.buffer = buffer
  source.playbackRate.value = 1 + (Math.random() - 0.5) * variance
  source.connect(audioCtx.destination)
  source.start(0)
}

Cada tiro ou passo soa ligeiramente diferente.

Ducking reduz o volume da música durante diálogos ou sons importantes:

js
function duckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + duration)
}

function unduckMusic(musicGain, duration = 0.3) {
  musicGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + duration)
}

Armadilhas no iOS

O iOS tem restrições de áudio mais rígidas do que outras plataformas. O AudioContext precisa ser criado e retomado durante um gesto do usuário. Algumas versões do iOS também exigem que você reproduza um som (mesmo que seja silencioso) durante o gesto, e não apenas crie o contexto. Se o áudio funciona no desktop, mas não no iOS, esse provavelmente é o motivo.

Mais recursos

Publique um jogo web que carregue rápido aborda a compactação de recursos, incluindo áudio.

Jogos web compatíveis com dispositivos móveis apresenta mais informações sobre problemas específicos do iOS.

PWA para jogos offline mostra como armazenar arquivos de áudio em cache para jogar offline.

Tone.js para áudio de jogos aborda a geração procedural de sons e sistemas de música usando uma biblioteca de nível mais alto.

Onde encontrar recursos gratuitos para jogos apresenta fontes gratuitas de efeitos sonoros e músicas, como Freesound e Poly Haven.

Recursos externos