サウンドの生成

サウンド生成については概要のページで簡単に解説しましたが, このページではもう少し詳細を解説します.

とりあえずサウンドを生成する

まず, おさらいとして, とりあえずサウンド生成するコードを確認します.

ノード接続

図1 - 3 - a. ノード接続

サンプルコード 01


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

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

// 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 gain = context.createGain();

// OscillatorNode (Input) -> GainNode (Volume) -> AudioDestinationNode (Output)
oscillator.connect(gain);
gain.connect(context.destination);

// Start sound
oscillator.start(0);

// Stop sound (after 5 sec)
window.setTimeout(function() {
    oscillator.stop(0);
}, 5000);

  1. 1. 入力点を生成 (createOscillatorメソッド)
  2. 2. 入力点と出力点を接続 (connectメソッド)
  3. 3. 音源のスイッチをON (startメソッド)

アルゴリズムの概要は, オーディオデータを再生する場合と大差ありません.

OscillatorNode

OscillatorNodeのプロパティ

デモ 02・デモ 03においては, 1種類のサウンドしか生成することができませんでした. さすがに, これではアプリとして使えないですよね.

もちろん, 音の高さや音色を変化させることは可能です. これによってさまざな種類のサウンドを生成することができます. 音の高さや音色といった, 音の特性に大きく影響するOscillatorNodeインスタンスのプロパティは,

表1 - 3 - a. (音の特性に影響する) OscillatorNodeインスタンスのプロパティ
PropertyDescription
type波形を決定するためのプロパティ. 音色に大きく影響する.
frequency周波数を決定するためのプロパティ. 音の高さ (ピッチ) に大きく影響する.
detune複数のサウンドを合成する場合において, 音の高さを微妙にずらして, サウンドに厚みを出したり, オクターブ違いの音をミックスしたりするのに利用する. 前者はシンセサイザーのファインチューン機能で, 後者はオクターバーエフェクト.

波形って?周波数って?という方もいらっしゃるかと思いますが, 概要のページで紹介したデモに改良を施して, これらのプロパティを設定できるデモを用意したので, まずは体感してみてください. ちなみに, プロパティの設定は以下のようにします.

サンプルコード 02


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

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

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

// Set parameters
oscillator.type            = (typeof oscillator.type === 'string') ? 'square' : 1;  // Square wave
oscillator.frequency.value = 880;  // 880 Hz
oscillator.detune.value    = 100;  // 100 cent

デモ 10

いかがでしたか?これだけでも多様なサウンドを生成できるようになりました.

frequencyプロパティ, detuneプロパティもAudioParamのインスタンスなので, それぞれのvalueプロパティにアクセスする必要があります.

typeプロパティのとりうる値は, 現在の仕様では5種類の文字列型ですが, 初期の仕様においては, 数値型でした. したがって, ブラウザによってはどちらかの型に未対応の可能性もあります. そこで, サンプルコード 02のようにフォールバックを記述しておくと安全です.

表1 - 3 - b. OscillatorNodeインスタンスのtypeプロパティの値
Wave TypeStringNumber
正弦波sine0
矩形波square1
ノコギリ波sawtooth2
三角波triangle3
カスタム波形custom4

ただし, 'custom' (カスタム波形) のみは特殊で, 直接値を設定するとエラーが発生します. 実は, OscillatorNodeインスタンスのsetPeriodicWaveメソッドによって, (自動的に) 'custom' に設定されます. ちなみに, カスタム波形を生成する方法は, AudioContextインスタンスのcreatePeriodicWaveメソッドで波形テーブルを作成し, その波形テーブルをsetPeriodicWaveメソッドの引数に指定することです. 波形テーブルの作成は, スペクトルや倍音など音信号処理の知識が必要になるので, カスタム波形の作成は, サウンドの視覚化のページで解説します.

OscillatorNodeの特性

デモ 10や概要のページのデモのコードを見て疑問をもたれた方もいると思います. その疑問とはおそらく, stopメソッドを実行したあとに, どうして, 再度ノードを生成して接続しているのか? という疑問かと思います.

それは, OscillatorNodeインスタンスも言わば使い捨てのノードだからです. これと同じような仕様のノードは, AudioBufferSourceNodeインスタンスです. このような仕様であるので, サウンド生成のたびに新たにノードを生成して接続しているわけです.

また, デモで気づいたかもしれませんが, サウンド再生中にfrequencyプロパティを変化させると, 滑らかに音程が変化していたかと思います. このことは, 仕様には明記されていないのですが, OscillatorNodeインスタンスを生成して, 破棄せずに (すなわち, stopメソッドを実行せずに) frequencyプロパティを変化させると, 実装上ではグライドっぽく変化するようになっているみたいです.

グライド (ポルタメント) とは, 音程変化を滑らかにするエフェクトです. (制作者もそうですが) シンセサイザーのことはあまり知識がない…という方向けに他の楽器で言うと, ギターやベースのスライド奏法があてはまると思います. また, 楽譜で表現すればスラーになるでしょう.

もっとも, グライドタイム (音程の変化にどれくらいの時間を要するか? を決定するパラメータ) を設定できないので, 実際のグライドとしては利用できないでしょう. あくまでも, グライドっぽい感じを出せるにすぎないということです.

グライド

グライドっぽいのじゃ嫌だ…. (本物の) グライド機能を搭載したい方もいらっしゃると思います. グライドの話が出てきたところで, グライドを実装してみましょう.

ちなみに, このサイトの構成から考えると, エフェクターのページで解説することかもしれません. しかしながら, グライドは他のエフェクターと異なりDelayNodeなど他のノードを必要とせず, OscillatorNodeだけで実装可能です. グライドの原理の理解も, 音信号処理の知識は不要です. まあ, 音の高さの意味を感覚で理解している必要はあるかもしれませんが….

まずは, 先ほどのデモ 10を改良して, ピアノの鍵盤を (のイラストを) マウスで操作してサウンド生成するようにします. ピアノの音程に関することは, とりあえずはコメントを参考にしてみてください.

デモ 12

これだけでも, ちょっとアプリケーションっぽくなってきましたね. ピアノにこだわりのある方は, 改良して88鍵に対応させてみてはいかがでしょうか?

さて, ここから本題ですが, コードを記述する前に, グライドに必要なアルゴリズムとデータの概要を少し整理してみましょう. まずは, アルゴリズムの概要を考えてみます.

  1. 1. 最初はグライドOFF
  2. 2. 最初の音の高さを保存
  3. 3. 次の音の高さを取得
  4. 4. サウンド開始 (startメソッド)
  5. 5. 2. で保存した音の高さを3. で取得した音の高さに近づけていく (ループ)
  6. 6. ループ終了条件の判定
  7. 7. (3. で取得した音の高さ) を保存
  8. 8. 次のサウンド生成イベントで3. へ戻る

次に, アルゴリズムの概要から, 必要なデータを考えてみます.

最初の音の高さを保存するための変数
アルゴリズムの概要, 2. 5. 6. で必要.
次の音の高さを取得するための変数
アルゴリズムの概要, 3. 5. 6. 7. で必要.
音の高さの差 (音程) を埋めていく割合を決める変数
アルゴリズムの概要, 5. で必要.
グライド実装に必要な変数

図1 - 3 - b. グライド実装に必要な変数

では, それぞれの処理の詳細を順番に解説していきます.

1. 最初はグライドOFF

処理の重要なポイントは, 「最初」をどう判定するかです. 最初のサウンド生成ということが判定できれば, あとは同じです. 1つのアイデアとして, 2. で必要な変数をありえない値で初期化しておくという方法が考えられます.

つまり, このありえない値であれば, 「最初」と判定してグライドをOFFにします. 具体的には, デモ 12で考えると, -1 Hzの音はありえない値なので, これを利用します.

また, 2回目以降の場合は, 変数startFrequencyの値をfrequencyプロパティに設定します.

サンプルコード 03


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

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

// for the instance of OscillatorNode
var oscillator = null;

// Flag for starting or stopping sound
var isStop = true;

// Parameter for Glide
var startFrequency = -1;  // Abnormal value

