Introdução
O áudio é uma parte importante do que torna as experiências multimídia tão atraentes. Se você já tentou assistir a um filme sem som, provavelmente percebeu isso.
Os jogos não são exceção. Minhas melhores lembranças de videogames são da música e dos efeitos sonoros. Agora, em muitos casos, quase duas décadas depois de jogar meus favoritos, ainda não consigo tirar as composições de Zelda de Koji Kondo e a trilha sonora atmosférica de Diablo de Matt Uelmen da minha cabeça. O mesmo se aplica aos efeitos sonoros, como as respostas de clique de unidade instantaneamente reconhecíveis de Warcraft e as amostras dos clássicos da Nintendo.
O áudio de jogos apresenta alguns desafios interessantes. Para criar músicas de jogo convincentes, os designers precisam se ajustar ao estado potencialmente imprevisível em que o jogador se encontra. Na prática, partes do jogo podem durar um tempo desconhecido, os sons podem interagir com o ambiente e se misturar de maneiras complexas, como efeitos de sala e posicionamento relativo do som. Por fim, pode haver um grande número de sons sendo reproduzidos ao mesmo tempo, e todos eles precisam soar bem juntos e ser renderizados sem prejudicar o desempenho.
Áudio de jogos na Web
Para jogos simples, usar a tag <audio> pode ser suficiente. No entanto, muitos
navegadores oferecem implementações ruins, o que resulta em falhas de áudio
e alta latência. Esperamos que esse seja um problema temporário, já que os fornecedores
estão trabalhando para melhorar as respectivas implementações. Para ter uma
visão geral do estado da tag <audio>, há um bom pacote de testes
em areweplayingyet.org (em inglês).
No entanto, ao analisar mais detalhadamente a especificação da tag <audio>, fica
claro que há muitas coisas que simplesmente não podem ser feitas com ela,
o que não é surpreendente, já que ela foi projetada para reprodução de mídia. Algumas
limitações incluem:
- Não é possível aplicar filtros ao sinal de som
- Não há como acessar os dados brutos de PCM
- Não há conceito de posição e direção de fontes e ouvintes
- Sem tempo detalhado.
No restante do artigo, vou me aprofundar em alguns desses tópicos no contexto de áudio de jogos escrito com a API Web Audio. Para uma breve introdução a essa API, confira o tutorial de introdução.
Músicas de fundo
Os jogos geralmente têm uma música de fundo tocando em loop.
Pode ser muito irritante se o loop for curto e previsível. Se um jogador estiver preso em uma área ou nível, e a mesma amostra for reproduzida continuamente em segundo plano, talvez seja interessante diminuir gradualmente o volume da faixa para evitar mais frustração. Outra estratégia é ter mixagens de várias intensidades que se misturam gradualmente, dependendo do contexto do jogo.
Por exemplo, se o jogador estiver em uma zona com uma batalha épica contra um chefe, você pode ter várias mixagens que variam em alcance emocional, de atmosférico a prenúncio e intenso. O software de síntese musical geralmente permite exportar várias mixagens (da mesma duração) com base em uma música, escolhendo o conjunto de faixas que serão usadas na exportação. Dessa forma, você tem alguma consistência interna e evita transições bruscas ao fazer o crossfade de uma faixa para outra.
Em seguida, usando a API Web Audio, é possível importar todas essas amostras usando algo como a classe BufferLoader via XHR (isso é abordado em detalhes no artigo introdutório da API Web Audio. O carregamento de sons leva tempo. Por isso, os recursos usados no jogo precisam ser carregados no carregamento de página, no início do nível ou de forma incremental enquanto o jogador está jogando.
Em seguida, crie uma origem para cada nó e um nó de ganho para cada origem e conecte o gráfico.
Depois disso, é possível reproduzir todas essas fontes simultaneamente em um loop. Como todas têm o mesmo tamanho, a API Web Audio garante que elas permaneçam alinhadas. Conforme o personagem se aproxima ou se afasta da batalha final, o jogo pode variar os valores de ganho para cada um dos respectivos nós na cadeia, usando um algoritmo de quantidade de ganho como este:
// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
// If there is, adjust its gain.
gains[leftNode + 1].gain.value = gain2;
}
Na abordagem acima, duas fontes são reproduzidas ao mesmo tempo, e fazemos um crossfade entre elas usando curvas de potência iguais (conforme descrito na introdução).
O link que faltava: da tag de áudio para a Web Audio
Muitos desenvolvedores de jogos usam a tag <audio> para a música de fundo, porque ela é adequada para streaming de conteúdo. Agora é possível trazer
conteúdo da tag <audio> para um contexto de Web Audio.
Essa técnica pode ser útil, já que a tag <audio> pode funcionar com
conteúdo de streaming, o que permite tocar imediatamente a música de fundo
em vez de esperar que ela seja totalmente baixada. Ao trazer o stream
para a API Web Audio, é possível manipulá-lo ou analisá-lo. O
exemplo a seguir aplica um filtro passa-baixa à música tocada pela
tag <audio>:
var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);
Para uma discussão mais completa sobre como integrar a tag <audio> à
API Web Audio, consulte este artigo curto.
Efeitos sonoros
Os jogos costumam reproduzir efeitos sonoros em resposta à entrada do usuário ou a mudanças no estado do jogo. No entanto, assim como a música de fundo, os efeitos sonoros podem se tornar irritantes muito rapidamente. Para evitar isso, é útil ter um conjunto de sons semelhantes, mas diferentes, para tocar. Isso pode variar de variações leves de amostras de passos a variações drásticas, como visto na série Warcraft em resposta ao clique em unidades.
Outra característica importante dos efeitos sonoros em jogos é que pode haver muitos deles simultaneamente. Imagine que você está no meio de um tiroteio com vários atores atirando com metralhadoras. Cada metralhadora dispara muitas vezes por segundo, fazendo com que dezenas de efeitos sonoros sejam reproduzidos ao mesmo tempo. A reprodução simultânea de som de várias fontes com tempo preciso é um dos pontos fortes da API Web Audio.
O exemplo a seguir cria uma rodada de metralhadora com várias amostras de balas individuais, criando várias fontes de som com reprodução escalonada no tempo.
var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
var source = this.makeSource(this.buffers[M4A1]);
source.noteOn(time + i - interval);
}
Se todas as metralhadoras do seu jogo tivessem exatamente esse som, seria bem chato. É claro que eles variam de acordo com o som com base na distância do alvo e no posicionamento relativo (mais sobre isso depois), mas talvez isso não seja suficiente. Felizmente, a API Web Audio oferece uma maneira de ajustar facilmente o exemplo acima de duas maneiras:
- Com uma mudança sutil no tempo entre os disparos de balas
- Alterando playbackRate de cada amostra (também mudando o tom) para simular melhor a aleatoriedade do mundo real.
Para um exemplo mais real dessas técnicas em ação, confira a demonstração da mesa de sinuca, que usa amostragem aleatória e varia playbackRate para um som de colisão de bola mais interessante.
Som posicional 3D
Os jogos geralmente são ambientados em um mundo com algumas propriedades geométricas, em 2D ou 3D. Se esse for o caso, o áudio estéreo posicionado pode aumentar muito a imersão da experiência. Felizmente, a API Web Audio vem com recursos de áudio posicional com aceleração de hardware integrados que são bastante simples de usar. A propósito, verifique se você tem alto-falantes estéreo (de preferência fones de ouvido) para que o exemplo a seguir faça sentido.
No exemplo acima, há um ouvinte (ícone de pessoa) no meio da tela, e o mouse afeta a posição da fonte (ícone de alto-falante). O exemplo acima é um exemplo simples de como usar o AudioPannerNode para conseguir esse tipo de efeito. A ideia básica do exemplo acima é responder ao movimento do mouse definindo a posição da fonte de áudio, da seguinte maneira:
PositionSample.prototype.changePosition = function(position) {
// Position coordinates are in normalized canvas coordinates
// with -0.5 < x, y < 0.5
if (position) {
if (!this.isPlaying) {
this.play();
}
var mul = 2;
var x = position.x / this.size.width;
var y = -position.y / this.size.height;
this.panner.setPosition(x - mul, y - mul, -0.5);
} else {
this.stop();
}
};
O que você precisa saber sobre o tratamento da espacialização pela Web Audio:
- O listener fica na origem (0, 0, 0) por padrão.
- As APIs posicionais da Web Audio não têm unidade, então introduzi um multiplicador para melhorar o som da demonstração.
- A Web Audio usa as coordenadas cartesianas y-is-up (o oposto da maioria dos sistemas de computação gráfica). Por isso, estou trocando o eixo y no snippet acima.
Avançado: cones de som
O modelo posicional é muito avançado e eficiente, baseado principalmente no OpenAL. Para mais detalhes, consulte as seções 3 e 4 da especificação vinculada acima.
Há um único AudioListener anexado ao contexto da API Web Audio, que pode ser configurado no espaço por posição e orientação. Cada fonte pode ser transmitida por um AudioPannerNode, que espacializa o áudio de entrada. O nó de panorâmica tem posição e orientação, além de um modelo de distância e direcional.
O modelo de distância especifica a quantidade de ganho dependendo da proximidade com a fonte, enquanto o modelo direcional pode ser configurado especificando um cone interno e externo, que determinam a quantidade de ganho (geralmente negativo) se o ouvinte estiver dentro do cone interno, entre o cone interno e externo ou fora do cone externo.
var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;
Embora meu exemplo seja em 2D, esse modelo se generaliza facilmente para a terceira dimensão. Para ver um exemplo de som espacializado em 3D, consulte esta amostra posicional. Além da posição, o modelo de som da Web Audio também inclui opcionalmente a velocidade para mudanças de Doppler. Este exemplo mostra o efeito Doppler em mais detalhes.
Para mais informações sobre esse assunto, leia este tutorial detalhado sobre [como combinar áudio posicional e WebGL][webgl].
Efeitos e filtros de sala
Na realidade, a forma como o som é percebido depende muito do ambiente em que ele é ouvido. A mesma porta rangendo vai soar muito diferente em um porão, em comparação com um grande salão aberto. Os jogos com alto valor de produção vão querer imitar esses efeitos, já que criar um conjunto separado de amostras para cada ambiente é muito caro e levaria a ainda mais recursos e uma quantidade maior de dados de jogo.
Em termos gerais, o termo de áudio para a diferença entre o som bruto e o som na realidade é a resposta ao impulso. Essas respostas ao impulso podem ser gravadas com cuidado. Na verdade, existem sites que hospedam muitos desses arquivos de resposta ao impulso pré-gravados (armazenados como áudio) para sua conveniência.
Para mais informações sobre como as respostas de impulso podem ser criadas em um determinado ambiente, leia a seção "Configuração de gravação" na parte Convolução da especificação da API Web Audio.
O mais importante para nossos propósitos é que a API Web Audio oferece uma maneira fácil de aplicar essas respostas ao impulso aos nossos sons usando o ConvolverNode.
// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);
Confira também esta demonstração de efeitos de sala na página de especificações da API Web Audio, bem como este exemplo, que permite controlar a mixagem seca (bruta) e molhada (processada por convolver) de um ótimo padrão de jazz.
Contagem regressiva
Você criou um jogo, configurou o áudio posicional e agora tem um grande número de AudioNodes no gráfico, todos sendo reproduzidos simultaneamente. Ótimo, mas ainda há mais uma coisa a considerar:
Como vários sons se acumulam uns sobre os outros sem normalização, você pode se deparar com uma situação em que excede o limite de capacidade do alto-falante. Assim como as imagens que ultrapassam os limites da tela, os sons também podem ser cortados se a forma de onda exceder o limite máximo, produzindo uma distorção distinta. A forma de onda é mais ou menos assim:
Veja um exemplo real de clipping em ação. A forma de onda está ruim:
É importante ouvir distorções severas como a acima ou, por outro lado, mixagens excessivamente suaves que forçam os ouvintes a aumentar o volume. Se você estiver nessa situação, precisa corrigir isso.
Detectar corte
Do ponto de vista técnico, o corte ocorre quando o valor do sinal em qualquer canal excede o intervalo válido, ou seja, entre -1 e 1. Depois que isso for detectado, é útil dar um feedback visual de que isso está acontecendo. Para fazer isso de forma confiável, coloque um JavaScriptAudioNode no gráfico. O gráfico de áudio seria configurado da seguinte forma:
// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);
E o corte pode ser detectado no seguinte gerenciador processAudio:
function processAudio(e) {
var buffer = e.inputBuffer.getChannelData(0);
var isClipping = false;
// Iterate through buffer to check if any of the |values| exceeds 1.
for (var i = 0; i < buffer.length; i++) {
var absValue = Math.abs(buffer[i]);
if (absValue >= 1) {
isClipping = true;
break;
}
}
}
Em geral, tome cuidado para não usar demais o JavaScriptAudioNode por
motivos de desempenho. Nesse caso, uma implementação alternativa de medição pode sondar um RealtimeAnalyserNode no grafo de áudio para getByteFrequencyData, no tempo de renderização, conforme determinado por requestAnimationFrame. Essa abordagem é mais eficiente, mas perde
a maior parte do sinal (incluindo lugares em que ele pode ser cortado), já que
a renderização acontece no máximo 60 vezes por segundo, enquanto o sinal de áudio
muda muito mais rapidamente.
Como a detecção de clipes é muito importante, é provável que, no futuro, tenhamos um
nó integrado da API Web Audio para MeterNode.
Evitar cortes
Ao ajustar o ganho no AudioGainNode principal, é possível reduzir a mixagem para um nível que evite o corte. No entanto, na prática, como os sons reproduzidos no jogo podem depender de uma enorme variedade de fatores, pode ser difícil decidir o valor de ganho principal que evita o corte para todos os estados. Em geral, é recomendável ajustar os ganhos para antecipar o pior caso, mas isso é mais uma arte do que uma ciência.
Adicione um pouco de açúcar
Os compressores são usados com frequência na produção de músicas e jogos para suavizar
o sinal e controlar picos no sinal geral. Essa
funcionalidade está disponível no mundo do Web Audio pela
DynamicsCompressorNode, que pode ser inserida no gráfico de áudio para
oferecer um som mais alto, rico e completo, além de ajudar com o corte.
Citando diretamente a especificação, este nó
Usar a compactação dinâmica geralmente é uma boa ideia, especialmente em um jogo, em que, como discutido anteriormente, você não sabe exatamente quais sons serão reproduzidos e quando. O Plink (em inglês) do DinahMoe Labs é um ótimo exemplo disso, já que os sons reproduzidos dependem completamente de você e de outros participantes. Um compressor é útil na maioria dos casos, exceto em alguns raros, em que você está lidando com faixas masterizadas com cuidado que já foram ajustadas para soar "perfeitas".
Para implementar isso, basta incluir um DynamicsCompressorNode no gráfico de áudio, geralmente como o último nó antes do destino:
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
Para mais detalhes sobre a compactação dinâmica, este artigo da Wikipédia é muito informativo.
Em resumo, ouça com atenção para identificar o clipping e evite-o inserindo um nó de ganho mestre. Em seguida, ajuste toda a mixagem usando um nó de compressor dinâmico. Seu gráfico de áudio pode ser parecido com este:
Conclusão
Isso abrange o que considero os aspectos mais importantes do desenvolvimento de áudio de jogos usando a API Web Audio. Com essas técnicas, é possível criar experiências de áudio realmente incríveis diretamente no navegador. Antes de encerrar, vou deixar uma dica específica para navegadores: pause o som se a guia for para o segundo plano usando a API Page Visibility. Caso contrário, você vai criar uma experiência potencialmente frustrante para o usuário.
Para mais informações sobre a Web Audio, confira o artigo de introdução. Se tiver alguma dúvida, veja se ela já foi respondida nas perguntas frequentes sobre a Web Audio. Por fim, se tiver outras dúvidas, pergunte no Stack Overflow (em inglês) usando a tag web-audio.
Antes de me despedir, vou deixar alguns usos incríveis da API Web Audio em jogos reais:
- Field Runners e um artigo sobre alguns dos detalhes técnicos.
- Angry Birds, recentemente mudou para a API Web Audio. Consulte este artigo para mais informações.
- Skid Racer, que faz um ótimo uso do áudio espacial.