피치와 템포

Pitch and tempo

피치(음높이)와 템포(속도)는 서로 다른 축입니다 — 하나를 조절해도 반드시 다른 하나가 바뀌는 건 아닙니다.

다른 이름: Frequency and BPMSpeed vs tone
···
html
<div class="wrap">
  <div class="panel">
    <div class="cap">pitch</div>
    <div class="staff">
      <div class="line"></div><div class="line"></div><div class="line"></div>
      <div class="note" id="note"></div>
    </div>
  </div>
  <div class="panel">
    <div class="cap">tempo</div>
    <div class="metro"><div class="needle" id="needle"></div></div>
    <div class="bpm" id="bpm">96 BPM</div>
    <button class="play" id="play" type="button" aria-label="play sound"><span class="tri"></span></button>
  </div>
</div>
css
.wrap{position:relative;width:94%;height:88%;display:flex;gap:5%}
.panel{position:relative;flex:1;border:1px solid var(--line);border-radius:12px;background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;overflow:hidden}
.cap{position:absolute;top:8px;left:10px;font:700 10px/1 monospace;color:var(--muted)}
.staff{position:relative;width:70%;height:60%}
.line{position:absolute;left:0;right:0;height:1px;background:var(--line)}
.line:nth-child(1){top:20%}.line:nth-child(2){top:50%}.line:nth-child(3){top:80%}
.note{position:absolute;left:50%;width:12px;height:12px;border-radius:50%;background:var(--accent);transform:translate(-50%,-50%);top:50%;animation:notemove 2.4s ease-in-out infinite}
@keyframes notemove{0%,100%{top:80%}50%{top:12%}}
.metro{position:relative;width:56%;aspect-ratio:1;border-radius:50%;border:1px solid var(--line);display:grid;place-items:center}
.needle{width:2px;height:38%;background:var(--accent-2);transform-origin:bottom center;position:absolute;bottom:50%;animation:swing 1.2s ease-in-out infinite}
@keyframes swing{0%,100%{transform:rotate(-28deg)}50%{transform:rotate(28deg)}}
.bpm{font:600 10px/1 monospace;color:var(--muted)}
.play{position:absolute;bottom:6px;right:6px;width:20px;height:20px;border-radius:50%;border:1px solid var(--line);background:var(--surface);display:grid;place-items:center;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,.12)}
.play .tri{width:0;height:0;border-style:solid;border-width:5px 0 5px 7px;border-color:transparent transparent transparent var(--fg);margin-left:1px}
.play:active{transform:scale(.92)}
@media (min-width:460px){
  .cap{font-size:14px;top:14px;left:18px}
  .staff{width:80%;height:66%}
  .line{border-top:1.5px solid var(--line);height:0;background:none}
  .note{width:20px;height:20px}
  .metro{width:64%}
  .needle{width:3px}
  .bpm{font-size:15px}
  .play{width:34px;height:34px;bottom:14px;right:14px}
  .play .tri{border-width:7px 0 7px 10px;margin-left:2px}
}
js
const bpmEl = document.getElementById('bpm');
document.getElementById('play').addEventListener('click', () => {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  const now = ctx.currentTime;
  [392, 587.33].forEach((f, i) => {
    const t = now + i * 0.16;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.value = f;
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(0.13, t + 0.015);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.17);
    osc.connect(gain).connect(ctx.destination);
    osc.start(t);
    osc.stop(t + 0.18);
  });
  const gaps = [0.42, 0.28, 0.16];
  let tt = now + 0.4;
  gaps.forEach((gap) => {
    tt += gap;
    const t = tt;
    const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.012), ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let j = 0; j < d.length; j++) d[j] = (Math.random() * 2 - 1) * (1 - j / d.length);
    const src = ctx.createBufferSource();
    src.buffer = buf;
    const gain = ctx.createGain();
    gain.gain.value = 0.13;
    src.connect(gain).connect(ctx.destination);
    src.start(t);
    setTimeout(() => { bpmEl.textContent = Math.round(60 / gap) + ' BPM'; }, (t - now) * 1000);
  });
  setTimeout(() => ctx.close(), 1300);
});

아날로그 시대엔 테이프나 레코드 재생 속도를 바꾸면 피치와 템포가 함께 바뀌었습니다(빨리 돌리면 목소리가 높아지고 빨라짐). 디지털 오디오에서는 타임 스트레칭·피치 시프팅 알고리즘이 둘을 분리해, 속도만 2배로 올리거나(피치 고정) 음만 반음 올리는(속도 고정) 것이 가능합니다. Web Audio API의 OscillatorNode.frequency는 피치를, playbackRate는 (분리 처리 없이는) 둘을 함께 바꿉니다.

UI에서 피치는 주로 "긍정/부정"을 구분하는 데 씁니다 — 성공음은 음이 올라가고(상승 인터벌) 오류음은 내려갑니다(하강 인터벌). 템포는 주로 "긴급도"를 전달합니다 — 배터리 부족 경고음이 반복될수록 빨라지는 식입니다. 두 축을 UI 피드백 설계에 쓸 때는 각각 하나의 의미만 맡기고 섞지 않아야 사용자가 패턴을 학습하기 쉽습니다.

음성 통화의 배속 재생(팟캐스트 1.5배속), 게임의 위험 경고음 템포 가속, 알림음의 성공/실패 피치 대비에 쓰입니다.

언제 쓰나

긍정/부정은 피치(오르내림)로, 긴급도는 템포(속도)로 나눠서 전달하세요. 두 의미를 한 소리에 섞지 마세요.