1

I am building an app that needs to be able to display a real-time spectral analyzer. Here is the version I was able to successfully make on iOS:

Sample Spectral Analyser

I am using Wendykierp JTransforms library to perform the FFT calculations, and have managed to capture audio data and execute the FFT functions. See below:

short sData[] = new short[BufferElements2Rec];
int result = audioRecord.read(sData, 0, BufferElements2Rec);

try
{
    //Initiate FFT
    DoubleFFT_1D fft = new DoubleFFT_1D(sData.length);

    //Convert sample data from short[] to double[]
    double[] fftSamples = new double[sData.length];
    for (int i = 0; i < sData.length; i++) {
        //IMPORTANT: We cannot simply cast the short value to double.
        //As a double is only 2 bytes (values -32768 to 32768)
        //We must divide by 32768 before we cast to Double.
        fftSamples[i] = (double) sData[i] / 32768;
    }

    //Perform fft calcs
    fft.realForward(fftSamples);

    //TODO - Convert FFT data into 20 "bands"

} Catch (Exception e)
{

}

In iOS, I was using a library (Tempi-FFT) which had built in functionality for calculating magnitude, frequency, and providing averaged data for any given number of bands (I am using 20 bands as you can see in the image above). It seems I don't have that luxury with this library and I need to calculate this myself.

Looking for any good examples or tutorials on how to interperate the data returned by the FFT calculations. Here is some sample data I am receiving:

-11387.0, 183.0, -384.9121475854448, -224.66315714636642, -638.0173005872095, -236.2318653974911, -1137.1498541119106, -437.71599514435786, 1954.683405957685, -2142.742125980924 ...

Looking for simple explanation of how to interpret this data. Some other questions I have looked at that I was either unable to understand, or did not provide information on how to determine a given number of bands:

Power Spectral Density from jTransforms DoubleFFT_1D

How to develop a Spectrum Analyser from a realtime audio?

JCutting8
  • 732
  • 9
  • 29
  • FFT is definitely not a simple thing. The first link (https://stackoverflow.com/questions/5010261/power-spectral-density-from-jtransforms-doublefft-1d) provides the simplest explanation of how to interperet your output. Give me a little bit and I'll post a specific answer to your question. – Sub 6 Resources Nov 21 '18 at 19:25
  • Posted an answer. Hope its useful to you! – Sub 6 Resources Nov 21 '18 at 21:12

1 Answers1

3

Your question can be split into two parts: finding the magnitude of all frequencies (interpreting the output) and averaging the frequencies into bands


Finding the magnitude of all frequencies:

I won't go into the intricacies of the Fast Fourier Transform/Discrete Fourier Transform (if you would like to gain a basic understanding see this video), but know that there is a real and an imaginary part of each output.

The documentation of the realForward function describes where both the imaginary and the real parts are located in the output array (I'm assuming you have an even sample size):

a[2*k] = Re[k], 0 <= k < n / 2
a[2*k+1] = Im[k], 0 < k < n / 2
a[1] = Re[n/2] 

a is equivalent to your fftSamples, which means we can translate this documentation into code as follows (I've changed Re and Im to realPart and imaginaryPart respectively):

int n = fftSamples.length;

double[] realPart = new double[n / 2];
double[] imaginaryPart = new double[n / 2];

for(int k = 0; k < n / 2; k++) {
    realPart[k] = fftSamples[k * 2];
    imaginaryPart[k] = fftSamples[k * 2 + 1];
}

realPart[n / 2] = fftSamples[1];

Now we have the real and imaginary parts of each frequency. We could plot these on an x-y coordinate plane using the real part as the x value and the imaginary part as the y value. This creates a triangle, and the length of the triangle's hypotenuse is the magnitude of the frequency. We can use the pythagorean theorem to get this magnitude:

double[] spectrum = new double[n / 2];

for(int k = 1; k < n / 2; k++) {
    spectrum[k] = Math.sqrt(Math.pow(realPart[k], 2) + Math.pow(imaginaryPart[k], 2));
}

spectrum[0] = realPart[0];

Note that the 0th index of the spectrum doesn't have an imaginary part. This is the DC component of the signal (we won't use this).

Now, we have an array with the magnitudes of each frequency across your spectrum (If your sampling frequency is 44100Hz, this means you now have an array with the magnitudes of the frequencies between 0Hz and 44100Hz, and if you have 441 values in your array, then each index value represents a 100Hz step.)


Averaging the frequencies into bands:

Now that we've converted the FFT output to data that we can use, we can move on to the second part of your question: finding the averages of different bands of frequencies. This is relatively simple. We just need to split the array into different bands and find the average of each band. This can be generalized like so:

int NUM_BANDS = 20; //This can be any positive integer.
double[] bands = new double[NUM_BANDS];
int samplesPerBand = (n / 2) / NUM_BANDS;

for(int i = 0; i < NUM_BANDS; i++) {
    //Add up each part
    double total;
    for(int j = samplesPerBand * i ; j < samplesPerBand * (i+1); j++) {
        total += spectrum[j];
    }
    //Take average
    bands[i] = total / samplesPerBand;
}


Final Code:

And that's it! You now have an array called bands with the average magnitude of each band of frequencies. The code above is purposefully not optimized in order to show how each step works. Here is a shortened and optimized version:

int numFrequencies = fftSamples.length / 2;

double[] spectrum = new double[numFrequencies];

for(int k = 1; k < numFrequencies; k++) {
    spectrum[k] = Math.sqrt(Math.pow(fftSamples[k*2], 2) + Math.pow(fftSamples[k*2+1], 2));
}

spectrum[0] = fftSamples[0];

int NUM_BANDS = 20; //This can be any positive integer.
double[] bands = new double[NUM_BANDS];
int samplesPerBand = numFrequencies / NUM_BANDS;

for(int i = 0; i < NUM_BANDS; i++) {
    //Add up each part
    double total;
    for(int j = samplesPerBand * i ; j < samplesPerBand * (i+1); j++) {
        total += spectrum[j];
    }
    //Take average
    bands[i] = total / samplesPerBand;
}

//Use bands in view!

This has been a really long answer, and I haven't tested the code yet (though I do plan to). Feel free to comment if you find any mistakes.

Sub 6 Resources
  • 1,674
  • 15
  • 31
  • Super helpful answer, thank you. In addition, I was also doing something else incorrectly. For anyone else looking to do something similar - the way I convert my values from `short` to `double` for the purpose of the FFT calculation is incorrect. One must account for the fact that a `short` is 2 bytes (range from -32768 to 32768) hence one must divide the `short` by 32768 before casting to `double`. If you do not do this you will get unexpected behaviour/output from the FFT calculation. – JCutting8 Nov 21 '18 at 23:11
  • Yeah, that would cause some issues. :) – Sub 6 Resources Nov 21 '18 at 23:54