Web Audio APIでオシレーターの実装方式いろいろ試してみた

あなたの好きな波形は何ですか。人はだれしも思い入れのある波形があるかと思います。矩形波が好きなかたもいるようですが僕はだんぜんノコギリ波派です。ノコギリ波の優しくて存在感のある音色につつまれているだけでしあわせな気持ちになります。
 
そんなわけでみなさんこれから多くのシンセをWeb Audio APIで作成するかと思いますが、いくつかあるノコギリ波のオシレーター実装方法について比較してみたいと思います。また、ソフトシンセを作るうえで避けて通れないエイリアスノイズについて方式ごとの差異を聴き比べてみます。
 
こちらのページで実際に聴きながら読んでください。(Chrome推奨)
http://aikelab.net/sawwave/
 
■ナイーブな実装

    return 1.0 - (this.phase / Math.PI);

こんな感じの処理を1サンプルごとに呼ぶとノコギリ波になります。高い音になるほど濁ったようなエイリアスノイズが聴こえると思います。自分も完全に理解しているわけではありませんが、倍音の周波数(例:440Hzの場合、880Hz, 1320Hz, 1760Hz, ...)がナイキスト周波数(例:サンプリングレート44100Hzの場合22050Hz)を越えると波形が再現できなくて可聴域にノイズが出るとのことです。そのため高次の倍音を多く含む波形ほどノイズが目立ちます。
この実装方法は処理としては軽いものの高音にはちょっと使えませんね。僕のWebAudioSynthはこの方式です:-p
 
■オーバーサンプリング

    var w = 0.0;
    var i;
    for (i = 0; i < this.oversampling; i++) {
        this.phase += this.delta / this.oversampling;
        if (this.phase > Math.PI * 2) {
            this.phase -= Math.PI * 2;
        }
        w += (1.0 - (this.phase / Math.PI)) / this.oversampling;
    }
    return w;

エイリアスノイズ対策として良く使われるのがオーバーサンプリングです。たとえばサンプリングレートの2倍の細かさで計算して、その後隣接する2サンプルの平均をとってもとのサンプリングレートに戻す処理をします。先ほどよりだいぶ良い感じですが2倍程度だとまだ上の方でノイズが目立ちます。また計算量も当然ナイーブな実装のn倍かかります。
takmizさんのWeb FM synthesizerはこの方式をとっているようです。
 
■加算合成

   var out = 0.0;
    var n;
    for (n = 1; ; n++) {
        if (this.frequency * n > this.samplerate / 2)
            break;
        var overtone = Math.sin(this.phase * n) / n;
        out += overtone;
        if (n >= this.highestharmonics)
            break;
    }
    return out;

高次倍音ナイキスト周波数を越えるとノイズになるのであれば、下から倍音を積み重ねていってナイキスト周波数に達したら積み重ねるのをやめればいいのでは、という考え方で倍音を加算合成する方法です。
確かにノイズは無くなりますが、1サンプルあたり何十回も三角関数を呼ぶなど計算量がとても多くなります。また20〜30回くらいで計算を打ち切ると低音の方で倍音が足りずに甘い音色になってしまいます。
Synth1のDaichiさんが公開しているVSTiシンセのサンプルソースはウェーブテーブル方式ですが、アプリ起動時にウェーブテーブルの波形データを加算合成で作っています。
 
■BLIT

    if (this.x >= 0.5) {
        this.x -= 1.0;
        this.p = this.samplerate / this.frequency;
        this.f = 1.0 / this.p;
        this.m = 2 * Math.floor(this.p / 2) + 1;
        this.sum = this.c3 = 0.0;
    }
    this.sum += this.m / this.p * Math.sin(this.m * this.x * Math.PI)
                / (this.m * Math.sin(this.x * Math.PI));
    this.x += this.f;
    this.c3 += this.f;
    return 2.0 * (this.sum - this.c3);

BLIT(Band Limited Impulse Train)は比較的新しいアルゴリズムです。g200kgさんのBLITのお話が詳しいです。
計算式よりもソースコードが好きなみなさんはTALさんのページの一番下を見ると良いと思います。
自分なりに解釈すると、これは「方眼紙に波形を書くとゆがんでしまうので逆向きにゆがませた方眼紙の上で波形を書く」ようなイメージと理解しています。方眼紙にあたるのが通常のImpulse Trainで、ゆがませた方眼紙がBand Limited Impulse Trainなのかと。
BLITは1サンプルにつき三角関数が2個なので加算合成などに比べれば軽いです。
ノイズが完全にはなくなっていないのは僕の実装が悪いせいかもしれません。BLITのJavaScriptでの実装例は、いまのところ他では見たことがありません。
 
オシレーターノード

    this.onode = this.ctx.createOscillator();
    this.onode.type = 2;
    this.onode.connect(this.ctx.destination);
    this.onode.start(0);

ここまで書いておいてなんですが、Web Audio APIではオシレーターの部品が最初からあるので、ノコギリ波などそこに用意されている波形は普通にオシレーターノードで実装するのが正しい方法です。JavaScriptで実装するより当然軽いしカスタム波形も使えるようです。
実際に聴いてみるとノイズがないのはもちろんですが、音程切り替え時に少しポルタメントがかかる特徴があるようです。
ただ、残念なことに2013年5月現在FirefoxではオシレーターノードがAPIに存在しません。(前述の試聴ページではFirefoxの場合エミュレーションで鳴らしているので本来のオシレーターノードの音ではありません)
 
あとはやっぱりウェーブテーブル方式が良さそうなので今後調べてみたいと思っています。補間方式などいろいろノウハウがありそうな気がします。