I remember about 20 years ago one of my first electronics projects was a light organ, with light spots of 220V; that sounds quite dangerous in hindsight. I built a 21st century version of that using the modest DSP capability of the Arduino UNO. Instead of the common FFT (to split a signal in its frequency components) i used a variant call the Fast Hartley Transformation (FHT) as described here, and an example here. My sketch is at the bottom of this page.

IMG_20160322_120206990

middle frequencies when i whistle: blue-ish light

I used an amplified microphone signal on one of the Arduino’s analog inputs, A0, and 256 samples to calculate the frequency distribution. The FHT returns magnitudes in 128 frequency bins, with options for a linear or logarithmic scale. As my OLED screen has 128 pixels, i tried to display these 128 values but it seems the display (even if it’s an SPI connection) is too slow to draw the 128 bars, using the U8G library. So i looked into an interesting feature of the FHT library which is the optional octave output: each bin doubles the frequency, and with 256 samples we get 8 octaves. The first 2 octaves (below 280Hz) did not return any useful data (maybe sensitivity of my microphone) so i dropped them, and i display only 6 octave bars.

low frequencies when i hum: red-ish light

low frequencies when i hum: red-ish light

One important note: the version of the FHT library that i used did not work with Arduino IDE 1.6.x because of the way it uses PROGMEM, giving this compilation error:

