Combinación de audio posicional y WebGL

Ilmari Heikkinen

Introducción

En este artículo, abordaré cómo usar la función de audio posicional en la API de Web Audio para agregar sonido 3D a tus escenas con 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 utilizar 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 audio web 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 desplazamiento lateral en 3D.

Veamos cómo se ve el código de audio para la escena anterior. Este es un código de API de Audio muy básico. Crearás muchos nodos de la API de Audio y los conectarás 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();

Cargo

El audio posicional usa la posición de tus fuentes de audio y la del objeto de escucha para determinar cómo mezclar el sonido con los altavoces. Una fuente de audio del lado izquierdo del objeto de escucha sería más fuerte en el altavoz izquierdo y viceversa para el lado derecho.

Para comenzar, crea una fuente de audio y adjúntala a un objeto AudioPannerNode. Luego, establece la posición del AudioPannerNode. Ahora tienes un sonido 3D que se puede mover. La posición del objeto de escucha del contexto de audio es (0,0,0) de forma predeterminada, por lo que, cuando se usa de esta manera, la posición de AudioPannerNode es relativa a la posición de la cámara. Cada vez que muevas la cámara, debes actualizar la posición de AudioPannerNode. Para hacer que la posición de AudioPannerNode sea relativa al mundo, debes cambiar la posición del objeto de escucha del contexto de audio a la posición de tu cámara.

Para configurar el seguimiento de posición, debemos crear un objeto 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 los AudioPannerNodes. Usaré Three.js en los ejemplos que aparecen 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 realizar 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 del AudioPannerNode, centremos nuestra atención en sus velocidades. Al cambiar las propiedades de velocidad del objeto de escucha y del AudioPannerNode, puedes agregar un efecto doppler al sonido. Puedes encontrar algunos ejemplos útiles 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 el AudioPannerNode es realizar 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. De manera similar, 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, se indica 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 a la que apunta la fuente de sonido y la dirección hacia la que apunta el objeto de escucha. Con la orientación, puedes simular fuentes de sonido direccionales. Por ejemplo, piensa en una bocina direccional. Si te paras frente a la bocina, el sonido será más alto que si te paras detrás de la bocina. Lo que es más importante, necesitas la orientación del oyente para determinar de qué lado provienen los sonidos. Un sonido procedente de la izquierda deberá cambiar a la derecha cuando gires.

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 ella 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) por la orientación y vec3(0,-1,0) para el vector superior.

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

Hacer un seguimiento de la orientación en Three.js es un poco más complicado, ya que implica algunas matemáticas vectoriales y poner a cero la parte de traducción de las matrices de mundos 4x4. De todas formas, 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 arriba, por lo que debes multiplicar este vector arriba por 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 configurar el cono de sonido para tu sonido, debes configurar las propiedades correspondientes del nodo de desplazamiento lateral. Los ángulos del cono están 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 del contexto de audio sigue la posición, la orientación y la velocidad de la cámara, y los AudioPannerNodes siguen las posiciones, las orientaciones y las velocidades de sus respectivas fuentes de audio. Debes actualizar las posiciones, velocidades y orientaciones de los AudioPannerNodes y el objeto de escucha del contexto de audio en cada fotograma.

Efectos ambientales

Después de configurar el audio posicional, puedes establecer los efectos ambientales del audio para mejorar la experiencia envolvente de la escena 3D. Supongamos que la escena está ambientada en el interior de una gran catedral. Según 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 configurar el efecto ambiental de un sonido. Agrégalo al gráfico de procesamiento de la fuente de audio y logra que el sonido se ajuste a la configuración. Puedes encontrar muestras de respuestas impulsivas en la Web que puedes usar con ConvolverNodes, y también puedes crear las tuyas. Puede ser una experiencia un poco engorrosa, ya que debes registrar la respuesta impulsiva del lugar que deseas simular, pero la capacidad está allí 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 dirigirlo a través de ConvolverNode. Como te recomendamos controlar la intensidad del efecto ambiental, también debes dirigir el audio alrededor del ConvolverNode. Para controlar los volúmenes de mezcla, el ConvolverNode y el audio sin formato deben tener GainNodes adjuntos.

El gráfico final de procesamiento de audio que estoy usando tiene el audio de los objetos que pasan por un GainNode utilizado como mezclador de transferencia. Desde el mezclador, paso el audio al ConvolverNode y otro GainNode, que se usa para controlar el volumen del audio sin formato. El ConvolverNode está conectado a su propio GainNode para controlar el volumen del audio convolución. 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 ConvolverNode funcione, debes cargar una muestra de respuesta impulsiva en un búfer y hacer que el ConvolverNode la use. La carga de la muestra se realiza de la misma manera que con las muestras de sonido normales. A continuación, te mostramos un ejemplo de una manera 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 3D con la API de Web Audio. La API de Web Audio te permite configurar la posición, la orientación y la velocidad de las fuentes de audio y del objeto de escucha. Al configurarlos para realizar un seguimiento de los objetos en tu escena 3D, puedes crear un paisaje sonoro de calidad para tus aplicaciones 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. Con la API de Web Audio, puedes simular una variedad de efectos y entornos, desde catedrales hasta salas cerradas.

Referencias