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