Chapter 15

Audio Processing

Learning Objectives

  • Understand audio processing concepts
  • Learn to manipulate audio buffers
  • Apply effects and transformations
  • See how MixMind processes audio

Audio Buffer Manipulation

Audio buffers are just arrays of numbers. We can manipulate them directly:

// Get audio data
const channelData = audioBuffer.getChannelData(0); // Left channel

// Reverse the audio
const reversed = new Float32Array(channelData.length);
for (let i = 0; i < channelData.length; i++) {
    reversed[i] = channelData[channelData.length - 1 - i];
}

// Normalize (make loudest part = 1.0)
let maxAmplitude = 0;
for (let i = 0; i < channelData.length; i++) {
    maxAmplitude = Math.max(maxAmplitude, Math.abs(channelData[i]));
}

const normalized = new Float32Array(channelData.length);
for (let i = 0; i < channelData.length; i++) {
    normalized[i] = channelData[i] / maxAmplitude;
}

Volume and Gain

// Apply gain (volume adjustment)
function applyGain(audioBuffer, gainValue) {
    const output = audioContext.createBuffer(
        audioBuffer.numberOfChannels,
        audioBuffer.length,
        audioBuffer.sampleRate
    );
    
    for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
        const input = audioBuffer.getChannelData(channel);
        const outputChannel = output.getChannelData(channel);
        
        for (let i = 0; i < input.length; i++) {
            outputChannel[i] = input[i] * gainValue;
        }
    }
    
    return output;
}

// Fade in/out
function applyFade(audioBuffer, fadeDuration) {
    const fadeSamples = fadeDuration * audioBuffer.sampleRate;
    const output = audioBuffer;
    
    // Fade in
    for (let channel = 0; channel < output.numberOfChannels; channel++) {
        const data = output.getChannelData(channel);
        for (let i = 0; i < fadeSamples; i++) {
            data[i] *= i / fadeSamples;
        }
    }
    
    return output;
}

Mixing Multiple Tracks

// Mix two audio buffers together
function mixBuffers(buffer1, buffer2) {
    const length = Math.max(buffer1.length, buffer2.length);
    const output = audioContext.createBuffer(
        2, // stereo
        length,
        buffer1.sampleRate
    );
    
    for (let channel = 0; channel < 2; channel++) {
        const data1 = buffer1.getChannelData(channel);
        const data2 = buffer2.getChannelData(channel);
        const outputChannel = output.getChannelData(channel);
        
        for (let i = 0; i < length; i++) {
            const sample1 = i < data1.length ? data1[i] : 0;
            const sample2 = i < data2.length ? data2[i] : 0;
            
            // Mix with clipping protection
            outputChannel[i] = Math.max(-1, Math.min(1, 
                (sample1 + sample2) * 0.5
            ));
        }
    }
    
    return output;
}

Crossfade

// Create a crossfade between two tracks
function crossfade(buffer1, buffer2, fadeDuration) {
    const fadeSamples = Math.floor(fadeDuration * buffer1.sampleRate);
    const totalLength = buffer1.length + buffer2.length - fadeSamples;
    
    const output = audioContext.createBuffer(
        2,
        totalLength,
        buffer1.sampleRate
    );
    
    for (let channel = 0; channel < 2; channel++) {
        const data1 = buffer1.getChannelData(channel);
        const data2 = buffer2.getChannelData(channel);
        const outputChannel = output.getChannelData(channel);
        
        let outIndex = 0;
        
        // Copy buffer1 until fade start
        const fadeStart = buffer1.length - fadeSamples;
        for (let i = 0; i < fadeStart; i++) {
            outputChannel[outIndex++] = data1[i];
        }
        
        // Crossfade region
        for (let i = 0; i < fadeSamples; i++) {
            const fadeOut = 1 - (i / fadeSamples);
            const fadeIn = i / fadeSamples;
            
            const sample1 = data1[fadeStart + i] * fadeOut;
            const sample2 = data2[i] * fadeIn;
            
            outputChannel[outIndex++] = sample1 + sample2;
        }
        
        // Copy rest of buffer2
        for (let i = fadeSamples; i < buffer2.length; i++) {
            outputChannel[outIndex++] = data2[i];
        }
    }
    
    return output;
}

🎵 MixMind Connection

How MixMind Processes Audio

// MixMind's transition engine
class TransitionEngine {
    async createTransition(track1, track2, settings) {
        const { crossfadeDuration, type } = settings;
        
        switch(type) {
            case 'crossfade':
                return this.crossfade(track1.buffer, track2.buffer, crossfadeDuration);
            case 'cut':
                return this.hardCut(track1.buffer, track2.buffer);
            case 'eq_blend':
                return this.eqBlend(track1.buffer, track2.buffer, crossfadeDuration);
        }
    }
    
    // EQ blend - fade bass out, fade bass in
    async eqBlend(buffer1, buffer2, duration) {
        // Apply low-pass filter fade to track 1
        const filtered1 = await this.applyFilterFade(buffer1, 'lowpass', duration, true);
        
        // Apply high-pass filter fade to track 2
        const filtered2 = await this.applyFilterFade(buffer2, 'highpass', duration, false);
        
        return this.mixBuffers(filtered1, filtered2);
    }
}

// Rendering the final mix
async function renderMix(tracks, transitions) {
    let output = tracks[0].buffer;
    
    for (let i = 1; i < tracks.length; i++) {
        const transition = transitions[i - 1];
        output = await transitionEngine.createTransition(
            { buffer: output },
            tracks[i],
            transition.settings
        );
    }
    
    return output;
}

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