エンベロープジェネレータの概要

エンベロープとは?

エンベロープジェネレータを解説するために, まず, エンベロープの解説をします.

音の特性のページで, 周期性をもつ音波は, 周波数の異なる正弦波を合成して構成されていると解説しました. 実際の楽器音などの波形は, 図1 - 6 - aのような複雑な形をしています.

楽器音などの波形

図1 - 6 - a. 楽器音などの波形

ここで質問をしてみます. 図1 - 6 - aの波形の概形はどんな形になるでしょうか? (解答は, 図1 - 6 - bにマウスを重ねてみてください)

波形の概形

図1 - 6 - b. 波形の概形

この波形の概形 (図1 - 6 - bのマウスオーバー時の紫のライン) がエンベロープです. より正確には, 振幅に対する波形の概形なので, 振幅エンベロープと呼びます.

エンベロープジェネレータとは?

エンベロープジェネレータをそのまま翻訳すると,「波形の概形の生成をするもの」ということですね.

さすがに, これでは, 何のためにあるのかがわからないので, 少し詳しく解説します. 音の特性のページで解説したように, 波形の概形, つまり, 振幅エンベロープは音色に大きく影響しました. 実は, エンベロープジェネレータの目的も同じで, 音色を変化させるためにあります. エンベロープジェネレータのパラメータを適切に設定することで, 同じ波形タイプであるにもかかわらず, 例えば, ピアノに近い音色にしたり, バイオリンに近い音色にしたりといったことが可能になります.

再度, 図1 - 6 - bを見てもらいたいのですが, 波形の概形, つまり, 振幅エンベロープをもう少し分解して考えると, 時間に対する振幅値の変化を表していると言えます. 音の特性のページでも解説しましたが, 振幅とは音の大きさに大きく影響する物理量でした. 振幅エンベロープとは, 時間に対する音の大きさの変化を表していると言えます. つまり, エンベロープジェネレータとは, 時間に対する音の大きさの変化を決定する機能ということです.

正確には, 時間に対する音の大きさに大きく影響する振幅値のゲイン (増幅率) の変化を決定する機能です. ゲインを定量的に解説すると, 入力サウンドの振幅値に対する出力サウンドの振幅値の割合と定義できます (図1 - 6 - c). もし, ゲインについてよく理解できなければ, ゲインを音の大きさと考えても大きな問題はありません.

ゲイン (増幅率) の定義

図1 - 6 - c. ゲイン (増幅率)

入力の振幅値がどのくらい増幅あるいは減衰するのかを意味しています.

エンベロープジェネレータとは, 時間に対するゲイン (音の大きさ) の変化をスケジューリングする機能です.

エンベロープジェネレータのパラメータ

エンベロープジェネレータは, いくつかのパラメータを設定することによって, ゲインのスケジューリングを実現します. (一般的に) 設定可能なパラメータは4つあります.

エンベロープジェネレータのパラメータ

図1 - 6 - d. エンベロープジェネレータのパラメータ

  • アタック (タイム)
  • ディケイ (タイム)
  • サステイン (レベル)
  • リリース (タイム)

アタック

アタック (Attack) とは, ゲインが最大値 (1) になるまでの時間のことです. もう少しくだいて表現すれば, 音の立ち上がりの速さを決定するパラメータと言えます. 楽器で具体例をあげると, ピアノやギターは比較的音の立ち上がりが速い楽器で, バイオリンやフルートなどは比較的音の立ち上がりが遅い楽器です. すなわち, アタックを短くするとピアノやギターのように音の立ち上がりが速くなり, アタックを長くするとバイオリンやフルートのように音の立ち上がりが遅くなります.

ちなみに, 音の立ち上がりが比較的速い (アタックが短い) エレキギターでは, バイオリン奏法と呼ばれる奏法があります. これは, ピッキング時に, ギターのボリュームを0にすることによって, ピッキングした瞬間の音 (アタック音) を消し, そのあとに, ボリュームを増加させるという奏法です. エレキギターであるのに, まるでバイオリンのような音色を奏でることができます.

ディケイ

ディケイ (Decay) とは, ゲインがサステインにまで減衰する時間のことです. すなわち, ディケイを短くするとゲインが最大値 (1) から急激に減衰し, ディケイを長くすると最大値 (1) からゆっくりと減衰します.

サステイン

