Together with Wouter Jongeneel, we created a kaleidoscope that creates new versions of itself based on audio input. The goal was to create a generative visual that could run for hours on end at a party of ours.
After initial exploration, we came across a sketch by Jacob Joaquin which generates kaleidoscope-like figures using a random walker. We first made some minor adjustments to make the visuals a bit more fitting and attractive, after which we explored several ways of making the whole thing react to audio input. The minim-library was used for audio analysis within Processing.
CODE
Also available on GitHub.
/* Built upon Random Walker Kaleidoscope by Jacob Joaquin. SOURCE: https://www.openprocessing.org/sketch/135789 - Audio-Interaction added by Wouter Jongeneel and Ties Luiten. http://p-p-plus.tumblr.com/ http://www.tiesluiten.com/ - There are some minor adjustments plus added a simple way to include and analyze audio vol. as input. - For audio analysis we make use of the Minim library. SOURCE: http://code.compartmental.net/minim/audioinput_method_addlistener.html */ import processing.pdf.*; // Import Minim library for audio analysis. import ddf.minim.*; Minim minim; AudioInput in; // For amplitude analysis, we store the current amplitude in amp, // which we then add to our ampBuffer[] array to be able to calculate // the average value, which we later store in ampAvg. The size of the array, // defined by the number in float[], influences how directly the visual // reacts to amplitudal changes in the audio, i.e. beats, breaks, etc. float amp = 0; float ampAvg = 0; float ampBuffer [] = new float[10]; // The phase increments are used to get a back and forth motion - numerically wise they are random, // as almost everything in this code. So it actually is magic this time! // The first phase is for the colors, the second one for the vector's placement/growth float phasorInc = 1.0 / 500.0; float phasorInc2 = 1.0 / 90000.0; // nReflections sets the amount of reflections in the 'kaleidoscope' figure. int nReflections = 10; // nAngles sets the amount of possible directions the vector can grow towards. int nAngles = 3; // nPointsPerFrame sets amount of new points to calculated and drawn for each new frame. // Increase in points creates more dense figures, but also significantly drops performance. // Goes hand-in-hand with frameRate(), which is set in the setup(). int nPointsPerFrame = 300; // Sets boundary of the figure - set at beginning of setup() to match canvas height. // Used at the end of draw() for instance to draw a boundary ellipse. float rad = 540; // Active is used in draw() to check whether or not there is a figure on the canvas. int active = 0; Walker w; Phasor p; Phasor p2; float[] angles; // Set number of color palettes - actual colors set in setup(). Palette palette; Palette palette2; Palette palette3; Palette palette4; Palette palette5; // colorParam sets the number of the palette that's in use. int colorParam = 1; // counter increments with random(0,100) at end of draw(). // When counter>2222, a new colorParam is set using int(random(1,6)), // resulting in the use of a different palette and change of colors. int counter = 0; PVector getVCoordinates(PVector v, float d, float a) { // Because of the addition we have this "growing" movement. return new PVector(v.x + d * cos(a), v.y + d * sin(a)); } //Atan2 handles quadrant selection. float getAngleFromCenter(PVector v) { return atan2(v.y - height / 2, v.x - width / 2); } class Phasor { float inc; float phase; Phasor(float inc) { this.inc = inc; } void update() { phase += inc; //Phase from 1 to -1 and back while(abs(phase)>=1){ inc=inc*-1; phase += inc; } } } class Walker { PVector v; Walker(float x, float y) { v = new PVector(x, y); } //The name "gauss" indicates the randomness. void update() { float gauss = p2.phase * random(2,4); //gauss determines radius, angle is selected from array. v = getVCoordinates(v, gauss, angles[int(random(angles.length))]); /* Keep the scene bounded within a circle. If a point hits the border bring it back towards the center with a random offset. Later on we will draw a small stroke around the border, this cut-off contributes to the circulair appearance. */ float offset = rad*0.75; if((dist(v.x, v.y, width / 2, height / 2))>rad){ v.x = width/2+random(0,offset); v.y = height/2+random(0,offset); } float a = getAngleFromCenter(v); float d = dist(v.x, v.y, width / 2, height / 2); PVector center = new PVector(width / 2, height / 2); noStroke(); //Here comes the beauty, which is in the layering of the colors //which is possible due to a low opacity fill. if(colorParam == 1){ fill(palette.getNorm(abs(p.phase)), map(d, 0, width, 666*amp, 66)); } else if(colorParam == 2){ fill(palette2.getNorm(abs(p.phase)), map(d, 0, width, 666*amp, 66)); } else if(colorParam == 3){ fill(palette3.getNorm(abs(p.phase)), map(d, 0, width, 666*amp, 66)); } else if(colorParam == 4){ fill(palette4.getNorm(abs(p.phase)), map(d, 0, width, 666*amp, 66)); } else{ fill(palette5.getNorm(abs(p.phase)), map(d, 0, width, 666*amp, 66)); } //now use gauss for point thickness, can be done differenly of course. gauss = max(0.5, abs(gauss) / 2); for (int i = 0; i < nReflections; i++) { float thisAngle = a + (TWO_PI / (float) nReflections) * i; PVector thisV = getVCoordinates(center, d, thisAngle); ellipse(thisV.x, thisV.y, gauss, gauss); //mirror the point, thus actually add an extra reflection thisV = getVCoordinates(center, d, PI - thisAngle); ellipse(thisV.x, thisV.y, gauss, gauss); } } } // Cool way to set the colors class Palette { ArrayList<Integer> colors; Palette() { colors = new ArrayList<Integer>(); } void add(color c) { colors.add(c); } color getNorm(float p) { int index = (int) (p * colors.size()); color c1 = colors.get(index); color c2 = colors.get((index + 1) % colors.size()); //if we have a "high/very high" volume, we add a orange/red accent like in stained glass. if(amp>ampAvg*3.5){ color cr = color(225,0,38); color cb = color(0,0,0); float rand = random(0,1); return lerpColor(cr, cb, rand); } else if(amp>ampAvg*3){ color co = color(225,128,0); color cb = color(0,0,0); float rand = random(0,1); return lerpColor(co, cb, rand); } else{ return lerpColor(c1, c2, p * colors.size() - index); } } } void setup() { // Only with the P2D renderer we get the fullscreen unit to work. fullScreen(); // Framerate set to 30 for better performance. // Otherwise, framerate is auto-set to 55. frameRate(30); // Set background color. background(0); // Links boundary of the figure to the height of the canvas/screen. // Used at the end of draw() for instance to draw a boundary ellipse. rad = height/2; // Create the array of angles, simply 2*Pi/nAngles. angles = new float[nAngles]; for (int i = 0; i < angles.length; i++) { angles[i] = ((float) i / (float) nAngles) * TWO_PI; } // Set WALKER starting coordinates. w = new Walker(width / 2, height / 2); p = new Phasor(phasorInc); p2 = new Phasor(phasorInc2); // SET COLOR PALETTES ------------------------ // Color palettes source: // http://www.colourlovers.com/palette/ // Palette 1: palette = new Palette(); palette.add(color(4, 7, 28)); palette.add(color(9, 62, 112)); palette.add(color(136, 135, 217)); palette.add(color(199, 156, 0)); palette.add(color(255, 246, 79)); // Palette 2: palette2 = new Palette(); palette2.add(color(11, 17, 13)); palette2.add(color(44, 77, 86)); palette2.add(color(195, 170, 114)); palette2.add(color(220, 118, 18)); palette2.add(color(189, 50, 0)); // Palette 3: palette3 = new Palette(); palette3.add(color(146,179,38)); palette3.add(color(254, 234, 114)); palette3.add(color(37, 148, 132)); palette3.add(color(217, 255, 171)); palette3.add(color(246, 189, 76)); palette3.add(color(24,63,140)); // Palette 4: palette4 = new Palette(); palette4.add(color(255, 136, 231)); palette4.add(color(255, 245, 193)); palette4.add(color(252, 143, 110)); palette4.add(color(122, 11, 18)); palette4.add(color(123, 54, 98)); // Palette 5: palette5 = new Palette(); palette5.add(color(79, 156, 122)); palette5.add(color(137, 119, 193)); palette5.add(color(255, 255, 118)); palette5.add(color(244, 244, 244)); palette5.add(color(255, 40, 70)); // CREATE NEW AUDIO OBJECT ------------------- minim = new Minim(this); // DEFINE AUDIO INPUT ------------------------ // NOTE: Recording device cannot be selected through code, // but can only be changed through computer's // 'RECORDING DEVICES' settings. in = minim.getLineIn( Minim.MONO, 256); } void draw() { //Get the max amp from the buffer for(int i = 0; i < in.bufferSize() - 1; i++) { if ( abs(in.mix.get(i)) > amp ) { amp = abs(in.mix.get(i)); } } //Determine the average amplitude of the input audio //Using this is a better indication of changes in music then a fixed threshold ampAvg = amp/ampBuffer.length; for(int j=ampBuffer.length-1; j>0; j--){ ampBuffer[j]=ampBuffer[j-1]; ampAvg += (ampBuffer[j]/ampBuffer.length); } ampBuffer[0]=amp; //Randomly change the palette if(counter>2222){ colorParam = int(random(1,6)); counter = 0; } //Draw if the beat "changes" if(amp>1.00*ampAvg || (active == 1 && random(1,10)>2)){ active = 1; int lr = int(random(1,2)); for(int l=0; l<lr; l++){ for (int i = 0; i < nPointsPerFrame; i++) { p.update(); p2.update(); w.update(); } } } //slow down reaction time a bit to make it less intense else if(random(1,10)>0){ fill(0); ellipse(width/2,height/2,rad*2,rad*2); w.v.x=width/2+random(-rad*amp*5,rad*amp*5); w.v.y=height/2+random(-rad*amp*5,rad*amp*5); active = 0; } else{ active=0; } //In order to have a nice circular border(quickly), have a thin stroke. noFill(); stroke(0); strokeWeight(40); ellipse(width/2,height/2,rad*2,rad*2); amp=0; //Use counter for the colorchange counter+= int(random(0,100)); ampAvg=0; }