Starry Night

This is a music visualizer that simulates the starry night sky.

stars_prev

A music-loving friend of mine once told me he missed seeing the stars at night after coming to Pittsburgh. The idea for this project came as an idea for a present for that friend. I liked the idea of a portable, personal set of stars that could be charmed to life by playing music. The stars react to new notes being played, and the aurora appears at certain volume of music and duration of continuous music. (This may not seem very obvious in the video at the beginning because I wasn’t playing the notes hard enough. Also pardon my rustiness on piano – I haven’t really played in 2 years.)

The end product uses an Arduino Mega 2560, with an Electret Mic Amplifier for sound input, and loads of LEDs for display. Frequency analysis utilizes code from Adafruit’s Piccolo (https://github.com/adafruit/piccolo), which uses Elm-Chan’s FFT (Fast Fourier Transformation) library.

The creation of this project was a long and arduous process for me. My initial idea was to have a box filled with blue origami stars (http://fc04.deviantart.net/fs25/f/2008/072/f/e/Straw_Stars_by_Miraka.jpg), with white LEDs hidden inside white origami stars scattered around in the box. However, I quickly ran out of material for making the blue origami stars, and so replaced it with black cardstock and tissue paper. The end result of the stars adhere to my original idea in terms of visuals and functionality. The end product still has the white LEDs hidden inside white origami stars, and you just can’t tell clearly because they are now covered by black tissue paper. The white origami stars make the light of the white LEDs spread a little bit, and if you look carefully, the spread is in the shape of 5-pointed stars. I also wanted more white LED stars, but was limited by the number of PWM pins on the board (and later, space for the wires).

I also wanted to actually learn how to use the FFT library to implement more accurate frequency measurement, for picking out very roughly which notes are being played. It turned out that this is actually quite difficult due to harmonics, and it was hard to understand how to use the library partly due to poor documentation, so I ended up working with code from Adafruit for frequency analysis. A lot of testing was done to get it more suited for piano music. After getting the stars to work the way I wanted them to, I reflected on I could make it appear more interesting/visually appealing. The easy answer was “colors”, so I tried to implement something that appears similar to auroras. The source of the auroras are a number of LEDs. The ideal way to do this would be to use a LED strip (like this one http://www.adafruit.com/products/306), but since this was late into the project, I didn’t have time to get one.

Physically putting this together was also very hard and time-consuming. I had a lot of trouble getting the connections for all the LEDs to work. I had to basically tear my project apart several times because the conductive copper tape wasn’t effective for LEDs, or wires broke, or solder wasn’t strong enough, etc. In the end my breadboard had almost every single slot filled. Then more things fell apart as I was trying to get everything to fit inside a small box. I didn’t realize all those wires would take up so much space.

Weird, but useful tidbits I’ve learned about Arduino:
– variables with types that don’t match won’t raise an error while compiling, but would cause weird things when run
– error in uploading program to Mega board can sometimes be fixed by unplugging a few pins

In the end, I was fairly satisfied with the final product. The stars worked almost as well as I hoped they would. I just wish I was able to show off the craftsmanship that went into this project more. If I get up enough energy, I’d replace the RGB LEDs with an RGB strip. It would be difficult though, because I’d literally have to tear apart my project again, both physically and coding-wise. I enjoy watching it as someone else is playing the piano. Too bad I can’t really watch it while playing at the same time, since I have to watch the keyboard, haha.

[I just realized I accidentally named this the same as that famous van Gogh piece. Ugh. Need better naming skills.]

Code, if you’re interested. It’s messy and long and uncommented:

/* Starry Night -
a music visualizer that simulates the starry night sky.

Parts of the code are written by Adafruit Industries.  Distributed under the BSD license. 
See https://github.com/adafruit/piccolo for original source.

Additional code written by Jun Huo.
*/

#include 
#include 
#include 
#include 

#ifdef __AVR_ATmega32U4__
 #define ADC_CHANNEL 7
#else
 #define ADC_CHANNEL 0
#endif

int16_t       capture[FFT_N];    // Audio capture buffer
complex_t     bfly_buff[FFT_N];  // FFT "butterfly" buffer
uint16_t      spectrum[FFT_N/2]; // Spectrum output buffer
volatile byte samplePos = 0;     // Buffer position counter

byte
  peak[8],      // Peak level of each column; used for falling dots
  dotCount = 0, // Frame counter for delaying dot-falling speed
  colCount = 0, // Frame counter for storing past column data
  sparkCount = 0;
  
int
  col[8][10],   // Column levels for the prior 10 frames
  minLvlAvg[8], // For dynamic adjustment of low & high ends of graph,
  maxLvlAvg[8], // pseudo rolling averages for the prior few frames.
  colDiv[8];    // Used when filtering FFT output to 8 columns


PROGMEM uint8_t
  // This is low-level noise that's subtracted from each FFT output column:
  noise[64]={ 8,6,6,5,3,4,4,4,3,4,4,3,2,3,3,4,
              2,1,2,1,3,2,3,2,1,2,3,1,2,3,4,4,
              3,2,2,2,2,2,2,1,3,2,2,2,2,2,2,2,
              2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,4 },
  // These are scaling quotients for each FFT output column, sort of a
  // graphic EQ in reverse.  Most music is pretty heavy at the bass end.
  eq[64]={
    255, 175,218,225,220,198,147, 99, 68, 47, 33, 22, 14,  8,  4,  2,
      0,   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
      0,   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
      0,   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 },
  
  //New filter, tuned for general piano music
  //RED
  col0data[] = {  3,  1, 
     130, 190,  20}, 
  //RED/RED/GREEN
  col1data[] = {  5,  2,
           20, 160, 140, 20,  5},
  //RED/GREEN
  col2data[] = {  7,  4,
                     40, 170, 160,  80,  20,  10,
       5,   1},
  //GREEN
  col3data[] = {  9,  5,
                           5,  20,  90, 170, 120,  
      60,  20,  10,   2,   1},
  //GREEN/BLUE
  col4data[] = { 13,  7,
                                     4,  10,  60, 120, 
     160, 170, 160, 110,  60,  20,  10,   5,   3,   1},
  //BLUE
  col5data[] = { 19, 10,
       3,   8,  20,  50, 110, 150, 170, 180, 170, 140,
      90,  60,  40,  20,  10,   5,   2,   1,   1},
  //BLUE/RED
  col6data[] = { 23, 14,
                           1,   3,   8,  15,  35,  60,
     100, 120, 150, 170, 180, 185, 160, 130, 100,  50,
      20,  10,   5,   2,   1,   1,   1},
  //BLUE/RED/RED
  col7data[] = { 35, 18,
                                               1,   2,
       3,   5,  10,  20,  30,  60,  90, 120, 120, 160,
     180, 185, 165, 140, 120, 100,  80,  60,  45,  35,
      25,  20,  15,  10,   7,   5,   3,   2,   2,   1,
       1,   1,   1},
  // And then this points to the start of the data for each of the columns:
  *colData[] = {
    col0data, col1data, col2data, col3data,
    col4data, col5data, col6data, col7data };
    
    
int nPins = 8;
int ledPins[] = {6,7,8,9,10,11,12,13};
float brightness[] = {0,0,0,0,0,0,0,0};
float fadeFactor = 2.0;

boolean newSpark = false;

int rgb1[] = {44,45,46};
int rgb2[] = {3,4,5};
float color1[] = {0,0,0};
float color2[] = {0,0,0};

int vol[60];

int auroraWait = 0;
int waitThreshold = 0;
int auroraThreshold = 0;
int auroraCount = 0;

boolean playAurora = false;

void setup() {
  Serial.begin(9600);
  
  uint8_t i, j, nBins, binNum, *data;

  memset(peak, 0, sizeof(peak));
  memset(col , 0, sizeof(col));
  memset(brightness,0,sizeof(brightness));
  memset(vol, 0, sizeof(vol));

  for(i=0; i<8; i++) {
    minLvlAvg[i] = 0;
    maxLvlAvg[i] = 512;
    data         = (uint8_t *)pgm_read_word(&colData[i]);
    nBins        = pgm_read_byte(&data[0]) + 2;
    binNum       = pgm_read_byte(&data[1]);
    for(colDiv[i]=0, j=2; j complex #s
  samplePos = 0;                   // Reset sample counter
  ADCSRA |= _BV(ADIE);             // Resume sampling interrupt
  fft_execute(bfly_buff);          // Process complex data
  fft_output(bfly_buff, spectrum); // Complex -> spectrum

  // Remove noise and apply EQ levels
  for(x=0; x> 8);
  }

  int oldPeakSum = 0;
  int newPeakSum = 0;
  int highestPeakIndex = -1;
  int highestPeak = -1;
  // Downsample spectrum output to 8 columns:
  for(x=0; x<8; x++) {
    int oldPeak = peak[x];
    oldPeakSum+=oldPeak;
    
    data   = (uint8_t *)pgm_read_word(&colData[x]);
    nBins  = pgm_read_byte(&data[0]) + 2;
    binNum = pgm_read_byte(&data[1]);
    for(sum=0, i=2; i maxLvl) maxLvl = col[x][i];
    }
    
    if((maxLvl - minLvl) < 8) maxLvl = minLvl + 8;
    minLvlAvg[x] = (minLvlAvg[x] * 7 + minLvl) >> 3; // Dampen min/max levels
    maxLvlAvg[x] = (maxLvlAvg[x] * 7 + maxLvl) >> 3; // (fake rolling average)

    // Second fixed-point scale based on dynamic min/max levels:
    level = 10L * (col[x][colCount] - minLvlAvg[x]) /
      (long)(maxLvlAvg[x] - minLvlAvg[x]);

    // Clip output and convert to byte:
    if(level < 0L)      c = 0;
    else if(level >  8) c = 8; // Allow dot to go a couple pixels off top
    else                c = (uint8_t)level;

    if(c > peak[x]) peak[x] = c; // Keep dot on top

    y = 8 - peak[x];
    
    int newPeak = peak[x];
    newPeakSum+=newPeak;
    
    if (newPeak>0 && newPeak>highestPeak) {
      highestPeakIndex = x;
      highestPeak = newPeak;
    }
  }
  
  if (oldPeakSum< (newPeakSum-2) && newSpark==false) {
    newSpark = true;
  }
  
  int currVolSum = 0;
  for (int v=59; v>0; v--) {
    vol[v] = vol[v-1];
    currVolSum+=vol[v];
  }
  vol[0] = abs(512-int(ADC));
  currVolSum+=vol[0];
  
  int currVolAvg = currVolSum/60;
  
//  int currVolume = abs(512-int(ADC));
  Serial.println(currVolAvg);
  
  int newColor0 = 0, newColor1 = 0, newColor2 = 0;
  if (currVolAvg>64) {if (highestPeakIndex==0) {
    newColor0 = 80;
    newColor1 = 20;
    newColor2 = 20;
  } else if (highestPeakIndex==1) {
    newColor0 = 80;
    newColor1 = 60;
    newColor2 = 20;
  } else if (highestPeakIndex==2) {
    newColor0 = 70;
    newColor1 = 70;
    newColor2 = 20;
  } else if (highestPeakIndex==3) {
    newColor0 = 30;
    newColor1 = 100;
    newColor2 = 30;
  } else if (highestPeakIndex==4) {
    newColor0 = 20;
    newColor1 = 90;
    newColor2 = 90;
  } else if (highestPeakIndex==5) {
    newColor0 = 20;
    newColor1 = 60;
    newColor2 = 90;
  } else if (highestPeakIndex==6) {
    newColor0 = 20;
    newColor1 = 20;
    newColor2 = 100;
  } else {
    newColor0 = 60;
    newColor1 = 20;
    newColor2 = 90;
  }
}
  
  if (newSpark==true) {
    int led = random(nPins);
    float b = 255.0;
    brightness[led] = b; 
    newSpark = false;  
  }
  
  int starsLight = 0.0;
  for (int s=0; s0.0) auroraWait++;
  else auroraWait-=(waitThreshold/20);
  
  if (starsLight>0.0 && auroraWait>=waitThreshold){
    playAurora = true;
  } else {
    playAurora = false;
  }

  // Every third frame, make the peak pixels drop by 1:
  if(++dotCount >= 3) {
    dotCount = 0;
    for(x=0; x<8; x++) {
      if(peak[x] > 0) peak[x]--;
    }
  }
  
  if (++sparkCount>=50) {
    sparkCount = 0;
    if (newSpark==true) newSpark = false;
  }
  
  if (++auroraCount>=30) {
    auroraCount = 0;
    if (playAurora && newColor0+newColor1+newColor2>0) {
      color2[0] = color1[0];
      color2[1] = color1[1];
      color2[2] = color1[2];
      color1[0] = float(newColor0);
      color1[1] = float(newColor1);
      color1[2] = float(newColor2);
    }
  }
  
  analogWrite(rgb1[0],(color1[0]));
  analogWrite(rgb1[1],(color1[1]));
  analogWrite(rgb1[2],(color1[2]));
  if (color1[0]+color1[1]+color1[2]>0.0) {
    analogWrite(rgb2[0],color2[0]);
    analogWrite(rgb2[1],color2[1]);
    analogWrite(rgb2[2],color2[2]);
  }
  
  for (int p=0; p= 10) colCount = 0;
  
  for (int q=0; q<3; q++) {
    float newFade = float(fadeFactor)/5.0;
    float br1 = color1[q];
    br1 = (br1-newFade< =0) ?0: (br1-=newFade);
    color1[q] = br1;
    float br2 = color2[q];
    br2 = (br2-newFade<=0) ?0: (br2-=newFade);
    color2[q] = br2;
  }
}

ISR(ADC_vect) { // Audio-sampling interrupt
  static const int16_t noiseThreshold = 4;
  int16_t              sample         = ADC; // 0-1023

  capture[samplePos] =
    ((sample > (512-noiseThreshold)) &&
     (sample < (512+noiseThreshold))) ? 0 :
    sample - 512; // Sign-convert for FFT; -512 to +511

  if(++samplePos >= FFT_N) ADCSRA &= ~_BV(ADIE); // Buffer full, interrupt off
}

Project Proposals

[I apologize for the poor quality of the pictures.]

1st idea: Starry Audio Visualizer
I wanted to make a physical audio visualizer using Arduino and multiple LED lights. The LED lights would be stored inside semi-transparent hand-made stars, which would individually light up according to noise input.

Partially inspired by these:

This is a cube made up of LED lights controlled by arduino. It is an interesting visualizer for 3D motion and objects. It is simple but effective.


This is a music visualizer made using Processing. Its display is also 3D. The white on black background makes the string of dots seem to glow, which reminds me of the night sky.

Sketch:
My beautiful picture

2nd idea: continuing Kimpi (game)
.. With enemies to kill, and overhead map in corner to show the enemies positions.

Sketch:
My beautiful picture

I realized a big part of the concept is actually very similar to this game:
http://www.funland.com/cube-runner.html
Differences are that in this game, player is supposed to avoid obstacles, whereas Kimpi should kill enemies as well as avoid them. The movement of this game is also linear, but Kimpi should be able to go in circles, theoretically.

Pinwheel

Intended to use rotational speed on pinwheel to measure wind speed by converting angular velocity of the pinwheel to linear velocity. Looking back, I’m not sure how accurate this way of measuring wind speed is, considering properties of the pinwheel (such as friction).

Angular velocity is calculated by using a photocell to detect when a blade of the pinwheel passes over.

What it looks like:

My beautiful picture

A simple demo video that shows it working:

Fritzing board:
pinwheel_bb

Code:

#include 

#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h"

Adafruit_7segment matrix = Adafruit_7segment();

int historyLength = 100;
float sensorHistory[100];

float runningL;
float runningMaxL;
float runningMinL;

float threshhold;
boolean covered;

int speedHistLen = 400;
float speedHistory[400];

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  
  matrix.begin(0x70);
  
  for (int i=0; i0; i-- ){
    sensorHistory[i] = sensorHistory[i-1];
  }
  sensorHistory[0] = sensorValue;
  
  float currMax = -9999;
  float currMin = 9999;
  for (int i=0; icurrMax) { currMax = ith; }
    if (ith15.0) {
    if (!covered && currL< (runningL-threshhold*halfRange)) {
      //If sensor is NOT covered and current sensor value falls below threshhold,
      //then a blade is currently passing over
      updateLinearSpeed(0);
      covered = true;
    }
    else if (covered && currL>(runningL+threshhold*halfRange)) {
      //If sensor IS covered and current sensor value falls above threshhold,
      //then a blade just passed over, so update speed
      updateLinearSpeed(1);
      covered = false;
    }
    else {
      updateLinearSpeed(0);
    }
  }
  
  delay(1);
}


int nBlades = 8;
float radius = 0.085; //in meters


void updateLinearSpeed(int n) {
  int totalPasses = 0;
  for (int i=speedHistLen-1; i>0; i-- ){
    speedHistory[i] = speedHistory[i-1];
    totalPasses+=speedHistory[i];
  }
  speedHistory[0] = n;
  totalPasses+=n;
  
  float spinSpeed = 2.0*PI/nBlades*radius*((float)totalPasses)/((float)speedHistLen);
  Serial.println(spinSpeed);
  
  int displayed = (int)spinSpeed*100;
  
  int tens = displayed/1000;
  int ones = (displayed/100)%10;
  int tenths = (displayed/10)%10;
  int hundredths = displayed%10;
  
  matrix.writeDigitNum(0,tens,false);
  matrix.writeDigitNum(1,ones,true);
  matrix.drawColon(false);
  matrix.writeDigitNum(3,tenths,false);
  matrix.writeDigitNum(4,hundredths,false);
  
  matrix.writeDisplay();
  
}

Jun-Looking Outwards

Touch screen (Nintendo DSL digitizer)
http://www.adafruit.com/products/333

This would be great for user interactive projects. I’m thinking of games, here.

Geiger Counter Kit – Radiation Sensor
http://www.adafruit.com/products/483

Just learned about Geiger Counters in physics last week. I find being able to detect random radiation particles quite interesting. Not sure what I’d do with this yet, but it would be cool to play around with… if it weren’t so expensive.

RGB Color Sensor with IR filter
http://www.adafruit.com/products/1334

Having color input could be very useful. Since sight is arguably our strongest sense, I could imagine a nice program being made with color inputs from this sensor that reacts similarly to how humans would react to certain colors.

IR distance sensor includes cable (20cm-150cm)
http://www.adafruit.com/products/1031

Another sensor that might be good for user-interactive projects. Might be a good combination with RGB Color Sensor.

Charachromism

“Chara” is intended to be short for “character” or “characteristics”. “Chromism” is a chemistry term that stands for change in color.

This was intended to be a simulation of an over-simplified version of my observations of human group behavior.
The color of each Dot represents its core personality. In addition, each also has its own set of characteristics that influence how the Dot approaches with and is influenced by other Dots.

The simulation is not working as I intend it to yet. There are a few characteristics that I haven’t implemented yet. This is a work in progress. I would like to put the Processing program on here, but it does not run when I upload it onto OpenProcessing and I do not know why.

ArrayList allFlocks;
ArrayList allDots;
int nDots;
 
void setup() {
  size(400, 400);
  colorMode(HSB, 100);
  noStroke();
 
  nDots = 50;
 
  allFlocks = new ArrayList();
 
  allDots = new ArrayList();
  for (int i=0; i1.0) {
      ithDot.shade+=(shadeDiff*ithDot.agreeableness/100.0);
    }
 
    if (iFlock.stdev>(40.0*ithDot.openness)) {
      iFlock.removeDot(ithDot);
      if (iFlock.flockSize()==0) {
        allFlocks.remove(iFlock);
      }
      Flock fnew = new Flock();
      fnew.addDot(ithDot);
      ithDot.flock = fnew;
    }
    else {
      for (int j=0; j ithDot.radius && dh group; //prioritized members of group this Dot associates with
  //The following are the Dot's characteristics
  float agreeableness; //probability of going w/ flow of group
  float extraversion; //probability of associating with other Dots
  float openness; //probability of being interested in a different color
  float spontaneity; //probability of randomly going off somewhere
   
  Flock flock;
 
  Dot (float x, float y, float s, float rad) {
    position = new PVector(x, y);
 
    float angle = random(TWO_PI);
    velocity = new PVector(cos(angle), sin(angle));
     
    acceleration = new PVector(0, 0);
 
    shade = s;
     
    radius = rad;
 
    maxspeed = 1.0;
    maxforce = 0.01;
 
    group = new ArrayList();
    flock = new Flock();
    flock.addDot(this);
 
    agreeableness = random(0, 1.0);
    extraversion = random(0, 1.0);
    openness = random(0, 1.0);
    spontaneity = random(0, 1.0);
     
//    println(str(shade)+"   "+str(openness));
  }
 
  float checkCompatibility (float s) {
    float lower = min(s, shade);
    float higher = max(s, shade);
    float shadeDist = min(higher-lower,(100-higher)+lower);
    float distRatio = abs(shadeDist)/100;
    return distRatio;
  }
   
  void updateFlock(Flock f) {
    flock = f;
  }
 
  void run(ArrayList dots) {
    flock(dots);
    update();
    handleBoundaries();
    render();
  }
 
  void handleBoundaries() {
    if (position.x > width+radius ) position.x -= width;
    if (position.x < 0-radius     ) position.x += width;
    if (position.y > height+radius) position.y -= height;
    if (position.y < 0-radius     ) position.y += height;
  }
 
  void applyForce (PVector force) {
    acceleration.add(force);
  }
 
  //From Flocking by Daniel Shiffman
  //http://processing.org/examples/flocking.html
  void flock(ArrayList dots) {
    PVector sep = separate(allDots);
    PVector ali = align(dots);
    PVector coh = cohesion(dots);
    sep.mult(1.5);
    coh.mult(1.0);
    ali.mult(1.0);
 
    applyForce(sep);
    applyForce(coh);
    applyForce(ali);
  }
 
  // Method to update position
  void update() {
    // Update velocity
    velocity.add(acceleration);
    // Limit speed
    velocity.limit(maxspeed);
    position.add(velocity);
    // Reset accelertion to 0 each cycle
    acceleration.mult(0);
  }
 
  // A method that calculates and applies a steering force towards a target
  // STEER = DESIRED MINUS VELOCITY
  PVector seek(PVector target) {
    PVector desired = PVector.sub(target, position);  // A vector pointing from the position to the target
    // Scale to maximum speed
    desired.normalize();
    desired.mult(maxspeed);
 
    // Steering = Desired minus Velocity
    PVector steer = PVector.sub(desired, velocity);
    steer.limit(maxforce);  // Limit to maximum steering force
    return steer;
  }
 
  // Separation
  // Method checks for nearby dots and steers away
  PVector separate (ArrayList dots) {
    float desiredseparation = 15.0f;
    PVector steer = new PVector(0, 0, 0);
    int count = 0;
    // For every dot in the system, check if it's too close
    for (Dot other : dots) {
      float d = PVector.dist(position, other.position);
      // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
      if ((d > 0) && (d < desiredseparation)) {
        // Calculate vector pointing away from neighbor
        PVector diff = PVector.sub(position, other.position);
        diff.normalize();
        diff.div(d);        // Weight by distance
        steer.add(diff);
        count++;            // Keep track of how many
      }
    }
    // Average -- divide by how many
    if (count > 0) {
      steer.div((float)count);
    }
 
    // As long as the vector is greater than 0
    if (steer.mag() > 0) {
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // steer.setMag(maxspeed);
 
      // Implement Reynolds: Steering = Desired - Velocity
      steer.normalize();
      steer.mult(maxspeed);
      steer.sub(velocity);
      steer.limit(maxforce);
    }
    return steer;
  }
 
  // Alignment
  // For every nearby dot in the system, calculate the average velocity
  PVector align (ArrayList dots) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);
    int count = 0;
    for (Dot other : dots) {
      float d = PVector.dist(position, other.position);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.velocity);
        count++;
      }
    }
    if (count > 0) {
      sum.div((float)count);
      // First two lines of code below could be condensed with new PVector setMag() method
      // Not using this method until Processing.js catches up
      // sum.setMag(maxspeed);
 
      // Implement Reynolds: Steering = Desired - Velocity
      sum.normalize();
      sum.mult(maxspeed);
      PVector steer = PVector.sub(sum, velocity);
      steer.limit(maxforce);
      return steer;
    }
    else {
      return new PVector(0, 0);
    }
  }
 
  // Cohesion
  // For the average position (i.e. center) of all nearby dots, calculate steering vector towards that position
  PVector cohesion (ArrayList dots) {
    float neighbordist = 50;
    PVector sum = new PVector(0, 0);   // Start with empty vector to accumulate all positions
    int count = 0;
    for (Dot other : dots) {
      float d = PVector.dist(position, other.position);
      if ((d > 0) && (d < neighbordist)) {
        sum.add(other.position); // Add position
        count++;
      }
    }
    if (count > 0) {
      sum.div(count);
      return seek(sum);  // Steer towards the position
    }
    else {
      return new PVector(0, 0);
    }
  }
 
  void render() {
    fill(shade,100,100);
    ellipse(position.x, position.y, radius, radius);
  }
}
 
 
class Flock {
  ArrayList dots;
  float avgShade;
  float pcx;
  float pcy;
  float stdev;
   
