ADSR 엔벨로프

ADSR envelope

소리의 크기가 시간에 따라 어떻게 변하는지를 어택·디케이·서스테인·릴리즈 4단계로 나눈 모델입니다.

다른 이름: 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();
});

신시사이저의 가장 기본적인 회로 중 하나로, 건반을 누르고 뗄 때 소리 크기가 그리는 곡선을 어택(0에서 최대로 오르는 시간), 디케이(최대에서 지속 레벨로 내려가는 시간), 서스테인(키를 누르고 있는 동안 유지하는 레벨), 릴리즈(키를 뗀 뒤 0으로 사라지는 시간) 4단계로 쪼갠 모델입니다. Web Audio API의 GainNode.gain을 linearRampToValueAtTime / exponentialRampToValueAtTime으로 자동화하면 그대로 구현됩니다.

어택이 짧을수록(0에 가까울수록) 타건감이 "딱딱"하고 날카롭게 들리고, 길어질수록 "스윽" 부드럽게 시작합니다. UI 사운드는 대개 어택 5ms 이하·릴리즈 100ms 이하의 짧은 엔벨로프를 써서 잔향이 다음 조작과 겹치지 않게 합니다. 알림음처럼 반복되는 소리는 릴리즈를 갑자기 끊지 말고(클릭 잡음 방지) 최소 몇 ms 라도 부드럽게 감쇠시키는 게 좋습니다.

신시사이저·DAW의 모든 음색 설계, UI 사운드의 감쇠 곡선, 게임 이펙트음의 타격감 조절에 쓰입니다.

언제 쓰나

소리를 직접 합성하거나 게임 이펙트의 "타격감"을 조절할 때. 빠른 어택 = 날카로움, 느린 어택 = 부드러움을 기억하세요.