C#的FFT不精确

时间:2011-02-11 06:11:12

标签: c# signal-processing fft pitch pitch-tracking

我一直在试验FFT算法。我使用NAudio以及来自互联网的FFT算法的工作代码。根据我对性能的观察,得到的音高是不准确的。

我发现MIDI(由GuitarPro生成)转换为WAV文件(44.1khz,16位,单声道),其中包含从E2(最低吉他音符)到大约E6的音高级数。低音(E2-B3附近)的结果通常是非常错误的。但达到C4有点正确,因为你已经可以看到正确的进展(下一个音符是C#4,然后是D4等)但是,问题在于检测到的音高是低于实际音高的半音(例如,C4应该是音符,但是显示D#4。

您认为可能出错?如有必要,我可以发布代码。非常感谢!我还是开始掌握DSP的领域。

编辑:这是我正在做的事情的粗略划痕

byte[] buffer = new byte[8192];
int bytesRead;
do
{
  bytesRead = stream16.Read(buffer, 0, buffer.Length);
} while (bytesRead != 0);

然后:( waveBuffer只是一个将byte []转换为float []的类,因为该函数只接受float [])

public int Read(byte[] buffer, int offset, int bytesRead)
{
  int frames = bytesRead / sizeof(float);
  float pitch = DetectPitch(waveBuffer.FloatBuffer, frames);
}

最后:( Smbpitchfft是具有FFT算法的类...我相信它没有任何问题所以我不在这里发布它)

private float DetectPitch(float[] buffer, int inFrames)
{
  Func<int, int, float> window = HammingWindow;
  if (prevBuffer == null)
  {
    prevBuffer = new float[inFrames]; //only contains zeroes
  }  

  // double frames since we are combining present and previous buffers
  int frames = inFrames * 2;
  if (fftBuffer == null)
  {
    fftBuffer = new float[frames * 2]; // times 2 because it is complex input
  }

  for (int n = 0; n < frames; n++)
  {
     if (n < inFrames)
     {
       fftBuffer[n * 2] = prevBuffer[n] * window(n, frames);
       fftBuffer[n * 2 + 1] = 0; // need to clear out as fft modifies buffer
     }
     else
     {
       fftBuffer[n * 2] = buffer[n - inFrames] * window(n, frames);
       fftBuffer[n * 2 + 1] = 0; // need to clear out as fft modifies buffer
     }
   }
   SmbPitchShift.smbFft(fftBuffer, frames, -1);
  }

为了解释结果:

float binSize = sampleRate / frames;
int minBin = (int)(82.407 / binSize); //lowest E string on the guitar
int maxBin = (int)(1244.508 / binSize); //highest E string on the guitar

float maxIntensity = 0f;
int maxBinIndex = 0;

for (int bin = minBin; bin <= maxBin; bin++)
{
    float real = fftBuffer[bin * 2];
    float imaginary = fftBuffer[bin * 2 + 1];
    float intensity = real * real + imaginary * imaginary;
    if (intensity > maxIntensity)
    {
        maxIntensity = intensity;
        maxBinIndex = bin;
    }
}

return binSize * maxBinIndex;

更新(如果有人仍然感兴趣):

因此,下面的答案中的一个说明来自FFT的频率峰值并不总是等于音调。我明白那个。但是,如果是这种情况,我想为自己尝试一些事情(假设有时频率峰值是最终的音调)。所以基本上,我有2个软件(DewResearch的SpectraPLUS和FFTProperties;归功于他们),它能够显示音频信号的频域。

以下是时域中频率峰值的结果:

SpectraPLUS

SpectraPLUS

和FFT属性: enter image description here

这是使用A2的测试笔记(大约110Hz)完成的。在查看图像时,SpectraPLUS的频率峰值在102-112 Hz附近,FFT属性的频率峰值在108 Hz附近。在我的代码中,我得到104Hz(我使用8192个块,44.1khz的采样率... 8192然后加倍使其复杂输入,所以最后,我得到大约5Hz的binsize,与SpectraPLUS的10Hz binsize相比)。

所以现在我有点困惑,因为在软件上他们似乎返回了正确的结果,但在我的代码中,我总是得到104Hz(注意我已经比较了我和其他人使用的FFT函数,如Math.Net和这似乎是正确的。)

您是否认为问题可能与我对数据的解释有关?或者,在显示频谱之前,软件是否会做其他事情?谢谢!

4 个答案:

答案 0 :(得分:11)

听起来您的FFT输出可能存在解释问题。几个随机点:

  • FFT具有有限的分辨率 - 每个输出区的分辨率为Fs / N,其中Fs是采样率,N是FFT的大小

  • 对于音阶较低的音符,连续音符之间的频率差异相对较小,因此您需要足够大的N来区分半音音符之间的区别(见下面的注1)

  • 第一个bin(索引0)包含以0 Hz为中心的能量,但包含来自+/- Fs / 2N的能量

  • bin i包含以i * Fs / N为中心的能量,但包含此中心频率两侧+/- Fs / 2N的能量

  • 你会从相邻的垃圾箱中得到spectral leakage - 这有多糟糕取决于你使用的window function - 没有窗口(==矩形窗口)和频谱泄漏会非常糟糕(非常宽峰) - 对于频率估计,你想选择一个窗口函数,为你提供尖锐的峰值

  • 音高与频率不同 - 音高是感知,频率是物理量 - 乐器的感知音高可能与基频略有不同,具体取决于乐器的类型(一些仪器甚至不能在其基频上产生显着的能量,但我们仍然认为它们的音调就好像基本存在一样)

我从可用的有限信息中得到的最好的猜测是,在将bin索引转换为频率时,或许你的某个地方“一个接一个”,或者你的FFT太小而无法为低音提供足够的分辨率,你可能需要增加N.

您还可以通过几种技术改善音高估计,例如倒频谱分析,或者查看FFT输出的相位分量,并将其与连续FFT进行比较(这样可以在箱内进行更精确的频率估计)给定FFT大小)。