サステイン (Sustain) とは, ゲインの減衰が収束する値のことです. つまり, ディケイ後からリリース前までの間に, 持続させるゲイン (レベル) のことです. 例えば, シンセサイザーの鍵盤を押している間 (ディケイ後) は, サステインに依存した音の大きさで音が持続します.

サステインという用語は, エンベロープジェネレータに限らず, 楽器に対しても利用されるものです. 例えば, ピアノのダンパーペダル (右側のペダル) は, サステインペダルとも呼ばれますし, X JAPANのギタリストであるHIDEさんが愛用している, フェルナンデス製のモッキンバードには, サスティナーと呼ばれる機能が搭載されている機種があります. いずれも, 生成した音の余韻 (伸び) をコントロールことが目的です.

つまり, 楽器におけるサステインは, エンベロープジェネレータ (やシンセサイザー) におけるリリースに相当します. この点には注意してください.

リリース

リリース (Release) とは, ゲインがサステインから最小値 (0) になるまでに要する時間のことです. つまり, リリースを短くすると音の余韻が短く, リリースを長くすると余韻を長くすることができます. 例えば, ドラムのような音の余韻が短い楽器をシミュレートしたり, スタッカート (音を短く切って演奏する楽譜の記号) を実現したりする場合はリリースを短く, 逆に, ダンパーペダルを踏んだピアノの音や, フェルマータ (音を長く伸ばして演奏する楽譜の記号) を実現したりする場合はリリースを長くします.

アタック・ディケイ・リリースは物理量が時間なのに対して, サステインのみゲインなので注意してください.

表1 - 6 - a. エンベロープジェネレータのパラメータ
ParameterDescriptionDimension
アタック (Attack) 立ち上がり時間時間
ディケイ (Decay) 減衰時間時間
サステイン (Sustain) 持続レベルゲイン
リリース (Release) 余韻時間時間

エンベロープジェネレータの実装

Web Audio APIでエンベロープジェネレータを実装するには, AudioParamインスタンスであるGainNodeのgainプロパティを利用します.

AudioParam

AudioParamインスタンスは, 以下のように定義されています.

表1 - 6 - b. AudioParamインスタンスのプロパティ
PropertyDescription
valueパラメータ値
defaultValueパラメータの初期値 (readonly)
setValueAtTime(value, startTime)startTimeにパラメータをvalueに設定する
linearRampToValueAtTime(value, endTime)endTimeにパラメータがvalueになるように (直線的に) 変化させる
exponentialRampToValueAtTime(value, endTime)endTimeにパラメータがvalueになるように (指数関数的に) 変化させる
setTargetAtTime(target, startTime, timeConstant)startTimeになったら, パラメータをtargetに向けて, timeConstantの時間をかけて変化させる (より正確には, targetの約63.2%まで変化するのに, timeConstantの時間を要する)
setValueCurveAtTime(values, startTime, duration)startTimeになったら, 配列values (Float32Array) の値にしたがって, durationの時間をかけて変化させる
cancelScheduledValues(startTime)startTime以降のスケジューリングを解除する

エンベロープジェネレータとは, ゲインの変化をスケジューリングする機能であり, AudioParamインスタンスで定義されているメソッドの概要から, AudioParamインスタンスであるGainNodeのgainプロパティにおいて, これらのメソッドを利用すれば実装できそうです.

初期化

まず, 初期化処理として, setValueAtTimeメソッドを利用します.

サンプルコード 01


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

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

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

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

// Create the instance of GainNode (for Envelope Generator)
var eg = context.createGain();

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

var attack  = 0.5;
var decay   = 0.3;
var sustain = 0.5;
var release = 1.0;

document.body.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) -> GainNode (Envelope Generator) -> AudioDestinationNode (Output)
    oscillator.connect(eg);
    eg.connect(context.destination);

    var t0 = context.currentTime;

    // Start sound at t0
    oscillator.start(t0);

    // Start from gain = 0
    eg.gain.setValueAtTime(0, t0);

    // Attack -> Decay -> Sustain ....

    isStop = false;
}, false);

重要なポイントは, 以下の2つです.

  1. エンベロープジェネレータのノード (GainNodeインスタンス) を生成して接続する
  2. サウンド開始時刻にgainプロパティを0にする (setValueAtTimeメソッド)

エンベロープジェネレータのためのノードである, GainNodeインスタンスの生成と (接続) を実行しておきます. また, サウンド開始時刻にgainプロパティが0になるようにsetValueAtTimeメソッドでスケジューリングします.

