混合位置音频和 WebGL

Ilmari Heikkinen

简介

在本文中,我将介绍如何使用 Web Audio API 中的定位音频功能将 3D 音效添加到 WebGL 场景中。为了让音频更逼真,我还将向您介绍 Web Audio API 支持的环境效果。如需更详细地了解 Web Audio API,请参阅 Boris Smus 撰写的 Web Audio API 使用入门一文。

如需实现位置音频,您可以使用 Web Audio API 中的 AudioPannerNode。AudioPannerNode 用于定义声音的位置、方向和速度。此外,Web Audio API 音频上下文有一个监听器属性,可让您定义监听器的位置、方向和速度。借助这两项功能,您可以使用多普勒效应和 3D 平移来创建定向声音。

我们来看看上述场景的音频代码。这是非常基本的 Audio API 代码。您可以创建一组 Audio API 节点并将它们连接在一起。音频节点是指各个声音、音量控制器、效果节点和分析器等。构建此图表后,您需要将其连接到音频上下文目的地,以便能够听到音频。

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

位置

声源定位音频会根据音源的位置和听众的位置来确定如何将声音混合到扬声器。听众左侧的音源在左音箱中会更响亮,右侧则相反。

首先,创建一个音频源并将其附加到 AudioPannerNode。然后,设置 AudioPannerNode 的位置。现在,您就拥有可移动的 3D 音效了。默认情况下,音频上下文监听器位置为 (0,0,0),因此以这种方式使用时,AudioPannerNode 位置相对于相机位置。每当您移动相机时,都需要更新 AudioPannerNode 位置。如需使 AudioPannerNode 相对于世界的位置,您需要将音频上下文监听器位置更改为相机位置。

如需设置位置跟踪,我们需要创建一个 AudioPannerNode 并将其连接到主音量。

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

在每一帧中,更新 AudioPannerNode 的位置。在下面的示例中,我将使用 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);
...

如需跟踪监听器位置,请将音频上下文的监听器位置设置为与摄像头位置一致。

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

使用时间间隔

现在,我们已经知道了监听器和 AudioPannerNode 的位置,接下来我们来关注一下它们的速度。通过更改监听器和 AudioPannerNode 的速度属性,您可以为声音添加 Doppler 效果。Web Audio API 的示例页面上提供了一些不错的多普勒效应示例。

获取监听器和 AudioPannerNode 的速度的最简单方法是跟踪它们每帧的位置。监听器的速度是相机的当前位置减去相机在上一个帧中的位置。同样,AudioPannerNode 的速度为其当前位置减去其上一个位置。

如需跟踪速度,可以获取对象的上一个位置,将其从当前位置中减去,然后将结果除以从上一个帧以来经过的时间。在 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);
...

方向

方向是指声源所指的方向和听众所朝的方向。借助方向,您可以模拟定向声源。例如,想象一下定向音箱。与站在音箱后面相比,站在音箱前面听到的声音会更大。更重要的是,您需要监听器方向来确定声音来自监听器的哪一侧。当您转身时,来自左侧的声音需要切换到右侧。

如需获取 AudioPannerNode 的方向矢量,您需要获取发声 3D 对象的模型矩阵的旋转部分,并将 vec3(0,0,1) 与其相乘,以查看其最终指向的位置。对于上下文监听器屏幕方向,您需要获取摄像头的方向矢量。由于需要知道听众头部的滚动角度,因此听众的方向也需要一个向上向量。如需计算监听器方向,请获取相机视图矩阵的旋转部分,并将 vec3(0,0,1) 与方向矢量相乘,将 vec3(0,-1,0) 与上矢量相乘。

为了让屏幕方向对声音产生影响,您还需要为声音定义锥形。音箱锥体采用内角、外角和外增益。在内角内,声音会以正常音量播放,而当您接近外角时,增益会逐渐从内角增益变为外角增益。在外侧视角之外,声音会以外侧增益播放。

在 Three.js 中跟踪屏幕方向稍微复杂一些,因为它涉及一些矢量数学运算,以及将 4x4 世界矩阵的平移部分设为零。不过,代码行数还是不多。

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

相机方向跟踪也需要上矢量,因此您需要将上矢量与转换矩阵相乘。

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

如需为声音设置声音锥,您需要设置 Panner 节点的适当属性。锥形角度以度为单位,介于 0 到 360 之间。

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

全部

总而言之,音频上下文监听器会跟踪摄像头的位置、方向和速度,而 AudioPannerNode 会跟踪其各自音源的位置、方向和速度。您需要在每个帧更新 AudioPannerNode 和音频上下文监听器的位置、速度和方向。

环境影响

设置完位置音频后,您可以为音频设置环境效果,以增强 3D 场景的沉浸感。假设您的场景设在一个大型大教堂内。在默认设置下,场景中的声音会让您感觉自己站在户外。画面和音频之间的这种差异会破坏沉浸感,使场景的效果大打折扣。

Web Audio API 有一个 ConvolverNode,可让您为声音设置环境效果。将其添加到音频源的处理图中,即可让声音符合设置。您可以在网络上找到可与 ConvolverNode 搭配使用的脉冲响应示例,也可以自行制作。由于您需要录制要模拟的场所的冲击响应,因此使用起来可能稍显繁琐,但如果需要,您可以使用此功能。

如需使用 ConvolverNode 进行环境音频处理,需要重新布线音频处理图。您需要将声音通过 ConvolverNode 路由,而不是直接传递到主音量。由于您可能需要控制环境效果的强度,因此还需要在 ConvolverNode 周围路由音频。如需控制混音音量,需要将 GainNode 附加到 ConvolverNode 和普通音频。

我使用的最终音频处理图表中,对象的音频会通过用作直通式调音器的 GainNode。从混音器,我将音频传递到 ConvolverNode 和另一个 GainNode,用于控制普通音频的音量。ConvolverNode 会连接到自己的 GainNode 以控制混响后的音频音量。GainNode 的输出连接到主音量控制器。

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

如需使 ConvolverNode 正常运行,您需要将脉冲响应样本加载到缓冲区,并让 ConvolverNode 使用它。加载该选段的方式与加载常规音频选段的方式相同。以下示例展示了一种实现方式:

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

摘要

在本文中,您学习了如何使用 Web Audio API 向 3D 场景添加位置音频。借助 Web Audio API,您可以设置音源和监听器的位置、方向和速度。通过将这些音源设置为跟踪 3D 场景中的对象,您可以为 3D 应用创建丰富的声景。

为了让音频体验更加出色,您可以使用 Web Audio API 中的 ConvolverNode 来设置环境的一般音效。您可以使用 Web Audio API 模拟各种效果和环境,从大教堂到密闭房间。

参考