Arduino VU meter

I wanted to implement a VU meter in an amp project I’m working on. There are several dedicated IC’s out there like the famous LM3915, but I already had an Arduino (AtMega 328p) on board for some other tasks. Why not use that! More fun and saves an IC.

There are several examples of Arduino driven VU meters floating on the net, but with respect to those fun and educational DIY projects, they don’t pass as a proper VU meter. Very jumpy, nervous, linear scale, no peak hold and only half the waveform resolution (more on that later). To get it as functional and ‘good looking’ as the LM3915, some hardware and software additions have to be made.

Preparing the input
Audio waveforms consist of both peaks (positive voltage) and valleys (negative voltage). The Arduino’s analog pins only work in the positive range. To prevent driving the inputs negative and to capture the entire waveform, a bias point has to be set. This DC bias point should preferably be half the Arduino’s working voltage: 5V/2 = 2,5V so the audio can swing equally both ways. The easiest way to do this is by means of a voltage divider connected between Vcc and ground. A voltage divider is a network of two resistors in series. If these resistors are of same value*, the point where they connect will measure half the input voltage. They can be of high value, like 100kΩ or higher. Very little current is needed.Voltage divider

The protect the preceding stage (the audio source) from the DC point we’ve just created, a coupling capacitor has to be used. It should be of a high enough value not to cause any low frequency cutoff. A 100nF film capacitor is very common**

The analog input pin will now see 2,5VDC with a audio waveform swinging above and below this point. Now it’s time to capture this waveform and translate is to a visual output.

*You can calculate the voltage of a non-symmetrical divider with: V = R2/R1+R2 where R1 is the top- and R2 the bottom resistor.
** To calculate the low frequency rolloff point, the following formula can be used: f = 1/2πRC

Waveform resolution
The main reason to use the bias network is to capture the entire waveform instead of only the peaks (which would happen if we would connect an audio source directly to the analog pin). This doubles the resolution to convert to visual output. Why is this important? The AtMega 328 samples the analog pins at a maximum rate of 10.000 times a second of 10 kHz. Using two pins, e.g. for a stereo VU meter, will half the sample rate per pin, making 5kHz, which is well below the maximum audio frequencies used. The chance you’ll only measure somewhere on the slope of a waveform instead of the peak is greatly increased because of this. Off course there will be some software averaging, but using  both sides of the waveform doubles your chance to get a accurate reading.

The code
I’m not promoting to blindly copying my code. It’s there to show my thought process  to get what I found was the best performance for the VU meter and it might give you some needed inspiration. If you do just want to copy, by all means, do so. I’ll chop it up into manageable pieces for now (complete code at the end of the page):

First, the analog pin is read into a variable (audioLevel). The full measuring range will give a value between 0 and 1024. With no input, the analog pin should measure a value of 512, because of the center bias point. The next ‘if statement’ ignores measured values between 500 and 524 to account for noise and a not-so-perfect bias network to prevent a jittery meter. Next, the up-going waveform values are converted from 512-1023 to 0-512 and the down-going waveform from 512-0 to 0-512. Now both the peaks and valleys will give an equal measured value for analysis.

void vuMeter() {
  audioLevel = analogRead(AUDIO_INPUT);   //Read audio input
  if(audioLevel < 500) {                  //Audio is biased on 2.5 volts (half Vcc) by voltage divider.
    audioLevel = map(audioLevel,0,512,512,0);  //Convert downgoing signal to positive values.
  } else if(audioLevel >= 524) {          //Convert upgoing signal to the same range as the downgoing.
 audioLevel = map(audioLevel,512,1023,0,512);
  } else {                                //for detection the highest value within the detection time.
 audioLevel = 0;
}

You want the VU meter to indicate the peaks of the input waveform. Because there’s a large chance not every sample is at a peak but on a slope, some peak detection has to be done. The highest measured value is therefor stored in a variable (audioPeak)  and updated if a higher value is measured for as long as I want the peak detection to continue (vuSmoothDelay). Too short and you might miss the peaks, too long and the meter will be sluggish and choppy. I chose 50 miliseconds and it provides a nice responsive meter without it being too nervous.

if(audioLevel > audioPeak) {
  audioPeak = audioLevel;       //Save the highest measured value to variable
}

To keep track of the time passed for the peak detection (vuSmoothTimer), the time it took to complete one or more loops (previousVuSmoothTimer)  is compared to the current millis() (you should be aware of this statement, if not, look it up). Once more than 50ms has passed, the VU level is determined (vuLevel) according to the highest peak value.

vuSmoothTimer = millis() - previousVuSmoothTimer;  //Check if the detection time has passed.
if(vuSmoothTimer > vuSmoothDelay ) {
  vuLevel = (log10(audioPeak)*10)/vuSensit; //Convert the audioPeak to logarithmic and set the VU scaleif (vuLevel < previousVuLevel) {
  vuLevel = previousVuLevel - 1;
}

For most audio measuring purposes, the scale is logarithmic (base 10). The VU meter is no exception. The Arduino IDE provides a ‘log’ math operator, but that’s base 2! Include the math.h library (#include <math.h>) to utilize the log10 statement. Multiply the outcome by 10 for power ratios or 20 for gain ratios.

Next, you need to reduce the measured and calculated VU level to the amount of used LED’s. A variable (vuSensit) is used to do this. I found my value by trial and error. Since you want the meter to utilize all LED’s at maximum input, this ‘sensitivity level’ is dependent of the expected input signal and can vary per application.

Another incorporated visual trick; when the music goes from e.g. full on to completely silent, a VU meter never just shut off. Instead, it goes down gradually. I do this by checking after every 50ms cycle if the new VU level is lower than the previous one (previousVuLevel). If so, don’t just use the measured value, but use the old value -1 instead. Again, giving a smooth visual output.

After the next VU level is determined,  the subroutine to write to the LED’s is called, the peak measurement is reset and the timer for peak detection is reset.

  writeVu();
  audioPeak = 0;                     //Reset audio peak meter
  previousVuSmoothTimer = millis();  //Reset smoothing timer
  }
}

The subroutine to write to the LED’s starts with a for loop to set the pins HIGH or LOW, depending on the vuLevel variable from the previous subroutine. This works because the output pins are in an array.

void writeVu() {
  for(int i=0;i&amp;lt;3;i++) {
    if(i&amp;lt;vuLevel) {
      digitalWrite(VU_LEDS[i], HIGH);
    } else {
      digitalWrite(VU_LEDS[i], LOW);
    }
  }

For the last visual trick, I wanted the last LED, which is used to indicate the amp is running at it’s max, the remain lit after the input has dropped again. This to make sure the user is aware of the possible overload. A delay of 0.6 seconds (peakLedDelay) looks nice.

  if(vuLevel == 4) {   //Peak LED behaves different from other LED's, so it's excluded from the for loop.    digitalWrite(VU_LEDS[3], HIGH);
    previousPeakLedTimer = millis(); //Reset peak LED timer
  }
  if(millis() - previousPeakLedTimer &amp;gt; peakLedDelay) {
    digitalWrite(VU_LEDS[3], LOW);
  }
  previousVuLevel = vuLevel;
}


Recap
I hope this example helps you to take the average simple VU meter to the the next level. With just a few components and some code, the Arduino can perform just as well as any dedicated VU meter IC. With a little imagination, any amount of visual trickery can be achieved. Feel free to leave a comment if any help or deeper explanation is needed. You can find the uninterrupted code below.

