VU Meter

This experiment is something that has to happen with any LED stripe. We will create a VU meter. At the Arduino side this is a piece of cake with the Blinkenlight shield.

//
//	www.blinkenlight.net
//
//	Copyright 2011 Udo Klein
//
//	This program is free software: you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or
//	(at your option) any later version.
//
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program. If not, see http://www.gnu.org/licenses/


void setup() {
	for (uint8_t pin=2; pin<20; ++pin) {
		pinMode(pin, OUTPUT);	
	}
	Serial.begin(9600);
	Serial.println("ready, send characters a-s to control output");
	set_volume(0);
}

void set_volume(uint8_t volume) {
	volume+= 2;
	for (uint8_t pin=2; pin<20; ++pin) {
		digitalWrite(pin, pin<volume);
	}
}

void loop() {
	uint8_t volume = Serial.read() - 'a';
	if (volume < 't'-'a') {
		set_volume(volume);
	}
}

The sketch will initialize the 18 LEDs which do not belong to the serial port. Then it will listen for ascii characters. Depending on the character it will then activate the desired number of LEDs and continue to listen. There is nothing special about using characters instead of numbers. The only reason I chose this direction is that this allows for very easy testing with the serial monitor. So start the serial monitor and send some characters to the Arduino and see if everything works.

Simple VU Meter

On the computer side of the serial interface the volume information is send. I achieve this with a small Python script.

#!/usr/bin/python
# -*- coding: utf-8 -*-

#
#	www.blinkenlight.net
#
#	Copyright 2011 Udo Klein
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program. If not, see http://www.gnu.org/licenses/


import alsaaudio
import audioop
import serial
import sys
import math

port = "/dev/ttyUSB0"
baudrate = 9600

if len(sys.argv) == 3:
	output = serial.Serial(sys.argv[1], sys.argv[2])
else:
	print "# Please specify a port and a baudrate"
	print "# using hard coded defaults " + port + " " + str(baudrate)
	output = serial.Serial(port, baudrate)
	
input = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,alsaaudio.PCM_NONBLOCK)

input.setchannels(1)                          # Mono
input.setrate(8000)                           # 8000 Hz
input.setformat(alsaaudio.PCM_FORMAT_S16_LE)  # 16 bit little endian
input.setperiodsize(320)

lo  = 2000
hi = 32000

log_lo = math.log(lo)
log_hi = math.log(hi)
while True:
	len, data = input.read()
	if len > 0:
		# transform data to logarithmic scale
		vu = (math.log(float(max(audioop.max(data, 2),1)))-log_lo)/(log_hi-log_lo)
		# push it to arduino
		output.write(chr(ord('a')+min(max(int(vu*20),0),19)))

Really nothing fancy. It uses the advanced Linux sound architecture (ALSA) binding for Python to do the real work. If the script will not run due to missing libraries you will need to install the ALSA library. With Debian / Ubuntu it is available as a software package. You can easily install it with

sudo apt-get install python-alsaaudio

After the script has initialized everything it will read the microphone. The results are mapped to a logarithmic scale. This is because all VU meters measure dB which is a logarithmic scale. This is because perceived loudness is roughly logarithmic.
This python script may not work with non Linux machines. The general logic is simple enough. So replicate it in your language of choice.
One more afterthought. Sometimes the VU meter seems to fails. Usually this is because the microphone is deactivated in the system settings. Activating the microphone and setting the line amplification properly usually fixes this problem. Proper amplification is really key. If the amplification is to low the LEDs will stay off. If it is to high they will stay always on. No big deal but this has to be kept in mind.

OK, so now this is a basic VU meter. But of course the Arduino has more than enough computing power to make something a little bit more fancy. Let’s add a peak volume indicator with a „falling down“ effect.

Advanced VU Meter

Here is the corresponding sketch.

//
//	www.blinkenlight.net
//
//	Copyright 2011 Udo Klein
//
//	This program is free software: you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or
//	(at your option) any later version.
//
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU General Public License
//	along with this program. If not, see http://www.gnu.org/licenses/


#include <MsTimer2.h>

volatile uint8_t current_volume = 0;
volatile uint8_t current_top_volume = 0;
volatile uint32_t speed = 0;
volatile uint32_t height = 0;


void drop() {
	if (current_volume < current_top_volume) {
		// volume decreased recently
		// ensure top_volume LED is lit
		digitalWrite(current_top_volume+1, HIGH);

		// now let the top_volume indicator follow down
		++speed;
		height += speed;
		
		if (height > 20000) {
			height-= 20000;
			digitalWrite(current_top_volume+1, LOW);
			--current_top_volume;
			digitalWrite(current_top_volume+1, HIGH);
		}
	}
}

void set_volume(uint8_t volume) {
	cli();
	current_volume = volume;

	if (current_volume >= current_top_volume) {
		current_top_volume = current_volume;
		speed = 0;
		height = 0;
	}

	for (uint8_t pin=2; pin<20; ++pin) {
		digitalWrite(pin, pin < current_volume+2);
	}
	sei();
}

void setup() {
	for (uint8_t pin=2; pin<20; ++pin) {
		pinMode(pin, OUTPUT);
		digitalWrite(pin, LOW);
	}
	Serial.begin(9600);
	Serial.println("ready, send characters a-s to control output");
	set_volume(0);

    MsTimer2::set(1, drop);
    MsTimer2::start();
}

void loop() {
	uint8_t volume = Serial.read() - 'a';
	if (volume < 't'-'a') {
		set_volume(volume);
	}
}

This extends the basic script slightly. Most notably there is now the drop function. This function is registered at MsTimer2 and called once per millisecond. It communicates with the set_volume function by means of the 4 „volatile“ variables. The keyword volatile tells the compiler that these variables must not be kept in registers. This is required because it would be very bad if the interrupt routine would modify it and the main loop would not get the changes. Very keen readers will correctly infer that volatile is only needed for variables that are read from the main program. But in dealing with ISRs / threads defensive programming is mandatory. Mainly because troubleshooting is so annoying. Thus I declare everything that is accessed from different threads as volatile.
The next thing are the cli() / sei() statements in the set_volume function. They are a safety measure to ensure that no interrupts happen during processing of this function. Here the point is that anything else might lead to so called race conditions. Or it might happen that an interrupt strikes immediately in the middle of a (non atomic) 2 byte integer operation. Again it is possible to go through all possible paths and figure out if it is really needed. But there is no point in saving 2 cycles just „wait faster“ for the serial connection. The risk of removing them is „strange“ behavior that strikes during presentations and that is annoying to debug.
After the safety measures are in place the actual logic is pretty simple. In addition to turning the LEDs on and off I will remember a „top“ value in the set_volume function. But only if it exceeds or matches the last top value. No matter how the top value is computed the corresponding LED will be lit in addition. If a new top value is computed the „speed“ value will be reset to 0. So this part of the code basically ensures that the top value is „pushed up“.
The drop() function that is called each millisecond on the other hand is responsible for lighting the top value LED and pushing the top value down again. It does so by incrementing „speed“ 1 tick each millisecond and height depending on speed. So after n milliseconds speed will be n and height 1+2+3+…+n or n(n+1)/2. Which is a pretty good approximation of Newton’s law. By trial and error I found that a fall height of 20000 per LED seems pretty natural. Each 20000 the top_value will be decreased. Height will be decreased by 20000 as well because the absolute fall height is irrelevant. We only need to know then the next top_value change is to be triggered.

The following video shows the improved VU meter with an Acapella cover of a Nat King Cole song – performed by my friends Gersom and Verity from De Koning-Tan Music.

Just in case you might wonder all 4 VU displays show the same shield. For two of them I defocused the camera on purpose.

15 Responses to VU Meter

  1. fL0 says:

    in your python script, when you are calculating the max and min dB values, you are using log_10 and when you are calculating the vu dB SPL you are using log_2. I noticed you are multiplying with 20 later. so the formula is dB (SPL) = 20 * log_10(v1/v2).
    this seems to be inaccurate. am i missing something?

    • This is definitely a mistake, I will fix it during the weekend. Fortunately it has negligible impact on the proper function of the program. That’s why I did not catch it during testing.

  2. OK, I fixed it and retested it. As expected the difference is barely visible. But thanks a lot for paying attention. This is highly appreciated :)

  3. Larry says:

    what microphone? where does it go, yes i am not as smart as blinkenlight, where the hell is a microphone or what pin do i connect it to, I bought this thing I coulod use some info for it.

    • Good question. The answer is: “no microphone”. At least none that connects to the Arduino. This setup is driven by a computer that connects to the Arduino. That is the Python script running in the computer will read the microphone that is connected to the computer. The Arduino is acting as a display driver only. You can try this by sending numbers to the serial interface and see how the display will change.

  4. Larry says:

    that makes sense now, cool, python script, i will give her a try…thanks.

  5. I like the fact that you spell Python skript with K ;) I was looking for something like this – thanks.

  6. Vickie says:

    Hello there! I recently desire to give a massive browses upward for that nice data you’ve got the following on this post. I am going to likely to end up returning in your website for additional shortly.

  7. matulamatete says:

    ich bin ein begeisterter vu meter fan und auch vom blinkenlighty begeistert. ich bin amateur analog bastler und neu in der digitalen mikrokontrollerwelt. theoretisch sollte der sketch auch über einen elektretmikrofon und opv (100x) über z.b. analogpin 6 od.7 ansteuerbar sein. können die mir beim code etwas weiterhelfen?

  8. matulamatete says:

    Hallo zusammen. Gerne würde ich den VU Meter mit “falling down”-Effekt über ein elektret mikrofon (100x) ansteuern. Ich bin “digitaler Neuling”, aber eigentlich müsste es über einen der 2 zusätzlichen analog Pins A6 od. A7 möglich sein. Das Pin Mapping überfordert mich aber. Vieleicht kann mir jemand mit dem Code behilflich sein?
    Ich danke schon jetzt.

    • Gute Idee. Ja, kann ich gerne. Ich werde das einfach in meinem Blog als Experiment breittreten. Da ich November schon geschrieben habe und Dezember gerade in Arbeit ist käme das am 1. Januar dran. Ich würde den Code aber “inoffiziell” schon mal zum Testen zur Verfügung stellen. Wie wäre das?

  9. matulamatete says:

    Super Idee. Ich übe zur Zeit mindestens 2h/täglich Programmierungsgrundlagen. Als Anfänger im Selbststudium ist es nicht nur einfach. Ich habe den Blinkenlighty dieses Wochenende “vergrössert”. In der Front eines DJ Pults habe ich auf 1,6 m Breite 20 cyan Dioden eingebohrt und verdrahtet. über Pfostenleisten und Flachbandkabel konnte ich sie bequem mit den Arduino verbinden. Der Grosse Larsson Scanner kam beim Publikum besonders gut an. Für den VU Meter mache ich zur Zeit eine Installation auf einer drehenden Scheibe. Ich freue mich schon sehr auf den Code. Ein simpler VU Meter mit 14 Dioden ist mir bereits gelungen.
    Danke und bis bald, Matthi

  10. Djerk Geurts says:

    Now if only I could get alsaaudio to play nice with an aloop device rather than a microphone…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s