2
$\begingroup$

I'm trying to show frequency spectrum for audio data as a bars graph using FFT.

If I have data for a sine wave at 440 Hz and with sampling rate 44100 Hz and analyze its first 1024 samples I get an array with 1024 FFT values. I know that the first half of those values (512) represents the frequency range equal to half the sampling rate (22050 Hz).

So to draw a bars graph I just take the first 15-20 values from the FFT array, calculate the frequency range for each element and plot it.

  • In the case of FFT size 1024 I frequencies each ~43 Hz wide.
  • But if I change FFT size to 2048 each frequency range is ~21.5 Hz. So the peak of the spectrum of the sine wave will be outside my 15-20 bars.

So I wanted to ask if it's possible to scale/normalize the X axis for a frequency range and a constant graph bars count?

The example limiting data would be:

  • frequency range 0-500 Hz
  • 16 bars on the graph

I guess it's some kind of bins/magnitudes grouping. But should I sum them or get max? And what could be done with magnitudes when grouped bin number isn't integer? (I hope my explanation is not too confusing, it's hard to choose the correct terminology)

Thanks.

Here is my test code in Python:
(I used a static int array imported from another file that represents a sine wave)

import scipy.io.wavfile as wavfile
import scipy
import scipy.fftpack
import numpy as np
from scipy.signal.windows import *
from matplotlib import pyplot as plt

from imp.sine_440_hex import *

def run():
  # fp = 'e:/sine_440_hz.wav'
  # FS, data = wavfile.read(fp)

  # data - array of int for a sine wave at 440Hz and amplitude 0.5
  DS = len(data)
  FS = 44100

  FN = 1024
  # FN = 2048
  MAXS = 32768

  MN = 15   # number of first frequencies in the FFT result

  ndata = np.array(data)
  ndata = ndata / MAXS

  w = np.hamming(FN)
  y = ndata[:FN] * w

  freqs = scipy.fftpack.fftfreq(FN, 1/FS)
  mags = abs(scipy.fft(y))

  dbfs = 20 * np.log10(mags * 2 / np.sum(w))
  dbs = dbfs + 120

  # -------- plot --------
  fr_res = freqs[:MN]
  db_res = dbs[:MN]

  ax1 = plt.subplot(211)
  plt.plot(fr_res, db_res)
  plt.grid(True)
  plt.xlabel('Frequency [Hz]')
  plt.ylabel('Amplitude [dB]')

  ax2 = plt.subplot(212)
  plt.bar(fr_res, db_res, fr_res[1]-10)
  plt.grid(True)
  ax2.set_ylim([80, 120])
  ax2.set_xticks(fr_res)
  ax2.set_xticklabels(fr_res.astype(int))

  plt.show()

# -------
run()

And example graphs:

For FN = 1024 https://i.imgur.com/wSK80w8.png For FN = 2048 https://i.imgur.com/tdOJq4X.png

$\endgroup$
  • $\begingroup$ Yeah, are you OK, you can split your magnitude(mags) in 15-20 frequency band, Choose a size for each frequency range, ex. first Bar from 0hz to 200hz, sum the bins from magnitudes that correspond to this frequency band and take an average, this is your first Bar, second Bar from 200hz to 400hz, etc, etc, you can split the frequencies in equal parts, if you want 16 bars you can use spaced wide of 4096/16 (bars) =256 $\endgroup$ – ederwander May 24 '18 at 13:45
  • $\begingroup$ Thanks for the comment. About the average: wouldn't it lower/degrade the highest bin, for example for 4 bins with magnitudes [0,80,0,0] the joined bin would be 20 db high? I thought maybe it could be the max of 4 values but not sure either if it's correct. $\endgroup$ – mortalis May 24 '18 at 13:46
  • $\begingroup$ depends on what you are going to use, I figured I would be using just as a spectrum analyzer, maybe you could sum the bands and lock the bar at a certain value(threshold) $\endgroup$ – ederwander May 24 '18 at 13:57
  • $\begingroup$ Can I please ask if this was resolved? $\endgroup$ – A_A Jun 11 '18 at 5:47
  • $\begingroup$ Well basically yes. First I thought I shoul dsomehow recalculate the magnitudes between FFT bins so they fit in a constant number of graph bars. But as I understood it wouldn't be precise enough. So I had to sacrifice some parameters (change FFT size or number of bars or sampling rate). For now the project is delayed but I guess I'll look into log scaling for the X axis later (so it reflects as the audio is heard and the freqency range covers more values). $\endgroup$ – mortalis Jun 14 '18 at 17:03
2
$\begingroup$

To achieve that, use frequencies rather than bins. To convert from bin number to the frequency it represents use:

$$f = \frac{k}{N_{FFT}} \cdot Fs$$

Where:

  • $k$ is a bin number whose actual frequency you want to recover
  • $Fs$ is the sampling frequency
  • $N_{FFT}$ is the number of points of the FFT

You can work out your $N_{FFT}$ to ensure that your scanned frequency range will be divided into 16 segments. For example, to cover the frequency range $f_l=300 f_h=3000$ Hz with $N_{bars}=16$ bars and $Fs=44100 Hz$, you would have to use:

$$N_{FFT} = \frac{Fs \cdot N_{bars}}{|f_h-f_l|}$$

Where basically, you work out the spacing between the two frequencies $f_l, f_h$ for 16 segments and then "translate" it to the $Fs$ (but the ratio has been simplified there and that's why $Fs \cdot N_{bars}$)

This works out to be:

$$N_{FFT} = \frac{44100 \cdot 16}{|3000-300|} \approx 261.3333 $$

Which will have to be rounded to $262$ points.

So, now you know that your FFT has 262 points with a spacing of $\frac{Fs}{N_{FFT}}=\frac{44100}{262}\approx 168.3206$ Hz.

Your first bin is at $\frac{300}{168.3206} \approx 1.7823$ which will have to be rounded to $2$ and 3000 Hz should be at bin $2 + 16 = 18$.

Let's see, $\frac{18}{262}\cdot 44100 \approx 3029.7709 $ Hz.

Hope this helps.

$\endgroup$
  • $\begingroup$ Oh this is the opposite side of my thoughts. That works too. Thanks. But as I understand FFT works betters with $N=2^{N}$. Could it impact on performance? I thought I should have a constant N and calculate somehow the 16 values for the bars heights. Maybe magnitudes interpolation could solve this, but don't know if it's used anywhere in real world. Thanks a lot, it helped me to extend the field of my reflections. $\endgroup$ – mortalis May 24 '18 at 15:19
  • $\begingroup$ @mortalis glad to hear it was helpful. You can fix the $N_{FFT}$ and solve for $Fs$ too. I don't understand the bit about the "real world". You can always integrate a high resolution FFT down to just 16 bars within a range of interest with something like a moving average filter. $\endgroup$ – A_A May 24 '18 at 15:56
  • $\begingroup$ I mean in graphic-spectrum for audio data applications. Another doubt about this exact example is wouldn't the quality of FFT analysis be lower if it only has 262 points? I thought the more data you give to FFT engine the more precise frequencies it will recognize. $\endgroup$ – mortalis May 24 '18 at 16:04
  • $\begingroup$ @mortalis a VU-meter type of application is fine as a low $N_{FFT}$ is like distributing a set of bandpass filters at the bin centre frequencies. For graphic EQ, lookup "Shelf Filters" for frequency responses. In that case you could either approximate the filter with a higher res FFT or evaluate the set of digital filters. $\endgroup$ – A_A May 24 '18 at 16:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.