document.getElementById('key-440').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

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

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

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

    // The 1st sound ?
    if (startFrequency === -1) {
        oscillator.frequency.value = 440;
    } else {
        oscillator.frequency.value = startFrequency;
    }

    oscillator.start(0);

    isStop = false;
}, false);

1つ注意点としては, サンプルコードの変数startFrequencyは, イベント処理が終了したあともスコープが有効である必要があります. したがって, グローバル変数か, できればクラスのプロパティ, あるいは, クロージャを活用するようにします.

2. 最初の音の高さを保存

次の処理は, 最初に生成する音の高さ, すなわち, frequencyプロパティの値を変数に保存します. イベントが終了してもスコープが有効である必要を確認してください.

サンプルコード 04


/*
 * Add code to sample code 03
 */

// ....

// Parameter for Glide
var startFrequency = -1;  // Abnormal value

document.getElementById('key-440').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

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

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

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

    // The 1st sound ?
    if (startFrequency === -1) {
        oscillator.frequency.value = 440;
    } else {
        oscillator.frequency.value = startFrequency;
    }

    startFrequency = 440;  // Global

    oscillator.start(0);

    isStop = false;
}, false);

document.getElementById('key-440').addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    isStop = true;
}, false);

3. 次の音の高さを取得

次に生成する音の高さを880 Hzだとします.

サンプルコード 05


/*
 * Add code to sample code 04
 */

// ....

document.getElementById('key-880').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = 880;

    isStop = false;
}, false);

4. サウンド開始 (startメソッド)

処理の実行自体は, 難しいことではありませんが, このタイミングでサウンド開始を実行することが重要です. その理由は, 次の処理で音程を変化させていく処理を実行しますが, それより前にサウンドが開始されていないと, 音程変化を聴きとることができません. すなわち, プログラム上でグライド処理を実行しているだけで, 意味のないものになってしまいます.

サンプルコード 06


/*
 * Add code to sample code 05
 */

// ....

document.getElementById('key-880').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = 880;

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

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

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

    oscillator.start(0);

    /* oscillator.frequency.value -> endFrequency */

    isStop = false;
}, false);

5. 2. で保存した音の高さを3. で取得した音の高さに近づけていく (ループ)

ここがグライドのコアとなる処理です. 音の高さを目的の高さへ近づけていくことは難しい処理ではありません. しかし, どれくらいの割合で音の高さを近づけていくのかが重要です.

簡単な実装として, 1 Hz, 0.1 Hzなどの固定した値で音の高さを近づけていく方法が考えられます. この方法でも, 滑らかに音程を変化させることは可能ですが, あまり良い方法ではないでしょう.

その理由は, 楽器の音程関係にあります. 例えば, ピアノでは, 最も低いラ (A) と最も低いシ (B) の音程差は数Hz程度ですが, 最も高いラ (A) と最も高いシ (B) の音程差は数百Hzにもなります. この音程関係から, 固定値で音の高さを近づけてしまうと, 音程変化の時間にばらつきが生じてしまいます. すなわち, 低い部分では短く, 高い部分では長くなってしまいます.

この問題を引き起こさないためには, どうすればいいのでしょうか…?1つの解として考えられるのは, 音程差をある値で除算した値で音の高さを近づけていく方法です. これなら, 音程差が小さいほど音の高さが近づく割合が小さく, 大きいほど音の高さが近づく割合が大きくなるので, 音程が変化する時間を (理論上は) 一定に保つことが可能になります. そして, 「ある値」というのが, グライドタイムを意味しています. グライドタイムが短いほど, 音程変化の時間が短く, 長いほど音程変化の時間が長くなります. サンプルコード 07は, このアルゴリズムを実装したコードです.

サンプルコードではグライドタイムを固定していますが, 実際のアプリケーションにおいては, イベント処理で値を設定できるようすることが多くなるでしょう. また, グライドタイムが0のときには, グライドOFFとしています. さらに, 音程差がない場合も, グライド処理をする必要がないので, グライドOFFとします. ループ処理に入る直前に, これらのケースを判定します.

サンプルコード 07


