Chapter 21

Key Detection

Learning Objectives

  • Understand musical keys
  • Learn chroma features
  • Implement key detection algorithms
  • See how MixMind detects keys

What is a Musical Key?

A key defines which notes sound "right" in a piece of music. It's based on a scale and a root note.

  • Major keys: Happy, bright sound
  • Minor keys: Sad, dark sound
  • 12 possible root notes: C, C#, D, D#, E, F, F#, G, G#, A, A#, B
  • 24 total keys: 12 major + 12 minor
πŸ’‘ Example:

C Major uses notes: C, D, E, F, G, A, B (all natural notes). A Minor uses the same notes but starts on A.

Chroma Features

Chroma features represent how much energy is in each of the 12 pitch classes (C, C#, D, etc.), regardless of octave.

// Calculate chroma features
function calculateChroma(spectrogram, sampleRate) {
    // 12 bins: C, C#, D, D#, E, F, F#, G, G#, A, A#, B
    const chroma = new Array(12).fill(0);
    
    // Map frequency bins to pitch classes
    const binFrequencies = getBinFrequencies(spectrogram.length, sampleRate);
    
    for (let frame = 0; frame < spectrogram.length; frame++) {
        for (let bin = 0; bin < spectrogram[frame].length; bin++) {
            const freq = binFrequencies[bin];
            const magnitude = spectrogram[frame][bin];
            
            // Convert frequency to pitch class
            const pitchClass = frequencyToPitchClass(freq);
            
            // Accumulate energy
            chroma[pitchClass] += magnitude;
        }
    }
    
    // Normalize
    const max = Math.max(...chroma);
    return chroma.map(c => c / max);
}

// Convert frequency to pitch class (0-11)
function frequencyToPitchClass(freq) {
    // MIDI note number
    const midi = 69 + 12 * Math.log2(freq / 440);
    // Pitch class (0 = C, 1 = C#, etc.)
    return Math.round(midi) % 12;
}

Key Detection Algorithm

Compare the chroma features to key profilesβ€”templates of what each key sounds like.

// Krumhansl-Kessler key profiles (research-based)
const majorProfile = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88];
const minorProfile = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17];

// Correlate chroma with each key
function detectKey(chroma) {
    const correlations = [];
    
    // Test all 24 keys
    for (let root = 0; root < 12; root++) {
        // Rotate profile to this root
        const majorRotated = rotateArray(majorProfile, root);
        const minorRotated = rotateArray(minorProfile, root);
        
        // Correlate with chroma
        const majorCorr = correlate(chroma, majorRotated);
        const minorCorr = correlate(chroma, minorRotated);
        
        correlations.push({
            key: getNoteName(root),
            mode: 'major',
            correlation: majorCorr
        });
        
        correlations.push({
            key: getNoteName(root),
            mode: 'minor',
            correlation: minorCorr
        });
    }
    
    // Find best match
    correlations.sort((a, b) => b.correlation - a.correlation);
    return correlations[0];
}

// Pearson correlation
function correlate(x, y) {
    const n = x.length;
    const meanX = x.reduce((a, b) => a + b) / n;
    const meanY = y.reduce((a, b) => a + b) / n;
    
    let numerator = 0;
    let denomX = 0;
    let denomY = 0;
    
    for (let i = 0; i < n; i++) {
        const dx = x[i] - meanX;
        const dy = y[i] - meanY;
        numerator += dx * dy;
        denomX += dx * dx;
        denomY += dy * dy;
    }
    
    return numerator / Math.sqrt(denomX * denomY);
}

🎡 MixMind Connection

How MixMind Detects Keys

// MixMind's key detection system
class KeyDetector {
    async analyze(audioBuffer) {
        // Extract chroma features over time
        const chromaSequence = await this.extractChromaSequence(audioBuffer);
        
        // Detect key for each segment
        const segments = this.segmentTrack(chromaSequence);
        const keyPerSegment = segments.map(seg => this.detectKey(seg));
        
        // Find overall key (most common)
        const overallKey = this.findDominantKey(keyPerSegment);
        
        // Detect key changes
        const keyChanges = this.detectKeyChanges(keyPerSegment);
        
        return {
            key: overallKey,
            keyChanges: keyChanges,
            confidence: this.calculateConfidence(keyPerSegment, overallKey)
        };
    }
    
    // Used for compatibility checking
    areKeysCompatible(key1, key2) {
        // Compatible if:
        // 1. Same key
        // 2. Relative major/minor (e.g., C major ↔ A minor)
        // 3. Perfect fifth apart (e.g., C major ↔ G major)
        
        return this.compatibilityMatrix[key1][key2];
    }
}

// Critical for:
// - Harmonic mixing
// - Key compatibility warnings
// - Pitch shifting suggestions

Exercise 21.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.