VSTプラグイン作家のためのWeb Audio API入門 〜 adelayの作成

全国100人くらいのVSTプラグイン作家の方こんばんは。今日は皆さんおなじみのadelayのウェブブラウザバージョンを作ってみたいと思います。最近のウェブブラウザはAudio APIが急速に充実してきて簡単に音声信号処理ができるようになってきました。
 
adelayといえばVST 2.X SDKのサンプルコードとして大変なじみ深く、こんなシンプルなコードであのディレイが作れてしまうのかというインパクトもあり、プラグイン作家であればアルゴリズムをそらで暗唱できるくらい体に染み付いているかと思います。(←大げさ)
 
一方、Web Audio APIで用意されているDelayNodeは単に入力信号を遅延させて再生するものでDry/Wetのバランス設定やFeedbackもありません。ディレイプログラムはGainNodeなどと組み合わせて作ることが想定されているものと思いますが入門者が学習するにはちょっと遠回りのように思います。
そんなわけで今回はDelayNodeを使わずに、汎用の信号処理モジュールJavaScriptNodeを使ってadelayと同じロジックを素直に実装してみます。
 
本題のディレイに入る前にディレイエフェクトをかける音源が必要なので先にそっちを用意します。

Ryoya KAWAIさんのOscillatorを後続ノードに接続しやすいように改造したOscillatorクラスを用意しました。こんな風に呼び出すと3秒ごとに矩形波を鳴らします。

$(function() {
    var ctx = new webkitAudioContext();
    var osc = new Oscillator(ctx);
    osc.setOscType('square');
    setInterval(function() {
        osc.noteOn(72);
        setTimeout(function() {osc.noteOff(72);}, 100);
    }, 3000);
    osc.connect(ctx.destination);
});

http://aikelab.net/adelay/oscillator/ (注:ページを開くと数秒後に音が出ます。Chrome/Safariのみ)
 
さて、それではディレイの作成です。まずは信号処理をする単位を1024サンプルずつとします。

var Delay = function(ctx) {
    this.sampleFrames = 1024;

ディレイ成分を記憶するためのバッファを用意します。バッファサイズは以下のように計算できます。
 バッファサイズ = サンプリングレート(Hz) * 最大ディレイタイム(秒)
今回はシンプルに最大ディレイタイムを1秒としてオーディオコンテキストから取得したサンプリングレートと同じサイズのバッファを確保します。cursorはバッファ中の読み出し位置を保持する変数です。

    this.size = ctx.sampleRate;
    this.cursor = 0;
    this.buffer = new Float32Array(this.size);

パラメータはディレイタイムのdelay、feedback、ディレイ成分の音量outの3つです。それぞれ0.0〜1.0の値をとります。

    this.setDelay(0.5);
    this.setFeedback(0.5);
    this.setOut(0.75);

次に、入力1ch、出力2chのJavaScriptNodeを作ります。

    this.node = ctx.createJavaScriptNode(this.sampleFrames, 1, 2);

ここからがメインのロジック。onaudioprocess関数はサンプルフレーム(1024サンプル)ごとに呼ばれます。すべてのサンプルデータに対してドライ成分(x)と前回までのディレイ成分(y)を加算してバッファに格納します。これが次回のディレイ成分になります。

    var self = this;
    this.node.onaudioprocess = function(event) {
        var sampleFrames = self.sampleFrames;

        var in1 = event.inputBuffer.getChannelData(0);
        var out1 = event.outputBuffer.getChannelData(0);
        var out2 = event.outputBuffer.getChannelData(1);
        var n = 0;
        while (--sampleFrames >= 0) {
            var x = in1[n];
            var y = self.buffer[self.cursor];
            self.buffer[self.cursor++] = x + y * self.feedback;
            if (self.cursor >= self.delay)
                self.cursor = 0;
            out1[n] = y * self.out;
            out2[n] = y * self.out;
            n++;
        }
    };
};

あとはパラメータ設定用関数とノード接続用の関数を用意すれば完成。

// fDelay: 0.0 - 1.0 (0sec - 1.0sec)
Delay.prototype.setDelay = function(fdelay) {
    this.cursor = 0;
    this.delay = Math.floor(fdelay * (this.size - 1));
};

// fFeedback: 0.0 - 1.0
Delay.prototype.setFeedback = function(ffeedback) {
    this.feedback = ffeedback;
};

// fOut: 0.0 - 1.0
Delay.prototype.setOut = function(fOut) {
    this.out = fOut;
};

Delay.prototype.getNode = function() {
    return this.node;
};

Delay.prototype.connect = function(obj) {
    this.node.connect(obj);
};

さっきのOscillatorに接続するとこんな感じになります。
http://aikelab.net/adelay/simple/ (注:ページを開くと数秒後に音が出ます。Chrome/Safariのみ)
 
最後にGUIをつけてそれなりのエフェクターにしてみます。バッファも2個用意してLch/Rch別々にかかるようにしました。また、もともとのadelayはドライ出力がなかったのでドライとウェットを混ぜて出力するようにしました。

Oscillatorの矩形波の代わりにギターのリフをサンプリングした音に接続してできあがり。ノブを回して遊んでみてください。

f:id:aike:20121207053932j:image

http://aikelab.net/adelay/ (注:ページを開くと数秒後に音が出ます。Chrome/Safariのみ)
 
ね?簡単でしょ。

Web Music Developers JP Advent Calendar 2012(7日目)