주파수 스펙트럼

Frequency spectrum

소리를 저음부터 고음까지 주파수 대역별 세기로 쪼개 본 모습. 이퀄라이저 막대그래프가 바로 이것입니다.

다른 이름: FFTEqualizer visualizationSpectrum analyzer
···
html
<div class="wrap">
  <div class="bars" id="bars"></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:86%;display:flex;align-items:flex-end}
.bars{display:flex;align-items:flex-end;gap:3%;width:100%;height:100%;padding-bottom:4%}
.bars i{flex:1;background:linear-gradient(180deg,var(--accent),var(--accent-3));border-radius:3px 3px 0 0;height:6%}
.play{position:absolute;right:0;top:0;width:clamp(26px,14%,38px);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)}
js
const N = 16;
const barsEl = document.getElementById('bars');
const bars = [];
for (let i = 0; i < N; i++) { const b = document.createElement('i'); barsEl.appendChild(b); bars.push(b); }
let playing = false, analyser = null, data = null;
function idleFrame(t) {
  if (playing && analyser) {
    analyser.getByteFrequencyData(data);
    const step = Math.floor(data.length / N) || 1;
    bars.forEach((b, i) => { b.style.height = Math.max(6, (data[i * step] / 255) * 96) + '%'; });
  } else {
    bars.forEach((b, i) => {
      const v = 10 + Math.abs(Math.sin(t * 0.0016 + i * 0.5)) * 55 * Math.exp(-i * 0.045);
      b.style.height = v + '%';
    });
  }
  requestAnimationFrame(idleFrame);
}
requestAnimationFrame(idleFrame);
document.getElementById('play').addEventListener('click', () => {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  analyser = ctx.createAnalyser();
  analyser.fftSize = 128;
  data = new Uint8Array(analyser.frequencyBinCount);
  const master = ctx.createGain();
  const now = ctx.currentTime;
  master.gain.setValueAtTime(0.0001, now);
  master.gain.linearRampToValueAtTime(0.02, now + 0.03);
  master.gain.exponentialRampToValueAtTime(0.0001, now + 1);
  master.connect(analyser).connect(ctx.destination);
  [110, 220, 330, 440, 660, 880, 1320].forEach((f) => {
    const osc = ctx.createOscillator();
    osc.type = 'sawtooth';
    osc.frequency.value = f;
    osc.connect(master);
    osc.start(now);
    osc.stop(now + 1);
  });
  playing = true;
  setTimeout(() => { playing = false; ctx.close(); }, 1050);
});

모든 소리는 여러 주파수 성분이 섞여 있고, 고속 푸리에 변환(FFT)은 그 혼합을 "어떤 주파수가 얼마나 센가"로 분해합니다. Web Audio API의 AnalyserNode.getByteFrequencyData()가 이 작업을 실시간으로 해 줘서, 음악 플레이어의 막대 이퀄라이저나 음성 인식 대기 애니메이션을 만들 수 있습니다.

시각화용 막대 개수(FFT bin 수)는 보통 실제 분석 해상도(1024~2048)보다 훨씬 적게(16~64개) 묶어서 보여줍니다 — 전부 그리면 노이즈처럼 보이고 CPU도 낭비됩니다. 소리 자체의 음량은 낮게 유지하되, 시각화는 낮은 음량에서도 잘 보이도록 최소 높이를 줘서 "무음처럼 안 보이는" 상태를 피합니다. 스펙트럼을 실시간으로 노출하는 UI(라이브 방송, 통화)는 그 자체로 소리가 켜져 있다는 시각적 확인 수단이 되므로 무음 사용자에게도 유용합니다.

음악 플레이어 이퀄라이저, 라이브 스트리밍 오버레이, 음성 어시스턴트의 "듣고 있음" 표시, 오디오 편집 도구의 스펙트로그램에 쓰입니다.

언제 쓰나

오디오가 실제로 재생 중임을 시각적으로 확인시켜야 할 때. 막대 개수는 16~64개로 줄여서 읽기 쉽게 하세요.