Time Fountain Write up


The Idea:

I wanted to make a music visualizer, and inspired by the videos I saw online about time fountains, I decided to make a time fountain on my own. Instead of just having a fountain though, I planned on controlling the strobe with the music from the MP3 player itself.

The Science:

As invisible water droplets are falling, the UV lights light up the fluorescent dye in the water. Thus, the droplets are only visible when the strobing UV LED lights up. Your mind connects the images of the illuminated water droplets, thus you can control how the water is ‘moving’ based on how fast you’re strobing. If you’re strobing at the same rate as the water is dropping, the droplets will look like it’s frozen in mid-air. If the strobing is slower, the water will drop. If the strobing is faster, the water will look as if it’s going back in time. 

 

The Materials
VS1033D MP3 Decoder Chip [Sparkfun]

Arduino Nano [Amazon]

Audio Jack Breakout Board [Sparkfun] (note: the breakout board is incorrectly labeled, at least the one I used is)
Audio Jack [Sparkfun]
SD Card + Reader

Level Shifter [Jameco]

Multiplexer [Sparkfun]

16 Ultraviolet LED

Graphic Equalizer Display Filter  [Sparkfun]

2 Perfboards

3/8’ Tube (1/2 inch)

Two bowls

Plastic encasing

 

Wish:

I was going to use a multiplexer to control 16 lights and use the display filter to filter out all the 63 hertz frequencies from the mp3 player to control the LED lights. There would be two settings: a) control the strobing with a potentiometer and b) control the strobing with the music

 

The story:
At 3 am, my multiplexer stopped working. The signal came out as 3V and alternated to 5V, so obviously there was a shortage. Instead of re-soldering 16 wires, I decided to use rainbow ribbons for the first time. After figuring out rainbow ribbons, I hooked everything up. Because the rainbow ribbons actually shorted the Vin and ground, I cooked my Arduino.

 

Reality:

In the end I couldn’t get the multiplexer or the signal filter to work in time, so I controlled hooked in only 8 lights directly in the arduino and controlled them using the potentiometer.

 

 

Hardware:

 

 

Drip Technology

I needed to make something that dripped at a constant rate. At first I used a coke bottle with a hole drilled into it with a pump. But the dripping was inconsistent so the prototype was scrapped.

 

Prototype #1

Prototype #2

 

In the end, I drilled a tiny hole (0.1cm) and attached a 3/8 inch tube with hot glue. This works better when the whole is on the side rather than the bottom.

 

Case

I wanted a case that could house both the wiring, the lights, and the dripping boxes. At first, I had planned on making the top box and the catching box (for the dripping) with acrylic, but it didn’t work out, so the case is a bit bigger than I had hoped. I put a big hole on the back board so wires can come out.

Wiring

I kept my wiring on my breadboard. It’s pretty typical: barebone mp3 player connected to a the music equalizer filter. Because the multiplexer stopped working, I had to connect my 8 LED lights each to a pin on the arduino, and it could be controlled with a potentiometer.

Overall

I taped my LED lights onto the side of my case, with the wires coming out at the bottom connecting to the   arduino on the breadboard. I calibrate the top box to make sure the dripping water line up with the LED lights.

 

 

Code:

Strobe with Mux:

 

//Mux control pins
int s0 = 10;
int s1 = 9;
int s2 = 8;
int s3 = 7;

//Mux in "SIG" pin
int SIG_pin = 17;
  int potentiometer_pin = 2;
  int on_time = 100;
  int minimum_delay = 100;
int strobe_delay = 0;

void setup(){
  pinMode(s0, OUTPUT);
  pinMode(s1, OUTPUT);
  pinMode(s2, OUTPUT);
  pinMode(s3, OUTPUT);

  digitalWrite(s0, LOW);
  digitalWrite(s1, LOW);
  digitalWrite(s2, LOW);
  digitalWrite(s3, LOW);


  Serial.begin(9600);
}

void loop(){

  //Loop through and read all 16 values
  //Reports back Value at channel 6 is: 346
  for(int i = 0; i < 16; i ++){
 
    // strobe_delay = minimum_delay + analogRead(potentiometer_pin);
    onLED(i);
   // delay(100  );
     // delay(on_time);
      //offLED(i);
       // delay(strobe_delay - on_time);

  }

}

