Positionales Audio und WebGL mischen

Ilmari Heikkinen

Einführung

In diesem Artikel erkläre ich, wie Sie die Funktion für Positionsaudio in der Web Audio API verwenden, um Ihren WebGL-Szenen 3D-Sound hinzuzufügen. Damit der Audiotrack echter klingt, stelle ich Ihnen auch die Umgebungseffekte vor, die mit der Web Audio API möglich sind. Eine ausführlichere Einführung in die Web Audio API finden Sie im Artikel Getting Started with Web Audio API von Boris Smus.

Für Positionsaudio wird der AudioPannerNode in der Web Audio API verwendet. Der AudioPannerNode definiert die Position, Ausrichtung und Geschwindigkeit eines Tons. Darüber hinaus verfügt der Web Audio API-Audiokontext über ein Listener-Attribut, mit dem Sie die Position, Ausrichtung und Geschwindigkeit des Listeners definieren können. Mit diesen beiden Elementen kannst du Richtungsgeräusche mit Dopplereffekten und 3D-Panoramaeffekten erstellen.

Sehen wir uns an, wie der Audiocode für die obige Szene aussieht. Dies ist sehr einfacher Audio API-Code. Sie erstellen mehrere Audio API-Knoten und verbinden sie miteinander. Die Audioknoten sind einzelne Töne, Lautstärkeregler, Effektknoten und Analysetools. Nachdem Sie diese Grafik erstellt haben, müssen Sie sie mit dem Audiokontextziel verknüpfen, um sie hörbar zu machen.

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

Bei der räumlichen Audiowiedergabe wird die Position der Audioquellen und die Position des Hörers verwendet, um zu bestimmen, wie der Ton auf die Lautsprecher verteilt wird. Eine Audioquelle auf der linken Seite des Zuhörers wäre im linken Lautsprecher lauter und umgekehrt für die rechte Seite.

Erstellen Sie zunächst eine Audioquelle und hängen Sie sie an einen AudioPannerNode an. Legen Sie dann die Position des AudioPannerNode fest. Jetzt haben Sie einen beweglichen 3D-Ton. Die Position des Listeners für den Audiokontext befindet sich standardmäßig bei (0,0,0), sodass die AudioPannerNode-Position bei dieser Verwendung relativ zur Kameraposition ist. Jedes Mal, wenn Sie die Kamera bewegen, muss die AudioPannerNode-Position aktualisiert werden. Um die AudioPannerNode-Position relativ zur Welt zu positionieren, müssen Sie die Position des Audiokontext-Listeners auf Ihre Kameraposition setzen.

Um das Position-Tracking einzurichten, müssen wir einen AudioPannerNode erstellen und mit dem Haupt-Volume verbinden.

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

Aktualisiere die Positionen der AudioPannerNodes in jedem Frame. In den folgenden Beispielen verwende ich Three.js.

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

Wenn du die Listenerposition erfassen möchtest, musst du die Listenerposition des Audiokontexts an die Kameraposition anpassen.

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

Geschwindigkeit

Nachdem wir nun die Positionen des Listeners und des AudioPannerNode kennen, wenden wir uns auf deren Geschwindigkeiten. Durch Ändern der Geschwindigkeitseigenschaften des Listeners und des AudioPannerNode können Sie dem Ton einen Doppler-Effekt hinzufügen. Auf der Beispielseite der Web Audio API finden Sie einige schöne Beispiele für den Dopplereffekt.

Am einfachsten kannst du die Geschwindigkeiten für den Zuhörer und den AudioPannerNode ermitteln, indem du ihre Positionsdaten pro Frame aufnimmst. Die Geschwindigkeit des Listeners ist die aktuelle Kameraposition abzüglich der Kameraposition im vorherigen Frame. Die Geschwindigkeit des AudioPannerNode ist ebenfalls die aktuelle Position abzüglich der vorherigen Position.

Die Geschwindigkeit lässt sich ermitteln, indem die vorherige Position des Objekts abgezogen und das Ergebnis durch die Zeit geteilt wird, die seit dem letzten Frame vergangen ist. So gehts in 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);
...

Ausrichtung

Die Ausrichtung ist die Richtung, in die die Schallquelle zeigt, und die Richtung, in die der Hörer schaut. Mit der Ausrichtung können Sie gerichtete Schallquellen simulieren. Denken Sie zum Beispiel an einen gerichteten Lautsprecher. Wenn Sie sich vor dem Lautsprecher befinden, ist der Klang lauter als wenn Sie sich dahinter befinden. Noch wichtiger ist, dass Sie die Hörer ausrichten müssen, um festzustellen, von welcher Seite des Zuhörers die Töne kommen. Ein Geräusch, das von links kommt, muss zu rechts wechseln, wenn Sie sich umdrehen.

Um den Orientierungsvektor für den AudioPannerNode zu erhalten, müssen Sie den Drehungsteil der Modellmatrix des 3D-Objekts, das den Ton ausgibt, mit einem vec3(0,0,1) multiplizieren, um zu sehen, wohin es zeigt. Für die Orientierung des Kontext-Listeners müssen Sie den Orientierungsvektor der Kamera abrufen. Für die Ausrichtung des Zuhörers ist auch ein Aufwärtsvektor erforderlich, da der Rollwinkel des Kopfes des Zuhörers bekannt sein muss. Um die Ausrichtung des Zuhörers zu berechnen, holen Sie sich den Rotationsteil der Kamera-View-Matrix und multiplizieren Sie ihn mit vec3(0,0,1) für die Ausrichtung und vec3(0,-1,0) für den Aufwärtsvektor.

