Search code examples
javaandroidfftfrequency-analysis

Incorrect Frequency in tuner app


I am currently working on an OpenGL tuner app. I currently have the FFT plot on screen, and I would like to calculate the loudest frequency. Here is what my code looks like:

MainActivity

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // ...

    transformer = new RealDoubleFFT(samples);
    started = true;
    recordAudio = new RecordAudio();
    recordAudio.execute();
  }

  // ...

  public class RecordAudio extends AsyncTask<Void, double[], Void> {

    @Override
    protected Void doInBackground(Void... arg0) {
      try {
        int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

        AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate,
          channelConfig, audioFormat, bufferSize);

        short[] buffer = new short[samples];
        double[] fft = new double[samples];

        audioRecord.startRecording();

        while (started) {
          int bufferReadResult = audioRecord.read(buffer, 0, samples);

          for (int i = 0; i < samples && i < bufferReadResult; i++) {
            fft[i] = (double) buffer[i] / 32768.0;
          }
          transformer.ft(fft);
          publishProgress(fft);
        }

        audioRecord.stop();
      } catch (Throwable t) {
        t.printStackTrace();
        Log.e("MainActivity.AudioRecord", "The audio recording has encountered an error.");
      }
      return null;
    }

    @Override
    protected void onProgressUpdate(double[]... fft) {
      int peak = (int) ((frequency / ((float) sampleRate / (float) samples)) * 2f);
      int offset = 0;
      for (int i = 0; i < samples; i++) {
        fftVertices[offset++] = (((float) i / (float) samples) * 2f) - 1f;
        fftVertices[offset++] = -1f;

        float color = 0;
        if (i < peak) {
          color = (float) (peak - i) / (float) peak;
        } else if (i > peak) {
          color = (float) (i - peak) / (float) ((int) ((float) samples / 2f) - peak);
        } else {
          color = 0f;
        }
        color = 1f - color;

        fftVertices[offset++] = color;
        fftVertices[offset++] = color;
        fftVertices[offset++] = color;

        fftVertices[offset++] = (((float) i / (float) samples) * 2f) - 1f;
        fftVertices[offset++] = ((float) fft[0][i] / 2f) - 1f;

        fftVertices[offset++] = color;
        fftVertices[offset++] = color;
        fftVertices[offset++] = color;

        if (i % 2 == 0) {
          magnitude[(int) ((float) i / 2f)] = (float) Math.sqrt(fft[0][i] * fft[0][i] + fft[0][i + 1] * fft[0][i + 1]);
        }
      }
      updateFrequency(magnitude);
    }

  }

  private static void updateFrequency(float[] mag) {
    int peak = 0;
    for (int i = 0; i < (int) ((float) samples / 2f); i++) {
      if (mag[i] >= mag[peak]) {
        peak = i;
      }
    }
    frequency = peak * sampleRate / samples;
    Log.d("MainActivity", "Frequency: " + frequency + " Hz");
  }

  private static float[] getVertices() {
    return fftVertices;
  }

  private static class Renderer implements GLSurfaceView.Renderer {

    // ...

    private float[] vertices = new float[samples * 2 * (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT)];

    public Renderer(Context context) {

      // ...

      int offset = 0;
      for (int i = 0; i < samples; i++) {
        vertices[offset++] = (((float) i / (float) samples) * 2f) - 1f;
        vertices[offset++] = -1f;

        vertices[offset++] = 1f;
        vertices[offset++] = 1f;
        vertices[offset++] = 1f;

        vertices[offset++] = (((float) i / (float) samples) * 2f) - 1f;
        vertices[offset++] = ((float) Math.random() * 2f) - 1f;

        vertices[offset++] = 1f;
        vertices[offset++] = 1f;
        vertices[offset++] = 1f;
      }

      // ...
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

      // ...
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
      glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
      glClear(GL_COLOR_BUFFER_BIT);

      // ...
    }

  }

So the MainActivity contains the RecordAudio class. I left out some lines that weren't important, like OpenGL or application stuff. Basically, my application reads audio input and displays the FFT onscreen. I have set up a frequency calculation as follows:

I first make a float array of the magnitudes:

magnitude[(int) ((float) i / 2f)] = (float) Math.sqrt(fft[0][i] * fft[0][i] + fft[0][i + 1] * fft[0][i + 1]);

Here, inside a for loop which iterates through the FFT, each iteration where the index is even, a magnitude is added to the array. Then at the end, the frequency is calculated:

  private static void updateFrequency(float[] mag) {
    int peak = 0;
    for (int i = 0; i < (int) ((float) samples / 2f); i++) {
      if (mag[i] >= mag[peak]) {
        peak = i;
      }
    }
    frequency = peak * (int) ((float) sampleRate / (float) samples);
    Log.d("MainActivity", "Frequency: " + frequency + " Hz");
  }

By finding the peak position, then calculating the frequency.

My problem is: I am playing a 440 Hz sine wave into the phone's mic, and it's output is lots of sporadic numbers, but some of them are 430 Hz. Here is a sample output:

12-31 16:34:24.992    387.59766 Hz
12-31 16:34:25.022    430.66406 Hz
12-31 16:34:25.042    387.59766 Hz
12-31 16:34:25.072    430.66406 Hz
12-31 16:34:25.122    387.59766 Hz
12-31 16:34:25.142    430.66406 Hz
12-31 16:34:25.162    387.59766 Hz
12-31 16:34:25.182    430.66406 Hz
12-31 16:34:25.182    387.59766 Hz
12-31 16:34:25.192    430.66406 Hz
12-31 16:34:25.222    430.66406 Hz
12-31 16:34:25.242    387.59766 Hz
12-31 16:34:25.262    430.66406 Hz
12-31 16:34:25.292    430.66406 Hz
12-31 16:34:25.312    387.59766 Hz
12-31 16:34:25.332    387.59766 Hz
12-31 16:34:25.372    430.66406 Hz
12-31 16:34:25.392    387.59766 Hz
12-31 16:34:25.422    430.66406 Hz
12-31 16:34:25.432    1722.6563 Hz
12-31 16:34:25.452    387.59766 Hz
12-31 16:34:25.472    430.66406 Hz
12-31 16:34:25.502    387.59766 Hz
12-31 16:34:25.522    430.66406 Hz
12-31 16:34:25.553    387.59766 Hz
12-31 16:34:25.573    387.59766 Hz
12-31 16:34:25.603    430.66406 Hz
12-31 16:34:25.623    387.59766 Hz

How can I achieve a more stable and accurate result?


Solution

  • One of the issues of a DFT is that if your peak is wide and lies over two (or more) bins and it shifts ever so slightly (because of doppler or other reasons), you'll get fluctuations of energy between the two bins.

    One way is to increase the number of bins of the FFT (number of points of your FFT). You may want to consider Zero-padding for that as well.

    If you don't want to mess around with number of points of FFT, another approach is to interpolate peak value (and frequency) from the FFT data. This article gives good approach to such a technique.

    I am not sure about accuracy, but this should help with stability (reduce the fluctuation) of the result in my opinion.