ScriptProcessorNodeの概要

ScriptProcessorNodeクラスが可能することは, 柔軟なサウンド処理です. といっても, この表現だけではその必要性は伝わらないと思いますので, APIレベルという観点でその必要性を解説します.

ハイレベルなAPI

Web Audio APIには, 基本波形のサウンド生成やエフェクター, サウンドの視覚化など高度なサウンド処理をより簡単に実装するために, 様々なノードが定義されています.

OscillatorNode, GainNode, DelayNode, BiquadFilterNode, AnalyserNode…

これらのノードがあるおかげで, 内部で実行されている音信号処理の詳細を知らなくても, 高度なサウンド機能の実装が簡単にできるわけです. 例えば, 正弦波の数式を知らなくても, OscillatorNodeによって, 正弦波を生成することができました.

しかしながら, (ScriptProcessorNode以外の) ノードだけでアプリケーションを作成する限り, ブラウザに依存したサウンドになってしまいます. 例えば, ノコギリ波の生成方法はいくつかありますが (詳細はこちらを参照) , OscillatorNodeを利用する限り, ブラウザがその内部で実装している1種類のノコギリ波しか生成できません.

Web Audio APIが定義する多くのノードは, サウンドデータの実体にアクセスする機能をもちません. なぜなら, これらのノードは, サウンド処理を抽象化する, すなわち, ハイレベルなAPIとして定義されているからです.

ところが, ハイレベルなAPIを利用することで享受できるメリットの代償として, ブラウザに依存したサウンドになる, あるいは, サウンドデータの実体にアクセスする処理ができないといったデメリットをもってしまいます.

ローレベルなAPI

ブラウザに依存したサウンド処理を最大限排除するのが, ScriptProcessorNodeクラスの役割です. なぜなら, ScriptProcessorNodeクラスには, サウンドデータの実体にアクセスできる機能が定義されているからです. APIの観点から表現すれば, ScriptProcessorNodeクラスは, 抽象度の低い, ローレベルなAPIということです.

何らかのプログラミング言語で, サウンドプログラミングの知識と経験があるなら, ScriptProcessorNodeだけでサウンド処理を実装するほうが良いかもしれません.

また, そこまでのレベルでなくても, 基本的にはハイレベルなAPIを利用して必要に応じてScriptProcessorNodeで処理することも可能です. すなわち, サウンドプログラミングや音信号処理の知識・経験のレベルに合わせて, ScriptProcessorNodeと他のノードを併用するという開発手法も可能です. このように, サウンド処理・使い勝手の両方の観点から, 非常に高い柔軟性を備えていると言えます.

ScriptProcessorNode

図1 - 5 - a.

ScriptProcessorNodeのAPIレベル

Web Audio APIのノードの多くは, 抽象度の高い, ハイレベルなAPIですが, ScriptProcessorNodeは, 抽象度の低い, ローレベルなAPIの位置づけです.

ScriptProcessorNodeの利用

インスタンス生成

ScriptProcessorNodeクラスも, 他のクラスと同様に, インスタンスを生成する必要があります. インスタンスの生成は, AudioContextインスタンスのcreateScriptProcessorメソッドを利用します. このメソッドにおける3つの引数は, いずれもサウンド処理において重要な意味をもっています.

表1 - 5 - a. createScriptProcessorメソッドの引数
1st2nd3rd
処理単位となるバッファサイズ入力チャンネル数出力チャンネル数

第1引数のバッファサイズは, 256, 512, 1024, 2048, 4096, 8192, 16384の1つを選択するのですが, どれでもOK…というわけではなく, OSごとに最適なサイズがあります. ただし, この最適なサイズは仕様定義されているわけではなく, あくまで経験的に得たサイズです. 変動する可能性があることには注意してください.

制作者自身, すべての環境で常時試しているわけではないので, 表1-5 - bと異なるサイズのほうがよかったなどという場合は, コメントいただけると大変ありがたいです.

表1 - 5 - b. OSごとのバッファサイズ (2013. 9現在)
OSSize
Windows 7, Windows 81024
Windows Vista2048
Windows XP4096
Mac OS X1024
Linux (Ubuntu)8192
iOS2048

ちなみに, 最適なバッファサイズよりも小さなサイズを指定すると, ブツブツと音が途切れた感じになってしまいます. 最適なバッファサイズよりも大きなサイズを指定した場合にはそのようにならないので, とりあえず余裕をもって設定しておくというアプローチもありますが, バッファサイズが大きいほど入力に対する出力の遅延時間 (レイテンシー) が大きくなってしまうことには考慮が必要です.

第2引数と第3引数には, 入出力におけるチャンネル数を指定しますが. とりあえず, 2 (チャンネル) を指定すれば問題ないでしょう. ちなみに, 引数を省略した場合でも, 2 (チャンネル) として扱われます.

ちなみに, 初期の仕様においては, createScriptProcessorメソッドは定義されておらず, その代わりに, createJavaScriptNodeメソッドを利用していたので, フォールバックを記述するほうが安全です.

長くなりましたが, 以上の点を踏まえて, サンプルコードを記載します.

サンプルコード 01


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

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

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

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 instance of ScriptProcessorNode
var processor = context.createScriptProcessor(getBufferSize(), 2, 2);

ノード接続

ScriptProcessorNodeだけでは, サウンドを出力することはできません. 少なくとも, サウンドの出力点となるAudioDestinationNodeに接続する必要があります. そして, ScriptProcessorNodeクラスもAudioNodeクラスを (プロトタイプ) 継承しているクラスなので, connectメソッドを呼び出すことが可能です.

サンプルコード 02


/*
 * Add code to sample code 01
 */

// ....

// ScriptProcessorNode (Input) -> AudioDestinationNode (Output);
processor.connect(context.destination);

ところが, ScriptProcessorNodeに対する入力ノードがないと, iOSでバグが発生するみたいなので, ダミーの入力ノード (AudioBufferSourceNode) を接続しておきます (バグについてはこちらを参照) .

ノード接続

図1 - 5 - b. ノード接続

AudioBufferSourceNodeは, 実質的には入力ノードでないことに注意してください.

サンプルコード 03


/*
 * Add code to sample code 01
 */

// ....

// for iOS
var dummy = context.createBufferSource();

// (AudioBufferSourceNode ->) ScriptProcessorNode (Input) -> AudioDestinationNode (Output)
dummy.connect(processor);
processor.connect(context.destination);

GainNodeやDelayNodeなど他のノードを接続することも可能です. サウンド生成は, ScriptProcessorNodeで実行し, エフェクターはノードを利用して作成するケースがあてはまるかと思います.

ノード接続

図1 - 5 - c. ノード接続

サンプルコード 04


/*
 * Add code to sample code 01
 */

// ....

var dummy      = context.createBufferSource();
var filter     = context.createBiquadFilter();
var masterGain = context.createGain();

// (AudioBufferSourceNode ->)
// ScriptProcessorNode (Input) -> BiquadFilterNode -> GainNode -> AudioDestinationNode (Output);
dummy.connect(processor);
processor.connect(filter);
filter.connect(masterGain);
masterGain.connect(context.destination);

OscillatorNodeやAudioBufferSourceNodeなどが出力するサウンドデータにアクセスする場合には, それらのノードをScriptProcessorNodeに接続します.

ノード接続

図1 - 5 - d. ノード接続

OscillatorNodeから出力されたサウンドデータを利用する場合.

サンプルコード 05


/*
 * Add code to sample code 01
 */

// ....

var oscillator = context.createOscillator();

// OscillatorNode (Input) -> ScriptProcessorNode -> AudioDestinationNode (Output)
oscillator.connect(processor);
processor.connect(context.destination);

ノード接続

図1 - 5 - e. ノード接続

AudioBufferSourceNodeからの出力, つまり, オーディオデータを利用する場合. この場合の, AudioBufferSourceNodeはiOSのバグ対策ではないことに注意してください.

サンプルコード 06


/*
 * Add code to sample code 01
 */

// ....

var source = context.createBufferSource();

// AudioBufferSourceNode (Input) -> ScriptProcessorNode -> AudioDestinationNode (Output)
source.connect(processor);
processor.connect(context.destination);

onaudioprocess

ScriptProcessorNodeインスタンスで最も重要なプロパティは, onaudioprocessイベントハンドラです. onaudioprocessイベントハンドラにおいて, サウンドデータの実体にアクセス可能だからです. すなわち, ScriptProcessorNodeの機能のコアとなるインタフェースということです.

サンプルコード 07


/*
 * Add code to sample code 03 - 06
 */

// ....

processor.onaudioprocess = function(event) {
    // do something to control sound ....
};

ところで, このイベントの発生はいつなのでしょうか? onloadやonclickであれば, ページロード完了したときやマウスボタンをクリックしたとき…とイベント名から想像できますが, onaudioprocessは…イベント名から想像できないですよね.

onaudioprocessイベントの発生は以下のように実装されているようです.

  1. 1. AudioDestinationNodeへの接続イベントハンドラの設定が完了したときに発生
  2. 2. 1. のあとは, バッファサイズのデータを処理するごとに発生

「AudioDestinationNodeインスタンスへの接続」というのは, 間に他のノード接続があってもOKです.

onloadやonclickイベントなどと異なり, AudioDestinationNodeインスタンスへの接続とイベントハンドラ設定の両方が完了したときに発生する点は注意が必要です (通常のイベントハンドラは, ハンドラ設定時にイベントは発生しません) . そして, そのあとはsetIntervalメソッドでコールバック関数を設定した場合のように, 一定の間隔で, つまり, 定期的にイベントが発生するわけです.

このような仕様から, 一度イベントが発生すると何もしない限り, プログラムが終了する (ページ再読込, ブラウザを閉じる…etc) までイベントの発生が継続します. OscillatorNodeやAudioBuffreSourceNodeを接続している場合, stopメソッドを実行してもonaudioprocessイベントの発生は継続するので注意してください.

任意のタイミングで, onaudioprocessイベントの発生を停止させるには, 以下の2つの方法があります.

  • ノードの接続を断つ (disconnectメソッド)
  • イベントハンドラをクリア (nullの代入)

AudioNodeクラスには, ノード接続を断つためのdisconnectメソッドも定義されています. したがって, AudioNodeクラスを (プロトタイプ) 継承しているクラスのインスタンスであれば呼び出し可能です.

disconnectメソッドは数値型の引数を1つとります. この引数は, 接続を断つ対象となる出力先を意味しており, デフォルト値 (引数を省略した場合) は0です. この引数は, ノードが複数の入出力をもつ場合のみ重要となります. ほとんどのノードは入出力先となるノードを1つしかもたないので, 多くの場合, 0を指定するか, 省略することになるでしょう.

ちなみに, 入出力先となるノードを複数もつのは, ChannelSplitterNodeChannelMergerNodeのみです.

サンプルコード 08


/*
 * Add code to sample code 03 - 06
 */

// ....

processor.disconnect();           // = processor.disconnect(0);
processor.onaudioprocess = null;  // Clear onaudioprocess event handler


少なくとも, Chromeではどちらかの処理を実行すれば, onaudioprocessイベントの発生は停止するようですが, より多くのブラウザに対応させることを考慮すると2つの処理を実行しておくのがベターでしょう.

この処理によって, 任意のタイミングで, onaudioprocessイベントの発生を停止させることが可能になります.

onaudioprocessイベントオブジェクト

onaudioprocessイベントに渡されるイベントオブジェクトの重要なプロパティは, 以下の2つです.

inputBuffer
AudioBufferインスタンスで, getChannelDataメソッドによって, 入力ノードからFloat32Array型配列への参照を取得.
outputBuffer
AudioBufferインスタンスで, getChannelDataメソッドによって, 出力ノードへのインターフェースとなるFloat32Array型配列への参照を取得. その配列に格納したサウンドデータがそのまま出力になる.

onaudioprocessイベントハンドラの典型的な形となる, サンプルコード 09をコードリーディングしながら解説したほうが理解しやすいと思うので, まずはどんな形になるのかを見てください.

サンプルコード 09


/*
 * Add code to sample code 03 - 06
 */

