Skip to content

Tone.js para Áudio de Jogos

O Tone.js funciona sobre a API Web Audio e torna prático o uso de áudio sintetizado. Em vez de carregar arquivos de áudio, você gera sons por meio de programação. Isso significa tempo de carregamento zero, variações infinitas e sons capazes de reagir à jogabilidade em tempo real.

Experimente (clique nos botões para ouvir sons de jogo sintetizados):

Por que Usar Tone.js em Vez de Arquivos de Áudio

Arquivos de áudio funcionam bem para músicas e sons complexos. Mas, para efeitos sonoros de jogos, a síntese oferece vantagens reais. Você pode variar o tom e o tempo a cada reprodução para evitar que os sons pareçam repetitivos. Pode gerar sons dinamicamente com base no estado do jogo. E elimina completamente as requisições de rede.

A desvantagem é que o áudio sintetizado consome mais CPU do que a reprodução de um arquivo. Para jogos simples, isso não é um problema. Em jogos complexos, você pode combinar as duas abordagens.

Configuração

Carregue o Tone.js por uma CDN ou pelo npm:

html
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>

Ou com npm:

bash
npm install tone
js
import * as Tone from 'tone'

O Problema da Reprodução Automática

Os navegadores bloqueiam o áudio até que haja uma interação do usuário. O Tone.js lida com isso, mas você precisa chamar Tone.start() em resposta a um gesto do usuário:

js
document.addEventListener('click', async () => {
  await Tone.start()
  console.log('Audio ready')
}, { once: true })

Sem isso, seu AudioContext permanece suspenso e nada é reproduzido.

Seu Primeiro Sintetizador

Um sintetizador gera som. O mais simples é o Tone.Synth:

js
const synth = new Tone.Synth().toDestination()

// Play a note
synth.triggerAttackRelease('C4', '8n')

triggerAttackRelease toca uma nota por determinada duração. 'C4' é o dó central. '8n' é uma colcheia no andamento atual.

O sintetizador tem um oscilador (a fonte do som) e um envelope (como o som aumenta e diminui de intensidade). Você pode personalizar ambos:

js
const synth = new Tone.Synth({
  oscillator: { type: 'triangle' },
  envelope: {
    attack: 0.02,
    decay: 0.2,
    sustain: 0.3,
    release: 0.4,
  },
}).toDestination()

Os tipos de oscilador incluem sine (puro e suave), triangle (suave), sawtooth (vibrante) e square (oco e retrô).

Criando um Sistema de Efeitos Sonoros

Em um jogo, é útil ter vários sintetizadores para diferentes tipos de som. Veja um padrão que funciona bem:

js
let isInitialized = false
let softSynth = null
let subSynth = null
let noiseSynth = null
let sfxGain = null

