オートパンとは?

オートパン (オートパンナー) とは, パンを周期的に変化させるエフェクトです. パン (Panorama)とは, 知覚上の音源の位置のことです. 例えば, パンを右側に移動させると, 右側に音源があるかのように聴こえます. 専門用語では知覚上の音源の位置のことを音像と呼びます.

つまり, この音像の移動を周期的に実行するのがオートパンの原理です.

Web Audio APIにおいてオートパンを実装するには以下の2つの方法があります.

  • PannerNodeクラス / AudioListenerクラスを利用する
  • トレモロとChannelSplitterNodeクラス / ChannelMergerNodeクラスを利用する

どちらの実装においても, まだ解説していないクラスを利用することになるので, まずは, それらの解説をしたいと思います. オートパンをとにかく実装したい方は, 先に実装解説のセクションからご覧ください.

PannerNode / AudioListener

PannerNodeクラスとAudioListenerクラスの概要を解説すると, PannerNodeクラスは音源の位置や向き, 速度に関する設定のためのインターフェースを定義するクラスです. AudioListenerクラスはリスナー, つまり, 聴取者の位置や向き, 速度に関する設定のためのインターフェースを定義するクラスです.

PannerNode / AudioListenerにおける座標系とベクトル

PannerNodeクラス / AudioListenerクラスの詳細解説に入る前に, これらのクラス, つまりは, Web Audio APIにおける3D音響の座標系とベクトルについて解説しておきます. ベクトルに関しては, Web Audio APIに関することではなく, 数学・物理のことなので, そんなの知ってるよ〜という方はスルーしてください.

座標系

x軸
x軸は横方向の位置を表します. x軸の値が大きいほど右側に位置することになります.
y軸
y軸は高さ方向の位置を表します. y軸の値が大きいほど高く位置することになります.
z軸
z軸は奥行きの座標を表します. z軸の値が大きいほど前方に位置することになります.
3D音響の座標系

図2 - 6 - a. 3D音響の座標系

3D音響空間の座標を, 前方から見た場合における座標です. この場合, 3D音響空間における任意の座標は, 以下のように位置することになります.

  • x軸の値が大きいほど右側
  • y軸の値が大きいほど上側
  • z軸の値が大きいほど前方

ベクトル

ベクトルとは, 大きさ向きをもつ物理量のことです. ベクトルの代表例としては, 速度, 重力や電磁気力などの力があります. それに対して, スカラーとは大きさのみで向きの概念をもたない物理量ことです. スカラーの代表例としては, 速さや質量などがあります. また, 音の特性に関連する物理量 (振幅・周波数・周期) や, Web Audio APIのAudioParamインスタンスの値 (ゲインやディレイタイム, フィルタのカットオフ周波数) も向きの概念をもたず, 大きさのみをもつスカラーです.

ベクトルをより具体的に理解するために, 2次元上で移動することを考えます. まず, ベクトルを定義するためには, どの方向に向かうと値が増減するのか?を決定する必要があります. さらに, 2次元上の空間であるので, 方向の定義を2つ定義する必要があります. また, プログラミング言語で2次元のベクトルを表現するために, 以下のようなJavaScriptのクラス (コンストラクタ) を定義します.

サンプルコード 01

< function Vector2(x, y) { // do something .... }

このクラスと一般的な数学のグラフを関連づけて考えます. つまり, 画面右側に移動するほどコンストラクタの第1引数に渡される値が大きくなり, また, 画面上側に移動するほどコンストラクタの第2引数に渡される値が大きくなります. そして, それぞれの方向の両端の中間点をベクトルの始点, つまり, 原点とします. 原点である場合は, Vector2コンストラクタの引数がどちらも0になります.

上記のように定義すると,

  • 原点から右側へ5移動するとベクトルは (5, 0)
  • 原点から左側へ5移動するとベクトルは (-5, 0)
  • 原点から上側へ5移動するとベクトルは (0, 5)
  • 原点から下側へ5移動するとベクトルは (0, -5)
  • 原点から右側へ5, 上側へ5移動するとベクトルは (5, 5)
  • 原点から左側へ5, 下側へ5移動するとベクトルは (-5, -5)
  • 原点から右側へ5, 下側へ5移動するとベクトルは (5, -5)
  • 原点から左側へ5, 上側へ5移動するとベクトルは (-5, 5)

ベクトルは大きさと向きをもつので, 2次元以上のベクトルでは, スカラーのように単一の値で表現することができません. 必ず次元の数だけ要素が存在します. プログラミング言語で表現すれば, 配列 (コレクション) ということです. 上記の8つのベクトル例をVector2のインスタンスで表現してみましょう.

サンプルコード 02


    function Vector2(x, y) {
        this.vector = [x, y];
    }

    Vector2.prototype.getVector = function() {
        return this.vector;
    };

    var vector1 = new Vector2(5, 0);
    var vector2 = new Vector2(-5, 0);
    var vector3 = new Vector2(0, 5);
    var vector4 = new Vector2(0, -5);
    var vector5 = new Vector2(5, 5);
    var vector6 = new Vector2(-5, -5);
    var vector7 = new Vector2(5, -5);
    var vector8 = new Vector2(-5, 5);

    console.dir(vector1.getVector())  // -> [5, 0]
    // ....

ところで, ベクトルは大きさと向きによって構成されていると解説しました. 2次元のベクトル, つまり, 2つの値から大きさと向きに分解するにはどうすればいいのでしょうか?

ベクトルから大きさと向きを数値 (スカラー) として取得するためには, 3平方の定理 (ピタゴラスの原理) と三角関数が必要です. しかしながら, イメージとして大きさと向きに分解するだけであればそれらは不要です.

具体例として, 原点から右側へ5, 上側へ5移動した場合, すなわち, ベクトルが (5, 5) の場合を考えます.

(5, 0) のベクトル

図2 - 6 - b. (5, 0) のベクトル

原点から右側へ5移動したベクトルです.

(5, 5) のベクトル

図2 - 6 - c. (5, 5) のベクトル

原点から右側へ5, 上側へ5移動したベクトルです.

ここで, x方向のベクトルの始点とy方向のベクトルの終点を, 始点から終点に向かうようにつなぎます. この矢印こそが, x方向のベクトルとy方向のベクトルを合成した2次元のベクトルとなります.

合成したベクトル

図2 - 6 - d. 合成したベクトル

x方向のベクトル, y方向のベクトルは1次元のベクトルです. それらの始点と終点をつなぐことで, 合成した2次元のベクトルが表現できます.

この合成した2次元のベクトルの長さが大きさであり, x軸から反時計回りにみた角度が向きになります.

ベクトルの分解

図2 - 6 - e. ベクトルの分解

2次元のベクトルを大きさと向きに分解するイメージ図です.

では, イメージとして大きさと向きに分解したものを数値で表現してみます. イメージがつかめればOKという方はスルーしてください.

ベクトルの大きさは3平方の定理 (ピタゴラスの原理) で算出することが可能です. なぜかというと, 横方向のベクトルと縦方向のベクトルで直角三角形を構成することが可能だからです. 2次元のベクトルの大きさとは, 横方向のベクトルの始点と縦方向のベクトルの終点をつないだベクトルの長さのことでした. そして, 横方向と縦方向のベクトルの大きさ (他の2辺の長さ) はわかっているので, 3平方の定理で算出することができます. これをVector2のプロパティに定義しましょう.

サンプルコード 03


    function Vector2(x, y) {
        this.vector = [x, y];
        this.scalar = Math.sqrt((x * x) + (y * y));
    }

    Vector2.prototype.getVector = function() {
        return this.vector;
    };

    Vector2.prototype.getScalar = function() {
        return this.scalar;
    };

    var vector = new Vector2(5, 5);

    console.log(vector.getScalar());  // -> 7.0710678118654755... = 5√2

ベクトルの向きは角度で表すので, 三角関数で算出することが可能ですが, JavaScriptでは2次元のベクトルから角度 (向き) をラジアン単位で取得できる, Mathクラスのatan2メソッドがあります. そして, ラジアンが取得できれば, 度 (degree) 単位も算出できるのでそれもプロパティに追加します.

サンプルコード 04


    function Vector2(x, y) {
        this.vector = [x, y];
        this.scalar = Math.sqrt((x * x) + (y * y));
        this.radian = Math.atan2(y, x);
        this.degree = (this.radian * 180) / Math.PI;
    }

    Vector2.prototype.getVector = function() {
        return this.vector;
    };

    Vector2.prototype.getScalar = function() {
        return this.scalar;
    };

    Vector2.prototype.getRadian = function() {
        return this.radian;
    };

    Vector2.prototype.getDegree = function() {
        return this.degree;
    };

    var vector = new Vector2(5, 5);

    console.log(vector.getRadian());  // -> 0.7853981633974483... = π / 4
    console.log(vector.getDegree());  // -> 45

ベクトルの大きさと向きの算出

図2 - 6 - f. ベクトルの大きさと向きの算出

矢印 (ベクトル) を囲んだ形が, 直角三角形であることが重要な点です. これによって, 3平方の定理や三角関数を適用して大きさと向きを算出することができます.

以上で, 2次元のベクトルの大きさと向きを数値で表現することができました.

Scalar
Angle (Radian)
Angle (Degree)

図2 - 6 -g . 2次元のベクトル まとめ

PannerNodeクラス / AudioListenerクラスの音響空間, つまり, Web Audio APIにおける音響空間は3次元なので, 3次元のベクトルを扱うことになります. しかしながら, ベクトルの演算はPannerNodeインスタンス / AudioListenerインスタンスのメソッドが隠蔽 (ブラックボックス化) しているので, そのメソッドの仕様とベクトルのイメージさえつかむことができれば十分です. 内部ではサンプルコードで記載したようなベクトルの演算が実行されていると理解しておけばいいでしょう.

PannerNode

PannerNodeクラスを利用するためには, インスタンスを生成する必要があります. AudioContextインスタンスのcreatePannerメソッドを利用します.

PannerNodeインスタンスで定義されているプロパティは多いので, まずはそれらの概要を記載します.

表2 - 6 - a. PannerNodeインスタンスのプロパティ
PropertyDescriptionDefault
panningModel空間音響のアルゴリズムHRTF
distanceModel音がリスナーに伝達する際の音量減衰のアルゴリズムinverse
refDistance音量減衰の算出に影響する1
maxDistance音量減衰の算出に影響する10000
rolloffFactor音量減衰の算出に影響する1
coneInnerAngle音の指向性に影響する360
coneOuterAngle音の指向性に影響する360
coneOuterGain音の指向性に影響する0
setPosition(x, y, z)パンの位置を設定する(0, 0, 0)
setOrientation(x, y, z)パンの向きを設定する(1, 0, 0)
setVelocity(x, y, z)パンの移動速度を設定する(0, 0, 0)

panningModel

panningModelプロパティは, 空間音響アルゴリズムを決定するプロパティです. といってもよくわからないと思いますので, くだいて表現すれば, どれぐらい忠実に3D音響空間, つまり, 実際の音響空間をシミュレートするかを決定するプロパティです. 指定可能なアルゴリズム (値) は2種類です.

現状の仕様では, panningModelプロパティの型は文字列型ですが, 少し前の仕様ではそれに対応する数値型でした. また, panningModelプロパティのデフォルト値は 'HRTF' です.

表2 - 6 - b. panningModelの値
StringNumberDescription
equalpower0各チャンネルに対して等しく音を伝達する
HRTF1人の頭部 (頭や耳) が音の伝達に影響している状態を再現する

equalpowerよりもHRTFのほうがより高度な音響空間のアルゴリズムなので, (あくまで理論上は) HRTFのほうがより忠実に実際の音響空間をシミュレートしています.

paddingModelプロパティはオートパンの実装, および, デモでも利用しているので, 実際にどの程度の違いがあるのかを体感してもらえればと思います.

distanceModel

distanceModelプロパティは, 音がリスナーに伝達する際の音量減衰のアルゴリズムを決定するプロパティです. 実際の音響空間では, 近くの音は大きく, 遠くの音は小さくなるように音が伝達されます. つまり, その音響現象をWeb Audio APIでシミュレートするためのアルゴリズムを決定するプロパティということです. その詳細は, 表2 - 6 - cの数式を参照してください.

また, 現状の仕様では, distanceModelプロパティの型は文字列型ですが, 少し前の仕様ではそれに対応する数値型でした. distanceModelプロパティのデフォルト値は 'inverse' です.

表2 - 6 - c. distanceModelの値
StringNumberDescription
linear01 - rolloffFactor * (distance - refDistance) / (maxDistance - refDistance)
inverse1refDistance / (refDistance + rolloffFactor * (distance - refDistance))
exponential2pow(distance / refDistance, -rolloffFactor)

refDistance / maxDistance / rolloffFactor

これらのプロパティは, 表2 - 6 - cに記載しているように, 音量減衰を算出するための値です.

もっとも, 音量減衰を決定づけるのはrefDistanceプロパティです. refDistanceプロパティのデフォルト値は1です.

maxDistanceプロパティは, 音量減衰の限界となる距離を決定するプロパティです. つまり, この値を超える距離までパンとリスナーが離れても, それ以上の音量減衰は発生しません. また, maxDistanceプロパティのデフォルト値は10000です.

rolloffFactorプロパティは, 音量減衰の速さを決定するプロパティです. この値が小さいほど緩やかに音量減衰し, 大きいほど急激に音量減衰します. rolloffFactorプロパティのデフォルト値は1です.

coneInnerAngle / coneOuterAngle / coneOuterGain

向きをもたないパン (無指向性の音源) は, どの向きにおいても距離に応じて音量が決まります. 一方で, 向きをもつパン (指向性をもつ音源) は, 向きに応じて音量が変化します. 指向性をもつ音源の音量を表すモデルはサウンドコーンと呼ばれます.

サウンドコーンは, 内部コーン外部コーンによって構成されます.

coneInnerAngleプロパティは, 内部コーンの範囲を角度で決定するプロパティです. 内部コーンの内側では, 音量減衰が発生しません. coneInnerAngleプロパティのデフォルト値は360です.

coneOuterAngleプロパティは, 外部コーンの範囲を角度で決定するプロパティです. 内部コーンの外側で, かつ, 外部コーンの内側ではconeOuterGainプロパティで設定された音量減衰の減衰率に徐々に近づくように音量減衰が発生します. そして, 外部コーンの外側ではconeOuterGainプロパティで設定された値に応じて常に一定量の音量減衰が発生します. coneOuterAngleプロパティのデフォルト値は360です.

coneOuterGainプロパティは, 外部コーンの外側における音量減衰の減衰率を決定するプロパティです. coneOuterGainプロパティのデフォルト値は0です.

サウンドコーンを決定するこれら3つのプロパティがデフォルト値の場合, パンは向きをもちません (つまり, 無指向性の音源となります). またその場合, PannerNodeインスタンスのsetOrientationメソッドで指定するパンの向きはサウンドに影響しません.

サウンドコーンのイメージ

図2 - 6 - h. サウンドコーンのイメージ

サウンドコーンを決定する3つのプロパティがデフォルト値の場合, 無指向性の音源に (左側), coneInnerAngle / coneOuterAngleプロパティを設定すると指向性をもつ音源 (右側) になります. coneInnerAngleプロパティで指定した角度の内側 (青色の領域) では音量減衰は発生しません.

それより外側 (赤色の領域とそれより外側) では音量減衰が発生します. 音量減衰の範囲はconeOuterAngleプロパティで指定した角度, 音量減衰量はconeOuterGainプロパティで指定した値に応じます.

setPosition(x, y, z)

setPositionメソッドは, パンの位置を設定するメソッドです. setPositionメソッドは3つの引数を指定する必要があり, それぞれの引数が3D音響空間における, x座標・y座標・z座標の位置を表します.

デフォルトの位置は, (0, 0, 0) なので, 原点に位置することになります.

setOrientation(x, y, z)

setOrientationメソッドは, パンの向きを設定するメソッドです.

デフォルトの方向は, (1, 0, 0) なので, x軸のプラスの方向を向いている状態です.

setVelocity(x, y, z)

setVelocityメソッドは, パンの速度を設定するメソッドです. setVelocityメソッドは3つの引数を指定する必要があり, それぞれの引数が3次元のベクトルを構成するx・y・z方向の1次元のベクトルです. これらの3つのベクトルを合成したベクトルの大きさと向きが, 速さと移動する方向, すなわち, 速度を決定します.

デフォルトの速度は, (0, 0, 0) なので, 速度をもたない, つまり, 静止している状態です.

AudioListener

AudioListenerクラスを利用するためには, インスタンスを生成する必要がありますが, インスタンス生成のためのメソッドは定義されていません. その理由は, リスナーが複数存在する状況をシミュレートする意味がないからです. したがって, AudioListenerクラスはシングルトンパターンのような設計で, AudioContextインスタンスのlistenerプロパティがAudioListenerインスタンスとなります. つまり, AudioContextインスタンスを1つしか生成しなければ, AudioListenerインスタンスも1つしか存在しません.

PannerNodeインスタンスと比較すると, AudioListenerインスタンスのプロパティはそれほど多くありません. また, PannerNodeインスタンスと同じようなメソッドが定義されています.

表2 - 6 - d. AudioListenerインスタンスのプロパティ
PropertyDescriptionDefault
dopplerFactorドップラー効果のレベル1
speedOfSound音速343.3 [m/s]
setPosition(x, y, z)リスナーの位置を設定する(0, 0, 0)
setOrientation(x, y, z, xUp, yUp, zUp)リスナーの向きを設定する(0, 0, -1, 0, 1, 0)
setVelocity(x, y, z)リスナーの移動速度を設定する(0, 0, 0)

dopplerFactor / speedOfSound

これらのプロパティの作用を理解してコントロールするためには, ドップラー効果と呼ばれる物理現象の概要を把握している必要があります. ドップラー効果という物理現象はおそらく誰もが体験しています. その代表例として頻繁にとりあげられるのが, 救急車のサイレンです. 救急車のサイレンの周波数は一定ですが, 救急車が近づくにつれて音が高く, また, 遠ざかるにつれ音が低く聴こえます.

つまり, ドップラー効果とは, パン (音源) やリスナー (聴取者) が移動する (速度をもつ) ことによって生じる, 音の高さ (ピッチ) の変化ということです.

AudioListenerインスタンスには, ドップラー効果をシミュレートするためのプロパティが定義されています.

dopplerFactorプロパティはドップラー効果のレベルを決定するためです. 値が大きいほどドップラー効果のレベルが大きくなり, 音の高さの変化も激しくなります.

speedOfSoundプロパティは音速を表します. ドップラー効果に影響を与える物理量は, パンやリスナーの速度と音速です. デフォルト値は空気中の音速にほぼ等しい, 343.3 [m/s] です.

f_L = \left(\frac{V - v_L}{V - v_P}\right) \cdot f_P

ドップラー効果のイメージ

図2 - 6 - i. ドップラー効果のイメージと数式

Vは音速, v_Lはリスナーの速度, v_Pはパンの速度, f_Pはパンの周波数, そして, f_Lがリスナーが知覚する周波数です. 分数式の分母が小さくなる (= パンの速度があがる) ほど, f_Lが大きくなる, つまり, リスナーが知覚する音が高くなります.

また, イラストではパンが速度をもつことによるドップラー効果のイメージを表現していますが, 同じことはリスナーが速度をもつことによっても発生します.

もっとも, dopplerFactorとspeedOfSoundだけではドップラー効果をシミュレートすることはできません. ドップラー効果の原理にしたがうと, これは当然のことです. なぜなら, ドップラー効果は, パンやリスナーが移動することによって発生する物理現象だからです. パンやリスナーを移動させる, すなわち, パンやリスナーに速度を設定するには, PannerNodeインスタンス / AudioListenerインスタンスのsetVelocityメソッドを利用します.

setPosition(x, y, z)

setPositionメソッドは, リスナーの位置を設定するメソッドです. setPositionメソッドは3つの引数を指定する必要があり, それぞれの引数が3D音響空間における, x座標・y座標・z座標の位置を表します.

デフォルトの位置は, (0, 0, 0) なので, 原点に位置することになります.

setOrientation(x, y, z, xUp, yUp, zUp)

setOrientationメソッドは, リスナーの向きを設定するメソッドです. setOrientationメソッドで設定可能な向きは2つあり, 1つはリスナーの鼻が向いている方向と, もう1つはリスナーの頭のてっぺんが向いている方向です. setOrientationメソッドは6つの引数でこの2つの向きを設定します. 最初の3つの引数が, リスナーの鼻が向いている方向を表す3次元のベクトルです. そして, 残りの3つの引数が, リスナーの頭のてっぺんが向いている方向を表す3次元のベクトルです.

AudioListenerのsetOrientationメソッド

図2 - 6 - j. AudioListenerのsetOrientationメソッド

最初の3つの引数がリスナーの鼻の向き, 最後の3つの引数がリスナーの頭のてっぺんの向きを決定します.

デフォルトの方向は, (0, 0, -1, 0, 1, 0) なので, z軸のマイナスの方向 (つまり, 後方) を (頭のてっぺんがy軸のプラスの方向を向いているので) おおよそ水平に (目線が) 向いている状態です.

setVelocity(x, y, z)

setVelocityメソッドは, リスナーの速度を設定するメソッドです. setVelocityメソッドは3つの引数を指定する必要があり, それぞれの引数が3次元のベクトルを構成するx・y・z方向の1次元のベクトルです. これらの3つのベクトルを合成したベクトルの大きさと向きが, 速さと移動する方向, すなわち, 速度を決定します.

デフォルトの速度は, (0, 0, 0) なので, 速度をもたない, つまり, 静止している状態です.

残念ながら (?), Web Audio APIの次期仕様においては, AudioListenerのsetVelocityメソッドは削除される予定です. したがって, このメソッドを利用しているアプリケーションでは最新のブラウザの実装を確認しておきましょう.

最後に1つ注意点として, PannerNodeインスタンス / AudioListenerインスタンスのベクトル設定のメソッドの引数に単位は定義されていません. したがって, 作成するアプリケーションに応じた適切な値を調整をするようにしてください.

ChannelSplitterNode / ChannelMergerNode

ChannelSplitterNodeクラス / ChannelMergerNodeクラスの機能は, 入力されたサウンドを複数のチャンネル (トラック) に分割してそれぞれのチャンネル (トラック) ごとに必要なサウンド処理を適用可能な状態にすること, そして, それらの複数に分割されたチャンネル (トラック) のサウンドを1つのサウンドデータに統合 (ミックス) することです. いずれのメソッドも引数には, 分割するチャンネル数, および, 統合するチャンネル数を指定します. インスタンス生成で指定したチャンネル数は, connectメソッドの引数にも関係してきます.

イメージとしては, 1つの楽曲を入力 (= 入力ノード) すると, ChannelSplitterNodeは, ドラム・ベース・リズムギター・リードギター・ボーカルのそれぞれのパート (= チャンネル) に分解します. 分解することによって, ドラムのボリュームだけをもっと大きくしたり, ベースの低音部だけをブーストしたり, ボーカルだけにコーラスを少しかけたり…といったことが可能になります. もちろん, 分解したままでは楽曲にならないのでパート (= チャンネル) ごとに必要な処理をしたあとにChannelMergerNodeが1つの楽曲としてミックスします.

ChannelSplitterNode / ChannelMergerNode

図2 - 6 - k. ChannelSplitterNode / ChannelMergerNode

ChannelSplitterNodeによって, 分解された各チャンネルごとにサウンド処理を適用していること, および, ChannelMergerNodeによって, それらをミックスしていることに着目してください.

ChannelSplitterNodeクラス / ChannelMergerNodeクラスの利用ケースとしては, あとのセクションで解説するトレモロと併用したオートパンの実装が比較的簡単なので1つの好例となるでしょう. したがって, このセクションでは, ChannelSplitterNodeクラス / ChannelMergerNodeクラスのインスタンス生成とAudioNodeクラスからプロトタイプ継承しているconnectメソッドについて解説します.

インスタンス生成

ChannelSplitterNodeインスタンス / ChannelMergerNodeインスタンスを生成するためには, AudioContextインスタンスのcreateChannelSplitterメソッド / createChannelMergerメソッドを利用します.

サンプルコード 05


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();  // for input

// for legacy browsers
oscillator.start = oscillator.start || oscillator.noteOn;
oscillator.stop  = oscillator.stop  || oscillator.noteOff;

// Create the instance of ChannelSplitterNode
var splitter = context.createChannelSplitter(2);  // The number of splitted channels

// Create the instance of ChannelMergerNode
var merger = context.createChannelMerger(2);  // The number of merged channels

サンプルコード 05では左右のチャンネルごとに異なるサウンド処理を適用することを想定して2 (チャンネル) を指定しています. ちなみに, 引数を省略した場合のデフォルト値は6です. これは, 5.1チャンネルサラウンド方式を考慮しての値だと思います. また, 指定できる最大のチャンネル数は32で, それを超えるチャンネル数を指定すると例外が発生します.

connectメソッド

ChannelSplitterNodeクラスもChannelMergerNodeクラスもAudioNodeクラスをプロトタイプ継承しているので, connectメソッドを利用可能です. さらに, ChannelSplitterNode / ChannelMergerNodeを利用する場合のみ, connectメソッドの第2引数と第3引数が実質的に意味をもってきます.

ChannelSplitterNodeインスタンスのconnectメソッドは, AudioNode / AudioParamいずれの接続においても, 第2引数で (ChannelSplitterNodeからの) 出力のチャンネルを指定することが可能です. また, ChannelSplitterNodeにより分割されたノードのconnectメソッドはChannelMergerNodeに接続する場合に, 第3引数で (ChannelMergerNodeに対する) 入力のチャンネルを指定することが可能です.

ただし, 指定可能な最大のチャンネル数はインスタンス生成時に指定したチャンネル数です. チャンネルは0から割り当てられているので, (インスタンス生成時に指定したチャンネル数 - 1) が指定可能な最大のチャンネルです. それを超えるチャンネルを指定した場合はエラーが発生します.

サンプルコード 06


/*
 * sample code 01
 */

// ....

// Create the instance of GainNode
var gainL = context.createGain();  // for Left  Channel
var gainR = context.createGain();  // for Right Channel

gainL.gain.value = 1.00;
gainR.gain.value = 0.25;

// To Stereo
var processor = context.createScriptProcessor(2048, 1, 2);

oscillator.connect(processor);        // OscillatorNode (Monaural input) -> Stereo
processor.connect(splitter);          // ScriptProcessorNode (Stereo input) -> ChannelSplitterNode
splitter.connect(gainL, 0, 0);        // ChannelSplitterNode -> GainNode (Left  Channel)
splitter.connect(gainR, 1, 0);        // ChannelSplitterNode -> GainNode (Right Channel)
gainL.connect(merger, 0, 0);          // GainNode (Left  Channel) -> ChannelMergerNode (Left  Channel)
gainR.connect(merger, 0, 1);          // GainNode (Right Channel) => ChannelMergerNode (Right Channel)
merger.connect(context.destination);  // ChannelMergerNode -> AudioDestinationNode (Output)

processor.onaudioprocess = function(event) {
    var inputs   = event.inputBuffer.getChannelData(0);
    var outputLs = event.outputBuffer.getChannelData(0);
    var outputRs = event.outputBuffer.getChannelData(1);

    // Monaural -> Stereo
    outputLs.set(inputs);
    outputRs.set(inputs);
};

oscillator.start(0);

サンプルコード 06のノード接続

図2 - 6 - l. サンプルコード 06のノード接続

ChannelSplitterNodeによって, 分解された各チャンネルごとにサウンド処理を適用していること, および, ChannelMergerNodeによって, それらをミックスしていることに着目してください.

OscillatorNodeは1チャンネルしかもたないモノラルな入力なので, そのままChannelSplitterNodeに接続すると1チャンネル (左チャンネル) のみしか出力されないので, ScriptProcessorNodeを利用して擬似的にステレオ化しています (したがって, ステレオのオーディオデータを入力に利用する場合は必要ない処理です).

ChannelSplitterNodeからの出力を左チャンネル用のGainNodeと右チャンネル用のGainNodeにそれぞれ接続しています. ChannelSplitterNodeにより分割されたノード (左右チャンネル用のGainNode) をそれぞれ, ChannelMergerNodeの対応するチャンネルに入力ノードとして接続しています.

エフェクトとしては何もおもしろくないですが, 左チャンネルのGainNodeのgainプロパティを右チャンネルよりも大きくすることで, 左側に音源 (音像) があるように知覚することができると思います. これは左チャンネルからの出力しかないわけではありません (試しに, 右チャンネルだけで聴いてもボリュームは小さいですが出力されているはずです). 逆に, 右チャンネルのGainNodeのgainプロパティを左チャンネルよりも大きくすることで, 右側に音源 (音像) があるように知覚することができると思います.

サンプルコードではかなり単純で特に意味のない処理をチャンネルごとに実行しましたが, 何となくこんな感じで使うのかなということをインプットしてもらえれば十分です. もう少し実践的な実装は, オートパンの実装解説でいたします.

最後に, connectメソッドの引数指定 (connectメソッドのオーバーロード定義) をまとめておきます.

表2 - 6 - e. connectメソッドの引数 (オーバーロード定義)
1st2st3rd
ChannelSplitterNode / ChannelMergerNodeを利用する場合以外は指定不要
AudioNode出力チャンネル (デフォルト値 0)入力チャンネル (デフォルト値 0)
AudioParam出力チャンネル (デフォルト値 0)指定不可

オートパン

このセクションでは, オートパンの2つの実装方法, すなわち, PannerNodeクラス / AudioListenerクラスを利用した実装と, トレモロとChannelSplitterNodeクラス / ChannerlMergerNodeクラスを利用した実装を解説します.

PannerNode / AudioListenerを利用した実装

まずは, PannerNodeインスタンスを生成してノード接続を実装します.

また, AudioListerインスタンスはAudioContextインスタンスのlistenerプロパティで参照します. AudioListenerインスタンスは, AudioNodeクラスをプロトタイプ継承していないので, connectメソッドは利用できません (クラスの名前からもわかります. AudioNodeクラスをプロトタイプ継承しているクラスは 'Node' と接尾辞がつきます). 実は, PannerNodeをAudioDestinationNodeに (直接的, または, 間接的に) 接続することで有効になります. これは注意すべき点であり, AudioListenerだけを利用したいと思っても, PannerNodeの接続がなければ利用できないということです.

サンプルコード 01


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of PannerNode
var panner   = context.createPanner();

// Get the instance of AudioListener
var listener = context.listener;

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();  // for input

// for legacy browsers
oscillator.start = oscillator.start || oscillator.noteOn;
oscillator.stop  = oscillator.stop  || oscillator.noteOff;

// Connect nodes for effect (Auto Panner) sound
// OscillatorNode (Input) -> PannerNode (pan) -> AudioDestinationNode (Output)
oscillator.connect(panner);
panner.connect(context.destination);

次に, パンを周期的に変化させるためにLFOを実装します. 他のモジュレーション系エフェクトと異なり, オートパンのLFOの実装は少々異なります.

その理由は, PannerNodeインスタンスにはポジションを直接設定するためのプロパティが定義されておらず, setPositionメソッドで設定する必要があります.

また, イントロダクションで解説したconnectメソッドについて思い出してほしいのですが, connectメソッドが接続可能, つまり, connectメソッドの第1引数に指定できる型は, AudioNodeインスタンスかAudioParamインスタンスのみです. さらに言えば, 現状の仕様では, PannerNodeクラス / AudioListenerクラスともに, AudioParam型のプロパティは定義されていません.

LFOを実装するために必要なノードは, OscillatorNodeとScriptProcessorNodeです. ScriptProcessorNodeが必要な理由は, LFO (OscillatorNode) からの入力をPannerNodeインスタンスのsetPositionメソッドの引数に指定するためです. つまり, LFOからのサウンドデータに直接アクセスする必要があるからです.

では, LFOのためのインスタンス生成とノード接続を実装します.

サンプルコード 02


/*
 * sample code 01
 */

// ....

// Create the instance of OscillatorNode (for LFO)
var lfo = context.createOscillator();

// for leagcy browsers
lfo.start = lfo.start || lfo.noteOn;
lfo.stop  = lfo.stop  || lfo.noteOff;

// Set Rate
lfo.frequency.value = 0.5;  // 0.5 Hz

// for legacy browsers
context.createScriptProcessor = context.createScriptProcessor || context.createJavaScriptNode;

// for selecting optimized buffer size
var getBufferSize = function() {
    if (/(Win(dows )?NT 6\.2)/.test(navigator.userAgent)) {
        return 1024;  // Windows 8
    } else if (/(Win(dows )?NT 6\.1)/.test(navigator.userAgent)) {
        return 1024;  // Windows 7
    } else if (/(Win(dows )?NT 6\.0)/.test(navigator.userAgent)) {
        return 2048;  // Windows Vista
    } else if (/Win(dows )?(NT 5\.1|XP)/.test(navigator.userAgent)) {
        return 4096;  // Windows XP
    } else if (/Mac|PPC/.test(navigator.userAgent)) {
        return 1024;  // Mac OS X
    } else if (/Linux/.test(navigator.userAgent)) {
        return 8192;  // Linux
    } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
        return 2048;  // iOS
    } else {
        return 16384;  // Otherwise
    }
};

// Create the instance of ScriptProcessorNode for LFO
var processor = context.createScriptProcessor(getBufferSize(), 1, 1);

// Connect nodes for LFO that changes pan periodically
// OscillatorNode (LFO) -> ScriptProcessorNode (setPosition) (-> AudioDestinationNode (dummy))
lfo.connect(processor);
processor.connect(context.destination);

ScriptProcessorNodeの出力は必要ないので, createScriptProcessorメソッドの第3引数は0でもよさそうなのですが, ScriptProcessorNodeインスタンスのonaudioprocessイベントハンドラはAudioDestinationNodeに接続しないと発生しないので, 出力チャンネル数に1を指定 (createScriptProcessorメソッドの第3引数) します. createScriptProcessorメソッドの第2引数は入力チャンネル数ですが, LFOからの入力はデバイスに出力しないので, 1チャンネルで十分です.

そして, LFOの実装で最も重要となるのがonaudioprocessイベントハンドラです. もっとも, 単純に左右 (つまり, x軸方向) にパンを移動させるだけであればそれほど難しくありません. LFOから入力されたサウンドデータを, PannerNodeインスタンスのsetPositionメソッドの第1引数に指定するだけです (y, z軸方向への移動はさせないので, 0を指定します).

サンプルコード 03


/*
 * sample code 02
 */

