ディレイ・リバーブとは?

ディレイ・リバーブがどんなエフェクトかを簡単に表現すると, ディレイはやまびこ現象を再現するエフェクト, リバーブはコンサートホールなどの音の響きを再現するエフェクトとなるでしょう.

まずは, 実際にそのエフェクトを体感してみてください.

もっとも, 表現上はまったく別のエフェクトのように思えますが, その原理はほとんど同じです. また, ディレイのパラメータの設定しだいでは, リバーブようなエフェクトを再現することも可能です

原理がほとんど同じと表現したのは, ディレイ・リバーブともに, 遅延という処理が重要な処理となっているからです. 遅延処理によって生成された音を原音 (元の音) とミックスすることでディレイ・リバーブは実現されます. そして, 遅延音に対する演算処理の違いがディレイとリバーブの違いを生み出しています.

ディレイ

ディレイを実装するために必要な処理は, DelayNodeクラスの利用とフィードバックです.

DelayNode

遅延処理を実現するために定義されているのが, DelayNodeクラスです. また, DelayNodeインスタンスを生成するには, AudioContextインスタンスのcreateDelayメソッドを利用します. ちなみに, 初期の仕様においては, createDelayメソッドは定義されておらず, 代わりにcreateDelayNodeメソッドを利用してました. したがって, サンプルコード 01のようにフォールバックを記述するほうが安全です.

サンプルコード 01


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

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

// for legacy browsers
context.createDelay = context.createDelay || context.createDelayNode;

// Max delay time
var MAX_DELAY_TIME = 5;  // 5 sec

// Create the instance of DelayNode
var delay = context.createDelay(MAX_DELAY_TIME);

createDelayメソッドの第1引数には, 遅延時間 (ディレイタイム) の最大値秒単位で指定します. ちなみに, 省略した場合のデフォルト値は1 (秒) です.

DelayNodeインスタンスには, AudioParamインスタンスのdelayTimeプロパティが定義されています. これが, 遅延時間 (ディレイタイム) を決定づけるプロパティです. 最小値は0 (秒), 最大値はcreateDelayメソッドの引数に指定した値です.

サンプルコード 02


/*
 * Add code to sample code 01
 */

// ....

// Set delay time
delay.delayTime.value = 2.5;  // 2.5 sec

そして, 遅延した音を生成するには, サウンドの入力点となるノード (OscillatorNodeなど) をDelayNodeに接続します. DelayNodeクラスは, AudioNodeクラスを (プロトタイプ) 継承しているので, DelayNodeインスタンスからconnectメソッドを利用して遅延した音を他のノードへ出力することが可能です.

サンプルコード 03


/*
 * Add code to sample code 02
 */

// ....

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

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

// Connect nodes for original sound
// OscillatorNode (Input) -> AudioDestinationNode (Output)
oscillator.connect(context.destination);

// Connect nodes for effect (Delay) sound
// OscillatorNode (Input) -> DelayNode (Delay) -> AudioDestinationNode (Output)
oscillator.connect(delay);
delay.connect(context.destination);

oscillator.start(0);

サンプルコード 03では, サウンド出力点であるAudioDestinationNodeに対して2つの入力ノードが接続されています. 1つは, 原音を出力するため, そして, もう1つは遅延音を出力するためです. このように, 複数のノードを入力ノードとして接続することで, それぞれ入力された音をミックスすることが可能になります. これは, ディレイだけでなく他のエフェクターを実装する場合においても, 原音とエフェクト音をミックスするという処理は必要になります.

DelayNodeの接続

図2 - 2 - a. DelayNodeの接続

これで, ディレイが完成しました…と言いたいところですが, これだけでは, エフェクターとしてのディレイは実現できていません. ちょっと微妙な表現をしたのは, サンプルコード 03でも遅延した音は発生します. しかしながら, やまびこ現象のように, 遅延音が少しずつ減衰しながら何度も繰り返し生成することが実現できていない (つまり, エフェクターとしてのディレイではない) からです.

別の視点で言えば, DelayNodeの役割は, あくまでも指定された遅延時間で遅延音を1つだけ生成することだからです. すなわち, エフェクターのディレイを実現するという役割までは担いません.

エフェクターとしてのディレイを実装するには, DelayNodeの利用と次のセクションで解説するフィードバックという処理が必要になります.

フィードバック

フィードバックとは, 出力された音を入力音として利用することです.

ちなみに, エレキギターではフィードバック奏法と呼ばれる奏法があります. この奏法は, アンプから出力された音 (つまり, 空気の振動) で弦を振動させて, それをピックアップが拾い, 再びアンプから出力させることで, 理論上, 永遠の音の伸びを奏でる奏法です.

すなわち, DelayNodeインスタンスによって出力された遅延音を, 再び入力音とすればディレイを実現することが可能です. これを, Web Audio APIで実装するためには, フィードバックのためのGainNodeインスタンスを生成して, その入出力に同じDelayNodeインスタンスを接続します.

サンプルコード 04


/*
 * Add code to sample code 02
 */

// ....

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

// Create the instance of GainNode
var feedback = context.createGain();  // for feedback

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

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

// Connect nodes for original sound
// OscillatorNode (Input) -> AudioDestinationNode (Output)
oscillator.connect(context.destination);

// Connect nodes for effect (Delay) sound
// OscillatorNode (Input) -> DelayNode (Delay) -> AudioDestinationNode (Output)
oscillator.connect(delay);
delay.connect(context.destination);

// Connect nodes for Feedback
// (OscillatorNode (Input) ->) DelayNode (Delay) -> GainNode (Feedback) -> DelayNode -> ...
delay.connect(feedback);
feedback.connect(delay);

oscillator.start(0);

サンプルコード 04では, 大きく3つの接続が作成されました.

  • 原音を出力する接続
  • エフェクト音 (遅延音) を出力する接続
  • フィードバック (エフェクト音を再び入力する) のための接続
フィードバックの接続

図2 - 2 - b. フィードバックの接続

フィードバック (エフェクト音を再び入力する) の接続によって, 遅延音が少しずつ減衰しながら何度も繰り返し生成されます. 具体例として, 原音のゲインを1, フィードバッグのゲインが0.5とすると, 1つめの遅延音のゲインは, 0.5 (1 x 0.5), 2つめの遅延音のゲインは, 0.25 (0.5 x 0.5), 3つめの遅延音のゲインは, 0.125 (0.25 x 0.5) … という感じで, 遅延音が少しずつ減衰しながら繰り返し生成されます. つまり, フィードバックの役割を担う, GainNodeのgainプロパティを変更すれば, 遅延音の生成にバリエーションが出せるというこでもあります.

フィードバックのイメージ

図2 - 2 - c. フィードバックのイメージ

これで, ディレイのコアとなる実装が完成しましたが, もう1つ機能を実装します. それは, 原音とエフェクト音のゲインを調整できるようにすることです. といっても, 難しいことではなく, GainNodeインスタンスを上記3つの接続のうち, 原音を出力する接続とエフェクト音 (遅延音) を出力する接続に追加するだけです.

サンプルコード 04


/*
 * Add code to sample code 02
 */

// ....

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

// Create the instance of GainNode
var dry      = context.createGain();  // for gain of original sound
var wet      = context.createGain();  // for gain of effect (Delay) sound
var feedback = context.createGain();  // for feedback

// Set parameters
dry.gain.value      = 0.7;
wet.gain.value      = 0.3;
feedback.gain.value = 0.5;

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

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

// Connect nodes for original sound
// OscillatorNode (Input) -> GainNode (Dry) -> AudioDestinationNode (Output)
oscillator.connect(dry);
dry.connect(context.destination);

// Connect nodes for effect (Delay) sound
// OscillatorNode (Input) -> DelayNode (Delay) -> GainNode (Wet) -> AudioDestinationNode (Output)
oscillator.connect(delay);
delay.connect(wet);
wet.connect(context.destination);

// Connect nodes for Feedback
// (OscillatorNode (Input) ->) DelayNode (Delay) -> GainNode (Feedback) -> DelayNode (Delay) -> ...
delay.connect(feedback);
feedback.connect(delay);

oscillator.start(0);

Dry / Wetの接続

図2 - 2 - d. Dry / Wetの接続

このような接続を追加することによって, 原音とエフェクト音のゲインを調整するだけで, エフェクトにさらなるバリエーションが生まれます. さらに, ノード接続を変更することなくエフェクトのON / OFFを切り替えることが可能になります. 例えば, 原音のゲイン (Dry) を1, エフェクト音のゲイン (Wet) を0にすれば, エフェクトOFF (つまり, 原音のみ) のサウンドになります. また, 原音のためのゲイン (Dry) を0, エフェクト音のゲイン (Wet) を0より大きくすれば, エフェクト音のみのサウンドになります.

