Como combinar áudio posicional e WebGL

Ilmari Heikkinen

Introdução

Neste artigo, vou mostrar como usar o recurso de áudio posicional na API Web Audio para adicionar som 3D às cenas WebGL. Para tornar o áudio mais convincente, também apresentarei os efeitos ambientais possíveis com a API de áudio da Web. Para obter uma introdução mais completa à API de áudio da Web, confira o artigo Introdução à API de áudio da Web de Boris Smus.

Para reproduzir áudio posicional, use o AudioPannerNode na API Web Audio. O AudioPannerNode define a posição, orientação e velocidade de um som. Além disso, o contexto de áudio da API de áudio da Web tem um atributo de listener que permite definir a posição, orientação e velocidade do listener. Com esses dois recursos, você pode criar sons direcionais com efeitos doppler e panorâmica em 3D.

Vamos ver como é o código de áudio da cena acima. Este é um código muito básico da API de áudio. Você vai criar vários nós da API de áudio e conectá-los. Os nós de áudio são sons individuais, controladores de volume, nós de efeito, analisadores e similares. Depois de criar esse gráfico, é preciso conectá-lo ao destino do contexto de áudio para torná-lo audível.

// Detect if the audio context is supported.
window.AudioContext = (
  window.AudioContext ||
  window.webkitAudioContext ||
  null
);

if (!AudioContext) {
  throw new Error("AudioContext not supported!");
} 

// Create a new audio context.
var ctx = new AudioContext();

// Create a AudioGainNode to control the main volume.
var mainVolume = ctx.createGain();
// Connect the main volume node to the context destination.
mainVolume.connect(ctx.destination);

// Create an object with a sound source and a volume control.
var sound = {};
sound.source = ctx.createBufferSource();
sound.volume = ctx.createGain();

// Connect the sound source to the volume control.
sound.source.connect(sound.volume);
// Hook up the sound volume control to the main volume.
sound.volume.connect(mainVolume);

// Make the sound source loop.
sound.source.loop = true;

// Load a sound file using an ArrayBuffer XMLHttpRequest.
var request = new XMLHttpRequest();
request.open("GET", soundFileName, true);
request.responseType = "arraybuffer";
request.onload = function(e) {

  // Create a buffer from the response ArrayBuffer.
  ctx.decodeAudioData(this.response, function onSuccess(buffer) {
    sound.buffer = buffer;

    // Make the sound source use the buffer and start playing it.
    sound.source.buffer = sound.buffer;
    sound.source.start(ctx.currentTime);
  }, function onFailure() {
    alert("Decoding the audio buffer failed");
  });
};
request.send();

Cargo

O áudio posicional usa a posição das suas fontes de áudio e do listener para determinar como mixar o som nos alto-falantes. Uma fonte de áudio no lado esquerdo do listener seria mais alta no alto-falante esquerdo e vice-versa no lado direito.

Para começar, crie uma fonte de áudio e anexe-a a um AudioPannerNode. Depois, defina a posição do AudioPannerNode. Agora você tem um som 3D móvel. A posição do listener de contexto de áudio é em (0,0,0) por padrão. Quando usada dessa forma, a posição do AudioPannerNode é relativa à posição da câmera. Sempre que você mover a câmera, será necessário atualizar a posição do AudioPannerNode. Para deixar a posição do AudioPannerNode em relação ao mundo, mude a posição do listener de contexto de áudio para a posição da câmera.

Para configurar o rastreamento de posição, precisamos criar um AudioPannerNode e conectá-lo ao volume principal.

...
sound.panner = ctx.createPanner();
// Instead of hooking up the volume to the main volume, hook it up to the panner.
sound.volume.connect(sound.panner);
// And hook up the panner to the main volume.
sound.panner.connect(mainVolume);
...

Em cada frame, atualize as posições dos AudioPannerNodes. Vou usar o three.js nos exemplos abaixo.

...
// In the frame handler function, get the object's position.
object.position.set(newX, newY, newZ);
object.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);

// And copy the position over to the sound of the object.
sound.panner.setPosition(p.x, p.y, p.z);
...

Para rastrear a posição do listener, defina a posição do listener do contexto de áudio para que corresponda à posição da câmera.

...
// Get the camera position.
camera.position.set(newX, newY, newZ);
camera.updateMatrixWorld();
var p = new THREE.Vector3();
p.setFromMatrixPosition(camera.matrixWorld);

// And copy the position over to the listener.
ctx.listener.setPosition(p.x, p.y, p.z);
...

Velocidade