/*
 * Add code to sample code 06
 */

// ....

document.getElementById('key-880').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = 880;

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

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

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

    oscillator.start(0);

    var time = 100000;  // Glide time
    var diff = endFrequency - startFrequency;  // Pitch difference

    // Get rate
    if (time !== 0) {
        var rate = diff / time;
    }

    // Glide ON or OFF ?
    if ((time === 0) || (diff === 0)) {
        // When Glide time is 0 or the same sound is created, Glide is OFF.
        oscillator.frequency.value = endFrequency;
    } else {
        // Glide ON
        while (true) {
            oscillator.frequency.value += rate;  // Change frequency gradually

            /* End ? -> break; */
        }
    }

    isStop = false;
}, false);

グライドタイムの設定範囲ですが, 実際に試してみて, 10000 ~ 1000000ぐらいが適切かな…?と思いました. これよりも短すぎるとグライド効果が弱すぎ, 長すぎると音程変化が収束するまでに時間がかかりすぎます.

もっとも, 個人的な感覚なので, あくまでも目安として参考にしてください.

6. ループ終了条件の判定

先ほどのサンプルコードのままでは, 大変なことになってしまいます. 無限ループが存在するので…. ループ処理の終了条件は, 差分 (音程差) が0になることです. つまり, この条件は, frequencyプロパティがendFrequencyの値と等しくなった場合と等価です. もっとも, 小数点演算を繰り返しているので, (ほぼ確実に) 誤差が発生しているでしょう. したがって, 差分 (音程差) が0, すなわち, frequencyプロパティが, endFrequencyの値と等しくなるケースが必ず存在するとは限りません. そこで, 「等しい」ではなく「以上, または, 以下」で判定します (差分が正なら「以上」, 負なら「以下」) .

さらに, 小数点演算による誤差を考慮して, ループを抜けたらfrequencyプロパティをendFrequencyの値で更新しておきます. この処理は, グライドOFFの場合の処理とまとめてしまいます.

サンプルコード 08


/*
 * Add code to sample code 07
 */

// ....

document.getElementById('key-880').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = 880;

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

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

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

    oscillator.start(0);

    var time = 100000;  // Glide time
    var diff = endFrequency - startFrequency;  // Pitch difference

    // Get rate
    if (time !== 0) {
        var rate = diff / time;
    }

    // Glide ON or OFF ?
    if ((time === 0) || (diff === 0)) {
        // When Glide time is 0 or the same sound is created, Glide is OFF.
        oscillator.frequency.value = endFrequency;
    } else {
        // Glide ON
        while (true) {
            oscillator.frequency.value += rate;  // Change frequency gradually

            if ((diff > 0) && (oscillator.frequency.value >= endFrequency)) {break;}  // Up
            if ((diff < 0) && (oscillator.frequency.value <= endFrequency)) {break;}  // Down
        }
    }

    // Glide OFF or End of Glide
    oscillator.frequency.value = endFrequency;

    isStop = false;
}, false);

7. (3. で取得した音の高さ) を保存

ここまで処理が完了すれば, グライドのコア部分の処理は終わっています. あと必要な処理は, 次のサウンド生成でもグライド効果を得るように処理するだけです. それには, 最初の音の高さを保存している変数startFrequencyの値を, 到達した音の高さを格納している変数endFrequencyの値で更新する必要があります.

サンプルコード 09


/*
 * Add code to sample code 08
 */

// ....

document.getElementById('key-880').addEventListener('mousedown', function() {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = 880;

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

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

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

    oscillator.start(0);

    var time = 100000;  // Glide time
    var diff = endFrequency - startFrequency;  // Pitch difference

    // Get rate
    if (time !== 0) {
        var rate = diff / time;
    }

    // Glide ON or OFF ?
    if ((time === 0) || (diff === 0)) {
        // When Glide time is 0 or the same sound is created, Glide is OFF.
        oscillator.frequency.value = endFrequency;
    } else {
        // Glide ON
        while (true) {
            oscillator.frequency.value += rate;  // Change frequency gradually

            if ((diff > 0) && (oscillator.frequency.value >= endFrequency)) {break;}  // Up
            if ((diff < 0) && (oscillator.frequency.value <= endFrequency)) {break;}  // Down
        }
    }

    // Glide OFF or End of Glide
    oscillator.frequency.value = endFrequency;

    // Update
    startFrequency = endFrequency;

    isStop = false;
}, false);