Damit die Ausrichtung einen Effekt auf die Geräusche hat, musst du auch die Kegel für den Ton definieren. Der Schallkegel nimmt einen Innenwinkel, einen Außenwinkel und einen Außengewinn auf. Innerhalb des inneren Winkels wird der Ton mit normaler Lautstärke wiedergegeben. Wenn Sie sich dem äußeren Winkel nähern, wird die Verstärkung allmählich auf die äußere Verstärkung umgestellt. Außerhalb des äußeren Winkels wird der Ton mit der äußeren Verstärkung wiedergegeben.

Die Verfolgung der Ausrichtung in Three.js ist etwas komplizierter, da hierfür einige Vektorberechnungen erforderlich sind und der Übersetzungsteil der 4x4-Weltmatrizen auf null gesetzt wird. Trotzdem nicht viele Codezeilen.

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

Für die Kameraorientierung ist auch der Aufwärtsvektor erforderlich. Sie müssen also einen Aufwärtsvektor mit der Transformationsmatrix multiplizieren.

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

Um den Klangkegel für den Ton festzulegen, legen Sie die entsprechenden Eigenschaften des Panner-Knotens fest. Die Kegelwinkel werden in Grad angegeben und liegen zwischen 0 und 360.

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

Alles zusammen

Zusammengenommen folgt der Audiokontext-Listener der Position, Ausrichtung und Geschwindigkeit der Kamera und die AudioPannerNodes den Positionen, Ausrichtungen und Geschwindigkeiten ihrer jeweiligen Audioquellen. Sie müssen die Positionen, Geschwindigkeiten und Ausrichtungen der AudioPannerNodes und des Audiokontext-Listeners in jedem Frame aktualisieren.

Umweltauswirkungen

Nachdem du die räumliche Audiowiedergabe eingerichtet hast, kannst du die Umgebungseffekte für deinen Audiotrack festlegen, um die Immersion deiner 3D-Szene zu verbessern. Angenommen, Ihre Szene spielt in einer großen Kathedrale. In den Standardeinstellungen klingen die Geräusche in Ihrer Szene so, als stünden Sie draußen. Diese Diskrepanz zwischen Bild und Ton stört die Immersion und macht die Szene weniger beeindruckend.

Die Web Audio API hat einen ConvolverNode, mit dem Sie den Umgebungseffekt für einen Ton festlegen können. Fügen Sie ihn dem Verarbeitungsdiagramm für die Audioquelle hinzu, um den Ton an die Einstellung anzupassen. Im Web finden Sie Impulsantwort-Samples, die Sie mit ConvolverNodes verwenden können. Sie können auch eigene erstellen. Dies kann etwas umständlich sein, da Sie die Impulsantwort des Ortes, den Sie simulieren möchten, aufzeichnen müssen.

Wenn Sie Umgebungsaudio mit ConvolverNodes erstellen möchten, müssen Sie den Audioverarbeitungsgraphen neu verdrahten. Anstatt den Ton direkt an das Haupt-Volume weiterzuleiten, müssen Sie ihn über den ConvolverNode leiten. Da Sie möglicherweise die Stärke des Umgebungseffekts steuern möchten, müssen Sie auch das Audio um den ConvolverNode herum leiten. Um die Lautstärke des Mixes zu steuern, müssen dem ConvolverNode und dem Audiosignal GainNodes zugeordnet werden.

In der finalen Audioverarbeitungsgrafik, die ich verwende, wird das Audio der Objekte durch einen GainNode geleitet, der als Passthrough-Mixer verwendet wird. Vom Mixer leite ich das Audiosignal an den ConvolverNode und einen weiteren GainNode weiter, mit dem die Lautstärke des einfachen Audiosignals gesteuert wird. Der ConvolverNode ist mit seinem eigenen GainNode verbunden, um die Lautstärke des gefilterten Audiosignals zu steuern. Die Ausgänge der GainNodes sind mit dem Hauptregler für die Lautstärke verbunden.

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

Damit der ConvolverNode funktioniert, müssen Sie ein Impulsantwort-Sample in einen Puffer laden und dem ConvolverNode zur Verfügung stellen. Das Laden des Samples erfolgt genauso wie bei normalen Audiosamples. Hier ist ein Beispiel dafür:

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

Zusammenfassung

In diesem Artikel haben Sie gelernt, wie Sie Ihren 3D-Szenen mithilfe der Web Audio API Positionsaudio hinzufügen. Mit der Web Audio API können Sie die Position, Ausrichtung und Geschwindigkeit von Audioquellen und des Hörers festlegen. Wenn Sie sie so einstellen, dass sie die Objekte in Ihrer 3D-Szene verfolgen, können Sie ein umfassendes Klangbild für Ihre 3D-Anwendungen erstellen.

Um den Audioeffekt noch ansprechender zu gestalten, können Sie mit dem ConvolverNode in der Web Audio API den allgemeinen Klang der Umgebung einrichten. Mit der Web Audio API können Sie eine Vielzahl von Effekten und Umgebungen simulieren, von Kathedralen bis hin zu geschlossenen Räumen.

Verweise