  Flock() {
    dots = new ArrayList();
    avgShade = 0;
    pcx = 0;
    pcy = 0;
    stdev = 0;
    allFlocks.add(this);
  }
   
  void updateInfo(){
    float newx = 0;
    float newy = 0;
    int n = dots.size();
    float tempsum = 0;
    float totalShade = avgShade*n;
    for (Dot d: dots) {
      PVector pos = d.position;
      newx+=pos.x;
      newy+=pos.y;
       
      tempsum+=pow((totalShade-d.shade),2);
    }
     
    pcx = newx/dots.size();
    pcy = newy/dots.size();
     
    stdev = sqrt(tempsum/n);
  }
   
  void addDot(Dot d) {
    //Using previous numbers to calculate new avgs for runtime
    int n0 = dots.size();
    float totShade0 = avgShade*n0;
    int n = n0+1;
    float totShade = totShade0+d.shade;
    //Update the avg
    avgShade = totShade/n;
     
    d.updateFlock(this);
    dots.add(d);
  }
   
  void removeDot(Dot d) {
    //Using previous numbers to calculate new avgs for runtime
    int n0 = dots.size();
    float totShade0 = avgShade*n0;
    int n = n0-1;
    float totShade = totShade0-d.shade;
    //Update the avg
    avgShade = totShade/n;
     
    dots.remove(d);
  }
   
  int flockSize() {
    return dots.size();
  }
   
  void run() {
    for (Dot d: dots) {
      d.run(dots);
    }
  }
}

Vortex

Jun’s thought process for this project:
Need something creative to project on -> ceiling lamp in room with dome shaped shades look cool -> need to find place on campus with similar lamp -> Porter has ceiling fans, ceiling fans are objects on ceiling too -> ceiling fans spin, so need to make something that plays on the spinning motion of the fan

And I ended up with something like this.

The final product did end up looking more like a planetary system than I intended to. This is just because the initial velocity of the particles are set to be tangent to the circles, and the gravity toward the center makes the particles orbit. I also didn’t foresee the projection would be projected onto the ceiling too, but I think in a way that made it look cooler. This is a projection that can’t live without the place it is projected on. AKA, if you run the program on your computer, it looks very simple, to the point of being boring/dull. However, once it is projected onto the fan, the motion of the fan makes the projection seem mesmerizing.

Code (non-Keystone version):

import pbox2d.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.collision.shapes.Shape;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;
import org.jbox2d.dynamics.contacts.*;

PBox2D box2d;

ArrayList allParticles;

Surface surface;

boolean showOutline;
boolean showCircles;
float outerR;
float innerR;

void setup() {
  size (600, 600);
  showOutline = true;
  showCircles = true;
  outerR = 500;
  innerR = 80;

  smooth();

  box2d = new PBox2D(this);
  box2d.createWorld();
  //  box2d.setGravity(width/2, -height/2);

  allParticles = new ArrayList();
  surface = new Surface(innerR/2);
}

