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.
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.
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.
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).
Driving the LEDs
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.
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.
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.
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 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.
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.
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!
You can download the sketch for this project (project #15) from our Arduino page over at apcmag.com/arduino.htm. It’s designed for Arduino IDE v1.0.5.