function initSFX() {
  if (isInitialized) return
  
  // Master volume control
  sfxGain = new Tone.Gain(0.7).toDestination()
  
  // Soft synth for melodic sounds
  softSynth = new Tone.PolySynth(Tone.Synth, {
    maxPolyphony: 6,
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.2,
      sustain: 0.3,
      release: 0.4,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Sub bass for deep impacts
  subSynth = new Tone.Synth({
    oscillator: { type: 'sine' },
    envelope: {
      attack: 0.02,
      decay: 0.3,
      sustain: 0.1,
      release: 0.3,
    },
    volume: -8,
  }).connect(sfxGain)
  
  // Noise for texture
  noiseSynth = new Tone.NoiseSynth({
    noise: { type: 'pink' },
    envelope: {
      attack: 0.02,
      decay: 0.15,
      sustain: 0,
      release: 0.15,
    },
    volume: -20,
  }).connect(sfxGain)
  
  isInitialized = true
}

PolySynth permite tocar várias notas ao mesmo tempo. O Synth comum é monofônico.

Exemplos de Sons de Jogos

Veja alguns sons comuns em jogos e como criá-los:

Coleta de moeda — notas ascendentes transmitem uma sensação de recompensa:

js
function coinCollect() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('C4', '8n', now, 0.3)
  softSynth.triggerAttackRelease('E4', '8n', now + 0.1, 0.35)
  softSynth.triggerAttackRelease('G4', '4n', now + 0.2, 0.4)
}

Pulo — varredura rápida para cima:

js
function jump() {
  const synth = new Tone.Synth({
    oscillator: { type: 'square' },
    envelope: { attack: 0.01, decay: 0.1, sustain: 0, release: 0.1 },
  }).toDestination()
  
  const now = Tone.now()
  synth.frequency.setValueAtTime(150, now)
  synth.frequency.exponentialRampToValueAtTime(400, now + 0.1)
  synth.triggerAttackRelease('C4', '8n', now, 0.2)
}

Golpe/dano — som descendente com ruído:

js
function hit() {
  const now = Tone.now()
  subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
  noiseSynth.triggerAttackRelease('4n', now, 0.25)
}

Morte — notas descendentes e tristes:

js
function death() {
  const now = Tone.now()
  softSynth.triggerAttackRelease('E4', '4n', now, 0.4)
  softSynth.triggerAttackRelease('C4', '4n', now + 0.2, 0.35)
  softSynth.triggerAttackRelease('A3', '4n', now + 0.4, 0.3)
}

Adicionando Efeitos

Os efeitos deixam os sons mais ricos. Conecte-os em cadeia entre o sintetizador e o destino:

js
const filter = new Tone.Filter({
  frequency: 3000,
  type: 'lowpass',
  rolloff: -12,
})

const reverb = new Tone.Reverb({
  decay: 1.2,
  wet: 0.25,
})

const gain = new Tone.Gain(0.7)

// Chain: synth -> filter -> reverb -> gain -> destination
filter.connect(reverb)
reverb.connect(gain)
gain.toDestination()

const synth = new Tone.Synth().connect(filter)

O filtro passa-baixas reduz frequências agudas desagradáveis. O reverb acrescenta espaço. O ganho controla o volume.

Limitação de Frequência

Se o jogo disparar muitos sons rapidamente, eles poderão se sobrepor e virar uma confusão. Adicione uma limitação de frequência:

js
const MIN_INTERVAL_MS = 35
let lastNoteTime = 0

function canPlayNote() {
  const now = performance.now()
  if (now - lastNoteTime < MIN_INTERVAL_MS) {
    return false
  }
  lastNoteTime = now
  return true
}

function playSound() {
  if (!canPlayNote()) return
  synth.triggerAttackRelease('C4', '8n')
}

Música de Fundo Dinâmica

Para criar música que reage à jogabilidade, use um sistema baseado em batidas:

js
const BPM = 140
const BEAT_MS = (60 / BPM) * 1000

let bassSynth = null
let kickDrum = null
let beatInterval = null
let beatCount = 0
let energy = 0.3

const PROGRESSION = [
  { root: 'E2', chord: ['E3', 'G3', 'B3'] },
  { root: 'C2', chord: ['C3', 'E3', 'G3'] },
  { root: 'D2', chord: ['D3', 'F#3', 'A3'] },
  { root: 'A1', chord: ['A2', 'C3', 'E3'] },
]

function playBeat() {
  const beatInMeasure = beatCount % 8
  const chordIndex = Math.floor(beatCount / 8) % PROGRESSION.length
  const chord = PROGRESSION[chordIndex]
  const now = Tone.now()
  
  // Bass on beat 1
  if (beatInMeasure === 0) {
    bassSynth.triggerAttackRelease(chord.root, '4n', now, 0.6)
  }
  
  // Kick drum - only when energy is high
  if (beatInMeasure === 0 && energy > 0.3) {
    kickDrum.triggerAttackRelease('C1', '8n', now, 0.4)
  }
  
  beatCount++
}

function startMusic() {
  beatInterval = setInterval(playBeat, BEAT_MS)
}

function stopMusic() {
  clearInterval(beatInterval)
}

// Game events affect energy
function onEnemyKill() {
  energy = Math.min(1, energy + 0.1)
}

A intensidade da música muda de acordo com o que está acontecendo no jogo.

Bateria com MembraneSynth e NoiseSynth

Para percussão, use sintetizadores especializados:

js
// Kick drum
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 4,
  envelope: {
    attack: 0.001,
    decay: 0.2,
    sustain: 0,
    release: 0.1,
  },
  volume: -14,
}).toDestination()

kick.triggerAttackRelease('C1', '8n')

// Snare
const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.002,
    decay: 0.15,
    sustain: 0,
    release: 0.1,
  },
  volume: -12,
}).toDestination()

snare.triggerAttackRelease('8n')

// Hi-hat
const hihat = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: {
    attack: 0.001,
    decay: 0.04,
    sustain: 0,
    release: 0.03,
  },
  volume: -22,
}).toDestination()

hihat.triggerAttackRelease('32n')

Limpeza

Descarte os sintetizadores quando terminar para liberar recursos:

js
function cleanup() {
  if (softSynth) softSynth.dispose()
  if (subSynth) subSynth.dispose()
  if (noiseSynth) noiseSynth.dispose()
  if (sfxGain) sfxGain.dispose()
}

Erros Comuns

Não aguardar Tone.start() — o áudio não será reproduzido até que o contexto seja iniciado durante uma interação do usuário.

Criar sintetizadores para cada som — crie os sintetizadores uma vez durante a inicialização e reutilize-os. Criar novos a cada vez causa vazamentos de memória.

Sons demais sobrepostos — use limitação de frequência e desduplicação para evitar que o áudio vire uma massa indistinta.

Frequências desagradáveis — use filtros passa-baixas e tipos de oscilador mais suaves (sine, triangle) para criar sons que não incomodem os jogadores.

Mais Recursos

API Web Audio para jogos aborda a API subjacente e o carregamento de arquivos de áudio.

A documentação do Tone.js contém a referência completa da API.

Onde Encontrar Recursos Gratuitos para Jogos inclui provedores de áudio e efeitos sonoros, como o Freesound.

Jogos web adaptados para dispositivos móveis aborda as restrições de áudio do iOS e padrões de toque para desbloquear o áudio.