void onLED(int channel){
  int controlPin[] = {s0, s1, s2, s3};

  int muxChannel[16][4]={
    {0,0,0,0}, //channel 0
    {1,0,0,0}, //channel 1
    {0,1,0,0}, //channel 2
    {1,1,0,0}, //channel 3
    {0,0,1,0}, //channel 4
    {1,0,1,0}, //channel 5
    {0,1,1,0}, //channel 6
    {1,1,1,0}, //channel 7
    {0,0,0,1}, //channel 8
    {1,0,0,1}, //channel 9
    {0,1,0,1}, //channel 10
    {1,1,0,1}, //channel 11
    {0,0,1,1}, //channel 12
    {1,0,1,1}, //channel 13
    {0,1,1,1}, //channel 14
    {1,1,1,1}  //channel 15
  };

  //loop through the 4 sig
  for(int i = 0; i < 4; i ++){
    digitalWrite(controlPin[i], muxChannel[channel][i]);
       digitalWrite(SIG_pin, HIGH);
  }

}

void offLED(int channel){
  int controlPin[] = {s0, s1, s2, s3};

  int muxChannel[16][4]={
    {0,0,0,0}, //channel 0
    {1,0,0,0}, //channel 1
    {0,1,0,0}, //channel 2
    {1,1,0,0}, //channel 3
    {0,0,1,0}, //channel 4
    {1,0,1,0}, //channel 5
    {0,1,1,0}, //channel 6
    {1,1,1,0}, //channel 7
    {0,0,0,1}, //channel 8
    {1,0,0,1}, //channel 9
    {0,1,0,1}, //channel 10
    {1,1,0,1}, //channel 11
    {0,0,1,1}, //channel 12
    {1,0,1,1}, //channel 13
    {0,1,1,1}, //channel 14
    {1,1,1,1}  //channel 15
  };

  //loop through the 4 sig
  for(int i = 0; i < 4; i ++){
    digitalWrite(controlPin[i], muxChannel[channel][i]);
    digitalWrite(SIG_pin, LOW);
  }

}

 

Code with MP3 Player

 

// Use 40*13 = 520 bytes of EEPROM
#define MAX_FILE_COUNT 40
#define MAX_NAME_LENGTH 13

#include <mp3.h>
#include <mp3conf.h>

// include the SD library:
#include <SD.h>
#include <EEPROM.h>

// set up variables using the SD utility library functions:
Sd2Card card;
SdVolume volume;
SdFile root;

#define DEBUG // Comment this line to remove debugging features



#define mp3_cs      5             // 'command chip select' to cs pin
#define sd_cs       4             // 'chip select' for SD card

#define dcs         14            // (Pin A0) 'data chip select' to bsync pin
#define rst         6             // 'reset' to decoder's reset pin
#define dreq        15            // (Pin A1) 'data request line' to dreq pin

#define read_buffer 256           // size of the microsd read buffer
#define mp3_vol     200           // output volume range is 0 to 254

/* This function places the current value of the heap and stack pointers in the
 * variables. You can call it from any place in your code and save the data for
 * outputting or displaying later. This allows you to check at different parts of
 * your program flow.
 * The stack pointer starts at the top of RAM and grows downwards. The heap pointer
 * starts just above the static variables etc. and grows upwards. SP should always
 * be larger than HP or you'll be in big trouble! The smaller the gap, the more
 * careful you need to be. Julian Gall 6-Feb-2009.
 */
 
 const int strobe = 8;     // the number of the pushbutton pin
const int songInfo =  A1;      // the number of the LED pin
const int reset = 9;
// variables will change:
int state = 0;  
uint8_t *heapptr, *stackptr;
uint16_t diff=0;
void check_mem() {
  stackptr = (uint8_t *)malloc(4);          // use stackptr temporarily
  heapptr = stackptr;                     // save value of heap pointer
  free(stackptr);      // free up the memory again (sets stackptr to 0)
  stackptr =  (uint8_t *)(SP);           // save value of stack pointer
}


/* Stack and heap memory collision detector from: http://forum.pololu.com/viewtopic.php?f=10&t=989&view=unread#p4218
 * (found this link and good discussion from: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1213583720%3Bstart=all )
 * The idea is that you need to subtract your current stack pointer (conveniently given by the address of a local variable)
 * from a pointer to the top of the static variable memory (__bss_end). If malloc() is being used, the top of the heap
 * (__brkval) needs to be used instead. In a simple test, this function seemed to do the job, showing memory gradually
 * being used up until, with around 29 bytes free, the program started behaving erratically.
 */
extern int __bss_end;
extern void *__brkval;

int get_free_memory()
{
  int free_memory;

  if((int)__brkval == 0)
     free_memory = ((int)&free_memory) - ((int)&__bss_end);
  else
    free_memory = ((int)&free_memory) - ((int)__brkval);

  return free_memory;
}

