13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WebAudio PannerNodeを使用して、手軽に立体的なサウンドを実現する

Posted at

Canvas,WebGLの登場により、ブラウザでも手軽に2Dや、3Dのグラフィックができるようになりましたが、「音」についても、

を利用することで、2Dや3Dなどの空間を考慮したアプリケーションが作成できるようになります。
具体例をいくつかあげると、

  • 遠くいる人(音源)の声が、小さく聞こえ、近くにいる人の声が、大きく聞こえる
  • 右側にいる人の声が、ステレオヘッドホンのR側から聞こえる
  • 自分(音の聞き手)に対して、正対している人の声の方が、顔を背けている人より大きく聞こえる

などができるようになります。

本記事では、このPannerNodeAudioListenerについて、基本的な使用方法と、各パラメータの意味や指定方法を紹介します。

前提知識

以下で紹介するデモやサンプルでは、Three.jsを利用して3Dグラフィックを実装していますが、Three.jsの知識は特になくても、PannerNodeそのものの使い方を理解する上で、あまり支障はありません。ただし、Three.jsに関する知識がある方が、使い方のイメージはつきやすいと思います。

PannerNode、AudioListenerとは

いずれも、Web Audio APIの一部です。多くのブラウザで標準で実装されています。

PannerNode

PannerNodeは「音源」に関する情報を表すもので、音源の位置、向かっている方向、その他様々なプロパティが定義されています。

(参考)

The PannerNode interface represents the position and behavior of an audio source signal in space

MDN - PannerNodeより引用

AudioListener

AudioListenerは「聞き手」(音を聞いている人物)に関する情報を表すもので、その位置、向かっている方向などが定義されています。

(参考)

The AudioListener interface represents the position and orientation of the unique person listening to the audio scene

MDN - AudioListenerより引用

なお、AudioListenerは、AudioContext毎に1つのみ存在しますが、PannerNodeは、AudioContext毎に複数存在しえます。

ブラウザのサポート状況

Can I use (PannerNode)
MDN - PannerNode - Browser compatibility

Can I use (AudioListener)
MDN - AudioListener - Browser compatibility

概ねIE以外のどのブラウザでも使用できますが、現時点では、FireFox、Safariにおいて一部処理でdeprecatedとなったメソッドが呼ばれるように処理を分岐させる必要があります。

準備

ステレオヘッドホンなどの、ステレオ再生ができる機器を用意してください。

デモやサンプルコードなどで音声が一切再生されない場合、ブラウザのミュート設定なども確認してみてください(参考)

デモ

デモを作ったので、まずは体感してみたい方は実際に触ってみてください。
カメラ=視点が音の聞き手(Listener)であり、青い人形のようなオブジェクトが音源です。Listenerと音源の位置関係により、音の聞こえ方が変化することが体感できると思います。

画面右上のインスペクターで、青い人形の位置や向きを変更できます。
また、画面右下のインスペクターで、PannerNodeの各プロパティを変更できます。

  • 視点が3D空間内を自由に動き回るタイプのデモ。(Camera Control=Orbit, マウスドラックで視点移動)

Orbit-demo.gif

  • 視点が定位置で回転するタイプ。主に、視点が「逆さま」になる状況を試す目的で作成。(Camera Control=Simple Rotation, カーソルキーで回転)

Simple-rotation-demo.gif

  • ソースコード
  • 動作確認ブラウザ: Google Chrome, FireFox, Microsoft Edge, Safari(MacOS, iPadOS)1

ブラウザバージョンに関しては、いづれも執筆時点の最新を使用して確認しました。

使用方法の説明

大別して、以下3つの作業が必要となります。以降で順に説明します。

  • PannerNodeを生成し、オーディオグラフに接続する
  • PannerNode、AudioListenerに位置と向きなどを設定する
  • PannerNodeの各プロパティを調整する

ユーザーの操作などにより、音源やListenerの位置や向きが変化する場合、変化に合わせて更新します。
また、PannerNodeには、聞こえ方を調整する様々なプロパティがあるので、これらも適宜設定します。

それぞれデフォルト値があるため、デフォルト値で構わない場合、明示的に設定しなくても動作します。

(PannerNode.orientationX, PannerNode.orientationY, PannerNode.orientationZ) = (1, 0, 0)
(PannerNode.positionX, PannerNode.positionY, PannerNode.positionZ) = (0, 0, 0)