GainNodeインスタンスのgainプロパティは, AudioParamのインスタンスなので, setValueAtTimeメソッドにアクセス可能で, そのvalueプロパティがスケジューリングの対象 (= ゲイン) になるわけです.

そして, サウンド開始時刻は, AudioContextインスタンスのcurrenTimeプロパティで取得することができます. currentTimeプロパティは, AudioContextインスタンスの生成からアクセス時点までの経過時間を秒単位で格納しています.

初期化処理

図1 - 6 - e.

時刻t0においてゲインが0になるようにします.

アタック

アタックは, ゲインが最大値, すなわち, 1になるまでに要する時間のことでした. そこで, アタックの実装には, linearRampToValueAtTimeメソッドを利用します.

サンプルコード 02


/*
 * Add code to sample code 01
 */

// ....

// (at start) + (attack time)
var t1 = t0 + attack;

// Attack : gain increases linearly until assigned time (t1)
eg.gain.linearRampToValueAtTime(1, t1);

// Decay -> Sustain ....

isStop = false;

// ....

linearRampToValueAtTimeメソッドの第1引数には, ある時刻にgainプロパティが1になるように変化させたいので, ゲインの最大値である1を指定します.

注意が必要なのは第2引数です. アタック (タイム) の値をそのまま指定してしまうとうまくいきません. なぜなら, 時間ではなく時刻を指定する必要があるからです. したがって, サウンド開始時刻 (変数t0) にアタックを加算した値 (変数t1) を第2引数に指定します.

アタックの実装は, 直線的に変化させるlinearRampToValueAtTimeメソッドではなく, 指数関数的に変化させるexponentialRampToValueAtTimeメソッドでも可能です. 引数も同じです. ただし, 一般的に, エンベロープジェネレータでは, 直線的に変化させることが多いみたいです.

アタックの実装

図1 - 6 - f.

ゲインが最大値 (1) になる時刻t1は, 時刻t0とアタックタイムの加算で算出可能です.

時刻t0から時刻t1までに, ゲインが直線的に変化していることに着目してください.

ディケイ / サステイン

ディケイは, ゲインが最大値 (1) からサステインにまで減衰する時間でした. setTargetAtTimeメソッドを利用することで実装できます.

サンプルコード 03


/*
 * Add code to sample code 02
 */

// ....

var t2      = decay;
var t2Value = sustain;

// for legacy browsers
eg.gain.setTargetAtTime = eg.gain.setTargetAtTime ||
                          eg.gain.setTargetValueAtTime;

// Decay -> Sustain :
//    gain gradually decreases to value of sustain
//        during decay time (t2) from assigned time (t1)
eg.gain.setTargetAtTime(t2Value, t1, t2);

isStop = false;

// ....

初期の仕様においては, setTargetAtTimeメソッドは定義されておらず, その代わりにsetTargetValueAtTimeメソッドを利用していました. したがって, メソッド呼び出しより前にフォールバックの記述をしています.

注意が必要なのは, 第2引数と第3引数です. 第2引数にはパラメータが変化を開始する時刻を指定し, 第3引数にはパラメータが第1引数で指定した値 (の約63.2%) まで変化するのに要する時間を指定します.

したがって, 第2引数はgainプロパティが1となる時刻 (減衰開始時刻) である変数t1を指定し, 第3引数はディケイ (タイム) である変数t2を指定します.

そして, 第1引数はgainプロパティが収束する値であるサステイン (レベル) を指定します.

ディケイ / サステインの実装

図1 - 6 - g.

パラメータ変化が, 時刻t1から時間t2を要してサステインレベル (の63%) にまで変化していくことに着目してください.

リリース

リリースは, ゲインがサステインから最小値 (0) に変化するまでの時間なので, ディケイ / サステインと同じく, setTargetAtTimeメソッドを利用します.

サンプルコード 04


/*
 * Add code to sample code 03
 */

// ....

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

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

    var t3 = context.currentTime;
    var t4 = release;

    // for legacy browsers
    eg.gain.setTargetAtTime = eg.gain.setTargetAtTime ||
                              eg.gain.setTargetValueAtTime;

    // Release :
    //    gain gradually decreases to 0
    //        during release time (t4) from assigned time (t3)
    eg.gain.setTargetAtTime(0, t3, t4);  // Release
}, false);

