Friday, September 16, 2011

Our Burning Man Story

We came, we built, we burned... and now we're back! It feels like barely a blink has gone by - or perhaps a year, full as it is with new experiences, the warmth of wonderful people, and gallons of energy drinks. We're tremendously grateful to this community, and excited to share a few photos and stories from our adventure...

Our week began as a study in determination. We had already test-loaded all the lumber for the garden into our Ford Ranger and trailer, but we needed to add in our regular camp supplies for the drive up. With everything piled up beside the truck for the first time, the image conjured up lessons from old nature documentaries, about how ants could carry leaves 50-times their size. There was no turning back though, so we went to work, packing and repacking over the next six hours. In the end, bungees and creative use of vertical space won out, and we had all our supplies suspended off the ground.

Driving turned out to be another unforeseen adventure. If we turned up any abrupt hills, the lumber, which stuck out six feet behind the back of the truck, would crash down on top of the trailer. We were constantly afraid that a traffic light would stop us on an incline, and we wouldn't be able to get the truck moving again. On the steepest parts of I-80, we had to shift into second gear. Luckily, the playa was as flat as ever. As we arrived triumphantly, three separate burners shouted out compliments about our packing.


The line to enter burning man was 7 hours long, but that was probably a blessing in disguise. We hadn't had any time to update the code since adding lights to our podium, so this was our chance to get things working again. We pulled the laptop out in the truck, along with a small mockup of the garden we made out of a few extra LEDs and some plywood. By the time we got in, we had a few working animations.




Early on Tuesday, we registered our Garden with the Artery, and headed out to build with our crew of four. The great people from Dustworks came out to drill postholes in the ground for us, while we moved our lumber to the site. We assembled each gate completely on the ground, then discovered that it was about a six-person job to lift it into the postholes. Fortunately, helpful people are rarely in short supply at burning man, and some wonderful folks stopped to lift with us, giving up several hours of their burn. We couldn't find a trencher to bury our cables, so we ended up digging little trenches with hammers. It was nearly 4am, but we finally had everything up and all the lights turned on. It was time to return to camp and sleep deeply.

After sleeping past noon - quite a feat under the desert sun - we made plans to head back to the garden and fix some animations. A lot of things that looked great on our little mockup were a visual mess on the larger lanterns. Just after the sun set, two of us set out for the Garden once again. As we arrived, a group of five or six people stood around the podium reading from the Book of Missed Connections. As we hung back in the shadows, the crowd stayed firmly planted, and we got more excited with every minute that ticked by. When a few people finally drifted off, we came in for a closer look and were instantly stupefied.


Page after page of the book had been filled since we left. There were short, funny notes, and entire pages of heartrending streams of consciousness. People wrote to missed connections, about missed connections, to other writers. Some were wistful, some nonsensical, some poetic. Though we put the book out for people to fill, we were never sure that anyone would care to write anything at all. Instead this collection of thoughts and emotions had flourished, outgrowing all the art we had toiled to construct. It was a humbling, and perfect result, and we spent an hour pouring over the book we had set in motion.

Finally, we sat down to fix up some animations. Coding next to the podium was a fun experience, as curious folks looked over to see what was happening. After we fixed a few things, it was time to replace the batteries. This was when we realized a mistake we made: while we spent hours carefully dust-proofing our Lightuino, we never bothered to put our battery pack in anything. We had to wiggle the new batteries around for several minutes before they finally made contact and turned the lights back on. It would eventually work each evening, but not without making us very nervous.

The Garden was a tremendous experience for all of us. We learned so much and feel tremendously grateful to this community for letting us pursue this project. We're especially humbled by everyone that donated to the project on Kickstarter: thank you for trusting us to follow through and make something worth contributing to. Hopefully, many of you managed to stop by the Garden while it was up. We know some of you didn't go to burning man, and we hope you'll get to see even greater incarnations of the Garden in future years. If you haven't already, take a look at Jesse's collection of photos, and hold on to your most imaginative, inclusive, and joyful spirit!

The Gardeners


Technical Post: Our Project Box

Another post for artists working with an Arduino: at some point, you'll need to think about how to protect your circuitry from the elements. This may seem like a trivial bother, but the dust at Burning Man places unusual demands on a project box, and we had trouble finding good examples to work from.

Our case is made out of a plastic Sterilite storage box. The top clips on and doesn't seal as tightly as a box made for food, but we needed a box with nice flat walls, and this was the best we could find. We had read that M3 screws were the right size to mount an Arduino, so we ordered some, planning to screw them into these 10mm plastic spacers to elevate the board. It turned out, however, that the spacers were too loose to bind the screws, and the screws weren't long enough to reach the other side. It was the last day, and we couldn't find longer M3 screws anywhere, so we bought the thinnest machine screws we could find at Home Depot, which were #4. Luckily, these seemed to work just fine. Each screw passes through 1) a nylon washer, 2) the circuit board, 3) a 10mm plastic spacer, 4) a pre-drilled hole in the bottom of the box, and 5) a nut.

For power, we drilled a hole in the side of the box, and screwed in a barrel-style DC power jack. To a get a good electrical connection, it was important to screw the jack down tightly. We kept the batteries outside the main box, so that we wouldn't have to open it in the desert, but we made the mistake of leaving the battery pack out in the open air. With all the dust, it was always a trial to get replacement batteries to make contact. Next time, we'll make sure the batteries get their own box.

Dustproofing the lantern connections proved to be a harder problem. Early in our design process, we discussed wiring with Andrew Stone, who designed our Lightuino 5 microcontroller. Andrew suggested running ethernet cables to the lanterns, and agreed to push forward the design of a converter board he had been thinking about. Each converter has a row of 6 ethernet jacks, but we couldn't think of a good way to dustproof around these. In the end, we decided to mount keystone ethernet couplers on the sides of our box, and run short ethernet cables to these from the converters.

All the extra wires meant extra connections that could fail, and one coupler gave us some trouble during the week. On the other hand, we were able to seal around the keystone faceplates with superglue, so the result was quite dustproof. We added extra screws to the corners of the faceplates to keep them flush against the sides of the box.

In the end, our box survived a week in the desert and kept our circuit boards entirely clean. Here are some pictures of our setup:

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;
}

Garden of Missed Connections Flickr Set

We're still running our post-event wrap up and should have a couple interesting and fun things coming out in the next few days, but since we know a lot of you are itching to see photos we went ahead and got the first batch up here. A few highlights are below. You can also find more photos of the gates at Nanasaurus Rex's photostream here, and we'll be updating this page as more sets come online.

Enjoy!






.