Combinación de audio posicional y WebGL

Ilmari Heikkinen

Introducción

En este artículo, hablaré sobre cómo usar la función de audio posicional en la API de Web Audio para agregar sonido 3D a tus escenas de WebGL. Para que el audio sea más creíble, también te presentaré los efectos ambientales posibles con la API de Web Audio. Para obtener una introducción más detallada a la API de Web Audio, consulta el artículo Cómo comenzar a usar la API de Web Audio de Boris Smus.

Para crear audio posicional, usa AudioPannerNode en la API de Web Audio. AudioPannerNode define la posición, la orientación y la velocidad de un sonido. Además, el contexto de audio de la API de Web Audio tiene un atributo de objeto de escucha que te permite definir la posición, la orientación y la velocidad del objeto de escucha. Con estos dos elementos, puedes crear sonidos direccionales con efectos Doppler y panorámicas 3D.

Veamos cómo se ve el código de audio de la escena anterior. Este es un código de API de audio muy básico. Creas muchos nodos de la API de Audio y los conectas entre sí. Los nodos de audio son sonidos individuales, controladores de volumen, nodos de efectos y analizadores, entre otros. Después de compilar este gráfico, debes conectarlo al destino del contexto de audio para que sea audible.

// 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();

Posición

El audio posicional usa la posición de las fuentes de audio y la posición del usuario para determinar cómo mezclar el sonido con las bocinas. Una fuente de audio en el lado izquierdo del usuario sería más alta en la bocina izquierda, y viceversa para el lado derecho.

Para comenzar, crea una fuente de audio y conéctala a un AudioPannerNode. Luego, establece la posición del AudioPannerNode. Ahora tienes un sonido 3D móvil. De forma predeterminada, la posición del objeto de escucha de contexto de audio es (0,0,0), por lo que, cuando se usa de esta manera, la posición de AudioPannerNode es relativa a la de la cámara. Cada vez que muevas la cámara, deberás actualizar la posición de AudioPannerNode. Para que la posición de AudioPannerNode sea relativa al mundo, debes cambiar la posición del objeto de escucha de contexto de audio a la posición de la cámara.

Para configurar el seguimiento de posición, debemos crear un AudioPannerNode y conectarlo al volumen 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);
...

En cada fotograma, actualiza las posiciones de AudioPannerNodes. Usaré Three.js en los ejemplos a continuación.

...
// 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 hacer un seguimiento de la posición del objeto de escucha, configura la posición del objeto de escucha del contexto de audio para que coincida con la posición de la cámara.

...
// 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);
...

Velocidad

Ahora que tenemos las posiciones del objeto de escucha y el AudioPannerNode, centremos nuestra atención en sus velocidades. Si cambias las propiedades de velocidad del objeto de escucha y el AudioPannerNode, puedes agregar un efecto doppler al sonido. Hay algunos ejemplos interesantes del efecto Doppler en la página de ejemplos de la API de Web Audio.

La forma más fácil de obtener las velocidades del objeto de escucha y AudioPannerNode es hacer un seguimiento de sus posiciones por fotograma. La velocidad del objeto de escucha es la posición actual de la cámara menos la posición de la cámara en el fotograma anterior. Del mismo modo, la velocidad del AudioPannerNode es su posición actual menos su posición anterior.

Para hacer un seguimiento de la velocidad, se puede obtener la posición anterior del objeto, restarla de la posición actual y dividir el resultado por el tiempo transcurrido desde el último fotograma. A continuación, te mostramos cómo hacerlo en 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);
...

Orientación

La orientación es la dirección hacia la que apunta la fuente de sonido y la dirección hacia la que se orienta el objeto de escucha. Con la orientación, puedes simular fuentes de sonido direccionales. Por ejemplo, piensa en un orador direccional. Si te paras frente a la bocina, el sonido será más alto que si te paras detrás de ella. Lo más importante es que necesitas la orientación del objeto de escucha para determinar de qué lado del objeto provienen los sonidos. Un sonido que proviene de la izquierda debe cambiar a la derecha cuando te das la vuelta.

Para obtener el vector de orientación del AudioPannerNode, debes tomar la parte de rotación de la matriz del modelo del objeto 3D que emite sonido y multiplicar un vec3(0,0,1) con él para ver hacia dónde apunta. Para la orientación del objeto de escucha de contexto, debes obtener el vector de orientación de la cámara. La orientación del objeto de escucha también necesita un vector hacia arriba, ya que necesita conocer el ángulo de balanceo de la cabeza del objeto de escucha. Para calcular la orientación del objeto de escucha, obtén la parte de rotación de la matriz de vista de la cámara y multiplica un vec3(0,0,1) para la orientación y un vec3(0,-1,0) para el vector hacia arriba.