/*
 * Adopted from the ladyada.net tutorial on Ethernet protocol
 * @http://www.ladyada.net/learn/arduino/ethfiles.html
 *
 * Lists all files from a directory:
 * ListFiles(SdFile *dir, char *doubleBuf, int maxCount)
 *
 * dir is the directory to list
 * doubleBuf is the buffer to store the results in -- this should be fileNames to start
 * maxCount is the size of the buffer.
 *
 * returns the number of file names written into the doubleBuf array
 */
int ListFiles(SdFile *dir) {
  int count = 0;
  dir_t p;
 
  dir->rewind();
  while (dir->readDir(&p) > 0 && count < MAX_FILE_COUNT) {
    // done if past last used entry
    if (p.name[0] == DIR_NAME_FREE) break;

    // skip deleted entry and entries for . and  ..
    if (p.name[0] == DIR_NAME_DELETED || p.name[0] == '.') continue;

    /* Uncomment to allow subdirectories to be listed (with a '/' after the name) */
    // only list subdirectories and files
    //if (!DIR_IS_FILE_OR_SUBDIR(&p)) continue;
   
    /* Uncomment to only allow files to be listed */
    // only list files
    if (!DIR_IS_FILE(&p)) continue;

    // print file name into string
    uint8_t pos = 0;
    for (uint8_t i = 0; i < 11; i++) {
      if (p.name[i] != ' ') {
        EEPROM.write((count*MAX_NAME_LENGTH)+pos, p.name[i]);
        pos++;
      }
    }
   
    // append slash if file is a directory
    if (DIR_IS_SUBDIR(&p)) {
      EEPROM.write((count*MAX_NAME_LENGTH)+pos, '/');
      pos++;
    }
   
    // add the end string character
    EEPROM.write((count*MAX_NAME_LENGTH)+pos, '\0');
    count++;
  }
#ifdef DEBUG
  Serial.print("Stored ");
  Serial.print(count+1);
  Serial.print(" files, using ");
  Serial.print((count+1)*MAX_NAME_LENGTH, DEC);
  Serial.println(" byes in EEPROM.");
#endif
  return count+1;
}

/*
 * read in buffer 'bytes' of 'read_buffer' size from the file opened
 * in the while loop below. This function assumes that file has already
 * been opened and does NOT close the file. This means you need to do this
 * outside of the function.
 */
void mp3_play (SdFile *file) {
  unsigned char bytes[read_buffer]; // buffer to send to the decoder
  unsigned int bytes_to_read;       // number of bytes to read from sd card
  // reset the file to be at the beginning of data
  file->seekSet(0);
  // Try to read 'read_buffer' length of bytes and send it to the decoder.
  // If less than 'read_buffer' bytes are available, stop last send.
  do {
    bytes_to_read = file->read(bytes, read_buffer);
    Mp3.play(bytes, bytes_to_read);
    //Serial.println('.');
    
      getSongFreq();
  }

  while (bytes_to_read == read_buffer);

#ifdef DEBUG
  Serial.println("Played song...");
#endif
}

void getSongFreq(){
  digitalWrite(strobe, HIGH);
  delay(18);
  digitalWrite(strobe, LOW);
  state = analogRead(songInfo);
 Serial.println(state);
 for (int i=0; i++; i<9){
    digitalWrite(strobe, HIGH);
    delay(18);
    digitalWrite(strobe, LOW);
    delay(72);
 } 

 

 

}

  

}

 /*

 * play back an mp3 or wav file (only!) in the root directory. first

 * check that it's a file (and not a directory); next, check that it

 * has a proper extension; finally, play only if it opens cleanly.

 */

