Chapter 20

BPM Detection

Learning Objectives

  • Understand BPM calculation
  • Learn tempo estimation methods
  • Handle tempo variations
  • See how MixMind detects BPM

What is BPM?

BPM stands for "Beats Per Minute"β€”it's the tempo of a song.

  • 60 BPM: One beat per second (slow)
  • 120 BPM: Two beats per second (common in pop/dance)
  • 140 BPM: Fast (dubstep, drum & bass)
  • 170+ BPM: Very fast (hardcore, speed metal)
πŸ’‘ Common Tempos:
  • House: 120-130 BPM
  • Techno: 125-140 BPM
  • Hip-hop: 85-115 BPM
  • DnB: 170-180 BPM
  • Dubstep: 140 BPM (half-time feel)

Autocorrelation Method

Autocorrelation measures how similar a signal is to a delayed version of itself. If the delay matches the beat interval, correlation is high.

// Calculate autocorrelation
function autocorrelation(data, lag) {
    let sum = 0;
    for (let i = 0; i < data.length - lag; i++) {
        sum += data[i] * data[i + lag];
    }
    return sum / data.length;
}

// Find tempo using autocorrelation
function detectTempoAutocorrelation(onsets, sampleRate) {
    // Test lag range: 60-200 BPM
    const minLag = Math.floor(sampleRate * 60 / 200);
    const maxLag = Math.floor(sampleRate * 60 / 60);
    
    let bestLag = 0;
    let bestCorrelation = -Infinity;
    
    for (let lag = minLag; lag <= maxLag; lag++) {
        const correlation = autocorrelation(onsets, lag);
        
        if (correlation > bestCorrelation) {
            bestCorrelation = correlation;
            bestLag = lag;
        }
    }
    
    // Convert lag to BPM
    const bpm = (sampleRate * 60) / bestLag;
    return bpm;
}

Spectral Flux Method

Spectral flux measures how quickly the frequency spectrum changes. Beats often cause sudden spectral changes.

// Calculate spectral flux
function spectralFlux(spectrogram) {
    const flux = [];
    
    for (let i = 1; i < spectrogram.length; i++) {
        let sum = 0;
        
        for (let bin = 0; bin < spectrogram[i].length; bin++) {
            const diff = spectrogram[i][bin] - spectrogram[i - 1][bin];
            // Only count increases (onsets)
            if (diff > 0) {
                sum += diff * diff;
            }
        }
        
        flux.push(Math.sqrt(sum));
    }
    
    return flux;
}

// Find periodicity in spectral flux
function findTempo(flux, sampleRate, hopSize) {
    const autocorr = autocorrelation(flux, ...);
    // Similar to beat detection
}

Handling Tempo Variations

Songs don't always have a constant tempo. Common variations:

  • Tempo changes: Song speeds up or slows down
  • Tempo drift: Gradual tempo change
  • Human performance: Slight timing variations
// Detect tempo changes
function detectTempoChanges(onsets, windowSize) {
    const tempos = [];
    
    // Analyze tempo in sliding windows
    for (let i = 0; i < onsets.length - windowSize; i++) {
        const window = onsets.slice(i, i + windowSize);
        const tempo = estimateTempo(window);
        tempos.push({
            time: onsets[i].time,
            bpm: tempo
        });
    }
    
    // Find significant changes
    const changes = [];
    for (let i = 1; i < tempos.length; i++) {
        const diff = Math.abs(tempos[i].bpm - tempos[i - 1].bpm);
        if (diff > 5) { // More than 5 BPM change
            changes.push(tempos[i]);
        }
    }
    
    return changes;
}

🎡 MixMind Connection

How MixMind Detects BPM

// MixMind's BPM detection
class BPMDetector {
    async analyze(audioBuffer) {
        // Multiple detection methods for robustness
        const results = await Promise.all([
            this.autocorrelationMethod(audioBuffer),
            this.spectralFluxMethod(audioBuffer),
            this.beatHistogramMethod(audioBuffer)
        ]);
        
        // Consensus voting
        const bpm = this.selectConsensus(results);
        
        // Refine with local analysis
        const refined = this.refineTempo(audioBuffer, bpm);
        
        return {
            bpm: refined.bpm,
            confidence: refined.confidence,
            tempoChanges: refined.changes
        };
    }
    
    // Beat histogram - count intervals between onsets
    async beatHistogramMethod(audioBuffer) {
        const onsets = await this.detectOnsets(audioBuffer);
        
        // Create histogram of inter-onset intervals
        const histogram = new Array(200).fill(0); // 60-260 BPM
        
        for (let i = 1; i < onsets.length; i++) {
            const interval = onsets[i].time - onsets[i - 1].time;
            const bpm = 60 / interval;
            
            if (bpm >= 60 && bpm <= 260) {
                const bin = Math.floor(bpm) - 60;
                histogram[bin]++;
            }
        }
        
        // Find peak
        let maxBin = 0;
        let maxCount = 0;
        for (let i = 0; i < histogram.length; i++) {
            if (histogram[i] > maxCount) {
                maxCount = histogram[i];
                maxBin = i;
            }
        }
        
        return maxBin + 60;
    }
}

// Critical for:
// - Syncing tracks together
// - Calculating transition timing
// - Beat grid alignment
// - Quantization

Exercise 20.1: Practice

Goal: Apply what you've learned in this chapter

Try the concepts from this chapter in your browser's console or a simple HTML file. Experiment with the code examples and modify them to see what happens.

πŸ“ Quick Reference:

Review the key concepts from this chapter and make sure you understand each one before moving to the next chapter.