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.
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.
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.
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.
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 🙂
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.
that makes sense now, cool, python script, i will give her a try…thanks.
I like the fact that you spell Python skript with K 😉 I was looking for something like this – thanks.
Yeah – I am not a native speaker. I fix it although you like it 😉
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.
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?
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?
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
Sehr gut. Lade mal irgendwo Bilder hoch damit ich das auch sehen kann 🙂
Now if only I could get alsaaudio to play nice with an aloop device rather than a microphone…
Very good project. Thank you very much.
Unfortunally, I do not see any activity since 2014. I hope, there is still someone who could answer my Question: How could I implement python code for the VU-Meter in Windows (e.g. Win10)? How can I install alsaaudio and audioop?
Greetings,
Äd
I have no idea how you do this on windows. However I would assume there should be other options as well (e.g. Pygame). In the end anything that will allow you to feed it into the serial interface in more or less real time would do it.
Hello Udo,
thank you for your answer. Ok, I will think about it.
I know Python, but not so much about the access to the sound card. Maybe someone of the readers may have an idea.
Regards,
Äd