document.getElementById('key-880').addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    isStop = true;
}, false);

8. 次のサウンド生成イベントで3. へ戻る

ここまでの処理をイベントハンドラもしくはイベントリスナーとして設定していれば, イベントが発生するたびに, 必要な処理が実行されるでしょう. ここでは, これまでのサンプルコードを整理した形で記載します.

サンプルコード 10


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

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

// for the instance of OscillatorNode
var oscillator = null;

// Flag for starting or stopping sound
var isStop = true;

// Parameter for Glide
var startFrequency = -1;  // Abnormal value
var time           = 0;   // Glide time

var startSound = function(frequency) {
    if (!isStop) {
        oscillator.stop(0);
    }

    var endFrequency = frequency;

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

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

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

    // The 1st sound ?
    if (startFrequency === -1) {
        oscillator.frequency.value = endFrequency;
    } else {
        oscillator.frequency.value = startFrequency;
    }

    oscillator.start(0);

    var diff = endFrequency - startFrequency;  // Pitch difference

    // Get rate
    if (time !== 0) {
        var rate = diff / time;
    }

    // Glide ON or OFF ?
    if ((startFrequency === -1) || (time === 0) || (diff === 0)) {
        // When the 1st sound is created or 
        //                  Glide time is 0 or 
        //                      the same sound is created, Glide is OFF.
    } else {
        // Glide ON
        while (true) {
            oscillator.frequency.value += rate;  // Change frequency gradually

            if ((diff > 0) && (oscillator.frequency.value >= endFrequency)) {break;}  // Up
            if ((diff < 0) && (oscillator.frequency.value <= endFrequency)) {break;}  // Down
        }
    }

    // Glide OFF or End of Glide
    oscillator.frequency.value = endFrequency;

    // Update
    startFrequency = endFrequency;

    isStop = false;
};

var stopSound = function() {
    if (isStop) {
        return;
    }

    oscillator.stop(0);

    isStop = true;
};

document.getElementById('key-440').addEventListener('mousedown', function() {
    startSound(440);
}, false);

document.getElementById('key-440').addEventListener('mouseup', function() {
    stopSound();
}, false);

document.getElementById('key-880').addEventListener('mousedown', function() {
    startSound(880);
}, false);

document.getElementById('key-880').addEventListener('mouseup', function() {
    stopSound();
}, false);

document.querySelector('[type="range"]').addEventListener('change', function() {
    time = this.valueAsNumber;
}, false);

これで, グライドが完成しました. デモ 13では, 鍵盤のノードオブジェクトを配列で取得して, イベントリスナーの設定をしていますが, グライドの本質的な処理は同じです.

デモ 13

エンベロープジェネレータのページにおいて解説するAudioParamインスタンスであるfrequencyプロパティのメソッドを利用することによって, よりスマートに, さらに, UIをブロックすることなくグライドを実装可能です. ただし, その場合でもグライドのアルゴリズムの考え方はほとんど同じです. もし余力のある方は, AudioParamインスタンスのメソッドを利用して実装してみてください.

サウンドスケジューリング

時刻の取得と設定

サウンドスケジューリングには, start / stopメソッドの引数にスケジューリングに応じた時刻を指定します. AudioContextインスタンスのcurrentTimeプロパティにアクセスすることで, 時刻を取得することが可能です. currentTimeプロパティは, AudioContextインスタンスの生成を基準時として, そこからの経過時間を秒単位で格納しています.

具体例として, 1秒ごとにサウンドを生成して, 一定時間経過後, 1秒ごとに停止させたい場合, サンプルコード 11のようなコードになります.

サンプルコード 11


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

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

// Create the instances of OscillatorNode

// Chord C
var C = context.createOscillator();
C.frequency.value = 261.63;
C.start = C.start || C.noteOn;
C.stop  = C.stop  || C.noteOff;