// ....

processor.onaudioprocess = function(event) {
    // Get the instance of Float32Array for input data (Array size equals buffer size)
    var inputs  = event.inputBuffer.getChannelData(0);
    // var outputs = event.outputBuffer.getChannelData(0);  // Not used

    for (var i = 0; i < this.bufferSize; i++) {
        // Move pan
        panner.setPosition(inputs[i], 0, 0);
    }
}

// Start LFO
lfo.start(0);

これでパンが周期的に変化します. しかしながら, LFOからの入力は-1 〜 1までの値なので, パンの移動も-1 〜 1の範囲に限られてしまいます. そこで, LFOのdepthを設定することによって, パンの移動量を任意に設定することが可能になります.

サンプルコード 04


/*
 * sample code 03
 */

// ....

// Set Depth
var depth = 2.5;

processor.onaudioprocess = function(event) {
    // Get the instance of Float32Array for input data (Array size equals buffer size)
    var inputs  = event.inputBuffer.getChannelData(0);
    // var outputs = event.outputBuffer.getChannelData(0);  // Not used

    for (var i = 0; i < this.bufferSize; i++) {
        // Move pan
        var x = depth * inputs[i];
        panner.setPosition(x, 0, 0);
    }
}

// Effector (Auto Panner) ON
lfo.start(0);

