Introduction: Non-Addressable RGB LED Strip Audio Visualizer

I’ve had a 12v RGB LED strip around my TV cabinet for a while and it’s controlled by a boring LED driver that lets me choose one out of 16 pre-programmed colours!

I listen to a lot of music which keeps me motivated but the lighting just doesn’t set the mood right. To fix that decided to take the audio signal that was given to my speaker through AUX (3.5 mm jack), process it and control the RGB strip accordingly.

The LEDs react to the music based on the magnitude of Bass (Low), Treble (Mid) and High frequencies.

The Frequency range – Colour is as follows:

Low – Red

Mid – Green

High – Blue

This project involves a lot of DIY stuff because the whole the circuit was built from scratch. This should be pretty easy if you are setting it up on a breadboard, but quite challenging to solder it onto a PCB.

Supplies

(x1) RGB LED Strip

(x1) Arduino Uno/Nano (Mega is recommended)

(x1) TL072 or TL082 (TL081/TL071 are fine too)

(x3) TIP120 NPN Transistor (TIP121, TIP122 or N-Channel MOSFETs like IRF540, IRF 530 are fine too)

(x1) 10kOhm potentiometer linear

(x3) 100kOhm 1/4watt resistors

(x1) 10uF electrolytic capacitor

(x1) 47nF ceramic capacitor

(x2) 3.5 mm audio connector – Female

(x2) 9V battery

(x2) 9V battery snap connector

Step 1: Understanding the Types of RGB LED Strips

There are two basic kinds of LED strips, the "analog" kind and "digital" kind.

Analog-type(fig 1) strips have all the LEDs connected in parallel and so it acts like one huge tri-colour LED; you can set the entire strip to any colour you want, but you can't control the individual LED's colours. They are very easy to use and fairly inexpensive.

The Digital-type(fig 2) strips work in a different way. They have a chip for each LED, to use the strip you have to send digitally coded data to the chips. However, this means you can control each LED individually! Because of the extra complexity of the chip, they are more expensive.

If you are finding it difficult to physically identify the differences between the analog and digital-type strips,

  1. Anolog-type use 4 pins, 1 common positive and 3 negatives i.e. one for each colour of RGB.
  2. Digital-type use 3 pins, positive, data and ground.

I will be using the Analog-type strips, because

  1. There are very few to no Instructables that teach how to make a music reactive Analog-type strip. Majority of them focus on the Digital-type and it is easier to make them react to music.
  2. I had some Analog-type strips lying around.

Step 2: Amplifying the Audio Signal

The audio signal that is sent out through the audio jack is

an analog signal which oscillates within +200mV and -200mV. Now this is a problem is we want to measure the audio signal with one of Arduino’s analog inputs because the Arduino’s analog inputs can only measure voltages between 0 and 5V. If we tried to measure the negative voltages in the audio signal from, the Arduino would read only 0V and we would end up clipping the bottom of the signal.

To solve it we have to amplify and offset the audio signals so that they fall within a range of 0-5V. Ideally, the signal should have an amplitude of 2.5V that oscillates around 2.5V so that its min voltage is 0V and its max voltage is 5V.

Amplification

The amplifier is the first step in the circuit, it increases the amplitude of the signal from around + or - 200mV to + or - 2.5V (ideally). The other function of the amplifier is to protect the audio source (the thing generating the audio signal in the first place) from the rest of the circuit. The outgoing amplified signal will source all its current from the amplifier, so any load put on it later in the circuit will not be "felt" by the audio source (the phone/iPod/laptop in my case). Do this by setting up one of the op-amps in the TL072 or TL082(fig 2) package in a non-inverting amplifier configuration.

The datasheet of the TL072 or TL082 says that it should be powered with +15 and -15V, but since the signal will never be amplified above + or - 2.5V it's fine to run the op-amp with something lower. I used two nine-volt batteries wired in series to create a + or - 9V power supply.

Wire up your +V (pin 8) and –V (pin 4) to the op-amp. Wire the signal from the mono jack to the non-inverting input (pin 3) and connect the ground pin of the jack to the 0V reference on your voltage supply (for me this was the junction between the two 9V batteries in series). Wire a 100kOhm resistor between the output (pin 1) and inverting input (pin 2) of the op-amp. In this circuit, I used a 10kOhm potentiometer wired as a variable resistor to adjust the gain (the amount that the amplifier amplifies) of my non-inverting amplifier. Wire this 10K linear taper pot between the inverting input and the 0V reference.

