Introdução
Neste artigo, vou falar sobre como usar o recurso de áudio posicional na API Web Audio para adicionar som 3D às suas cenas WebGL. Para tornar o áudio mais verossímil, também vou apresentar os efeitos ambientais possíveis com a API Web Audio. Para uma introdução mais detalhada à API Web Audio, consulte o artigo Como começar a usar a API Web Audio de Boris Smus.
Para fazer á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 Web Audio tem um atributo listener que permite definir a posição, a orientação e a velocidade do listener. Com essas duas coisas, você pode criar sons direcionais com efeitos Doppler e panorâmica 3D.
Vamos conferir como é o código de áudio para a cena acima. Este é um código muito básico da API Audio. Você cria vários nós da API Audio e os conecta. Os nós de áudio são sons individuais, controles de volume, nós de efeitos e analisadores e assim por diante. Depois de criar o gráfico, é necessário 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();
Posição
O áudio posicional usa a posição das fontes de áudio e do ouvinte para determinar como mixar o som para os alto-falantes. Uma fonte de áudio no lado esquerdo do ouvinte seria mais alta no alto-falante esquerdo e vice-versa para o lado direito.
Para começar, crie uma fonte de áudio e anexe-a a um AudioPannerNode. Em seguida, defina a posição do AudioPannerNode. Agora você tem um som 3D móvel. A posição do listener de contexto de áudio é (0,0,0) por padrão. Portanto, quando usada dessa maneira, 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 fazer com que a posição do AudioPannerNode seja relativa 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 corresponder à 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 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, você pode adicionar um efeito doppler ao som. Há alguns exemplos legais do efeito Doppler na página de exemplos da API Web Audio.
A maneira mais fácil de conseguir as velocidades do listener e do AudioPannerNode é acompanhar as posições por frame. A velocidade do listener é a posição atual da câmera menos a posição da câmera 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ção
A orientação é a direção para onde a fonte de som está apontando e a direção em que o ouvinte está. Com a orientação, é possível simular fontes de som direcionais. Por exemplo, pense em um alto-falante direcional. Se você ficar na frente do 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 do ouvinte os sons estão vindo. Um som que vem da esquerda precisa mudar para a direita quando você se vira.
Para conseguir o vetor de orientação do AudioPannerNode, você precisa pegar a parte de rotação da matriz do modelo do objeto 3D que emite som e multiplicar um vec3(0,0,1) com ele para ver para onde ele aponta. Para a orientação do listener de contexto, é necessário acessar 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 inclinação da cabeça do listener. Para calcular a orientação do listener, extraia 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 para cima.
Para que a orientação tenha efeito nos sons, você também precisa definir o cone do som. O cone de som tem um ângulo interno, um ângulo externo e um ganho externo. O som toca 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 ganho externo.
Rastrear a orientação no Three.js é um pouco mais complicado, porque envolve alguns cálculos vetoriais e zerar a parte de tradução das matrizes mundiais 4 x 4. 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 de orientação da câmera também requer 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, defina as propriedades adequadas do nó de panner. Os ângulos do cone estão em graus e variam de 0 a 360.
...
sound.panner.coneInnerAngle = innerAngleInDegrees;
sound.panner.coneOuterAngle = outerAngleInDegrees;
sound.panner.coneOuterGain = outerGainFactor;
...
Todos juntos
Juntando tudo, o listener de contexto de áudio segue a posição, a orientação e a velocidade da câmera, e o AudioPannerNodes segue as posições, orientações e velocidades das respectivas fontes de áudio. É necessário atualizar as posições, velocidades e orientações dos AudioPannerNodes e do 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 melhorar a imersão da cena 3D. Suponha que a cena esteja definida dentro de uma grande catedral. Nas configurações padrão, os sons da cena parecem como se você estivesse do lado de fora. Essa discrepância entre os recursos visuais e o áudio quebra a imersão e torna a 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 ajustar o som à configuração. Você pode encontrar amostras de resposta de impulso na Web para usar com o ConvolverNodes e também criar as suas. Essa experiência pode ser um pouco complicada, porque você precisa gravar a resposta de impulso do lugar que quer simular, mas o recurso está disponível se você precisar.
O uso de ConvolverNodes para fazer áudio ambiental requer a reconfiguração do gráfico de processamento de áudio. Em vez de transmitir o som diretamente para o volume principal, você precisa encaminhar o som pelo ConvolverNode. Como você pode querer controlar a intensidade do efeito ambiental, também é necessário rotear o áudio em torno do ConvolverNode. Para controlar os volumes de mixagem, 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 passando por um GainNode usado como um mixer de passagem. No mixer, transmito o áudio para o ConvolverNode e outro GainNode, que é usado para controlar o volume do áudio simples. O ConvolverNode é conectado ao próprio GainNode para controlar o volume do áudio convolado. As saídas dos GainNodes sã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, você precisa carregar uma amostra de resposta ao impulso em um buffer e fazer com que o ConvolverNode a use. O carregamento da amostra acontece da mesma forma que as amostras de som normais. Veja abaixo um exemplo de uma maneira de 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 suas cenas 3D usando a API Web Audio. A API Web Audio oferece uma maneira de definir a posição, a orientação e a velocidade das fontes de áudio e do ouvinte. Ao defini-los para rastrear os objetos na cena 3D, você pode criar uma paisagem sonora rica para seus aplicativos 3D.
Para tornar a experiência de áudio ainda mais interessante, use o ConvolverNode na API Web Audio para configurar o som geral do ambiente. De catedrais a salas fechadas, é possível simular uma variedade de efeitos e ambientes usando a API Web Audio.
Referências
- Especificação da API Web Audio
- Resposta de impulso
- Three.js (link em inglês)
- Um exemplo legal de áudio posicional 3D
- Exemplos de áudio da Web: tem muitos exemplos de uso dos recursos da API Web Audio.