Arduino project: Stereo Peak Program Meter

Previously, we introduced the Arduino’s analog-to-digital converter (ADC) in detail, looking at successive-approximation A-D conversion and how it’s the best compromise between speed and cost. This time, we start putting some of that theory into practice by building a stereo peak-program meter. Our Peak Program Meter (pictured above) takes audio from any phone or tablet.

Modern-day VU meter

VU or ‘volume unit’ meters have been around for years as a visual aid used by professional audio engineers to gauge the voltage levels of an audio signal. Today with the prevalence of digital audio recording, a different type of meter is more commonly used that presents a linear display of a logarithmic voltage scale and it’s called a ‘peak program meter’ or PPM. When you’re recording audio with a digital recording device, the rule is never allow the signal level to exceed the maximum sample level (known as 0dBFS); otherwise, you end up with clipping distortion, which sounds like your music is playing through a gravel pit. A PPM gives you a clear indication of how ‘close to the wind’ you’re sailing.


PPMs are a must on good-quality digital audio recorders.

How it works

Digitising audio turns an analog voltage into a digital representation or ‘sample’ — the number of bits in the sample determines its precision (and helps with accuracy). It’s why 16-bit audio almost always sounds better than 8-bit audio — more bits, greater precision, better accuracy.

In the real world, the louder the sound, the larger the voltage — and when sampling that voltage, the closer to the maximum digital sample it’ll be. This maximum sample is known as ‘full scale’. Audio signals are typically measured using a logarithmic scale called decibels (dB) and our PPM measures the in-coming audio signal as a logarithmic ratio with respect to this ‘full scale’ limit. For an incoming signal that is at the maximum sample level, that ratio is said to be 0dB relative to Full Scale (FS) — and that’s how we get the ‘0dBFS’ tag.

To work out this relative ratio of an incoming audio sample, you use the basic equation:

dB = 20 x log10(input/FS)

Where ‘input’ is the input signal as a digital sample level, ‘FS’ is the maximum sample level and ‘log10’ is the log function to base 10.

Let’s say the input signal level was half-FS — that would be dB = 20 x log (0.5 / 1) = 20 log (0.5) = -6.02dB. If the input was only at 10% of FS, it’d be dB = 20 log (0.1) = -20dB. With the input at 1/100th of FS, that works out to be dB = 20 log (0.01) = -40dB.

Hopefully you can see a few things from this — every time you drop half the signal level, you drop 6dB; every order-of-magnitude drop (or divide-by-10), you drop 20dB. Since the signal level is always less than full scale, the logarithmic scale always produces a negative number — the larger the absolute number, the smaller the input signal.


Audacity has a peak program meter; we’re making ours with an Arduino.

Dynamic range

When you sample an audio signal, the number of bits in the ADC determines what’s called the ‘dynamic range’, the ratio between maximum and minimum samples (or loudest and softest sounds) it can capture. The 16-bit ADC in your PC’s sound card can capture 65,535 possible signal steps. Using a slight twist to our equation, we get:

dB = 20 x log10 (ADC steps) = 20 log10 (65535) = 96.32dB

You may have heard that audio CDs (also 16-bit sampled) have a dynamic range of 96dB — this is how they get it. However, the ADC in the Arduino’s ATMEGA328P microcontroller is only 10-bit, which gives a maximum dynamic range of 20 x log10 (1024) = 60.2dB. So from that, we know we can only cover a very tiny portion of that 16-bit range — the top 1,024 rungs in a 65,535-step ladder if you like — but represented as a logarithmic scale, we actually cover more than half of that range (54 of 96dB), which is more than enough for our PPM to do its thing.

Making our display

As we said at the top, a PPM is a linear display of a logarithmic scale. We’ve covered the log scale; now let’s look at the linear display. What we want to do is produce a stereo (right- and left-channel) bargraph-style display, so the first thing we need to do is look at the digital I/O ports we have to play with.

For this project, we’re using the Arduino Nano, a compact, ‘breadboard-able’ version of the Arduino Uno R3 using the same ATMEGA328P microcontroller.


Breadboards allow you to build up projects without soldering.

The Nano gives us 14 digital I/O ports, which, if we use one per LED in our display, creates two banks of seven LEDs. That’s not bad, but we can do better — we’ll use 12 I/O ports to drive 20 LEDs, ten per channel. More on that in a moment.

The PPM’s linear scale is designed to make it easy to follow since each LED represents a fixed decibel step, however, most PPMs built from discrete components only manage an approximate logarithmic scale or ‘decibel linearity’ because it’s actually quite hard to do. Our meter gets around that problem but also has perfect decibel linearity, thanks to the ‘mathemagic’ we do inside the Arduino. And as a little something extra, you can set the decibel-per-LED scale in the Arduino sketch (code).