void draw() {
  background(0);

  float make = random(0, 1.0);
  if (make<0.1) {
    float angle = random(0, TWO_PI);
    float x = width/2+cos(angle)*outerR/2;
    float y = height/2+sin(angle)*outerR/2;
    Particle p = new Particle(x, y, 5, angle);
    allParticles.add(p);
  }

  if (keyPressed) {
    if (key=='r' || key=='R') {
      for (Particle p: allParticles) {
        p.done();
      }
      allParticles.clear();
    }
    else if (key=='c' || key=='C') {
      if (showCircles) showCircles = false;
      else showCircles = true;
    }
    else if (key=='o' || key=='O') {
      if (showOutline) showOutline = false;
      else showOutline = true;
    }
    else if (key==CODED) {
      if (keyCode==UP) {
        innerR+=2;
      }
      else if (keyCode==DOWN) {
        innerR-=2;
      }
      else if (keyCode==LEFT) {
        outerR-=2;
      }
      else if (keyCode==RIGHT) {
        outerR+=2;
      }
    }
  }

  if (showCircles) {
    float r;  
    noFill();

    stroke(255);
    strokeWeight(3);
    r = innerR+0.95*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(245);
    strokeWeight(2.5);
    r = innerR+0.90*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(225);
    strokeWeight(2.2);
    r = innerR+0.82*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(195);
    strokeWeight(1.5);
    r = innerR+0.70*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(155);
    strokeWeight(1.0);
    r = innerR+0.55*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(105);
    strokeWeight(1.0);
    r = innerR+0.40*(outerR-innerR);
    ellipse(width/2, height/2, r, r);

    stroke(45);
    strokeWeight(0.8);
    r = innerR+0.20*(outerR-innerR);
    ellipse(width/2, height/2, r, r);
  }

  if (showOutline) {
    surface.display();

    noFill();
    stroke(0, 255, 0);
    strokeWeight(1);
    ellipse(width/2, height/2, outerR, outerR);
  }

  box2d.step();

  for (Particle p: allParticles) {
    Vec2 pos = new Vec2();
    pos = box2d.getBodyPixelCoord(p.body);
    //Find distance from center
    float dx = width/2-pos.x;
    float dy = -height/2+pos.y;
    Vec2 grav = new Vec2(dx, dy);
    float dfc = sqrt(dx*dx+dy*dy);
    float r0 = p.r0;
    float newR = map(dfc, 0, outerR/2, r0/5, r0);
    p.applyForce(grav);
    //    p.r = newR;
    if (dfc height+r*2) {
      killBody();
      return true;
    }
    return false;
  }

  // 
  void display() {
    // We look at each body and get its screen position
    Vec2 pos = box2d.getBodyPixelCoord(body);
    // Get its angle of rotation
    float a = body.getAngle();
    pushMatrix();
    translate(pos.x,pos.y);
    rotate(-a);
    fill(100,140,200);
    noStroke();
    strokeWeight(1);
    ellipse(0,0,r*2,r*2);
    popMatrix();
  }

  // Here's our function that adds the particle to the Box2D world
  void makeBody(float x, float y, float r) {
    // Define a body
    BodyDef bd = new BodyDef();
    // Set its position
    bd.position = box2d.coordPixelsToWorld(x,y);
    bd.type = BodyType.DYNAMIC;
    bd.linearDamping = 0.1f;
    body = box2d.world.createBody(bd);

    // Make the body's shape a circle
    CircleShape cs = new CircleShape();
    cs.m_radius = box2d.scalarPixelsToWorld(r);
    
    FixtureDef fd = new FixtureDef();
    fd.shape = cs;
    // Parameters that affect physics
    fd.density = 1;
    fd.friction = 0.01;
    fd.restitution = 0.3;
    
    // Attach fixture to body
    body.createFixture(fd);

    // Give it a random initial velocity (and angular velocity)
    body.setLinearVelocity(new Vec2((50*cos(PI/2-a0)),(50*sin(PI/2-a0))));
    body.setAngularVelocity(0);
  }
  
  void applyForce(Vec2 force) {
    Vec2 pos = body.getWorldCenter();
    body.applyForce(force, pos);
  }
}

