2026年6月3日 · 技術解説

ADSR エンベロープを WebAudio で実装する

adsr-envelope の記事アイキャッチ画像

シンセサイザーの音作りで欠かせない概念が ADSR エンベロープです。Attack(立ち上がり)/ Decay(減衰)/ Sustain(維持)/ Release(余韻)の4段階で音量変化を制御することで、同じ波形でも「ピアノ的」にも「パッド的」にも、「打撃音」にも「ロングノート」にもなります。この記事では、Web Audio API でADSRを実装する具体的なコードと、各パラメータの効果を解説します。

ADSR とは何か

音の音量は時間によって変化します。これを4つの局面に分けて捉えるのがADSRです。

パラメータ 意味 典型的な値
A: Attack 0から最大音量まで上がる時間 0.005s(打撃系)〜 2s(パッド系)
D: Decay 最大音量からSustainレベルまで減衰する時間 0.1s 〜 0.5s
S: Sustain キーを押し続けている間の音量レベル(0〜1) 0.5 〜 0.8
R: Release キーを離した後、音量0まで下がる時間 0.1s(短い)〜 3s(ロング)

「キーが押された瞬間」「キーが離された瞬間」をトリガーに、4段階の遷移が起こります。

Web Audio API での実装方針

Web Audio で時間変化を扱う中心的なメソッドは以下の3つです:

  • setValueAtTime(value, time): 指定時刻に瞬時に値をセット
  • linearRampToValueAtTime(value, time): 直線的に変化
  • exponentialRampToValueAtTime(value, time): 指数的に変化(人間の聴覚に自然)

ADSRは GainNode の gain プロパティを使って実装するのが定番です。

基本実装: Attack + Decay + Sustain

まずはキーを押した瞬間のADS部分を実装します。

function triggerAttack(audioCtx, gain, opts) {
  const { attack = 0.01, decay = 0.2, sustain = 0.7 } = opts;
  const now = audioCtx.currentTime;

  // 0から始める
  gain.gain.cancelScheduledValues(now);
  gain.gain.setValueAtTime(0, now);

  // A: 0 → 1.0
  gain.gain.linearRampToValueAtTime(1.0, now + attack);

  // D: 1.0 → Sustain
  gain.gain.linearRampToValueAtTime(sustain, now + attack + decay);
}

ポイント: cancelScheduledValues で過去のスケジュール値をクリアしてから新しい遷移を始めます。これをしないと、前の Release が干渉してプチノイズが入ることがあります。

Release: キーを離した時

function triggerRelease(audioCtx, gain, opts) {
  const { release = 0.3 } = opts;
  const now = audioCtx.currentTime;

  // 現在の値を保持してから release を開始
  gain.gain.cancelScheduledValues(now);
  gain.gain.setValueAtTime(gain.gain.value, now);
  gain.gain.linearRampToValueAtTime(0, now + release);
}

「現在の値を setValueAtTime で固定 → そこから 0 へランプ」というパターンが安定動作の秘訣です。

使い方の流れ

const audioCtx = new AudioContext();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 440;
gain.gain.value = 0;
osc.connect(gain).connect(audioCtx.destination);
osc.start();

const envOpts = { attack: 0.02, decay: 0.2, sustain: 0.6, release: 0.5 };

document.getElementById('on').onmousedown  = () => triggerAttack(audioCtx, gain, envOpts);
document.getElementById('off').onmouseup   = () => triggerRelease(audioCtx, gain, envOpts);

これで「ボタンを押すとアタック、離すとリリース」というシンセキーボードの基本動作が再現できます。

音色別のおすすめ設定

1. ピアノ系(打撃 → 余韻)

{ attack: 0.005, decay: 0.3, sustain: 0.0, release: 0.8 }

Sustain を 0 にすると、キーを押し続けても音は減衰し続けます(ピアノは弦が振動を続けるためで、シンセでこれを再現するなら decay を長め、sustain を 0 に)。

2. オルガン系(瞬時アタック・無減衰)

{ attack: 0.005, decay: 0.0, sustain: 1.0, release: 0.05 }

押されている間はずっと一定音量、離した瞬間にすぐ消える。

3. パッド系(ゆっくり立ち上がり)

{ attack: 1.5, decay: 0.3, sustain: 0.8, release: 2.0 }

Attack が長いと「フワーッ」と立ち上がるシンセパッドサウンドに。

4. プラック系(短い余韻)

{ attack: 0.005, decay: 0.15, sustain: 0.0, release: 0.1 }

ピックで弦を弾いたような、短い減衰の音色。

フィルタ・エンベロープも同じ仕組みで作れる

ADSR は音量だけでなく、フィルタの cutoff にも適用できます。これで「最初は明るく、徐々に暗くなる」音色変化が作れます。

const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;

// フィルタにエンベロープ
const now = audioCtx.currentTime;
filter.frequency.cancelScheduledValues(now);
filter.frequency.setValueAtTime(2000, now);                   // Attack開始時に2000Hzまで上昇
filter.frequency.linearRampToValueAtTime(400, now + 0.3);    // Decayで400Hzまで降下
// (sustainレベルは400Hzのまま)

これがアシッドベース(TB-303 風)の「キュキュッ」「ニュッ」という鳴き声の正体です。

注意したい落とし穴

1. 指数ランプは値0を扱えない

exponentialRampToValueAtTime(0, ...) は内部で対数を取るため、ターゲット値が0だとエラーになります。代わりに linearRampToValueAtTime(0.0001, ...) のように極小値を使うか、最後だけ linearRampTo にしましょう。

2. cancelScheduledValues は必須

連打したときに前のRelease ramp が残っていると、新しい Attack が「途中から始まる」現象が起きます。必ず最初にキャンセルしましょう。

3. setValueAtTime(現在値, now) のテクニック

キー連打時に「現時点の値を固定してから新しい遷移を始める」ためには、setValueAtTime(gain.gain.value, now) が有効です。これでスムーズな再アタックが可能になります。

まとめ

ADSR は「同じ波形でも別の楽器に聞こえる」マジックの源泉です。Web Audio API は GainNode のオートメーション機能が強力なので、音楽的に意味のあるADSRが少ないコードで実装できます。波形(Oscillator)・音量(Gain + ADSR)・音色(フィルタ)の3要素を組み合わせれば、シンセサイザーの主要機能はカバーできます。