Monday, September 12, 2011

Technical Post: Our Project Code

For future artists embarking on a similar Arduino-based project, we'd like to take a post to share our programming experience. It might be worth noting ahead of time that we're hardly experts, and we wrote a lot of the code sitting in a truck on the way to Burning Man, so it may be on the rough side.

Our project is controlled by a Lightuino 5, an Arduino-compatible microcontroller created by the tremendously helpful Andrew Stone. We programmed our Lightuino to take readings from a microphone (purchased from SparkFun, http://www.sparkfun.com/products/9964), and analyze the sound to control 8 RGB lanterns (and 4 more RGB LEDs found in our central podium). To analyze the sound, our first idea was to take advantage of the 8-bit FFT library contributed by deif (http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1286718155). We therefore started by coding a loop with the following steps:

1) Take microphone readings at 1ms intervals
2) Take FFT and compute average amplitude
3) Update LED brightnesses, taking volume as input

It soon became clear, however, that our lanterns were updating too
slowly; a complete loop would take 150-200ms, making transitions
visually jarring. Moreover, we were missing loud beats in the music,
likely since we were only listening to the microphone during a
fraction of the loop. This could be improved by interlacing the
listening phase with the LED updates, but the FFT algorithm is
in-place - it overwrites the array of microphone readings - so we
would still need to stop listening during the transform, or totally
redesign the FFT algorithm.

In the end, we decided to scrap this entire strategy. Instead of an
FFT, we tried a naive approach, measuring a few select frequencies
from the microphone in real time. For each frequency, we maintain a
running sine and cosine wave. As we take each reading from the
microphone, we immediately multiply it against each of these
functions, and add it to a running total, creating a convolution.
Once a loop, just before it's time to update the lanterns, we combine
the sine and cosine totals to find the amplitude at that frequency.

We weren't sure if this strategy would work - after all, the FFT is
famously fast (on the order of N log N), but we had a hunch that
simplicity might win out for a small number of samples by reducing
overhead. In fact, the first time we ran the new algorithm, our time
between lantern updates immediately fell to about 50ms, eliminating
the jarring effect during transitions! Our approach saves on memory,
since each microphone reading is immediately decomposed into
frequencies. We are also able to take readings continually, every 1ms
without breaks, and lantern updates can come at any time, instead of
waiting for a sound array to fill up.

Since the Lightuino PWM routine uses Timer 2 to operate, we adapted the code (originally from http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/) to use Timer 1 to schedule sound readings. This causes a bit of trouble. Very briefly, each timer increments a counter that periodically "overflows," interrupting the program. After the interruption, the code resets the counter below the overflow threshold, but adds some amount to compensate for the time spent away from the main program, the "timer latency." Perhaps because microphone readings take so long, this extra amount causes the timer to "loop over," creating a long gap between interruptions and making the LEDs flicker. To get around this problem, we disabled this timer latency compensation, by commenting out this variable from the following line in lightuinoPwm.cpp, as shown:

//Reload the timer and correct for latency.
TCNT2=/*timerLatency+*/timerLoadValue;

With this fix in place, our code was pretty much ready to run. We spent some time writing animations that take advantage of the sound readings. Our bass and sub-bass measurements seemed to follow our experience of the music quite faithfully. Our measurement of treble, set to 3000 Hz, was rather unresponsive. We suspect that one-millisecond samples are just not frequent enough to capture this range.

Hope you find the code handy!


#include <lightuino5.h>


int i=0,val,volume;
int numPatterns = 6; // store the number of patterns
int currentPattern=5; // the current pattern being shown on the LEDs
long switchTime=60000; // time (in ms) between switching patterns
long lastPatternTime; // last time in millis that the light pattern was changed
long lastsamplemicros; // store the last time a microphone sample was taken, so the next read will be 1ms later.
long lastsamplelooptime; // store the last time we started sampling for performance testing.


////// Sound Sampling
float subbassr, subbassi, subbass; // real and imaginary parts of each frequency being sampled, and the measured amplitude at that frequency
float bassr, bassi, bass;
float midr, midi, mid;
float trebr, trebi, treb;

const prog_int8_t Sinewave[256] PROGMEM = {
0, 3, 6, 9, 12, 15, 18, 21,
24, 28, 31, 34, 37, 40, 43, 46,
48, 51, 54, 57, 60, 63, 65, 68,
71, 73, 76, 78, 81, 83, 85, 88,
90, 92, 94, 96, 98, 100, 102, 104,
106, 108, 109, 111, 112, 114, 115, 117,
118, 119, 120, 121, 122, 123, 124, 124,
125, 126, 126, 127, 127, 127, 127, 127,

127, 127, 127, 127, 127, 127, 126, 126,
125, 124, 124, 123, 122, 121, 120, 119,
118, 117, 115, 114, 112, 111, 109, 108,
106, 104, 102, 100, 98, 96, 94, 92,
90, 88, 85, 83, 81, 78, 76, 73,
71, 68, 65, 63, 60, 57, 54, 51,
48, 46, 43, 40, 37, 34, 31, 28,
24, 21, 18, 15, 12, 9, 6, 3,

0, -3, -6, -9, -12, -15, -18, -21,
-24, -28, -31, -34, -37, -40, -43, -46,
-48, -51, -54, -57, -60, -63, -65, -68,
-71, -73, -76, -78, -81, -83, -85, -88,
-90, -92, -94, -96, -98, -100, -102, -104,
-106, -108, -109, -111, -112, -114, -115, -117,
-118, -119, -120, -121, -122, -123, -124, -124,
-125, -126, -126, -127, -127, -127, -127, -127,

-127, -127, -127, -127, -127, -127, -126, -126,
-125, -124, -124, -123, -122, -121, -120, -119,
-118, -117, -115, -114, -112, -111, -109, -108,
-106, -104, -102, -100, -98, -96, -94, -92,
-90, -88, -85, -83, -81, -78, -76, -73,
-71, -68, -65, -63, -60, -57, -54, -51,
-48, -46, -43, -40, -37, -34, -31, -28,
-24, -21, -18, -15, -12, -9, -6, -3
};

// timer code is taken from http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/
#define TIMER_CLOCK_FREQ (F_CPU/1024.0) //Found this frequency by trial and error
unsigned int timer1Latency;
unsigned int timer1LoadValue;
unsigned char timerCounter = 0;

ISR(TIMER1_OVF_vect) {

val = analogRead(0)/4 -128;
subbassr += val * Sinewave[timerCounter * 16 % 256]; //62 hertz
subbassi += val * Sinewave[(timerCounter * 16 + 64 ) % 256];
bassr += val * Sinewave[timerCounter * 31 % 256]; // 121 hertz
bassi += val * Sinewave[(timerCounter * 31 + 64 ) % 256];
midr += val * Sinewave[timerCounter * 200 % 256]; //781 hertz
midi += val * Sinewave[(timerCounter * 130 + 64 ) % 256];
trebr += val * Sinewave[timerCounter * 768 % 256]; //3000 hertz
trebi += val * Sinewave[(timerCounter * 768 + 64 ) % 256];
timerCounter++;
//Capture the current timer value. This is how much error we have
//due to interrupt latency and the work in this function
timer1Latency=TCNT1;

//Reload the timer and correct for latency.
TCNT1=timer1Latency+timer1LoadValue;
}

void StartSoundSampling(float timeoutFrequency)
{
//Calculate the timer load value
timer1LoadValue=(unsigned int)((65535-(TIMER_CLOCK_FREQ/timeoutFrequency))+0.5); //the 0.5 is for rounding;

TCCR1A = 0;
TCCR1B = 1<<CS22 | 0<<CS21 | 1<<CS20;


//Timer2 Overflow Interrupt Enable
TIMSK1 = 1<<TOIE2;

//load the timer for its first cycle
TCNT1=timer1LoadValue;
}



// Create the basic Lightuino 70 LED sink controller (the pins in the 2 40-pin IDE connectors)
LightuinoSink sinks;
// Create the Lightuino 16 channel source driver controller (the 16 pin connector)
LightuinoSourceDriver sources;

// This object PWMs the Lightuino sinks allowing individual LED brightness control, and provides array-based access to the Leds
FlickerBrightness pwm(sinks);

//?? Turn all the LEDs and source drivers off
void AllOff(void)
{
sources.set(0);
sinks.set(0,0,0);
}

// set a lantern color with RGB values, each from 0 to 8192
void setLanternColor(int lantern, int r, int g, int b)
{
// only 35 sink pins on each side, so we have to subtract 1 to compensate for laterns 6-11.
pwm.brightness[lantern * 6 - (lantern > 5)]= r;
pwm.brightness[lantern * 6 - (lantern > 5) + 1]= g;
pwm.brightness[lantern * 6 - (lantern > 5) + 2]= b;
}

// set a lantern color with a hue (between 0 and 360) and brightness value
void setLanternColor(int lantern, int hue, int brightness)
{
// when hue is between 0 and 60, r = brightness, b = 0.0 and g goes linearly from
// 0.0 to brightness; similarly, r goes down when 60<hue<120, b ramps up when 120<hue<180,
// g goes down when 180<hue<240, r ramps up when 240<hue<300 and b down when 300<hue<360.

static int r, g, b;
r = (60*(hue<60) + (120-hue)*((hue>=60)&&(hue<120)) + /* 0*((hue>=120)&&(hue<240))/60 + */
(hue-240)*((hue>=240)&&(hue<300)) + 60*(hue>=300))*(brightness/60);
g = (hue*(hue<60) + 60*((hue>=60)&&(hue<180)) + (240-hue)*((hue>=180)&&(hue<240))
/* + 0*(hue>240) */ )*(brightness/60);
b = (/* 0*(hue<120) + */ (hue-120)*((hue>=120)&&(hue<180)) + 60*((hue>=180)&&(hue<300)) +
(360-hue)*(hue>=300))*(brightness/60);
setLanternColor(lantern, r, g, b);
}

void setUV(int lantern, int brightness)
{
pwm.brightness[lantern * 6 + 3]= brightness;
pwm.brightness[lantern * 6 + 4]= brightness;
}

void setup()
{
analogReference(DEFAULT); // Use default (5v) aref voltage.
// Start up the serial port. I don't think this is actually working, but the USB works.
Serial.begin(9600);
Serial.println("serial initialized");
// Start up the Lightuino's USB serial port.
#ifdef Lightuino_USB // This #ifdef,#endif wrapper means the the code inside will only compile if your Lightuino has a USB port.
// That way this sketch will work with multiple versions of the circuitboard.
// But since you probably don't care that your sketch does so, you can leave these lines out.

Usb.begin();

#endif // This line need to be removed if #ifdef is removed too!

AllOff(); // When the board boots up there will be random values in various chips resulting in some lights being on.
pwm.StartAutoLoop(3000);
StartSoundSampling(1000);
setLanternColor(10, Lightuino_MAX_BRIGHTNESS/2, Lightuino_MAX_BRIGHTNESS/2, Lightuino_MAX_BRIGHTNESS/2);
setLanternColor(11, Lightuino_MAX_BRIGHTNESS/2, Lightuino_MAX_BRIGHTNESS/2, Lightuino_MAX_BRIGHTNESS/2);
lastPatternTime = millis();
};
void loop()
{
Usb.print("Time since last read loop (ms): ");
Usb.println(millis()-lastsamplelooptime);
lastsamplelooptime = millis();
Usb.print("Number of samples since last loop: ");
Usb.println(timerCounter);
////// Analyze Sound
subbass = sqrt(subbassr * subbassr + subbassi * subbassi)/ timerCounter;
bass = sqrt(bassr * bassr + bassi * bassi)/ timerCounter;
mid = sqrt(midr * midr + midi * midi)/ timerCounter;
treb = sqrt(trebr * trebr + trebi * trebi)/ timerCounter;
subbassr=0; subbassi=0;
bassr=0; bassi=0;
midr=0; midi=0;
trebr=0; trebi=0;
timerCounter=0;
volume = (subbass + bass + mid + treb)/4;
Usb.print(" Volume: ");
Usb.println(volume);
////// Change the light pattern every so often
if (millis()-lastPatternTime > switchTime){
currentPattern = (currentPattern + 1) % numPatterns; // just scroll through for now
lastPatternTime = millis();
}
////// Update LEDs
switch(currentPattern){
case (0):
rgbuLoop();
break;
case (1):
color_wheel_chase();
break;
case(2):
color_wheel_static();
break;
case(3):
whiteVolumeMeter();
break;
case(4):
equalizer();
break;
default:
twin_light_chase();
break;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Light Patterns for the LED Lights
////////////////////////////////////////////////////////////////////////////////////////////////////////////


void whiteVolumeMeter()
{
int brightness;
brightness = max(500, min(Lightuino_MAX_BRIGHTNESS, volume*4));
for (i=0; i<10; i++){
setLanternColor(i, brightness, brightness, brightness);
}
}

void rgbuLoop()
{
static int color=0;
static long lastColorTime=0;
if (millis()-lastColorTime>1000){
color = (color +1) %4;
lastColorTime=millis();

for (i=0; i<10; i++){
setLanternColor(i, ((color + i) % 4 == 0 || (color + i) % 4 == 3) * Lightuino_MAX_BRIGHTNESS,
((color + i) % 4 == 1 || (color + i) % 4 == 3) * Lightuino_MAX_BRIGHTNESS,
((color + i) % 4 == 2 || (color + i) % 4 == 3) * Lightuino_MAX_BRIGHTNESS);
}
}
}
void color_wheel_chase()
{
static int hue=0; // the hue
static int lantern_number=10; // number of lanterns
int h=hue;
for (int i=0; i
setLanternColor(i, h, max(4000, min(volume*2, Lightuino_MAX_BRIGHTNESS)));
h = (h + (360/lantern_number)) % 360;
}
hue = (hue + max(1, volume/50-15)) % 360;
}
void color_wheel_static()
{
static int h=0; // the hue
for (int i=0; i<10; i++) {
setLanternColor(i, h, max(4000, volume * 2));
}
h = (h + max(1, volume/50-15)) % 360;
}

void equalizer()
{
setLanternColor(0, 0, max(500, subbass*2));
setLanternColor(1, 0, max(500, subbass*2));
setLanternColor(2, 90, max(500, bass*2));
setLanternColor(3, 90, max(500, bass*2));
setLanternColor(4, 180, max(500, mid*2));
setLanternColor(5, 180, max(500, mid*2));
setLanternColor(6, 270, max(500, treb*2));
setLanternColor(7, 270, max(500, treb*2));
setLanternColor(8, max(500, volume*2), max(500, volume*2), max(500, volume*2));
setLanternColor(9, max(500, volume*2), max(500, volume*2), max(500, volume*2));
}

void twin_light_chase()
{
static int hue=0; // the hue
static int lantern=0; // current lantern position
static int timeSinceSwitch=0;

if (30-timeSinceSwitch++ < min(15, max(0, volume / 30 - 20)))
{
setLanternColor(lantern, 0,0,0);
setLanternColor((lantern + 4) % 8, 0,0,0);
lantern = (lantern + 1) % 8;
setLanternColor(lantern, hue, Lightuino_MAX_BRIGHTNESS);
setLanternColor((lantern + 4) % 8, hue, Lightuino_MAX_BRIGHTNESS);
timeSinceSwitch=0;
}
hue = (hue + 1) % 360;
}

No comments:

Post a Comment