Para que la orientación tenga un efecto en los sonidos, también debes definir el cono del sonido. El cono de sonido tiene un ángulo interior, un ángulo exterior y una ganancia exterior. El sonido se reproduce con el volumen normal dentro del ángulo interior y cambia gradualmente la ganancia a la ganancia externa a medida que te acercas al ángulo externo. Fuera del ángulo externo, el sonido se reproduce con una ganancia externa.

Hacer un seguimiento de la orientación en Three.js es un poco más complicado, ya que implica algunos cálculos vectoriales y poner a cero la parte de traducción de las matrices del mundo 4 × 4. Sin embargo, no hay muchas líneas 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;
...

El seguimiento de la orientación de la cámara también requiere el vector hacia arriba, por lo que debes multiplicar un vector hacia arriba con la matriz de transformación.

...
// 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 establecer el cono de sonido, debes configurar las propiedades adecuadas del nodo panorámico. Los ángulos de cono se miden en grados y van de 0 a 360.

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

Todo junto

En resumen, el objeto de escucha de contexto de audio sigue la posición, la orientación y la velocidad de la cámara, y AudioPannerNodes sigue las posiciones, las orientaciones y las velocidades de sus respectivas fuentes de audio. Debes actualizar las posiciones, velocidades y orientaciones de AudioPannerNodes y el objeto de escucha de contexto de audio en cada fotograma.

Efectos ambientales

Después de configurar el audio posicional, puedes establecer los efectos ambientales para mejorar la capacidad de inmersión de tu escena 3D. Supongamos que tu escena se desarrolla dentro de una gran catedral. Con la configuración predeterminada, los sonidos de la escena suenan como si estuvieras al aire libre. Esta discrepancia entre las imágenes y el audio rompe la inmersión y hace que la escena sea menos impresionante.

La API de Web Audio tiene un ConvolverNode que te permite establecer el efecto ambiental de un sonido. Agrega el filtro al gráfico de procesamiento de la fuente de audio y podrás hacer que el sonido se ajuste a la configuración. Puedes encontrar muestras de respuestas por impulso en la Web que puedes usar con ConvolverNodes, o bien crear las tuyas. Puede ser una experiencia un poco engorrosa, ya que debes registrar la respuesta al impulso del lugar que deseas simular, pero la función está disponible si la necesitas.

El uso de ConvolverNodes para realizar audio ambiental requiere volver a conectar el gráfico de procesamiento de audio. En lugar de pasar el sonido directamente al volumen principal, debes enrutarlo a través de ConvolverNode. Y, como es posible que quieras controlar la intensidad del efecto ambiental, también debes enrutar el audio alrededor de ConvolverNode. Para controlar los volúmenes de la mezcla, el ConvolverNode y el audio sin procesar deben tener GainNodes adjuntos.

El gráfico de procesamiento de audio final que uso tiene el audio de los objetos que pasan por un GainNode que se usa como mezclador de transferencia. Desde el mezclador, paso el audio al ConvolverNode y a otro GainNode, que se usa para controlar el volumen del audio sin procesar. El ConvolverNode está conectado a su propio GainNode para controlar el volumen de audio convolute. Las salidas de los GainNodes se conectan al controlador de volumen 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 que funcione ConvolverNode, debes cargar una muestra de respuesta al impulso en un búfer y hacer que ConvolverNode la use. La carga de la muestra se realiza de la misma manera que con las muestras de sonido normales. A continuación, se muestra un ejemplo de una forma de hacerlo:

...
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();
}

Resumen

En este artículo, aprendiste a agregar audio posicional a tus escenas en 3D con la API de Web Audio. La API de Web Audio te brinda una manera de configurar la posición, la orientación y la velocidad de las fuentes de audio y el objeto de escucha. Al configurarlos para que realicen un seguimiento de los objetos en tu escena 3D, puedes crear un paisaje sonoro enriquecido para tus aplicaciones en 3D.

Para que la experiencia de audio sea aún más atractiva, puedes usar ConvolverNode en la API de Web Audio para configurar el sonido general del entorno. Desde catedrales hasta habitaciones cerradas, puedes simular una variedad de efectos y entornos con la API de Web Audio.

Referencias