// Chord E
var E = context.createOscillator();
E.frequency.value = 329.63;
E.start = E.start || E.noteOn;
E.stop  = E.stop  || E.noteOff;

// Chord G
var G = context.createOscillator();
G.frequency.value = 392.00;
G.start = G.start || G.noteOn;
G.stop  = G.stop  || G.noteOff;

// OscillatorNode (Input) -> AudioDestinationNode (Output)
C.connect(context.destination);
E.connect(context.destination);
G.connect(context.destination);

// Get base time
var t0 = context.currentTime;

// Wait time until stopping sound
var wait = 3;  // 3 sec

// Start sound (at intervals of 1 sec)
C.start(t0 + 1);
E.start(t0 + 2);
G.start(t0 + 3);

// Stop sound (at intervals of 1 sec)
C.stop(t0 + wait + 1);
E.stop(t0 + wait + 2);
G.stop(t0 + wait + 3);

サウンドスケジューリング

図1 - 3 - c. サンプルコード 111のサウンドスケジューリング

クリッピング対策

ところが, サンプルコード 11の実装のままでは, 音源が重なっていくにつれて, 意図しない音割れ (歪み) が生じてしまいます (また, このような音割れをクリッピングと呼びます. クリッピングを意図的に発生させたエフェクトがディストーションです) .

これを解決する最も単純な方法は, GainNodeを接続して, 音割れが生じないようにゲインを設定することです.

サンプルコード 12


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

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

// Create the instances of OscillatorNode

// Chord C
var C = context.createOscillator();
C.frequency.value = 261.63;
C.start = C.start || C.noteOn;
C.stop  = C.stop  || C.noteOff;

// Chord E
var E = context.createOscillator();
E.frequency.value = 329.63;
E.start = E.start || E.noteOn;
E.stop  = E.stop  || E.noteOff;

// Chord G
var G = context.createOscillator();
G.frequency.value = 392.00;
G.start = G.start || G.noteOn;
G.stop  = G.stop  || G.noteOff;

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

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

// for preventing clipping
gain.gain.value = 0.3;

// OscillatorNode (Input) -> GainNode (Master Volume) -> AudioDestinationNode (Output)
C.connect(gain);
E.connect(gain);
G.connect(gain);
gain.connect(context.destination);

// Get base time
var t0 = context.currentTime;

// Wait time until stopping sound
var wait = 3;  // 3 sec

// Start sound (at intervals of 1 sec)
C.start(t0 + 1);
E.start(t0 + 2);
G.start(t0 + 3);

// Stop sound (at intervals of 1 sec)
C.stop(t0 + wait + 1);
E.stop(t0 + wait + 2);
G.stop(t0 + wait + 3);

より性能の良い方法は, DynamicsCompressorNodeクラスを利用することです. DynamicsCompressorNodeについてはエフェクターのページで解説します.

それでは, ここまでのまとめとしてデモ 14を実行してみてください. デモ 14では, currentTimeプロパティの値の表示と, 設定した間隔でコード (和音) のCの構成音 (ド・ミ・ソ) を順次生成して, 停止します. ゲインを大きくしたときに, クリッピング (音割れ) が発生することも確認してみてください.

デモ 14

デモ 14のようにコードの構成音を少しずつずらして発音させる奏法は, アルペジオ (分散和音) と呼ばれます.

ガベージコレクション

OscillatorNodeは, 使い捨てのノードという仕様であることを解説しました. つまり, インスタンスの生成と破棄を繰り返すので, OscillatorNodeインスタンスに割り当てられたメモリの解放が実行される条件については理解しておく必要があります.

OscillatorNodeインスタンスに限らず, Web Audio APIが定義するクラスのインスタンスにおいては, 以下の5つの条件すべてにあてはまる場合, ガベージコレクションの対象になります.

  • 参照が残っていない
  • サウンドが停止している
  • サウンドスケジューリングが設定されていない
  • ノードが接続されていない
  • 処理すべきデータが残っていない

