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.