Chapter 17

FFT & Spectrums

Learning Objectives

  • Understand FFT in depth
  • Work with frequency data
  • Create spectrograms
  • See how MixMind analyzes frequencies

FFT Windowing

FFT analyzes audio in small chunks called windows. To reduce artifacts, we apply window functions that fade the edges:

// Hann window (most common)
function hannWindow(size) {
    const window = new Float32Array(size);
    for (let i = 0; i < size; i++) {
        window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (size - 1)));
    }
    return window;
}

// Apply window to audio data
function applyWindow(data, window) {
    const result = new Float32Array(data.length);
    for (let i = 0; i < data.length; i++) {
        result[i] = data[i] * window[i];
    }
    return result;
}

// Common window types:
// - Hann: Good general purpose
// - Hamming: Similar to Hann
// - Blackman: Better sidelobe suppression
// - Kaiser: Adjustable

Magnitude and Phase

FFT output contains both magnitude (amplitude) and phase (timing) information:

// Complex FFT output
function fft(input) {
    // Returns array of complex numbers: {real, imag}
    // Magnitude = sqrt(real² + imag²)
    // Phase = atan2(imag, real)
}

// Convert to magnitude spectrum
function getMagnitudeSpectrum(complexData) {
    const magnitude = new Float32Array(complexData.length);
    for (let i = 0; i < complexData.length; i++) {
        const { real, imag } = complexData[i];
        magnitude[i] = Math.sqrt(real * real + imag * imag);
    }
    return magnitude;
}

// Convert to decibels (dB)
function toDecibels(magnitude) {
    const dB = new Float32Array(magnitude.length);
    for (let i = 0; i < magnitude.length; i++) {
        // Avoid log(0)
        dB[i] = 20 * Math.log10(Math.max(magnitude[i], 1e-10));
    }
    return dB;
}

Spectrograms

A spectrogram shows how the frequency spectrum changes over time:

// Create spectrogram data
async function createSpectrogram(audioBuffer, hopSize = 512) {
    const fftSize = 2048;
    const numFrames = Math.floor(audioBuffer.length / hopSize);
    const numBins = fftSize / 2;
    
    const spectrogram = [];
    
    for (let frame = 0; frame < numFrames; frame++) {
        const start = frame * hopSize;
        const segment = audioBuffer.slice(start, start + fftSize);
        
        // Apply window
        const window = hannWindow(fftSize);
        const windowed = applyWindow(segment, window);
        
        // FFT (simplified)
        const complex = fft(windowed);
        const magnitude = getMagnitudeSpectrum(complex);
        
        spectrogram.push(magnitude);
    }
    
    return spectrogram; // 2D array: [time][frequency]
}

// Visualize spectrogram
function drawSpectrogram(spectrogram, canvas) {
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;
    
    for (let x = 0; x < width; x++) {
        const frameIndex = Math.floor(x / width * spectrogram.length);
        const frame = spectrogram[frameIndex];
        
        for (let y = 0; y < height; y++) {
            const binIndex = Math.floor((1 - y / height) * frame.length);
            const magnitude = frame[binIndex];
            
            // Convert to color (dB scale)
            const dB = 20 * Math.log10(Math.max(magnitude, 1e-10));
            const normalized = (dB + 100) / 100; // Map -100dB to 0dB -> 0 to 1
            
            const hue = 240 - normalized * 240; // Blue to red
            ctx.fillStyle = `hsl(${hue}, 100%, ${normalized * 50}%)`;
            ctx.fillRect(x, y, 1, 1);
        }
    }
}

🎵 MixMind Connection

How MixMind Uses FFT

// MixMind's spectral analysis
class SpectralAnalyzer {
    async analyze(audioBuffer) {
        const spectrogram = await createSpectrogram(audioBuffer);
        
        // Extract features
        const features = {
            spectralCentroid: this.calculateCentroid(spectrogram),
            spectralRolloff: this.calculateRolloff(spectrogram),
            spectralFlatness: this.calculateFlatness(spectrogram),
            zcr: this.calculateZeroCrossingRate(audioBuffer)
        };
        
        return features;
    }
    
    // Spectral centroid - "brightness" of sound
    calculateCentroid(spectrogram) {
        let sum = 0;
        let totalWeight = 0;
        
        for (const frame of spectrogram) {
            for (let i = 0; i < frame.length; i++) {
                sum += i * frame[i];
                totalWeight += frame[i];
            }
        }
        
        return sum / totalWeight;
    }
}

// Used for:
// - Timbre analysis
// - Instrument detection
// - Mood/energy classification
// - Genre detection

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