つまり, 何らかの形で利用されているノードはガベージコレクションの対象とならないということです.

OscillatorNodeにおいて, ガベージコレクションが実行される条件で重要なのは, 最初の3つです. なぜなら, OscillatorNodeは他のノードの出力先 (接続先) として利用されることがないこと, また, 最後の条件はDelayNodeやConvolverNodeなどにおいて考慮すべきことであり, OscillatorNodeでは無関係だからです.

したがって, このセクションでは, 最初の3つの条件に関して解説します.

参照が残っていない

これは, ガベージコレクションが実装されている言語 (Javaなど) と同じことです.

サンプルコード 13は, ガベージコレクションが実行されます. その理由は, インスタンスの生成と同時に, 以前のインスタンスへの参照が破棄されて, かつ, インスタンス生成以外の処理 (サウンドの開始やスケジューリングの設定) をしていないからです (ただし, 新しく生成されたインスタンスへの参照は残ります) .

サンプルコード 13


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

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

// for the instance of OscillatorNode
var oscillator = null;

window.setInterval(function() {
    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();
}, 0);

サウンドが停止している

サウンドを再生しているOscillatorNodeインスタンスは, 参照が残っていなくても, ガベージコレクションの対象となりません.

サンプルコード 14は, ガベージコレクションが実行されず, メモリがしだいに不足していく例です. その理由は, コールバック関数実行のたびに, 以前のインスタンスへの参照は破棄されますが, それに対応するサウンドが停止していないからです.

サンプルコード 14


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

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

// for the instance of OscillatorNode
var oscillator = null;

window.setInterval(function() {
    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

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

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

    // Start sound
    oscillator.start(0);
}, 0);

OscillatorNodeインスタンスのガベージコレクションを適切に実行するために, startメソッドとstopメソッドは一対という考え方は重要です. さらに, OscillatorNodeインスタンスの状態遷移を設計するためにも必要です.

ちなみに, 状態遷移の設計が必要な理由は, start / stopメソッドの多重呼び出しや, startメソッドを実行せずに, stopメソッドを実行するとエラーが発生するからです (サンプルコード 15, 16) .

サンプルコード 15


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

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

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

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

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

// Start sound
oscillator.start(0);

// Error !!
oscillator.start(0);

サンプルコード 16


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

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

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

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

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

// oscillator.start(0);

// Error !!
oscillator.stop(0);

サウンドスケジューリングが設定されていない

サウンドスケジューリングが設定されているOscillatorNodeインスタンスは, 参照が残っていなくても, ガベージコレクションの対象となりません.

サンプルコード 17は, 時間が経過するほどサウンドの開始が少しずつ遅延するようにサウンドスケジューリングされているので, OscillatorNodeインスタンスへのメモリの割り当てが増加し, メモリが不足していきます.

サンプルコード 17


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

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

// for the instance of OscillatorNode
var oscillator = null;

var counter = 0;

window.setInterval(function() {
    // Create the instance of OscillatorNode
    oscillator = context.createOscillator();

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

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

    // Start sound
    oscillator.start(context.currentTime + counter++);
}, 0);

サウンドの生成 まとめ

このページでは, サウンド生成と言いつつグライドの実装も解説したので, コンテンツ量が多くなり大変だったと思いますが, 重要なポイントを以下にまとめます.

OscillatorNode
  • typeプロパティ
  • frequencyプロパティ
  • detuneプロパティ
アルゴリズム
  1. 1. 入力点を生成
  2. 2. 入力点と出力点を接続
  3. 3. 音源のスイッチをON
  4. 4. 音源のスイッチをOFF
  5. 5. ノードを再生成・再接続
サウンドスケジューリング
  • 時刻の取得 (AudioContextインスタンスのcurrentTimeプロパティ)
  • start / stopメソッドの引数
  • クリッピング対策

また, ガベージコレクションや状態遷移を設計するうえで重要なのは, 以下の2点です.

  • 何らかの形で利用されているノードはガベージコレクションの対象とならない
  • startメソッドとstopメソッドは一対

ポイントとなる処理は, オーディオデータの再生でもサウンドの生成でも同じです.