原音とエフェクト音のゲインを調整する機能は, 他のエフェクターでも実装します. また, 現実世界のエフェクターにおいて, これらのパラメータはDry (原音) / Wet (エフェクト音) としてコントロール可能になっているものが多いです.

ディレイのノード接続

図2 - 2 - e. ディレイのノード接続

以上でディレイが完成しました. さっそく, デモ 02・デモ 03で試してみてください. 基本となるディレイタイムと, Dry / Wet / フィードバックの値を組み合わせてお好みのディレイを発見してください. また, フィードバックが0のとき, 遅延音が1つしか生成されないこともぜひ確認してみてください.

デモ 02 (サウンド)

デモ 03 (オーディオ)

リバーブ

リバーブがディレイと実装上異なる点は, 遅延音の値を算出するときにフィードバック値を利用するのではなく, シミュレートしたい音響空間のインパルス応答と呼ばれるオーディオデータを利用します. インパルス応答の詳細に関しては, あとのセクションで解説しますので, とりあえず, リバーブを実装してみましょう.

ConvolverNode

インパルス応答のオーディオデータをエフェクトとして利用するには, ConvolverNodeクラスを利用します. AudioContextインスタンスのcreateConvolverメソッドでインスタンスを生成可能です.

サンプルコード 05


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

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

// Create the instance of ConvolverNode
var convolver = context.createConvolver();

ConvolverNodeインスタンスには, bufferプロパティが定義されており, このbufferプロパティに, インパルス応答のオーディオデータのAudioBufferインスタンスを設定します. AudioBufferインスタンスの生成処理は, 楽曲データやワンショットサンプルのオーディオデータと同じです. すなわち, オーディオデータのArrayBufferとdecodeAudioDataメソッドを利用します.

サンプルコード 06


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

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

var xhr = new XMLHttpRequest();
var url = 'http:// xxx.jp/reverb.wav';

// XMLHttpRequest Level 2
xhr.responseType = 'arraybuffer';

xhr.onload = function() {
    if (xhr.status === 200) {
        var arrayBuffer = xhr.response;

        if (arrayBuffer instanceof ArrayBuffer) {
            // The 2nd argument for decodeAudioData
            var successCallback = function(audioBuffer) {
                /* audioBuffer is the instance of AudioBuffer */

                // Create the instance of ConvolverNode
                var convolver = context.createConvolver();

                // Set the instance of AudioBuffer
                convolver.buffer = audioBuffer;
            };

            // The 3rd argument for decodeAudioData
            var errorCallback = function(error) {
                if (error instanceof Error) {
                    window.alert(error.message);
                } else {
                    window.alert('Error : "decodeAudioData" method.');
                }
            };

            // Create the instance of AudioBuffer (Asynchronously)
            context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
        }
    }
};

xhr.open('GET', url, true);
xhr.send(null);

あとは, サウンドの入力点となるノード (OscillatorNodeなど) をConvolverNodeに接続して, ConvolverNodeをAudioDestinationNodeに接続すればリバーブの完成です. ConvolverNodeクラスは, AudioNodeクラスを (プロトタイプ) 継承しているので, connectメソッドが利用可能です.

サンプルコード 06


/*
 * Add code to sample code 05
 */

// ....

// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of ConvolverNode
    var convolver = context.createConvolver();

    // Set the instance of AudioBuffer
    convolver.buffer = audioBuffer;

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

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

    // Connect nodes for original sound
    // OscillatorNode (Input) -> AudioDestinationNode (Output)
    oscillator.connect(context.destination);

    // Connect nodes for effect (Reverb) sound
    // OscillatorNode (Input) -> ConvolverNode (Reverb) -> AudioDestinationNode (Output)
    oscillator.connect(convolver);
    convolver.connect(context.destination);

    oscillator.start(0);
};

// ....

ディレイの場合と同じように, 原音とエフェクト音の接続を作成しています. 遅延音の生成処理と遅延音に対する演算処理は, ConvolverNodeクラスが内部に隠蔽しているので, フィードバックのための接続は不要です.

ConvolverNodeの接続

図2 - 2 - f. ConvolverNodeの接続

最後に, 原音とエフェクト音のゲイン調整ができるように, Dry / WetのためのGainNodeを接続します.

Dry / Wetの接続

図2 - 2 - g. Dry / Wetの接続

サンプルコード 07


/*
 * Add code to sample code 05
 */

// ....

var successCallback = function(audioBuffer) {
    /* audioBuffer is the instance of AudioBuffer */

    // Create the instance of ConvolverNode
    var convolver = context.createConvolver();

    // Set the instance of AudioBuffer
    convolver.buffer = audioBuffer;

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

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

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

    // Create the instance of GainNode
    var dry = context.createGain();  // for gain of original sound
    var wet = context.createGain();  // for gain of effect (Revreb) sound

    // Set parameters
    dry.gain.value = 0.7;
    wet.gain.value = 0.3;

    // Connect nodes for original sound
    // OscillatorNode (Input) -> GainNode (Dry) -> AudioDestinationNode (Output)
    oscillator.connect(dry);
    dry.connect(context.destination);

    // Connect nodes for effect (Reverb) sound
    // OscillatorNode (Input) -> ConvolverNode (Reverb) -> GainNode (Wet) -> AudioDestinationNode (Output)
    oscillator.connect(convolver);
    convolver.connect(wet);
    wet.connect(context.destination);

    oscillator.start(0);
};

// ....

リバーブのノード接続

図2 - 2 - h. リバーブのノード接続

以上でリバーブが完成しました. ところで, リバーブを利用するためには, インパルス応答のオーディオファイルが必要です. 楽曲やワンショットサンプルのオーディオファイルはあっても, インパルス応答のオーディオデータをもっている人は少ないと思います. 機材をもっていれば実際に測定してもOKです…が, そこまでするのはちょっとめんどうです. となると, Web上で公開されているファイルを利用することになるのですが, 利用条件や著作権の関係から無償で自由に利用できるのは意外とありません.

とりあえず, 制作者から1つ紹介するのは, こちらのサイトです. readme.txtの5. Copyrightのセクションに, 「The data are provided free for noncommercial purposes, provided the authors are cited when the data are used in any research application.」 と記載されているので, 非商用であれば, 自分のサイトにおいてWebアプリに利用しても問題なさそうです.

では, リバーブをデモ 04・デモ 05で試してみてください. インパルス応答のオーディオファイルは, 上記で紹介したサイトのものを利用しています. ぜひ, いろんな音響空間を楽しんでみてください.

デモ 04 (サウンド)

デモ 05 (オーディオ)

ところで, ConvolverNodeインスタンスのbufferプロパティに, インパルス応答のAudioBufferインスタンスを設定したあと, リバーブをOFFにするために, 初期値であるnullを設定してもOFFになりません. ブラウザの実装上のバグかと思いますが, とりあえず, 現状ではデモ 04・デモ 05のように, ノード接続をクリアして再び接続することによって, リバーブをOFFにしています.

インパルス応答

インパルス応答を理解するには, まずは, インパルス音について理解する必要があります.

インパルス音とは, ごく短時間において瞬間的に発生する音のことです. ピストルの音や風船が破裂するときの音がこれにあたります. いますぐ, インパルス音を発生させることもできます. 両手を拍手をするようにして思いっきり叩いてみてください. 「パン !!」と音が発生したと思います. それが, まさにインパルス音です.

このインパルス音をグラフに表すと図2 - 2 - gのようになります (このような物理現象を数式で表現するための最適な関数が, デルタ関数です).

インパルス音

図2 - 2 - i. インパルス音

時間 dt は限りなく0に近い時間を表しています.

音を室内で発生させると, 音が天井や壁などに反射して, 音の響き, すなわち, 残響が発生します. カラオケが好きな方であれば, エコーのような効果と考えてもらってもいいでしょう. この残響が付加されたインパルス音は図2 - 2 - iのようになります.

直接音と反射音 (残響)

図2 - 2 - j. 直接音と反射音 (残響)

室内で知覚している音は, 音源からの直接音 (矢印) と天井や壁などからの反射音 (破線の矢印) が含まれています. 反射音は天井や壁を経由するので, 直接音を知覚した時刻を基準にすると, 反射音を知覚する時刻は遅延します.

インパルス応答

図2 - 2 - k. インパルス応答