The Arduino Nano is a compact version of the Arduino Uno board.

Driving the LEDs


The meter is two banks of ten LEDs driven by the Arduino Nano.

So how do we get 12 digital outputs to drive 20 LEDs? We use a technique called ‘multiplexing’. It takes advantage of the fact that our eyes are pretty hopeless at noticing fast changes much beyond about 60Hz. We take ten I/O ports connected to ten 1,000ohm resistors, but then use an extra two I/O ports to drive two transistors, each controlling a bank of ten LEDs.


The BC337 transistor also needs to go in the right way around.

If you time everything just right (and timing is everything in this project), you can switch the ten digital outputs between the two banks of LEDs, with each bank showing a different audio channel. Switch between the banks fast enough and it looks like you have two completely separate displays handled by two ADCs, when in reality you only have one ADC and some ‘sleight-of-hand’ multiplexing.


The circuit diagram of our Arduino PPM project.

You can see in the circuit diagram that each LED pair is connected to digital ports D2 through D11 via those 1,000ohm resistors. LEDs are the electrical equivalent of cars with no brakes and these resistors protect both the Nano’s digital ports and the LEDs from blowing up. Standard 5mm LEDs typically only handle 20mA of current before they get cranky and the Arduino can deliver up to 40mA per I/O port. The 1,000ohm resistors limit the current to around 2-3mA — more than enough to light each LED (we’re not lighting up the Opera House) but stay well below everyone’s maximum ratings.


LEDs have a polarity — an anode (A) and a cathode (K).

Protecting the ADC

But the LEDs aren’t the only elements needing protection. We know an ADC turns analog voltage into digital samples, but that input voltage range by default is set to 0-5VDC, the Arduino’s supply voltage. The problem is that audio is an AC (alternating current) signal and its voltage ‘swings’ positive and negative. If we feed a raw AC audio signal straight into the ADC input, not only can’t we measure the negative-going voltage swings, those swings also have the potential to blow up the ADC’s input circuitry.

The solution is to lift up or ‘bias’ each ADC input to a voltage that’s (within 1%) exactly half-FS through two 10,000ohm 1%-tolerance resistors connected between the AREF (analog reference) port and ground. The capacitor at each input allows us to piggy-back our audio signal on top, so that the ADC now sees the positive and negative voltage swings about this new half-way point. It requires a little more mathematical judo, but it means all of our circuitry stays happy. The 1,000 ohm resistor at the very front of the input ensures that no damage can be done if the audio voltage peak exceeds the Arduino 5VDC supply voltage (within reason).

Building the meter

We’ve engineered our meter on a standard 830-tiepoint breadboard, but the final version would receive a purpose-built circuitboard or general-purpose veroboard. The audio input can come from your phone or tablet via a 3.5mm to 3.5mm stereo cable. We’re using 5mm high-brightness white LEDs but you can choose any colour you wish. A Fritzing overlay diagram gives you a closer view of how we engineered the project.


The overlay diagram for our Arduino PPM.

The sketch

The multiplexing adds some complexity but it’s the sketch (that’s Arduino for ‘program code’) that pulls the project together. That said, we’ve deliberately kept the sketch as simple as we can so you can see how each section works, the whole thing taking just 40 lines of code. There are four mains sections — setup, digital sample acquisition, mathematical conversion to dB scale and driving the LEDs.

The standard logarithmic function is just one mathematical hurdle we have to jump, but it’s not part of the basic Arduino codebase, so we include it by adding the math.h header file through the #include <math.h> code line. This file comes with the Arduino IDE, so we’re not including in our project download. Math.h provides a number of other useful functions, including sine, cos, tan and their trigonometric inverts.


The whole sketch for our simple PPM takes just 40 lines of code.

Remember that an Arduino sketch is designed to run through the setup() procedure once and infinitely around the loop() procedure until power is removed, so the first thing we do in the setup() routine is set the analog voltage reference (AREF). Our ADC has to compare the input voltage to a reference voltage in order to create a meaningful sample. As we mentioned before, the Arduino Nano/Uno R3 just uses the 5VDC supply voltage, but we can gain greater sensitivity by using the ATMEGA328P’s internal 1.1VDC voltage reference instead. The analogReference(INTERNAL); codeline sets AREF to this new reference.

To further simplify coding, we add the digital I/O pin numbers (the ten LED outputs plus the two multiplex drivers) to an integer array and use that array here to assign each one as a digital output. After that, we hit the main loop() procedure.

The first thing we do here is acquire our digital sample through the getPPMsample() procedure. A standard PPM has a five-millisecond integration time and we cover that by getting the ADC to take 48 samples, noting the highest peak. With the default ADC sampling rate 9.6kHz, multiply that by 48 samples and you get five milliseconds.

To work out which channel we sample, we create an integer variable called ch and use the command ch=!ch to toggle it between one and zero. If ch equals zero, we read from analog input A0; otherwise we read A1.

Now here’s where we correct the ADC sample scale — once we have our peak sample, we need to change the scale so that the half-FS point (sample-level 512) becomes the new zero-point, allowing us to work out the peak sample. Our ADC has 1,024 steps so half-FS has to be 512. The solution is to simply subtract 512 from each sample and we now change the scale from 0/1024 to -512/+512. We take the absolute value of the peak and work out the dB ratio of the sample using the single equation:

dBAudio = 20 * log10 (abs(maxAudio-newZero)/newZero);

Changing the scale loses us 6dB of dynamic range because we now have 512 steps instead of 1024, but this has to be done to measure the peak sample of both positive and negative-going voltage swings.

Back in the loop() procedure, we now flip the multiplex switch and using the for() loop, we switch on the appropriate number of LEDs in the bargraph to match the dB scale of our peak sample for that audio channel. Once that’s done, the loop() procedure restarts, we take our next set of samples and around we go again.

Port manipulation

The standard way of writing to digital outputs is through the digitalWrite command, however, there are times where it’s not fast enough. During development, we had a problem whereby we were getting brief ‘crosstalk’ errors with one sample lighting up the other LED bank just briefly as the multiplexing transistor took time to switch off. The solution was to write the digital outputs using the fast PORT command. PORTD covers D0 to D7 and PORTB handles D8-D13, so by writing PORTD and PORTB to zero, the I/O ports drop to zero much faster and the crosstalk display errors are fixed.

Only the start

The Arduino’s ADC might well be modest by today’s standards, but it’s still arguably its most important feature, linking the real analog world to the digital computing environment. And this is just one way you can use the ADC. We’re sure you can come up with dozens more!

Sketch download

You can download the sketch for this project (project #15) from our Arduino page over at It’s designed for Arduino IDE v1.0.5.

  • Marco Pashkov

    What can I do to get less noise and more accuracy in my measurements?

    I have replicated your setup – at least the input part, as I am using the neo-pixels for my output instead of straight up LEDs.
    However, despite working in general I get quite a bit of noise – which in turn lets the LEDs flickr a lot.
    I also wanted to auto-calibrate the signal by continuously measuring the extreme values. However, roughly every 10 seconds there is a super-high peak, which makes this more difficult. Could this be coming from the capacitors?

    Would you have any suggestions on how to improve your design?

  • Just make sure you have your wiring right. There shouldn’t be much a problem with noise if you’ve built it using our circuit, so check that your ground-return for the audio input is separate from the transistors and the switching LEDs.
    What you want to avoid is what’s called a ‘ground loop’, which can see some of the output switching signal coupled back into the input, which can upset the input level.
    What I would do is first short the analog input to ground and check the output – if you’re getting flickering in the LEDs with the input grounded, you’ve either got a code problem or a wiring problem. As I said, the version I built with that code didn’t show any flickering, so check your wiring – and look for any ground-loops.
    As for that ‘super high peak’, this is a good opportunity to test your debug skills – I can’t give you much help because I can’t see your build. But go through the code (it’s not that complicated) and try changing things around to see what’s going on.
    If you’ve used decent caps, it shouldn’t be caused by them.
    Not quite sure what you mean by ‘more accuracy’ though – the circuit is designed (along with the code) to give an accurate 6dB change between each LED – remember, decibels are logarithmic. You can change this by adjusting the equation in the code mentioned in the text.

  • Burt not ernie

    Show a video of it working

  • David B Levi

    Hey, I’m trying to use this circuit with an Arduino Micro. The internal reference voltage of the Micro is 2.56VDC instead of the 1.1VDC of the Uno. Any suggestions for circuit or sketch changes because of this? Thanks.

    • You shouldn’t need to change the circuit – the only thing a different Aref does is make the circuit less sensitive because you now have 2.56V spread over ten LEDs, instead of just 1.1V.
      Unfortunately, I’m not sure there’s much you can do since the ADC uses Aref as it’s reference (I’d have to read the ATMEGA32u4 data manual) – I don’t think you can set this to something else. Just do a search for ATMEGA32u4 datasheet and download the PDF from the atmel website – it’ll tell you whether you can or not.

      • David B Levi

        HI Darren,
        Thanks for the quick response. So the lack of sensitivity is exactly what I’m experiencing. I’m wondering if the following changes would address this:
        1. Change the analogReference(INTERNAL) to analogReference(EXTERNAL) in the sketch.
        2. Wire up a voltage divider off the arduino’s 3.3v pin to produce 1.1v. Connect that to the AREF pin (and to the two 10k resistors currently connected to AREF)
        From my understanding, that would make the ADC use 1.1v as a reference, which seems like it would correct the sensitivity issue. Does that sound like it would work?

        • Possibly – just check to see what load AREF puts on your resistor divider. Hard to explain here, but if AREF loads down your divider, it may end up being less than 1.1V. Best best is to use a multimeter and measure the AREF voltage when you’re done to be sure.
          But otherwise, yeah, I think that might work.

          • David B Levi

            Thought I’d follow up on this in case anybody tries this circuit with an Arduino Micro (I just got back around to trying it 10 months later!!). I got it to work with a voltage divider as discussed above. The comment about AREF loading down the divider was accurate, it was pulling it down to around .8 volts. That was using three 1k resistors to split the micro’s 3.3 volt output down to 1.1 volts (two 1k for R1 in the divider, and one 1k for R2). Switching one of the 1k resistors in R1 to a 470 results in about 1.05 volts at AREF, which did the trick, and the circuit is working for me.

  • Marius Paškevičius

    Thanks for the article. It is very clear.
    I just wonder about analog input biasing part when you apply voltage divider between gnd and AREF. Does AREF pin output 1.1V when using internal voltage reference? Othervise it would not work I suppose and blow ADC…

    • Yep, that’s what the AREF is supposed to do – it’s an output voltage, albeit, only for light-duty as we’re using it here. It biases the ADC inputs so that they sit at 0.55VDC. The capacitor connected each input acts as a DC-blocking capacitor to ensure that no DC from the audio source feeds into the ADC.

      In terms of voltage level accuracy, the 1kohm resistor in series with the DC-block capacitor to each ADC input will reduce the input signal amplitude, by a factor about 1/6 (the two 10kohm resistors forming the voltage divider act as two resistors in parallel to AC signals, giving an AC resistance of 5kohm – and this forms a voltage divider with the 1kohm input resistor).
      The theory for this starts to get a chunky quickly.
      To maximise scale accuracy, feed in a steady state 1kHz sinewave at ‘0dbFS’ voltage level and adjust the code until you get that on the LED display.

      • Marius Paškevičius

        Ok, I will try it later, but now I wonder about accuracy – you use 10 LEDs and you set 3dB per LED sensitivity. So I suppose your dynamic audio range is 3*10=30dB and it corresponds ADC value of around 30 (20*log10(30) = 30 dB). Having 0dBFS = 512 ADC value (it is 0.55V if using 1.1V AREF), 1 ADC step corresponds ~1mV (0.55/512=0.001 V), so audio signal max amplitude is expected to be 30mV.
        Am I correct with my calculation? I plan to have 15 LEDs in my project for all dynamic range, so for 30dB range, each LED for me would represent 2dB step (30 dB/15 LED = 2 dB/LED)?

        sorry to bother you with math

        • Should be the same. They’re essentially the same chip – ATMEGA328P.
          AREF should provide an output of 1.1VDC that’s the reference for full-scale over the full 10-bit ADC range. By biasing the A0 and A1 inputs to half-AREF, we setting the ADC to steady-state 512 (half the 10-bit/1023 range). An analog signal then swings from 0 to 1023 about the 512 ‘ground’.
          Sorry, I’m in the middle of something at the moment and don’t have time to delve into it, but load the PPM sketch and measure the output again – just to check if you’ve got it set right. The AREF pin should provide 1.1VDC output when the ‘internal’ argument in analogReference.

  • John Barnetson

    Hi Darren,
    It is fantastic that I can access this sketch, I really enjoyed building it. I merged it with your VU meter and a Mega to create a good looking bit of kit. I ask as a noob however, I want to control the volume of the sound file that will also lower or increase the VU output. I have plugged my 10K pot any which way I can to no avail. Any ideas?


    • John, if you’re just looking to control the level of input audio, you’ll need a dual-ganged pot if you’ve built this as a stereo setup. Connect one end of the pots to ground, the other end to the input signal and the wipers (the middle bits) go to the 1kohm resistors feeding the Arduino analog inputs.
      Hope that helps,