back from the desert!

12 views
Skip to first unread message

Paul Laskowski

unread,
Sep 13, 2011, 11:02:00 PM9/13/11
to toasted-circu...@googlegroups.com, Andrew Stone
Hi Andrew and everyone. So the news: we're back from burning man,
where we successfully installed our lightuino-powered project! It was
a tremendous adventure, and it felt great to contribute to the event
as an artist. If you're interested, you can check out my write-up and
see a bunch of our photos here:
http://gardenofmissedconnections.blogspot.com/2011/09/our-burning-man-story.html

I also wanted to share some of our programming experience with this
group. I wrote up an entire post about it, but discovered that
blogger does terrible things to code, so I'll paste it below too. The
code is rough (we wrote a lot of it sitting in a truck on the way to
burning man), but I hope it proves handy to some future project
nonetheless. If any of you have used a microphone with your
lightuino, I'd love to hear about your strategy too. We have just 12
months to make improvements before next year's garden!

------
http://gardenofmissedconnections.blogspot.com/2011/09/technical-post-our-project-code.html

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

Andrew Stone

unread,
Sep 14, 2011, 1:10:16 PM9/14/11
to toasted-circu...@googlegroups.com
This is great!  I'm glad the Burning Man was such a success for you!  I really have GOT to get around to updating my web site with these new projects soon! :-)

Cheers!
Andrew

Paul Laskowski

unread,
Sep 16, 2011, 11:33:19 PM9/16/11
to toasted-circu...@googlegroups.com
Thanks Andrew, you were a great resource for us! I actually have one
more link to share with this group. When we built our project case,
we had trouble finding good examples to work from, so we just added
pictures of our own setup:
http://gardenofmissedconnections.blogspot.com/2011/09/technical-post-our-project-box.html.
Hope somebody finds them useful!

Best,
Paul

Reply all
Reply to author
Forward
0 new messages