DC Offset

The DC offset circuit has two main components: a voltage divider and a capacitor. The voltage divider is made from two 100k resistors wired in series from the Arduino's 5V supply to ground. Since the resistors have the same resistance, the voltage at the junction between them equals 2.5V. This 2.5V junction is tied to the output of the amplifier via a 10uF capacitor. As the voltage on the amplifier side of the capacitor rises and falls, it causes charge to momentarily accumulate and repel from the side of the capacitor attached to the 2.5V junction. This causes the voltage at the 2.5V junction to oscillate up and down, centred around 2.5V.

As shown in the schematic, connect the negative lead of a 10uF capacitor to the output from the amplifier. Connect the other side of the cap to the junction between two 100k resistors wired in series between 5V and ground. Also, add a 47nF capacitor from 2.5V to ground.

Step 3: Decomposing the Signal Into a Sum of Stationary Sinusoids - Theory

The audio signal sent out through any 3.5mm jack is in the

range of 20 Hz to 20 kHz. It is sampled at 44.1 kHz and each sample is encoded on 16 bits.

To deconstruct the basic elemental frequencies that make up the audio signal, we apply Fourier Transform to the signal, which decomposes the signal into a sum of stationary sinusoids. In other words, Fourier analysis converts a signal from its original domain (often time or space) to a representation in the frequency domain and vice versa. But computing it directly from the definition is often too slow to be practical.

Figures show how the signal looks in time and frequency domain.

This is where the Fast Fourier Transform (FFT) algorithm is pretty useful!

By Definition,

An FFT rapidly computes such transformations by factorizing the DFT matrix into a product of sparse (mostly zero) factors. As a result, it manages to reduce the complexity of computing the DFT from O(N2), which arises if one simply applies the definition of DFT, to O(N log N), where N is the data size. The difference in speed can be enormous, especially for long data sets where N may be in the thousands or millions. In the presence of round-off error, many FFT algorithms are much more accurate than evaluating the DFT definition directly or indirectly.

In simple terms, it just means that the FFT algorithm is a faster way to compute the Fourier Transform of any signal. This is generally used on devices with low computing power.

Check out this gif for a visual understanding of how the signal is decomposed.

Fourier Transform gif

Step 4: How to Apply FFT? - Code

To begin with, here are some FFT Libraries available for

Arduino:

Arduino FFT

fix_fft

I will be using the fix_fft library in this instructable.

Let’s go over the code line by line:

#include "fix_fft.h"

Include the FFT library into your code.

const int FFT_N =128;

Initialize a variable FFT_N with 128. This will be the number of samples we take from the audio signal. The sampling rate should be 128 for Arduino Uno/Nano, and 256 for Arduino Mega. Arduino Uno/Nano cannot take in 256 samples due to memory restrictions on the hardware.

char im[FFT_N], data[FFT_N];

Here we declare two variables to store the real and imaginary values of the sampled audio signal.

We read FFT_N (128 for Uno/Nano) number of real values from the audio signal and pass that data onto the FFT algorithm, which returns us a real number and an imaginary number for each real value we pass into the algorithm. (Detailed explanation given below)

for (i = 0; i < FFT_N; i++) {

val = analogRead(A0);

data[i] = val / 4 - 128;

im[i] = 0;

}

We collect 128 samples from the audio signal within a for loop. The data collected with the analogRead(A0) function returns us a number between 0 and 1023, which corresponds to the instantaneous voltage given to A0. Now we cannot directly pass the data onto the FFT algorithm because data[i] is a char variable which means it can hold only 8 bits i.e. -128 to 127. But the output from analogRead() is 10 bits, i.e. 0 to 1023. Hence, we scale it to fit between -128 to 127. Since we are not giving any complex number we initialize it to 0.

fix_fft(data, im, 7, 0);

Performs forward Fourier transform and returns a complex vector, where the magnitude of the vector is the magnitude of the corresponding frequency. The function parameters are as follows:

fix_fft (char fr[], char fi[], int m, int inverse);

fr is a real data set,

fi is the imaginary data set,

The 3rd parameter m, are 2 ^ m 'bins', if m = 7 then so 128, m is log2 (n) where n (FFT_N) is number of data points (log2 (128) = 7), (log2 (256) = 8)

