Commit 9a16f5db by PLN (Algolia)

feat: Viz

parent 615a27dd
......@@ -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 ""
/**
* 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;
}
}
/**
* 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();
}
}
}
/**
* 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;
}
}
/**
* 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;
}
}
/**
* 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();
}
}
/**
* 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;
}
}
/**
* 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;
}
}
/**
* 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;
}
}
/**
* 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();
}
/**
* 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
}
}
#!/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"
/**
* 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();
}
}
main=ParVaguesLauncher.pde
# 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
main=ParVaguesViz.pde
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment