ADSR エンベロープを WebAudio で実装する
シンセサイザーの音作りで欠かせない概念が 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要素を組み合わせれば、シンセサイザーの主要機能はカバーできます。