/** * 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)); } }