位置オーディオと WebGL の統合

HTML5 Rocks

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

はじめに

この記事ではWeb Audio APIが提供する位置オーディオの機能を使って、WebGLのシーンに3Dサウンドを追加する方法について説明します。また、よりリアルな音を得るために、Web Audio APIで実現可能な環境エフェクトを紹介します。Web Audio API のより基礎的な入門は、Boris Smus の Getting started with Web Audio API を参照ください。

位置オーディオを実現するには、Web Audio API の AudioPannerNode を使います。AudioPannerNode は音源の位置、方位、速度を定義します。また、Web Audio API の AudioContext は リスナー自身の位置、方位、速度を定義するための listener 属性を持ちます。これら2つを使って、ドップラー効果やパン等の方向を持ったサウンドを作り出すことが出来ます。

まず、平坦な音を聴いてください。下の3D シーンでは、音に一切エフェクトをかけずに再生しています。あまり面白くないですね。位置を変えるには キーボードの[W][A][S][D]キーか矢印キーを使います。またマウスでドラッグすることで向きを変えることが出来ます。

平坦な音のデモ

では、コードを見てみましょう。以下は非常に初歩的な Web Audio API のコードですが、このようにして AudioNode を生成して、それらを接続することができます。AudioNode は個々の音源や、ボリュームコントローラー、エフェクト、アナライザー等を表します。これらの Node で構成されたグラフを作った後、AudioContext の destination に接続することで、音が発せられます。

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

// 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 サウンドが得られます。AudioContext の listener の位置はデフォルトで (0,0,0) なので、この場合 AudioPannerNode の位置はカメラの位置から相対になります。AudioPannerNode の位置をワールド座標に対して相対にするには、カメラの移動に合わせて、AudioContext の listener の位置を変更してやる必要があります。

音の位置のデモ

音源の位置を追跡するために、まず 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);
...

さらに、リスナーの位置を追跡するために、AudioContext の listener の位置をカメラの位置に合わせて更新します。

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

速度

前節で listener と AudioPannerNode の位置を設定したので、次は速度に着目しましょう。listener と AudioPannerNode の velocity プロパティを変更することで、ドップラー効果が得られます。Web Audio API を使ったドップラー効果のよいサンプルがあるのでぜひご覧ください

listener と AudioPannerNode の速度を得る最も簡単な方法は、フレーム毎に位置を計測することです。listener の速度はカメラの現在位置から直前のフレームでの位置を差し引くことで得られます。同様に、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);
...

方位

ここで言っている方位とは、音源が指し示している方向と、リスナーが向いている方向の2つがあります。方位を設定することで、指向性を持った音源を擬似的に作り出すことができます。例えば、指向性スピーカーでは、スピーカーの前面で聴くと、背面で聴くのに比べて音が大きく聴こえます。さらに重要なことは、リスナーがどちらを向いているかにより、音がどの方向から聴こえるかが決まるということです。例えば、リスナーが振り返れば、左側から聴こえていた音は、右側から聴こえなければなりません。

AudioPannerNode の方位を表すベクトルを取得するには、音を発している物体の3D マトリックスのローテーション部分を取り出し、vec3(0,0,1) を掛けることで、その物体が指し示している方向が分かります。同様に listener の方位は、カメラのベクターを取得します。また、リスナーの頭の回転角を知る必要があるため、listener の方位は上方向のベクターを含みます。listener の方位を計算するには、カメラの View マトリックスのローテーション部分を取り出し、vec3(0,0,1) を掛けることで、また、上方向のベクターは vec3(0,-1,0) を掛けることで得られます。

方位が実際に音に影響を及ぼすには、音源に対してサウンドコーンを定義する必要があります。サウンドコーンは InnerAngle、OuterAngle、および OuterGain の3つの値をとります。音は InnerAngle で設定された角度よりも内側では通常のボリュームで再生されますが、OuterAngle で設定された角度に近づくにつれ、OuterGain で設定されたボリュームに向かって徐々に変化します。OuterAngle で設定された角度よりも外側では、OuterGain で設定されたボリュームで再生されます。

音の方位のデモ

以下のコードでは、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;
...

カメラの方位を取得するには上方向のベクターを求める必要があるので、(0,-1,0) をトランスフォームマトリックスに掛けています。

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

音源にサウンドコーンを設定するには、AudioPannerNode の該当プロパティを設定してやる必要があります。角度の単位は degree ですので、0 から 360 の値となります。

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

全部入り

いったんまとめると、AudioContext の listener はカメラの位置、方位、速度を反映し、また、AudioPannerNode は対応する音源の位置、方位、速度を反映します。そして、フレーム毎にこれらの位置、方向、速度を更新する必要があります。

以下は上で紹介した位置オーディオの機能をすべて使ったデモです。

位置オーディオの全部入りデモ

環境エフェクト

ひとたび位置オーディオの設定が完了すると、次は環境エフェクトを使うことにより、3D シーンの没入感をさらに向上させることができます。例えば、大聖堂のシーンを想定しましょう。デフォルトの設定では、音は戸外にいるかのように響くので、シーンの見た目と音が食い違って没入感が損なわれ、感動も覚めてしまいます。

Web Audio API の ConvolverNode を使うことにより、サウンドに環境エフェクトをかけることができます。ConvolverNode をオーディオグラフに追加することで、設定どおりのサウンドを作り出すことができます。ConvolverNode に設定するインパルス応答は、Web上にサンプルが見つかりますし、自身で作ることもできます。音響を再現したい場所に出向いてインパルス応答を録音するのは少々骨が折れますが、しかし不可能ではありません。

以下のデモでは、異なるサウンドスケープを切り替えることができます。

環境エフェクトのデモ

ConvolverNode を使って環境エフェクトを実現するには、オーディオグラフに手を加える必要があります。音をメインボリュームに直接つなぐのではなく、ConvolverNodeを経由します。また、環境エフェクトの度合いを調整するために、ConvolverNode を迂回したパスも作っておきます。ConvolverNodeと素のオーディオを、音量を調整してミックスするために、両方にGainNodeを接続する必要があります。

最後にお見せするオーディオグラフでは、音はパススルーミキサーとして使う GainNode を経由します。そしてミキサーからの出力を ConvolverNode と、素のオーディオの音量を調節するための別の GainNode に接続します。ConvolverNode はさらに GainNode に接続されて、エフェクトがかかった音の音量を調節します。これら2つの GainNode からの出力はメインボリュームのコントローラである GainNode に接続されます。

...
var ctx = new webkitAudioContext();
var mainVolume = ctx.createGainNode();

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

sounds.forEach(function(sound){
  sound.panner.connect(mixer);
});

// Create volume controllers for the plain audio and the convolver.
var plainGain = ctx.createGainNode();
var convolverGain = ctx.createGainNode();

// 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 を使って擬似的に作り出すことが出来ます。

参考文献

Comments

0