Mélanger l'audio positionnel et WebGL

Ilmari Heikkinen

Introduction

Dans cet article, je vais vous expliquer comment utiliser la fonctionnalité de son positionnel de l'API Web Audio pour ajouter un son 3D à vos scènes WebGL. Pour rendre l'audio plus crédible, je vous présenterai également les effets environnementaux possibles avec l'API Web Audio. Pour une présentation plus complète de l'API Web Audio, consultez l'article Getting started with Web Audio API (Premiers pas avec l'API Web Audio) de Boris Smus.

Pour réaliser du son positionnel, utilisez 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 comporte un attribut d'écouteur qui vous permet de définir la position, l'orientation et la vitesse de l'écouteur. Grâce à ces deux éléments, vous pouvez 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 d'API Audio très basique. Vous allez créer plusieurs nœuds de l'API Audio et les connecter entre eux. Les nœuds audio sont des sons individuels, des commandes de volume, des nœuds d'effets, des analyseurs, etc. Une fois ce graphique créé, vous devez l'associer à la destination de 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();

Poste

Le son positionnel utilise la position de vos sources audio et celle de l'écouteur pour déterminer comment mixer le son sur les enceintes. Une source audio située à gauche de l'écouteur aurait un son plus fort dans l'enceinte de gauche, et inversement pour l'enceinte de droite.

Pour commencer, créez une source audio et associez-la à un AudioPannerNode. Définissez ensuite la position de AudioPannerNode. Vous pouvez désormais déplacer le son en 3D. Par défaut, la position de l'écouteur de contexte audio est à (0,0,0). Par conséquent, lorsqu'elle est utilisée de cette manière, la position d'AudioPannerNode dépend de la position de la caméra. Chaque fois que vous déplacez la caméra, vous devez modifier la position d'AudioPannerNode. Pour ajuster la position d'AudioPannerNode par rapport au monde, vous devez modifier la position de l'écouteur de contexte audio sur la position de la caméra.

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

Sur 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 celle du contexte audio de sorte qu'elle corresponde à celle 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 disposons des positions de l'écouteur et de l'AudioPannerNode, penchons-nous sur 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 bons exemples d'effets Doppler sur la page des exemples de l'API Web Audio.

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

Le suivi de la vitesse peut être effectué en obtenant la position précédente de l'objet, en le soustrayant de la position actuelle et en divisant le résultat par le temps écoulé depuis la dernière image. 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);
...

Intégration

L'orientation correspond à la direction vers laquelle pointe la source audio et à l'utilisateur. L'orientation vous permet de simuler des sources sonores directionnelles. Prenons l'exemple d'un locuteur directionnel. Le son sera plus fort si vous vous tenez devant l'enceinte que si vous vous tenez derrière elle. Plus important encore, vous avez besoin de l'orientation de l'auditeur pour déterminer de quel côté de l'auditeur proviennent les sons. Un son venant de votre gauche doit basculer vers la droite lorsque vous faites demi-tour.

Pour obtenir le vecteur d'orientation de l'AudioPannerNode, vous devez utiliser la partie de rotation de la matrice de modèle de l'objet 3D émettant du son et multiplier une valeur vec3(0,0,1) avec celle-ci pour voir où elle pointe. Pour l'orientation de l'écouteur de contexte, vous devez obtenir le vecteur d'orientation de l'appareil photo. L'orientation de l'écouteur a également besoin d'un vecteur vers le haut, car il doit connaître l'angle de retournement de la tête de l'écouteur. Pour calculer l'orientation de l'écouteur, obtenez la partie rotation de la matrice de vue de la caméra et multipliez par vec3(0,0,1) l'orientation et par vec3(0,-1,0) pour le vecteur vertical.

Pour que l'orientation ait un effet sur les sons, vous devez également définir le cône correspondant au son. Le cône sonore prend un angle intérieur, un angle extérieur et un gain extérieur. Le son est émis à un volume normal à l'intérieur de l'angle intérieur et progresse vers le gain extérieur à mesure que vous vous approchez de l'angle extérieur. En dehors de l'angle extérieur, le son est émis lors de gainage 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 translation des matrices mondiales 4x4. Pourtant, il n'y a 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 haut. Vous devez donc multiplier un vecteur 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 de son pour votre son, vous devez définir les propriétés appropriées du nœud de panneau. Les angles des cônes sont exprimés en degrés et compris entre 0 et 360.

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

Tous ensemble

En somme, l'écouteur de contexte audio suit la position, l'orientation et la vitesse de la caméra, tandis que les AudioPannerNodes suivent les positions, l'orientation et les vitesses de leurs sources audio respectives. Vous devez modifier les positions, les vitesses et les orientations des AudioPannerNodes et de l'écouteur de contexte audio sur chaque image.

Effets sur l'environnement

Une fois que vous avez configuré l'audio de position, vous pouvez définir des effets environnementaux pour le son afin d'améliorer l'immersion dans votre scène 3D. Supposons que votre scène se déroule dans une grande cathédrale. Avec les paramètres par défaut, le son émis ressemble à un lieu à l'extérieur. Ce décalage entre l'image et l'audio rompt l'immersion et rend la scène moins impressionnante.

L'API Web Audio dispose d'un ConvolverNode qui vous permet de définir l'effet environnemental pour un son. Ajoutez-le au graphique de traitement de la source audio pour adapter le son au paramètre défini. Vous trouverez sur le Web des exemples de réponses impulsives que vous pouvez utiliser avec les ConvolverNodes, ou que vous pouvez créer les vôtres. L'enregistrement de la réponse impulsive du lieu que vous souhaitez simuler peut s'avérer un peu fastidieux, mais la capacité est disponible en cas de besoin.

L'utilisation de ConvolverNodes pour l'audio de l'environnement nécessite de raccorder le graphique de traitement audio. Au lieu de transmettre le son directement au volume principal, vous devez le faire passer par le ConvolverNode. Et comme vous souhaitez peut-être contrôler l'intensité de l'effet environnemental, vous devez également acheminer le son autour du ConvolverNode. Pour contrôler le volume du mixage, des GainNodes doivent être associés au ConvolverNode et au son.

Le dernier graphique de traitement audio que j'utilise présente le contenu audio des objets qui transitent par un GainNode utilisé comme mélangeur pass-through. À partir du mélangeur, je transmets le son dans le ConvolverNode et un autre GainNode, qui permettent de contrôler le volume de l'audio. Le ConvolverNode est relié à son propre GainNode pour contrôler le volume audio associé. 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 le ConvolverNode fonctionne, vous devez charger un échantillon de réponse impulsive dans un tampon et faire en sorte que le ConvolverNode l'utilise. Le chargement de l'échantillon s'effectue de la même manière que pour les échantillons audio normaux. Vous trouverez ci-dessous un exemple de méthode:

...
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 du son positionnel à 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 configurant ces éléments 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 le ConvolverNode dans l'API Web Audio afin de configurer le son général de l'environnement. Des cathédrales aux salles fermées, vous pouvez simuler différents effets et environnements à l'aide de l'API Web Audio.

Références