Agora que já temos as posições do listener e do AudioPannerNode, vamos nos concentrar nas velocidades deles. Ao alterar as propriedades de velocidade do listener e do AudioPannerNode, é possível adicionar um efeito doppler ao som. Há alguns bons exemplos de efeito doppler na página de exemplos da API Web Audio.

A maneira mais fácil de descobrir as velocidades do listener e do AudioPannerNode é monitorar as posições por frame. A velocidade do listener é a posição atual da câmera menos a posição dela no frame anterior. Da mesma forma, a velocidade do AudioPannerNode é a posição atual menos a posição anterior.

O rastreamento da velocidade pode ser feito obtendo a posição anterior do objeto, subtraindo-a da posição atual e dividindo o resultado pelo tempo decorrido desde o último frame. Veja como fazer isso no Three.js:

...
var dt = secondsSinceLastFrame;

var p = new THREE.Vector3();
p.setFromMatrixPosition(object.matrixWorld);
var px = p.x, py = p.y, pz = p.z;

object.position.set(newX, newY, newZ);
object.updateMatrixWorld();

var q = new THREE.Vector3();
q.setFromMatrixPosition(object.matrixWorld);
var dx = q.x-px, dy = q.y-py, dz = q.z-pz;

sound.panner.setPosition(q.x, q.y, q.z);
sound.panner.setVelocity(dx/dt, dy/dt, dz/dt);
...

Orientações

A orientação é para onde a fonte sonora está apontando e para onde o listener está. Com a orientação, você pode simular fontes de som direcionais. Por exemplo, pense em um alto-falante direcional. Se você ficar em frente ao alto-falante, o som será mais alto do que se você ficar atrás dele. Mais importante, você precisa da orientação do ouvinte para determinar de qual lado dele os sons estão vindo. Quando você virar, um som vindo da esquerda precisa mudar para a direita.

Para encontrar o vetor de orientação do AudioPannerNode, você precisa multiplicar a parte de rotação da matriz do objeto 3D que emite som e multiplicar um vec3(0,0,1) com ela para saber para onde o objeto está apontando. Para a orientação do listener de contexto, é necessário extrair o vetor de orientação da câmera. A orientação do listener também precisa de um vetor para cima, já que precisa saber o ângulo de rolagem da cabeça do listener. Para computar a orientação do listener, acesse a parte de rotação da matriz de visualização da câmera e multiplique um vec3(0,0,1) para a orientação e um vec3(0,-1,0) para o vetor superior.

Para que a orientação tenha efeito nos seus sons, você também precisa definir o cone para o som. O cone de som tem um ângulo interno, um ângulo externo e um ganho externo. O som é reproduzido no volume normal dentro do ângulo interno e muda gradualmente para o ganho externo à medida que você se aproxima do ângulo externo. Fora do ângulo externo, o som é reproduzido com o ganho externo.

Acompanhar a orientação no Three.js é um pouco mais complicado, porque envolve alguns cálculos vetoriais e zerar a parte de translação das matrizes mundiais 4x4. Ainda assim, não há muitas linhas de código.

...
var vec = new THREE.Vector3(0,0,1);
var m = object.matrixWorld;

// Save the translation column and zero it.
var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the 0,0,1 vector by the world matrix and normalize the result.
vec.applyProjection(m);
vec.normalize();

sound.panner.setOrientation(vec.x, vec.y, vec.z);

// Restore the translation column.
m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

O rastreamento da orientação da câmera também exige o vetor para cima. Portanto, é necessário multiplicar um vetor para cima com a matriz de transformação.

...
// The camera's world matrix is named "matrix".
var m = camera.matrix;

var mx = m.elements[12], my = m.elements[13], mz = m.elements[14];
m.elements[12] = m.elements[13] = m.elements[14] = 0;

// Multiply the orientation vector by the world matrix of the camera.
var vec = new THREE.Vector3(0,0,1);
vec.applyProjection(m);
vec.normalize();

// Multiply the up vector by the world matrix.
var up = new THREE.Vector3(0,-1,0);
up.applyProjection(m);
up.normalize();

// Set the orientation and the up-vector for the listener.
ctx.listener.setOrientation(vec.x, vec.y, vec.z, up.x, up.y, up.z);

m.elements[12] = mx;
m.elements[13] = my;
m.elements[14] = mz;
...

Para definir o cone de som do seu som, defina as propriedades adequadas do nó do panner. Os ângulos do cone estão em graus e vão de 0 a 360.

...
sound.panner.coneInnerAngle = innerAngleInDegrees;
sound.panner.coneOuterAngle = outerAngleInDegrees;
sound.panner.coneOuterGain = outerGainFactor;
...

