位置オーディオと WebGL のミキシング

Ilmari Heikkinen

はじめに

この記事では、Web Audio API の位置オーディオ機能を使用して、WebGL シーンに 3D サウンドを追加する方法について説明します。音声の信頼性を高めるために、Web Audio API で可能な環境効果についても説明します。Web Audio API について詳しくは、Boris Smus による Web Audio API のスタートガイドをご覧ください。

位置オーディオを行うには、Web Audio API の AudioPannerNode を使用します。AudioPannerNode は、サウンドの位置、向き、速度を定義します。さらに、Web Audio API のオーディオ コンテキストには、リスナーの位置、向き、速度を定義できるリスナー属性があります。この 2 つを使うと、ドップラー効果と 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 の速度プロパティを変更することで、サウンドにドップラー効果を追加できます。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;
...

音声のサウンド コーンを設定するには、パナー ノードの適切なプロパティを設定します。円錐の角度は 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 を中心とした音声のルーティングも必要です。ミックス ボリュームを制御するには、ConvolverNode とプレーン オーディオに GainNode をアタッチする必要があります。

ここで使用している最終的な音声処理グラフには、ゲインノードを通過するオブジェクトの音声が含まれており、パススルー ミキサーとして使用されます。ミキサーから、音声を ConvolverNode と別の GainNode に渡します。これはプレーン オーディオの音量を制御するために使用します。ConvolverNode は独自の 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 を使用してさまざまな効果や環境をシミュレートできます。

参照