Sound feedback timing

사운드 피드백 타이밍

The longer the gap between an action and its sound, the harder it gets for the brain to feel the two are connected.

Also known as: Perceived latencyCause-and-effect timing
···
html
<div class="wrap">
  <button class="tile good" id="good" type="button">
    <span class="tag">즉시 · 0ms</span>
    <span class="dot" id="gdot"></span>
  </button>
  <button class="tile bad" id="bad" type="button">
    <span class="tag">지연 · 500ms</span>
    <span class="dot" id="bdot"></span>
  </button>
</div>
css
.wrap{position:relative;width:94%;height:82%;display:flex;gap:6%}
.tile{position:relative;flex:1;border:1px solid var(--line);border-radius:12px;background:var(--surface);display:grid;place-items:center;cursor:pointer;padding:0}
.tag{position:absolute;top:8px;left:10px;font:700 10px/1 monospace;color:var(--muted)}
.dot{width:22%;aspect-ratio:1;border-radius:50%;background:var(--line)}
.dot.go{background:var(--accent-3)}
.tile.bad .dot.go{background:var(--accent-2)}
js
const good = document.getElementById('good');
const bad = document.getElementById('bad');
const gdot = document.getElementById('gdot');
const bdot = document.getElementById('bdot');
function beep(ctx, t, freq) {
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sine';
  osc.frequency.value = freq;
  gain.gain.setValueAtTime(0, t);
  gain.gain.linearRampToValueAtTime(0.13, t + 0.01);
  gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.13);
  osc.connect(gain).connect(ctx.destination);
  osc.start(t);
  osc.stop(t + 0.14);
}
function trigger(el, dot, delayMs, playSound) {
  el.style.transform = 'scale(.97)';
  setTimeout(() => { el.style.transform = ''; }, 90);
  setTimeout(() => {
    dot.classList.add('go');
    setTimeout(() => dot.classList.remove('go'), 160);
    if (playSound) {
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      beep(ctx, ctx.currentTime, delayMs === 0 ? 660 : 330);
      setTimeout(() => ctx.close(), 300);
    }
  }, delayMs);
}
let cycle = 0;
setInterval(() => {
  cycle++;
  if (cycle % 2) trigger(good, gdot, 0, false); else trigger(bad, bdot, 500, false);
}, 2000);
good.addEventListener('click', () => trigger(good, gdot, 0, true));
bad.addEventListener('click', () => trigger(bad, bdot, 500, true));

If cause and effect land within roughly 100ms, the brain naturally attributes the sound to its own action (sensorimotor synchrony). Past that window, a delayed sound is still heard — but it no longer feels bound to the tap. It just reads as "the screen feels slow."

For interaction sounds, latency is felt performance, so they should play immediately on local recognition of the gesture — not after a network round-trip finishes. Actions that need server confirmation (payment, submit) can still give a short "acknowledged" sound optimistically, then correct with a distinct sound and visual alert if it later fails. On low-end devices where frames can drop, check that the visual change itself isn't lagging before worrying about the sound.

Applies to button-tap feedback, keystroke sounds, and hit-confirmation sounds in games — anywhere "right now" is the whole point.

When to use

Play interaction sounds immediately, locally — don't wait on a server round-trip. Past 100ms, cause and effect stop feeling bound together.