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);
}

 

Leave a Reply

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