時間 dt は限りなく0に近い時間を表しています.

これが, インパルス応答です. つまり, インパルス音を音響空間に対する入力としたときの出力 (応答) ということです. そして, その出力が音響空間における, 残響特性を表すことになるので, インパルス応答を利用することによって, コンサートホールなどの音響空間の音の響きをシミュレートするエフェクトであるリバーブが実現できるというわけです.

ちなみに, 室内でのインパルス応答は, RIR (Room Impulse Response) と表現されることもあります.

コンボリューション積分

ディレイとリバーブは遅延処理を利用しているという点で原理が同じと解説しましたが, それを音信号処理の視点で見ると, コンボリューション積分 (畳み込み積分)という処理を利用している点で原理が同じです.

そこで, このセクションでは, コンボリューション積分に関して簡単に解説しておきたいと思います. とりあえず, ディレイ・リバーブが実装できればOKという場合はスルーしてください. また, 数学の話になってくるので, 無理をせずによくわからないことは見なかったことにして気楽に進めてください.

事前知識として, A - D変換については簡単でいいので理解しているほうがいいと思いますので, 全然知らないよ〜という方は, こちらのページなどを参考に理解してみてください.

コンボリューション積分の構成処理

コンボリューション積分をくだいて解説すれば, それぞれの時刻における振幅を, その時刻に発生した原音の振幅と, それより過去に発生した音の遅延音の振幅 (フィードバック (ディレイ) やインパルス応答 (リバーブ) の値) の加算で算出する処理のことです. また, 遅延音の振幅は原音とフィードバック値やインパルス応答との乗算で算出されます. つまり, 遅延乗算加算という3つの演算がコンボリューション積分を構成する処理ということです.

コンボリューション積分の数式

コンボリューション積分を数式で表現すると以下のようになります.

s_1(n) = \sum_{m=0}^{J}b(m)s_0(n-m)

図2 - 2 - l. コンボリューション積分

よりイメージをつかみやすいように, Σ記号をとって表現します.

s_1(n) = b(0)s_0(n) + b(1)s_0(n-1) + b(2)s_0(n-2) + \cdots + b(J)s_0(n-J)

図2 - 2 - m. コンボリューション積分

s0は原音の振幅, bはディレイであればフィードバックの値, リバーブであればインパルス応答の振幅, そして, s1が原音と遅延音を加算した出力音の振幅です.

具体的にイメージをつかむために, 図2 - 2 - nのイラストを利用して解説します.

コンボリューション積分のイメージ図

図2 - 2 - n. コンボリューション積分のイメージ図

図2 - 2 - nは, 原音と2つの遅延音を1つのセットとしています (インパルス応答のサイズが3と考えてもOKです). それを, 時刻n - 2, n - 1, nにおいて発生させます. ここで, 時刻n (トップにあるグラフ) における振幅は, 原音 (時刻nにおいて発生させた音) と過去の時刻 (時刻n - 1, n - 2) において発生した音 (遅延音) の総和となります. そして, コンボリューション積分の数式が表すように, 各時刻における振幅は, 原音とフィードバック値との乗算 (ディレイ), あるいは, 原音とインパルス応答の振幅との乗算 (リバーブ) によって算出されます.

ディレイ・リバーブ まとめ

このページでは, ディレイ・リバーブの実装とその原理の概要を解説しました. そのために, 必要なノードとノード接続のエッセンスについてまとめておきます.

表2 - 2 - a. DelayNode / ConvolverNode
NodeDescription
DelayNode
  • createDelayメソッドでインスタンス生成 (引数はディレイタイムの最大値)
  • delayTimeがディレイタイムを制御する
  • 遅延音を1つ発生させる
ConvolverNode
  • createConvolverメソッドでインスタンス生成
  • bufferプロパティにインパルス応答のAudioBufferインスタンスを設定する
表2 - 2 - b. ノード接続
EffectorConnection
ディレイ
原音
入力ノード + Dry (GainNode) + AudioDestinationNode
エフェクト音
入力ノード + DelayNode + Wet (GainNode) + AudioDestinationNode
フィードバック
DelayNode + Feedback (GainNode) + DelayNode + ...
リバーブ
原音
入力ノード + Dry (GainNode) + AudioDestinationNode
エフェクト音
入力ノード + ConvolverNode + Wet (GainNode) + AudioDestinationNode