Introduction

Tap your foot to a song and your brain locks onto the beat effortlessly. Getting a computer to do the same — called beat detection or beat tracking — is harder than it sounds, yet it powers everything from DJ software to rhythm games to automatic playlist BPM tags.

The central insight is surprisingly simple: music beats tend to be loud events. A kick drum, a snare hit, a chord strum — each pumps energy into the sound signal at a moment we perceive as a beat. So beat detection starts by measuring how much the signal's energy changes over time, then looks for a periodic pattern in those energy spikes.

The pipeline has two main stages:

  1. Onset detection — find the moments where energy rises sharply (called onsets).
  2. Tempo estimation — measure the time between onsets and find the most consistent period, giving the tempo in beats per minute (BPM).

Both stages rely on ideas that reach deep into signal processing and the mathematics of periodicity. Related ideas appear in pattern matching and Bayesian inference, where the goal is also to extract a hidden structure from noisy observations.

Try It

The demo below simulates the beat-detection pipeline on a synthetic drum loop. The top row shows the energy envelope — how loud each frame is. Spikes mark onsets. The bottom row shows the autocorrelation of those spikes: a peak at lag τ\tau means "spikes repeat every τ\tau frames." The tallest autocorrelation peak gives the estimated tempo.

<div class="hint-box">A synthetic drum loop plays at the chosen BPM. The <b>energy envelope</b> shows how loud each frame is — spikes are beats. The <b>autocorrelation</b> reveals the repeating period: the tallest bar gives the estimated tempo. The metronome flashes at that rate.</div>
<div class="controls">
  <label>BPM: <span id="bpm-val">120</span> BPM
    <input type="range" id="bpm-slider" min="60" max="180" value="120" step="1">
  </label>
  <label>Noise level:
    <input type="range" id="noise-slider" min="0" max="40" value="10" step="1">
  </label>
  <button id="btn-restart" type="button">Restart</button>
</div>
<div class="panel">
  <div class="panel-title">Energy envelope (onset spikes)</div>
  <canvas id="cv-energy" width="560" height="100"></canvas>
</div>
<div class="panel">
  <div class="panel-title">Autocorrelation (periodicity)</div>
  <canvas id="cv-acf" width="560" height="100"></canvas>
</div>
<div class="metronome-row">
  <div id="metro-light" class="metro-light"></div>
  <span id="metro-label">Estimated tempo: —</span>
