Claude Code Plugins

Community-maintained marketplace

Feedback

mixmi-mixer-architecture

@djchikk/mixmi-alpha-fresh
1
0

Complete technical reference for the professional mixer system architecture, audio routing, recording implementation, and all internal mechanics

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name mixmi-mixer-architecture
description Complete technical reference for the professional mixer system architecture, audio routing, recording implementation, and all internal mechanics
metadata [object Object]

mixmi 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

  1. Professional Audio Quality: No quality loss, proper gain staging, clean signal flow
  2. Real-time Performance: 60fps waveform updates, instant FX response
  3. Memory Safety: Proper cleanup, no leaks, stable for extended sessions
  4. 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:

  1. Tone.Player: Loads audio, handles playback rate (for BPM sync), loop points
  2. Filter: Tone.Filter (lowpass), default 20kHz (wide open), FX control adjusts cutoff
  3. Reverb: Tone.Reverb, 2.0s decay, wet/dry mix controlled by FX knob
  4. Delay: Tone.FeedbackDelay, 8th note timing (synced to BPM), feedback adjustable
  5. 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

  1. Recording Format: WebM/Opus not universally supported (need MP3 encoding)
  2. No Upload: Recordings download only, not saved to Supabase
  3. FX Automation: No automation lanes, manual FX control only
  4. No EQ: Basic filter only, no 3-band EQ
  5. No Beat Matching: Manual BPM sync only, no auto-beat detection
  6. Single Crossfader Curve: Linear only, no curve options

Future Enhancements

High Priority:

  1. MP3 Recording: Convert WebM to MP3 using lamejs or similar
  2. Upload to Supabase: Save recordings as new tracks
  3. Remix Metadata: Track source loops, auto-calculate IP splits
  4. Waveform Preview: Visual preview before download

Medium Priority:

  1. 3-Band EQ: Low/Mid/High per deck
  2. Additional FX: Flanger, phaser, distortion
  3. FX Automation: Record FX parameter changes
  4. Cue Points: Mark points in loops for quick jumps
  5. Beat Grid: Visual beat alignment

Low Priority:

  1. MIDI Support: Control mixer with MIDI controllers
  2. Multiple Crossfader Curves: Fast cut, slow cut options
  3. Advanced Loop Modes: Reverse, half-speed, double-speed
  4. 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