节拍检测和FFT


13

我正在开发一款平台游戏,其中包括带有节拍检测功能的音乐。我目前正在通过检查当前幅度何时超过历史样本来检测节拍。这不适用于振幅很稳定的摇滚等音乐流派。

因此,我进一步研究了发现使用FFT将声音分成多个频段的算法...然后我找到了Cooley-Tukey FFt算法

我唯一的问题是我对音频还很陌生,我也不知道如何使用它将信号分成多个信号。

所以我的问题是:

如何使用FFT将信号分成多个频段?

对于有兴趣的人,这也是我在c#中的算法:

// C = threshold, N = size of history buffer / 1024
    public void PlaceBeatMarkers(float C, int N)
    {
        List<float> instantEnergyList = new List<float>();
        short[] samples = soundData.Samples;

        float timePerSample = 1 / (float)soundData.SampleRate;
        int sampleIndex = 0;
        int nextSamples = 1024;

        // Calculate instant energy for every 1024 samples.
        while (sampleIndex + nextSamples < samples.Length)
        {

            float instantEnergy = 0;

            for (int i = 0; i < nextSamples; i++)
            {
                instantEnergy += Math.Abs((float)samples[sampleIndex + i]);
            }

            instantEnergy /= nextSamples;
            instantEnergyList.Add(instantEnergy);

            if(sampleIndex + nextSamples >= samples.Length)
                nextSamples = samples.Length - sampleIndex - 1;

            sampleIndex += nextSamples;
        }


        int index = N;
        int numInBuffer = index;
        float historyBuffer = 0;

        //Fill the history buffer with n * instant energy
        for (int i = 0; i < index; i++)
        {
            historyBuffer += instantEnergyList[i];
        }

        // If instantEnergy / samples in buffer < instantEnergy for the next sample then add beatmarker.
        while (index + 1 < instantEnergyList.Count)
        {
            if(instantEnergyList[index + 1] > (historyBuffer / numInBuffer) * C)
                beatMarkers.Add((index + 1) * 1024 * timePerSample); 
            historyBuffer -= instantEnergyList[index - numInBuffer];
            historyBuffer += instantEnergyList[index + 1];
            index++;
        }
    }

我想维基百科的FFTDSP条目是一个很好的起点。节拍检测条目很少,但链接到gamedev.net
Tobias Kienzler 2011年

Answers:


14

好吧,如果您的输入信号是实数(例如,每个样本都是实数),则频谱将是对称且复杂的。利用对称性,通常FFT算法通过只给您频谱的正半部分来回馈结果。每个频带的实部在偶数样本中,虚部在奇数样本中。有时,实际部分在响应的前半部分打包在一起,而虚构部分在后半部分打包在一起。

在公式中,如果X [k] = FFT(x [n]),则给它一个向量i [n] = x [n],并获得输出o [m],然后

X[k] = o[2k] + j·o[2k+1]

(尽管有时您得到X [k] = o [k] + j·o [k + K / 2],其中K是窗口的长度,在示例中为1024)。顺便说一句,j是虚数单位sqrt(-1)。

一个带的大小被计算为该带与其复共轭的乘积的根:

|X[k]| = sqrt( X[k] · X[k]* )

能量定义为幅度的平方。

如果我们称a = o [2k]和b = o [2k + 1],我们得到

X[k] = a + j·b

因此

E[k] = |X[k]|^2 = (a+j·b)·(a-j·b) = a·a + b·b

展开整个过程,如果您从FFT算法获得o [m]作为输出,则频带k的能量为:

E[k] = o[2k] · o[2k] + o[2k+1] · o[2k+1]

(注意:为了避免与共轭运算符混淆,我使用符号·而不是通常的*来表示乘法)

假设采样频率为44.1Khz,窗口为1024个采样,则频段k的频率为

freq(k) = k / 1024 * 44100 [Hz]

因此,例如,您的第一个频带k = 0表示0 Hz,k = 1表示43 Hz,最后一个k = 511表示22KHz(奈奎斯特频率)。

我希望这能回答您有关如何使用FFT获取每个频带的信号能量的问题。

附录:在评论中回答您的问题,并假设您使用问题中发布的链接中的代码(C语言中的Cooley-Tukey算法):假设您将输入数据作为短整数的向量:

// len is 1024 in this example.  It MUST be a power of 2
// centerFreq is given in Hz, for example 43.0
double EnergyForBand( short *input, int len, double centerFreq)
{
  int i;
  int band;
  complex *xin;
  complex *xout;
  double magnitude;
  double samplingFreq = 44100.0; 

  // 1. Get the input as a vector of complex samples
  xin = (complex *)malloc(sizeof(struct complex_t) * len);

  for (i=0;i<len;i++) {
    xin[i].re = (double)input[i];
    xin[i].im = 0;
  }

  // 2. Transform the signal
  xout = FFT_simple(xin, len);

  // 3. Find the band ( Note: floor(x+0.5) = round(x) )
  band = (int) floor(centerFreq * len / samplingFreq + 0.5); 

  // 4. Get the magnitude
  magnitude = complex_magnitude( xout[band] );

  // 5. Don't leak memory
  free( xin );
  free( xout );

  // 6. Return energy
  return magnitude * magnitude;
}

我的C有点生疏(如今,我主要使用C ++进行编码),但是我希望我不会对此代码犯任何大错误。当然,如果您对其他频段的能量感兴趣,则没有必要为每个频段转换整个窗口,这将浪费CPU时间。在这种情况下,只需进行一次转换,然后从xout中获取所需的所有值。


哦,我只是看了一下链接的代码,它已经为您提供了“复杂”形式的结果,甚至为您提供了一个计算复数大小的函数。然后,您只需要为输出向量的每个元素计算该大小的平方,而不必担心对结果进行排序。
CeeJay 2011年

举例来说,如果我从窗口0-1024中获取所有1024个样本,并且将它们作为真实值获取,那么就没有复杂的部分了。我想计算43Hz频段上的能量。那我该如何整合呢?(我只需要返回真实部分,即阳性部分)如果您可以用伪代码来做,那么我将永远深入您,然后我可能会真正理解这个概念:)
Quincy

我编写的代码使用的是您链接的C库,该库已经包含“复杂”结构。这使我在问题中描述的展开变得不必要了(并且代码反映了这一点)
CeeJay 2011年


0

我还没有做过这件事或自己了解过很多,但是我的第一枪是这样的:

首先,您需要应用窗函数以通过FFT获得与时间相关的频谱。拍频通常位于较低的频率上,因此在其中一些频率的强度上应用具有较大时间窗口的另一个 FFT(为简单起见,仅在1 Hz处以100 Hz开始,看看是否足够可靠)。在该频谱中找到峰值,该频率就是节拍的猜测。


它不是我遇到麻烦的节拍检测,而是了解FFT的工作原理。对于信号处理,我真的很陌生,例如:“应用窗口函数通过FFT获得与时间相关的频谱”对我来说没有任何意义。无论如何,谢谢:)
昆西
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.