オートパンのノード接続

図2 - 6 - m. オートパンのノード接続

PannerNodeを接続することによって, AudioListenerも有効になります. 他のモジュレーション系エフェクトと異なり, AudioParamインスタンスを変化させるのではなく, setPositionメソッドに指定する引数をLFOで周期的に変化させることに着目してください.

以上でオートパンの実装が完了しましたが, 1つだけ問題があります. それは, 空間音響アルゴリズムがHRTF, つまり, PannerNodeインスタンスのpanningModelプロパティがデフォルト値の 'HRTF' のままだと, 完全に左右に音が割り振られないことです. 空間音響アルゴリズムのついては, 他の書籍やWebサイトなど参考にしてもらいたいと思いますが, HRTF (Head - Related Transfer Function) とは頭部伝達関数のことで, 簡単に表現すると, 人の頭部 (頭や耳) が音の伝達に影響している状態を再現するためのアルゴリズムです. これによって, 完全に右側にパンがあったとしても, その状態をシミュレートするので, わずかながらですが左側にも音が回り込みます.

もちろん, これでもオートパンエフェクトの1つのバリエーションとしては利用できるので大きな問題ではありません. しかしながら, 完全に左右に音を割り振るバリエーションも実装したいところです. 空間音響アルゴリズムをequalpower, つまり, PannerNodeインスタンスのpanningModelプロパティを 'equalpower' に設定することによってそれが可能です.

HRTF / equalpowerのイメージ

図2 - 6 - n. HRTF / equalpowerのイメージ

HRTFは人の頭部 (頭や耳) が音の伝達に影響している状態を再現しているので, パンの位置の反対側にも音が伝達していることに着目してください.

サンプルコード 05


/*
 * sample code 04
 */

// ....

panner.panningModel = (typeof panner.panningModel === 'string') ? 'equalpower' : 0;

最後に, オートパンの実装をまとめます.

サンプルコード 06


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// Create the instance of PannerNode
var panner   = context.createPanner();

// Get the instance of AudioListener
var listener = context.listener;

// Create the instance of OscillatorNode
var oscillator = context.createOscillator();  // for input
var lfo        = context.createOscillator();  // for LFO

// for legacy browsers
oscillator.start = oscillator.start || oscillator.noteOn;
oscillator.stop  = oscillator.stop  || oscillator.noteOff;
lfo.start        = lfo.start        || lfo.noteOn;
lfo.stop         = lfo.stop         || lfo.noteOff;

// for legacy browsers
context.createScriptProcessor = context.createScriptProcessor || context.createJavaScriptNode;

// for selecting optimized buffer size
var getBufferSize = function() {
    if (/(Win(dows )?NT 6\.2)/.test(navigator.userAgent)) {
        return 1024;  // Windows 8
    } else if (/(Win(dows )?NT 6\.1)/.test(navigator.userAgent)) {
        return 1024;  // Windows 7
    } else if (/(Win(dows )?NT 6\.0)/.test(navigator.userAgent)) {
        return 2048;  // Windows Vista
    } else if (/Win(dows )?(NT 5\.1|XP)/.test(navigator.userAgent)) {
        return 4096;  // Windows XP
    } else if (/Mac|PPC/.test(navigator.userAgent)) {
        return 1024;  // Mac OS X
    } else if (/Linux/.test(navigator.userAgent)) {
        return 8192;  // Linux
    } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
        return 2048;  // iOS
    } else {
        return 16384;  // Otherwise
    }
};

// Create the instance of ScriptProcessorNode for LFO
var processor = context.createScriptProcessor(getBufferSize(), 1, 1);

// Connect nodes for effect (Auto Panner) sound
// OscillatorNode (Input) -> PannerNode (pan) -> AudioDestinationNode (Output)
oscillator.connect(panner);
panner.connect(context.destination);

// Connect nodes for LFO that changes pan periodically
// OscillatorNode (LFO) -> ScriptProcessorNode (setPosition) (-> AudioDestinationNode (dummy))
lfo.connect(processor);
processor.connect(context.destination);

// Set Depth
var depth = 2.5;

// Set Rate
lfo.frequency.value = 0.5;  // 0.5 Hz

processor.onaudioprocess = function(event) {
    // Get the instance of Float32Array for input data (Array size equals buffer size)
    var inputs  = event.inputBuffer.getChannelData(0);
    // var outputs = event.outputBuffer.getChannelData(0);  // Not used

    for (var i = 0; i < this.bufferSize; i++) {
        // Move pan
        var x = depth * inputs[i];
        panner.setPosition(x, 0, 0);
    }
}

// Start sound
oscillator.start(0);

// Effector (Auto Panner) ON
lfo.start(0);

以上でオートパンが完成しました. ところで, AudioListenerインスタンスにもsetPositionメソッドがあります. パン (音源) を移動させるのではなく, リスナー (聴取者) を移動させることによってオートパンを実装することも可能ですが, 初期状態のリスナーはz軸のマイナス方向を向いているので, リスナーを右側 (x > 0) に移動させると, パンは左側にあることになるので, 左側から音が聴こえ (音像が左側に位置する), 逆に, リスナーを左側 (x < 0) に移動させると, パンは右側にあることになるので, 右側から音が聞こえます (音像が右側に位置する). これを解決するには, リスナーの向きをsetOrientationメソッドで変更する必要があります. また, AudioListenerを利用するためには, 必ずPannerNodeが必要になるのでパンを移動させる実装を解説しました.

デモ 17では, パンの移動を単純に左右に移動させるだけではなく, PannerNodeにおけるz軸方向にも移動させることが可能になっています. panningModelプロパティのデフォルト値は 'HRTF' ですが, 'equalpower' にも設定可能です. パンが左右に移動しても, 'HRTF' の場合は左右から出力されること, 'equalpower' の場合はパンの方向からしか出力されないことを実際に試してみてください. また, Canvasに描画されているリスナー (ヘッドフォンのアイコン) はドラッグによって移動可能です. ドラッグした座標に対応するようにAudioListenerインスタンスのsetPositionメソッドを利用してリスナーを移動させています.

ちなみに, パン (スピーカーのイラスト) は右側を向いています. すなわち, setOrientationメソッドのデフォルトの設定 (1, 0, 0) を反映した状態になっています. しかしながら, サウンドコーンのためのプロパティはデフォルト値のままなので, 指向性をもちません. したがって, パンの向きはサウンドには影響していません.

デモ 17 * ヘッドフォンやイヤホンをご利用ください

トレモロとChannelSplitterNode / ChannerlMergerNodeを利用した実装

オートパンの実装方法としては, トレモロとChannelSplitterNodeクラス / ChannerlMergerNodeクラスを利用するアプローチもあります. トレモロについては, トレモロ・リングモジュレーターのページで解説していますので, トレモロって…何?という方は, ぜひご覧になってください.

ところで, トレモロとオートパン, すなわち, トレモロとパンの移動はまったく関係がないように思えます. なぜ, トレモロ (とチャンネル制御のためのクラス) を利用してオートパンが実装可能なのでしょうか?それは, 左右のチャンネルに対して同様にトレモロをかけるのではなく, 振幅の大小が互い違いになる (例えば, 左チャンネルの振幅が1であれば, 右チャンネルの振幅が0) ようにトレモロをかけることによって, 擬似的にパンが移動しているように知覚させることが可能だからです.

トレモロによるオートパン

図2 - 6 - o. トレモロによるオートパン

左右のチャンネルの振幅の大小がちょうど互い違いになるように出力することによって, パン (音像) が左右に移動しているように知覚させることが可能になります.

したがって, トレモロの実装にプラスしてChannelSplitterNodeクラス / ChannerlMergerNodeクラスが必要になるわけです.

もっとも, 擬似的なパンの移動なので, PannerNodeクラスを利用した場合のように, y軸やz軸方向にパンを移動させたり, 空間音響のアルゴリズムを指定したりすることはできないので, オートパンのバリエーションとしては劣ってしまいますが, ChannelSplitterNodeクラス / ChannerlMergerNodeクラスの実用例としては最適なので解説しておきたいと思います.

まずは, エフェクト音の出力のためのノード生成と接続を実装します.

サンプルコード 07


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// for the instance of MediaElementAudioSourceNode
var source = null;

// for legacy browsers
context.createGain = context.createGain || context.createGainNode;

// Create the instance of GainNode
var amplitudeL = context.createGain();              // for Left  Channel Tremolo
var amplitudeR = context.createGain();              // for Right Channel Tremolo
var splitter   = context.createChannelSplitter(2);  // for reversing Tremolo each channel
var merger     = context.createChannelMerger(2);    // for reversing Tremolo each channel

amplitudeL.gain.value = 1;  // 1 +- depth
amplitudeR.gain.value = 1;  // 1 +- depth

var audio = new Audio('sample.wav');

audio.addEventListener('loadstart', function() {
    // Create the instance of MediaElementAudioSourceNode
    source = context.createMediaElementSource(audio);

    // Connect nodes for effect (Auto Panner) sound
    //                                                             -> GainNode for Left  Channel (amplitude) -
    // MediaElementAudioSourceNode (Input) -> ChannelSplitterNode -|                                           |-> ChannelMergerNode -> AudioDestinationNode (Output)
    //                                                             -> GainNode for Right Channel (amplitude) -
    source.connect(splitter);
    splitter.connect(amplitudeL, 0, 0);
    splitter.connect(amplitudeR, 1, 0);
    amplitudeL.connect(merger, 0, 0);
    amplitudeR.connect(merger, 0, 1);
    merger.connect(context.destination);
}, false);

OscillatorNodeは1チャンネルしかもたないモノラルな入力なので, 入力ノードはオーディオデータをもつMediaElementAudioSourceNodeにしています. OscillatorNodeを入力ノードにする場合, サンプルコード 02を参考にしてください.

まず, チャンネル数は2つ必要なのでcreateChannelSplitterメソッド / createChannelMergerメソッドの引数に2を指定してインスタンスを生成します. そして, 入力ノードをChannelSplitterNodeに接続することで, 左右のチャンネルごとに異なるサウンド処理が可能になります.

チャンネルごとに適用したいサウンド処理とは, 振幅の大小が互い違いになるようにトレモロをかけることです. チャンネルごとにトレモロをコントロールできるように, 分割した左右のチャンネルをそれぞれチャンネルごとに生成しておいたトレモロのためのGainNodeに接続します.

チャンネルごとにトレモロをかけたら, 1つのオーディオデータに統合するために, ChannelMergerNodeに接続します.

あとは, トレモロのためのLFOを実装すれば完成です. まずは, LFOのためのノードを生成します.

サンプルコード 08


/*
 * sample code 07
 */

// ....

// for LFO

// for legacy browsers
context.createScriptProcessor = context.createScriptProcessor || context.createJavaScriptNode;

// for selecting optimized buffer size
var getBufferSize = function() {
    if (/(Win(dows )?NT 6\.2)/.test(navigator.userAgent)) {
        return 1024;  // Windows 8
    } else if (/(Win(dows )?NT 6\.1)/.test(navigator.userAgent)) {
        return 1024;  // Windows 7
    } else if (/(Win(dows )?NT 6\.0)/.test(navigator.userAgent)) {
        return 2048;  // Windows Vista
    } else if (/Win(dows )?(NT 5\.1|XP)/.test(navigator.userAgent)) {
        return 4096;  // Windows XP
    } else if (/Mac|PPC/.test(navigator.userAgent)) {
        return 1024;  // Mac OS X
    } else if (/Linux/.test(navigator.userAgent)) {
        return 8192;  // Linux
    } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
        return 2048;  // iOS
    } else {
        return 16384;  // Otherwise
    }
};

// Create the instance of OscillatorNode
var lfo = context.createOscillator();

// for legacy browsers
lfo.start = lfo.start || lfo.noteOn;
lfo.stop  = lfo.stop  || lfo.noteOff;

var depth       = context.createGain();
var lfoSplitter = context.createChannelSplitter(2);
var processor   = context.createScriptProcessor(getBufferSize(), 2, 2);

var audio = new Audio('sample.wav');

// ....

重要なポイントは, トレモロをコントロールするためのScriptProcessorNodeインスタンスを生成していることと, LFOのためのChannelSplitterNodeインスタンスを生成していることです.

ScriptProcessorNodeを利用することでサウンドデータに直接アクセスすることが可能となり, 分割した左右のチャンネルの振幅の大小が互い違いになるように実装することができます.

あとは, ScriptProcessorNodeによって振幅の大小が互い違いになった左右のチャンネルからのサウンド出力を, トレモロのためのGainNodeインスタンスのgainプロパティ (AudioParamインスタンス) に対して, チャンネルごとに入力する必要があります. そのために, ChannelSpitterNodeを利用します.

サンプルコード 09


/*
 * sample code 08
 */

// ....

audio.addEventListener('loadstart', function() {
    // Create the instance of MediaElementAudioSourceNode
    source = context.createMediaElementSource(audio);

    // Connect nodes for effect (Auto Panner) sound
    //                                                             -> GainNode for Left  Channel (amplitude) -
    // MediaElementAudioSourceNode (Input) -> ChannelSplitterNode -|                                           |-> ChannelMergerNode -> AudioDestinationNode (Output)
    //                                                             -> GainNode for Right Channel (amplitude) -
    source.connect(splitter);
    splitter.connect(amplitudeL, 0, 0);
    splitter.connect(amplitudeR, 1, 0);
    amplitudeL.connect(merger, 0, 0);
    amplitudeR.connect(merger, 0, 1);
    merger.connect(context.destination);

    // Connect nodes for LFO that changes amplitude periodically
    //                                                                                                         -> gain (GainNode for Left  Channel)
    // OscillatorNode (LFO) -> GainNode (depth) -> ScriptProcessorNode (reverse amplitude) -> ChannelSplitter -|
    //                                                                                                         -> gain (GainNode for Right Channel)
    lfo.connect(depth);
    depth.connect(processor);
    processor.connect(lfoSplitter);
    lfoSplitter.connect(amplitudeL.gain, 0);
    lfoSplitter.connect(amplitudeR.gain, 1);

    processor.onaudioprocess = function(event) {
        // Get the instance of Float32Array for input data (Array size equals buffer size)
        var inputLs = event.inputBuffer.getChannelData(0);  // Left  channel
        var inputRs = event.inputBuffer.getChannelData(1);  // Right channel

        // Get the instance of Float32Array for output data (Array size equals buffer size)
        var outputLs = event.outputBuffer.getChannelData(0);  // Left  channel
        var outputRs = event.outputBuffer.getChannelData(1);  // Right channel

        // Reverse amplitude each channel
        for (var i = 0; i < this.bufferSize; i++) {
            outputLs[i] =  1 * inputLs[i];  // GainNode.gain (1) + outputLs[i]
            outputRs[i] = -1 * inputRs[i];  // GainNode.gain (1) - outputRs[i]
        }
    };

    // Start audio
    audio.play();

    // Effector (Auto Panner) ON
    lfo.start(0);

}, false);

onaudioprocessイベントハンドラの出力のサウンドデータを設定する処理で, 分割した左右のチャンネルの振幅の大小が互い違いになるように, 右チャンネルの出力は右チャンネルからの入力に-1を乗算しています.

トレモロの数式で表現すると図2 - 6 - oような関係になっています.

L(n) = 1 + depth \cdot \sin \left(\frac{2{\pi} \cdot rate \cdot n}{f_s}\right)

R(n) = 1 - depth \cdot \sin \left(\frac{2{\pi} \cdot rate \cdot n}{f_s}\right)

図2 - 6 - p. 左チャンネルと右チャンネルのトレモロ

最後に, オートパンの実装をまとめます.

サンプルコード 10


window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Create the instance of AudioContext
var context = new AudioContext();

// for the instance of MediaElementAudioSourceNode
var source = null;

// for legacy browsers
context.createGain = context.createGain || context.createGainNode;

// Create the instance of GainNode
var amplitudeL = context.createGain();              // for Left  Channel Tremolo
var amplitudeR = context.createGain();              // for Right Channel Tremolo
var splitter   = context.createChannelSplitter(2);  // for reversing Tremolo each channel
var merger     = context.createChannelMerger(2);    // for reversing Tremolo each channel

amplitudeL.gain.value = 1;  // 1 +- depth
amplitudeR.gain.value = 1;  // 1 +- depth

// for LFO

// for legacy browsers
context.createScriptProcessor = context.createScriptProcessor || context.createJavaScriptNode;

// for selecting optimized buffer size
var getBufferSize = function() {
    if (/(Win(dows )?NT 6\.2)/.test(navigator.userAgent)) {
        return 1024;  // Windows 8
    } else if (/(Win(dows )?NT 6\.1)/.test(navigator.userAgent)) {
        return 1024;  // Windows 7
    } else if (/(Win(dows )?NT 6\.0)/.test(navigator.userAgent)) {
        return 2048;  // Windows Vista
    } else if (/Win(dows )?(NT 5\.1|XP)/.test(navigator.userAgent)) {
        return 4096;  // Windows XP
    } else if (/Mac|PPC/.test(navigator.userAgent)) {
        return 1024;  // Mac OS X
    } else if (/Linux/.test(navigator.userAgent)) {
        return 8192;  // Linux
    } else if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
        return 2048;  // iOS
    } else {
        return 16384;  // Otherwise
    }
};

// Create the instance of OscillatorNode
var lfo = context.createOscillator();

// for legacy browsers
lfo.start = lfo.start || lfo.noteOn;
lfo.stop  = lfo.stop  || lfo.noteOff;

var depth       = context.createGain();
var lfoSplitter = context.createChannelSplitter(2);
var processor   = context.createScriptProcessor(getBufferSize(), 2, 2);

var audio = new Audio('sample.wav');

audio.addEventListener('loadstart', function() {
    // Create the instance of MediaElementAudioSourceNode
    source = context.createMediaElementSource(audio);

    // Connect nodes for effect (Auto Panner) sound
    //                                                             -> GainNode for Left  Channel (amplitude) -
    // MediaElementAudioSourceNode (Input) -> ChannelSplitterNode -|                                           |-> ChannelMergerNode -> AudioDestinationNode (Output)
    //                                                             -> GainNode for Right Channel (amplitude) -
    source.connect(splitter);
    splitter.connect(amplitudeL, 0, 0);
    splitter.connect(amplitudeR, 1, 0);
    amplitudeL.connect(merger, 0, 0);
    amplitudeR.connect(merger, 0, 1);
    merger.connect(context.destination);

    // Connect nodes for LFO that changes amplitude periodically
    //                                                                                                         -> gain (GainNode for Left  Channel)
    // OscillatorNode (LFO) -> GainNode (depth) -> ScriptProcessorNode (reverse amplitude) -> ChannelSplitter -|
    //                                                                                                         -> gain (GainNode for Right Channel)
    lfo.connect(depth);
    depth.connect(processor);
    processor.connect(lfoSplitter);
    lfoSplitter.connect(amplitudeL.gain, 0);
    lfoSplitter.connect(amplitudeR.gain, 1);

    processor.onaudioprocess = function(event) {
        // Get the instance of Float32Array for input data (Array size equals buffer size)
        var inputLs = event.inputBuffer.getChannelData(0);  // Left  channel
        var inputRs = event.inputBuffer.getChannelData(1);  // Right channel

        // Get the instance of Float32Array for output data (Array size equals buffer size)
        var outputLs = event.outputBuffer.getChannelData(0);  // Left  channel
        var outputRs = event.outputBuffer.getChannelData(1);  // Right channel

        // Reverse amplitude each channel
        for (var i = 0; i < this.bufferSize; i++) {
            outputLs[i] =  1 * inputLs[i];  // GainNode.gain (1) + outputLs[i]
            outputRs[i] = -1 * inputRs[i];  // GainNode.gain (1) - outputRs[i]
        }
    };

    // Set Depth
    depth.gain.value = 0.8;

    // Set Rate
    lfo.frequency.value = 0.5;  // 0.5 Hz

    // Start audio
    audio.play();

    // Effector (Auto Panner) ON
    lfo.start(0);

}, false);

オートパンのノード接続

図2 - 6 - q. オートパンのノード接続

LFOからの入力をScriptProcessorNodeを利用して振幅の大小が互い違いになるように処理していること. そして, その出力をChannelSplitterNodeで分解して, チャンネルごとのGainNodeインスタンスのgainプロパティに接続していることが重要です.

以上でトレモロとChannelSplitterNodeクラス / ChannerlMergerNodeクラスによるオートパンの実装が完成しました. このセクションの最初で述べたように, PannerNodeクラス / AudioListenerクラスを利用した場合と比較すると, エフェクトのバリエーションは劣りますが, オートパンとしては十分に機能します. デモ 19を試してみて, ChannelSplitterNodeクラス / ChannerlMergerNodeクラスの機能を体感してみてください.

デモ 19 * ヘッドフォンやイヤホンをご利用ください

オートパン まとめ

このページでは, オートパンを実装するために必要なPannerNodeクラス / AudioListenerクラスの解説と実装, また, その基礎となる3D音響空間の座標系とベクトルの解説もしました.

トレモロとChannelSplitterNodeクラス / ChannerlMergerNodeクラスを利用したオートパンの実装まで解説したので, かなり量も多く大変だったかなと思います.

まとめとしては, とりあえずオートパンを実装するために重量なポイントを記載しておきます.

表2 - 6 - f. オートパン実装のポイント
ImplementsKey Points
PannerNode + AudioListener
  • PannerNodeインスタンスのsetPositionメソッドとpanningModelプロパティが最も重要なインターフェース
  • PannerNodeを出力ノードに接続することによって, AudioListenerが有効になる
トレモロ + ChannelSplitterNode / ChannerlMergerNode
  • ChannelSplitterNodeでチャンネルごとのGainNodeに接続可能にして, それらの出力をChannelMergerNodeでミックスする
  • ChannelSplitterNodeでLFOからの入力を2チャンネルに分割して, 振幅の大小が互い違いになるようにする