void vuMeter() {
  audioLevel = analogRead(AUDIO_INPUT); //Read audio input
  if(audioLevel &amp;lt; 500) { //Audio is biased on 2.5 volts (half Vcc) by voltage divider.
    audioLevel = map(audioLevel,0,512,512,0); //Convert downgoing signal to positive values.
  } else if(audioLevel &amp;gt;= 524){ //Convert upgoing signal to the same range as the downgoing signal.
    audioLevel = map(audioLevel,512,1023,0,512);
  } else { //for detection the highest value within the detection time.
    audioLevel = 0;
  }
  if(audioLevel &amp;gt; audioPeak) {
    audioPeak = audioLevel; //Save the highest measured value to variable
  }
  vuSmoothTimer = millis() - previousVuSmoothTimer; //Check if the detection time has passed.
  if(vuSmoothTimer &amp;gt; vuSmoothDelay ) {
    vuLevel = (log10(audioPeak)*10) / vuSensit; //Convert the audioPeak to logarithmic and set the VU scale.
    if (vuLevel &amp;lt; previousVuLevel) {
      vuLevel = previousVuLevel - 1;
    }
    writeVu();
    audioPeak = 0; //Reset audio peak meter
    previousVuSmoothTimer = millis(); //Reset smoothing timer
  }
}

void writeVu() {
  for(int i=0;i&amp;lt;3;i++) {
    if(i&amp;lt;vuLevel) {
      digitalWrite(VU_LEDS[i], HIGH);
    } else {
      digitalWrite(VU_LEDS[i], LOW);
    }
  }
  if(vuLevel == 4) { //Peak LED behaves different from other LED's, so it's excluded from the for loop.    digitalWrite(VU_LEDS[3], HIGH);
    previousPeakLedTimer = millis(); //Reset peak LED timer
  }
  if(millis() - previousPeakLedTimer &amp;gt; peakLedDelay) {
    digitalWrite(VU_LEDS[3], LOW);
  }
  previousVuLevel = vuLevel;
}
Advertisements

Tags: , , , , , ,

About thijsdebont

Enthausiast electronics DIY'er and music lover. Passion for (tube) audio equipment in particular, but I get sidetracked by all things digital as well.

7 responses to “Arduino VU meter”

  1. Marcus Mason says :

    This code was perfect for a VU meter I was making with a LPD8806 LED strip 🙂

    Thanks!

  2. Darren Faulke says :

    You give an equation for calculating the low frequency cut-off point, but it isn’t clear which value of R should be used. My electronics is a bit rusty but as far as I know, the value of R is actually the input impedance of the ADC input and not the resistances of the divider. Trying to find the input impedance of the ADC input is a lesson in patience though. Similar circuits, e.g. spectrum analyser projects that I’ve come across all use different values for the coupling capacitor. Do you have a strict basis for your choice, or was it empirical, or just a good guess?

    • thijsdebont says :

      Hi Darren,
      in this case, both the ADC’s and voltage divider input impedance have to be taken into account. Since the ADC input impedance is so huge (several Mega ohms) compared to the voltage divider, we can ignore it. This leaves us with the two 100k values in my example. As far as AC (e.g. audio signals) are concerend, the powersupply forms a short circuit, effectivly paralleling the 100k resistors to each other, resulting in a 50k value for ‘R’ to use in the equation. I hope this helps!

  3. William Petersen says :

    Nice tutorial, I have been looking for a vu-meter code with peak hold all over the net and have found nothing, Could you please provide a complete functional vu-meter code? Thanks!

  4. Carlos says :

    Hey, great tutorial. I’ve been using the audio (left channel) input directly from the audio controller of my PC and my vu meter works, but you said that the arduino needs only positive values. I’ve done some debugging and I get values above 0 if I do analog read, does this mean that everything is ok and I don’t need that voltage divider? Thanks

    • thijsdebont says :

      Hi Carlos, in your current setup, the Arduino will only measure the positive side of the audio waveform. The negative is ignored so to say. The voltage divider is in place so the Arduino can measure both the positive ánd the negative parts of the waveform, making it more accurate and higher resolution. But it will also work without it. This will make programming a bit easier too, since you don’t have to do the conversion (map) of the two parts.

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

%d bloggers like this: