Como desenvolver áudio de jogos com a API de áudio da Web

Introdução

O áudio é uma parte importante do que torna as experiências multimídia tão atraentes. Se você já tentou assistir um filme sem som, provavelmente notou isso.

Os jogos não são uma exceção. Minhas melhores lembranças de videogame são da música e dos efeitos sonoros. Agora, em muitos casos, quase duas décadas depois de jogar os meus favoritos, ainda não consigo tirar as composições de Koji Kondo do Zelda e a trilha sonora atrativa de Matt Uelmen da minha cabeça. O mesmo efeito se aplica a efeitos sonoros, como as respostas de clique de unidade instantaneamente reconhecíveis de Warcraft e amostras de clássicos da Nintendo.

O áudio do jogo apresenta alguns desafios interessantes. Para criar músicas convencedoras, os designers precisam se ajustar ao estado de jogo potencialmente imprevisível em que o jogador se encontra. Na prática, partes do jogo podem continuar por 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 renderizar sem introduzir penalidades de desempenho.

Áudio de jogos na Web

Para jogos simples, o uso da 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 conferir o estado da tag <audio>, há um bom conjunto de testes em areweplayingyet.org.

No entanto, ao analisar mais 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 sonoro
  • Não há como acessar os dados PCM brutos
  • Nenhum conceito de posição e direção de fontes e listeners
  • Sem temporização granular.

No restante do artigo, vou abordar alguns desses tópicos no contexto de áudio de jogos criados com a API Web Audio. Para uma breve introdução a essa API, consulte o tutorial de primeiros passos.

Músicas de fundo

Os jogos geralmente têm música de fundo que é reproduzida em loop.

Isso poderá ser muito irritante se o loop for curto e previsível. Se um jogador ficar preso em uma área ou nível e a mesma amostra for reproduzida continuamente em segundo plano, pode ser útil desativá-la gradualmente para evitar mais frustração. Outra estratégia é ter misturas 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 o chefe, você poderá ter várias misturas variando na faixa emocional, de atmosférica a prenúncio e intensa. Os softwares de síntese musical geralmente permitem exportar várias mixagens (do mesmo tamanho) com base em uma peça escolhendo o conjunto de faixas a serem usadas na exportação. Assim, você tem uma certa consistência interna e evita transições bruscas ao fazer um crossfade de uma faixa para outra.

GarageBand

Em seguida, usando a API Web Audio, é possível importar todos esses exemplos usando algo como a classe BufferLoader pelo 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 da página, no início da fase ou talvez de modo 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, você pode reproduzir todas essas fontes simultaneamente em um loop. Como elas têm o mesmo comprimento, a API Web Audio vai garantir que elas permaneçam alinhadas. À medida que o personagem se aproxima ou se afasta da batalha final do chefe, o jogo pode variar os valores de ganho para cada um dos respectivos nós na cadeia, usando um algoritmo de valor 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 tocadas ao mesmo tempo, e fazemos um crossfade entre elas usando curvas de potência iguais (conforme descrito na introdução).

Muitos desenvolvedores de jogos usam a tag <audio> para a música de plano de fundo, porque ela é adequada para conteúdo de streaming. Agora é possível trazer o conteúdo da tag <audio> para um contexto do 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 o download de tudo. Ao trazer o fluxo para a API Web Audio, você pode manipular ou analisar o fluxo. O exemplo abaixo aplica um filtro de passagem de baixas frequências à 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 a integração da tag <audio> com a API Web Audio, consulte este artigo breve.

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 pequenas variações de amostras de passos a variações drásticas, como visto na série Warcraft em resposta a um clique nas unidades.

Outro recurso importante dos efeitos sonoros em jogos é que eles podem ser usados 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 tocados ao mesmo tempo. A reprodução de som de várias fontes com tempo preciso ao mesmo tempo é um dos pontos fortes da API Web Audio.

O exemplo a seguir cria uma rodada de metralhadora a partir de várias amostras de projéteis 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 soassem exatamente assim, seria muito chato. É claro que eles variam de acordo com o som com base na distância do alvo e na posição relativa (falaremos sobre isso mais tarde), mas até mesmo isso pode não ser suficiente. Felizmente, a API Web Audio oferece uma maneira de ajustar facilmente o exemplo acima de duas maneiras:

  1. Com uma mudança sutil no tempo entre disparos de balas
  2. Alterando a playbackRate de cada amostra (também mudando o tom) para simular melhor a aleatoriedade do mundo real.

Para conferir um exemplo mais realista dessas técnicas em ação, consulte a demonstração da mesa de sinuca, que usa amostragem aleatória e varia a playbackRate para um som de colisão de bolas mais interessante.

Som posicional 3D

Os jogos geralmente se passam em um mundo com algumas propriedades geométricas, seja em 2D ou 3D. Nesse caso, o áudio posicionado em estéreo pode aumentar consideravelmente a imersão da experiência. Felizmente, a API Web Audio vem com recursos de áudio posicional integrados e acelerados por hardware que são bastante simples de usar. Confira 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 listener (ícone de pessoa) no meio da tela, e o mouse afeta a posição da fonte (ícone de alto-falante). O exemplo acima é simples de usar AudioPannerNode para criar 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();
    }
};

Informações importantes sobre o tratamento da espacialização do Web Audio:

  • O listener está na origem (0, 0, 0) por padrão.
  • As APIs posicionais do Web Audio não têm unidades. Por isso, apresentei um multiplicador para melhorar o som da demonstração.
  • O Web Audio usa as coordenadas cartesianas y-is-up (o oposto da maioria dos sistemas de gráficos de computador). É por isso que estou trocando o eixo y no snippet acima.

Avançado: cones de som

O modelo posicional é muito poderoso e bastante avançado, baseado principalmente no OpenAL. Para mais detalhes, consulte as seções 3 e 4 da especificação vinculada acima.

Modelo de posição

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 origem pode ser transmitida por um AudioPannerNode, que espacializa o áudio de entrada. O nó do panner tem posição e orientação, além de um modelo de distância e direção.

O modelo de distância especifica a quantidade de ganho dependendo da proximidade à fonte, enquanto o modelo direcional pode ser configurado especificando um cone interno e externo, que determinam a quantidade de ganho (geralmente negativo) se o listener 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 esteja em 2D, esse modelo é facilmente generalizado para a terceira dimensão. Para conferir um exemplo de som espacial em 3D, consulte esta amostra posicional. Além da posição, o modelo de som de áudio da Web também inclui opcionalmente a velocidade para mudanças doppler. Este exemplo mostra o efeito Doppler com mais detalhes.

Para mais informações sobre esse tópico, leia este tutorial detalhado sobre [como misturar á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 rangente vai soar muito diferente em um porão, em comparação com um grande salão aberto. Jogos com alto valor de produção vão querer imitar esses efeitos, já que a criação de um conjunto separado de amostras para cada ambiente é proibitivamente caro, e levaria a ainda mais recursos e uma quantidade maior de dados do jogo.

Em termos gerais, o termo de áudio para a diferença entre o som bruto e a forma como ele soa na realidade é a resposta de impulso. Essas respostas ao impulso podem ser gravadas meticulosamente e, na verdade, há 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.

Mais importante para nossos propósitos, a API Web Audio oferece uma maneira fácil de aplicar essas respostas de 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ção da API Web Audio e este exemplo, que permite controlar a mixagem seca (crua) e molhada (processada por convolução) de um grande padrão de jazz.

A contagem regressiva final

Você criou um jogo, configurou seu á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 sobrepõem sem normalização, você pode se encontrar em uma situação em que está excedendo o limite da 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 ultrapassar o limite máximo, produzindo uma distorção distinta. A forma de onda é mais ou menos assim:

Clipe

Confira um exemplo real de recorte em ação. A forma de onda está ruim:

Corte

É importante ouvir distorções fortes 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, é melhor corrigir isso.

Detectar clipes

Do ponto de vista técnico, o clipping acontece quando o valor do sinal em qualquer canal excede o intervalo válido, ou seja, entre -1 e 1. Depois que isso é detectado, é útil dar um feedback visual de que isso está acontecendo. Para fazer isso de maneira confiável, coloque um JavaScriptAudioNode no seu gráfico. O gráfico de áudio seria configurado da seguinte maneira:

// 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 recorte 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, tenha cuidado para não usar demais o JavaScriptAudioNode por motivos de desempenho. Nesse caso, uma implementação alternativa de medição poderia consultar um RealtimeAnalyserNode no gráfico de áudio para getByteFrequencyData, no momento da renderização, conforme determinado por requestAnimationFrame. Essa abordagem é mais eficiente, mas deixa passar a maior parte do sinal (incluindo lugares em que ele pode ser recortado), 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 vejamos um nó integrado MeterNode da API Web Audio no futuro.

Evitar clipes

Ao ajustar o ganho no AudioGainNode mestre, você pode atenuar a mistura para um nível que impeça o recorte. No entanto, na prática, como os sons que tocam no jogo podem depender de uma grande variedade de fatores, pode ser difícil decidir o valor de ganho principal que impede o clipping para todos os estados. Em geral, é preciso ajustar os ganhos para antecipar o pior cenário, 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 pelo DynamicsCompressorNode, que pode ser inserido no gráfico de áudio para proporcionar um som mais alto, rico e completo, além de ajudar no clipping. Citando diretamente a especificação, este nó

Geralmente, usar a compactação dinâmica é uma boa ideia, especialmente em um ambiente de jogos em que, como discutido anteriormente, você não sabe exatamente quais sons vão tocar e quando. O Plink da DinahMoe Labs é um ótimo exemplo disso, já que os sons que são reproduzidos dependem completamente de você e dos outros participantes. Um compressor é útil na maioria dos casos, exceto em alguns raros, em que você está lidando com faixas masterizadas com muito 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.

Para resumir, preste atenção aos recortes e evite-os inserindo um nó de ganho mestre. Em seguida, apertando toda a combinação usando um nó do compressor dinâmico. Seu gráfico de áudio pode ser parecido com este:

Resultado final

Conclusão

Isso aborda 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 interessantes diretamente no navegador. Antes de encerrar, gostaria de dar uma dica específica do navegador: interrompa o som se a guia for para o segundo plano usando a API de visibilidade da página. Caso contrário, você vai criar uma experiência frustrante para o usuário.

Para mais informações sobre o Web Audio, consulte o artigo introdutório sobre como começar. Se você tiver alguma pergunta, confira se ela já foi respondida nas perguntas frequentes sobre áudio na Web. Por fim, se você tiver outras dúvidas, use a tag web-audio no Stack Overflow (link em inglês).

Antes de encerrar, vou deixar algumas formas incríveis de usar a API Web Audio em jogos reais: