RSS Twitter Facebook

2019/02/01 (2019年02月 のアーカイブ)

[Web Audio API] 既存ノードを組み合わせたカスタムノードの作り方


少し前に GitHub にサンプルのリポジトリだけ置いてあったのですが、これは Web Audio API で幾つかのノードを組み合わせて独自のノードを作る方法の説明です。処理をすべてスクラッチから作るなら AudioWorkletNode を使えば良いのですが、単に既存のノードを組み合わせて1つのノードとして動作させるには少し裏技的な手法が必要になります。

GitHub : g200kg/webaudio-customnode

基本的に Web Audio API の各種ノードの機能はかなり細かく分けられていますので、一般的なエフェクターのようにまとまった機能のものを作るには幾つかのノードを組み合わせる必要があります。典型的な例がディレイエフェクトで、Web Audio API の DelayNode は信号を指定の時間遅らせるだけのものですから、エフェクター的ないわゆるディレイ効果を作るには、元音とのミックスや音を繰り返させるフィードバックの機能が欲しい所です。

上の図が今回作るサンプルのカスタムノード、MyDelayNode の構造です。入力端及び出力端は必ずしも GainNode でなくても良いのですが、単一の connect() メソッドでの接続ができるように信号が 1 本にまとまっている必要があります。

この MyDelayNode を標準の1つのノードと同等の方法で使えるようにします。つまり、
  1. var myNode = new MyNode(audiocontext, option) // コンストラクタでノードを作成できる。
  2. myNode.parameter.setValueAtTime(0,0) // 必要なパラメータにアクセスしオートメーションも使える。
  3. myNode.connect(otherNode) // ノードから出力を接続できる。
  4. otherNode.connect(myNode) // このノードに対して接続できる。
これを class にしたものが下のコードです。


class MyDelayNode extends GainNode {
  constructor(actx,opt){
    super(actx);
    // Internal Nodes
    this._delay = new DelayNode(actx, {delayTime:0.5});
    this._mix = new GainNode(actx, {gain:0.5});
    this._feedback = new GainNode(actx, {gain:0.5});
    this._output = new GainNode(actx, {gain:1.0});
    // Export parameters
    this.delayTime = this._delay.delayTime;
    this.feedback = this._feedback.gain;
    this.mix = this._mix.gain;
    // Options setup
    for(let k in opt){
      switch(k){
      case 'delayTime': this.delayTime.value = opt[k];
        break;
      case 'feedback': this.feedback.value = opt[k];
        break;
      case 'mix': this.mix.value = opt[k];
        break;
      }
    }
    this._inputConnect = this.connect;   // input side, connect of super class
    this.connect = this._outputConnect;  // connect() method of output
    this._inputConnect(this._delay).connect(this._mix).connect(this._output);
    this._inputConnect(this._output);
    this._delay.connect(this._feedback).connect(this._delay);
  }
  _outputConnect(to,output,input){
    return this._output.connect(to,output,input);
  }
}
ポイントはこのカスタムノードに対する接続の処理の仕方です。他のノードから connect() メソッドで接続するためには、このノード自体が AudioNode である必要があるので、既存ノードの子クラスとして作成します。
  • 1 行目、クラスは既存のノード、例えば GainNode の子クラスとして extends で派生させます。適当に作ったクラスでは他のノードからの接続ができません。この親クラスのノードが入力端のノードとなります。
  • 24、25 行目、このカスタムノードからの出力となる connect() メソッドは子クラス側でオーバーライドすれば良いのですが、その前に元の connect() メソッドを確保しておきます。ここで確保した inputConnect で、入力端子である親クラスの GainNode に入ってくる信号を取り出す事ができます。
  • 4 ~ 8 行目、内部で使用するノードです。
  • 9 ~ 12 行目、カスタムノードとして必要なパラメータを外に引っ張り出します。
MyDelayNode サンプル


もう1つの例、ピンポンディレイです。これはディレイ音が左右のチャンネルから交互に出るエフェクトで、DelayNode を分割して途中からタップを出し、左右のチャンネルに振り分けています。

入出力の接続に関しては前のサンプルと同様ですが、ここで問題になるのは、2 つある DelayNode の delayTime パラメータをどうやって制御するかという点です。単一の AudioParam 型パラメータから 2 つの DeleyNode を連動して動かす必要があります。これには ConstantSourceNode が使えます。ConstantSourceNode の offset パラメータを外部に対して delayTime パラメータとして露出させ、内部では connect() メソッドによるノード間の接続で DelayNode の delayTime に接続します。


class MyPingPongDelayNode extends GainNode {
  constructor(actx,opt){
    super(actx);
    // Internal Nodes
    this._delay1 = new DelayNode(actx, {delayTime:0.5});
    this._delay2 = new DelayNode(actx, {delayTime:0.5});
    this._merger = new ChannelMergerNode(actx);
    this._constant = new ConstantSourceNode(actx, {offset:0.5});
    this._mix = new GainNode(actx, {gain:0.5});
    this._feedback = new GainNode(actx, {gain:0.5});
    this._output = new GainNode(actx, {gain:1.0});
    // Export parameters
    this.delayTime = this._constant.offset;
    this.feedback = this._feedback.gain;
    this.mix = this._mix.gain;
    // Options setup
    for(let k in opt){
      switch(k){
      case 'delayTime': this.delayTime.value = opt[k];
        break;
      case 'feedback': this.feedback.value = opt[k];
        break;
      case 'mix': this.mix.value = opt[k];
        break;
      }
    }
    this._inputConnect=this.connect;   // input side, connect of super class
    this.connect=this._outputConnect;  // connect() method of output
    this._inputConnect(this._delay1).connect(this._delay2).connect(this._merger,0,0).connect(this._mix).connect(this._output);
    this._delay1.connect(this._merger,0,1);
    this._inputConnect(this._output);
    this._delay2.connect(this._feedback).connect(this._delay1);
    this._constant.connect(this._delay1.delayTime);
    this._constant.connect(this._delay2.delayTime);
    this._constant.start();
  }
  _outputConnect(to, output, input){
    return this._output.connect(to, output, input);
  }
}

MyPingPongDelayNode サンプル


このような手法で class の登録さえすれば、標準のノードと同様の使い方が可能な、独自のノードが使えるようになります。あまり複雑な構造になりすぎるとパフォーマンスに影響が出てきそうではありますが、内部のノードの JavaScript の処理をノードの接続で置き換えるのがポイントになります。

なお、Web Audio API の V2 では、複数のノードを組み合わせたものを格納できるコンテナとなるノードの検討がされるようです。これが使えるようになれば、もっと自然な方法でこういう事ができるようになると思いますが、現在の仕様内でもやり方を工夫すれば同様の事ができますので、覚えておけば役に立つ事もあるかも知れません。

Posted by g200kg : 2019/02/01 04:13:36