ADSR envelope

ADSR 엔벨로프

A four-stage model — attack, decay, sustain, release — describing how a sound's loudness changes over time.

Also known as: Attack Decay Sustain ReleaseAmplitude envelope
···
html
<div class="wrap">
  <canvas id="cv"></canvas>
  <div class="cap"><span class="short">A → D → S → R</span><span class="full">Attack → Decay → Sustain → Release</span></div>
  <button class="play" id="play" type="button" aria-label="play sound"><span class="tri"></span></button>
</div>
css
.wrap{position:relative;width:92%;height:84%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px}
canvas{width:100%;height:70%;display:block}
.cap{font:600 11px/1 monospace;color:var(--muted);letter-spacing:.02em}
.cap .full{display:none}
.play{position:absolute;right:0;bottom:0;width:clamp(28px,15%,40px);aspect-ratio:1;border-radius:50%;border:1px solid var(--line);background:var(--surface);display:grid;place-items:center;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.12)}
.play .tri{width:0;height:0;border-style:solid;border-width:6px 0 6px 9px;border-color:transparent transparent transparent var(--fg);margin-left:2px}
.play:active{transform:scale(.92)}
@media (min-width:460px){
  .cap{font-size:16px}
  .cap .short{display:none}
  .cap .full{display:inline}
  .play{width:44px;height:44px}
  .play .tri{border-width:8px 0 8px 12px;margin-left:3px}
}
js
const cv = document.getElementById('cv');
const g2d = cv.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const LINEC = cs.getPropertyValue('--muted').trim() || '#888';
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
let w, h;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = cv.clientWidth; h = cv.clientHeight;
  cv.width = w * dpr; cv.height = h * dpr;
  g2d.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
const A = 0.14, D = 0.30, S = 0.68, SUSTAIN = 0.45;
function envY(x) {
  if (x <= A) return x / A;
  if (x <= D) return 1 - (1 - SUSTAIN) * (x - A) / (D - A);
  if (x <= S) return SUSTAIN;
  return SUSTAIN * (1 - (x - S) / (1 - S));
}
function drawCurve(pad) {
  g2d.clearRect(0, 0, w, h);
  g2d.beginPath();
  g2d.moveTo(pad, h - pad);
  for (let px = 0; px <= w - pad * 2; px++) {
    const x = px / (w - pad * 2);
    g2d.lineTo(pad + px, h - pad - envY(x) * (h - pad * 2));
  }
  g2d.lineTo(w - pad, h - pad);
  g2d.closePath();
  g2d.fillStyle = ACCENT + '22';
  g2d.fill();
  g2d.beginPath();
  g2d.moveTo(pad, h - pad);
  for (let px = 0; px <= w - pad * 2; px++) {
    const x = px / (w - pad * 2);
    g2d.lineTo(pad + px, h - pad - envY(x) * (h - pad * 2));
  }
  g2d.strokeStyle = ACCENT;
  g2d.lineWidth = 2;
  g2d.stroke();
}
const LOOP_MS = 1800;
function frame(t) {
  const pad = 10;
  drawCurve(pad);
  const frac = (t % LOOP_MS) / LOOP_MS;
  const px = pad + frac * (w - pad * 2);
  const y = envY(frac);
  g2d.strokeStyle = LINEC;
  g2d.lineWidth = 1;
  g2d.beginPath();
  g2d.moveTo(px, h - pad);
  g2d.lineTo(px, h - pad - y * (h - pad * 2));
  g2d.stroke();
  g2d.beginPath();
  g2d.arc(px, h - pad - y * (h - pad * 2), 3.5, 0, Math.PI * 2);
  g2d.fillStyle = ACCENT;
  g2d.fill();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
document.getElementById('play').addEventListener('click', () => {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  const now = ctx.currentTime;
  const total = 1.1;
  const aT = total * A, dT = total * D, sT = total * S, rT = total;
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sawtooth';
  osc.frequency.value = 220;
  gain.gain.setValueAtTime(0, now);
  gain.gain.linearRampToValueAtTime(0.14, now + aT);
  gain.gain.linearRampToValueAtTime(0.14 * SUSTAIN, now + dT);
  gain.gain.setValueAtTime(0.14 * SUSTAIN, now + sT);
  gain.gain.linearRampToValueAtTime(0.0001, now + rT);
  osc.connect(gain).connect(ctx.destination);
  osc.start(now);
  osc.stop(now + rT + 0.05);
  osc.onended = () => ctx.close();
});

One of the most fundamental circuits in a synthesizer: it splits the loudness curve traced when you press and release a key into four stages — attack (time to rise from 0 to peak), decay (time to fall from peak to the sustain level), sustain (the level held while the key stays down), and release (time to fade to 0 after release). Automating a GainNode's gain with linearRampToValueAtTime / exponentialRampToValueAtTime implements exactly this.

A shorter attack (closer to 0) sounds sharp and percussive; a longer one eases in softly. UI sounds usually use a short envelope — attack under 5ms, release under 100ms — so the tail doesn't overlap the next interaction. For repeating sounds like notifications, never cut the release abruptly (it causes a click) — even a few milliseconds of fade-out avoids the pop.

Used to shape every synth/DAW timbre, the decay curve of UI sounds, and the impact feel of game effect sounds.

When to use

Reach for it when synthesizing a sound or tuning a game effect's impact. Remember: fast attack = sharp, slow attack = soft.