Chapter 35

Testing & Debugging

Learning Objectives

  • Learn testing strategies
  • Debug audio issues
  • Test AI models
  • Ensure reliability

Unit Testing

// Test individual functions
function testRMS() {
    const audio = new Float32Array([0.5, 0.5, 0.5, 0.5]);
    const rms = calculateRMS(audio);
    
    const expected = 0.5;
    const tolerance = 0.001;
    
    if (Math.abs(rms - expected) < tolerance) {
        console.log('โœ… RMS test passed');
    } else {
        console.error('โŒ RMS test failed');
        console.error(`Expected: ${expected}, Got: ${rms}`);
    }
}

// Test with edge cases
function testEdgeCases() {
    // Empty audio
    const empty = new Float32Array(0);
    const emptyRMS = calculateRMS(empty);
    console.assert(emptyRMS === 0, 'Empty audio should have 0 RMS');
    
    // Silent audio
    const silent = new Float32Array(1000).fill(0);
    const silentRMS = calculateRMS(silent);
    console.assert(silentRMS === 0, 'Silent audio should have 0 RMS');
    
    // Max amplitude
    const max = new Float32Array(100).fill(1);
    const maxRMS = calculateRMS(max);
    console.assert(maxRMS === 1, 'Max amplitude should have RMS = 1');
}

Integration Testing

// Test full pipeline
async function testSeparationPipeline() {
    // Load test audio
    const testAudio = await loadTestAudio('test-track.wav');
    
    // Run separation
    const stems = await separationEngine.separate(testAudio);
    
    // Verify results
    console.assert(stems.vocals instanceof AudioBuffer, 'Vocals should be AudioBuffer');
    console.assert(stems.drums instanceof AudioBuffer, 'Drums should be AudioBuffer');
    console.assert(stems.bass instanceof AudioBuffer, 'Bass should be AudioBuffer');
    console.assert(stems.other instanceof AudioBuffer, 'Other should be AudioBuffer');
    
    // Check durations match
    console.assert(
        stems.vocals.duration === testAudio.duration,
        'Stem duration should match original'
    );
    
    // Check energy (vocals should be quieter than mix)
    const mixEnergy = calculateRMS(testAudio.getChannelData(0));
    const vocalEnergy = calculateRMS(stems.vocals.getChannelData(0));
    console.assert(vocalEnergy < mixEnergy, 'Vocals should have less energy than mix');
    
    console.log('โœ… Integration test passed');
}

// Test analysis pipeline
async function testAnalysis() {
    const testAudio = await loadTestAudio('test-track.wav');
    const analysis = await analysisEngine.analyze(testAudio);
    
    console.assert(analysis.bpm > 0, 'BPM should be positive');
    console.assert(analysis.bpm < 300, 'BPM should be reasonable');
    console.assert(analysis.beats.length > 0, 'Should detect beats');
    console.assert(analysis.key, 'Should detect key');
    
    console.log('โœ… Analysis test passed');
}

Debugging Audio

// Visual debugging
function visualizeAudio(audioBuffer) {
    const canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 200;
    const ctx = canvas.getContext('2d');
    
    const data = audioBuffer.getChannelData(0);
    const step = Math.ceil(data.length / canvas.width);
    
    ctx.beginPath();
    ctx.moveTo(0, canvas.height / 2);
    
    for (let i = 0; i < canvas.width; i++) {
        const sample = data[i * step];
        const y = (1 - sample) * canvas.height / 2;
        ctx.lineTo(i, y);
    }
    
    ctx.stroke();
    document.body.appendChild(canvas);
}

// Debug spectrograms
function visualizeSpectrogram(spectrogram) {
    const canvas = document.createElement('canvas');
    canvas.width = spectrogram.width;
    canvas.height = spectrogram.height;
    const ctx = canvas.getContext('2d');
    
    const imageData = ctx.createImageData(canvas.width, canvas.height);
    
    for (let y = 0; y < canvas.height; y++) {
        for (let x = 0; x < canvas.width; x++) {
            const value = spectrogram.data[y * canvas.width + x];
            const normalized = Math.min(255, value * 255);
            
            const index = (y * canvas.width + x) * 4;
            imageData.data[index] = normalized; // R
            imageData.data[index + 1] = normalized; // G
            imageData.data[index + 2] = normalized; // B
            imageData.data[index + 3] = 255; // A
        }
    }
    
    ctx.putImageData(imageData, 0, 0);
    document.body.appendChild(canvas);
}

// Log audio stats
function logAudioStats(audioBuffer, label = '') {
    const data = audioBuffer.getChannelData(0);
    const rms = calculateRMS(data);
    const peak = Math.max(...Array.from(data).map(Math.abs));
    
    console.log(`${label} Audio Stats:`);
    console.log(`  Duration: ${audioBuffer.duration.toFixed(2)}s`);
    console.log(`  Sample Rate: ${audioBuffer.sampleRate} Hz`);
    console.log(`  Channels: ${audioBuffer.numberOfChannels}`);
    console.log(`  RMS: ${rms.toFixed(4)} (${toDB(rms).toFixed(1)} dB)`);
    console.log(`  Peak: ${peak.toFixed(4)} (${toDB(peak).toFixed(1)} dB)`);
    console.log(`  Samples: ${data.length.toLocaleString()}`);
}

Performance Testing

// Measure execution time
async function benchmark(fn, label) {
    const start = performance.now();
    await fn();
    const end = performance.now();
    const duration = end - start;
    
    console.log(`${label}: ${duration.toFixed(2)}ms`);
    return duration;
}

// Benchmark separation
async function benchmarkSeparation() {
    const testAudio = await loadTestAudio('test-track.wav');
    
    const duration = await benchmark(async () => {
        await separationEngine.separate(testAudio);
    }, 'Separation');
    
    // Should be < 30 seconds for 5-minute track
    console.assert(duration < 30000, 'Separation too slow');
}

// Memory profiling
function logMemoryUsage() {
    if (performance.memory) {
        const used = performance.memory.usedJSHeapSize / 1024 / 1024;
        const total = performance.memory.totalJSHeapSize / 1024 / 1024;
        const limit = performance.memory.jsHeapSizeLimit / 1024 / 1024;
        
        console.log(`Memory: ${used.toFixed(1)}MB / ${total.toFixed(1)}MB / ${limit.toFixed(1)}MB`);
    }
}

// Stress test
async function stressTest() {
    for (let i = 0; i < 10; i++) {
        const audio = generateTestAudio(60); // 1 minute
        await separationEngine.separate(audio);
        logMemoryUsage();
    }
}

๐ŸŽต MixMind Connection

How MixMind Ensures Quality

// MixMind's testing strategy

// 1. Automated tests
class TestSuite {
    async runAll() {
        console.log('๐Ÿงช Running test suite...');
        
        await this.testAudioLoading();
        await this.testAnalysis();
        await this.testSeparation();
        await this.testTransitions();
        await this.testExport();
        
        console.log('โœ… All tests passed!');
    }
    
    async testSeparation() {
        // Test with different file types
        const formats = ['wav', 'mp3', 'ogg'];
        
        for (const format of formats) {
            const audio = await loadTestAudio(`test.${format}`);
            const stems = await separationEngine.separate(audio);
            
            console.assert(stems.vocals, `${format} separation failed`);
        }
        
        // Test with different durations
        const durations = [10, 60, 300]; // seconds
        
        for (const duration of durations) {
            const audio = generateTestAudio(duration);
            const stems = await separationEngine.separate(audio);
            
            console.assert(
                stems.vocals.duration === duration,
                `Duration mismatch: ${duration}s`
            );
        }
    }
}

// 2. Error boundaries
class ErrorBoundary {
    constructor(component) {
        this.component = component;
    }
    
    async execute(fn) {
        try {
            return await fn();
        } catch (error) {
            this.logError(error);
            this.showFallback();
            throw error;
        }
    }
    
    logError(error) {
        console.error('Error in', this.component, ':', error);
        
        // Send to error tracking (optional)
        if (window.errorTracker) {
            window.errorTracker.report(error);
        }
    }
    
    showFallback() {
        // Show user-friendly error
        showError('Something went wrong. Please try again.');
    }
}

// 3. Validation
class Validator {
    static validateAudioFile(file) {
        const errors = [];
        
        if (!file.type.startsWith('audio/')) {
            errors.push('Not an audio file');
        }
        
        if (file.size > 50 * 1024 * 1024) {
            errors.push('File too large (max 50 MB)');
        }
        
        if (file.size < 1024) {
            errors.push('File too small');
        }
        
        return errors;
    }
    
    static validateAudioBuffer(buffer) {
        const errors = [];
        
        if (!buffer || buffer.length === 0) {
            errors.push('Empty audio buffer');
        }
        
        if (buffer.duration > 600) {
            errors.push('Audio too long (max 10 minutes)');
        }
        
        if (buffer.sampleRate < 8000 || buffer.sampleRate > 192000) {
            errors.push('Invalid sample rate');
        }
        
        return errors;
    }
}

// 4. User feedback
class DebugPanel {
    constructor() {
        this.logs = [];
        this.panel = this.createPanel();
    }
    
    log(message, type = 'info') {
        const entry = {
            time: new Date().toISOString(),
            message: message,
            type: type
        };
        
        this.logs.push(entry);
        this.updatePanel();
        
        console.log(`[${type}] ${message}`);
    }
    
    createPanel() {
        const panel = document.createElement('div');
        panel.className = 'debug-panel';
        panel.style.display = 'none';
        document.body.appendChild(panel);
        return panel;
    }
    
    toggle() {
        this.panel.style.display = this.panel.style.display === 'none' ? 'block' : 'none';
    }
}

// Enable debug mode
const debug = new DebugPanel();
window.debug = debug; // Accessible from console

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