注释

(1)只是为此加上一些数字,E2为82.4 Hz,F2为87.3 Hz,所以你需要一个比5 Hz更好的分辨率来区分吉他上最低的两个音符(并且比这个更精细)如果你真的想要做,比如说,准确的调整)。在44.1 kHz样本处,您可能需要至少N = 8192的FFT才能获得足够的分辨率(44100/8192 = 5.4 Hz),可能N = 16384会更好。

答案 1 :(得分:3)

我认为这可能会对你有所帮助。我制作了一些吉他的6个开弦图。代码是使用pylab的Python,我推荐用于实验:

# analyze distorted guitar notes from
# http://www.freesound.org/packsViewSingle.php?id=643
#
# 329.6 E - open 1st string
# 246.9 B - open 2nd string
# 196.0 G - open 3rd string
# 146.8 D - open 4th string
# 110.0 A - open 5th string
#  82.4 E - open 6th string

from pylab import *
import wave

fs = 44100.0 
N = 8192 * 10
t = r_[:N] / fs
f = r_[:N/2+1] * fs / N 
gtr_fun = [329.6, 246.9, 196.0, 146.8, 110.0, 82.4]

gtr_wav = [wave.open('dist_gtr_{0}.wav'.format(n),'r') for n in r_[1:7]]
gtr = [fromstring(g.readframes(N), dtype='int16') for g in gtr_wav]
gtr_t = [g / float64(max(abs(g))) for g in gtr]
gtr_f = [2 * abs(rfft(g)) / N for g in gtr_t]

def make_plots():
    for n in r_[:len(gtr_t)]:
        fig = figure()
        fig.subplots_adjust(wspace=0.5, hspace=0.5)
        subplot2grid((2,2), (0,0))
        plot(t, gtr_t[n]); axis('tight')
        title('String ' + str(n+1) + ' Waveform')
        subplot2grid((2,2), (0,1))
        plot(f, gtr_f[n]); axis('tight')
        title('String ' + str(n+1) + ' DFT')
        subplot2grid((2,2), (1,0), colspan=2)
        M = int(gtr_fun[n] * 16.5 / fs * N)
        plot(f[:M], gtr_f[n][:M]); axis('tight')
        title('String ' + str(n+1) + ' DFT (16 Harmonics)')

if __name__ == '__main__':
    make_plots()
    show()

字符串1,基本= 329.6 Hz:

String 1, f0 = 329.6 Hz

字符串2,基本= 246.9 Hz:

enter image description here

字符串3,基本= 196.0 Hz:

enter image description here

字符串4,基本= 146.8 Hz:

enter image description here

字符串5,基本= 110.0 Hz:

enter image description here

字符串6,基本= 82.4 Hz:

enter image description here

基频并不总是主要的谐波。它确定周期信号的谐波之间的间隔。

答案 2 :(得分:1)

我有similar question,我的答案是使用Goertzel代替FFT。如果你知道你正在寻找什么音(MIDI)Goertzel能够在一个正弦波(一个周期)内检测音调。它通过生成声音的正弦波并“将其置于原始数据之上”来查看它是否存在来实现此目的。 FFT对大量数据进行采样,以提供近似的频谱。

答案 3 :(得分:1)

音高不同于频率峰值。音高是一种心理感知现象,可能更多地依赖于泛音等。人类称之为音高的频率在实际信号频谱中可能会丢失或非常小。

频谱中的频率峰值可能与任何FFT bin中心不同。 FFT bin中心频率的频率和间距将根据FFT长度和采样率而变化,而不是数据中的频谱。

所以你至少要遇到两个问题。关于频率估计的大量学术论文以及音调估计的单独主题。从那里开始。