位置オーディオと 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 で使用できるインパルス応答のサンプルはウェブで入手できます。また、独自のサンプルを作成することもできます。シミュレートする場所のインパルス レスポンスを記録する必要があるため、少し手間がかかりますが、必要に応じてこの機能を使用してください。

ConvolverNodes を使用して環境オーディオを実行するには、オーディオ処理グラフを再配線する必要があります。音声をメイン ボリュームに直接渡すのではなく、ConvolverNode 経由でルーティングする必要があります。また、環境効果の強さを制御したい場合は、ConvolverNode の周囲にオーディオをルーティングする必要もあります。ミックス音量を制御するには、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 を使用してさまざまな効果と環境をシミュレートできます。

参照