Mélanger l'audio positionnel et WebGL

Ilmari Heikkinen

Introduction

Dans cet article, je vais vous expliquer comment utiliser la fonctionnalité audio positionnelle de l'API Web Audio pour ajouter du son 3D à vos scènes WebGL. Pour rendre l'audio plus crédible, je vais également vous présenter les effets environnementaux possibles avec l'API Web Audio. Pour une présentation plus détaillée de l'API Web Audio, consultez l'article Premiers pas avec l'API Web Audio de Boris Smus.

Pour créer un son spatialisé, vous devez utiliser AudioPannerNode dans l'API Web Audio. AudioPannerNode définit la position, l'orientation et la vitesse d'un son. De plus, le contexte audio de l'API Web Audio possède un attribut d'écouteur qui vous permet de définir la position, l'orientation et la vitesse de l'écouteur. Ces deux éléments vous permettent de créer des sons directionnels avec des effets Doppler et un panoramique 3D.

Voyons à quoi ressemble le code audio de la scène ci-dessus. Il s'agit d'un code API Audio très basique. Vous créez un certain nombre de nœuds d'API Audio et les connectez entre eux. Les nœuds audio sont des sons individuels, des contrôleurs de volume, des nœuds d'effets et des analyseurs, etc. Une fois ce graphique créé, vous devez l'associer à la destination du contexte audio pour le rendre 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();

Position

L'audio positionnel utilise la position de vos sources audio et celle de l'auditeur pour déterminer comment mixer le son sur les enceintes. Une source audio située à gauche de l'auditeur sera plus forte dans l'enceinte de gauche, et vice-versa pour le côté droit.

Pour commencer, créez une source audio et associez-la à un AudioPannerNode. Définissez ensuite la position de l'AudioPannerNode. Vous disposez maintenant d'un son 3D mobile. La position de l'écouteur du contexte audio est (0,0,0) par défaut. Par conséquent, lorsqu'il est utilisé de cette manière, la position d'AudioPannerNode est relative à la position de la caméra. Chaque fois que vous déplacez la caméra, vous devez modifier la position d'AudioPannerNode. Pour que la position du AudioPannerNode soit relative au monde, vous devez remplacer la position de l'écouteur de contexte audio par celle de votre caméra.

Pour configurer le suivi de position, nous devons créer un AudioPannerNode et le connecter au 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);
...

À chaque image, mettez à jour les positions des AudioPannerNodes. Je vais utiliser Three.js dans les exemples ci-dessous.

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

Pour suivre la position de l'écouteur, définissez la position de l'écouteur du contexte audio sur la position de la caméra.

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

Vélocité

Maintenant que nous avons les positions de l'écouteur et du AudioPannerNode, tournons-nous vers leurs vitesses. En modifiant les propriétés de vitesse de l'écouteur et de l'AudioPannerNode, vous pouvez ajouter un effet Doppler au son. Vous trouverez de beaux exemples d'effet Doppler sur la page d'exemples de l'API Web Audio.

Le moyen le plus simple d'obtenir les vitesses de l'écouteur et de l'AudioPannerNode consiste à suivre leurs positions par frame. La vitesse de l'écouteur correspond à la position actuelle de la caméra moins sa position dans le frame précédent. De même, la vitesse de l'AudioPannerNode correspond à sa position actuelle moins sa position précédente.

Pour suivre la vitesse, vous pouvez obtenir la position précédente de l'objet, la soustraire de la position actuelle, puis diviser le résultat par le temps écoulé depuis le dernier frame. Voici comment procéder dans 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);
...

Orientation

L'orientation correspond à la direction vers laquelle pointe la source sonore et à la direction vers laquelle l'auditeur est orienté. L'orientation vous permet de simuler des sources sonores directionnelles. Prenons l'exemple d'un haut-parleur directionnel. Si vous vous tenez devant l'enceinte, le son sera plus fort que si vous vous tenez derrière elle. Plus important encore, vous avez besoin de l'orientation de l'écouteur pour déterminer de quel côté les sons proviennent. Un son provenant de votre gauche doit passer à droite lorsque vous vous retournez.

Pour obtenir le vecteur d'orientation de l'AudioPannerNode, vous devez prendre la partie de rotation de la matrice de modèle de l'objet 3D émettant du son et multiplier un vec3(0,0,1) par celui-ci pour voir où il finit par pointer. Pour l'orientation de l'écouteur de contexte, vous devez obtenir le vecteur d'orientation de la caméra. L'orientation de l'écouteur nécessite également un vecteur vers le haut, car il doit connaître l'angle de roulis de la tête de l'écouteur. Pour calculer l'orientation de l'écouteur, obtenez la partie de rotation de la matrice de vue de la caméra et multipliez un vec3(0,0,1) pour l'orientation et un vec3(0,-1,0) pour le vecteur vers le haut.

Pour que l'orientation ait un effet sur vos sons, vous devez également définir le cône du son. Le cône sonore prend en compte un angle intérieur, un angle extérieur et un gain extérieur. Le son est diffusé au volume normal à l'intérieur de l'angle intérieur et passe progressivement du gain intérieur au gain extérieur lorsque vous vous approchez de l'angle extérieur. En dehors de l'angle extérieur, le son est diffusé avec un gain externe.

Le suivi de l'orientation dans Three.js est un peu plus délicat, car il implique des calculs vectoriels et la mise à zéro de la partie de traduction des matrices du monde 4x4. Il n'y a toujours pas beaucoup de lignes de code.

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

Le suivi de l'orientation de l'appareil photo nécessite également le vecteur vers le haut. Vous devez donc multiplier un vecteur vers le haut par la matrice de transformation.

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

Pour définir le cône sonore de votre son, vous devez définir les propriétés appropriées du nœud panner. Les angles du cône sont exprimés en degrés et vont de 0 à 360.

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

Tous ensemble

Pour résumer, l'écouteur de contexte audio suit la position, l'orientation et la vitesse de la caméra, et les AudioPannerNodes suivent les positions, les orientations et les vitesses de leurs sources audio respectives. Vous devez mettre à jour les positions, les vitesses et les orientations des AudioPannerNodes et de l'écouteur de contexte audio à chaque frame.

Effets sur l'environnement

Une fois l'audio spatial configuré, vous pouvez définir les effets environnementaux pour votre audio afin d'améliorer l'immersion de votre scène 3D. Supposons que votre scène se déroule dans une grande cathédrale. Par défaut, les sons de votre scène donnent l'impression que vous êtes à l'extérieur. Cet écart entre les visuels et l'audio rompt l'immersion et rend votre scène moins impressionnante.

L'API Web Audio dispose d'un ConvolverNode qui vous permet de définir l'effet environnemental d'un son. Ajoutez-le au graphique de traitement de la source audio pour adapter le son au paramètre. Vous pouvez trouver sur le Web des exemples de réponses impulsionnelles que vous pouvez utiliser avec ConvolverNodes. Vous pouvez également créer les vôtres. Cette expérience peut être un peu fastidieuse, car vous devez enregistrer la réponse impulsionnelle de l'endroit que vous souhaitez simuler. Toutefois, cette fonctionnalité est disponible si vous en avez besoin.

L'utilisation de ConvolverNodes pour créer des sons d'environnement nécessite de rebrancher le graphique de traitement audio. Au lieu de transmettre le son directement au volume principal, vous devez le router via ConvolverNode. Comme vous souhaitez peut-être contrôler l'intensité de l'effet environnemental, vous devez également acheminer l'audio autour du ConvolverNode. Pour contrôler les volumes de mixage, le ConvolverNode et l'audio brut doivent être associés à des GainNodes.

Le graphique de traitement audio final que j'utilise fait passer l'audio des objets via un GainNode utilisé comme mélangeur de transmission. À partir du mixeur, je transmets l'audio au ConvolverNode et à un autre GainNode, qui permet de contrôler le volume de l'audio brut. Le ConvolverNode est connecté à son propre GainNode pour contrôler le volume audio convolué. Les sorties des GainNodes sont connectées au contrôleur 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);
...

Pour que ConvolverNode fonctionne, vous devez charger un échantillon de réponse impulsionnelle dans un tampon et le faire utiliser par ConvolverNode. Le chargement de l'échantillon se fait de la même manière que pour les échantillons audio normaux. Vous trouverez ci-dessous un exemple de la façon dont vous pouvez procéder:

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

Résumé

Dans cet article, vous avez appris à ajouter un son spatial à vos scènes 3D à l'aide de l'API Web Audio. L'API Web Audio vous permet de définir la position, l'orientation et la vitesse des sources audio et de l'écouteur. En les configurant pour suivre les objets de votre scène 3D, vous pouvez créer un paysage sonore riche pour vos applications 3D.

Pour rendre l'expérience audio encore plus attrayante, vous pouvez utiliser ConvolverNode dans l'API Web Audio pour configurer le son général de l'environnement. Des cathédrales aux pièces fermées, vous pouvez simuler divers effets et environnements à l'aide de l'API Web Audio.

Références