// ....

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

    for (var i = 0; i < this.bufferSize; i++) {
        outputLs[i] = /* do something .... */;
        outputRs[i] = /* do something .... */;
    }
};

入力データの取得

入力ノードからサウンドデータを取得するには, AudioBufferインスタンスであるinputBufferプロパティのgetChannelDataメソッドを呼び出します. getChannelDataメソッドの引数は, 入力元のチャンネルです (0が左チャンネル, 1が右チャンネルです). ただし, インスタンス生成において, 入力チャンネル数に1を指定 (第2引数) している場合には, 左チャンネルからの入力データしか取得できません. この場合, 引数に1を指定するとエラーが発生します.

入力ノードがない (ダミーの入力ノードしかない) 場合であっても, メソッド呼び出しでエラーは発生せずに, 要素がすべて0の配列, つまり, 無音のデータが返されます. また, 入力ノードがある場合でも, サウンドを開始していないと同じ結果になります. OscillatorNode, AudioBufferSourceNodeを入力ノードとして接続している場合はstartメソッド, MediaElementAudioSourceNodeを入力ノードとして接続している場合はplayメソッドを実行する必要があります.

出力インターフェースの生成

onaudioprocessイベントハンドラで生成したサウンドデータはそのままでは出力されません. なぜなら, ScriptProcessorNodeの接続先ノードに, データを渡す役割 (インターフェース) が必要だからです. それが, AudioBufferインスタンスであるoutputBufferプロパティのgetChannelDataメソッドによって, 取得 (参照) できるFloat32Array型の配列です. もっとも, 取得した時点の配列は, 要素がすべて0の配列です. この配列に, 生成したサウンドデータを格納することによって, そのサウンドデータを接続先の出力ノードに渡すことが可能になります.

また, inputBufferプロパティと同じように, getChannelDataメソッドの引数にはチャンネルを指定します. 0が左チャンネルの出力, 1が右チャンネルの出力にサウンドデータを渡す配列への参照となります. ただし, インスタンス生成において, 出力チャンネル数に1を指定 (第3引数) している場合には, 出力はモノラル (左右のチャンネルが, 同じサウンド出力) となります. この場合, 引数に1を指定するとエラーが発生します.

サウンド処理

参照する配列は, いすれもFloat32Array型の配列なので, 高速にアクセス可能となっています. そして, その配列サイズは, createScriptProcessorメソッド呼び出しで指定したバッファサイズ (第1引数) と同じです. バッファサイズはScriptProcessorNodeインスタンスのbufferSizeプロパティでアクセス可能です.

このバッファサイズを単位としてサウンドデータの処理を実行し, それが完了すれば, 次のonaudioprocessイベントハンドラの呼び出しに入ります.

実際のサウンド処理は次のセクションでいくつか実践するので, まずは, ここまでの整理として, デモ 17を試してみてください. デモ 17では, OscillatorNodeをScriptProcessorNodeへの入力ノードとして接続しています. onaudioprocessイベントハンドラでサウンドデータの実体にアクセスすることによって, GainNodeを利用することなく, ボリュームコントロール機能を実装しています.

デモ 17

ScriptProcessorNodeの実践

このセクションでは, ScriptProcessorNodeによる, 実践的なサウンドプログラムをいくつか紹介します. コードの意味をきちんと理解するには, 音信号処理の簡単な知識が必要になりますが, そういった知識があまりない方は, とりあえずこのコードで, こんなサウンドができるぐらいの感覚的な理解で進めてもらって構いません.

ホワイトノイズ (白色雑音) の生成

まずは, ノイズを作成してみましょう. ホワイトノイズという複雑そうな名前ですが, 生成方法はとても簡単です. 乱数を生成するだけで, Math.randomメソッドを利用します. ただし, このままだと, 0 ~ 1未満の値になるので, -1 ~ 1の間の値をとるように値を調整します. ちなみに, 入力サウンドデータをコードによって生成しているので, inputBufferプロパティは不要です.

ノード接続

図1 - 5 - f. ノード接続

実質的な入力ノードは不要ですが, iOS対策のためにダミーの入力ノードを接続しています.

サンプルコード 10


/*
 * Add code to sample code 03
 */

// ....

processor.onaudioprocess = function(event) {
    // 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

    for (var i = 0; i < this.bufferSize; i++) {
        outputLs[i] = 2 * (Math.random() - 0.5);  // between -1 and 1
        outputRs[i] = 2 * (Math.random() - 0.5);  // between -1 and 1
    }
};

デモ 18

基本波形の生成

正弦波はMath.sinメソッドを利用することで実装できそうですが, 他の基本波形はどうでしょう?結論としては, 矩形波・ノコギリ波・三角波もMath.sinメソッドを利用することで実装できます. なぜなら, これらの基本波形はすべて (異なる倍音をもつ) 正弦波を合成した波形だからです.

しかし, サウンドプログラミングでは, 正弦波を合成して生成する方法ではなく, 直線の組み合わせによって波形を近似させる方法を利用することが多いです.

表1 - 5 - c. 基本波形の数式
WaveFormula
正弦波 Math.sin((2 * Math.PI * frequency * phase) / sampleRate);
矩形波 var t0 = sampleRate / frequency;
(phase < (t0 / 2)) ? 1 : -1;
変数phaseの範囲は, 0 <= phase < t0
ノコギリ波 var t0 = sampleRate / frequency;
(2 * phase / t0) - 1;
変数phaseの範囲は, 0 <= phase < t0
三角波 var t0 = sampleRate / frequency;
var s = 4 * phase / t0;
(phase < (t0 / 2)) ? (-1 + s) : (3 - s);
変数phaseの範囲は, 0 <= phase < t0
ノード接続

図1 - 5 - g. ノード接続

実質的な入力ノードは不要ですが, iOS対策のためにダミーの入力ノードを接続しています.

サンプルコード 11


/*
 * Add code to sample code 03
 */

// ....

var type       = 'sine';
var frequency  = 440;

var fs = context.sampleRate;  // Sampling frequency
var n  = 0;                   // Phase

processor.onaudioprocess = function(event) {
    // 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

    for (var i = 0; i < this.bufferSize; i++) {
        // Fundamental period
        var t0 = fs / frequency;

        var output = 0;

        switch (type) {
            case 'sine' :
                output = Math.sin((2 * Math.PI * frequency * n) / fs);
                break;
            case 'square' :
                output = (n < (t0 / 2)) ? 1 : -1;
                break;
            case 'sawtooth' :
                var s = 2 * n / t0;

                output = s - 1;
                break;
            case 'triangle' :
                var s = 4 * n / t0;

                output = (n < (t0 / 2)) ? (-1 + s) : (3 - s);
                break;
            default :
                break;
        }

        // Output sound
        outputLs[i] = output;
        outputRs[i] = output;

        // Update phase
        n++;

        // Exceed fundamental period ?
        if (n >= t0) {
            n = 0;
        }
    }
};

よくわからかった部分は見なかったことにしましょう. とりあえずは, こんな感じで基本波形が生成できるのか〜とながめてください. また, デモ 19で実際にサウンドが生成できているかを確認してみてください.

デモ 19

チャンネルリバース

左チャンネルからのサウンド出力と右チャンネルからのサウンド出力を反転する短銃なエフェクトです. ただし, その原理から, 左右のチャンネルのサウンドデータが異なる場合にしか効果がありません.

チャンネルリバースにおいては, 左右のinputBufferプロパティが必要となります.

一般的には, オーディオデータに対して適用するので, オーディオデータの準備としてAudioBufferSourceNodeかMediaElementAudioSourceNodeを入力ノードとして接続しておきます. コードはとてもシンプルです.

ノード接続

図1 - 5 - h. ノード接続

オーディオデータをonaudioprocessイベントハンドで処理するために, 入力ノードを接続していることに着目してください.

サンプルコード 12


/*
 * Add code to sample code 05
 */

// ....

// AudioBufferSourceNode start() or MediaElementAudioSourceNode play()

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
    for (var i = 0; i < this.bufferSize; i++) {
        outputLs[i] = inputRs[i];
        outputRs[i] = inputLs[i];
    }
};

デモ 20

ボーカルキャンセラ

一般的に, 音楽のオーディオデータはステレオで, 臨場感あるサウンドになるように, 左チャンネルと右チャンネルの音の大きさを調整しています. ボーカルの聴こえる位置は中央になるように左右のチャンネルを同じ音の大きさで録音し, ギターなどのボーカル以外の楽器音は, 左右どちらかの音の大きさを大きくして, 聴こえる位置が左右のどちらかになるようにすることで実現しています.

このように録音されたオーディオデータであれば, 左右のチャンネルのサウンドデータの差分をとることにより, 左右均等に録音されているボーカルの音を取り除くことが可能になります. これが, ボーカルキャンセラです.

ただし, その原理から, ドラムなど中央に位置する楽器音も取り除かれてしまいます.

ノード接続

図1 - 5 - i. ノード接続

オーディオデータをonaudioprocessイベントハンドで処理するために, 入力ノードを接続していることに着目してください.

サンプルコード 13


/*
 * Add code to sample code 05
 */

// ....

// AudioBufferSourceNode start() or MediaElementAudioSourceNode play()

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

    // Cancel Vocal
    for (var i = 0; i < this.bufferSize; i++) {
        outputLs[i] = inputLs[i] - inputRs[i];
        outputRs[i] = inputRs[i] - inputLs[i];
    }
};

サンプルコード 13でも, ボーカルキャンセラ自体は実装できていますが, これだと, ボーカルキャンセラのレベルを調整できないので, 取り除く音に係数を付加します.

サンプルコード 14


/*
 * Add code to sample code 05
 */

// ....

// AudioBufferSourceNode start() or MediaElementAudioSourceNode play()

var depth = 0.8;

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

    // Cancel Vocal
    for (var i = 0; i < this.bufferSize; i++) {
        outputLs[i] = inputLs[i] - (depth * inputRs[i]);
        outputRs[i] = inputRs[i] - (depth * inputLs[i]);
    }
};

これなら, 変数depthの値を調整することで, ボーカルキャンセラにバリエーションが出せます. 実際に, デモ 21で試してみてください.

デモ 21

ScriptProcessorNode まとめ

このページでは, サウンドデータの実体にアクセスすることによって, 柔軟性の高いサウンド処理を実現するScriptProcessorNodeについて解説しました. また, ScriptProcessorNodeを利用した簡単なサウンド処理の例もいくつか解説しました.

ScriptProcessorNodeの真価は, サウンドプログラミングや音信号処理の知識・経験レベルに応じてきます.

また, あまり理解できなかった…という場合でも, 大丈夫です. 経験を積んでいくと理解できることもありますし, Web Audio APIを利用して, 何かアプリケーションを作成することが1つの目標なら, ScriptProcessorNodeの利用は必須ではありません. なぜなら, Web Audio APIには, サウンドの入出力, エフェクター, サウンドの視覚化のためのノード, つまり, ハイレベルなAPIが定義されているからです.

前置きが長くなりましたが, ScriptProcessorNodeに関するポイントをまとめます.

インスタンス生成
createScriptProcessorメソッドで生成. 引数のバッファサイズはOSに依存.
ノード接続
(ダミーの) 入力ノードの接続とAudioDestinationNodeへの接続は必須.
onaudioprocessイベント
AudioDestinationNodeへの接続とイベントハンドラの設定が完了したときに最初のイベントが発生. そして, そのあとは, バッファサイズの処理ごとに発生. また, イベントの発生を停止するには, disconnectメソッドとonaudioprocessイベントハンドラのクリア処理 (nullの代入) を実行する.
onaudioprocessイベントオブジェクト
サウンドデータの実体にアクセスするためのインターフェースとなる.

ちなみに, ActionScript 3.0やAudio Data APIにおけるサウンド処理も同じようなアルゴリズムになっています. すなわち, 一定のバッファサイズごとにサウンドデータを設定して, それを繰り返すという処理です. したがって, ScriptProcessorNodeを使いこなすことが可能になれば, ActionScript 3.0やAudio Data APIのサウンド処理を習得するのも容易になるはずです. 一石二鳥どころか, 一石三鳥というわけです.