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.
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:
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:
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:
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:
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:
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:
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:
const audioUrl = canPlayOgg() ? 'sound.ogg' : 'sound.mp3'Truques úteis
Variação de tom faz com que sons repetidos pareçam menos robóticos:
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:
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
- MDN: Web Audio API — referência completa da API
- MDN: AudioContext — criação e gerenciamento de contextos de áudio
- Especificação da Web Audio API — especificação do W3C