Chapter 23

Energy & Loudness

Learning Objectives

  • Understand energy and loudness
  • Calculate RMS and LUFS
  • Create energy envelopes
  • See how MixMind measures energy

Energy vs Loudness

Energy is the physical power of the audio signal. Loudness is how humans perceive that energy.

  • Energy: Objective measurement (math)
  • Loudness: Subjective perception (psychoacoustics)

Loudness depends on:

  • Frequency (we hear 2-5 kHz best)
  • Duration (short sounds seem quieter)
  • Masking (loud sounds hide quiet ones)

RMS (Root Mean Square)

RMS is the standard way to measure audio energy:

// Calculate RMS energy
function calculateRMS(audioData) {
    let sum = 0;
    for (let i = 0; i < audioData.length; i++) {
        sum += audioData[i] * audioData[i]; // Square each sample
    }
    return Math.sqrt(sum / audioData.length); // Mean of squares, then sqrt
}

// RMS over time (energy envelope)
function calculateEnergyEnvelope(audioData, windowSize, hopSize) {
    const envelope = [];
    
    for (let i = 0; i < audioData.length - windowSize; i += hopSize) {
        const window = audioData.slice(i, i + windowSize);
        const rms = calculateRMS(window);
        envelope.push(rms);
    }
    
    return envelope;
}

// Convert to dB
function toDB(rms) {
    return 20 * Math.log10(Math.max(rms, 1e-10));
}

LUFS (Loudness Units Full Scale)

LUFS is the modern standard for measuring perceived loudness. It accounts for human hearing.

// Simplified LUFS calculation
function calculateLUFS(audioBuffer) {
    // 1. Apply K-weighting filter (accounts for human hearing)
    const filtered = applyKWeightingFilter(audioBuffer);
    
    // 2. Calculate mean square
    const channels = [];
    for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
        const data = filtered.getChannelData(ch);
        let sum = 0;
        for (let i = 0; i < data.length; i++) {
            sum += data[i] * data[i];
        }
        channels.push(sum / data.length);
    }
    
    // 3. Sum channels with weighting
    const power = channels[0] + channels[1]; // Stereo
    
    // 4. Convert to LUFS
    const lufs = -0.691 + 10 * Math.log10(power);
    
    return lufs;
}

// Typical LUFS values:
// - Streaming: -14 LUFS (Spotify, Apple Music)
// - CD: -9 to -12 LUFS
// - Broadcasting: -23 to -24 LUFS

Energy Mapping

Create an energy map showing how energy changes throughout a track:

// Create energy map
function createEnergyMap(audioBuffer, resolution = 1) {
    const windowSize = audioBuffer.sampleRate * resolution;
    const hopSize = windowSize / 2; // 50% overlap
    
    const energyMap = [];
    
    for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
        const data = audioBuffer.getChannelData(ch);
        
        for (let i = 0; i < data.length - windowSize; i += hopSize) {
            const window = data.slice(i, i + windowSize);
            const rms = calculateRMS(window);
            
            if (!energyMap[Math.floor(i / hopSize)]) {
                energyMap[Math.floor(i / hopSize)] = {
                    time: i / audioBuffer.sampleRate,
                    energy: 0
                };
            }
            
            energyMap[Math.floor(i / hopSize)].energy += rms;
        }
    }
    
    // Average channels
    energyMap.forEach(e => e.energy /= audioBuffer.numberOfChannels);
    
    return energyMap;
}

// Find high/low energy sections
function findEnergySections(energyMap) {
    const avgEnergy = energyMap.reduce((sum, e) => sum + e.energy, 0) / energyMap.length;
    
    const highEnergy = energyMap.filter(e => e.energy > avgEnergy * 1.2);
    const lowEnergy = energyMap.filter(e => e.energy < avgEnergy * 0.8);
    
    return { highEnergy, lowEnergy };
}

🎵 MixMind Connection

How MixMind Uses Energy Analysis

// MixMind's energy analysis system
class EnergyAnalyzer {
    async analyze(audioBuffer) {
        // Multi-resolution energy map
        const energyMap = {
            coarse: this.createEnergyMap(audioBuffer, 4), // 4-second resolution
            fine: this.createEnergyMap(audioBuffer, 0.5) // 0.5-second resolution
        };
        
        // Calculate loudness metrics
        const loudness = {
            integrated: this.calculateIntegratedLUFS(audioBuffer),
            shortTerm: this.calculateShortTermLUFS(audioBuffer),
            momentary: this.calculateMomentaryLUFS(audioBuffer)
        };
        
        // Find dynamic sections
        const dynamics = this.analyzeDynamics(energyMap);
        
        return {
            energyMap: energyMap,
            loudness: loudness,
            dynamics: dynamics,
            suggestions: this.generateSuggestions(dynamics)
        };
    }
    
    // Used for energy matching in transitions
    matchEnergy(track1, track2, transitionPoint) {
        const energy1 = this.getEnergyAtTime(track1, transitionPoint);
        const energy2 = this.getEnergyAtTime(track2, 0);
        
        const gainAdjustment = energy1 / energy2;
        
        return {
            adjustment: gainAdjustment,
            db: 20 * Math.log10(gainAdjustment)
        };
    }
}

// Used for:
// - Energy-matched transitions
// - Auto-gain staging
// - Dynamic range analysis
// - Drop detection

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