(AudioListener.positionX, AudioListener.positionY, AudioListener.positionZ) = (0, 0, 0)
(AudioListener.forwardX, AudioListener.forwardY, AudioListener.forwardZ) = (0, 0, -1)
(AudioListener.upX, AudioListener.upY, AudioListener.upZ) = (0, 1, 0)

PannerNodeを生成し、オーディオグラフに接続する

以下のようにAudioContext#createPannerを呼び出して、PannerNodeのインスタンスを生成し、connectします。

pannerNode = audioContext.createPanner();
....
source.connect(pannerNode);
pannerNode.connect(audioContext.destination);
....

例:
クリックイベントで再生する場合、以下のようになります。
デモソースコードから抜粋)


const $audio = document.querySelector('audio');
let audioContext;
let pannerNode;
var AudioContext = window.AudioContext || window.webkitAudioContext;

document.querySelector('#play').addEventListener('click', () => {

    audioContext = new AudioContext();
     
    $audio.play();

    const source = audioContext.createMediaElementSource($audio);
    pannerNode = audioContext.createPanner();

    source.connect(pannerNode);
    pannerNode.connect(audioContext.destination);

});

PannerNode、AudioListenerに位置と向きなどを設定する

生成したPannerNodeのインスタンスのposition~orientation~を設定します。
また、AudioContextlistenerプロパティ(AudioListener)にposition~forward~up~を設定します。

(本記事ではpositionXpositionYpositionZを一まとめにして、position~などと表記します)

座標系

右手系。
(WebGLなどと同じ。(参考)wgld.org - 3D 描画の基礎知識

PannerNode

デモソースコードから抜粋)

obj.updateMatrixWorld();
const forward = new THREE.Vector3(0, 0, -1);
forward.applyQuaternion(obj.quaternion);

const { x, y, z } = obj.position;

if (pannerNode.positionX) {
    pannerNode.positionX.setValueAtTime(x, audioContext.currentTime);
    pannerNode.positionY.setValueAtTime(y, audioContext.currentTime);
    pannerNode.positionZ.setValueAtTime(z, audioContext.currentTime);
} else {
    pannerNode.setPosition(x, y, z);
}

if (pannerNode.orientationX) {
    pannerNode.orientationX.setValueAtTime(forward.x, audioContext.currentTime);
    pannerNode.orientationY.setValueAtTime(forward.y, audioContext.currentTime);
    pannerNode.orientationZ.setValueAtTime(forward.z, audioContext.currentTime);
} else {
    pannerNode.setOrientation(forward.x, forward.y, forward.z);
}

position、orientationの値の設定

  • position~: 3D空間における音源の座標
  • orientation~: 3D空間における音源の向きを表すベクトル

「右手系」であることに注意して設定します。

上記のソースコード例では、以下が該当します。Three.jsの機構を用いた処理です。


obj.updateMatrixWorld();
const forward = new THREE.Vector3(0, 0, -1);
forward.applyQuaternion(obj.quaternion);

const { x, y, z } = obj.position;
  • 初期状態の向き(0, 0, -1)に対して、音源となるオブジェクトの現在の向きを適用(applyQuaternion)し、forward(=orientation)を求めています。
  • オブジェクトのpositionプロパティに格納された、音源となるオブジェクトの位置(x, y, z)を取り出しています。

なお、ここにおける「初期状態の向き」とは、Three.js上のオブジェクトの初期値のことであり、PannerNodeのデフォルト値ではありません。

orientationをセットする必要性

coneInnerAngleconeOuterAngleの設定上、音源が向きを持たない場合、orientationの設定は不要です。
(逆にいうと、coneInnerAngleconeOuterAngleを適当に設定しないと、orientationの設定は意味を持ちません)

coneInnerAngleconeOuterAngleについて、詳細は後述

ブラウザ間の相違

現時点で、Safariでは、orientation~position~プロパティが使用できません。
代わりに、setPositionsetOrientationを使用します。

AudioListener

デモソースコードから抜粋)

obj.updateMatrixWorld();
const forward = new THREE.Vector3(0, 0, -1);
const up = new THREE.Vector3(0, 1, 0);
forward.applyQuaternion(obj.quaternion);
up.applyQuaternion(obj.quaternion);