</div>
<div id="status" class="status"></div>
/* base layout and typography styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint-box { font-size: .85rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .5rem; }
.controls label { font-size: .85rem; display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 90px; accent-color: #1d3557; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
/* chart panel and canvas styles */
.panel { margin-bottom: .4rem; }
.panel-title { font-size: .78rem; font-weight: 600; color: #1d3557; margin-bottom: 2px; }
canvas { width: 100%; max-width: 560px; height: 100px; display: block;
         border: 1px solid #cdd9e3; border-radius: 6px; background: #f7fafc; }
/* metronome light and label styles */
.metronome-row { display: flex; align-items: center; gap: .5rem; margin: .4rem 0; }
.metro-light { width: 22px; height: 22px; border-radius: 50%;
               background: #cdd9e3; border: 2px solid #adb1b8; transition: background .06s; }
.metro-light.on { background: #e63946; border-color: #c92f3c; }
#metro-label { font-size: .88rem; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Beat detection: estimate tempo from periodic energy spikes in the audio signal.

const FRAMES = 140;    // total frames to simulate
const SR = 100;        // simulated sample rate (frames per second)

// Generate a synthetic energy envelope with kick + snare beats at the given BPM.
function generateEnergy(bpm, noiseLvl) {
  // frames between beats
  const beatInterval = Math.round(SR * 60 / bpm);
  const energy = new Float32Array(FRAMES);
  for (let i = 0; i < FRAMES; i++) {
    // base noise floor
    energy[i] = Math.random() * noiseLvl;
  }
  // place kick drum peaks
  for (let b = 0; b * beatInterval < FRAMES; b++) {
    const pos = Math.round(b * beatInterval);
    // Gaussian kick shape centered at beat position
    for (let d = -2; d <= 4; d++) {
      const idx = pos + d;
      if (idx >= 0 && idx < FRAMES) {
        energy[idx] += 100 * Math.exp(-0.5 * d * d);
      }
    }
    // snare on the off-beat (every half-beat)
    const snarePos = Math.round((b + 0.5) * beatInterval);
    for (let d = -1; d <= 3; d++) {
      const idx = snarePos + d;
      if (idx >= 0 && idx < FRAMES) {
        energy[idx] += 55 * Math.exp(-0.6 * d * d);
      }
    }
  }
  return energy;
}

// Compute autocorrelation of the energy signal to find periodicity.
function autocorrelation(energy) {
  const maxLag = Math.floor(FRAMES / 2);
  const acf = new Float32Array(maxLag);
  // sum of products for each lag
  for (let lag = 1; lag < maxLag; lag++) {
    let sum = 0;
    for (let i = 0; i < FRAMES - lag; i++) {
      sum += energy[i] * energy[i + lag];
    }
    acf[lag] = sum / (FRAMES - lag);
  }
  return acf;
}

// Draw a bar chart on the canvas; highlight the peak bar.
function drawBar(canvas, data, peakIdx, color, peakColor) {
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  ctx.clearRect(0, 0, W, H);
  let maxV = 0;
  for (let i = 0; i < data.length; i++) if (data[i] > maxV) maxV = data[i];
  if (maxV === 0) maxV = 1;
  const bw = W / data.length;
  for (let i = 0; i < data.length; i++) {
    const h = (data[i] / maxV) * (H - 4);
    ctx.fillStyle = (peakIdx !== null && i === peakIdx) ? peakColor : color;
    ctx.fillRect(i * bw, H - h - 2, Math.max(bw - 1, 1), h);
  }
}

// Find the lag with highest autocorrelation = estimated beat period.
function detectTempo(acf) {
  // skip very short lags (too fast to be a musical beat)
  let bestLag = 1, bestVal = -1;
  const minLag = 20; // skip lags below 20 frames (~300 BPM) to avoid sub-beat peaks
  for (let lag = minLag; lag < acf.length; lag++) {
    if (acf[lag] > bestVal) { bestVal = acf[lag]; bestLag = lag; }
  }
  return bestLag;
}

// Flash the metronome light at the detected BPM.
let metroTimer = null;
let lightTimer = null;

function startMetronome(bpmEstimate) {
  clearInterval(metroTimer);
  clearTimeout(lightTimer);
  const light = document.getElementById('metro-light');
  const intervalMs = (60 / bpmEstimate) * 1000;
  function flash() {
    light.classList.add('on');
    lightTimer = setTimeout(() => light.classList.remove('on'), 80);
  }
  flash();
  metroTimer = setInterval(flash, intervalMs);
}

// Main function: generate signal, compute ACF, detect tempo, draw, run metronome.
function run() {
  const bpm = parseInt(document.getElementById('bpm-slider').value, 10);
  document.getElementById('bpm-val').textContent = bpm;
  const noiseLvl = parseInt(document.getElementById('noise-slider').value, 10);

  const energy = generateEnergy(bpm, noiseLvl);
  const acf = autocorrelation(energy);
  const bestLag = detectTempo(acf);
  const estBpm = Math.round(SR * 60 / bestLag);

  drawBar(document.getElementById('cv-energy'), energy, null, '#6aafe6', '#e63946');
  drawBar(document.getElementById('cv-acf'), acf, bestLag, '#7bcda0', '#e63946');

  const st = document.getElementById('status');
  if (Math.abs(estBpm - bpm) <= 5 || Math.abs(estBpm * 2 - bpm) <= 5 || Math.abs(estBpm - bpm * 2) <= 5) {
    st.className = 'status ok';
    st.textContent = i18nFmt('Detected %bpm% BPM — locked onto the beat!', { bpm: estBpm });
  } else {
    st.className = 'status bad';
    st.textContent = i18nFmt('Detected %est% BPM (actual %actual% BPM) — harmonic alias.', { est: estBpm, actual: bpm });
  }

  document.getElementById('metro-label').textContent = i18nFmt('Estimated tempo: %bpm% BPM', { bpm: estBpm });
  startMetronome(estBpm);
}

document.getElementById('btn-restart').addEventListener('click', run);
document.getElementById('bpm-slider').addEventListener('input', () => {
  document.getElementById('bpm-val').textContent = document.getElementById('bpm-slider').value;
});

run();
Result
Console

Try changing the BPM slider and pressing Restart. Watch how the autocorrelation peak shifts as tempo changes. At very low or very high BPM the detector may lock onto a harmonic (half or double time) — a known failure mode of simple energy-based systems.

The Real Complexity

The energy-spike idea works beautifully for four-on-the-floor dance music. Real music is harder:

  • Syncopation places accents between beats, so energy peaks fall off the grid.
  • Rubato lets a performer stretch or compress time expressively, drifting from a constant tempo.
  • Polyrhythm layers multiple simultaneous periodicities (e.g. 3 against 4), confusing a single-period detector.
  • Onset ambiguity — a long sustained note has energy above a threshold for many frames, not one clean spike.

Researchers have attacked these problems with progressively richer tools:

  1. Spectral flux: instead of total energy, measure how much the frequency content changes frame to frame — SF(n)=∑kmax⁡(∣X(n,k)∣−∣X(n−1,k)∣,0)\text{SF}(n) = \sum_{k} \max(|X(n,k)| - |X(n-1,k)|, 0). This picks up onsets in any frequency band, not just loud passages.
  2. Dynamic-programming tempo induction: rather than autocorrelation, find the set of beat times {ti}\{t_i\} that minimizes a cost penalizing both deviation from a constant period and poor onset alignment.
  3. Learned onset detectors: convolutional networks trained on hand-labeled data now outperform hand-crafted spectral flux on most benchmarks, achieving F1F_1 scores above 0.85 on standard datasets.

Even the best systems still struggle with free-tempo music and complex jazz. Beat tracking remains an open research problem, connected to the broader challenge of pattern matching in noisy, structured sequences.

Where It Matters

Beat detection is not just a curiosity — it is infrastructure for a surprisingly wide range of systems:

  • DJ and music production software: auto-sync lets two tracks snap to the same grid; time-stretching keeps vocals in key while changing tempo.
  • Rhythm games: every note chart in games like Guitar Hero or Beat Saber starts with an onset detector.
  • Music information retrieval (MIR): streaming services use BPM tags to filter playlists by energy (workout vs. study) and to power "sound-alike" recommendations.
  • Automatic transcription: knowing the beat grid is the first step in turning audio into sheet music.
  • Clinical gait analysis: the same periodicity detection used for music can measure walking cadence from wrist accelerometer data, useful in Parkinson's disease monitoring.
  • Film and advertising: editors use beat-sync tools to cut on the beat automatically.

The pipeline — energy envelope, onset detection, periodicity analysis — also generalises beyond music. Cardiac rhythm analysis, industrial vibration monitoring, and network traffic anomaly detection all follow the same mathematical skeleton.

Conclusion

Beat detection shows how an intuitive human skill — tapping in time to music — decomposes into a precise signal-processing pipeline: measure energy changes, find spikes, measure the period between them. The mathematics of autocorrelation does the heavy lifting, turning a raw waveform into a number (BPM) a machine can act on.

Yet the gap between "steady four-four rock" and "jazz trio with rubato" is enormous, and closing it has kept researchers busy for decades. Every improvement — spectral flux, dynamic programming, deep learning — adds a layer that handles one more musical reality. The beat is simple; capturing it perfectly is not. That tension between elegant simplicity and hard real-world complexity is what makes beat detection a fascinating corner of pattern matching and signal understanding.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/beat-detection/Content licensed under CC BY-NC 4.0.