class Surface {
  Body body;
  float r;
  
  Surface(float r_) {
    r = r_;
    makeBody(width/2,height/2,r);
    body.setUserData(this);
  }
  
  void display() {
    // We look at each body and get its screen position
    Vec2 pos = box2d.getBodyPixelCoord(body);
    pushMatrix();
    noFill();
    stroke(0,255,0);
    strokeWeight(1);
    ellipse(width/2, height/2, r*2, r*2);
    popMatrix();
  }
  
  // Here's our function that adds the particle to the Box2D world
  void makeBody(float x, float y, float r) {
    // Define a body
    BodyDef bd = new BodyDef();
    // Set its position
    bd.position = box2d.coordPixelsToWorld(x, y);
    bd.type = BodyType.STATIC;
    body = box2d.createBody(bd);

    // Make the body's shape a circle
    CircleShape cs = new CircleShape();
    cs.m_radius = box2d.scalarPixelsToWorld(r);

    FixtureDef fd = new FixtureDef();
    fd.shape = cs;

    // Attach fixture to body
    body.createFixture(fd);
  }
} 

Jun-Looking Outwards 3

void ()

It took me a little while to realize that the projection is under water, and the black particles that travel up are bubbles. Apparently, the bubbles are created according to frequency of sound, and the movement of the bubbles are then caught by a sensor and are used to create a real-time visual projection onto the bubble wall. All elements come together to create an audiovisual experience. I find the movements of the bubbles and projection quite calming, and projection in water is kind of unique. I’m not sure why the project is named “void ()”, unless it’s the default video title if no titles are entered? But it sounds cool.