Tudo em um só lugar

Juntando tudo isso, o listener de contexto de áudio segue a posição, orientação e velocidade da câmera, e os AudioPannerNodes seguem as posições, orientações e velocidades de suas respectivas fontes de áudio. Você precisa atualizar as posições, velocidades e orientações dos AudioPannerNodes e o listener de contexto de áudio em cada frame.

Efeitos ambientais

Depois de configurar o áudio posicional, você pode definir os efeitos ambientais do áudio para aumentar a imersão da sua cena 3D. Imagine que a cena se passa dentro de uma grande catedral. Nas configurações padrão, os sons na sua cena são como se você estivesse ao ar livre. Essa discrepância entre o visual e o áudio interrompe a imersão e torna sua cena menos impressionante.

A API Web Audio tem um ConvolverNode que permite definir o efeito ambiental de um som. Adicione-o ao gráfico de processamento da fonte de áudio para fazer com que o som se ajuste à configuração. É possível encontrar amostras de respostas a impulsos na Web que podem ser usadas com o ConvolverNodes ou criar as suas próprias. Pode ser uma experiência um pouco complicada, já que é preciso gravar a resposta ao impulso do lugar que você quer simular, mas o recurso está disponível se você precisar.

O uso do ConvolverNodes para produzir áudio de ambiente requer a reconexão do gráfico de processamento de áudio. Em vez de transmitir o som diretamente para o volume principal, você precisa roteá-lo pelo ConvolverNode. E, como você pode querer controlar a intensidade do efeito ambiental, também precisa rotear o áudio em torno do ConvolverNode. Para controlar os volumes de mix, o ConvolverNode e o áudio simples precisam ter GainNodes anexados a eles.

O gráfico final de processamento de áudio que estou usando tem o áudio dos objetos que passam por um GainNode usado como um mixer de passagem. Do mixer, passo o áudio para o ConvolverNode e outro GainNode, que é usado para controlar o volume do áudio simples. O ConvolverNode está conectado ao próprio GainNode para controlar o volume de áudio convolucional. As saídas dos GainNodes estão conectadas ao controlador de volume principal.

...
var ctx = new webkitAudioContext();
var mainVolume = ctx.createGain();

// Create a convolver to apply environmental effects to the audio.
var convolver = ctx.createConvolver();

// Create a mixer that receives sound from the panners.
var mixer = ctx.createGain();

sounds.forEach(function(sound){
  sound.panner.connect(mixer);
});

// Create volume controllers for the plain audio and the convolver.
var plainGain = ctx.createGain();
var convolverGain = ctx.createGain();

// Send audio from the mixer to plainGain and the convolver node.
mixer.connect(plainGain);
mixer.connect(convolver);

// Hook up the convolver to its volume control.
convolver.connect(convolverGain);

// Send audio from the volume controls to the main volume control.
plainGain.connect(mainVolume);
convolverGain.connect(mainVolume);

// Finally, connect the main volume to the audio context's destination.
volume.connect(ctx.destination);
...

Para fazer o ConvolverNode funcionar, é preciso carregar uma amostra de resposta a impulsos em um buffer e fazer com que o ConvolverNode a use. O carregamento das amostras acontece da mesma maneira que com amostras de som normais. Confira abaixo um exemplo de como fazer isso:

...
loadBuffer(ctx, "impulseResponseExample.wav", function(buffer){
  convolver.buffer = buffer;
  convolverGain.gain.value = 0.7;
  plainGain.gain.value = 0.3;
})
...
function loadBuffer(ctx, filename, callback) {
  var request = new XMLHttpRequest();
  request.open("GET", soundFileName, true);
  request.responseType = "arraybuffer";
  request.onload = function() {
    // Create a buffer and keep the channels unchanged.
    ctx.decodeAudioData(request.response, callback, function() {
      alert("Decoding the audio buffer failed");
    });
  };
  request.send();
}

Resumo

Neste artigo, você aprendeu como adicionar áudio posicional às cenas 3D usando a API Web Audio. A API de áudio da Web oferece uma maneira de definir a posição, orientação e velocidade das fontes de áudio e do listener. Ao configurá-los para rastrear os objetos na cena 3D, você cria uma paisagem sonora rica para seus aplicativos 3D.

Para tornar a experiência de áudio ainda mais atraente, você pode usar o ConvolverNode na API de áudio da Web para configurar o som geral do ambiente. De catedrais a salas fechadas, é possível simular diversos efeitos e ambientes usando a API Web Audio.

Referências