Chapter 34
User Experience
Learning Objectives
- Learn UX principles
- Design good feedback
- Handle errors gracefully
- Create intuitive interfaces
Progress Feedback
// Show progress for long operations
function updateProgress(percent, message) {
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
progressBar.style.width = percent + '%';
progressText.textContent = message || `${Math.round(percent)}%`;
}
// Detailed progress messages
const progressMessages = {
0: 'Starting...',
10: 'Loading audio...',
20: 'Analyzing audio...',
30: 'Loading AI model...',
40: 'Separating stems...',
70: 'Processing chunk 1/3...',
80: 'Processing chunk 2/3...',
90: 'Processing chunk 3/3...',
95: 'Finalizing...',
100: 'Complete!'
};
function updateProgress(percent) {
const message = progressMessages[percent] || 'Processing...';
updateProgressUI(percent, message);
}
Error Handling
// Graceful error handling
async function safeSeparation(file) {
try {
// Check file type
if (!file.type.startsWith('audio/')) {
throw new Error('Please upload an audio file (MP3, WAV, etc.)');
}
// Check file size
if (file.size > 50 * 1024 * 1024) { // 50 MB
throw new Error('File is too large. Please upload files under 50 MB.');
}
// Load audio
const audioBuffer = await audioEngine.loadFile(file);
// Check duration
if (audioBuffer.duration > 600) { // 10 minutes
throw new Error('Audio is too long. Please upload tracks under 10 minutes.');
}
// Separate
const stems = await separationEngine.separate(audioBuffer);
return { success: true, stems };
} catch (error) {
// User-friendly error messages
let message = 'An error occurred';
if (error.message.includes('decode')) {
message = 'Could not read audio file. Please try a different format.';
} else if (error.message.includes('ONNX')) {
message = 'AI model failed to load. Please check your internet connection.';
} else if (error.message.includes('memory')) {
message = 'Not enough memory. Please try a shorter track.';
}
showError(message);
return { success: false, error: message };
}
}
// Show error to user
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
Responsive Design
/* CSS for responsive layout */
.container {
display: grid;
grid-template-columns: 250px 1fr;
gap: 20px;
padding: 20px;
}
/* Mobile: stack vertically */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.sidebar {
position: fixed;
top: 0;
left: -100%;
width: 80%;
height: 100vh;
transition: left 0.3s;
}
.sidebar.open {
left: 0;
}
}
/* Touch-friendly buttons */
button {
min-height: 44px; /* iOS minimum touch target */
min-width: 44px;
font-size: 16px; /* Prevents zoom on iOS */
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Accessibility
Separates track into vocals, drums, bass, and other
function announce(message) {
document.getElementById('announcer').textContent = message;
}
// Progress updates
announce('Separation 50% complete');
// Color contrast
button {
background: #00d4ff;
color: #000;
/* Contrast ratio > 4.5:1 for WCAG AA */
}
/* Focus indicators */
button:focus {
outline: 2px solid #00d4ff;
outline-offset: 2px;
}
🎵 MixMind Connection
How MixMind Designs for Users
// MixMind's UX approach
// 1. Immediate feedback
class UploadHandler {
handleDrop(file) {
// Show loading immediately
this.showLoading();
// Validate
if (!this.validate(file)) {
this.showError('Invalid file');
return;
}
// Process
this.processFile(file)
.then(() => this.showSuccess())
.catch(err => this.showError(err.message));
}
}
// 2. Progress everywhere
class ProgressTracker {
constructor() {
this.progressBars = new Map();
}
createProgress(id, label) {
const bar = document.createElement('div');
bar.className = 'progress-bar';
bar.innerHTML = `
${label}
0%
`;
this.progressBars.set(id, bar);
return bar;
}
update(id, percent, message) {
const bar = this.progressBars.get(id);
bar.querySelector('.fill').style.width = percent + '%';
bar.querySelector('.percent').textContent = Math.round(percent) + '%';
if (message) {
bar.querySelector('.label').textContent = message;
}
}
}
// 3. Undo/Redo
class UndoManager {
constructor() {
this.history = [];
this.currentIndex = -1;
}
push(action) {
// Remove any redo history
this.history = this.history.slice(0, this.currentIndex + 1);
// Add action
this.history.push(action);
this.currentIndex++;
}
undo() {
if (this.currentIndex >= 0) {
const action = this.history[this.currentIndex];
action.undo();
this.currentIndex--;
}
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const action = this.history[this.currentIndex];
action.execute();
}
}
}
// 4. Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'z':
e.preventDefault();
undoManager.undo();
break;
case 'y':
e.preventDefault();
undoManager.redo();
break;
case 's':
e.preventDefault();
exportMix();
break;
}
}
});
Exercise 34.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.