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:
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>Ou com npm:
npm install toneimport * 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:
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:
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:
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:
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:
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:
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:
function hit() {
const now = Tone.now()
subSynth.triggerAttackRelease('E2', '4n', now, 0.5)
noiseSynth.triggerAttackRelease('4n', now, 0.25)
}Morte — notas descendentes e tristes:
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:
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:
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:
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:
// 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:
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.