const { x, y, z } = obj.position;

const listener = audioContext.listener;

if (listener.positionX) {
    listener.positionX.setValueAtTime(x, audioContext.currentTime);
    listener.positionY.setValueAtTime(y, audioContext.currentTime);
    listener.positionZ.setValueAtTime(z, audioContext.currentTime);
} else {
    listener.setPosition(x, y, z);
}

if (listener.forwardX) {
    listener.forwardX.setValueAtTime(forward.x, audioContext.currentTime);
    listener.forwardY.setValueAtTime(forward.y, audioContext.currentTime);
    listener.forwardZ.setValueAtTime(forward.z, audioContext.currentTime);
    listener.upX.setValueAtTime(up.x, audioContext.currentTime);
    listener.upY.setValueAtTime(up.y, audioContext.currentTime);
    listener.upZ.setValueAtTime(up.z, audioContext.currentTime);
} else {
    listener.setOrientation(
        forward.x, forward.y, forward.z, 
        up.x, up.y, up.z);
}

position、forward、upの値の設定

  • position~: 3D空間におけるListenerの座標
  • forward~: 3D空間におけるListenerの向きを表すベクトル
  • up~: 3D空間におけるListener頭の向き(the direction of the top of the listener's head)を表すベクトル

「右手系」であることに注意して設定します。

上記のソースコードでは、以下が該当します。
やっていることのイメージは、PannerNodeの場合と同様のため、説明は割愛します。


obj.updateMatrixWorld();
const forward = new THREE.Vector3(0, 0, -1);
const up = new THREE.Vector3(0, 1, 0);
forward.applyQuaternion(obj.quaternion);
up.applyQuaternion(obj.quaternion);

const { x, y, z } = obj.position;

upに設定すべき値

forward~に対して、up~をどのように設定すべきかですが、各ドキュメントに以下の記述があります。

Both the up and front vectors must be mutually perpendicular(at 90 degrees from one another).

(MDN - AudioListener - 冒頭の画像より引用

The forward and up values are linearly independent of each other.

(MDN - AudioListener - Properties)より引用

1つ目の記述からは、forward~up~は直交していなければならないかのように読めますが、2つ目の記述からはそこまでは求められていない(線形独立であれば良い)ように読めます。

仮に直交していることが理想なのだとしても、そもそも、forward~up~は、double(浮動小数点数)で定義されるため、厳密に直交するベクトルを求めることは、困難または不可能です。

そこで私は、初期状態のforward~up~の位置関係を維持した上で、Listenerの回転状態を考慮し、結果的にforward~とだいたい直交するベクトルが選択されるという落とし所にしています2

const forward = new THREE.Vector3(0, 0, -1);
const up = new THREE.Vector3(0, 1, 0);
forward.applyQuaternion(obj.quaternion);
up.applyQuaternion(obj.quaternion);

ブラウザ間の相違

現時点で、FireFox, Safariでは、forward~position~up~が使用できません。
代わりに、setPositionsetOrientationを使用します。

その他補足

値の設定(更新)のタイミング

今回用意したデモでは、Three.jsのレンダリング処理と同期させるため、requestAnimationFrame呼び出し毎に値が更新されるように実装しています。

値の設定方法

上記の例ではsetValueAtTimeを使っていますが、さまざまな選択肢があります。

positionXなどのプロパティは、AudioParamのインスタンスとなっており、例えば以下のような選択肢があります(一部のみ記載)。

  • setValueAtTime: あるタイミング(現時点も含む)で、瞬時に位置、向きを反映させる(今回のデモコードで使用)
  • linearRampToValueAtTime: 完了タイミングを指定し、徐々に位置、向きの値を反映させる
  • setTargetAtTime: 開始タイミングを指定し、徐々に位置、向きの値を反映させる

詳細は、MDN - AudioParam - Methodsを参照してください。

PannerNodeの各プロパティを調整する

今回用意したデモでも、インスペクターで数値を様々に変更することで、体感することができます。
inspector-image.png

詳細な情報

入力範囲から外れる値がセットされた場合の動作は、プロパティにより異なります。詳細は2つ目のサイトを参照してください。

向き関係

  • coneInnerAngle (double, default: 360, 0以上360以下)
  • coneOuterAngle (double, default: 3603, 0以上360以下)
  • coneOuterGain (double, default: 0, 0以上1以下)

Listenerに対する、音源の向きによって音量を変えたい場合に設定します。

  1. Listenerが、coneInnerAngle「内」にいる場合、音量は減衰しない(音量=1)
  2. Listenerが、coneOuterAngle「外」にいる場合、音量はconeOuterGainになる
  3. Listenerが、coneInnerAngle「外」かつ coneOuterAngle「内」にいる場合、音量は1〜coneOuterGainまで徐々に減衰する

w3c-cone-image.png
(W3C - Web Audio API - 6.5. Sound Conesより抜粋)

「3.」の場合に、どのように減衰するかですが、W3C - Web Audio API - 6.5. Sound Conesによると、次式で与えられるようです。
例えば、音源に対してListenerが前方40°にあり、coneInnerAngle=50coneOuterAngle=120場合、absAngle=40absInnerAngle=25absOuterAngle=60となります。

x = (absAngle - absInnerAngle) / (absOuterAngle - absInnerAngle)
gain = (1 - x) + coneOuterGain * x

グラフにプロットすると以下のようになります。

cone-gain-image.png

上記グラフは、jsfiddleにて作成

距離関係

  • distanceModel (linear/inverse/exponential, default: inverse)
  • maxDistance (double, default: 10000, 0より大きい)
  • refDistance (double, default: 1, 0以上)
  • rolloffFactor (double, default: 1, 0以上)

(rolloffFactorは、distanceMode=linearの場合のみ、0以上1以下)

これらのプロパティを用いて、Listenerと音源との距離に応じて、どのように音量を変化させるかを設定します。

  1. distanceModelについては後述
  2. Listenerと音源間の距離が、maxDistanceより大きい場合、音量がそれ以上減衰しなくなる。maxDistancedistanceModelがlinearの場合のみ使用される。
  3. Listenerと音源間の距離が、refDistance以上になると、音量の減衰が開始する
  4. rolloffFactor音量が減衰する速さ

distanceModel毎の音量の計算式は以下の通りです。
MDN - PannerNode - distanceModelより引用

linear

1 - rolloffFactor * (distance - refDistance) / (maxDistance - refDistance)

inverse

refDistance / (refDistance + rolloffFactor * (Math.max(distance, refDistance) - refDistance))

exponential

pow(Math.max(distance, refDistance) / refDistance, -rolloffFactor)

グラフにしてみると、distanceModelごとの特徴が見えてきます。
linearは距離に比例して音量が減衰し、inverse``exponentialはListenerと音源との距離が近い場合に急激に音量が減衰します。

cone-volume-reduction.png

上記グラフは、jsfiddleにて作成

panningModel

  • panningModel (equalpower/HRTF, default: equalpower)
  1. equalpower: シンプル、効率的な方法
  2. HRTF: より高品質。LRの表現だけでなく、Listenerに対して音源が前後/上下にある場合の表現もしたい場合に使用。

HRTFとは、「Head-related transfer function」(頭部伝達関数)の略です。
私自身は、理論的に詳しいことはわかりませんが、以下のような参考サイトの情報などを参照した結果、
HRTFは技術的に難しい部分が色々あり、現状実現できるものはそこまで高精細なものではないが、アプリケーションによっては使用するメリットがあるとの認識を得ました。

"HRTF" は人間の頭部の形状のデータを基に音の伝わり方をシミュレートするもので、前後/上下などの表現も可能なアルゴリズムです。基本的にヘッドホンを使用して聴く事を想定しており、いわゆる「バイノーラル録音」的なものと思えば良いかと思いますが、それなりに CPU の負荷はかかり、また実際にどう聞こえるかには結構個人差があります。

g200kg > Web Audio API 解説 > 17.パンナーの使い方より引用

個人的にはこういう立体音響は基本技術としてまだ万人が納得できるような完成度まで至っていない気がします。

g200kg > Web Audio API 解説 > 17.パンナーの使い方より引用

ゲーム
(中略)
敵の位置が正確に分かる、没入感が得られるなど、ユーザーメリットが大きい
と、好条件が並ぶのに対し音楽では、

MATLABでHRTF~頭部伝達関数とは?~聴覚の仕組みは解明されていない~より引用
「中略」は筆者記入

Three.jsを使用する場合

PositionalAudioなどのクラスが用意されており、これらを使うことで、自前でPannerNode、Listenerに位置や向きを設定する必要がなくなります。
具体的には、本記事「PannerNode、AudioListenerに位置と向きなどを設定する」で行っている処理を自前で記述する必要がなくなります。

three.js - PositionalAudio

使用例
デモソースコード(three.js版)から抜粋。


const $audio = document.querySelector('audio');

document.querySelector('#play').addEventListener('click', () => {

    const listener = new THREE.AudioListener();
    camera.add(listener);

    $audio.play();

    const sound = new THREE.PositionalAudio(listener);
    sound.setMediaElementSource($audio);

    speakerMesh.add(sound); //speakerMesh = 音源として扱うオブジェクト
});

ただし、AudioListenerのup~ベクトルについては注意が必要です。
現状のAudioListenerの実装では、(0, 1, 0)に固定されてしまい、Listenerが逆さまになるケースなどにうまく対処できません。(Listenerが逆さまになった際に、LRが逆になってしまう)

今回のデモでは、以下のようなクラスを作成して対処しました。

class RotatableUpVectorAudioListener extends THREE.AudioListener {
 
    _forward;
    _up;
    _quaternion;

    _position;
    _scale;

    constructor() {
        super();
        this._forward = new THREE.Vector3();
        this._up = new THREE.Vector3();
        this._quaternion = new THREE.Quaternion();

        this._position = new THREE.Vector3();
        this._scale = new THREE.Vector3();
    }

    updateMatrixWorld(force) {
        super.updateMatrixWorld(force);

        const listener = this.context.listener;
        this.matrixWorld.decompose(this._position, this._quaternion, this._scale);
        this._forward.set(0, 0, -1).applyQuaternion(this._quaternion);
        this._up.set(0, 1, 0).applyQuaternion(this._quaternion);

        if (listener.forwardX) {

            const endTime = this.context.currentTime + this.timeDelta;

            listener.upX.cancelScheduledValues(this.context.currentTime);
            listener.upY.cancelScheduledValues(this.context.currentTime);
            listener.upZ.cancelScheduledValues(this.context.currentTime);
            
            listener.upX.linearRampToValueAtTime(this._up.x, endTime);
            listener.upY.linearRampToValueAtTime(this._up.y, endTime);
            listener.upZ.linearRampToValueAtTime(this._up.z, endTime);

        } else {
            listener.setOrientation(
                this._forward.x, this._forward.y, this._forward.z, 
                this._up.x, this._up.y, this._up.z);
        }
    }
}


    // const listener = new THREE.AudioListener();
    const listener = new RotatableUpVectorAudioListener();
    camera.add(listener);

備考

  • ドップラー効果を実現するための仕組みも存在したようですが、現時点ではdeprecatedとなっています。(MDN - AudioListener - Deprecated features)
  • 単純なLRのpanningを行いたい場合、PannerNodeよりシンプルなStereoPannerNodeが利用できます。

参考サイト

(参照日付:2021/3/16)

  1. Safariに関しては一応動作はしますが、私が動作確認した限りでは、音源の再生の開始がスムーズでなく、また、MacOSの場合ノイズが入ることがあり、実用的なレベルだとは感じられませんでした。(MacOS: Big Sur 11.2.3 Safari 14.0.3, iPadOS 14.4.1)。PannerNodeをconnectせずに単純に音声の再生だけを行っても、同じような状況であるため、PannerNodeそのものの問題ではないと考えられます。

  2. ちなみに、up~の向きをある程度正しく更新しないと、LRが逆に聴こえてしまう場合があります。例えば、今回作成したデモにおいて、Camera Control=Simple Rotationのパターンで、up~(0, 1, 0)に固定して動作させると、Listenerが逆さまになった時に、LRが逆に聴こえてしまいます。また、デモで試すと実感できますがup~の回転によりLRが変化するケースでは、forward~の回転によりLRが変化する時ほど、スムーズではないです。(up~の回転自体は、LRのバランスに影響を与えていないように聞こえます)

  3. MDN - PannerNode - propertiesには「0」と表記されていますが、実際に動作させた結果及びW3C - Web Audio API - 1.27.2. Attributesの方を採用して「360」としました。

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?