Claude-skill-registry-data mixmi-mixer-architecture
Complete technical reference for the professional mixer system architecture, audio routing, recording implementation, and all internal mechanics
git clone https://github.com/majiayu000/claude-skill-registry-data
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/mixmi-mixer-architecture" ~/.claude/skills/majiayu000-claude-skill-registry-data-mixmi-mixer-architecture && rm -rf "$T"
data/mixmi-mixer-architecture/SKILL.mdmixmi Alpha - Mixer Architecture Deep Dive
Complete technical reference for the professional mixer system architecture, audio routing, recording implementation, and all internal mechanics
Overview
The mixmi Alpha professional mixer (
/mixer page) is a dual-deck DJ interface built with:
- Tone.js for professional audio processing and effects
- Web Audio API for low-level audio routing
- MediaRecorder API for live mix recording
- React Context (MixerContext) for global state
- Canvas API for waveform visualization
- requestAnimationFrame for smooth playhead updates
File:
components/mixer/SimplifiedMixer.tsx (68KB, 1800+ lines)
Architecture Principles
Design Philosophy
- Professional Audio Quality: No quality loss, proper gain staging, clean signal flow
- Real-time Performance: 60fps waveform updates, instant FX response
- Memory Safety: Proper cleanup, no leaks, stable for extended sessions
- Modular Design: Decks, FX, controls are independent, reusable components
Key Constraints
- Loop-only content: Mixer accepts 8-bar loops only (no songs/EPs)
- BPM range: 60-200 BPM supported
- Sync locked: When sync enabled, both decks match master BPM
- Fixed loop lengths: 2, 4, 8, 16 bars only
State Management
SimplifiedMixerState Structure
interface SimplifiedMixerState { deckA: DeckState; deckB: DeckState; masterBPM: number; // Global tempo (default 120) crossfaderPosition: number; // 0-100 (0=A only, 50=center, 100=B only) syncActive: boolean; // Master sync on/off } interface DeckState { track: Track | null; // Currently loaded track playing: boolean; // Playback state audioState?: any; // Tone.js player state audioControls?: any; // Playback controls loading?: boolean; // Track loading indicator loopEnabled: boolean; // Loop on/off (default true) loopLength: number; // 2, 4, 8, 16 bars loopPosition: number; // Which loop section (0, 1, 2...) boostLevel: number; // 0=off, 1=gentle (cyan), 2=aggressive (orange) }
State Persistence:
- MixerContext provides global state accessible across pages
- localStorage backing for track collection
- Deck states reset on page refresh (intentional - fresh session each time)
Audio Signal Flow
Complete Routing Diagram
┌─────────────────────────────────────────────────────────────────┐ │ DECK A │ │ │ │ Audio File (track.audioUrl) │ │ ↓ │ │ Tone.Player (playback, loop control) │ │ ↓ │ │ Filter (lowpass 20kHz, adjustable cutoff) │ │ ↓ │ │ Reverb (decay 2.0s, wet/dry mix) │ │ ↓ │ │ Delay (8th note feedback, adjustable) │ │ ↓ │ │ Deck Gain (volume control, boost levels) │ │ ↓ │ └─────────┼───────────────────────────────────────────────────────┘ │ ├─────────────→ Crossfader (mixing) │ ↓ ┌─────────┼───────────────────────────────────────────────────────┐ │ ↓ │ │ Deck Gain (volume control, boost levels) │ │ ↑ │ │ Delay (8th note feedback, adjustable) │ │ ↑ │ │ Reverb (decay 2.0s, wet/dry mix) │ │ ↑ │ │ Filter (lowpass 20kHz, adjustable cutoff) │ │ ↑ │ │ Tone.Player (playback, loop control) │ │ ↑ │ │ Audio File (track.audioUrl) │ │ │ │ DECK B │ └─────────────────────────────────────────────────────────────────┘ ↓ Master Gain ↓ AudioContext Destination (speakers) ↓ MediaStreamAudioDestinationNode (recording) ↓ MediaRecorder ↓ Recorded Blob (MP3)
Signal Flow Details
Per-Deck Chain:
- Tone.Player: Loads audio, handles playback rate (for BPM sync), loop points
- Filter: Tone.Filter (lowpass), default 20kHz (wide open), FX control adjusts cutoff
- Reverb: Tone.Reverb, 2.0s decay, wet/dry mix controlled by FX knob
- Delay: Tone.FeedbackDelay, 8th note timing (synced to BPM), feedback adjustable
- Deck Gain: Tone.Gain, controls volume + boost (gentle: 1.2x, aggressive: 1.5x)
Crossfader Mixing:
const crossfaderGainA = (100 - crossfaderPosition) / 100; const crossfaderGainB = crossfaderPosition / 100; // Position 0: A=1.0, B=0.0 (A only) // Position 50: A=0.5, B=0.5 (center) // Position 100: A=0.0, B=1.0 (B only)
Master Output:
- Master gain node (overall volume)
- Splits to:
- AudioContext.destination (speakers)
- MediaStreamAudioDestinationNode (recording capture point)
Audio Implementation Details
Tone.js Integration
Initialization:
import * as Tone from 'tone'; // Start audio context on user interaction (browser requirement) const startAudio = async () => { await Tone.start(); console.log('Audio context started'); }; // Create audio chain for deck const createDeckAudioChain = (audioUrl: string) => { const player = new Tone.Player(audioUrl).toDestination(); const filter = new Tone.Filter(20000, 'lowpass'); const reverb = new Tone.Reverb(2.0); const delay = new Tone.FeedbackDelay('8n', 0.5); const gain = new Tone.Gain(1.0); player .connect(filter) .connect(reverb) .connect(delay) .connect(gain) .connect(crossfaderGain); return { player, filter, reverb, delay, gain }; };
Playback Control:
// Play/pause if (playing) { player.start(); } else { player.stop(); } // Loop configuration player.loop = true; player.loopStart = loopPosition * loopDuration; player.loopEnd = (loopPosition + 1) * loopDuration; // BPM sync (adjust playback rate) const ratio = masterBPM / track.bpm; player.playbackRate = ratio;
Loop Implementation
Loop Timing Calculation:
// Calculate loop duration based on BPM and bar count const beatsPerLoop = loopLength * 4; // 4 beats per bar const secondsPerBeat = 60 / bpm; const loopDuration = beatsPerBeat * secondsPerBeat; // Example: 8-bar loop at 120 BPM // beatsPerLoop = 8 * 4 = 32 beats // secondsPerBeat = 60 / 120 = 0.5 seconds // loopDuration = 0.5 * 32 = 16 seconds
Loop Position Control:
// Loop position = which 8-bar section to play const setLoopPosition = (position: number) => { const startTime = position * loopDuration; const endTime = (position + 1) * loopDuration; player.loopStart = startTime; player.loopEnd = endTime; // If playing, seek to new position if (player.state === 'started') { player.seek(startTime); } }; // Loop length selector (2, 4, 8, 16 bars) const setLoopLength = (bars: number) => { const newDuration = (60 / bpm) * 4 * bars; player.loopEnd = player.loopStart + newDuration; };
BPM Sync Engine
File:
lib/mixerAudio.ts - SimpleLoopSync class
Core Logic:
class SimpleLoopSync { private masterBPM: number = 120; private deckAPlayer: Tone.Player | null = null; private deckBPlayer: Tone.Player | null = null; setMasterBPM(bpm: number) { this.masterBPM = bpm; this.syncAllDecks(); } syncDeck(player: Tone.Player, originalBPM: number) { const ratio = this.masterBPM / originalBPM; player.playbackRate = ratio; } syncAllDecks() { if (this.deckAPlayer && deckATrack) { this.syncDeck(this.deckAPlayer, deckATrack.bpm); } if (this.deckBPlayer && deckBTrack) { this.syncDeck(this.deckBPlayer, deckBTrack.bpm); } } // Master BPM increment/decrement incrementBPM() { this.setMasterBPM(this.masterBPM + 1); } decrementBPM() { this.setMasterBPM(this.masterBPM - 1); } }
Sync Behavior:
- When sync enabled: Both decks match master BPM via playback rate adjustment
- When sync disabled: Each deck plays at its original BPM
- BPM changes affect all synced decks instantly
- Range: 60-200 BPM (enforced by UI controls)
Recording Architecture
Recording Pipeline
Setup:
// Create destination node for recording const mixerDestination = Tone.context.createMediaStreamDestination(); masterGainNode.connect(mixerDestination); // Create MediaRecorder const mediaRecorder = new MediaRecorder(mixerDestination.stream, { mimeType: 'audio/webm;codecs=opus', audioBitsPerSecond: 128000 }); // Capture chunks const audioChunks: BlobPart[] = []; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) { audioChunks.push(e.data); } }; // Start recording mediaRecorder.start();
Stop & Download:
mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); const audioUrl = URL.createObjectURL(audioBlob); // Create download link const a = document.createElement('a'); a.href = audioUrl; a.download = `mixmi_recording_${Date.now()}.webm`; a.click(); // Cleanup URL.revokeObjectURL(audioUrl); }; // Stop recording mediaRecorder.stop();
Recording Metadata
Tracking Source Loops:
interface RecordingMetadata { recordedAt: Date; duration: number; masterBPM: number; sourceTracks: { deckA: { trackId: string; title: string; artist: string; bpm: number; } | null; deckB: { trackId: string; title: string; artist: string; bpm: number; } | null; }; ipSplits?: { // Auto-calculated based on source tracks recipients: IPSplitRecipient[]; totalPercentage: number; }; }
FX System
FX Component Architecture
File:
components/mixer/FX.tsx
Component Structure:
interface FXProps { isActive: boolean; label: string; ref?: React.RefObject<FXElement>; } const FX = forwardRef<FXElement, FXProps>((props, ref) => { const audioInputRef = useRef<GainNode | null>(null); const audioOutputRef = useRef<GainNode | null>(null); const filterRef = useRef<Tone.Filter | null>(null); const reverbRef = useRef<Tone.Reverb | null>(null); const delayRef = useRef<Tone.FeedbackDelay | null>(null); useImperativeHandle(ref, () => ({ audioInput: audioInputRef.current, audioOutput: audioOutputRef.current, resetToDefaults: () => { // Reset all FX parameters if (filterRef.current) { filterRef.current.frequency.value = 20000; } if (reverbRef.current) { reverbRef.current.wet.value = 0; } if (delayRef.current) { delayRef.current.wet.value = 0; } } })); return ( <div className="fx-controls"> {/* Knobs for filter, reverb, delay */} </div> ); });
FX Connection Strategy
Retry Logic:
const connectDeckToFX = async ( player: Tone.Player, fxRef: React.RefObject<FXElement>, retryCount = 0 ): Promise<void> => { const maxRetries = 50; const retryDelay = 100; // ms if (!fxRef.current?.audioInput) { if (retryCount < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay)); return connectDeckToFX(player, fxRef, retryCount + 1); } else { console.warn('FX connection failed after max retries'); return; } } // Connect player to FX input player.connect(fxRef.current.audioInput); // Connect FX output to crossfader if (fxRef.current.audioOutput) { fxRef.current.audioOutput.connect(crossfaderGain); } };
Waveform Display
Canvas Implementation
File:
components/mixer/WaveformDisplay.tsx
Rendering Logic:
const drawWaveform = ( canvas: HTMLCanvasElement, audioBuffer: AudioBuffer, currentTime: number, duration: number ) => { const ctx = canvas.getContext('2d'); if (!ctx) return; const width = canvas.width; const height = canvas.height; // Clear canvas ctx.clearRect(0, 0, width, height); // Draw waveform const data = audioBuffer.getChannelData(0); const step = Math.ceil(data.length / width); const amp = height / 2; ctx.beginPath(); for (let i = 0; i < width; i++) { const min = Math.min(...data.slice(i * step, (i + 1) * step)); const max = Math.max(...data.slice(i * step, (i + 1) * step)); ctx.moveTo(i, amp * (1 + min)); ctx.lineTo(i, amp * (1 + max)); } ctx.strokeStyle = '#81E4F2'; ctx.stroke(); // Draw playhead const playheadX = (currentTime / duration) * width; ctx.beginPath(); ctx.moveTo(playheadX, 0); ctx.lineTo(playheadX, height); ctx.strokeStyle = '#FFE4B5'; ctx.lineWidth = 2; ctx.stroke(); };
Animation Loop:
const animate = () => { if (playing && playerRef.current) { const currentTime = Tone.Transport.seconds % duration; currentTimeRef.current = currentTime; if (canvasRef.current && audioBufferRef.current) { drawWaveform( canvasRef.current, audioBufferRef.current, currentTime, duration ); } } animationFrameRef.current = requestAnimationFrame(animate); };
Memory Management
Critical Memory Fixes (Oct 23, 2025)
Problem 1: Tone.js Objects Not Disposed
// BEFORE: Memory leak const loadTrack = (track: Track) => { const player = new Tone.Player(track.audioUrl); setDeckAPlayer(player); // ❌ Previous player never disposed }; // AFTER: Proper cleanup const loadTrack = (track: Track) => { // Clean up previous track if (deckAPlayer) { deckAPlayer.stop(); deckAPlayer.disconnect(); deckAPlayer.dispose(); } // Load new track const player = new Tone.Player(track.audioUrl); setDeckAPlayer(player); }; // Component unmount cleanup useEffect(() => { return () => { if (deckAPlayer) { deckAPlayer.stop(); deckAPlayer.disconnect(); deckAPlayer.dispose(); } if (deckBPlayer) { deckBPlayer.stop(); deckBPlayer.disconnect(); deckBPlayer.dispose(); } }; }, []);
Problem 2: FX Retry Timeouts Leaked
// BEFORE: Timeouts created but never cleared const connectFX = () => { setTimeout(() => {/* retry */}, 100); // ❌ Leaked }; // AFTER: Track and cleanup timeouts const fxRetryTimeoutsRef = useRef<Set<NodeJS.Timeout>>(new Set()); const connectFX = () => { const timeout = setTimeout(() => {/* retry */}, 100); fxRetryTimeoutsRef.current.add(timeout); return timeout; }; useEffect(() => { return () => { fxRetryTimeoutsRef.current.forEach(t => clearTimeout(t)); fxRetryTimeoutsRef.current.clear(); }; }, []);
Problem 3: Animation Frame Not Canceled
// BEFORE: requestAnimationFrame never canceled const animate = () => { requestAnimationFrame(animate); // ❌ Runs forever }; // AFTER: Track and cancel animation frame const animationFrameRef = useRef<number | null>(null); const animate = () => { animationFrameRef.current = requestAnimationFrame(animate); }; useEffect(() => { animationFrameRef.current = requestAnimationFrame(animate); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, []);
Type Safety Improvements (Oct 23, 2025)
Eliminated All 'any' Types:
// BEFORE: Type safety issues const handleDrop = (item: any) => { // ❌ any type const track = item.track; }; // AFTER: Proper types interface DropItem { track: IPTrack; sourceIndex?: number; } const handleDrop = (item: DropItem) => { // ✅ Type-safe const track = item.track; };
FX Element Type:
// Custom type for FX component ref interface FXElement extends HTMLDivElement { audioInput?: GainNode; audioOutput?: GainNode; resetToDefaults?: () => void; } const deckAFXRef = useRef<FXElement>(null);
Keyboard Shortcuts
Current Shortcuts:
// Playback Space: Play/Pause Deck A Shift+Space: Play/Pause Deck B // BPM ArrowUp: Increment Master BPM (+1) ArrowDown: Decrement Master BPM (-1) // Sync S: Toggle Sync // Recording R: Start/Stop Recording // Loop L: Toggle Loop (Deck A) Shift+L: Toggle Loop (Deck B) // Loop Position [: Previous Loop Position (Deck A) ]: Next Loop Position (Deck A) // Crossfader A: Crossfader to Deck A (position 0) B: Crossfader to Deck B (position 100) C: Crossfader to Center (position 50)
Implementation:
useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { // Ignore if typing in input if (e.target instanceof HTMLInputElement) return; switch (e.key) { case ' ': e.preventDefault(); if (e.shiftKey) { toggleDeckB(); } else { toggleDeckA(); } break; case 'ArrowUp': e.preventDefault(); incrementBPM(); break; case 'ArrowDown': e.preventDefault(); decrementBPM(); break; case 's': case 'S': toggleSync(); break; case 'r': case 'R': isRecording ? stopRecording() : startRecording(); break; // ... other shortcuts } }; document.addEventListener('keydown', handleKeyPress); return () => document.removeEventListener('keydown', handleKeyPress); }, [/* dependencies */]);
Known Issues & Limitations
Current Issues
- Recording Format: WebM/Opus not universally supported (need MP3 encoding)
- No Upload: Recordings download only, not saved to Supabase
- FX Automation: No automation lanes, manual FX control only
- No EQ: Basic filter only, no 3-band EQ
- No Beat Matching: Manual BPM sync only, no auto-beat detection
- Single Crossfader Curve: Linear only, no curve options
Future Enhancements
High Priority:
- MP3 Recording: Convert WebM to MP3 using lamejs or similar
- Upload to Supabase: Save recordings as new tracks
- Remix Metadata: Track source loops, auto-calculate IP splits
- Waveform Preview: Visual preview before download
Medium Priority:
- 3-Band EQ: Low/Mid/High per deck
- Additional FX: Flanger, phaser, distortion
- FX Automation: Record FX parameter changes
- Cue Points: Mark points in loops for quick jumps
- Beat Grid: Visual beat alignment
Low Priority:
- MIDI Support: Control mixer with MIDI controllers
- Multiple Crossfader Curves: Fast cut, slow cut options
- Advanced Loop Modes: Reverse, half-speed, double-speed
- Spectrum Analyzer: Frequency visualization
Troubleshooting Guide
Common Issues
Problem: No audio playback
// Solution: Ensure AudioContext started await Tone.start(); // Check browser autoplay policy // User interaction required before audio starts
Problem: Sync not working
// Check: Is sync engine receiving BPM updates? handleBPMChange(newBPM) { setMasterBPM(newBPM); syncEngine.setMasterBPM(newBPM); // ← Must call this! }
Problem: Recording produces no file
// Check: Is master gain connected to destination? masterGain.connect(mixerDestinationRef.current); // Check: MediaRecorder state console.log(mediaRecorderRef.current?.state); // Should be "recording"
Problem: FX not working
// Check: Are FX refs connected? console.log(deckAFXRef.current?.audioInput); // Should be GainNode // Check: FX retry logic completing? // Look for console warnings: "FX connection failed for deck A"
Problem: Waveform not updating
// Check: Is animation frame running? console.log(animationFrameRef.current); // Should be number (frame ID) // Check: Are refs being updated? console.log(deckACurrentTimeRef.current); // Should match playback time
Related Skills
- mixmi-component-library - UI components (SimplifiedDeck, WaveformDisplay, etc.)
- mixmi-payment-flow - Smart contract integration for remix payments
- mixmi-user-flows - Mixer usage flows and user journeys
- mixmi-design-patterns - Visual design patterns for new features