リリースは, アタック ~ ディケイ / サステインとは異なるイベントで定義しています. また, setTargetAtTimeメソッドのフォールバックを記述しています (このフォールバックは共通化するのがベターでしょう).

setTargetAtTimeメソッドの引数ですが, gainプロパティを0に近づけていくので, 第1引数には0を指定します. 第2引数に指定するリリースの開始時刻は, AudioContextインスタンスのcurrentTimeプロパティ値です. また, 第3引数には変化に要する時間, すなわち, リリース (タイム) を指定します.

リリースの実装

図1 - 6 - h.

時刻t3 (イベント発生) から, 時間t4を要して, ゲインが0に近づくことに着目してください.

リリースを実装する場合は, OscillatorNodeインスタンスのstopメソッドの即時実行は不要です. その理由は, stopメソッドを即時実行すると, その時点で音が停止してしまうので, 音に余韻が生まれません.

といっても, このままでは, startメソッドの多重呼び出しになります. すなわち, startメソッドとstopメソッドは一対ということが順守できていません.

そこで, タイマー処理でgainプロパティをチェックして, 停止とみなせる値になれば, stopメソッドを実行 (フラグも変更) します. ここで, 最小値である0と表現しなかったのは理由があります. 確かに, 理論上は, 停止とみなせる値は0ですが, 実装上では, (原因はわかりませんが) 半永久的に0にはなりません. したがって, サンプルコード 05やデモ 22では, 停止とみなせる値を0.001未満と設定しています.

サンプルコード 05


/*
 * Add code to sample code 04
 */

// ....

// for detecting sound stop
var intervalid = null;

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

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

    var t3 = context.currentTime;
    var t4 = release;

    // for legacy browsers
    eg.gain.setTargetAtTime = eg.gain.setTargetAtTime ||
                              eg.gain.setTargetValueAtTime;

    // Release :
    //    gain gradually decreases to 0
    //        during release time (t4) from assigned time (t3)
    eg.gain.setTargetAtTime(0, t3, t4);  // Release

    intervalid = window.setInterval(function() {
        var VALUE_OF_STOP = 1e-3;

        if (eg.gain.value < VALUE_OF_STOP) {
            // Stop sound
            oscillator.stop(0);

            if (intervalid !== null) {
                window.clearInterval(intervalid);
                intervalid = null;
            }

            isStop = true;
        }
    }, 0);
}, false);

これで, 完成しました…と言いたいところですが, 1つ問題点があります. もし, アタックタイムもしくはディケイタイムが経過する前に, mouseupイベントが発生するとどうなるでしょう?アタック, ディケイ / サステインのゲイン変化のスケジューリングと, リリースにおけるゲイン変化のスケジューリングが…混在してしまいますね.

すなわち, サンプルコード 05だと, 意図したスケジューリングにならない可能性があるという問題点があります. これを解決するには, イベント発生時にスケジューリングをすべて解除すればOKです. そして, スケジューリングの解除には, cancelScheduledValuesメソッドを利用します.

サンプルコード 06


/*
 * Add code to sample code 05
 */

// ....

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

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

    var t3 = context.currentTime;
    var t4 = release;

    // for legacy browsers
    eg.gain.setTargetAtTime = eg.gain.setTargetAtTime ||
                              eg.gain.setTargetValueAtTime;

    // in the case of mouse up on the way of Attack or Decay
    eg.gain.cancelScheduledValues(t3);
    eg.gain.setValueAtTime(eg.gain.value, t3);

    // Release :
    //    gain gradually decreases to 0
    //        during release time (t4) from assigned time (t3)
    eg.gain.setTargetAtTime(0, t3, t4);  // Release

    intervalid = window.setInterval(function() {
        var VALUE_OF_STOP = 1e-3;

        if (eg.gain.value < VALUE_OF_STOP) {
            // Stop sound
            oscillator.stop(0);

            if (intervalid !== null) {
                window.clearInterval(intervalid);
                intervalid = null;
            }

            isStop = true;
        }
    }, 0);
}, false);

cancelScheduledValuesメソッドは, 引数で指定した時刻以降のパラメータのスケジューリングを解除するので, リリースの開始時刻である変数t3, すなわち, AudioContextインスタンスのcurrentTimeプロパティの値を指定します.

そして, ゲインの変化がサステイン (レベル) から開始されるように, リリース開始時刻 (t3 ) になったら, 現在のgainプロパティ (= サステインレベル) になるように, setValueAtTimeメソッドでスケジューリングします.

最後に, これまでのコードをまとめます.

サンプルコード 07


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

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

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

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

// Create the instance of GainNode (for Envelope Generator)
var eg = context.createGain();

// for legacy browsers
eg.gain.setTargetAtTime = eg.gain.setTargetAtTime ||
                          eg.gain.setTargetValueAtTime;

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

var attack  = 0.5;
var decay   = 0.3;
var sustain = 0.5;
var release = 1.0;

document.body.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) -> GainNode (Envelope Generator) -> AudioDestinationNode (Output)
    oscillator.connect(eg);
    eg.connect(context.destination);

    var t0 = context.currentTime;

    // Start sound at t0
    oscillator.start(t0);

    // Start from gain = 0
    eg.gain.setValueAtTime(0, t0);

    var t1      = t0 + attack;  // (at start) + (attack time)
    var t2      = decay;
    var t2Value = sustain;

    // Attack : gain increases linearly until assigned time (t1)
    eg.gain.linearRampToValueAtTime(1, t1);

    // Decay -> Sustain :
    //    gain gradually decreases to value of sustain
    //        during decay time (t2) from assigned time (t1)
    eg.gain.setTargetAtTime(t2Value, t1, t2);

    isStop = false;
}, false);

document.body.addEventListener('mouseup', function() {
    if (isStop) {
        return;
    }

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

    var t3 = context.currentTime;
    var t4 = release;

    // in the case of mouse up on the way of Attack or Decay
    eg.gain.cancelScheduledValues(t3);
    eg.gain.setValueAtTime(eg.gain.value, t3);

    // Release :
    //    gain gradually decreases to 0
    //        during release time (t4) from assigned time (t3)
    eg.gain.setTargetAtTime(0, t3, t4);  // Release

    intervalid = window.setInterval(function() {
        var VALUE_OF_STOP = 1e-3;

        if (eg.gain.value < VALUE_OF_STOP) {
            // Stop sound
            oscillator.stop(0);

            if (intervalid !== null) {
                window.clearInterval(intervalid);
                intervalid = null;
            }

            isStop = true;
        }
    }, 0);
}, false);

以上で, エンベロープジェネレータが完成しました. まとめとしてデモ 22を実行してみてください. デモ 22では, 全体のボリューム (= マスターボリューム) コントロールのためのGainNodeインスタンスも利用していますが, エンベロープジェネレータの本質的な処理に関しては, サンプルコードと変わりありません. さらに, ゲインの変化を視覚化しているので, 理解に役立ててもらえればと思います.

デモ 22

ちなみに, これらのスケジューリングをsetValueCurveAtTimeメソッドを利用して設定することも可能です. このメソッドの第1引数にはパラメータの変化を指定したFloat32Array型の配列を指定し, 第2引数には開始時刻, 第3引数には変化に要する時間を指定します. ただ, こだわりがなければ, サンプルコードで利用したメソッドでも十分でしょう.

エンベロープジェネレータ まとめ

このページでは, エンベロープおよびエンベロープジェネレータの概念からその実装まで解説してきました. そのエッセンスを以下にまとめておきます.

(振幅) エンベロープ
振幅に対する波形の概形.
エンベロープジェネレータ
ゲインの変化をスケジューリングする機能.
表1 - 6 - c. エンベロープジェネレータのパラメータとAudioParamインスタンスのメソッド
ParameterDescriptionMethod
アタック立ち上がり時間setValueAtTime
linearRampToValueAtTime
ディケイ減衰時間setTargetAtTime
サステイン持続レベルsetTargetAtTime
リリース余韻時間cancelScheduledValues
setValueAtTime
setTargetAtTime

エンベロープジェネレータをオーディオデータの再生に適用すれば, オーディオのフェードイン・フェードアウトを実装できます. また, ゲインのみでなく, フィルタのパラメータやディレイタイムなどもスケジューリングの対象になりえます. このことは, エフェクターの実装に利用できるでしょう. デモ 24はサウンドの生成のページで解説したグライドを, AudioParamインスタンスのメソッドを利用して実装しています.

デモ 24

AudioParamインスタンスのメソッドを使いこなせるだけで, サウンドクリエイトの幅が広がります. そのために, このページで解説したことが少しでも役に立てばと思います.