C#中FFT的不准确性

10

我一直在尝试使用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中的频率峰值并不总是等同于音高。我理解这个道理。但是,我想自己尝试一些东西,看看是否如此(在假设有时频率峰值就是结果音高的情况下)。因此,我得到了两个软件(SpectraPLUS和FFTProperties by DewResearch;感谢他们)可以显示音频信号的频域。

接下来是时间域内频率峰值的结果:

SpectraPLUS

SpectraPLUS

和FFT Properties: enter image description here

这是使用 A2 测试音符(约为 110Hz)完成的。在查看图片时,它们的频率峰值约在 102-112 Hz 范围内(SpectraPLUS)和 108 Hz(FFT Properties)。在我的代码中,我得到了 104Hz(我使用 8192 块和 44.1khz 的采样率…然后将 8192 倍增加为复杂输入,因此最终获得 binsize 约为 5Hz,相对于 SpectraPLUS 的 10Hz binsize)。

现在我有点困惑了,因为在软件上它们似乎返回了正确的结果,但是在我的代码中,我总是得到 104Hz(请注意,我已经将所使用的 FFT 函数与 Math.Net 等其他函数进行了比较,并且似乎是正确的)。

您认为问题可能出现在我的数据解读上吗?还是软件在显示频率谱之前做了其他事情?谢谢!


嗨!我得到的maxBinIndex值在第20个bin(约100-104 Hz),这导致大约是G#,比应该的A低半音。这与其他.wav文件一致,有时会降整个音阶。 - user488792
@eryksun 谢谢!你最后的那个观点很有意思。我会尝试去了解一下。 - user488792
@eryksun 你好!非常感谢!这似乎是问题所在。我的代码现在可以正常工作并返回正确的频率。看来我错过了Paul R答案中的这个解决方案,因为当时我对FFT还不太了解。然而,由于你们所有人的帮助,我学到了很多。再次感谢! - user488792
然而,prevBuffer元素从未被设置,因此其值始终为0。这是正确的行为吗? - linquize
4个回答

11

看起来你的FFT输出可能存在解释问题。以下是一些随机提示:

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

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

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

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

  • 相邻 bin 会产生spectral leakage - 这取决于您使用的window function - 如果不使用窗口(== 矩形窗口),则谱泄漏将非常严重(峰值非常宽) - 对于频率估计,您需要选择一个可以给您带来尖锐峰值的窗口函数。

  • 音高不等同于频率 - 音高是一种感知,频率是一种物理量 - 音乐乐器的感知音高可能与基频略有不同,这取决于乐器类型(某些乐器甚至不会在其基频产生显著能量,但我们仍然感知它们的音高就像基频存在一样)。

根据有限的信息,我的最佳猜测是您在将二进制索引转换为频率时可能存在“偏移一位”的问题,或者您的FFT过小,无法为低音提供足够的分辨率,您可能需要增加N。

您还可以通过几种技术来改善音高估计,例如倒谱分析,或通过查看FFT输出的相位组件并将其与连续的FFT进行比较(这允许更准确地估计给定FFT大小的一个bin内的频率)。


注意事项

(1) 仅为了说明一些数字,E2 的频率为82.4 Hz,F2 的频率为87.3 Hz,因此您需要比5 Hz 更好的分辨率来区分吉他上最低的两个音符(如果您实际上想进行精确的调谐,则需要比这更细)。在44.1 kHz采样率下,您可能需要至少N = 8192的FFT才能给您足够的分辨率(44100/8192 = 5.4 Hz),可能N = 16384会更好。


嗨,保罗!非常感谢你的回答!我目前正在使用汉明窗口作为窗函数,并使用N = 4096。但原因是我利用交错使FFT算法的输入缓冲区变得更大。通常,我会在输入缓冲区中插入零。我将尝试一些方法来检查是否可以提高准确性。谢谢! - user488792
2
@user488792:好的,听起来你已经有了一个不错的开始。Hamming窗口是一个合理的选择,但请注意,将零填充到数据中以获得更多“表面”分辨率并没有真正为你带来任何好处——它只是插值了结果FFT输出,使其看起来更平滑,但没有额外的信息(没有免费的午餐!)。 - Paul R
@eryksun:说得好 - 我把“interleaving”误读为“padding”了。@user488792:需要将零附加到缓冲区以获得插值频谱,正如@eryksun所说的那样 - 这是你正在做的吗?还是你真的在样本之间交错插入零? - Paul R
大家好!我对代码进行了一些更改,例如重新分配prevBuffer并使采样大小更大(8192,因为我尝试了16384但效果更差)。因此,我想知道是否可能我错误地解释了FFT的结果? 我已经发布了我目前所拥有的东西。更好的FFT结果澄清也很重要,因为目前我不确定如何通过更改minBin和maxBin的值来改变结果。谢谢! - user488792
1
你好!非常感谢所有的帮助。经过长时间的跟踪、调试和运行时值的检查,我得出结论:也许问题出在我的音频信号上(因为有些人提到了频率-音高估计并不是真正定义的)。虽然我将继续尝试并进行实验,但与此同时,我认为我已经学到了很多,并对FFT算法有了更深入的理解。再次感谢您的大力支持! - user488792
显示剩余7条评论

3

我想这可能会对你有所帮助。我制作了一些关于吉他6根开放弦的图表。代码是使用Python和pylab编写的,我建议用它进行实验:

# 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:

弦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

基频并非始终为主导的谐波。它确定周期信号谐波之间的间隔。


嗨!非常感谢您,我很欣赏您的努力。这将对学习和进一步分析非常有帮助。谢谢! - user488792
你好!我对我的情况进行了一些更新。你能看一下吗?非常感谢! - user488792

1

我之前有个类似的问题,我的解决方案是使用高兹贝尔算法代替快速傅里叶变换(FFT)。如果你知道你所寻找的音调(MIDI),那么高兹贝尔算法能够检测到一定范围内的音调(一个正弦周期内)。它通过产生声音的正弦波,并将其“放置在原始数据的顶部”来判断它是否存在。FFT则会取样大量数据,提供近似的频谱。


嗨!感谢您的建议!不过,我正在处理WAV文件,所以在这种情况下我认为FFT会更好。此外,我正在尝试让它工作并更好地学习它,因为将来我将与和弦检测一起使用它(当然还有其他算法)。谢谢! - user488792

1

音高并不等同于频率峰值。音高是一种心理知觉现象,可能更多取决于泛音等因素。人类称为音高的频率在实际信号频谱中可能缺失或非常微小。

而频谱中的频率峰可能与任何FFT(快速傅里叶变换)的频率中心不同。FFT的频率中心会根据FFT长度和采样率的变化而改变频率和间距,而不受数据中频谱的影响。

因此,您至少需要解决2个问题。有大量关于频率估计以及音高估计的学术论文,请从这里开始。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接