atOms – Ryo Kishi

This project is fairly different from the art projects I usually focus on. He uses air flow to suspend balls in air, and then used the rotations of the air blowers controlled by Arduino to choreograph some kind of “dance”. I find the idea and the physics quite ingenious. It seems like he is still working on this. It’d be cool to see some fancier choreography, such as incorporating varying air flow force if it’s possible, so the balls would suspend at different heights.

Pixelate

Pixelate – Guitar-Hero-style eating game which detects food you are eating


A very creative outtake on an old concept. This project turns eating into competitive gaming. The fork uses the difference in various fruits’ current resistance to figure out which food the player is picking up. Arduino is built into the table that shows the players what food they’re supposed to eat. The presentation of the game looks like 8-bit old-school, an homage to the old pixelated games. I’m not sure how they’re generating the food order, but I could see a potential problem with running out of a certain food on the plate. Also, although a purpose of this project is to encourage healthy eating, making eating competitive could make competitive players rush while eating (maybe not chew thoroughly before swallowing), which isn’t healthy. Anyway, I bet they had a really good time during the development of this project. Yum.

Perception – Volvoxlabs

‘Perception of Consequence’ by Volvoxlabs


[It seems like the main focus of this project, the animation, has little to do with Arduino. The Arduino use is in the real room viewing of the video, though I’m not exactly sure what… fans, maybe? But man, the animation looked cool.] Watching this video made me feel uncomfortable in my stomach and feel captivated at the same time. The general gist looks like it’s two blobs of milk in vague shapes of beasts fighting against each other (it’s supposed to simulate human states and emotions). The animation is projected on a fabric on a custom shaped structure to make it seem more “organic”.

Snowflake-like

http://cmuems.com/2013/a/wp-content/uploads/sites/2/2013/10/laser_cut1.pdf

Was supposed to go for shattered-glass look, but shattered glass has a lot more randomness… So it turned out looking like a spider web. Heh. Had quite a bit of trouble not getting the lines to overlap, which made adding randomness a lot harder.

Updated to draw shapes using only black lines (no whites), and made the border of the shapes proportional to closeness to center. Had to review some trig to do this. Now it looks like a snowflake people can cut out just from paper. Eh..

import processing.pdf.*;

ArrayList< Arraylist > allVeins;

float defaultDP; //default percentage of length of vein
float currDP;
int defaultV; //base # of veins
int currV;
boolean recording;

void setup() {
  size(864, 864);
  background(255);
  recording = false;

  allVeins = new ArrayList< Arraylist >();

  defaultDP = 1.0;
  defaultV = 24;
  currDP = defaultDP;
  currV = defaultV;

  setNewVeins(currV);
}

void setNewVeins(int n) {
  allVeins.clear();
  setLvl(n, 1.0);
}

void setLvl(int n, float percent) {
  ArrayList veins = new ArrayList< Vein>();

  for (int i=0; i lvl = allVeins.get(0);
  int n = lvl.size();

  beginShape();
  for (int i=0; i lvl0 = allVeins.get(j);
  int n = lvl0.size();
  float da = 0.07;
  float dd = 0.2;

  ArrayList lvl1 = allVeins.get(j+1);

  for (int i=0; i

Kimpi


(The video was laggy because my computer was having trouble handling Processing, FaceOSC, and video capture at the same time. Welp.)

I bit off a bit more than I can chew with trying to do things in 3D. I thought it would be cool to make a 3D game controlled by head movements that makes use of 3-D information provided by FaceOSC. The idea is to have a critter run on a grid and fire beams from its mouth to destroy obstacles. Properties used: face rotation, mouth height. The design of Kimpi (the critter) is supposed to include more complicated patterns, but I haven’t quite figured out how to draw them on correctly yet (too much math and pixel-manipulating). After making the general movements work, I realized that my visions of this game doesn’t really fit using FaceOSC very well – FaceOSC loses track of face easily, especially when the face is turned too much, so it is not fit for a slightly fast-paced game I wanted.

Task queue if/when time/interest allows/persists. As I was typing this, I realized this is way too ambitious even if I duplicated myself for the sole completion of this task:
– make Kimpi bounce up/down instead of glide (shouldn’t be hard)
– draw designs on Kimpi
– make keyboard controls
– include board tilt – Kimpi slides rapidly to one side
– include obstacles that can be destroyed when beam lands on them
– include enemies that actively attack Kimpi (flocking?) that can be destroyed by beam

Code for Kimpi Demo, Kimpi outward of screen and mirrors head movements. Made for testing Kimpi object class:

import oscP5.*;
OscP5 oscP5;

int     found;
PVector poseOrientation = new PVector();
float mouthHeight;

Kimpi kimpi = new Kimpi(50);

void setup() {
  size(400, 400, OPENGL, P3D);
  background(200);
  oscP5 = new OscP5(this, 8338);
  oscP5.plug(this, "found", "/found");
  oscP5.plug(this, "poseOrientation", "/pose/orientation");
  oscP5.plug(this, "mouthHeightReceived", "/gesture/mouth/height");

}
  
void draw() {
  background(200);
  translate(width/2, height/2, 0);
  spotLight(255, 255, 255, width/2, height/4, height*2, 0, 0, -1, PI/4, 2);

  if (found>0) {
    rotateY (poseOrientation.y); 
    rotateX (0-poseOrientation.x); 
    rotateZ (0-poseOrientation.z);
    println(mouthHeight);
  }


  kimpi.update(mouthHeight);
  kimpi.drawKimpi();

}


//----------------------------------
public void found (int i) { 
  found = i; 
}
public void poseOrientation(float x, float y, float z) {
  poseOrientation.set(x, y, z);
}
public void mouthHeightReceived(float h) {
  println("mouth height: " + h);
  mouthHeight = h;
}

class Kimpi{
  float radius;
  float headpieceW;
  float headpieceL;
  float mouthR = 0;
  float mouthRmax = 6;
  float eyeH;
  PFont f = createFont("Courier New Bold",16,true);
  
  Kimpi(float r){
    radius = r;
    headpieceW = radius/2;
    headpieceL = 5*headpieceW;
    eyeH = radius/12;
  }
  
  void update(float r){
    if (r<0) {
      mouthR = 0;
    }
    else if (r>mouthRmax) {
      mouthR = mouthRmax;
    }
    else {
      mouthR = r;
    }
  }

  void drawKimpi(){
    noStroke();
    fill(255,255,255);
    sphere(radius);
    
    float hx = 0;
    float hy = -radius;
    float hz = 0;
    
    pushMatrix();
    noFill();
    stroke(0,0,255,200);
    int nLines = 30;
    for (int i=0; i0) {
      rotateX(-PI/6);
      stroke(100);
      strokeWeight(3);
      noFill();
      beginShape();
      float theta = map((mouthR-1)/mouthRmax,0,mouthRmax,0,PI);
      for (int i=0; i< =20; i++){
        float phi = map(i,0,20,0,TWO_PI);
        float x = radius * sin(theta) * cos(phi); 
        float y = radius * sin(theta) * sin(phi); 
        float z = radius * cos(theta/2);
        vertex(x, y, z);
      }
      endShape();
      
      if ((mouthR-2>0)){
        float temp;
        if (mouthR-2<0) {
          temp = 0;
        } else {
          temp = mouthR-2;
        }
        stroke(200,220,255,200);
        float theta1 = map(temp/mouthRmax,0,mouthRmax,0,PI);        
        for (int i=0; i< =20; i++){
          float phi1 = map(i,0,20,0,TWO_PI);
          float x1 = radius * sin(theta1) * cos(phi1); 
          float y1 = radius * sin(theta1) * sin(phi1); 
          float z1 = radius * cos(theta1/2);
        
          line(x1,y1,z1,x1,y1-height/6,z1+height);
        }
      }
    }
    popMatrix();
  }
}

Code for Kimpi Beam, Kimpi faces into screen and travels on a grid:

import oscP5.*;
OscP5 oscP5;

int found;
PVector poseOrientation = new PVector();
float mouthHeight;

Kimpi kimpi = new Kimpi(20);

float unitSize = 30;
int nFrames0 = 50;
int nFrames = nFrames0;
float VX;
float VZ;
float maxV = 2;

Grid floor;

void setup() {
  size(800, 400, P3D);
  background(255,255,255);
  //  colorMode(HSB, 100);  
  floor = new Grid(width/2,height/5);
  
  oscP5 = new OscP5(this, 8338);
  oscP5.plug(this, "found", "/found");
  oscP5.plug(this, "poseOrientation", "/pose/orientation");
  oscP5.plug(this, "mouthHeightReceived", "/gesture/mouth/height");
  
  VX = sin(0)*maxV;
  VZ = cos(0)*maxV;
}

void draw() {
  background(255,255,255);
  
  float percentComplete = (float)(frameCount%nFrames)/ (float)nFrames;
  float runPercent = percentComplete;

  pushMatrix();
  translate(width/2,height/2,0);
  rotateX(PI/2.2);
  if (found>0) {
    VX = -sin(poseOrientation.y)*maxV;
    VZ = cos(poseOrientation.y)*maxV;
  }
  else {
    VX = sin(0)*maxV;
    VZ = cos(0)*maxV;
  }
  floor.drawGrid(percentComplete);
  popMatrix();
  
  
  pushMatrix();
  noStroke();
  fill(255,255,255);
  translate(width/2, 8.3*height/16, height/2);
  spotLight(255, 255, 255, width/2, height/4, height*2, 0, 0, -1, PI/4, 2);
  scale(1,1,-1);
  rotateX(PI/6);
  if (found>0) rotateY(poseOrientation.y);
  kimpi.update(mouthHeight);
  kimpi.drawKimpi();
  popMatrix();

}


class Grid {
  float left;
  float top;
  float currTX = 0;
  float currTZ = VZ;
  int dimension = int(2*width/unitSize);

  Grid(float cx, float cy) {
    left = cx-3*width/2;
    top = cy-height/1.5;
  }

  void drawGrid(float percent) {
    currTX+=VX;
    currTZ+=VZ;
    if (currTX>unitSize) currTX-=unitSize;
    if (currTZ>unitSize) currTZ-=unitSize;
    pushMatrix();
    translate(currTX,currTZ,0);
    stroke(0,240,255);
    fill(0);
    for (int i=0; imouthRmax) {
      mouthR = mouthRmax;
    }
    else {
      mouthR = r;
    }
  }

  void drawKimpi(){
    noStroke();
    fill(255,255,255);
    sphere(radius);
    
    float hx = 0;
    float hy = -radius;
    float hz = 0;
    
    pushMatrix();
    noFill();
    stroke(0,0,255,200);
    int nLines = 30;
    for (int i=0; i0) {
      rotateX(-PI/6);
      stroke(100);
      strokeWeight(3);
      noFill();
      beginShape();
      float theta = map((mouthR-1)/mouthRmax,0,mouthRmax,0,PI);
      for (int i=0; i< =20; i++){
        float phi = map(i,0,20,0,TWO_PI);
        float x = radius * sin(theta) * cos(phi); 
        float y = radius * sin(theta) * sin(phi); 
        float z = radius * cos(theta/2);
        vertex(x, y, z);
      }
      endShape();
      
      if ((mouthR-2>0)){
        float temp;
        if (mouthR-2<0) {
          temp = 0;
        } else {
          temp = mouthR-2;
        }
        stroke(200,220,255,200);
        float theta1 = map(temp/mouthRmax,0,mouthRmax,0,PI);        
        for (int i=0; i< =20; i++){
          float phi1 = map(i,0,20,0,TWO_PI);
          float x1 = radius * sin(theta1) * cos(phi1); 
          float y1 = radius * sin(theta1) * sin(phi1); 
          float z1 = radius * cos(theta1/2);
        
          line(x1,y1,z1,x1,y1-height/6,z1+height);
        }
      }
    }
    popMatrix();
  }
}