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.