In file included from spectrum-analyser-v1.ino:16:0:
/home/tom/sketchbook/libraries/FHT/FHT.h:72:10: error: ‘prog_int16_t’ does not name a type
PROGMEM prog_int16_t _cas_constants[] = {

I was lazy to update the library so went back to IDE 1.0.6 for this project, and that works just fine.

high frequencies when i make an 'ssssss' sound: green-ish light

high frequencies when i make an ‘ssssss’ sound: green-ish light

So i display 6 octaves on the OLED screen, subtracting a constant ‘noise level’ of the FHT output values.  For the RGB LED, divided those 6 octaves into 3 categories for the Red, Green, Blue, subtracting the same constant noise level first. Then i squared those values to make the more responsive. A short video clip with music:

The sketch does not have a precise timing, but rather reads a new  ADC value as soon as it is available. I tried to calculate this sampling frequency as follows: check micros() before the 256 sample loop, and after (see Serial.print in the sketch). I needed to comment out the line that says TIMSK0 = 0 because that disables timer0 used by the micros() function, and the interrupt disable/enable cli() and sei(). This gave me a rather constant interval of around 6620 microseconds, i.e. about 28 microseconds per sample = 35,700Hz sampling frequency.

That gives a Nyquist frequency of around 18kHz: that’s about as high as (some) humans can hear so that works out great! So in theory, we would have these frequency bins:

  • 9,000-18,000 Hz
  • 4,500-9,000 Hz
  • 2,250-4,500 Hz
  • 1,125-2,250 Hz
  • 560-1,125 Hz
  • 280-560 Hz
  • 140-280 Hz (not displayed)
  • 0-140 Hz (not displayed)

Another way to arrive at this is to start with the Nyquist frequency of 18kHz and divide by the 128 frequency bins that we would get with the normal FHT; that gives frequency bins of 140 Hz. What the Octave representation does is grouping the bins together progressively, as described in the fht_read_me.txt:

G. fht_mag_octave() – this outputs the RMS value of the bins in an octave
(doubling of frequencies) format. this is more useful in some ways, as it is
closer to how humans percieve sound. it doesnt take any variables, and doesnt
return any variables. the input is taken from fht_output[] and returned on
fht_oct_out[]. the data is represented in and 8b value of 16*log2(sqrt(mag)).
there are LOG_N bins. and they are given as follows:

FHT_N = 256 :: bins = [0, 1, 2:4, 5:8, 9:16, 17:32, 3:64, 65:128]
FHT_N = 128 :: bins = [0, 1, 2:4, 5:8, 9:16, 17:32, 3:64]

where (5:8) is a summation of all bins, 5 through 8. the data for each bin is
squared, imaginary and real parts, and then added with all the squared magnitudes
for the range. it is then divided down by the numbe of bins (which can be turned
off – see #defines below), and then the square root is taken, and then the log is
taken.

I tested with different tone frequencies and the results confirm the above theory pretty well as you can see in below pictures.

IMG_20160323_090932738

IMG_20160323_090655824

IMG_20160323_090749989

IMG_20160323_090815208

IMG_20160323_090831962

IMG_20160323_090846633

My sketch:

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/*
FHT library for frequency spectrum analyser http://wiki.openmusiclabs.com/wiki/ArduinoFHT
settings: frequency bins combined in octaves, see fht_read_me.txt for details
8 octaves, first 2 are not useful

USE IDE 1.0.x because of changes to PROGMEM causes error in 1.6.x
*/

// amplified MIC on A0
// RGB LED on D3, D5, D6

#define OCT_NORM 0 // 0: no normalisation, more high freq 1: divided by number of bins, less high freq
#define OCTAVE 1
#define FHT_N 256 // set to 256 point fht

#include <FHT.h> // include the library

#include “U8glib.h”
U8GLIB_SSD1306_128X64 u8g(10, 9); // OLED display: HW SPI CS = 10, A0/DC = 9 (Hardware Pins are SCK/D0 = 13 and MOSI/D1 = 11) + RST to Arduino D8

const int oled_rst = 8;
const int red = 3;
const int blue = 5;
const int green = 6;
const int noise_level = 50;

void setup() {

pinMode(oled_rst, OUTPUT); // reset OLED display
digitalWrite(oled_rst, LOW);
delay(100);
digitalWrite(oled_rst, HIGH);

drawIntro();
delay(2000);

Serial.begin(115200); // use the serial port
TIMSK0 = 0; // turn off timer0 for lower jitter // no for TIMING
ADCSRA = 0xe5; // set the adc to free running mode
ADMUX = 0x40; // use adc0
DIDR0 = 0x01; // turn off the digital input for adc0
}

void loop() {
while(1) { // reduces jitter
cli(); // UDRE interrupt slows this way down on arduino1.0 // no for TIMING

// unsigned long start_time = micros(); // yes for TIMING

for (int i = 0 ; i < FHT_N ; i++) { // save 256 samples
while(!(ADCSRA & 0x10)); // wait for adc to be ready
ADCSRA = 0xf5; // restart adc
byte m = ADCL; // fetch adc data
byte j = ADCH;
int k = (j << 8) | m; // form into an int
k -= 0x0200; // form into a signed int
k <<= 6; // form into a 16b signed int
fht_input[i] = k; // put real data into bins
}

// Serial.println(micros()-start_time); // yes for TIMING

fht_window(); // window the data for better frequency response
fht_reorder(); // reorder the data before doing the fht
fht_run(); // process the data in the fht
fht_mag_octave(); // take the output of the fht
sei(); // no for TIMING

drawBars();
RGB();
}
}

///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

void drawBars() {
u8g.firstPage(); // draw 6 bars
do {
for (int x=2; x< 8; x++) {
int h = max((fht_oct_out[x] – noise_level) / 4, 0); // scale the height
u8g.drawBox((x-2) * 20, 63-h, 15, h+1);
}
} while ( u8g.nextPage() );
}

///////////////////////////////////////////////////////////////////////////////////////////

void drawIntro() {
u8g.firstPage(); // draw intro
do {
u8g.setFont(u8g_font_fub14r);
u8g.setPrintPos(10, 15);
u8g.print(“Spectrum”);
u8g.setPrintPos(30, 30);
u8g.print(“analyser”);
u8g.setPrintPos(2, 60);
u8g.print(“BuffaloLabs”);
} while ( u8g.nextPage() );
}

///////////////////////////////////////////////////////////////////////////////////////////

void RGB() {
int red_level = min( pow(fht_oct_out[2]+fht_oct_out[3] – 2 * noise_level,2)/100, 255); // limit at 255
int blue_level = min( pow(fht_oct_out[4]+fht_oct_out[5] – 2 * noise_level,2)/100, 255); // limit at 255
int green_level = min( pow(fht_oct_out[6]+fht_oct_out[7] – 2 * noise_level,2)/100, 255); // limit at 255
analogWrite(red, 255-red_level); // for common positive RGB LEDs
analogWrite(blue, 255-blue_level);
analogWrite(green, 255-green_level);
}

 

15 thoughts on “Arduino spectrum analyser and light organ

  • 02/02/2018 at 02:06
    Permalink

    How could I modify this to break out the channels to 3 frequencies pushing the signal out to relays? this would be to activate either LED strands or Bulbs like the old color organs.

    Thank you!
    Rick

    Reply
    • 02/02/2018 at 07:36
      Permalink

      Hi Rick, you could modify the RGB() function at the bottom, to use the red_level, green_level, blue_level to switch your relays, e.g.
      if (red_level > 100) {
      digitalWrite(red_relay_pin, HIGH); }
      else {
      digitalWrite(red_relay_pin, LOW); }
      I assume you are using logic level relays that can be switched with the relatively weak Arduino output.

      Reply
      • 02/02/2018 at 07:57
        Permalink

        Yes, im using a solid state relay. So essentially, copy the code provided and alter for each color. Im assuming I’d need to int the relays as well, correct?

        Would the level need to be different for each channel if I am looking to get separation between low, mid and high?

        Thank you for your reply!

        Reply
        • 02/02/2018 at 08:00
          Permalink

          Also, I dont have a need for the screen / level display. If I were to not hook one up, would I encounter any errors? Im fairly new to arduino, but love vintage color organs and want to make a safer and more modern version.

          Reply
        • 02/02/2018 at 08:12
          Permalink

          yes declare each of your relay pins as OUTPUT
          red_level represent low frequencies, blue_level mid freq, green_level high freq. the threshold for each can be same, or different, as you like.

          Reply
          • 02/02/2018 at 08:23
            Permalink

            Thank you so much!

  • 02/02/2018 at 22:14
    Permalink

    Where did you get your mic and light? Also, how can I make a string of dedicated colored lights connected to separate pins? Such as, low tone are red on pin 2, mid are all blue on pin 5 and high notes are on pin 7 as green ?

    Reply
  • 08/02/2018 at 04:50
    Permalink

    Here is another question for you. If I were to attach a neopixel strip in place of the current LED you have, is should technically work? All the LEDS would blink at the same rate im thinking.

    Reply
    • 12/02/2018 at 08:45
      Permalink

      yes neopixels could work too, so you could use 1 pin to drive an entire neopixel strip
      then you just need to change the code to send the right colour to the right pixel, using the neopixel library
      however, i am not sure how fast the neopixel library is and if it will affect the FFT calculations

      Reply
  • 13/02/2018 at 08:22
    Permalink

    Im now getting this error when compiling

    Arduino: 1.8.5 (Windows 10), TD: 1.41, Board: “Arduino/Genuino Micro”

    spectrum:81: error: stray ‘\342’ in program

    int h = max((fht_oct_out[x] – noise_level) / 4, 0); // scale the height

    ^

    spectrum:81: error: stray ‘\200’ in program

    spectrum:81: error: stray ‘\223’ in program

    spectrum:81: error: stray ‘\342’ in program

    spectrum:81: error: stray ‘\200’ in program

    spectrum:81: error: stray ‘\223’ in program

    spectrum:94: error: stray ‘\342’ in program

    u8g.print(“Spectrum�);

    ^

    spectrum:94: error: stray ‘\200’ in program

    spectrum:94: error: stray ‘\234’ in program

    spectrum:94: error: stray ‘\342’ in program

    spectrum:94: error: stray ‘\200’ in program

    spectrum:94: error: stray ‘\235’ in program

    spectrum:96: error: stray ‘\342’ in program

    u8g.print(“analyser�);

    ^

    spectrum:96: error: stray ‘\200’ in program

    spectrum:96: error: stray ‘\234’ in program

    spectrum:96: error: stray ‘\342’ in program

    spectrum:96: error: stray ‘\200’ in program

    spectrum:96: error: stray ‘\235’ in program

    spectrum:98: error: stray ‘\342’ in program

    u8g.print(“BuffaloLabs�);

    ^

    spectrum:98: error: stray ‘\200’ in program

    spectrum:98: error: stray ‘\234’ in program

    spectrum:98: error: stray ‘\342’ in program

    spectrum:98: error: stray ‘\200’ in program

    spectrum:98: error: stray ‘\235’ in program

    spectrum:105: error: stray ‘\342’ in program

    int red_level = min( pow(fht_oct_out[2]+fht_oct_out[3] – 2 * noise_level,2)/100, 255); // limit at 255

    ^

    spectrum:105: error: stray ‘\200’ in program

    spectrum:105: error: stray ‘\223’ in program

    spectrum:105: error: stray ‘\342’ in program

    spectrum:105: error: stray ‘\200’ in program

    spectrum:105: error: stray ‘\223’ in program

    C:\Users\Rick\Desktop\spectrum\spectrum.ino: In function ‘void drawBars()’:

    spectrum:78: error: ‘u8g’ was not declared in this scope

    u8g.firstPage(); // draw 6 bars

    ^

    In file included from sketch\spectrum.ino.cpp:1:0:

    spectrum:81: error: expected ‘)’ before ‘noise_level’

    int h = max((fht_oct_out[x] – noise_level) / 4, 0); // scale the height

    ^

    C:\Users\Rick\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.6.20\cores\arduino/Arduino.h:93:20: note: in definition of macro ‘max’

    #define max(a,b) ((a)>(b)?(a):(b))

    ^

    spectrum:81: error: expected ‘)’ before ‘;’ token

    int h = max((fht_oct_out[x] – noise_level) / 4, 0); // scale the height

    ^

    spectrum:81: error: expected ‘)’ before ‘;’ token

    C:\Users\Rick\Desktop\spectrum\spectrum.ino: In function ‘void drawIntro()’:

    spectrum:90: error: ‘u8g’ was not declared in this scope

    u8g.firstPage(); // draw intro

    ^

    spectrum:92: error: ‘u8g_font_fub14r’ was not declared in this scope

    u8g.setFont(u8g_font_fub14r);

    ^

    spectrum:94: error: ‘Spectrum’ was not declared in this scope

    u8g.print(“Spectrum�);

    ^

    spectrum:96: error: ‘analyser’ was not declared in this scope

    u8g.print(“analyser�);

    ^

    spectrum:98: error: ‘BuffaloLabs’ was not declared in this scope

    u8g.print(“BuffaloLabs�);

    ^

    In file included from sketch\spectrum.ino.cpp:1:0:

    C:\Users\Rick\Desktop\spectrum\spectrum.ino: In function ‘void RGB()’:

    spectrum:105: error: expected ‘)’ before numeric constant

    int red_level = min( pow(fht_oct_out[2]+fht_oct_out[3] – 2 * noise_level,2)/100, 255); // limit at 255

    ^

    C:\Users\Rick\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.6.20\cores\arduino/Arduino.h:92:20: note: in definition of macro ‘min’

    #define min(a,b) ((a)<(b)?(a):(b))

    ^

    spectrum:105: error: expected ')' before numeric constant

    int red_level = min( pow(fht_oct_out[2]+fht_oct_out[3] – 2 * noise_level,2)/100, 255); // limit at 255

    ^

    C:\Users\Rick\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.6.20\cores\arduino/Arduino.h:92:28: note: in definition of macro 'min'

    #define min(a,b) ((a) Preferences.

    Reply
    • 22/02/2018 at 08:29
      Permalink

      yes that’s a wordpress problem; when you do the copy/paste of the code, it introduces a bunch of ghost characters in the code; you will have to go through it line by line to remove them, sorry

      Reply
      • 08/03/2018 at 06:39
        Permalink

        I was able to fix it. I am going to try this over the weekend and use strings of LEDs for each of the 3 pins as you have it in your code. I briefly tried it with a single LED and it worked.

        Reply
  • 22/05/2018 at 16:21
    Permalink

    Hi Sir,

    can you please brief the Pin connection for Arduino and OLed and also the Mic Pin

    Reply
    • 28/05/2018 at 07:29
      Permalink

      please have a look in the code; the mic is on A0

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *