Another FHT thread

Hi all, my name is Axel, long time forum observer, first time poster.

First, thanks to the moderators, your wisdom & patience is very much appreciated! Second, mrExplore, I feel ya, I'm on the same track you are on. I'd be curious to hear how far you got.

My setup: genuine Arduino Nano, Adafruit MAX9814 board (electret + amp w/ AGC), Adafruit WS2801 RGB LED pixel strip.

After much review of DSP, Fourier, Nyquist, interrupts, speed of sampling etc. the project is up + running, but not perfectly. The concept and vision is proven, but I can't use it as intended.

The desired outcome is: use the setup in a loud musical environment (ie electronic music venue), expected SPL around 120 dB(A) max, have the LEDs act as a spectrum visualizer, ie a bunch of LED pixels flashing red (the louder the brighter) to the baseline (around 120 bpm / 2 Hz) and a bunch of LED pixels flashing green to the high-hats.

There are 50 LED pixels and I assigned 6...7 of them per frequency bin. I use the open music labs fht or fft library and modified sample code (tried FHT + FFT, made no difference in outcome for me); using the octave processing, grouping several frequency bins into 8 total output bins based on doubling of frequency, quite handy. As far as I can tell, I sample 256 times at 38.5 kHz, so I'm catching sound frequencies up to 19 kHz. I don't have an oscilloscope to see if there is data bove 19 kHz in the input signal.

Essentially my theoretical math is this:

  • sample rate 38.5 kHz, samples taken: 256
  • 8 octave output bins: FHT_N = 256 : bins = [0, 1, 2:4, 5:8, 9:16, 17:32, 33:64, 65:128]
  • 19.25 kHz / 128 bins = 150 Hz per bin
  • bin 0: 0-150 Hz; bin 1: 150-300 Hz, bin 2: 300-600 Hz, bin 3: 600-1200 Hz, bin 4: 1200-2400 Hz, bin 5: 2400-4800 Hz, bin 6: 4800-9600 Hz, bin 7: 9600-19200 Hz roughly

Code notes:

  • to reduce jitter I put in a minimum brightness minbri and only increase the brightness of the lights if the measured magnitude in a bin goes over a threshold
  • since the MAX9814 sends analog signals biased around 1.25 V with a max of 2.45 V and min of 0.003 V I rescale the brightness ("shine") to 0...255 range (the WS2801 lights use that to range to determine brightness with 255 being max brightness).

Status: the thing works for bins 2-7, but not for bin 0 and 1 - see serial mon output below; 256 samples taken at a rate of 38.5kHz are taken during a time window of 0.00665 seconds. One complete sine wave at 20 Hz happens during a time window of 0.05 seconds. As far as I understand, the FFT only picks up "things" that actually have time to complete one complete sine wave during the taking of 256 samples. So that would mean that the FFT is "deaf" below a certain frequency, which I believe for me is 1/0.00665s --> 150 Hz. Is that right? That would explain why bin 0 acts up. And maybe why bin 1 acts up, idk.

In any case, my code sends debug info to the serial monitor, I get "reasonable" outputs for bins 2...7 and the lights react to frequencies seemingly properly. Tried it with an online tone generator, not a precise match. Since the lights "look" ok, I won't investigate further for now.

So this is my question, are the above assumptions generally correct? And does anyone have a suggestion on how to make the code work for bin 0 and bin 1?

FYI I took the setup off of the stationary power supply to see if noise comes in through the power source, but that didn't improve things.

Here's a typical output in the serial monitor in a quiet room (bins 0 and 1 are "odd"):

bin 0
207 [note: this is the fft_oct_out[0] value]
185 [note: this is the shine value sent to the RGB pixel]
bin 1
191
161
bin 2
62 [note: as 62 is below the thresh value of 80, the shine value is set to minbri of 3]
3
bin 3
43
3
bin 4
37
3
bin 5
34
3
bin 6
32
3
...
...

Here's my full code:

//code source openmusiclabs.com 8.18.12
//Axel's notes: modified code from open music labs to incorporate code for adafruit pixels

#define OCTAVE 1 // use the octave function
#define OCT_NORM 1 // normalize the output bins (try 1 for noramlization ON and 0 for normaliz off)
#define FFT_N 256 // set to 256 point fft

#include <FFT.h>

#include "Adafruit_WS2801.h" // lib for the RGB pixels
uint8_t dataPin  = 2;    // Yellow wire on Adafruit Pixels
uint8_t clockPin = 3;    // Green wire on Adafruit Pixels
Adafruit_WS2801 strip = Adafruit_WS2801(50, dataPin, clockPin); 

float thresh = 80; // sets the threshold above which input magnitude the lights will start changing in brightness
uint8_t shine = 5; // variable - actual brightness sent to pixels
uint8_t minbri = 3; // sets the resting brightness of the lights when magnitude is lower than thresh
float tempstore = 0; // variable for the ADC value rescaling math to work

void setup() {
  Serial.begin(115200); 
  TIMSK0 = 0; // turn off timer0 for lower jitter - delay() and millis() killed
  ADCSRA = 0xe5; // set the adc to free running mode
  ADMUX = 0x40; // use adc0
  DIDR0 = 0x01; // turn off the digital input for adc0
  
  strip.begin();
  strip.show();   // Update LED contents, to start they are all 'off'
  
}

void loop() {
  while(1) { // reduces jitter
    cli();  // UDRE interrupt slows this way down on arduino1.0
    for (int i = 0 ; i < 512 ; i += 2) { // 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
      fft_input[i] = k; // put real data into even bins
      fft_input[i+1] = 0; // set odd bins to 0
    }
    fft_window(); // window the data for better frequency response
    fft_reorder(); // reorder the data before doing the fft
    fft_run(); // process the data in the fft
    fft_mag_octave(); // take the output of the fft
    sei(); // turn interrupts back on
    
    // below code is to process the fft_mag_octave info and send it to the lights
    // doing this for each bin individually

    // BIN 0
    tempstore = fft_oct_out[0];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 0 ; s <= 6 ; s++) {strip.setPixelColor(s, Color(shine,0,0));}
    Serial.println("bin 0");
    Serial.println(fft_oct_out[0]);
    Serial.println(shine);
 
    // BIN 1
    tempstore = fft_oct_out[1];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 7 ; s <= 13 ; s++) {strip.setPixelColor(s, Color(0,shine,0));}
    Serial.println("bin 1");
    Serial.println(fft_oct_out[1]);
    Serial.println(shine);

    // BIN 2
    tempstore = fft_oct_out[2];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 14 ; s <= 19 ; s++) {strip.setPixelColor(s, Color(0,0,shine));}
    Serial.println("bin 2");
    Serial.println(fft_oct_out[2]);
    Serial.println(shine);

    // BIN 3
    tempstore = fft_oct_out[3];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 20 ; s <= 26 ; s++) {strip.setPixelColor(s, Color(shine,0,0));}
    Serial.println("bin 3");
    Serial.println(fft_oct_out[3]);
    Serial.println(shine);

    // BIN 4
    tempstore = fft_oct_out[4];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 27 ; s <= 33 ; s++) {strip.setPixelColor(s, Color(0,shine,0));}
    Serial.println("bin 4");
    Serial.println(fft_oct_out[4]);
    Serial.println(shine);

    // BIN 5
    tempstore = fft_oct_out[5];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 34 ; s <= 39 ; s++) { strip.setPixelColor(s, Color(0,0,shine));}
    Serial.println("bin 5");
    Serial.println(fft_oct_out[5]);
    Serial.println(shine);
    
    // BIN 6
    tempstore = fft_oct_out[6];
    if (tempstore > thresh) {shine = (((tempstore-thresh)/(255-thresh))*255);} else shine = minbri;
    for (int s = 40 ; s <= 50 ; s++) {strip.setPixelColor(s, Color(shine,0,0));}
    Serial.println("bin 6");
    Serial.println(fft_oct_out[6]);
    Serial.println(shine);

    Serial.println("...");
    Serial.println("...");

    strip.show();   // final step: write all the pixels out at the proper brightness
    
  }
}

// below function is to create the color value sent to the pixels (helper function)

uint32_t Color(byte r, byte g, byte b)
{
  uint32_t c;
  c = r;
  c <<= 8;
  c |= g;
  c <<= 8;
  c |= b;
  return c;
}

And does anyone have a suggestion on how to make the code work for bin 0 and bin 1?

In what way is it not working?

The 0 bin is the DC term (the average value your data) which tends to leak into the 1 bin with windowing.

After processing the samples with the fft_mag_octave() function, fft_oct_out[2]....fft_oct_out[7] produce seemingly reasonable results ie the value reacts to sound inputs and the brightness changes. However, fft_oct_out[0] and fft_oct_out[1], the values are "stuck" at 207 and 191 respectively and vary very very little, no matter what external sound is present. Consequently the brightness of the LED pixels is steady.

the values are "stuck" at 207 and 191 respectively and vary very very little, no matter what external sound is present.

Yes that is what you will see. The DC level of your signal is fixed, it is the bias point of the waveform so you would not expect it to change. Bin 1 probably has nothing in it but leakage from bin 0, which is not changing.

What do you expect to see in these bins?

Ok, I'm really trying to understand this, but maybe I have misunderstood completely. Let me recap.

Let's say I have a musical signal in my house that contains frequencies above the noise level of the mike & amp capabilities in all frequency bands from 20 Hz to 20 kHz.

  • I'm sampling the sound at 256 samples at 38.5 kHz. (looking at my code, I'm kindly asking you is that statement true or false?)
  • Due to Nyquist, only frequencies at half that are used for the FFT so 19.25 kHz and below (true or false?)
  • First, the FFT operation is decomposes the array of 256 time domain samples into the frequency bands and puts the info into 128 discrete frequency bins (true or false?) - let's call them i-bins
  • Each of those initial i-bins collects the magnitude of frequencies present in steps of 150 Hz (true or false?) - [19.25 kHz / 128 bins = 150 Hz]
  • The octave function further processes the 128 i-bins down into a total of 8 output bins (true or false?) - let's call them o-bins
  • The o-bins collect ambient sound frequencies present in the following frequency ranges:
    o-bin 0: 0-150 Hz (sum and average of i-bin 0)
    o-bin 1: 150-300 Hz (sum and average of i-bin 1)
    o-bin 2: 300-600 Hz (sum and average of i-bin 2-4)
    o-bin 3: 600-1200 Hz (sum and average of i-bin 5-8)
    o-bin 4: 1200-2400 Hz (sum and average of i-bin 9-16)
    o-bin 5: 2400-4800 Hz (sum and average of i-bin 17-32)
    o-bin 6: 4800-9600 Hz (sum and average of i-bin 33-64)
    o-bin 7: 9600-19200 Hz (sum and average of i-bin 64-128)

What do you expect to see in these bins?

I expect o-bins 0 and 1 to behave exactly the same as o-bins 2-7.

To rephrase the question: Why are o-bins 0 and 1 different in behavior from o-bins 2-7? They do not react to sound, but are stuck at a constant value. So for o-bins 0 and 1, I'm unclear as to why they don't change value. Even with a clear and loud bass line in the ambient sound, they don't do anything, while bins 2-7 happily react to ambient sound.

The 0 bin is the DC term (the average value your data)

I'm not sure I understand, sorry! The o-bin 0 is the average of the amplitudes? Why is o-bin 0 different from o-bin 5 for example. I thought all bins behave the same way, using an average. It sounds like bin 0 is indeed different? That's the part I struggle with.

The DC level of your signal is fixed, it is the bias point of the waveform so you would not expect it to change.

Do you refer to my signal from the amplifier to pin A0, or the signal inside the FFT function in the code?

Why are o-bins 0 and 1 different in behavior from o-bins 2-7?

Because the average value of your signal IS NOT ZERO.

Calculate the average value of all samples, and subtract that off from every sample.

That is what this bit of the code is supposed to do, but fails because it makes the wrong assumption about the average value.

     int k = (j << 8) | m; // form into an int
      k -= 0x0200; // form into a signed int
      k <<= 6; // form into a 16b signed int

Not quite with you, need to ask more questions.

Because the average value of your signal IS NOT ZERO.

Are you saying it should be zero? Or is it because the o-bins 0 and 1 only have one frequency i-bin and the average is their actual value?

Calculate the average value of all samples, and subtract that off from every sample.

I can program that. So add all real data samples up (sum of all real data points fft_input*) and then divide them by 256 (or 512)? I'm not sure why because I'm flying blind regarding the math needed to do fft but I can try.*
> That is what this bit of the code is supposed to do, but fails because it makes the wrong assumption about the average value.
I tried to understand that bit of code better, since that part of my ctrl-c ctrl v. Until now I thought that that bit is inside the loop that collects individual samples 512 times. So I'm confused as to how that can have awareness of all the other samples and their average, since those samples aren't even collected yet.
Anyway, as far as I can tell the first line takes j and leftshifts it by 8 bits, then bitwise ors it by m and puts it into k. The next line subtracts 512 for whatever reason. The last line leftshifts some more. Wouldn't calculating an average require a division by 512 or 256 somewhere?
Thanks

To calculate an average, sum up all the individual samples and divide by the number of samples.

If you could be bothered to read and understand something about Fourier transforms, you will learn that the value in the zeroth output bin is the average of all the input values. It is the zero frequency component, or the DC term.

Finally, depending on the details of windowing, during the transformation process, bin values may bleed into each other. So the bin number 1 value may contain some fraction of the average (the bin number 0 value) as well.

The next line subtracts 512 for whatever reason.

You've been told the reason.

I’m quite bothered about reading up on Fourier, the reading is the easy bit, but the understanding is the difficult bit, as I’m not a math PhD. I honestly read the complete fft and fht part of the open music labs website - will definitely go back now and reread it a tenth time to see where it says bin 0 is different.

A simple straight forward ‘yo you missed the fact that bin 0 is different’ would have done but honestly sincere thanks for taking the time to bear with me on this learning curve! I feel your pain.

I’m not lazy, just reaching the end of my math wits here. Cheers.

The Fourier transform is just an application of trigonometry (sines and cosines), which is taught in high school. No Ph.D. required.

However, the OpenMusicLabs site is a terrible place to learn about Fourier transforms. There are thousands of better ones on the web. Please spend some time looking through a few. Here is one starting place.

Well, that gives me hope that I can understand Fourier transforms, since I did complete high school.

Thanks for the link, I will go away studying it. Once I'm further in my quest, I will come back to this forum, hopefully with more qualified questions.

All these discrete transforms return spot frequencies, so "bin" 0 returns DC only, bin1 returns 150Hz only, ...

However if the signal is not repeating indefinitely exactly every 256 samples, you are not actually doing the
mathematical transform, but some quasi-approximation to it, and frequencies leak between bins - an effect
that is gross and unusable unless some sort of windowing function is used (ie you must use a window
function with an FFT / DHT of an audio signal). Windowing functions reduce the leakage across bins
to a consistent and managable level, at the expense of increasing leakage in the few cases where it
wasn't happening already (ie when a component of the waveform was exactly on a spot frequency).

Basically bin 0 is DC offset (unless you null that out), bin 1 is 150Hz +/- perhaps 200Hz, etc. You simply
will not get good frequency resolution at low frequencies from such a coarse spread of bins.