inverse is 0 (Forward transform) or 1 (Inverse transform)

for (i = 1; i < FFT_N/2; i++) {

int dat = sqrt(data[i] * data[i] + im[i] * im[i]);

}

The FFT returns a data array which contains frequency bin data in locations 0….127 for samples up to the sampling frequency of 9 kHz.

Why is the sampling frequency 9kHz?

The Arduino Uno/Nano clock runs at 16Mhz and the analogRead() has some overhead with a Prescaler of 128, so 16MHz/128 gives us 125 kHz but the analog conversion takes 13 ADC cycles, 125kHz/13 which gives us about 9.61 kHz to work with, also because we are going through a loop and we are doing some other operations, in the real world it should leave us with 9 kHz.

What is a frequency bin?

Using The Nyquist theorem, if we had to sample an audio signal then we will have to have a sampling frequency that is twice as fast as or more than the frequency of the audio signal.

Now that we know that the Arduino Uno/Nano can sample at 9 kHz, according to the Nyquist theorem our sampled audio will be 4.5 kHz.

Since we are taking 128 samples at 9 kHz, 9 kHz/128 will give us 70 Hz i.e. each frequency bin () is 70 Hz wide.

We are taking 128 samples at 9 kHz but we can only consider data up to 4.5 kHz to be valid (according to Nyquist theorem), we basically ignore the upper half and use samples up to 64.

This is why our loop runs from 0-64 or 1-64 (to reduce low-frequency noise).

How to determine the magnitude of each frequency bin?

Once FFT is applied to the data, a complex vector is returned. The vector has a real and an imaginary part. The real number is on the x-axis and the imaginary number is on the y-axis as depicted in the fig. In order to find the magnitude of the vector, we apply the Pythagoras theorem which gives us the magnitude of the vector.

I understand that it was intense so let’s summarize:

We take in 128 real values form the audio signal, throw it into the FFT algorithm which returns us a complex number i.e. real values and complex values in two separate arrays. According to Nyquist theorem, we can use only half of the values that we have sampled so we run the loop till 64. In the loop we apply Pythagoras theorem to determine the magnitude of the frequency bin.

⚠⚠⚠ The Code attached in this step only calculates the FFT

Check the last Step for the full code that includes control algorithm for the RGB LED Strip too.

Step 5: Circuit to Control the 12v Analog RBG LED Strip

You can use any NPN transistors like TIP120, TIP121, TIP122 or N-Channel MOSFETs like IRF540, IRF 530. The difference is that they have a different collector-emitter current rating. For example, if you are using a large length of RGB LED strip then to drive them you will be needing high current transistors like IRF540 which have Drain Current (Id): 28 Amps. Or if you need only a few LEDs then you can use any other NPN transistors like TIP120 which have Collector Current of 5 Amps continuous and 8 Amps peak.

  1. Connect the emitter of all the transistors to the Ground of Arduino.
  2. Connect the Ground of 12V power supply to the Ground of Arduino.
  3. Connect the positive terminal of 12V power supply to the 12V pin of RGB LED strip.
  4. Connect 1 K resistor to the base of each transistor as shown.
  5. Connect wires from the digital pins D9, D10, D11 to the other ends of the resistors as shown in the circuit diagram.

Finally, connect wires from the R, G and B terminals of the RGB LED strip to the collector (middle pin) in the TIP120 transistors as shown.

Step 6: Final Code

Our final goal is to output a PWM wave between 0 and 255 that corresponds to the magnitude of low/mid/high frequencies present in the audio signal which in turn controls the RGB light respectively.

Let’s group certain frequencies and assign a colour to them:

Frequency Range = Colour

70 Hz – 490 Hz = RED

1400 Hz – 3080 Hz = GREEN

3360 Hz – 4200 Hz = BLUE

To express the above in code:

Collect 1 – 7 Frequency bin data → Average them → Map it to 0 – 255 → analogWrite(pin_no, value)

Collect 20 – 44 Frequency bin data → Average them → Map it to 0 – 255 → analogWrite(pin_no, value)

Collect 48 – 60 Frequency bin data → Average them → Map it to 0 – 255 → analogWrite(pin_no, value)

Lighting Challenge

Participated in the
Lighting Challenge