diff --git a/BootTidal.hs b/BootTidal.hs
index 82266b4..1273293 100644
--- a/BootTidal.hs
+++ b/BootTidal.hs
@@ -6,32 +6,66 @@ import System.IO (hSetEncoding, stdout, utf8)
 
 hSetEncoding stdout utf8
 
--- DPV setup
+-- Metadata & Visualization OSC setup
 :{
-let targetdpv = Target {oName = "didacticpatternvisualizer",
-                       oAddress = "127.0.0.1",
-                       oPort = 1818,
-                       oLatency = 0.2,
-                       oWindow = Nothing,
-                       oSchedule = Live,
-                       oBusPort = Nothing,
-                       oHandshake = False
-                      }
-    formatsdpv = [OSC "/delivery" Named {requiredArgs = []} ]
-:}
+-- Target for visualizer metadata OSC messages
+metadataTarget = Target { oName = "ParVaguesViz", 
+                          oAddress = "127.0.0.1", 
+                          oPort = 57120, 
+                          oBusPort = Nothing,
+                          oLatency = 0.1, 
+                          oWindow = Nothing, 
+                          oSchedule = Live,
+                          oHandshake = False }
+
+-- DPV visualization target
+targetdpv = Target {oName = "didacticpatternvisualizer",
+                   oAddress = "127.0.0.1",
+                   oPort = 1818,
+                   oLatency = 0.2,
+                   oWindow = Nothing,
+                   oSchedule = Live,
+                   oBusPort = Nothing,
+                   oHandshake = False
+                  }
+formatsdpv = [OSC "/delivery" Named {requiredArgs = []} ]
+
 -- SuperDirt target
-:{
-let superdirtTarget' = superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}
-:}
-:{
--- Set up targets for both SuperDirt and DPV
-let oscmapdpv = [(targetdpv, formatsdpv),
-                (superdirtTarget', [superdirtShape])
-                ]
+superdirtTarget' = superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}
+
+-- Set up targets for both SuperDirt, DPV, and metadata
+oscmapdpv = [(targetdpv, formatsdpv),
+            (superdirtTarget', [superdirtShape]),
+            (metadataTarget, [])
+            ]
 :}
--- Initialize TidalCycles with both targets
+
+-- Initialize TidalCycles with all targets
 tidal <- startStream defaultConfig {cFrameTimespan = 1/20} oscmapdpv
 
+:{
+-- Simpler metadata system using parameters that get sent through normal patterns
+let trackName = pS "trackName"    -- Custom parameter for track name
+    trackType = pS "trackType"    -- Custom parameter for track type
+    trackSubtype = pS "trackSubtype"  -- For variants like bass2, fx2, etc.
+    
+    -- Simple shorthands - add these to ANY pattern
+    labelKick n = (# trackName n) . (# trackType "kick")
+    labelSnare n = (# trackName n) . (# trackType "snare")
+    labelHihat n = (# trackName n) . (# trackType "hihat")
+    labelBreak n = (# trackName n) . (# trackType "breaks")
+    
+    -- ParVagues specific shorthands
+    labelBass n = (# trackName n) . (# trackType "bass")
+    labelBass2 n = (# trackName n) . (# trackType "bass") . (# trackSubtype "bass2")
+    labelMelody n = (# trackName n) . (# trackType "melodic")
+    labelMelody2 n = (# trackName n) . (# trackType "melodic") . (# trackSubtype "melody2")
+    labelFX n = (# trackName n) . (# trackType "fx")
+    labelFX2 n = (# trackName n) . (# trackType "fx") . (# trackSubtype "fx2")
+    labelFX3 n = (# trackName n) . (# trackType "fx") . (# trackSubtype "fx3")
+    labelVocal n = (# trackName n) . (# trackType "vocals")
+    labelVocal2 n = (# trackName n) . (# trackType "vocals") . (# trackSubtype "vocal2")
+:}
 
 :{
 let p = streamReplace tidal
@@ -141,5 +175,33 @@ let -- DPV specific parameters
     gF3 = (# djfbus 3 (range 0.05 0.95 "^51"))
 :}
 
+-- FIXME: STALE EXAMPLES
+-- :{
+-- -- Example metadata setup for common tracks
+-- -- Run these at the start of your session
+-- setupMetadata = do
+--   labelKick 0 "Kick"            -- d1: Kick
+--   labelSnare 1 "Snare"          -- d2: Snare
+--   labelHihat 2 "Drums"          -- d3: Drums
+--   labelBass 3 "Bass"            -- d4: Bass
+--   labelMelody 4 "Synth"         -- d5: Synth
+--   labelFX 5 "FX"                -- d6: FX
+--   labelVocal 8 "Vocals"         -- d9: Vocals
+--   labelBreak 7 "Breakbeats"     -- d8: Breaks
+-- 
+-- -- For BLUE GOLD tracks
+-- setupBlueGold = do
+--   labelKick 0 "Kick"
+--   labelSnare 1 "Snare" 
+--   labelHihat 2 "Hihats"
+--   labelBass 3 "Bass"
+--   labelMelody 4 "Suns Keys"
+--   labelFX2 5 "Guitars 1"
+--   labelFX3 6 "Guitars 2"
+--   labelBreak 7 "Breakbeats"
+--   labelVocal 8 "Father Voice"
+--   labelFX 9 "Cues & Noise"
+-- :}
+
 :set prompt "tidal> "
 -- :set prompt-cont ""
diff --git a/viz/Background.pde b/viz/Background.pde
new file mode 100644
index 0000000..7da89b1
--- /dev/null
+++ b/viz/Background.pde
@@ -0,0 +1,101 @@
+/**
+ * Background class
+ * 
+ * Creates a dynamic cyberpunk background with subtle animations
+ */
+class Background {
+  // Background properties
+  color bgColor;
+  color accentColor1;
+  color accentColor2;
+  
+  // Movement properties
+  float noiseScale = 0.02;
+  float noiseOffset = 0;
+  float noiseSpeed = 0.005;
+  
+  // Timing
+  float cps = 0.5;
+  float lastCycleTime = 0;
+  
+  Background() {
+    // Dark cyberpunk colors
+    bgColor = color(10, 12, 18);
+    accentColor1 = color(0, 70, 100);
+    accentColor2 = color(60, 0, 80);
+  }
+  
+  void update() {
+    // Update noise movement
+    noiseOffset += noiseSpeed;
+    
+    // Check for cycle changes
+    float currentCycleTime = millis() / 1000.0 * cps;
+    if (floor(currentCycleTime) > floor(lastCycleTime)) {
+      // New cycle - could trigger effects here
+    }
+    lastCycleTime = currentCycleTime;
+  }
+  
+  void display() {
+    // Create gradient background
+    noiseDetail(8, 0.5);
+    
+    // Fill with base color
+    noStroke();
+    fill(bgColor, 40); // Semi-transparent for motion blur effect
+    rect(0, 0, width, height);
+    
+    // Add subtle noise pattern
+    float motionFactor = sin(millis() * 0.001) * 0.5 + 0.5;
+    
+    loadPixels();
+    for (int y = 0; y < height; y += 4) {
+      for (int x = 0; x < width; x += 4) {
+        float noiseVal = noise(x * noiseScale, y * noiseScale, noiseOffset);
+        
+        if (noiseVal > 0.7) {
+          color pixelColor;
+          
+          // Create different zones
+          if (noiseVal > 0.85) {
+            // Highlight areas
+            pixelColor = lerpColor(accentColor1, accentColor2, 
+                                   sin(x * 0.01 + millis() * 0.0005) * 0.5 + 0.5);
+            pixelColor = color(red(pixelColor), green(pixelColor), blue(pixelColor), 
+                              20 + 20 * motionFactor);
+          } else {
+            // Subtle accent
+            pixelColor = lerpColor(bgColor, accentColor1, 0.3);
+            pixelColor = color(red(pixelColor), green(pixelColor), blue(pixelColor), 10);
+          }
+          
+          // Draw 4x4 pixel block for better performance
+          fill(pixelColor);
+          rect(x, y, 4, 4);
+        }
+      }
+    }
+    
+    // Draw horizontal scan lines
+    drawScanlines();
+  }
+  
+  void drawScanlines() {
+    stroke(255, 8);
+    strokeWeight(1);
+    for (int y = 0; y < height; y += 4) {
+      line(0, y, width, y);
+    }
+    
+    // Draw brighter scanline that moves
+    float movingScanline = (millis() % 5000) / 5000.0 * height;
+    stroke(255, 15);
+    strokeWeight(2);
+    line(0, movingScanline, width, movingScanline);
+  }
+  
+  void setCPS(float newCps) {
+    this.cps = newCps;
+  }
+}
diff --git a/viz/BreakbeatEvent.pde b/viz/BreakbeatEvent.pde
new file mode 100644
index 0000000..6966f57
--- /dev/null
+++ b/viz/BreakbeatEvent.pde
@@ -0,0 +1,224 @@
+/**
+ * BreakbeatEvent class
+ * 
+ * Specialized visualization for breakbeats, jungle, and deconstructed rhythms
+ * Designed specifically for d8 track in ParVagues performances
+ */
+class BreakbeatEvent extends SoundEvent {
+  // Slice visualization properties
+  int numSlices;
+  ArrayList<Slice> slices;
+  float rotation;
+  float originalSize;
+  boolean isReversePattern;
+  float beatPhase;
+  
+  BreakbeatEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    
+    // Extended lifespan for breakbeats
+    lifespan = 800 + (gain * 500);
+    originalSize = size;
+    
+    // Determine number of slices based on the sound name and gain
+    if (sound.contains("jungle")) {
+      numSlices = 16;
+    } else if (sound.contains("break")) {
+      numSlices = 8;
+    } else {
+      numSlices = int(random(4, 12));
+    }
+    
+    // Check if this might be a reversed sample
+    isReversePattern = sound.contains("rev") || random(100) < 20;
+    
+    // Random rotation for variety
+    rotation = random(TWO_PI);
+    
+    // Initialize slices
+    createSlices();
+    
+    // Beat phase for animation
+    beatPhase = random(TWO_PI);
+  }
+  
+  void createSlices() {
+    slices = new ArrayList<Slice>();
+    
+    // Create slices in a circle
+    for (int i = 0; i < numSlices; i++) {
+      float angle = map(i, 0, numSlices, 0, TWO_PI);
+      float distance = originalSize * 0.7;
+      
+      // Randomized slice properties
+      float sliceWidth = originalSize * 0.3 * random(0.5, 1.5);
+      float sliceHeight = originalSize * 0.2 * random(0.5, 1.5);
+      float fadeOffset = random(0, lifespan * 0.5);
+      
+      // Calculate position
+      float x = cos(angle) * distance;
+      float y = sin(angle) * distance;
+      
+      // Add the slice
+      slices.add(new Slice(x, y, sliceWidth, sliceHeight, angle, fadeOffset));
+    }
+  }
+  
+  @Override
+  void update() {
+    super.update();
+    
+    // Calculate age as a percentage
+    float age = millis() - birthTime;
+    float progress = constrain(age / lifespan, 0, 1);
+    
+    // Update each slice
+    for (Slice slice : slices) {
+      slice.update(progress, age);
+    }
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = constrain(age / lifespan, 0, 1);
+    
+    // Display core only at the beginning
+    if (progress < 0.2) {
+      // Draw core with fading alpha
+      float coreAlpha = map(progress, 0, 0.2, alpha, 0);
+      fill(red(eventColor), green(eventColor), blue(eventColor), coreAlpha);
+      noStroke();
+      
+      pushMatrix();
+      translate(position.x, position.y);
+      rotate(rotation + (progress * PI * (isReversePattern ? -2 : 2)));
+      ellipse(0, 0, size * (1-progress), size * (1-progress));
+      popMatrix();
+    }
+    
+    // Display slices
+    pushMatrix();
+    translate(position.x, position.y);
+    
+    // Apply global rotation
+    float rotationSpeed = isReversePattern ? -1 : 1;
+    rotate(rotation + (progress * rotationSpeed * PI * 0.5));
+    
+    // Draw each slice
+    for (Slice slice : slices) {
+      slice.display(eventColor, alpha);
+    }
+    popMatrix();
+    
+    // Draw rhythmic rings
+    drawRhythmicRings(progress, age);
+  }
+  
+  void drawRhythmicRings(float progress, float age) {
+    // Number of rings depends on the track energy
+    int numRings = int(3 + (gain * 3));
+    
+    for (int i = 0; i < numRings; i++) {
+      // Each ring has its own phase and timing
+      float ringPhase = (beatPhase + (i * TWO_PI / numRings)) % TWO_PI;
+      float ringPulse = sin(ringPhase + (age * 0.01 * (isReversePattern ? -1 : 1))) * 0.5 + 0.5;
+      
+      // Ring size increases with progress and pulses with rhythm
+      float ringSize = originalSize * (0.5 + progress * 1.5) * (0.8 + ringPulse * 0.4);
+      
+      // Ring opacity fades with progress and pulses
+      float ringAlpha = alpha * (1 - progress) * ringPulse * 0.7;
+      
+      if (ringAlpha > 5) {
+        noFill();
+        stroke(red(eventColor), green(eventColor), blue(eventColor), ringAlpha);
+        strokeWeight(1 + ringPulse * 2);
+        
+        // Draw a slightly distorted ring for glitchy effect
+        beginShape();
+        for (int j = 0; j < 24; j++) {
+          float angle = j * TWO_PI / 24;
+          float distortion = 1.0 + (sin(angle * 3 + age * 0.01) * 0.1 * ringPulse);
+          float x = position.x + cos(angle) * ringSize * distortion;
+          float y = position.y + sin(angle) * ringSize * distortion;
+          vertex(x, y);
+        }
+        endShape(CLOSE);
+      }
+    }
+  }
+  
+  /**
+   * Slice inner class
+   * Represents a single slice of the breakbeat visualization
+   */
+  class Slice {
+    PVector position;
+    float width, height;
+    float angle;
+    float fadeOffset;
+    float jitterX, jitterY;
+    float originalX, originalY;
+    float pulsePhase;
+    
+    Slice(float x, float y, float w, float h, float a, float offset) {
+      originalX = x;
+      originalY = y;
+      position = new PVector(x, y);
+      width = w;
+      height = h;
+      angle = a;
+      fadeOffset = offset;
+      
+      // Random phase for pulse animation
+      pulsePhase = random(TWO_PI);
+    }
+    
+    void update(float progress, float age) {
+      // Apply movement based on progress
+      float expansionFactor = 1.0 + (progress * 2.0);
+      
+      // Add rhythmic jitter (more for jungle/break patterns)
+      jitterX = sin(age * 0.03 + pulsePhase) * width * 0.3 * progress;
+      jitterY = cos(age * 0.02 + pulsePhase) * height * 0.3 * progress;
+      
+      // Update position with expansion and jitter
+      position.x = originalX * expansionFactor + jitterX;
+      position.y = originalY * expansionFactor + jitterY;
+    }
+    
+    void display(color sliceColor, float baseAlpha) {
+      // Calculate individual alpha with offset fading
+      float age = millis() - birthTime;
+      float individualProgress = constrain((age - fadeOffset) / (lifespan - fadeOffset), 0, 1);
+      float sliceAlpha = baseAlpha * (1 - individualProgress);
+      
+      if (sliceAlpha <= 0) return;
+      
+      // Apply a pulsing effect
+      float pulse = sin(age * 0.01 + pulsePhase) * 0.5 + 0.5;
+      float pulseSize = 0.8 + (pulse * 0.4);
+      
+      // Draw the slice
+      pushMatrix();
+      translate(position.x, position.y);
+      rotate(angle + (individualProgress * PI * (isReversePattern ? -1 : 1)));
+      
+      // Main slice
+      fill(red(sliceColor), green(sliceColor), blue(sliceColor), sliceAlpha);
+      noStroke();
+      rect(-width/2 * pulseSize, -height/2 * pulseSize, 
+           width * pulseSize, height * pulseSize, 2);
+      
+      // Inner detail
+      fill(255, sliceAlpha * 0.5 * pulse);
+      rect(-width/4 * pulseSize, -height/4 * pulseSize, 
+           width/2 * pulseSize, height/2 * pulseSize, 1);
+      
+      popMatrix();
+    }
+  }
+}
diff --git a/viz/GlitchEffect.pde b/viz/GlitchEffect.pde
new file mode 100644
index 0000000..3ee7494
--- /dev/null
+++ b/viz/GlitchEffect.pde
@@ -0,0 +1,263 @@
+/**
+ * GlitchEffect class
+ * 
+ * Creates cyberpunk-style digital glitch effects
+ */
+class GlitchEffect {
+  float glitchIntensity;
+  ArrayList<GlitchLine> glitchLines;
+  ArrayList<GlitchBlock> glitchBlocks;
+  
+  // Timing
+  float lastGlitchTime;
+  float glitchDuration;
+  
+  GlitchEffect() {
+    glitchIntensity = 0;
+    glitchLines = new ArrayList<GlitchLine>();
+    glitchBlocks = new ArrayList<GlitchBlock>();
+    lastGlitchTime = 0;
+    glitchDuration = 0;
+  }
+  
+  void apply() {
+    // Natural decay of effect
+    glitchIntensity *= 0.95;
+    
+    // Check if glitch duration is over
+    if (millis() - lastGlitchTime > glitchDuration) {
+      // Random chance of a new glitch
+      if (random(100) < 2) {
+        trigger(random(0.1, 0.3));
+      }
+    }
+    
+    // Apply glitch effects if active
+    if (glitchIntensity > 0.01) {
+      // Update and draw glitch lines
+      updateGlitchLines();
+      drawGlitchLines();
+      
+      // Update and draw glitch blocks
+      updateGlitchBlocks();
+      drawGlitchBlocks();
+      
+      // Optional: Apply color shift
+      if (glitchIntensity > 0.2) {
+        applyColorShift();
+      }
+    }
+  }
+  
+  void trigger(float intensity) {
+    // Start a new glitch effect
+    glitchIntensity = min(glitchIntensity + intensity, 1.0);
+    lastGlitchTime = millis();
+    glitchDuration = random(100, 500) * intensity;
+    
+    // Create new glitch elements
+    createGlitchLines();
+    createGlitchBlocks();
+  }
+  
+  void createGlitchLines() {
+    // Clear existing lines
+    glitchLines.clear();
+    
+    // Create new lines
+    int numLines = int(5 * glitchIntensity);
+    for (int i = 0; i < numLines; i++) {
+      glitchLines.add(new GlitchLine());
+    }
+  }
+  
+  void updateGlitchLines() {
+    // Update all glitch lines
+    for (GlitchLine line : glitchLines) {
+      line.update();
+    }
+  }
+  
+  void drawGlitchLines() {
+    // Draw all glitch lines
+    for (GlitchLine line : glitchLines) {
+      line.display();
+    }
+  }
+  
+  void createGlitchBlocks() {
+    // Clear existing blocks
+    glitchBlocks.clear();
+    
+    // Create new blocks
+    int numBlocks = int(10 * glitchIntensity);
+    for (int i = 0; i < numBlocks; i++) {
+      glitchBlocks.add(new GlitchBlock());
+    }
+  }
+  
+  void updateGlitchBlocks() {
+    // Update all glitch blocks
+    for (int i = glitchBlocks.size() - 1; i >= 0; i--) {
+      GlitchBlock block = glitchBlocks.get(i);
+      block.update();
+      
+      // Remove expired blocks
+      if (block.isDead()) {
+        glitchBlocks.remove(i);
+      }
+    }
+    
+    // Add new blocks randomly
+    if (random(100) < 20 * glitchIntensity && glitchBlocks.size() < 20) {
+      glitchBlocks.add(new GlitchBlock());
+    }
+  }
+  
+  void drawGlitchBlocks() {
+    // Draw all glitch blocks
+    for (GlitchBlock block : glitchBlocks) {
+      block.display();
+    }
+  }
+  
+  void applyColorShift() {
+    // Create RGB shift effect
+    blendMode(EXCLUSION);
+    noStroke();
+    
+    // Red shift
+    fill(255, 0, 0, 40 * glitchIntensity);
+    rect(random(-5, 5), random(-5, 5), width, height);
+    
+    // Blue shift
+    fill(0, 0, 255, 40 * glitchIntensity);
+    rect(random(-5, 5), random(-5, 5), width, height);
+    
+    blendMode(BLEND);
+  }
+}
+
+/**
+ * GlitchLine class
+ * 
+ * Horizontal line that creates a scan-line glitch effect
+ */
+class GlitchLine {
+  float yPosition;
+  float height;
+  float offset;
+  float speed;
+  color lineColor;
+  float alpha;
+  
+  GlitchLine() {
+    yPosition = random(height);
+    this.height = random(2, 10);
+    offset = random(-30, 30);
+    speed = random(-2, 2);
+    
+    // Random RGB channel emphasis
+    int channel = int(random(3));
+    switch(channel) {
+      case 0:
+        lineColor = color(255, 50, 50); // Red
+        break;
+      case 1:
+        lineColor = color(50, 255, 50); // Green
+        break;
+      case 2:
+        lineColor = color(50, 50, 255); // Blue
+        break;
+    }
+    
+    alpha = random(100, 200);
+  }
+  
+  void update() {
+    // Move the line
+    yPosition += speed;
+    
+    // Wrap around screen
+    if (yPosition < 0) yPosition = height;
+    if (yPosition > height) yPosition = 0;
+    
+    // Randomize offset occasionally
+    if (random(100) < 5) {
+      offset = random(-30, 30);
+    }
+  }
+  
+  void display() {
+    // Draw the line
+    noStroke();
+    fill(red(lineColor), green(lineColor), blue(lineColor), alpha);
+    rect(0, yPosition, width, this.height);
+    
+    // Draw offset segment
+    int segmentWidth = int(random(50, width/2));
+    int segmentX = int(random(width - segmentWidth));
+    
+    // Draw offset segment
+    rect(segmentX, yPosition + offset, segmentWidth, this.height);
+  }
+}
+
+/**
+ * GlitchBlock class
+ * 
+ * Rectangular block that creates digital artifact effects
+ */
+class GlitchBlock {
+  PVector position;
+  float blockWidth;
+  float blockHeight;
+  color blockColor;
+  float alpha;
+  float lifespan;
+  
+  GlitchBlock() {
+    position = new PVector(random(width), random(height));
+    blockWidth = random(20, 100);
+    blockHeight = random(5, 30);
+    
+    // Use a bright cyberpunk color
+    float hue = random(360);
+    colorMode(HSB, 360, 100, 100);
+    blockColor = color(hue, 80, 100);
+    colorMode(RGB, 255, 255, 255);
+    
+    alpha = random(50, 150);
+    lifespan = random(10, 30);
+  }
+  
+  void update() {
+    // Decrease lifespan
+    lifespan--;
+    
+    // Random position changes
+    if (random(100) < 30) {
+      position.x = random(width);
+    }
+  }
+  
+  void display() {
+    // Draw the block
+    noStroke();
+    fill(red(blockColor), green(blockColor), blue(blockColor), alpha);
+    rect(position.x, position.y, blockWidth, blockHeight);
+    
+    // Draw some noise inside
+    fill(255, alpha * 0.7);
+    for (int i = 0; i < 5; i++) {
+      float noiseX = position.x + random(blockWidth);
+      float noiseY = position.y + random(blockHeight);
+      float noiseSize = random(2, 5);
+      rect(noiseX, noiseY, noiseSize, noiseSize);
+    }
+  }
+  
+  boolean isDead() {
+    return lifespan <= 0;
+  }
+}
diff --git a/viz/Grid.pde b/viz/Grid.pde
new file mode 100644
index 0000000..8298210
--- /dev/null
+++ b/viz/Grid.pde
@@ -0,0 +1,151 @@
+
+/**
+ * Grid class
+ * 
+ * Creates a cyberpunk grid with pulse effects that react to the music
+ */
+class Grid {
+  // Grid properties
+  int gridStyle = 0; // 0: standard, 1: polar, 2: hexagonal
+  int numStyles = 3;
+  
+  // Timing properties
+  float cps = 0.5;
+  float pulseIntensity = 0;
+  
+  // Colors
+  color gridColor;
+  color accentColor;
+  
+  Grid() {
+    gridColor = color(0, 150, 180, 50);
+    accentColor = color(0, 255, 255, 100);
+  }
+  
+  void update() {
+    // Decay pulse intensity
+    pulseIntensity *= 0.95;
+  }
+  
+  void display() {
+    switch(gridStyle) {
+      case 0:
+        drawStandardGrid();
+        break;
+      case 1:
+        drawPolarGrid();
+        break;
+      case 2:
+        drawHexGrid();
+        break;
+    }
+  }
+  
+  void drawStandardGrid() {
+    strokeWeight(1);
+    
+    // Draw vertical lines
+    float verticalSpacing = width / 20.0;
+    for (int i = 0; i <= 20; i++) {
+      float x = i * verticalSpacing;
+      float intensity = pulseIntensity * (1 - abs((x / width) - 0.5) * 2);
+      
+      stroke(lerpColor(gridColor, accentColor, intensity));
+      line(x, 0, x, height);
+    }
+    
+    // Draw horizontal lines
+    float horizontalSpacing = height / 15.0;
+    for (int i = 0; i <= 15; i++) {
+      float y = i * horizontalSpacing;
+      float intensity = pulseIntensity * (1 - abs((y / height) - 0.5) * 2);
+      
+      stroke(lerpColor(gridColor, accentColor, intensity));
+      line(0, y, width, y);
+    }
+    
+    // Draw horizon line with stronger pulse
+    stroke(lerpColor(gridColor, accentColor, pulseIntensity));
+    strokeWeight(2 + pulseIntensity * 3);
+    line(0, height * 0.5, width, height * 0.5);
+  }
+  
+  void drawPolarGrid() {
+    pushMatrix();
+    translate(width / 2, height / 2);
+    
+    // Draw circular grid
+    noFill();
+    for (int i = 1; i <= 10; i++) {
+      float radius = i * (min(width, height) / 20.0);
+      float intensity = pulseIntensity * (1 - (i / 10.0) * 0.8);
+      
+      stroke(lerpColor(gridColor, accentColor, intensity));
+      strokeWeight(1 + intensity * 2);
+      ellipse(0, 0, radius * 2, radius * 2);
+    }
+    
+    // Draw radial lines
+    int numRadials = 16;
+    for (int i = 0; i < numRadials; i++) {
+      float angle = i * TWO_PI / numRadials;
+      float intensity = pulseIntensity * 0.8;
+      
+      stroke(lerpColor(gridColor, accentColor, intensity));
+      strokeWeight(1 + intensity * 2);
+      
+      float radius = min(width, height) / 2;
+      line(0, 0, cos(angle) * radius, sin(angle) * radius);
+    }
+    
+    popMatrix();
+  }
+  
+  void drawHexGrid() {
+    float hexSize = 40;
+    float horizontalSpacing = hexSize * 1.5;
+    float verticalSpacing = hexSize * sqrt(3);
+    
+    stroke(lerpColor(gridColor, accentColor, pulseIntensity * 0.5));
+    strokeWeight(1 + pulseIntensity * 2);
+    noFill();
+    
+    for (int row = -1; row < height / verticalSpacing + 1; row++) {
+      for (int col = -1; col < width / horizontalSpacing + 1; col++) {
+        float xCenter = col * horizontalSpacing + ((row % 2 == 0) ? 0 : horizontalSpacing / 2);
+        float yCenter = row * verticalSpacing;
+        
+        // Intensity based on distance from center
+        float distFromCenter = dist(xCenter, yCenter, width/2, height/2) / (width/2);
+        float intensity = pulseIntensity * (1 - distFromCenter * 0.7);
+        
+        if (intensity > 0.05) {
+          stroke(lerpColor(gridColor, accentColor, intensity));
+          drawHexagon(xCenter, yCenter, hexSize);
+        }
+      }
+    }
+  }
+  
+  void drawHexagon(float xCenter, float yCenter, float size) {
+    beginShape();
+    for (int i = 0; i < 6; i++) {
+      float angle = i * TWO_PI / 6;
+      vertex(xCenter + cos(angle) * size, yCenter + sin(angle) * size);
+    }
+    endShape(CLOSE);
+  }
+  
+  void trigger(float intensity) {
+    // Trigger a pulse effect
+    pulseIntensity = min(pulseIntensity + intensity, 1.0);
+  }
+  
+  void setCPS(float newCps) {
+    this.cps = newCps;
+  }
+  
+  void toggleStyle() {
+    gridStyle = (gridStyle + 1) % numStyles;
+  }
+}
diff --git a/viz/Launch.pde.disabled b/viz/Launch.pde.disabled
new file mode 100644
index 0000000..387e848
--- /dev/null
+++ b/viz/Launch.pde.disabled
@@ -0,0 +1,62 @@
+/**
+ * Launcher Sketch for ParVaguesViz
+ * 
+ * This simple sketch launches the main ParVaguesViz directly,
+ * bypassing the GUI launcher issues.
+ */
+
+// Array of files to import
+String[] fileList = {
+  "ParVaguesViz.pde",
+  "TrackManager.pde",
+  "BreakbeatEvent.pde",
+  "Background.pde",
+  "Grid.pde",
+  "ParticleSystem.pde",
+  "GlitchEffect.pde",
+  "MetadataSystem.pde",
+  "SampleAnalyzer.pde",
+  "Processing4Compatibility.pde"
+};
+
+void setup() {
+  size(400, 300);
+  background(0);
+  fill(255);
+  textAlign(CENTER, CENTER);
+  textSize(16);
+  text("Launching ParVaguesViz...\nPress any key to begin.", width/2, height/2);
+}
+
+void draw() {
+  // Just wait for keypress
+}
+
+void keyPressed() {
+  background(0);
+  text("Loading sketch...", width/2, height/2);
+  
+  // Try to directly launch the main sketch
+  try {
+    String sketchPath = sketchPath("");
+    println("Sketch path: " + sketchPath);
+    
+    // Inform the user
+    println("Attempting to launch ParVaguesViz...");
+    println("If there are any errors, they will appear below:");
+    
+    // Launch the main sketch in this JVM
+    PApplet.main("ParVaguesViz");
+    
+    // Success message
+    background(0, 100, 0);
+    text("ParVaguesViz launched successfully!\nYou can close this window.", width/2, height/2);
+  } 
+  catch (Exception e) {
+    // Error handling
+    background(100, 0, 0);
+    text("Error launching sketch:\n" + e.getMessage(), width/2, height/2);
+    println("Error: " + e.getMessage());
+    e.printStackTrace();
+  }
+}
diff --git a/viz/MetadataSystem.pde b/viz/MetadataSystem.pde
new file mode 100644
index 0000000..99be170
--- /dev/null
+++ b/viz/MetadataSystem.pde
@@ -0,0 +1,414 @@
+/**
+ * MetadataSystem class
+ * 
+ * Manages metadata for tracks and provides an interface for:
+ * 1. Auto-detection of track types based on patterns and samples
+ * 2. Manual metadata setting through special OSC messages
+ * 3. Persistent configuration through a metadata file
+ */
+class MetadataSystem {
+  HashMap<Integer, TrackMetadata> trackMetadata;
+  HashMap<String, ArrayList<String>> sampleCategories;
+  boolean hasLoadedMetadata;
+  
+  MetadataSystem() {
+    trackMetadata = new HashMap<Integer, TrackMetadata>();
+    sampleCategories = new HashMap<String, ArrayList<String>>();
+    hasLoadedMetadata = false;
+    
+    // Initialize with default categories
+    initializeSampleCategories();
+    
+    // Try to load metadata file
+    loadMetadataFile();
+  }
+  
+  void initializeSampleCategories() {
+    // Define categories of samples for auto-detection
+    
+    // Kick drums
+    ArrayList<String> kicks = new ArrayList<String>();
+    kicks.add("bd");
+    kicks.add("kick");
+    kicks.add("808bd");
+    kicks.add("bass drum");
+    kicks.add("jazz");
+    sampleCategories.put("kick", kicks);
+    
+    // Snares
+    ArrayList<String> snares = new ArrayList<String>();
+    snares.add("sn");
+    snares.add("snare");
+    snares.add("cp");
+    snares.add("clap");
+    snares.add("rimshot");
+    sampleCategories.put("snare", snares);
+    
+    // Hi-hats
+    ArrayList<String> hats = new ArrayList<String>();
+    hats.add("hh");
+    hats.add("hat");
+    hats.add("hihat");
+    hats.add("openhat");
+    hats.add("closehat");
+    hats.add("ch");
+    hats.add("oh");
+    sampleCategories.put("hihat", hats);
+    
+    // Bass
+    ArrayList<String> bass = new ArrayList<String>();
+    bass.add("bass");
+    bass.add("sub");
+    bass.add("808");
+    bass.add("bassWarsaw");
+    sampleCategories.put("bass", bass);
+    
+    // Breaks
+    ArrayList<String> breaks = new ArrayList<String>();
+    breaks.add("break");
+    breaks.add("jungle");
+    breaks.add("amen");
+    breaks.add("think");
+    breaks.add("apache");
+    breaks.add("funky");
+    sampleCategories.put("breaks", breaks);
+    
+    // Melodic
+    ArrayList<String> melodic = new ArrayList<String>();
+    melodic.add("keys");
+    melodic.add("pad");
+    melodic.add("piano");
+    melodic.add("rhodes");
+    melodic.add("chord");
+    melodic.add("synth");
+    melodic.add("psin");
+    melodic.add("superfork");
+    sampleCategories.put("melodic", melodic);
+    
+    // FX
+    ArrayList<String> fx = new ArrayList<String>();
+    fx.add("fx");
+    fx.add("riser");
+    fx.add("sweep");
+    fx.add("weird");
+    fx.add("glitch");
+    fx.add("noise");
+    sampleCategories.put("fx", fx);
+    
+    // Vocals
+    ArrayList<String> vocals = new ArrayList<String>();
+    vocals.add("voc");
+    vocals.add("voice");
+    vocals.add("vocal");
+    vocals.add("speech");
+    vocals.add("talk");
+    vocals.add("sing");
+    sampleCategories.put("vocals", vocals);
+    
+    // ParVagues specific samples (from the code examples)
+    ArrayList<String> parVaguesSpecific = new ArrayList<String>();
+    parVaguesSpecific.add("suns_keys");
+    parVaguesSpecific.add("suns_guitar");
+    parVaguesSpecific.add("suns_voice");
+    parVaguesSpecific.add("rampleM2");
+    parVaguesSpecific.add("rampleD");
+    parVaguesSpecific.add("armora");
+    parVaguesSpecific.add("FMRhodes1");
+    sampleCategories.put("parvagues", parVaguesSpecific);
+  }
+  
+  void loadMetadataFile() {
+    // Try to load metadata from a file
+    try {
+      String[] lines = loadStrings("parvagues_metadata.txt");
+      if (lines != null && lines.length > 0) {
+        for (String line : lines) {
+          if (line.trim().startsWith("#") || line.trim().isEmpty()) continue; // Skip comments and empty lines
+          
+          String[] parts = line.split(":");
+          if (parts.length >= 3) {
+            int orbit = int(parts[0].trim());
+            String trackName = parts[1].trim();
+            String trackType = parts[2].trim();
+            
+            // Create metadata
+            TrackMetadata metadata = new TrackMetadata(orbit, trackName, trackType);
+            
+            // Add additional properties if available
+            if (parts.length > 3) {
+              String[] properties = parts[3].split(",");
+              for (String prop : properties) {
+                String[] keyValue = prop.split("=");
+                if (keyValue.length == 2) {
+                  metadata.setProperty(keyValue[0].trim(), keyValue[1].trim());
+                }
+              }
+            }
+            
+            // Store metadata
+            trackMetadata.put(orbit, metadata);
+          }
+        }
+        hasLoadedMetadata = true;
+        println("Loaded metadata for " + trackMetadata.size() + " tracks");
+      }
+    } catch (Exception e) {
+      println("Could not load metadata file: " + e.getMessage());
+      // File might not exist yet, that's OK
+    }
+  }
+  
+  void saveMetadataFile() {
+    // Save metadata to a file
+    ArrayList<String> lines = new ArrayList<String>();
+    lines.add("# ParVagues Visualization Metadata");
+    lines.add("# Format: orbit:name:type:prop1=value1,prop2=value2,...");
+    lines.add("");
+    
+    for (int orbit : trackMetadata.keySet()) {
+      TrackMetadata metadata = trackMetadata.get(orbit);
+      String line = orbit + ":" + metadata.name + ":" + metadata.type;
+      
+      // Add properties
+      if (metadata.properties.size() > 0) {
+        line += ":";
+        boolean first = true;
+        for (String key : metadata.properties.keySet()) {
+          if (!first) line += ",";
+          line += key + "=" + metadata.properties.get(key);
+          first = false;
+        }
+      }
+      
+      lines.add(line);
+    }
+    
+    saveStrings("data/parvagues_metadata.txt", lines.toArray(new String[0]));
+    println("Saved metadata for " + trackMetadata.size() + " tracks");
+  }
+  
+  TrackMetadata getMetadata(int orbit) {
+    // Get metadata for a specific orbit
+    if (trackMetadata.containsKey(orbit)) {
+      return trackMetadata.get(orbit);
+    }
+    
+    // Create default metadata if none exists
+    TrackMetadata defaultMetadata = createDefaultMetadata(orbit);
+    trackMetadata.put(orbit, defaultMetadata);
+    return defaultMetadata;
+  }
+  
+  TrackMetadata createDefaultMetadata(int orbit) {
+    // Create default metadata based on orbit
+    String name = "Track " + (orbit + 1);
+    String type = "default";
+    
+    // Assign types based on common usage
+    switch(orbit) {
+      case 0: 
+        type = "kick"; 
+        name = "Kick";
+        break;
+      case 1: 
+        type = "snare"; 
+        name = "Snare";
+        break;
+      case 2: 
+        type = "hihat"; 
+        name = "Drums";
+        break;
+      case 3: 
+        type = "bass"; 
+        name = "Bass";
+        break;
+      case 7: // d8
+        type = "breaks"; 
+        name = "Breakbeats";
+        break;
+    }
+    
+    return new TrackMetadata(orbit, name, type);
+  }
+  
+  void updateFromSample(int orbit, String sample) {
+    // Update metadata based on sample name detection
+    if (sample == null || sample.isEmpty()) return;
+    
+    TrackMetadata metadata = getMetadata(orbit);
+    
+    // If this is the first sample for this track, try to determine type
+    if (!metadata.hasDetectedSample) {
+      // Detect sample type
+      String detectedType = detectSampleType(sample);
+      if (detectedType != null) {
+        metadata.type = detectedType;
+        
+        // Also update name if it's still default
+        if (metadata.name.equals("Track " + (orbit + 1))) {
+          metadata.name = detectedType.substring(0, 1).toUpperCase() + detectedType.substring(1);
+        }
+      }
+      
+      metadata.hasDetectedSample = true;
+    }
+    
+    // Record this sample
+    metadata.addRecentSample(sample);
+    
+    // Save metadata periodically (after acquiring some data)
+    if (trackMetadata.size() >= 3 && !hasLoadedMetadata) {
+      saveMetadataFile();
+      hasLoadedMetadata = true;
+    }
+  }
+  
+  String detectSampleType(String sample) {
+    // Detect the type of a sample based on its name
+    String lowerSample = sample.toLowerCase();
+    
+    // Check each category
+    for (String category : sampleCategories.keySet()) {
+      ArrayList<String> keywords = sampleCategories.get(category);
+      for (String keyword : keywords) {
+        if (lowerSample.contains(keyword.toLowerCase())) {
+          return category;
+        }
+      }
+    }
+    
+    // No match found
+    return null;
+  }
+  
+  void processMetadataMessage(OscMessage msg) {
+    // Process a special metadata OSC message
+    // Format: /parvagues/metadata orbit "name" "type" ["prop1=value1,prop2=value2"]
+    
+    try {
+      // Extract data
+      int orbit = msg.get(0).intValue();
+      String name = msg.get(1).stringValue();
+      String type = msg.get(2).stringValue();
+      
+      // Create or update metadata
+      TrackMetadata metadata = getMetadata(orbit);
+      metadata.name = name;
+      metadata.type = type;
+      
+      // Process properties if present
+      if (msg.arguments().length > 3) {
+        String props = msg.get(3).stringValue();
+        String[] properties = props.split(",");
+        for (String prop : properties) {
+          String[] keyValue = prop.split("=");
+          if (keyValue.length == 2) {
+            metadata.setProperty(keyValue[0].trim(), keyValue[1].trim());
+          }
+        }
+      }
+      
+      // Track that this is explicit metadata
+      metadata.isExplicit = true;
+      
+      // Save metadata file
+      saveMetadataFile();
+      
+      println("Updated metadata for orbit " + orbit + ": " + name + " (" + type + ")");
+      
+    } catch (Exception e) {
+      println("Error processing metadata message: " + e.getMessage());
+    }
+  }
+  
+  String getDebugInfo() {
+    // Generate debug info about current metadata
+    StringBuilder info = new StringBuilder();
+    info.append("Track Metadata:\n");
+    
+    for (int orbit : trackMetadata.keySet()) {
+      TrackMetadata metadata = trackMetadata.get(orbit);
+      info.append("d" + (orbit + 1) + ": " + metadata.name + " (" + metadata.type + ")");
+      
+      // Add sample info
+      if (metadata.recentSamples.size() > 0) {
+        info.append(" - Samples: ");
+        for (int i = 0; i < Math.min(3, metadata.recentSamples.size()); i++) {
+          if (i > 0) info.append(", ");
+          info.append(metadata.recentSamples.get(i));
+        }
+        if (metadata.recentSamples.size() > 3) {
+          info.append(", ...");
+        }
+      }
+      
+      info.append("\n");
+    }
+    
+    return info.toString();
+  }
+}
+
+/**
+ * TrackMetadata class
+ * 
+ * Stores metadata about a track
+ */
+class TrackMetadata {
+  int orbit;
+  String name;
+  String type;
+  HashMap<String, String> properties;
+  ArrayList<String> recentSamples;
+  boolean isExplicit;
+  boolean hasDetectedSample;
+  
+  TrackMetadata(int orbit, String name, String type) {
+    this.orbit = orbit;
+    this.name = name;
+    this.type = type;
+    this.properties = new HashMap<String, String>();
+    this.recentSamples = new ArrayList<String>();
+    this.isExplicit = false;
+    this.hasDetectedSample = false;
+  }
+  
+  void setProperty(String key, String value) {
+    properties.put(key, value);
+  }
+  
+  String getProperty(String key, String defaultValue) {
+    if (properties.containsKey(key)) {
+      return properties.get(key);
+    }
+    return defaultValue;
+  }
+  
+  void addRecentSample(String sample) {
+    // Add to recent samples, avoiding duplicates
+    if (!recentSamples.contains(sample)) {
+      recentSamples.add(sample);
+      
+      // Keep only the 10 most recent samples
+      if (recentSamples.size() > 10) {
+        recentSamples.remove(0);
+      }
+    }
+  }
+  
+  color getTrackColor() {
+    // Get the color for this track based on type or explicit setting
+    
+    // Check if color is explicitly set in properties
+    if (properties.containsKey("color")) {
+      String hexColor = properties.get("color");
+      if (hexColor.startsWith("#")) {
+        hexColor = hexColor.substring(1);
+      }
+      return unhex("FF" + hexColor);
+    }
+    
+    // Otherwise, use default color based on orbit
+    return orbitColors[orbit];
+  }
+}
diff --git a/viz/ParVaguesViz.pde b/viz/ParVaguesViz.pde
new file mode 100644
index 0000000..5cfb9c7
--- /dev/null
+++ b/viz/ParVaguesViz.pde
@@ -0,0 +1,375 @@
+/**
+ * ParVaguesViz - Cyberpunk TidalCycles Visualizer
+ * 
+ * A Processing-based visualizer that works with SuperDirt OSC messages
+ * to create cyberpunk-style visualizations for TidalCycles performances.
+ * 
+ * Features:
+ * - Automatic track detection and visualization
+ * - Cyberpunk aesthetic with neon colors and grid effects
+ * - Real-time audio-reactive visual elements
+ * - Orbit-based color schemes
+ * - Metadata system for track identification
+ */
+
+import oscP5.*;
+import netP5.*;
+
+// Processing 4.x compatibility check
+boolean isProcessing4 = true;
+
+// OSC connection settings
+OscP5 oscP5;
+NetAddress superdirtAddress;
+int listenPort = 57120; // Default SuperDirt port
+
+// Visualization components
+TrackManager trackManager;
+Grid grid;
+Background background;
+ParticleSystem particleSystem;
+GlitchEffect glitchEffect;
+
+// Metadata system
+MetadataSystem metadataSystem;
+
+// Sample analyzer
+SampleAnalyzer sampleAnalyzer;
+
+// UI settings
+boolean debug = false;
+boolean showHelp = false;
+boolean showMetadata = false;
+PFont debugFont;
+PFont titleFont;
+PFont metadataFont;
+
+// Timing
+float bpm = 120;
+float cps = 0.5;
+float currentCycle = 0;
+float elapsedTime = 0;
+float lastBeatTime = 0;
+
+// Colors (cyberpunk palette)
+color[] orbitColors = {
+  #00FFFF, // Cyan (d1 - kick)
+  #FF00FF, // Magenta (d2 - snare)
+  #00FF99, // Neon green (d3 - drums)
+  #FF5500, // Orange (d4 - bass)
+  #9900FF, // Purple (d5)
+  #FFFF00, // Yellow (d6)
+  #FF0066, // Pink (d7)
+  #0099FF, // Blue (d8 - breaks)
+  #33FF33, // Green (d9)
+  #FF3300, // Red (d10)
+  #CC00FF, // Violet (d11)
+  #00CCFF, // Light blue (d12)
+  #FFFFFF, // White (d13-d16)
+  #FFFFFF,
+  #FFFFFF,
+  #FFFFFF
+};
+
+void setup() {
+  // Apply Processing 4.x compatibility fixes
+  checkProcessingVersion();
+  applyProcessing4Fixes();
+  
+  size(1280, 720, P3D);
+  frameRate(60);
+  smooth(8);
+  
+  // Initialize OSC
+  oscP5 = new OscP5(this, listenPort);
+  superdirtAddress = new NetAddress("127.0.0.1", listenPort);
+  
+  // Initialize components
+  trackManager = new TrackManager();
+  grid = new Grid();
+  background = new Background();
+  particleSystem = new ParticleSystem();
+  glitchEffect = new GlitchEffect();
+  
+  // Initialize metadata system
+  metadataSystem = new MetadataSystem();
+  
+  // Initialize sample analyzer
+  sampleAnalyzer = new SampleAnalyzer();
+  
+  // Load fonts
+  debugFont = createFont("Courier New Bold", 12);
+  titleFont = createFont("Arial Bold", 24);
+  metadataFont = createFont("Arial", 14);
+  
+  // Print startup message
+  println("ParVaguesViz started");
+  println("Listening for SuperDirt OSC messages on port " + listenPort);
+}
+
+void draw() {
+  // Update timing
+  elapsedTime = millis() / 1000.0;
+  
+  // Clear background with fade effect
+  background.update();
+  background.display();
+  
+  // Update grid
+  grid.update();
+  grid.display();
+  
+  // Update and display tracks
+  trackManager.update();
+  trackManager.display();
+  
+  // Update and display particles
+  particleSystem.update();
+  particleSystem.display();
+  
+  // Apply glitch effects
+  glitchEffect.apply();
+  
+  // Draw UI elements if debug mode is on
+  if (debug) {
+    drawDebugInfo();
+  }
+  
+  // Draw metadata overlay if enabled
+  if (showMetadata) {
+    drawMetadataOverlay();
+  }
+  
+  // Draw help if enabled
+  if (showHelp) {
+    drawHelp();
+  }
+}
+
+// Handle OSC messages
+void oscEvent(OscMessage msg) {
+  // Check for metadata messages
+  if (msg.addrPattern().equals("/parvagues/metadata")) {
+    metadataSystem.processMetadataMessage(msg);
+    return;
+  }
+  
+  // Forward the message to our handler
+  if (msg.addrPattern().equals("/dirt/play")) {
+    handleDirtMessage(msg);
+  } else if (msg.addrPattern().equals("/cps")) {
+    updateCPS(msg);
+  }
+}
+
+// Process SuperDirt message
+void handleDirtMessage(OscMessage msg) {
+  // Extract basic information
+  int orbit = -1;
+  String sound = "";
+  float cycle = 0;
+  float delta = 0;
+  float gain = 1.0;
+  float pan = 0.5;
+  
+  // Extract all parameters from the message
+  for (int i = 0; i < msg.typetag().length(); i++) {
+    String paramName = msg.get(i).stringValue();
+    
+    if (paramName.equals("orbit")) {
+      orbit = msg.get(i+1).intValue();
+    } 
+    else if (paramName.equals("s")) {
+      sound = msg.get(i+1).stringValue();
+    } 
+    else if (paramName.equals("cycle")) {
+      cycle = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("delta")) {
+      delta = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("gain")) {
+      gain = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("pan")) {
+      pan = msg.get(i+1).floatValue();
+    }
+  }
+  
+  // Only process valid messages with an orbit
+  if (orbit >= 0) {
+    // Update metadata system with sample information
+    metadataSystem.updateFromSample(orbit, sound);
+    
+    // Update sample analyzer
+    sampleAnalyzer.processSample(orbit, sound, gain, delta);
+    
+    // Beat detection logic
+    currentCycle = cycle;
+    float now = millis() / 1000.0;
+    if (now - lastBeatTime > 0.1) { // Debounce
+      lastBeatTime = now;
+      grid.trigger(0.3); // Trigger grid effect on beats
+      glitchEffect.trigger(0.1); // Small glitch on beats
+    }
+    
+    // Create a new visual event for this sound
+    trackManager.addEvent(orbit, sound, gain, pan, delta);
+    
+    // Add particles
+    particleSystem.addParticles(orbit, pan, gain);
+  }
+}
+
+// Update timing information from CPS messages
+void updateCPS(OscMessage msg) {
+  if (msg.checkTypetag("f")) {
+    cps = msg.get(0).floatValue();
+    bpm = cps * 60 * 4; // Convert to BPM
+    
+    // Update components with new timing
+    grid.setCPS(cps);
+    background.setCPS(cps);
+  }
+}
+
+// Handle keyboard inputs
+void keyPressed() {
+  if (key == 'd' || key == 'D') {
+    debug = !debug;
+  } else if (key == 'h' || key == 'H') {
+    showHelp = !showHelp;
+  } else if (key == 'g' || key == 'G') {
+    grid.toggleStyle();
+  } else if (key == 'f' || key == 'F') {
+    // Use the compatible fullscreen toggle
+    handleFullscreenToggle();
+  } else if (key == 'r' || key == 'R') {
+    // Reset all visuals
+    trackManager.reset();
+    particleSystem.reset();
+  } else if (key == 'm' || key == 'M') {
+    // Toggle metadata display
+    showMetadata = !showMetadata;
+  }
+}
+
+// Debug information display
+void drawDebugInfo() {
+  fill(255);
+  textFont(debugFont);
+  textAlign(LEFT);
+  
+  text("FPS: " + int(frameRate), 10, 20);
+  text("CPS: " + nf(cps, 0, 2) + " (BPM: " + int(bpm) + ")", 10, 35);
+  text("Cycle: " + nf(currentCycle, 0, 2), 10, 50);
+  text("Active Tracks: " + trackManager.getActiveTrackCount(), 10, 65);
+  text("Particles: " + particleSystem.getParticleCount(), 10, 80);
+  text("Tracked Samples: " + sampleAnalyzer.getSampleCount(), 10, 95);
+  
+  // Add hint about metadata
+  fill(200);
+  text("Press 'M' to toggle metadata display", 10, 125);
+}
+
+// Draw metadata overlay
+void drawMetadataOverlay() {
+  // Semi-transparent background
+  fill(0, 200);
+  noStroke();
+  rect(width - 320, 10, 310, height - 20, 10);
+  
+  // Title
+  fill(255);
+  textFont(titleFont);
+  textAlign(CENTER);
+  text("Track Metadata", width - 165, 40);
+  
+  // Track information
+  textFont(metadataFont);
+  textAlign(LEFT);
+  float y = 70;
+  
+  // Draw metadata for each active track
+  ArrayList<Integer> activeOrbits = trackManager.getActiveOrbits();
+  
+  if (activeOrbits.size() == 0) {
+    text("No active tracks", width - 300, y);
+  } else {
+    for (Integer orbit : activeOrbits) {
+      TrackMetadata metadata = metadataSystem.getMetadata(orbit);
+      
+      // Draw track name and type with track color
+      fill(metadata.getTrackColor());
+      text("d" + (orbit + 1) + ": " + metadata.name, width - 300, y);
+      text("Type: " + metadata.type, width - 300, y + 20);
+      
+      // Draw most recent sample
+      fill(200);
+      if (metadata.recentSamples.size() > 0) {
+        text("Sample: " + metadata.recentSamples.get(metadata.recentSamples.size() - 1), width - 300, y + 40);
+      }
+      
+      // Add analyzed features if available
+      SampleFeatures features = sampleAnalyzer.getFeaturesForOrbit(orbit);
+      if (features != null) {
+        fill(180);
+        text("Tempo: " + nf(features.tempo, 0, 1) + " BPM", width - 300, y + 60);
+        text("Energy: " + nf(features.energy, 0, 2), width - 150, y + 60);
+      }
+      
+      // Draw separator
+      stroke(100);
+      line(width - 300, y + 75, width - 30, y + 75);
+      noStroke();
+      
+      // Move to next track position
+      y += 90;
+      
+      // Avoid drawing outside screen
+      if (y > height - 50) break;
+    }
+  }
+}
+
+// Help information display
+void drawHelp() {
+  fill(0, 180);
+  noStroke();
+  rect(width/2 - 200, height/2 - 150, 400, 300);
+  
+  fill(255);
+  textFont(titleFont);
+  textAlign(CENTER);
+  
+  text("ParVaguesViz Controls", width/2, height/2 - 120);
+  
+  textFont(debugFont);
+  textAlign(LEFT);
+  
+  String[] helpText = {
+    "D - Toggle debug info",
+    "H - Toggle help",
+    "G - Change grid style",
+    "F - Toggle fullscreen",
+    "R - Reset visuals",
+    "M - Toggle metadata display",
+    "",
+    "Automatically visualizes tracks d1-d16",
+    "Special visualization for d8 breakbeats",
+    "No TidalCycles configuration needed"
+  };
+  
+  for (int i = 0; i < helpText.length; i++) {
+    text(helpText[i], width/2 - 180, height/2 - 80 + i * 20);
+  }
+}
+
+// Check if currently fullscreen
+boolean isFullScreen() {
+  if (isProcessing4) {
+    return width == displayWidth && height == displayHeight;
+  } else {
+    return width == displayWidth && height == displayHeight;
+  }
+}
diff --git a/viz/ParVaguesViz.pde.bak b/viz/ParVaguesViz.pde.bak
new file mode 100644
index 0000000..6e56898
--- /dev/null
+++ b/viz/ParVaguesViz.pde.bak
@@ -0,0 +1,375 @@
+/**
+ * ParVaguesViz - Cyberpunk TidalCycles Visualizer
+ * 
+ * A Processing-based visualizer that works with SuperDirt OSC messages
+ * to create cyberpunk-style visualizations for TidalCycles performances.
+ * 
+ * Features:
+ * - Automatic track detection and visualization
+ * - Cyberpunk aesthetic with neon colors and grid effects
+ * - Real-time audio-reactive visual elements
+ * - Orbit-based color schemes
+ * - Metadata system for track identification
+ */
+
+import oscP5.*;
+import netP5.*;
+
+// Processing 4.x compatibility check
+boolean isProcessing4 = true;
+
+// OSC connection settings
+OscP5 oscP5;
+NetAddress superdirtAddress;
+int listenPort = 57120; // Default SuperDirt port
+
+// Visualization components
+TrackManager trackManager;
+Grid grid;
+Background background;
+ParticleSystem particleSystem;
+GlitchEffect glitchEffect;
+
+// Metadata system
+MetadataSystem metadataSystem;
+
+// Sample analyzer
+SampleAnalyzer sampleAnalyzer;
+
+// UI settings
+boolean debug = false;
+boolean showHelp = false;
+boolean showMetadata = false;
+PFont debugFont;
+PFont titleFont;
+PFont metadataFont;
+
+// Timing
+float bpm = 120;
+float cps = 0.5;
+float currentCycle = 0;
+float elapsedTime = 0;
+float lastBeatTime = 0;
+
+// Colors (cyberpunk palette)
+color[] orbitColors = {
+  #00FFFF, // Cyan (d1 - kick)
+  #FF00FF, // Magenta (d2 - snare)
+  #00FF99, // Neon green (d3 - drums)
+  #FF5500, // Orange (d4 - bass)
+  #9900FF, // Purple (d5)
+  #FFFF00, // Yellow (d6)
+  #FF0066, // Pink (d7)
+  #0099FF, // Blue (d8 - breaks)
+  #33FF33, // Green (d9)
+  #FF3300, // Red (d10)
+  #CC00FF, // Violet (d11)
+  #00CCFF, // Light blue (d12)
+  #FFFFFF, // White (d13-d16)
+  #FFFFFF,
+  #FFFFFF,
+  #FFFFFF
+};
+
+void setup() {
+  // Apply Processing 4.x compatibility fixes
+  checkProcessingVersion();
+  applyProcessing4Fixes();
+  
+  size(1280, 720, P3D);
+  frameRate(60);
+  smooth(8);
+  
+  // Initialize OSC
+  oscP5 = new OscP5(this, listenPort);
+  superdirtAddress = new NetAddress("127.0.0.1", listenPort);
+  
+  // Initialize components
+  trackManager = new TrackManager();
+  grid = new Grid();
+  background = new Background();
+  particleSystem = new ParticleSystem();
+  glitchEffect = new GlitchEffect();
+  
+  // Initialize metadata system
+  metadataSystem = new MetadataSystem();
+  
+  // Initialize sample analyzer
+  sampleAnalyzer = new SampleAnalyzer();
+  
+  // Load fonts
+  debugFont = createFont("Courier New Bold", 12);
+  titleFont = createFont("Arial Bold", 24);
+  metadataFont = createFont("Arial", 14);
+  
+  // Print startup message
+  println("ParVaguesViz started");
+  println("Listening for SuperDirt OSC messages on port " + listenPort);
+}
+
+void draw() {
+  // Update timing
+  elapsedTime = millis() / 1000.0;
+  
+  // Clear background with fade effect
+  background.update();
+  background.display();
+  
+  // Update grid
+  grid.update();
+  grid.display();
+  
+  // Update and display tracks
+  trackManager.update();
+  trackManager.display();
+  
+  // Update and display particles
+  particleSystem.update();
+  particleSystem.display();
+  
+  // Apply glitch effects
+  glitchEffect.apply();
+  
+  // Draw UI elements if debug mode is on
+  if (debug) {
+    drawDebugInfo();
+  }
+  
+  // Draw metadata overlay if enabled
+  if (showMetadata) {
+    drawMetadataOverlay();
+  }
+  
+  // Draw help if enabled
+  if (showHelp) {
+    drawHelp();
+  }
+}
+
+// Handle OSC messages
+void oscEvent(OscMessage msg) {
+  // Check for metadata messages
+  if (msg.addrPattern().equals("/parvagues/metadata")) {
+    metadataSystem.processMetadataMessage(msg);
+    return;
+  }
+  
+  // Forward the message to our handler
+  if (msg.addrPattern().equals("/dirt/play")) {
+    handleDirtMessage(msg);
+  } else if (msg.addrPattern().equals("/cps")) {
+    updateCPS(msg);
+  }
+}
+
+// Process SuperDirt message
+void handleDirtMessage(OscMessage msg) {
+  // Extract basic information
+  int orbit = -1;
+  String sound = "";
+  float cycle = 0;
+  float delta = 0;
+  float gain = 1.0;
+  float pan = 0.5;
+  
+  // Extract all parameters from the message
+  for (int i = 0; i < msg.typetag().length(); i++) {
+    String paramName = msg.get(i).stringValue();
+    
+    if (paramName.equals("orbit")) {
+      orbit = msg.get(i+1).intValue();
+    } 
+    else if (paramName.equals("s")) {
+      sound = msg.get(i+1).stringValue();
+    } 
+    else if (paramName.equals("cycle")) {
+      cycle = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("delta")) {
+      delta = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("gain")) {
+      gain = msg.get(i+1).floatValue();
+    } 
+    else if (paramName.equals("pan")) {
+      pan = msg.get(i+1).floatValue();
+    }
+  }
+  
+  // Only process valid messages with an orbit
+  if (orbit >= 0) {
+    // Update metadata system with sample information
+    metadataSystem.updateFromSample(orbit, sound);
+    
+    // Update sample analyzer
+    sampleAnalyzer.processSample(orbit, sound, gain, delta);
+    
+    // Beat detection logic
+    currentCycle = cycle;
+    float now = millis() / 1000.0;
+    if (now - lastBeatTime > 0.1) { // Debounce
+      lastBeatTime = now;
+      grid.trigger(0.3); // Trigger grid effect on beats
+      glitchEffect.trigger(0.1); // Small glitch on beats
+    }
+    
+    // Create a new visual event for this sound
+    trackManager.addEvent(orbit, sound, gain, pan, delta);
+    
+    // Add particles
+    particleSystem.addParticles(orbit, pan, gain);
+  }
+}
+
+// Update timing information from CPS messages
+void updateCPS(OscMessage msg) {
+  if (msg.checkTypetag("f")) {
+    cps = msg.get(0).floatValue();
+    bpm = cps * 60 * 4; // Convert to BPM
+    
+    // Update components with new timing
+    grid.setCPS(cps);
+    background.setCPS(cps);
+  }
+}
+
+// Handle keyboard inputs
+void keyPressed() {
+  if (key == 'd' || key == 'D') {
+    debug = !debug;
+  } else if (key == 'h' || key == 'H') {
+    showHelp = !showHelp;
+  } else if (key == 'g' || key == 'G') {
+    grid.toggleStyle();
+  } else if (key == 'f' || key == 'F') {
+    // Use the compatible fullscreen toggle
+    handleFullscreenToggle();
+  } else if (key == 'r' || key == 'R') {
+    // Reset all visuals
+    trackManager.reset();
+    particleSystem.reset();
+  } else if (key == 'm' || key == 'M') {
+    // Toggle metadata display
+    showMetadata = !showMetadata;
+  }
+}
+
+// Debug information display
+void drawDebugInfo() {
+  fill(255);
+  textFont(debugFont);
+  textAlign(LEFT);
+  
+  text("FPS: " + int(frameRate), 10, 20);
+  text("CPS: " + nf(cps, 0, 2) + " (BPM: " + int(bpm) + ")", 10, 35);
+  text("Cycle: " + nf(currentCycle, 0, 2), 10, 50);
+  text("Active Tracks: " + trackManager.getActiveTrackCount(), 10, 65);
+  text("Particles: " + particleSystem.getParticleCount(), 10, 80);
+  text("Tracked Samples: " + sampleAnalyzer.getSampleCount(), 10, 95);
+  
+  // Add hint about metadata
+  fill(200);
+  text("Press 'M' to toggle metadata display", 10, 125);
+}
+
+// Draw metadata overlay
+void drawMetadataOverlay() {
+  // Semi-transparent background
+  fill(0, 200);
+  noStroke();
+  rect(width - 320, 10, 310, height - 20, 10);
+  
+  // Title
+  fill(255);
+  textFont(titleFont);
+  textAlign(CENTER);
+  text("Track Metadata", width - 165, 40);
+  
+  // Track information
+  textFont(metadataFont);
+  textAlign(LEFT);
+  float y = 70;
+  
+  // Draw metadata for each active track
+  ArrayList<Integer> activeOrbits = trackManager.getActiveOrbits();
+  
+  if (activeOrbits.size() == 0) {
+    text("No active tracks", width - 300, y);
+  } else {
+    for (Integer orbit : activeOrbits) {
+      TrackMetadata metadata = metadataSystem.getMetadata(orbit);
+      
+      // Draw track name and type with track color
+      fill(metadata.getTrackColor());
+      text("d" + (orbit + 1) + ": " + metadata.name, width - 300, y);
+      text("Type: " + metadata.type, width - 300, y + 20);
+      
+      // Draw most recent sample
+      fill(200);
+      if (metadata.recentSamples.size() > 0) {
+        text("Sample: " + metadata.recentSamples.get(metadata.recentSamples.size() - 1), width - 300, y + 40);
+      }
+      
+      // Add analyzed features if available
+      SampleFeatures features = sampleAnalyzer.getFeaturesForOrbit(orbit);
+      if (features != null) {
+        fill(180);
+        text("Tempo: " + nf(features.tempo, 0, 1) + " BPM", width - 300, y + 60);
+        text("Energy: " + nf(features.energy, 0, 2), width - 150, y + 60);
+      }
+      
+      // Draw separator
+      stroke(100);
+      line(width - 300, y + 75, width - 30, y + 75);
+      noStroke();
+      
+      // Move to next track position
+      y += 90;
+      
+      // Avoid drawing outside screen
+      if (y > height - 50) break;
+    }
+  }
+}
+
+// Help information display
+void drawHelp() {
+  fill(0, 180);
+  noStroke();
+  rect(width/2 - 200, height/2 - 150, 400, 300);
+  
+  fill(255);
+  textFont(titleFont);
+  textAlign(CENTER);
+  
+  text("ParVaguesViz Controls", width/2, height/2 - 120);
+  
+  textFont(debugFont);
+  textAlign(LEFT);
+  
+  String[] helpText = {
+    "D - Toggle debug info",
+    "H - Toggle help",
+    "G - Change grid style",
+    "F - Toggle fullscreen",
+    "R - Reset visuals",
+    "M - Toggle metadata display",
+    "",
+    "Automatically visualizes tracks d1-d16",
+    "Special visualization for d8 breakbeats",
+    "No TidalCycles configuration needed"
+  };
+  
+  for (int i = 0; i < helpText.length; i++) {
+    text(helpText[i], width/2 - 180, height/2 - 80 + i * 20);
+  }
+}
+
+// Check if currently fullscreen
+boolean sketchFullScreen() {
+  if (isProcessing4) {
+    return width == displayWidth && height == displayHeight;
+  } else {
+    return width == displayWidth && height == displayHeight;
+  }
+}
diff --git a/viz/ParticleSystem.pde b/viz/ParticleSystem.pde
new file mode 100644
index 0000000..4286469
--- /dev/null
+++ b/viz/ParticleSystem.pde
@@ -0,0 +1,156 @@
+/**
+ * ParticleSystem class
+ * 
+ * Creates and manages particles that respond to musical events
+ */
+class ParticleSystem {
+  ArrayList<Particle> particles;
+  int maxParticles = 500;
+  
+  ParticleSystem() {
+    particles = new ArrayList<Particle>();
+  }
+  
+  void update() {
+    // Update all particles
+    for (int i = particles.size() - 1; i >= 0; i--) {
+      Particle p = particles.get(i);
+      p.update();
+      
+      // Remove dead particles
+      if (p.isDead()) {
+        particles.remove(i);
+      }
+    }
+  }
+  
+  void display() {
+    // Display all particles
+    for (Particle p : particles) {
+      p.display();
+    }
+    
+    // Apply blending for glow effect
+    blendMode(ADD);
+    for (Particle p : particles) {
+      if (p.energy > 0.5) {
+        p.displayGlow();
+      }
+    }
+    blendMode(BLEND);
+  }
+  
+  void addParticles(int orbit, float pan, float gain) {
+    // Skip if at max capacity
+    if (particles.size() >= maxParticles) return;
+    
+    // Number of particles based on gain
+    int count = int(5 + (gain * 20));
+    
+    // Position based on orbit and pan
+    float x = map(pan, 0, 1, width * 0.3, width * 0.7);
+    float y = map(orbit, 0, 15, height * 0.2, height * 0.8);
+    
+    // Create particles
+    for (int i = 0; i < count; i++) {
+      if (particles.size() < maxParticles) {
+        particles.add(new Particle(x, y, orbitColors[orbit], gain));
+      }
+    }
+  }
+  
+  int getParticleCount() {
+    return particles.size();
+  }
+  
+  void reset() {
+    particles.clear();
+  }
+}
+
+/**
+ * Particle class
+ * 
+ * Individual particle with physics for visual effects
+ */
+class Particle {
+  PVector position;
+  PVector velocity;
+  PVector acceleration;
+  
+  color particleColor;
+  float size;
+  float lifespan;
+  float maxLife;
+  float energy;
+  
+  Particle(float x, float y, color c, float gain) {
+    position = new PVector(x, y);
+    
+    // Random velocity
+    float angle = random(TWO_PI);
+    float speed = random(1, 3 + (gain * 5));
+    velocity = new PVector(cos(angle) * speed, sin(angle) * speed);
+    
+    // Slight downward acceleration (gravity)
+    acceleration = new PVector(0, 0.05);
+    
+    // Visual properties
+    particleColor = c;
+    size = random(2, 8);
+    maxLife = random(500, 2000);
+    lifespan = maxLife;
+    energy = gain;
+  }
+  
+  void update() {
+    // Apply physics
+    velocity.add(acceleration);
+    position.add(velocity);
+    
+    // Add some random movement
+    velocity.x += random(-0.1, 0.1);
+    velocity.y += random(-0.1, 0.1);
+    
+    // Slow down over time
+    velocity.mult(0.98);
+    
+    // Decrease lifespan
+    lifespan -= 10;
+    
+    // Bounce off edges with energy loss
+    if (position.x < 0 || position.x > width) {
+      velocity.x *= -0.8;
+      position.x = constrain(position.x, 0, width);
+    }
+    
+    if (position.y < 0 || position.y > height) {
+      velocity.y *= -0.8;
+      position.y = constrain(position.y, 0, height);
+    }
+  }
+  
+  void display() {
+    // Calculate alpha based on remaining life
+    float alpha = map(lifespan, 0, maxLife, 0, 200);
+    
+    // Draw particle
+    noStroke();
+    fill(red(particleColor), green(particleColor), blue(particleColor), alpha);
+    ellipse(position.x, position.y, size, size);
+  }
+  
+  void displayGlow() {
+    // Draw glow effect
+    float glowSize = size * 3;
+    float alpha = map(lifespan, 0, maxLife, 0, 50) * energy;
+    
+    noStroke();
+    fill(red(particleColor), green(particleColor), blue(particleColor), alpha);
+    ellipse(position.x, position.y, glowSize, glowSize);
+  }
+  
+  boolean isDead() {
+    return lifespan <= 0;
+  }
+}
diff --git a/viz/Processing4Compatibility.pde b/viz/Processing4Compatibility.pde
new file mode 100644
index 0000000..5f3a098
--- /dev/null
+++ b/viz/Processing4Compatibility.pde
@@ -0,0 +1,79 @@
+/**
+ * Processing 4.x Compatibility Helper
+ * 
+ * This file contains workarounds for issues specific to Processing 4.x
+ */
+
+// NOTE: The isProcessing4 variable is now declared only in ParVaguesViz.pde
+// We reference it directly without redeclaring it here
+
+// Some helper functions for Processing 4 compatibility
+void checkProcessingVersion() {
+  // Check Processing version - this approach works with Processing 3 and 4
+  try {
+    // Get processing version using reflection to avoid errors
+    java.lang.reflect.Field versionField = processing.core.PApplet.class.getDeclaredField("VERSION");
+    versionField.setAccessible(true);
+    String versionStr = (String) versionField.get(null);
+    
+    // Parse the version
+    if (versionStr != null && versionStr.length() > 0) {
+      String[] parts = versionStr.split("\\.");
+      if (parts.length > 0) {
+        int majorVersion = Integer.parseInt(parts[0]);
+        isProcessing4 = (majorVersion >= 4);
+        println("Detected Processing version: " + versionStr);
+        println("Using Processing 4 compatibility: " + (isProcessing4 ? "yes" : "no"));
+      }
+    }
+  } catch (Exception e) {
+    // If we can't check the version, assume it's Processing 4 or higher
+    println("Could not determine Processing version: " + e.getMessage());
+    println("Assuming Processing 4 compatibility is needed");
+    isProcessing4 = true;
+  }
+}
+
+// Call this in your setup function near the beginning
+void applyProcessing4Fixes() {
+  if (isProcessing4) {
+    // Fix to allow fullscreen toggle
+    // In Processing 4, we use a different method for fullscreen
+    frameRate(60); // Ensure decent frame rate
+    
+    // Fix for certain Linux/JDK combinations
+    try {
+      // Disable potential problem with X11 toolkit
+      System.setProperty("awt.useSystemAAFontSettings", "on");
+      System.setProperty("swing.aatext", "true");
+    } catch (Exception e) {
+      println("Warning: Could not set system properties: " + e.getMessage());
+    }
+  }
+}
+
+// Use this instead of the old fullscreen toggle
+void toggleFullscreen() {
+  if (isProcessing4) {
+    // Processing 4.x way
+    if (width != displayWidth || height != displayHeight) {
+      surface.setSize(displayWidth, displayHeight);
+      surface.setLocation(0, 0);
+    } else {
+      surface.setSize(1280, 720);
+      surface.setLocation(displayWidth/2 - 640, displayHeight/2 - 360);
+    }
+  } else {
+    // Processing 3.x way (original code)
+    if (width == displayWidth && height == displayHeight) {
+      surface.setSize(1280, 720);
+    } else {
+      surface.setSize(displayWidth, displayHeight);
+    }
+  }
+}
+
+// Update your keyPressed function to use this
+void handleFullscreenToggle() {
+  toggleFullscreen();
+}
diff --git a/viz/SampleAnalyzer.pde b/viz/SampleAnalyzer.pde
new file mode 100644
index 0000000..e77a89b
--- /dev/null
+++ b/viz/SampleAnalyzer.pde
@@ -0,0 +1,280 @@
+/**
+ * SampleAnalyzer class
+ * 
+ * Analyzes sample usage patterns to detect properties like:
+ * - Tempo
+ * - Rhythmic structure
+ * - Energy
+ * - Role in the musical composition
+ */
+class SampleAnalyzer {
+  HashMap<Integer, TrackAnalysis> trackAnalysis;
+  HashMap<String, SampleFeatures> sampleFeatures;
+  
+  SampleAnalyzer() {
+    trackAnalysis = new HashMap<Integer, TrackAnalysis>();
+    sampleFeatures = new HashMap<String, SampleFeatures>();
+    
+    // Initialize with some known samples
+    initializeKnownSamples();
+  }
+  
+  void initializeKnownSamples() {
+    // Predefined features for common samples
+    
+    // Drums
+    sampleFeatures.put("bd", new SampleFeatures(120, 0.9, "kick", 0.1));
+    sampleFeatures.put("sn", new SampleFeatures(120, 0.7, "snare", 0.2));
+    sampleFeatures.put("hh", new SampleFeatures(120, 0.5, "hihat", 0.05));
+    sampleFeatures.put("cp", new SampleFeatures(120, 0.8, "clap", 0.15));
+    
+    // Breakbeats (longer durations)
+    SampleFeatures amenFeatures = new SampleFeatures(165, 0.95, "break", 0.5);
+    amenFeatures.complexity = 0.9;
+    sampleFeatures.put("jungle_breaks", amenFeatures);
+    
+    // From ParVagues examples
+    sampleFeatures.put("suns_keys", new SampleFeatures(120, 0.6, "melodic", 0.8));
+    sampleFeatures.put("suns_guitar", new SampleFeatures(120, 0.7, "melodic", 0.5));
+    sampleFeatures.put("suns_voice", new SampleFeatures(120, 0.4, "vocal", 1.0));
+    sampleFeatures.put("bassWarsaw", new SampleFeatures(120, 0.85, "bass", 0.3));
+    sampleFeatures.put("armora", new SampleFeatures(120, 0.6, "melodic", 0.4));
+    sampleFeatures.put("FMRhodes1", new SampleFeatures(120, 0.5, "melodic", 0.7));
+  }
+  
+  void processSample(int orbit, String sound, float gain, float delta) {
+    // Get or create track analysis
+    TrackAnalysis analysis = getTrackAnalysis(orbit);
+    
+    // Record hit time
+    float hitTime = millis() / 1000.0;
+    analysis.recordHit(hitTime, sound, gain, delta);
+    
+    // Extract sample name prefix (before ":" if present)
+    String samplePrefix = sound;
+    if (sound.contains(":")) {
+      samplePrefix = sound.substring(0, sound.indexOf(":"));
+    }
+    
+    // Check if we have features for this sample
+    if (!sampleFeatures.containsKey(samplePrefix)) {
+      // Try to extract features based on sample name and context
+      SampleFeatures features = extractFeatures(sound, delta, gain, analysis);
+      sampleFeatures.put(samplePrefix, features);
+    }
+  }
+  
+  SampleFeatures extractFeatures(String sound, float delta, float gain, TrackAnalysis analysis) {
+    // Attempt to extract sample features based on name and context
+    
+    // Start with defaults
+    float tempo = 120;
+    float energy = 0.7;
+    String role = "unknown";
+    float duration = 0.2;
+    
+    // Estimate tempo from delta (time between events)
+    if (delta > 0) {
+      tempo = 60.0 / delta;
+    } else if (analysis.getHitCount() > 1) {
+      tempo = analysis.estimateTempo();
+    }
+    
+    // Adjust tempo to a reasonable range
+    tempo = constrain(tempo, 60, 200);
+    
+    // Estimate energy from gain and sample name
+    energy = gain * 0.8; // Base energy on gain
+    
+    String lowerSound = sound.toLowerCase();
+    
+    // Estimate role from sample name
+    if (lowerSound.contains("bd") || lowerSound.contains("kick") || lowerSound.contains("bass drum")) {
+      role = "kick";
+      energy *= 1.2; // Kicks are typically high energy
+      duration = 0.1;
+    } 
+    else if (lowerSound.contains("sn") || lowerSound.contains("snare") || lowerSound.contains("cp") || lowerSound.contains("clap")) {
+      role = "snare";
+      energy *= 1.1;
+      duration = 0.15;
+    } 
+    else if (lowerSound.contains("hh") || lowerSound.contains("hat")) {
+      role = "hihat";
+      energy *= 0.9;
+      duration = 0.05;
+    } 
+    else if (lowerSound.contains("bass") || lowerSound.contains("sub") || lowerSound.contains("808")) {
+      role = "bass";
+      energy *= 1.1;
+      duration = 0.4;
+    } 
+    else if (lowerSound.contains("break") || lowerSound.contains("jungle") || lowerSound.contains("amen")) {
+      role = "break";
+      energy *= 1.2;
+      duration = 0.5;
+    } 
+    else if (lowerSound.contains("key") || lowerSound.contains("pad") || lowerSound.contains("chord") || lowerSound.contains("synth")) {
+      role = "melodic";
+      energy *= 0.8;
+      duration = 0.7;
+    } 
+    else if (lowerSound.contains("fx") || lowerSound.contains("riser") || lowerSound.contains("sweep")) {
+      role = "fx";
+      energy *= 0.9;
+      duration = 1.0;
+    } 
+    else if (lowerSound.contains("voc") || lowerSound.contains("voice")) {
+      role = "vocal";
+      energy *= 0.7;
+      duration = 0.8;
+    }
+    
+    // Constrain energy
+    energy = constrain(energy, 0.1, 1.0);
+    
+    return new SampleFeatures(tempo, energy, role, duration);
+  }
+  
+  TrackAnalysis getTrackAnalysis(int orbit) {
+    if (!trackAnalysis.containsKey(orbit)) {
+      trackAnalysis.put(orbit, new TrackAnalysis(orbit));
+    }
+    return trackAnalysis.get(orbit);
+  }
+  
+  SampleFeatures getFeaturesForOrbit(int orbit) {
+    TrackAnalysis analysis = getTrackAnalysis(orbit);
+    String lastSample = analysis.getLastSample();
+    
+    if (lastSample != null) {
+      // Extract sample name prefix
+      String samplePrefix = lastSample;
+      if (lastSample.contains(":")) {
+        samplePrefix = lastSample.substring(0, lastSample.indexOf(":"));
+      }
+      
+      // Get features
+      if (sampleFeatures.containsKey(samplePrefix)) {
+        return sampleFeatures.get(samplePrefix);
+      }
+    }
+    
+    return null;
+  }
+  
+  int getSampleCount() {
+    return sampleFeatures.size();
+  }
+}
+
+/**
+ * TrackAnalysis class
+ * 
+ * Analyzes the hit patterns for a specific track
+ */
+class TrackAnalysis {
+  int orbit;
+  ArrayList<HitInfo> hits;
+  String lastSample;
+  
+  // Pattern analysis
+  float[] intervalHistogram; // For tempo detection
+  float lastHitTime;
+  
+  TrackAnalysis(int orbit) {
+    this.orbit = orbit;
+    this.hits = new ArrayList<HitInfo>();
+    this.lastSample = null;
+    
+    // Initialize histogram (for intervals between 50ms and 2000ms)
+    intervalHistogram = new float[100]; // 20ms bins
+    lastHitTime = -1;
+  }
+  
+  void recordHit(float time, String sound, float gain, float delta) {
+    // Add hit info
+    hits.add(new HitInfo(time, sound, gain, delta));
+    
+    // Keep only recent hits (last 10 seconds)
+    while (hits.size() > 0 && time - hits.get(0).time > 10) {
+      hits.remove(0);
+    }
+    
+    // Update last sample
+    lastSample = sound;
+    
+    // Update interval histogram
+    if (lastHitTime > 0) {
+      float interval = time - lastHitTime;
+      if (interval >= 0.05 && interval <= 2.0) {
+        int bin = constrain(floor((interval - 0.05) * 50), 0, 99);
+        intervalHistogram[bin] += 1;
+      }
+    }
+    lastHitTime = time;
+  }
+  
+  float estimateTempo() {
+    // Find the most common interval
+    int maxBin = 0;
+    float maxValue = 0;
+    
+    for (int i = 0; i < intervalHistogram.length; i++) {
+      if (intervalHistogram[i] > maxValue) {
+        maxValue = intervalHistogram[i];
+        maxBin = i;
+      }
+    }
+    
+    // Convert bin to tempo
+    float interval = 0.05 + (maxBin / 50.0);
+    float tempo = 60.0 / interval;
+    
+    return tempo;
+  }
+  
+  int getHitCount() {
+    return hits.size();
+  }
+  
+  String getLastSample() {
+    return lastSample;
+  }
+  
+  // Inner class to store hit information
+  class HitInfo {
+    float time;
+    String sound;
+    float gain;
+    float delta;
+    
+    HitInfo(float time, String sound, float gain, float delta) {
+      this.time = time;
+      this.sound = sound;
+      this.gain = gain;
+      this.delta = delta;
+    }
+  }
+}
+
+/**
+ * SampleFeatures class
+ * 
+ * Stores detected features of a sample
+ */
+class SampleFeatures {
+  float tempo;      // Estimated tempo in BPM
+  float energy;     // 0-1 energy level
+  String role;      // Role of the sample (kick, snare, etc.)
+  float duration;   // Typical duration in seconds
+  float complexity; // 0-1 complexity level
+  
+  SampleFeatures(float tempo, float energy, String role, float duration) {
+    this.tempo = tempo;
+    this.energy = energy;
+    this.role = role;
+    this.duration = duration;
+    this.complexity = 0.5; // Default complexity
+  }
+}
diff --git a/viz/TrackManager.pde b/viz/TrackManager.pde
new file mode 100644
index 0000000..1fcfa11
--- /dev/null
+++ b/viz/TrackManager.pde
@@ -0,0 +1,640 @@
+/**
+ * TrackManager class
+ * 
+ * Handles the management and visualization of TidalCycles tracks (d1-d16)
+ * Each track has its own visual representation and effects
+ */
+class TrackManager {
+  ArrayList<Track> tracks;
+  ArrayList<SoundEvent> activeEvents;
+  
+  TrackManager() {
+    tracks = new ArrayList<Track>();
+    activeEvents = new ArrayList<SoundEvent>();
+    
+    // Initialize tracks for all possible orbits (0-15)
+    for (int i = 0; i < 16; i++) {
+      tracks.add(new Track(i));
+    }
+  }
+  
+  void update() {
+    // Update all tracks
+    for (Track track : tracks) {
+      track.update();
+    }
+    
+    // Update active events and remove completed ones
+    for (int i = activeEvents.size() - 1; i >= 0; i--) {
+      SoundEvent event = activeEvents.get(i);
+      event.update();
+      
+      if (event.isDone()) {
+        activeEvents.remove(i);
+      }
+    }
+  }
+  
+  void display() {
+    // Display all tracks
+    for (Track track : tracks) {
+      track.display();
+    }
+    
+    // Display all active events
+    for (SoundEvent event : activeEvents) {
+      event.display();
+    }
+  }
+  
+  void addEvent(int orbit, String sound, float gain, float pan, float delta) {
+    // Get appropriate track
+    Track track = tracks.get(orbit);
+    
+    // Update track state
+    track.onSound(sound, gain);
+    
+    // Create new event with appropriate visualization style based on orbit and sound
+    SoundEvent event = createEvent(orbit, sound, gain, pan, delta);
+    activeEvents.add(event);
+  }
+  
+  SoundEvent createEvent(int orbit, String sound, float gain, float pan, float delta) {
+    // Different visualization based on orbit (track number)
+    switch(orbit) {
+      case 0: // d1 - typically kick
+        return new KickEvent(orbit, sound, gain, pan, delta);
+      case 1: // d2 - typically snare
+        return new SnareEvent(orbit, sound, gain, pan, delta);
+      case 2: // d3 - typically hats or percussion
+        return new HihatEvent(orbit, sound, gain, pan, delta);
+      case 3: // d4 - typically bass
+        return new BassEvent(orbit, sound, gain, pan, delta);
+      default: // Other instruments
+        if (sound.contains("suns") || sound.contains("key")) {
+          return new MelodicEvent(orbit, sound, gain, pan, delta);
+        } else if (sound.contains("break") || sound.contains("jungle")) {
+          return new BreakEvent(orbit, sound, gain, pan, delta);
+        } else if (sound.contains("voice") || sound.contains("voc")) {
+          return new VoiceEvent(orbit, sound, gain, pan, delta);
+        } else if (sound.contains("riser") || sound.contains("fx")) {
+          return new FXEvent(orbit, sound, gain, pan, delta);
+        } else {
+          return new SoundEvent(orbit, sound, gain, pan, delta);
+        }
+    }
+  }
+  
+  int getActiveTrackCount() {
+    int count = 0;
+    for (Track track : tracks) {
+      if (track.isActive()) {
+        count++;
+      }
+    }
+    return count;
+  }
+  
+  
+  ArrayList<Integer> getActiveOrbits() {
+    ArrayList<Integer> activeOrbits = new ArrayList<Integer>();
+    
+    // Add orbits of all active tracks
+    for (int i = 0; i < tracks.size(); i++) {
+      if (tracks.get(i).isActive()) {
+        activeOrbits.add(i);
+      }
+    }
+    
+    return activeOrbits;
+  }
+
+  
+  void reset() {
+    activeEvents.clear();
+    for (Track track : tracks) {
+      track.reset();
+    }
+  }
+}
+
+/**
+ * Track class
+ * 
+ * Represents a single TidalCycles track (d1-d16)
+ * Maintains state and provides visual representation
+ */
+class Track {
+  int orbit;
+  color trackColor;
+  boolean active;
+  float activity;
+  float lastTriggerTime;
+  String lastSound;
+  ArrayList<Float> historyGain;
+  
+  // Visual properties
+  float baseHeight;
+  float targetHeight;
+  float currentHeight;
+  
+  Track(int orbit) {
+    this.orbit = orbit;
+    this.trackColor = orbitColors[orbit];
+    this.active = false;
+    this.activity = 0;
+    this.lastTriggerTime = -1000;
+    this.lastSound = "";
+    
+    this.historyGain = new ArrayList<Float>();
+    
+    // Visual initialization
+    this.baseHeight = height / 32.0;
+    this.targetHeight = baseHeight;
+    this.currentHeight = baseHeight;
+  }
+  
+  void update() {
+    // Decay activity over time
+    activity *= 0.95;
+    
+    // Update height with smooth animation
+    currentHeight = lerp(currentHeight, targetHeight, 0.2);
+    
+    // Reset target height if activity is low
+    if (activity < 0.1) {
+      targetHeight = baseHeight;
+      active = false;
+    }
+  }
+  
+  void display() {
+    if (activity < 0.05) return; // Don't display inactive tracks
+    
+    float yPos = map(orbit, 0, 15, height * 0.1, height * 0.9);
+    float trackWidth = width * 0.8;
+    float xOffset = width * 0.1;
+    
+    // Draw track background with trail effect
+    noStroke();
+    fill(red(trackColor), green(trackColor), blue(trackColor), activity * 150);
+    rect(xOffset, yPos - currentHeight/2, trackWidth, currentHeight, 5);
+    
+    // Draw glowing edge
+    stroke(trackColor, activity * 255);
+    strokeWeight(2);
+    noFill();
+    rect(xOffset, yPos - currentHeight/2, trackWidth, currentHeight, 5);
+    
+    // Add glow effect
+    drawGlow(xOffset, yPos, trackWidth, currentHeight);
+    
+    // Draw activity meter
+    float meterWidth = map(activity, 0, 1, 0, trackWidth);
+    noStroke();
+    fill(trackColor, activity * 200);
+    rect(xOffset, yPos - currentHeight/3, meterWidth, currentHeight/3, 5);
+  }
+  
+  void drawGlow(float x, float y, float w, float h) {
+    // Create glow effect using multiple transparent strokes
+    for (int i = 0; i < 5; i++) {
+      float alpha = map(i, 0, 4, activity * 100, 0);
+      stroke(red(trackColor), green(trackColor), blue(trackColor), alpha);
+      strokeWeight(i * 2 + 2);
+      noFill();
+      rect(x, y - currentHeight/2, w, currentHeight, 5);
+    }
+  }
+  
+  void onSound(String sound, float gain) {
+    // Update track state
+    active = true;
+    lastTriggerTime = millis();
+    lastSound = sound;
+    activity = 1.0;
+    
+    // Store gain history (for visual patterns)
+    historyGain.add(gain);
+    if (historyGain.size() > 16) {
+      historyGain.remove(0);
+    }
+    
+    // Update visual properties
+    targetHeight = baseHeight + (gain * baseHeight * 2);
+  }
+  
+  boolean isActive() {
+    return active;
+  }
+  
+  void reset() {
+    activity = 0;
+    historyGain.clear();
+    currentHeight = baseHeight;
+    targetHeight = baseHeight;
+    active = false;
+  }
+}
+
+/**
+ * SoundEvent class
+ * 
+ * Base class for visualizing individual sound events
+ */
+class SoundEvent {
+  int orbit;
+  String sound;
+  float gain;
+  float pan;
+  float delta;
+  float birthTime;
+  float lifespan;
+  color eventColor;
+  
+  // Visual properties
+  float size;
+  float alpha;
+  PVector position;
+  
+  SoundEvent(int orbit, String sound, float gain, float pan, float delta) {
+    this.orbit = orbit;
+    this.sound = sound;
+    this.gain = gain;
+    this.pan = pan;
+    this.delta = delta;
+    this.birthTime = millis();
+    this.lifespan = 500 + (gain * 500); // Duration based on gain
+    
+    // Initialize visuals
+    this.eventColor = orbitColors[orbit];
+    this.size = 20 + (gain * 60);
+    this.alpha = 255;
+    
+    // Position based on pan value
+    float xPos = map(pan, 0, 1, width * 0.3, width * 0.7);
+    float yPos = map(orbit, 0, 15, height * 0.2, height * 0.8);
+    this.position = new PVector(xPos, yPos);
+  }
+  
+  void update() {
+    // Calculate age
+    float age = millis() - birthTime;
+    
+    // Fade out as the event ages
+    alpha = map(age, 0, lifespan, 255, 0);
+    
+    // Grow size slightly over time
+    size = 20 + (gain * 60) * (1 + (age / lifespan) * 0.5);
+  }
+  
+  void display() {
+    if (alpha <= 0) return;
+    
+    // Draw the event
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    ellipse(position.x, position.y, size, size);
+    
+    // Add glow effect
+    drawGlow(position.x, position.y, size);
+  }
+  
+  void drawGlow(float x, float y, float s) {
+    for (int i = 0; i < 5; i++) {
+      float glowAlpha = map(i, 0, 4, alpha * 0.5, 0);
+      fill(red(eventColor), green(eventColor), blue(eventColor), glowAlpha);
+      noStroke();
+      ellipse(x, y, s + (i * 10), s + (i * 10));
+    }
+  }
+  
+  boolean isDone() {
+    return (millis() - birthTime) > lifespan;
+  }
+}
+
+/**
+ * Specialized sound event classes for different orbits/instruments
+ */
+class KickEvent extends SoundEvent {
+  KickEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 400 + (gain * 200); // Shorter for kicks
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    // Specialized kick visualization (more impactful)
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Draw shock wave effect
+    noFill();
+    stroke(red(eventColor), green(eventColor), blue(eventColor), alpha * (1-progress));
+    strokeWeight(3 * (1-progress));
+    ellipse(position.x, position.y, size * (1 + progress * 3), size * (1 + progress * 3));
+    
+    // Draw core
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    ellipse(position.x, position.y, size * (1-progress*0.5), size * (1-progress*0.5));
+  }
+}
+
+class SnareEvent extends SoundEvent {
+  ArrayList<PVector> particles;
+  
+  SnareEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 600; // Longer for snares
+    
+    // Create particles for snare effect
+    particles = new ArrayList<PVector>();
+    int particleCount = int(10 + (gain * 20));
+    
+    for (int i = 0; i < particleCount; i++) {
+      float angle = random(TWO_PI);
+      float speed = random(1, 5);
+      particles.add(new PVector(cos(angle) * speed, sin(angle) * speed));
+    }
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Draw particles
+    noStroke();
+    for (PVector p : particles) {
+      float x = position.x + (p.x * age * 0.1);
+      float y = position.y + (p.y * age * 0.1);
+      float particleSize = size * 0.2 * (1-progress);
+      
+      fill(red(eventColor), green(eventColor), blue(eventColor), alpha * 0.7);
+      ellipse(x, y, particleSize, particleSize);
+    }
+    
+    // Draw core
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    ellipse(position.x, position.y, size * (1-progress*0.7), size * (1-progress*0.7));
+  }
+}
+
+class HihatEvent extends SoundEvent {
+  HihatEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 300; // Very short for hihats
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Draw star-like shape
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    
+    pushMatrix();
+    translate(position.x, position.y);
+    rotate(progress * PI);
+    
+    beginShape();
+    for (int i = 0; i < 6; i++) {
+      float angle = i * TWO_PI / 6;
+      float x1 = cos(angle) * size * 0.5 * (1-progress*0.5);
+      float y1 = sin(angle) * size * 0.5 * (1-progress*0.5);
+      vertex(x1, y1);
+      
+      angle += TWO_PI / 12;
+      float x2 = cos(angle) * size * 0.2 * (1-progress*0.5);
+      float y2 = sin(angle) * size * 0.2 * (1-progress*0.5);
+      vertex(x2, y2);
+    }
+    endShape(CLOSE);
+    
+    popMatrix();
+  }
+}
+
+class BassEvent extends SoundEvent {
+  BassEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 800 + (gain * 400); // Longer for bass
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Draw ripple effect
+    for (int i = 0; i < 3; i++) {
+      float rippleProgress = (progress + (i * 0.2)) % 1.0;
+      float rippleSize = size * (0.5 + rippleProgress * 2);
+      float rippleAlpha = alpha * (1 - rippleProgress);
+      
+      noFill();
+      stroke(red(eventColor), green(eventColor), blue(eventColor), rippleAlpha);
+      strokeWeight(3 * (1-rippleProgress));
+      ellipse(position.x, position.y, rippleSize, rippleSize * 0.5); // Oval for bass
+    }
+    
+    // Draw core
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    ellipse(position.x, position.y, size * 0.8, size * 0.4);
+  }
+}
+
+class MelodicEvent extends SoundEvent {
+  float rotation;
+  
+  MelodicEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 1000 + (gain * 500);
+    rotation = random(TWO_PI);
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    pushMatrix();
+    translate(position.x, position.y);
+    rotate(rotation + (progress * TWO_PI * 0.5));
+    
+    // Draw geometric shape
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    
+    float sizeScale = 1 - (progress * 0.3);
+    
+    // Create polygon shape
+    beginShape();
+    int sides = 5;
+    for (int i = 0; i < sides; i++) {
+      float angle = i * TWO_PI / sides;
+      float x = cos(angle) * size * 0.5 * sizeScale;
+      float y = sin(angle) * size * 0.5 * sizeScale;
+      vertex(x, y);
+    }
+    endShape(CLOSE);
+    
+    // Add inner detail
+    fill(0, alpha * 0.5);
+    beginShape();
+    for (int i = 0; i < sides; i++) {
+      float angle = i * TWO_PI / sides;
+      float x = cos(angle) * size * 0.3 * sizeScale;
+      float y = sin(angle) * size * 0.3 * sizeScale;
+      vertex(x, y);
+    }
+    endShape(CLOSE);
+    
+    popMatrix();
+    
+    // Add glow
+    drawGlow(position.x, position.y, size * sizeScale);
+  }
+}
+
+class BreakEvent extends SoundEvent {
+  ArrayList<PVector> chunks;
+  
+  BreakEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 700;
+    
+    // Create chunks for break visualization
+    chunks = new ArrayList<PVector>();
+    int chunkCount = int(5 + (gain * 10));
+    
+    for (int i = 0; i < chunkCount; i++) {
+      float angle = random(TWO_PI);
+      float distance = random(size * 0.2, size * 0.6);
+      float chunkSize = random(size * 0.1, size * 0.3);
+      chunks.add(new PVector(cos(angle) * distance, sin(angle) * distance, chunkSize));
+    }
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Draw chunks
+    rectMode(CENTER);
+    for (PVector chunk : chunks) {
+      float x = position.x + (chunk.x * (0.5 + progress));
+      float y = position.y + (chunk.y * (0.5 + progress));
+      float chunkSize = chunk.z * (1 - progress * 0.5);
+      
+      fill(red(eventColor), green(eventColor), blue(eventColor), alpha * 0.8);
+      noStroke();
+      rect(x, y, chunkSize, chunkSize, 2);
+    }
+    rectMode(CORNER);
+    
+    // Draw center
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    ellipse(position.x, position.y, size * 0.4 * (1-progress), size * 0.4 * (1-progress));
+  }
+}
+
+class VoiceEvent extends SoundEvent {
+  VoiceEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 1200;
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Waveform visualization
+    stroke(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noFill();
+    strokeWeight(2);
+    
+    beginShape();
+    for (int i = 0; i < 20; i++) {
+      float x = position.x - (size/2) + (i * (size/20));
+      float waveHeight = sin(i * 0.5 + (millis() * 0.005)) * size * 0.2 * (1-progress*0.7);
+      float y = position.y + waveHeight;
+      vertex(x, y);
+    }
+    endShape();
+    
+    // Draw central point
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha);
+    noStroke();
+    ellipse(position.x, position.y, size * 0.2, size * 0.2);
+  }
+}
+
+class FXEvent extends SoundEvent {
+  FXEvent(int orbit, String sound, float gain, float pan, float delta) {
+    super(orbit, sound, gain, pan, delta);
+    lifespan = 1500;
+    size *= 1.5; // Larger for FX
+  }
+  
+  @Override
+  void display() {
+    if (alpha <= 0) return;
+    
+    float age = millis() - birthTime;
+    float progress = age / lifespan;
+    
+    // Lightning effect
+    stroke(red(eventColor), green(eventColor), blue(eventColor), alpha * (1-progress*0.5));
+    
+    // Draw multiple lightning bolts
+    for (int j = 0; j < 3; j++) {
+      float offsetX = random(-size/4, size/4);
+      float offsetY = random(-size/4, size/4);
+      
+      strokeWeight(3 * (1-progress));
+      noFill();
+      
+      beginShape();
+      vertex(position.x + offsetX, position.y - size/2);
+      
+      // Create jagged lines
+      int segments = 5;
+      for (int i = 1; i < segments; i++) {
+        float segmentY = position.y - size/2 + (i * size/segments);
+        float segmentX = position.x + offsetX + random(-size/4, size/4);
+        vertex(segmentX, segmentY);
+      }
+      
+      vertex(position.x + offsetX, position.y + size/2);
+      endShape();
+    }
+    
+    // Draw core
+    fill(red(eventColor), green(eventColor), blue(eventColor), alpha * 0.7);
+    noStroke();
+    ellipse(position.x, position.y, size * 0.3 * (1-progress*0.5), size * 0.3 * (1-progress*0.5));
+  }
+}
diff --git a/viz/launch.sh b/viz/launch.sh
new file mode 100644
index 0000000..2340437
--- /dev/null
+++ b/viz/launch.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Direct launcher script for ParVaguesViz
+# This uses what we've learned works on your system
+
+# Get the full path to the sketch directory
+SKETCH_DIR=$(cd "$(dirname "$0")" && pwd)
+
+# Colors for terminal output
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Print banner
+echo -e "${BLUE}"
+echo "██████╗  █████╗ ██████╗ ██╗   ██╗ █████╗  ██████╗ ██╗   ██╗███████╗███████╗"
+echo "██╔══██╗██╔══██╗██╔══██╗██║   ██║██╔══██╗██╔════╝ ██║   ██║██╔════╝██╔════╝"
+echo "██████╔╝███████║██████╔╝██║   ██║███████║██║  ███╗██║   ██║█████╗  ███████╗"
+echo "██╔═══╝ ██╔══██║██╔══██╗╚██╗ ██╔╝██╔══██║██║   ██║██║   ██║██╔══╝  ╚════██║"
+echo "██║     ██║  ██║██║  ██║ ╚████╔╝ ██║  ██║╚██████╔╝╚██████╔╝███████╗███████║"
+echo "╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝  ╚═══╝  ╚═╝  ╚═╝ ╚═════╝  ╚═════╝ ╚══════╝╚══════╝"
+echo -e "                     TIDALCYCLES VISUALIZER\n${NC}"
+
+# Set Java options to fix module restrictions
+export _JAVA_OPTIONS="--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED -Djava.awt.headless=false -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true"
+
+echo -e "${YELLOW}Starting ParVaguesViz...${NC}"
+echo -e "${BLUE}Controls:${NC}"
+echo "  D - Toggle debug info"
+echo "  H - Toggle help screen"
+echo "  G - Change grid style"
+echo "  F - Toggle fullscreen"
+echo "  R - Reset visualization"
+echo "  M - Toggle metadata display"
+echo
+
+# Launch Processing with the full path to the main sketch
+echo -e "${GREEN}Launching visualizer...${NC}"
+cd "$SKETCH_DIR"
+processing "$SKETCH_DIR/ParVaguesViz.pde"
diff --git a/viz/launcher/ParVaguesLauncher.pde b/viz/launcher/ParVaguesLauncher.pde
new file mode 100644
index 0000000..6698f28
--- /dev/null
+++ b/viz/launcher/ParVaguesLauncher.pde
@@ -0,0 +1,49 @@
+/**
+ * Launcher for ParVaguesViz
+ */
+
+void setup() {
+  size(400, 300);
+  background(0);
+  fill(255);
+  textAlign(CENTER, CENTER);
+  textSize(16);
+  text("ParVaguesViz Launcher\nPress any key to launch.", width/2, height/2);
+  
+  println("Launcher ready. Press any key to start ParVaguesViz...");
+}
+
+void draw() {
+  // Empty draw loop
+}
+
+void keyPressed() {
+  background(0);
+  text("Launching...", width/2, height/2);
+  
+  try {
+    // Get the path to the main sketch
+    String mainSketchPath = sketchPath(".."); 
+    println("Main sketch path: " + mainSketchPath);
+    
+    // Launch the main sketch with its full path
+    String[] args = new String[] { 
+      mainSketchPath + "/ParVaguesViz.pde"
+    };
+    
+    // Use Java's ProcessBuilder to launch the main sketch
+    ProcessBuilder pb = new ProcessBuilder("processing", args[0]);
+    pb.directory(new File(mainSketchPath));
+    pb.inheritIO(); // Forward output to our console
+    Process p = pb.start();
+    
+    background(0, 100, 0);
+    text("ParVaguesViz launched!\nYou can close this window.", width/2, height/2);
+  } 
+  catch (Exception e) {
+    background(100, 0, 0);
+    text("Error launching sketch.", width/2, height/2);
+    println("Error: " + e.getMessage());
+    e.printStackTrace();
+  }
+}
diff --git a/viz/launcher/sketch.properties b/viz/launcher/sketch.properties
new file mode 100644
index 0000000..e6735e8
--- /dev/null
+++ b/viz/launcher/sketch.properties
@@ -0,0 +1 @@
+main=ParVaguesLauncher.pde
diff --git a/viz/readme.md b/viz/readme.md
new file mode 100644
index 0000000..860c324
--- /dev/null
+++ b/viz/readme.md
@@ -0,0 +1,126 @@
+# ParVaguesViz - Cyberpunk TidalCycles Visualizer
+
+A cyberpunk-style visualization system for TidalCycles performances that works with your existing configuration without requiring any code changes.
+
+## Features
+
+- **Zero-configuration**: Works with standard TidalCycles/SuperDirt setup without modifying your patterns
+- **Automatic track detection**: Different visual styles for different orbits (d1-d16)
+- **Cyberpunk aesthetic**: Dark mode with neon colors, grids, and glitch effects
+- **Audio-reactive**: Visualizations respond to the rhythm and intensity of your music
+- **Customizable**: Multiple visualization styles and interactive controls
+
+## Requirements
+
+- [Processing](https://processing.org/) (v3.0+)
+- [oscP5 library](https://sojamo.de/libraries/oscP5/)
+- TidalCycles with SuperDirt running on the default port (57120)
+
+## Installation
+
+1. Download this repository to your `$WORK/Tidal/viz/` directory:
+
+   ```bash
+   mkdir -p $WORK/Tidal/viz
+   cd $WORK/Tidal/viz
+   # Copy all files here
+   ```
+
+2. Make sure you have Processing installed:
+
+   ```bash
+   # On Debian/Ubuntu
+   sudo apt-get install processing
+   
+   # Or download from processing.org and install manually
+   ```
+
+3. Install the oscP5 library through the Processing IDE:
+   - Open Processing
+   - Go to Sketch > Import Library > Add Library
+   - Search for "oscP5" and install it
+
+4. Make the run script executable:
+
+   ```bash
+   chmod +x run_visualizer.sh
+   ```
+
+## Usage
+
+1. Start TidalCycles and SuperDirt as usual without any modifications
+
+2. Run the visualizer:
+
+   ```bash
+   cd $WORK/Tidal/viz
+   ./run_visualizer.sh
+   ```
+
+3. Start creating music with TidalCycles! The visualizer will automatically detect and visualize your patterns.
+
+## Controls
+
+| Key | Action |
+|-----|--------|
+| D   | Toggle debug information display |
+| H   | Toggle help screen |
+| G   | Change grid style (standard, polar, hexagonal) |
+| F   | Toggle fullscreen mode |
+| R   | Reset all visualizations |
+
+## How It Works
+
+The visualizer listens for OSC messages from SuperDirt on port 57120. When TidalCycles sends messages to SuperDirt, the visualizer intercepts these messages and creates visual representations based on:
+
+- **Orbit number**: Determines the track type and color (d1-d16)
+- **Sound name**: Influences the visualization style
+- **Gain**: Controls size and intensity of visual elements
+- **Pan**: Determines horizontal position
+- **CPS**: Affects timing and rhythm of the visualization
+
+## Visualization Guide
+
+Different tracks have distinct visual styles:
+
+- **d1 (Orbit 0)**: Kicks - Impactful circular waves (Cyan)
+- **d2 (Orbit 1)**: Snares - Particle explosions (Magenta)
+- **d3 (Orbit 2)**: Hi-hats - Star-like shapes (Neon Green)
+- **d4 (Orbit 3)**: Bass - Rippling ovals (Orange)
+- **d5-d12**: Various instruments with appropriate visualizations
+
+Sample-specific visualizations are also applied:
+
+- **Keys/melodic**: Polygonal shapes
+- **Breaks/jungle**: Chunky fragmented patterns
+- **Voice samples**: Waveform-like visualizations
+- **FX/risers**: Lightning and glow effects
+
+## Customization
+
+All visual elements can be customized by editing the Processing files. 
+The main files are:
+
+- **ParVaguesViz.pde**: Main sketch with initialization and OSC handling
+- **TrackManager.pde**: Handles track visualization and sound events
+- **Background.pde**: Creates the cyberpunk background
+- **Grid.pde**: Manages the grid system with different styles
+- **ParticleSystem.pde**: Particle effects for additional visual interest
+- **GlitchEffect.pde**: Cyberpunk-style digital glitch effects
+
+## Troubleshooting
+
+- **No visualization**: Make sure SuperDirt is running on port 57120
+- **Processing not found**: Set the correct path to processing-java in run_visualizer.sh
+- **Missing oscP5 library**: Install it through the Processing IDE
+- **Performance issues**: Lower the window size in ParVaguesViz.pde or reduce particle count
+
+## License
+
+This project is provided as-is for the ParVagues project.
+
+## Acknowledgments
+
+- TidalCycles and SuperDirt for the amazing live coding environment
+- The Processing Foundation for their visualization tools
+- The oscP5 library for OSC communication
diff --git a/viz/sketch.properties b/viz/sketch.properties
new file mode 100644
index 0000000..15f0c5c
--- /dev/null
+++ b/viz/sketch.properties
@@ -0,0 +1 @@
+main=ParVaguesViz.pde