void dir_play (SdFile *dir) {

  int numFiles = ListFiles(dir);

  for (int i = 0; i < numFiles; i++) {

    char fn[MAX_NAME_LENGTH+2];

    int fnln = MAX_NAME_LENGTH;

    // get file name and name length

    for (int j = 0; j < MAX_NAME_LENGTH; j++) {

      fn[j] = EEPROM.read((i*MAX_NAME_LENGTH)+j);

      // end of name

      if (fn[j] == '\0') {

        fnln = j;

        j = MAX_NAME_LENGTH;

        break;

      } 

      // directory => nullify entry name

      else if (fn[j] == '/') {

        fn[0] = '\0';

        fnln = 0;

        j = MAX_NAME_LENGTH;

        break;

      }

    }

    if (fnln > 4) {

      fn[fnln+1] = '\0';

      fn[fnln] = fn[fnln-1];

      fn[fnln-1] = fn[fnln-2];

      fn[fnln-2] = fn[fnln-3];

      fn[fnln-3] = '.';

      fnln++;

#ifdef DEBUG

      Serial.print("Opening ");

      Serial.println(fn);

#endif

    }

    SdFile dataFile;

    // ensure we can open the file

    if (dataFile.open(dir, fn, O_RDONLY) > 0) {

      // ensure it's not a directory

      if (!dataFile.isDir() || fn == 0) {

        //get filenames in directory fn

        //get length of the filename fnln

        

#ifdef DEBUG

        Serial.print(fn);

        Serial.println(": File is valid.");

#endif

  

        if ((fn[fnln-3] == 'M' && fn[fnln-2] == 'P' && fn[fnln-1] == '3') ||

      (fn[fnln-3] == 'W' && fn[fnln-2] == 'A' && fn[fnln-1] == 'V')) {

            

#ifdef DEBUG

          Serial.print("Playing ");

          Serial.println(fn);

#endif

        //buffer data for playing

        mp3_play(&dataFile);

        }

#ifdef DEBUG

        else {

          Serial.print("Skipping ");

        Serial.println(fn);

        }

#endif

      }

#ifdef DEBUG

      else {

        Serial.print("File is directory ");

    Serial.println(fn);

      }

#endif

    }

    else {

      Serial.print("File is not valid ");

      Serial.println(fn);

    }

  }

}







/*

 * initialize the processor speed, setup the fatfs sd card (or, mms)

 * filesystem, setup mp3 playback and register the pins used (device

 * specific configuration)

 */

void setup() {

  Serial.begin(9600);            // initialize the serial terminal

  

  pinMode(10, OUTPUT);     // change this to 53 on a mega

  digitalWrite(10, HIGH);

   pinMode(strobe, OUTPUT);      

  // initialize the pushbutton pin as an input:

  pinMode(songInfo, INPUT);   

  pinMode(reset, OUTPUT);   

  Serial.println("hellooo?"); 

  digitalWrite(reset, HIGH);

  delay(100);

  digitalWrite(reset, LOW); 

  delay (72);



  // see if the card is present and can be initialized:

  if (!SD.begin(sd_cs)) {

    Serial.println("Card failed, or not present");

    // don't do anything more:

     pinMode(s0, OUTPUT);

  pinMode(s1, OUTPUT);

  pinMode(s2, OUTPUT);

  pinMode(s3, OUTPUT); 



  digitalWrite(s0, LOW);

  digitalWrite(s1, LOW);

  digitalWrite(s2, LOW);

  digitalWrite(s3, LOW);
 
 attachInterrupt(0, strobe, CHANGE);


 
    return;
  }
 
  // we'll use the initialization code from the utility libraries
  // since we're just testing if the card is working!
  if (!card.init(SPI_HALF_SPEED, sd_cs)) {
    Serial.println("Initialization failed. Things to check:");
    Serial.println("* is a card is inserted?");
    Serial.println("* Is your wiring correct?");
    Serial.println("* did you change the chipSelect pin to match your shield or module?");
    return;
  }
 
  // Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
  if (!volume.init(card)) {
    Serial.println("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");
    return;
  }
 
  if (!root.openRoot(&volume)) {
    Serial.println("Failed to open root");
    // don't do anything more:
    return;
  }
 
  Mp3.begin(mp3_cs,dcs,rst,dreq);  // decoder cs, dcs, rst, dreq pin
  Mp3.volume(mp3_vol);             // default volume level is silent
 
#ifdef DEBUG
  Serial.println("Card initialized.");
  Serial.println("\nFiles found on the card (name, date and size in bytes): ");
  // list all files in the card with date and size
  root.ls(LS_R | LS_DATE | LS_SIZE);
 
  Serial.println("Checking free memory...");
  Serial.print("There are ");
  Serial.print(get_free_memory(), DEC);
  Serial.println(" bytes of free memory.");
  Serial.print("~");
  Serial.print(read_buffer, DEC);
  Serial.println(" free bytes required for mp3 playing.");
#endif

  // List all files to in the root directory and play each
  // one at a time. Maximum of MAX_FILE_COUNT files will be read
  dir_play(&root);
  root.close();
}

// Do nothing for now
void loop() {
 
  while (1);
}

Things I learned:

 

NEVER ever try something new the night before the project is due, even if you think it will help your project. I tried to use a rainbow ribbon 12 hours before the project was due, and it ended up killing my multiplexor and my arduino around 5am, setting my project back by several hours.

 

 

Improvements:

Get the multiplexer and the frequency filter working

Use an IR sensor to sense how fast the water is dripping so I can strobe accordingly to make the water ‘stop in time’

Use a pump with a valve for steady dripping