Introduction: The LED Matrix Panel, or LMP, for the Arduino Microcomputer, Continued.

Last time, in part 1 we introduced you to the LMP, a simple, lost-cost part that connects directly to the Arduino. This time, we will continue with a simple program I call the Musicator.

It is a simple Spectrum Analyser, completely done in software, which reads the output from an earphone plug (on A01), and generates a 5-channel pattern on the LMP, based on the loudness and frequency of the signal.

Here is a sample video:


All you need, beside the LMP and, of course, the Arduino, is a cable with a jack that fits into an earphone socket. It doesn;t matter whether you use the Left or Right channel.

Step 1: The Sketch (LMP_Musicator01)

/*
  • VUmeter / Light organ (Full db presentation)
  • Horizontal presentation - center-out linear display
  • (c) Copyright 2009 QS@quantsuff.com
  • All Rights Reserved
*
  • LED Matrix row:1-5; col:1-6
  • Map PORTB == D8:D12 - pin[row+7] : +v
  • PORTD == D2:D7 - pin[8-col] ; Gnd
  • Our output: col::D2:D7 -ve (LOW) while row::D8:D13 +ve
*/

char msg[] = {
0x11, 0x0a, 0x0e, 0x0e, 0x0a, 0x11, // Starburst
0x00, 0x00, 0x00, 0x00, 0x00, 0x0e,
0x11, 0x19, 0x1e, 0x00, 0x1f, 0x10, // Q U
0x1f, 0x00, 0x1e, 0x05, 0x1e, 0x00, // A
0x1f, 0x06, 0x0c, 0x1f, 0x00, 0x03, // N T
0x1f, 0x03, 0x00, 0x12, 0x15, 0x09, // S
0x00, 0x1f, 0x10, 0x1f, 0x00, 0x1f, // U F
0x05, 0x05, 0x00, 0x1f, 0x05, 0x05, // F
0,0,0,0,0,0 }; //Plus an overrun buffer

// bit pattern, 1 byte per column. Top = lsb

char display[6], hdisp[6] ;

int w10kbuf[2], w1kbuf[20], w100buf[100]; // Assume all zeroed (:P)
int w10kidx, w1kidx, w100idx;
// int w10kmax, w5kmax, w1kmax, w500max, w100max;
long w, w10ksum=0, w1ksum=0, w100sum=0;
int www, w10k, w1k, w100, meter[5] ;

int scale=3; // log scale (start at 23)
int recal=0; //AGC rate
int inPin=1 ; // analogRead on A01

void setup() // run once, when the sketch starts
{
for (int ledPin=2; ledPin<=12; ledPin++) {
pinMode(ledPin, OUTPUT); // sets the digital pin as output
digitalWrite(ledPin,(ledPin<=7)) ; //Disable all cols
}
lmpWrite(msg,0,15); //Cycle delay is 1/10 secs
// (it's really to initialize the counters)
analogReference(INTERNAL) ; // Do not use DEFAULT or EXTERNAL!
int i= digitalRead(inPin); // remove old data
}

void loop() // run over and over again
{
w100 = (w100sum / 23) >> scale; // A cheap filter @ app 100Hz
w1k = abs((w1ksum / 3)-w100) >> scale; // 1kHz
w10k = (www+w10ksum) >> scale; // 10kHz
w= (w100 + w1k + w10k) ; // Total power (VU function)

if (w<13) {
recal++;
if (recal>=15) { //350mS counts before recalibration of meter
recal=0;
scale-= (scale>3);
} }// increase res only if less than 1/2 scale

// if ((w100>33) || (w1k>30) || (w10k>33)) { // log2
if ((w100+w1k+w10k)>83) { // Peaked: make it less sensitive
recal=0;
scale++;
w100= w100>>1;
w1k = w1k >>1;
w10k = w10k >>1;
}
meter[0]= min(w100>>1,31);
meter[1]= min(abs(w100+w1k-w10k)>>2,31);
meter[2]= min(w1k>>2,31);
meter[3]= min(abs(w1k+w10k-w100)>>2,31);
meter[4]= min((w10k*5)>>3,31);

for (int col=0;col<6;col++) {
int c=0;
for (int mm=0; mm<=4; mm++) {
c= c<<1;
c+= (meter[mm] > col);
}
hdisp[col]= c;
}
display[0]= hdisp[4];
display[1]= hdisp[2];
display[2]= hdisp[0];
display[3]= hdisp[0];
display[4]= hdisp[2];
display[5]= hdisp[4];

lmpWrite(display,0,1);

display[0]= hdisp[5];
display[1]= hdisp[3];
display[2]= hdisp[1];
display[3]= hdisp[1];
display[4]= hdisp[3];
display[5]= hdisp[5];

/* Vertical display routine
*
fillVBar(display,0,w100);
fillVBar(display,1,abs(w100+w1k-w10k));
fillVBar(display,2,w1k);
fillVBar(display,3,abs(w1k+w10k-w100));
fillVBar(display,4,w10k);
//fillVBar(display,5,w10k>>1);
//fillVBar (display,5,scale+1);
int i=0;
int s=scale-1;
for (int ii=0; ii<5; ii++) {
i= i | (s & 1);
i= i<<1;
s= s>>1;
}
display[5]= i;
*
*/

lmpWrite(display,0,1);
}

void fillVBar(char disp[],int o, int val) {
int j= B100000; // digit pattern
val= constrain(val,2,127) ;
while (val>2) {
j|= (j>>1) ;
val= val>>1 ;
}
disp[o]=j;
}

void lmpWrite(char disp[],int stchar,int steps)
/* Usage: Loads bit pattern in disp[], starting at stchar,
  • 1 byte per column, left-to-right, top-to-bottom
  • Map PORTB == D8:D12 - pin[row+7] : +v
  • PORTD == D2:D7 - pin[8-col] ; Gnd
*/
{
int col, row, cval, lit=0, ww=0;
long delayStart=millis() + 90*(constrain(steps,1,100)) ;
// Display / loop time
www= 0 ;
do {
for (col=0; col<6; col++) {
cval = disp[col+stchar];
for (row=1; row<=5; row++) { //only 5 lower bits
if (cval & 1) { // light this LED
if (lit==0)
digitalWrite(7-col,LOW); //col needs activating
digitalWrite(row+7,HIGH);
lit++ ; // in case we need to know how many LEDs are lit
}
// Cycle stealing here, to read VU input
ww=max(ww,analogRead(inPin)) ; //Raw volume data (rectified)
www=max(ww,www) ; //Peak retect
delayMicroseconds(80); //This makes sure the panel is
// lit the same period of time.
digitalWrite(row+7,LOW); //Turn LED off
cval = cval>>1 ; // check the next bit
}
digitalWrite(7-col,HIGH); // turn col OFF
w10ksum+= www-w10kbuf[w10kidx]; // This is our 'capacitor'
w10kbuf[w10kidx]=www;
w10kidx++;
w10kidx= ((w10kidx<1)); // 0 or 1
//if (w10kidx>3) w10kidx=0;
//w10ksum=ww ; // Increase the high-end response

w1ksum+= ww-w1kbuf[w1kidx];
w1kbuf[w1kidx]=ww;
w1kidx++;
if (w1kidx>19) w1kidx=0;
//w1kidx= ((w1kidx<19) && (w1kidx++)); // 0 - 19

w100sum+= ww-w100buf[w100idx];
w100buf[w100idx]=ww;
w100idx++;
if (w100idx>99) w100idx=0;
//w100idx= ((w100idx<1) && (w100idx++)); // 0 - 199

lit = 0;
}
}
while (delayStart > millis()) ;
}

Step 2: Program Notes

The major consideration for this program is time.

There are three primary tasks involved in this program:

1) Get the input voltage. The 16MHz Arduino takes 100uS to read the input, which means that even if it does nothing else, the maximum frequency it can detect reliably is 3kHz, so we use something called signal aliasing, beginning with taking a number of readings as quickly as possible (5 in our program) and summing them in separate locations. By taking averages of different ranges of samples, we can approximate the low and mid range frequencies, the difference is assumed to be the high reading. This is by no means exact science but here, we just need rough proportions, so exactness is not critical.

2) We will be doing statistical analysis of a large set of numbers, so everything is reduced to integers to save storage space and to speed up calculations. There will be no conversion from the basic 10-bit input of the Analog Input port.

3) Translation and display on the LMP. Each LED is switched on for about the time it takes to read the Analog pin, and each 'frame' (the 6 x 5 matrix) is shown twice, so that we can display 1.5db steps. This takes about 100mS. The time here is critical - any slower, the eye will start to see flicker, too fast, the 'patterns' will look dim and badly formed.

AGC is incuded, so that the program can work with a wide range of volume and music. A minimum of 150mV is required, but it should be no problem with regular listening levels. The "scale' variable can be lowered for more sensitivity, but will make the program more susceptible to electrical noise - in which casee, bypass the input with a 0.1uF capacitor to ground..

To maximize the 'dynamic' nature of the music, the most significant 2bits are dropped, and the remaining 4 are 'stretched' to fit the 6 horizontal LEDs.

Step 3: What's Next?

Send me your suggestions...

But here's a sneak peek at the next generation of the LMP... COLOR!

Electrically identical to (and interchangeable with) the mono-LMP, except for the addition of current limiting to allow us to use any color LED and still have the same current run through it.

Here's a quick look:


Another video of a soft ballad to show the Musicator's dynamic response: