I've whipped together a small example that tries to use the Harmonic Product Spectrum Algorithm to extract the pitch of a simple sine wave. I'm not sure about my implementation and if i understood everything completely, that's why i'm hoping for some helping hand here.
The code is here: https://gist.github.com/akuehntopf/4da9bced2cb88cfa2d19 (You'll need JTransform and JavaFX to compile, though).
Please see what happens with the data during the processing using the following graphs. Note: I'm using a sine wave with 160Hz and a sample rate of 8000. The duration of the sound data is 1 second.
First the sine wave is displayed (see first image).
Second, i'm applying a hamming window to the data (second image)
Then i calculate the power spectrum of the data as per
private float[] powerSpectrum(float[] window) {
float[] powerSpectrum = new float[window.length];
float[] fftBuffer = new float[window.length * 2 + 1];
System.arraycopy(window, 0, fftBuffer, 0, window.length);
FloatFFT_1D fft = new FloatFFT_1D(window.length);
fft.realForward(fftBuffer);
for (int i=0; i < fftBuffer.length / 2 - 1; i++) {
float real = fftBuffer[2*i];
float imag = fftBuffer[2*i+1];
powerSpectrum[i] = (float)Math.sqrt(real*real + imag * imag);
}
return powerSpectrum;
}
Which gives me the spectrum in the third graph.
Observe that the peak is approximately at the right spot already, but we go for HPS, so continue (i'm not really sure about everything from here on):
Next, i'm multiplying the signal with its compressed form several times (with increasing compression)...
// 4. Compress
float[] spectrumCopy = new float[spectrum.length];
System.arraycopy(spectrum, 0, spectrumCopy, 0, spectrum.length);
for (int compression = 2; compression < 4; compression++) {
for (int i = 1; i < spectrum.length; i++) {
spectrum[i] = spectrum[i] * getCompressedSample(spectrumCopy, 1, compression, i);
}
}
it uses the function
private float getCompressedSample(float[] buffer, int offset, int compression, int loc) {
if (offset + loc * compression < buffer.length) {
return buffer[offset + loc * compression];
}
return 0;
}
i found it somewhere on the interwebs. But my understanding is that we compress the power spectrum n times while taking only every 2, then only every 3 and so on samples from the power spectrum. The original spectrum is then multiplied with each of those compressed spectra.
The result i get is the fourth graph
Observe that the peaks have changed.
From my understanding the next step would be to find the bin with the highest peak, interpolate (i use quadratic interpolation) and recalculate the pitch frequency using this formula:
private float getFrequencyForIndex(int index, int size, int rate) {
float freq = (float)index * (float)rate / (float)size;
return freq;
}
However this gives me wrong results. I'm pretty much stuck. Any help with this topic would be very much appreciated! Thanks in advance!
For the next step i'm hoping to be able to extract pitch frequency data from a musical instrument (guitar/ukulele), but of course first things first :-)