Realtime C# Pitch Tracker

Use the PitchTracker class when you want to track the pitch of a waveform either in realtime or offline. The algorithm is loosely based on the auto-correlation algorithm, but it is heavily modified to solve two problems inherent in the standard auto-correlation algorithm:

  1. Auto-correlation is slow:
    Since you need to do a compare of all samples with all of the shifted samples, and repeat that once for each frequency you want to detect, this quickly becomes slow.

    The way this was solved was to have 2 passes where the first pass checks only every 8th sample, and then only with course frequency steps. The second pass is only centered around the frequency that the first pass detected, and it does a higher resolution test.
  2. Auto-correlation is inaccurate, especially at higher frequencies:
    The higher the frequency, the less accurate auto-correlation becomes. This is because a higher frequency waveform has fewer samples per cycle, and window size stepping is in whole sample values so the frequency steps become courser the higher it is.

    To solve this, the second pass in the algorithm uses interpolation in order to compare samples that are not limited to whole numbers. For example, sample 0 will be at position 0.0, sample 1 might be at position 0.674, etc. This allows the sample steps to be spaced exactly at the frequency that is to be detected during that pass, as opposed to being quantized into ever-more courser frequency steps. The algorithm uses a 4-point, 3rd-order Hermite interpolator.

    Each high resolution pass is 1.005 times the previous frequency, so the algorithm doesn't use linear frequency steps. Also, once the two passes with the highest correlation is found, a new value is calculated by interpolating between those two values, so the final detected frequency is even higher resolution than the second pass' step size.

Some of the advantages of auto-correlation, and in particular the way it is implemented in this case:

  1. Fast.  As mentioned above, the algorithm is quite fast. It can easily perform 3000 pitch tests per second.
     
  2. Accurate. Measured deviation from the actual frequency is less than +-0.02%.
     
  3. Accurate across a large range of input levels. Because the algorithm uses the ratios of different peaks and not absolute values, it stays accurate over a very wide range of input levels. There is no loss in accuracy across the range from -40dB to 0dB input level.
     
  4. Accurate across the full frequency range. The accuracy stays high across the full range of detected frequencies, from about 50 Hz to 1.6 kHz. This is due to the interpolation that is applied when calculating samples for the sliding windows.
     
  5. Accurate with any type of waveform. Unlike a lot of other types of pitch detecting algorithms, this algorithm is essentially unaffected by complex waveforms. This means that it works with male and female voices of any type, as well as other instruments like guitars, etc. The only requirement is that the signal be monophonic, so chords cannot be detected. This pitch detector will work well as a very responsive guitar tuner.
     
  6. Does not rely on previous results. This algorithm is accurate enough that it does not need to rely on previous results. Each pitch result is a completely new calculated value. Pitch algorithms that track pitch by "locking on" to the pitch suffer from the problem that if they detect the wrong pitch (usually an octave too high or low) they will continue to be wrong for many subsequent tests as well.

  

About the PitchTracker class

The PitchTracker class is simple to use. Just instantiate and optionally set properties like samplerate, the number of tests to perform per second, the number of pitch records to keep in the optional history buffer, etc.

PitchTracker pitchTracker = new PitchTracker();
pitchTracker.SampleRate = 44100.0;
pitchTracker.ProcessBuffer(audioBuffer);


 There are three ways to get pitch results from the PitchTracker class:

  1. After a call to ProcessBuffer, use the PitchTracker.CurrentPitchRecord property to retrieve the latest detected pitch.
     
  2. Enable the history buffer by setting PitchTracker.RecordPitchRecords to true. This will add pitch records into the internal pitch buffer. A limit can be set on the maximum number of records to keep by setting PitchTracker.PitchRecordHistorySize to a non-zero value. This will create a sliding buffer which is useful for displaying a scrolling graph, etc. Note that if no pitch was detected, it still counts as a recorded record.
     
  3. Subscribe to the event PitchTracker.PitchChanged event. This will be called each time a new pitch has been detected. Note that if no pitch was detected, it also counts as a unique pitch record.

Note that there are no constraints on the size of the passed in buffer. One very large buffer can be passed in, at which time multiple records will be created (if the history buffer is enabled), and/or the PitchChanged event will be fired multiple times. This is typical of an offline application. On the other hand, small buffers can be continually passed in, and samples will be buffered internally until there are enough to do a pitch test. This means that a single call to ProcessBuffer can result in zero, 1 or more new pitch records.

 

About the sample application

The sample application displays the PitchTracker class in use. Both pitch and amplitude can be adjusted. The results from the pitch detector is displayed, together with the current detection error.

PitchTracker Sample App

Last edited Feb 15, 2011 at 6:27 AM by BitFlipper, version 6

Comments

Freefall Oct 2, 2015 at 11:52 PM 
Very well documented and nice algorithm. Thank you very much!
If you are interested how this could fit into a media player, you can visit my article here:
http://www.codeproject.com/Articles/990040/MultiWave-a-portable-multi-device-NET-audio-player