Three.js From Zero · Article s9-09

S9-09 Music-Sync

Season 9 · Article 09

Music-Sync Interactives

Beat detection. Phase-locked animations. Rhythm games. The player's movement, the scene's changes, perfectly on the downbeat.

1. Know the BPM

If you authored the music: store BPM in metadata. If not: beat-detect with an algorithm.

2. Scheduling events to the beat

const BPM = 120;
const beatDuration = 60 / BPM;  // 0.5s per beat
const startTime = ctx.currentTime;

function beatIndex(now) {
  return Math.floor((now - startTime) / beatDuration);
}

let lastBeat = -1;
function tick() {
  const beat = beatIndex(ctx.currentTime);
  if (beat > lastBeat) {
    onBeat(beat);  // trigger animation
    lastBeat = beat;
  }
  requestAnimationFrame(tick);
}

3. Sample-accurate scheduling

Browsers can jitter setTimeout by 4-16ms. For music, too much.

Schedule audio events ~50-100ms ahead via setValueAtTime(). Audio engine plays them exactly.

function scheduleAhead() {
  // Schedule all beats up to currentTime + 0.1
  while (nextBeatTime < ctx.currentTime + 0.1) {
    playKickAt(nextBeatTime);
    nextBeatTime += beatDuration;
  }
}
setInterval(scheduleAhead, 25);

4. Phase-locked visuals

function tick() {
  const t = ctx.currentTime - startTime;
  const phase = (t % beatDuration) / beatDuration;  // 0..1 per beat

  // Scale pulse on downbeat
  sphere.scale.setScalar(1 + Math.max(0, 1 - phase * 4) * 0.5);
}

Envelope peaks at phase=0 (downbeat), decays to 0.25.

5. Beat detection (no BPM known)

// Simple: energy spike over rolling bass average (S9-03)
// Better: autocorrelation over bass history to find dominant period
// Best: BeatDetectr / Essentia.js libraries

6. Live demo — 4/4 with synced visuals

7. Rhythm game patterns

  • Notes spawned at noteTime - travelTime.
  • Target zone at fixed screen position.
  • Player input timed against ctx.currentTime.
  • Scoring: |inputTime - noteTime| buckets (Perfect / Good / Miss).

8. Song playback + sync

// Start audio
const source = ctx.createBufferSource();
source.buffer = decodedSongBuffer;
source.start(ctx.currentTime);
const songStartTime = ctx.currentTime;

// Game time = ctx.currentTime - songStartTime
// Compare to note chart times

9. Pitch detection

For Rock Band-style vocal: analyze mic input with autocorrelation to find fundamental frequency.

Libraries: Pitchy (UMD, simple), YIN algorithm implementations.

10. Takeaways

  • BPM known → schedule by time. BPM unknown → detect energy spikes.
  • Schedule 50-100ms ahead for sample-accurate audio.
  • Phase (fractional beat position) drives phase-locked visuals.
  • Rhythm games: fixed note chart, scored against ctx.currentTime.
  • Pitch detection via autocorrelation for vocal games.