如何对FFT数据进行滤波(用于音频可视化)?

21

我在看这个Web Audio API演示, 它是这本不错的书的一部分

如果你看一下演示,fft峰值会平滑地下降。我试图使用minim库在Java模式下做同样的事情。我查看了如何使用doFFTAnalysis()方法在Web音频API中完成此操作,并尝试使用minim复制它。我还尝试移植复数类型的abs()函数的工作方式:

/ 26.2.7/3 abs(__z):  Returns the magnitude of __z.
00565   template<typename _Tp>
00566     inline _Tp
00567     __complex_abs(const complex<_Tp>& __z)
00568     {
00569       _Tp __x = __z.real();
00570       _Tp __y = __z.imag();
00571       const _Tp __s = std::max(abs(__x), abs(__y));
00572       if (__s == _Tp())  // well ...
00573         return __s;
00574       __x /= __s; 
00575       __y /= __s;
00576       return __s * sqrt(__x * __x + __y * __y);
00577     }
00578 

我目前正在使用 Processing(Java 框架/库)进行快速原型开发。我的代码看起来像这样:

import ddf.minim.*;
import ddf.minim.analysis.*;

private int blockSize = 512;
private Minim minim;
private AudioInput in;
private FFT         mfft;
private float[]    time = new float[blockSize];//time domain
private float[]    real = new float[blockSize];
private float[]    imag = new float[blockSize];
private float[]    freq = new float[blockSize];//smoothed freq. domain

public void setup() {
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, blockSize);
  mfft = new FFT( in.bufferSize(), in.sampleRate() );
}
public void draw() {
  background(255);
  for (int i = 0; i < blockSize; i++) time[i] = in.left.get(i);
  mfft.forward( time);
  real = mfft.getSpectrumReal();
  imag = mfft.getSpectrumImaginary();

  final float magnitudeScale = 1.0 / mfft.specSize();
  final float k = (float)mouseX/width;

  for (int i = 0; i < blockSize; i++)
  {      
      float creal = real[i];
      float cimag = imag[i];
      float s = Math.max(creal,cimag);
      creal /= s;
      cimag /= s; 
      float absComplex = (float)(s * Math.sqrt(creal*creal + cimag*cimag));
      float scalarMagnitude = absComplex * magnitudeScale;        
      freq[i] = (k * mfft.getBand(i) + (1 - k) * scalarMagnitude);

      line( i, height, i, height - freq[i]*8 );
  }
  fill(0);
  text("smoothing: " + k,10,10);
}

我的代码没有出现错误,这很好,但是我没有得到期望的行为,这很糟糕。 当平滑(k)接近1时,我希望峰值下降得更慢,但据我所知,我的代码只缩放了频段。

不幸的是,数学和声音不是我的强项,所以我是在摸索着。如何才能复制Web Audio API演示中的不错可视化效果呢?

我想说这可能与编程语言无关,但例如使用JavaScript就行不通 :).不过,我愿意尝试任何其他可以进行FFT分析的Java库。

更新

我已经有了平滑的简单解决方案(如果当前fft频段不高,则连续减小每个先前fft频段的值):

import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
AudioInput  in;
FFT         fft;

float smoothing = 0;
float[] fftReal;
float[] fftImag;
float[] fftSmooth;
int specSize;
void setup(){
  size(640, 360, P3D);
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 512);
  fft = new FFT(in.bufferSize(), in.sampleRate());
  specSize = fft.specSize();
  fftSmooth = new float[specSize];
  fftReal   = new float[specSize];
  colorMode(HSB,specSize,100,100);
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.left);
  fftReal = fft.getSpectrumReal();
  fftImag = fft.getSpectrumImaginary();
  for(int i = 0; i < specSize; i++)
  {
    float band = fft.getBand(i);

    fftSmooth[i] *= smoothing;
    if(fftSmooth[i] < band) fftSmooth[i] = band;
    stroke(i,100,50);
    line( i, height, i, height - fftSmooth[i]*8 );
    stroke(i,100,100);
    line( i, height, i, height - band*8 );


  }
  text("smoothing: " + (int)(smoothing*100),10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
}

FFT smooth

褪色的图表是平滑的,完全饱和的是实时的。

然而,与Web音频API演示相比,我仍然缺少一些东西:

Web Audio API demo

我认为Web Audio API可能会考虑到中高频需要缩放以更接近我们的感知,但我不确定如何解决。
我试图阅读有关RealtimeAnalyser类如何为WebAudioAPI执行此操作的更多信息,但似乎FFTFrame类doFFT方法可能执行对数缩放。我还没有弄清楚doFFT的工作原理。
如何使用对数比例尺缩放原始FFT图形以考虑感知?我的目标是做出一个看起来不错的可视化效果,我猜我需要:
- 平滑值,否则元素将动画过快/抖动 - 缩放FFT bins/bands以获得更好的中高频数据 - 将FFT值映射到视觉元素(找到最大值/边界)
你有什么提示可以帮助我实现这一点吗?
更新2
我猜测这部分是在Web音频API中实现平滑和缩放的: // 将输入正弦波归一化为0dBfs,以便其注册为0dBfs(撤销FFT缩放因子)。 const double magnitudeScale = 1.0 / DefaultFFTSize;
// A value of 0 does no averaging with the previous result.  Larger values produce slower, but smoother changes.
double k = m_smoothingTimeConstant;
k = max(0.0, k);
k = min(1.0, k);    

// Convert the analysis data from complex to magnitude and average with the previous result.
float* destination = magnitudeBuffer().data();
size_t n = magnitudeBuffer().size();
for (size_t i = 0; i < n; ++i) {
    Complex c(realP[i], imagP[i]);
    double scalarMagnitude = abs(c) * magnitudeScale;        
    destination[i] = float(k * destination[i] + (1 - k) * scalarMagnitude);
}

似乎缩放是通过取复杂值的绝对值来完成的。这篇文章指向同一个方向。我尝试使用Minim使用复数的绝对值和使用各种窗口函数,但它仍然不像我所瞄准的(Web Audio API演示):
import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
AudioInput  in;
FFT         fft;

float smoothing = 0;
float[] fftReal;
float[] fftImag;
float[] fftSmooth;
int specSize;

WindowFunction[] window = {FFT.NONE,FFT.HAMMING,FFT.HANN,FFT.COSINE,FFT.TRIANGULAR,FFT.BARTLETT,FFT.BARTLETTHANN,FFT.LANCZOS,FFT.BLACKMAN,FFT.GAUSS};
String[] wlabel = {"NONE","HAMMING","HANN","COSINE","TRIANGULAR","BARTLETT","BARTLETTHANN","LANCZOS","BLACKMAN","GAUSS"};
int windex = 0;

void setup(){
  size(640, 360, P3D);
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 512);
  fft = new FFT(in.bufferSize(), in.sampleRate());
  fft.window(window[windex]);
  specSize = fft.specSize();
  fftSmooth = new float[specSize];
  fftReal   = new float[specSize];
  colorMode(HSB,specSize,100,100);
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);
  fftReal = fft.getSpectrumReal();
  fftImag = fft.getSpectrumImaginary();
  for(int i = 0; i < specSize; i++)
  {
    float band = fft.getBand(i);

    //Sw = abs(Sw(1:(1+N/2))); %# abs is sqrt(real^2 + imag^2)
    float abs = sqrt(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);

    fftSmooth[i] *= smoothing;
    if(fftSmooth[i] < abs) fftSmooth[i] = abs;

    stroke(i,100,50);
    line( i, height, i, height - fftSmooth[i]*8 );
    stroke(i,100,100);
    line( i, height, i, height - band*8 );


  }
  text("smoothing: " + (int)(smoothing*100)+"\nwindow:"+wlabel[windex],10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
  if(key == 'W' && windex < window.length-1) windex++;
  if(key == 'w' && windex > 0) windex--;
  if(key == 'w' || key == 'W') fft.window(window[windex]);
}

我不确定我是否正确使用窗口函数,因为我没有注意到它们之间有很大的区别。复数的绝对值是正确的吗?如何获得更接近我的目标的可视化效果?
更新3:
我尝试应用@wakjah的有用提示,如下:
import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
AudioInput  in;
FFT         fft;

float smoothing = 0;
float[] fftReal;
float[] fftImag;
float[] fftSmooth;
float[] fftPrev;
float[] fftCurr;
int specSize;

WindowFunction[] window = {FFT.NONE,FFT.HAMMING,FFT.HANN,FFT.COSINE,FFT.TRIANGULAR,FFT.BARTLETT,FFT.BARTLETTHANN,FFT.LANCZOS,FFT.BLACKMAN,FFT.GAUSS};
String[] wlabel = {"NONE","HAMMING","HANN","COSINE","TRIANGULAR","BARTLETT","BARTLETTHANN","LANCZOS","BLACKMAN","GAUSS"};
int windex = 0;

int scale = 10;

void setup(){
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 512);
  fft = new FFT(in.bufferSize(), in.sampleRate());
  fft.window(window[windex]);
  specSize = fft.specSize();
  fftSmooth = new float[specSize];
  fftPrev   = new float[specSize];
  fftCurr   = new float[specSize];
  size(specSize, specSize/2);
  colorMode(HSB,specSize,100,100);
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);
  fftReal = fft.getSpectrumReal();
  fftImag = fft.getSpectrumImaginary();
  for(int i = 0; i < specSize; i++)
  {
    //float band = fft.getBand(i);
    //Sw = abs(Sw(1:(1+N/2))); %# abs is sqrt(real^2 + imag^2)
    //float abs = sqrt(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);
    //fftSmooth[i] *= smoothing;
    //if(fftSmooth[i] < abs) fftSmooth[i] = abs;

    //x_dB = 10 * log10(real(x) ^ 2 + imag(x) ^ 2);
    fftCurr[i] = scale * (float)Math.log10(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);
    //Y[k] = alpha * Y_(t-1)[k] + (1 - alpha) * X[k]
    fftSmooth[i] = smoothing * fftPrev[i] + ((1 - smoothing) * fftCurr[i]);

    fftPrev[i] = fftCurr[i];//

    stroke(i,100,100);
    line( i, height, i, height - fftSmooth[i]);

  }
  text("smoothing: " + (int)(smoothing*100)+"\nwindow:"+wlabel[windex]+"\nscale:"+scale,10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
  if(key == 'W' && windex < window.length-1) windex++;
  if(key == 'w' && windex > 0) windex--;
  if(key == 'w' || key == 'W') fft.window(window[windex]);
  if(keyCode == LEFT && scale > 1) scale--;
  if(keyCode == RIGHT) scale++;
}

我不确定我是否按照意图应用了提示。以下是我的输出方式:

fft smooth second attempt

fft smooth second attempt

但是,如果将这个与我所追求的可视化效果进行比较,我认为还没有达到那个水平:
Windows Media Player 中的频谱。

spectrum WMP

在VLC播放器中的频谱 spectrum VLC

我不确定我是否正确应用了对数刻度。我的假设是,使用log10后(暂时忽略平滑),我会得到类似于我想要的绘图。

更新4:

import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
AudioInput  in;
FFT         fft;

float smoothing = 0;
float[] fftReal;
float[] fftImag;
float[] fftSmooth;
float[] fftPrev;
float[] fftCurr;
int specSize;

WindowFunction[] window = {FFT.NONE,FFT.HAMMING,FFT.HANN,FFT.COSINE,FFT.TRIANGULAR,FFT.BARTLETT,FFT.BARTLETTHANN,FFT.LANCZOS,FFT.BLACKMAN,FFT.GAUSS};
String[] wlabel = {"NONE","HAMMING","HANN","COSINE","TRIANGULAR","BARTLETT","BARTLETTHANN","LANCZOS","BLACKMAN","GAUSS"};
int windex = 0;

int scale = 10;

void setup(){
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 512);
  fft = new FFT(in.bufferSize(), in.sampleRate());
  fft.window(window[windex]);
  specSize = fft.specSize();
  fftSmooth = new float[specSize];
  fftPrev   = new float[specSize];
  fftCurr   = new float[specSize];
  size(specSize, specSize/2);
  colorMode(HSB,specSize,100,100);
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);
  fftReal = fft.getSpectrumReal();
  fftImag = fft.getSpectrumImaginary();
  for(int i = 0; i < specSize; i++)
  {    
    float maxVal = Math.max(Math.abs(fftReal[i]), Math.abs(fftImag[i]));
    if (maxVal != 0.0f) { // prevent divide-by-zero
        // Normalize
        fftReal[i] = fftReal[i] / maxVal;
        fftImag[i] = fftImag[i] / maxVal;
    }

    fftCurr[i] = -scale * (float)Math.log10(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);
    fftSmooth[i] = smoothing * fftSmooth[i] + ((1 - smoothing) * fftCurr[i]);

    stroke(i,100,100);
    line( i, height/2, i, height/2 - (mousePressed ? fftSmooth[i] : fftCurr[i]));

  }
  text("smoothing: " + (int)(smoothing*100)+"\nwindow:"+wlabel[windex]+"\nscale:"+scale,10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
  if(key == 'W' && windex < window.length-1) windex++;
  if(key == 'w' && windex > 0) windex--;
  if(key == 'w' || key == 'W') fft.window(window[windex]);
  if(keyCode == LEFT && scale > 1) scale--;
  if(keyCode == RIGHT) scale++;
}

产生这个:

FFT mod

在绘图循环中,由于缩放率为负,我从中心开始绘制。如果我将值放大,结果开始看起来随机。
更新6
import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
AudioInput  in;
FFT         fft;

float smoothing = 0;
float[] fftReal;
float[] fftImag;
float[] fftSmooth;
float[] fftPrev;
float[] fftCurr;
int specSize;

WindowFunction[] window = {FFT.NONE,FFT.HAMMING,FFT.HANN,FFT.COSINE,FFT.TRIANGULAR,FFT.BARTLETT,FFT.BARTLETTHANN,FFT.LANCZOS,FFT.BLACKMAN,FFT.GAUSS};
String[] wlabel = {"NONE","HAMMING","HANN","COSINE","TRIANGULAR","BARTLETT","BARTLETTHANN","LANCZOS","BLACKMAN","GAUSS"};
int windex = 0;

int scale = 10;

void setup(){
  minim = new Minim(this);
  in = minim.getLineIn(Minim.STEREO, 512);
  fft = new FFT(in.bufferSize(), in.sampleRate());
  fft.window(window[windex]);
  specSize = fft.specSize();
  fftSmooth = new float[specSize];
  fftPrev   = new float[specSize];
  fftCurr   = new float[specSize];
  size(specSize, specSize/2);
  colorMode(HSB,specSize,100,100);
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);
  fftReal = fft.getSpectrumReal();
  fftImag = fft.getSpectrumImaginary();
  for(int i = 0; i < specSize; i++)
  {
    fftCurr[i] = scale * (float)Math.log10(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);
    fftSmooth[i] = smoothing * fftSmooth[i] + ((1 - smoothing) * fftCurr[i]);

    stroke(i,100,100);
    line( i, height/2, i, height/2 - (mousePressed ? fftSmooth[i] : fftCurr[i]));

  }
  text("smoothing: " + (int)(smoothing*100)+"\nwindow:"+wlabel[windex]+"\nscale:"+scale,10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
  if(key == 'W' && windex < window.length-1) windex++;
  if(key == 'w' && windex > 0) windex--;
  if(key == 'w' || key == 'W') fft.window(window[windex]);
  if(keyCode == LEFT && scale > 1) scale--;
  if(keyCode == RIGHT) scale++;
  if(key == 's') saveFrame("fftmod.png");
}

这感觉如此接近:

FFT mod2

这个版本看起来比之前的好多了,但是光谱低/左侧的一些值看起来有点不对劲,而且形状似乎变化非常快。(平滑值图绘制了零点)
2个回答

29

我不太清楚您需要进行什么样的平滑处理,但我会尽力提供一些可能有帮助的信息。

为显示缩放FFT结果

通常,在进行傅里叶变换并且您想要显示它的图表时,您需要(如您所提到的)对其进行对数比例缩放。这是因为值的幅度将在巨大范围内变化 - 许多数量级 - 并将其压缩到图表上可观察的小空间中,会使主峰超过其他信息。

为了实际进行此缩放,我们将值转换为分贝。重要的是要注意,分贝是一个比例而不是一个单位 - 它代表两个数字之间的比率:通常是测量值和某个参考值。分贝的一般公式为

x_dB = 10 * log10((x ^ 2) / (ref ^ 2))

其中log10是以10为底的对数,^是幂运算符,x_ref是您选择的参考值。由于从音频文件FFT的值通常没有任何有意义的单位,因此在此应用程序中,x_ref通常被选择为简单的1。另外,由于x是复数,您需要取绝对值。因此,公式将是

x_dB = 10 * log10(abs(x) ^ 2)

这里有一个小的(数值和速度)优化可以实现,因为你正在对一个平方根的结果进行平方:

x_dB = 10 * log10(real(x) ^ 2 + imag(x) ^ 2)

感知加权

在测量声压和功率级时,通常会进行频域测量的缩放:选择给定应用程序的特定测量类型(我不会在这里介绍类型),并根据此测量类型录制声音。结果被FFT处理,然后在每个频率上乘以给定的加权,具体取决于结果将用于什么以及记录了什么类型的声音。常用的有两种加权:A和C。C通常仅用于极高振幅的声音。

请注意,如果您只想显示漂亮的图形,则不需要此类加权:它用于确保全世界的人都可以进行遵循相同标准的测量(和测量设备)。如果您决定包含此内容,则必须在转换为分贝(或加上加权的分贝值-在数学上等效)之前执行乘法。

A加权的信息在维基百科上。

窗函数

窗函数主要用于减少吉布斯现象的影响。我们永远无法完全摆脱它,但窗函数确实有所帮助。不幸的是,它还具有其他影响:尖峰会变宽,"旁瓣"会被引入;峰值锐度和旁瓣高度之间总是存在妥协。我不会在这里详细介绍所有细节,除非您特别要求;在这本免费在线书籍中,有一个相当冗长的窗函数解释。

时间域内单个频率区间的平滑处理

至于使每个频率区间中的线条缓慢衰减,这里有一个简单的想法可能会奏效:在每个频率区间中,应用一个简单的指数移动平均。假设您的FFT结果存储在X[k]中,其中k是频率索引。让您的显示值为Y[k],使其满足以下条件:

Y[k] = alpha * Y_(t-1)[k] + (1 - alpha) * X[k]

其中0 < alpha < 1是平滑系数,Y_(t-1)[k]是在上一个时间步骤t-1Y[k]的值。这实际上是一个简单的低通IIR(无限冲激响应)滤波器,希望能基本达到您想要的效果(也许需要微调)。alpha越接近零,新观测(输入X[k])影响结果的速度就越快。它越接近于一,结果衰减得越慢,但输入也会更慢地影响结果,因此可能显得“迟钝”。您可能希望在其周围添加条件,以便如果新值高于当前值,则立即采用新值。

请注意,再次强调,在转换为分贝之前应执行此操作。

(编辑)仔细查看您发布的代码后,这似乎是您尝试复制的示例中使用的方法。您的初始尝试很接近,但请注意,第一项是平滑系数乘以最后一个显示值,而不是当前输入。

(编辑2)您的第三次更新再次接近,但以下行中的公式存在轻微的误译。

fftSmooth[i] = smoothing * fftPrev[i] + ((1 - smoothing) * fftCurr[i]);

fftPrev[i] = fftCurr[i];//

你需要的是平滑处理后的FFT系数值,而不是之前的值。(注意这意味着你实际上不需要另一个数组来存储之前的值)

fftSmooth[i] = smoothing * fftSmooth[i] + ((1 - smoothing) * fftCurr[i]);

如果smoothing == 0,这行代码除了将结果乘以一个标量外,几乎没有其他影响。

绝对值计算中的归一化

仔细观察它们计算绝对值的方式,可以看出它们进行了归一化处理,使得两个复数中的最大值变为1,另一个复数相应地缩放。这意味着您始终会获得介于0和1之间的绝对值,这可能是他们用来替代分贝转换的方法。实际上,这并不完全符合他们的abs函数文档的建议,有点让人恼火...但无论如何,如果您复制这个方法,它将保证您的值始终处于合理范围内。
要在您的代码中简单实现此操作,您可以做如下操作:
float maxVal = Math.max(Math.abs(fftReal[i]), Math.abs(fftImag[i]));
if (maxVal != 0.0f) { // prevent divide-by-zero
    // Normalize
    fftReal[i] = fftReal[i] / maxVal;
    fftImag[i] = fftImag[i] / maxVal;
}

fftCurr[i] = scale * (float)Math.log10(fftReal[i]*fftReal[i] + fftImag[i]*fftImag[i]);
// ...

综合应用:示例代码

在使用 Processing 2.1 进行一段时间的尝试后,我有一个解决方案,相信您会很满意:

import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
//AudioInput  in;
AudioPlayer in;
FFT         fft;

float smoothing = 0.60;
final boolean useDB = true;
final int minBandwidthPerOctave = 200;
final int bandsPerOctave = 10;
float[] fftSmooth;
int avgSize;

float minVal = 0.0;
float maxVal = 0.0;
boolean firstMinDone = false;

void setup(){
  minim = new Minim(this);
  //in = minim.getLineIn(Minim.STEREO, 512);
  in = minim.loadFile("C:\\path\\to\\some\\audio\\file.ext", 2048);

  in.loop();

  fft = new FFT(in.bufferSize(), in.sampleRate());

  // Use logarithmically-spaced averaging
  fft.logAverages(minBandwidthPerOctave, bandsPerOctave);

  avgSize = fft.avgSize();
  fftSmooth = new float[avgSize];

  int myWidth = 500;
  int myHeight = 250;
  size(myWidth, myHeight);
  colorMode(HSB,avgSize,100,100);

}

float dB(float x) {
  if (x == 0) {
    return 0;
  }
  else {
    return 10 * (float)Math.log10(x);
  }
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);

  final int weight = width / avgSize;
  final float maxHeight = (height / 2) * 0.75;

  for (int i = 0; i < avgSize; i++) {
    // Get spectrum value (using dB conversion or not, as desired)
    float fftCurr;
    if (useDB) {
      fftCurr = dB(fft.getAvg(i));
    }
    else {
      fftCurr = fft.getAvg(i);
    }

    // Smooth using exponential moving average
    fftSmooth[i] = (smoothing) * fftSmooth[i] + ((1 - smoothing) * fftCurr);

    // Find max and min values ever displayed across whole spectrum
    if (fftSmooth[i] > maxVal) {
      maxVal = fftSmooth[i];
    }
    if (!firstMinDone || (fftSmooth[i] < minVal)) {
      minVal = fftSmooth[i];
    }
  }

  // Calculate the total range of smoothed spectrum; this will be used to scale all values to range 0...1
  final float range = maxVal - minVal;
  final float scaleFactor = range + 0.00001; // avoid div. by zero

  for(int i = 0; i < avgSize; i++)
  {
    stroke(i,100,100);
    strokeWeight(weight);

    // Y-coord of display line; fftSmooth is scaled to range 0...1; this is then multiplied by maxHeight
    // to make it within display port range
    float fftSmoothDisplay = maxHeight * ((fftSmooth[i] - minVal) / scaleFactor);

    // X-coord of display line
    float x = i * weight;

    line(x, height / 2, x, height / 2 - fftSmoothDisplay);
  }
  text("smoothing: " + (int)(smoothing*100)+"\n",10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
}

上述方法使用了略微不同的方法 - 对比总频谱大小稍小的一系列频段进行平均处理,这将产生一个更接近于WMP的结果而非你原来的结果。
以下是示例结果:Result example 增强功能:现在带有A加权
我有更新版本的代码,它在每个频段中应用了A加权(尽管仅在dB模式开启时才能使用,因为我拥有的表格是以dB为单位的 :). 打开A加权可以得到更接近WMP的结果,关闭可以得到更接近VLC的结果。
此外,还对显示方式进行了一些微调:它现在位于显示器中心,并且只会显示最大带中心频率。
以下是代码 - 感受一下吧!
import ddf.minim.analysis.*;
import ddf.minim.*;

Minim       minim;
//AudioInput  in;
AudioPlayer in;
FFT         fft;

float smoothing = 0.73;
final boolean useDB = true;
final boolean useAWeighting = true; // only used in dB mode, because the table I found was in dB 
final boolean resetBoundsAtEachStep = false;
final float maxViewportUsage = 0.85;
final int minBandwidthPerOctave = 200;
final int bandsPerOctave = 10;
final float maxCentreFrequency = 18000;
float[] fftSmooth;
int avgSize;

float minVal = 0.0;
float maxVal = 0.0;
boolean firstMinDone = false;

final float[] aWeightFrequency = { 
  10, 12.5, 16, 20, 
  25, 31.5, 40, 50, 
  63, 80, 100, 125, 
  160, 200, 250, 315, 
  400, 500, 630, 800, 
  1000, 1250, 1600, 2000, 
  2500, 3150, 4000, 5000,
  6300, 8000, 10000, 12500, 
  16000, 20000 
};

final float[] aWeightDecibels = {
  -70.4, -63.4, -56.7, -50.5, 
  -44.7, -39.4, -34.6, -30.2, 
  -26.2, -22.5, -19.1, -16.1, 
  -13.4, -10.9, -8.6, -6.6, 
  -4.8, -3.2, -1.9, -0.8, 
  0.0, 0.6, 1.0, 1.2, 
  1.3, 1.2, 1.0, 0.5, 
  -0.1, -1.1, -2.5, -4.3, 
  -6.6, -9.3 
};

float[] aWeightDBAtBandCentreFreqs;

void setup(){
  minim = new Minim(this);
  //in = minim.getLineIn(Minim.STEREO, 512);
  in = minim.loadFile("D:\\Music\\Arthur Brown\\The Crazy World Of Arthur Brown\\1-09 Fire.mp3", 2048);

  in.loop();

  fft = new FFT(in.bufferSize(), in.sampleRate());

  // Use logarithmically-spaced averaging
  fft.logAverages(minBandwidthPerOctave, bandsPerOctave);
  aWeightDBAtBandCentreFreqs = calculateAWeightingDBForFFTAverages(fft);

  avgSize = fft.avgSize();
  // Only use freqs up to maxCentreFrequency - ones above this may have
  // values too small that will skew our range calculation for all time
  while (fft.getAverageCenterFrequency(avgSize-1) > maxCentreFrequency) {
    avgSize--;
  }

  fftSmooth = new float[avgSize];

  int myWidth = 500;
  int myHeight = 250;
  size(myWidth, myHeight);
  colorMode(HSB,avgSize,100,100);

}

float[] calculateAWeightingDBForFFTAverages(FFT fft) {
  float[] result = new float[fft.avgSize()];
  for (int i = 0; i < result.length; i++) {
    result[i] = calculateAWeightingDBAtFrequency(fft.getAverageCenterFrequency(i));
  }
  return result;    
}

float calculateAWeightingDBAtFrequency(float frequency) {
  return linterp(aWeightFrequency, aWeightDecibels, frequency);    
}

float dB(float x) {
  if (x == 0) {
    return 0;
  }
  else {
    return 10 * (float)Math.log10(x);
  }
}

float linterp(float[] x, float[] y, float xx) {
  assert(x.length > 1);
  assert(x.length == y.length);

  float result = 0.0;
  boolean found = false;

  if (x[0] > xx) {
    result = y[0];
    found = true;
  }

  if (!found) {
    for (int i = 1; i < x.length; i++) {
      if (x[i] > xx) {
        result = y[i-1] + ((xx - x[i-1]) / (x[i] - x[i-1])) * (y[i] - y[i-1]);
        found = true;
        break;
      }
    }
  }

  if (!found) {
    result = y[y.length-1];
  }

  return result;     
}

void draw(){
  background(0);
  stroke(255);

  fft.forward( in.mix);

  final int weight = width / avgSize;
  final float maxHeight = height * maxViewportUsage;
  final float xOffset = weight / 2 + (width - avgSize * weight) / 2;

  if (resetBoundsAtEachStep) {
    minVal = 0.0;
    maxVal = 0.0;
    firstMinDone = false;
  }

  for (int i = 0; i < avgSize; i++) {
    // Get spectrum value (using dB conversion or not, as desired)
    float fftCurr;
    if (useDB) {
      fftCurr = dB(fft.getAvg(i));
      if (useAWeighting) {
        fftCurr += aWeightDBAtBandCentreFreqs[i];
      }
    }
    else {
      fftCurr = fft.getAvg(i);
    }

    // Smooth using exponential moving average
    fftSmooth[i] = (smoothing) * fftSmooth[i] + ((1 - smoothing) * fftCurr);

    // Find max and min values ever displayed across whole spectrum
    if (fftSmooth[i] > maxVal) {
      maxVal = fftSmooth[i];
    }
    if (!firstMinDone || (fftSmooth[i] < minVal)) {
      minVal = fftSmooth[i];
    }
  }

  // Calculate the total range of smoothed spectrum; this will be used to scale all values to range 0...1
  final float range = maxVal - minVal;
  final float scaleFactor = range + 0.00001; // avoid div. by zero

  for(int i = 0; i < avgSize; i++)
  {
    stroke(i,100,100);
    strokeWeight(weight);

    // Y-coord of display line; fftSmooth is scaled to range 0...1; this is then multiplied by maxHeight
    // to make it within display port range
    float fftSmoothDisplay = maxHeight * ((fftSmooth[i] - minVal) / scaleFactor);
    // Artificially impose a minimum of zero (this is mathematically bogus, but whatever)
    fftSmoothDisplay = max(0.0, fftSmoothDisplay);

    // X-coord of display line
    float x = xOffset + i * weight;

    line(x, height, x, height - fftSmoothDisplay);
  }
  text("smoothing: " + (int)(smoothing*100)+"\n",10,10);
}
void keyPressed(){
  float inc = 0.01;
  if(keyCode == UP && smoothing < 1-inc) smoothing += inc;
  if(keyCode == DOWN && smoothing > inc) smoothing -= inc;
}

result 2


非常感谢您详尽的解释。不幸的是,我仍需要一些信息来实现您的笔记,尽管如您在我的帖子中看到的更新3,这已经非常接近了。缩放FFT结果以显示:我同意并尝试应用上述方法。感知加权:这非常有趣。我会尝试实现它,尽管我现在不确定是否需要它。窗函数:在我的简单情况下,我将坚持使用GAUSS或HANN窗口。您推荐的书看起来非常有用... - George Profenza
我会再试一次。如果C++代码也进行了归一化,那看起来很方便。在第一次尝试中,我可能错过了(abs(real),abs(imag)),当获取最大值时。我只需要根据声音在屏幕上绘制图形:使用对数意味着更高的频率将更加明显,并且规范化数据意味着我可以轻松缩放图形。我感觉接近了,但仍有些事情我不理解,还没有达到目标。 - George Profenza
再次感谢您的建议,我感觉我还没有完全理解您的意思。我已经测试了代码,现在图表看起来很混乱(如果您查看我在问题中发布的最新图片)。只是为了确认一下,在您的计算机上是否得到相同的图表,或者我的计算机出现了一些奇怪的情况?谢谢。 - George Profenza
我还没有在我的电脑上尝试过这个 - 没有下载库。然而,我意识到 - 是不必要的... 进行归一化和dB转换两者都做可能也是不必要的。如果删除 - 不起作用,请尝试只进行归一化而不进行dB转换(从您提供的样本来看,这似乎是您正在尝试复制的样本所采用的方法)。 - wakjah
我已尝试了你的两个建议(注释掉标准化),就像你在更新5中看到的。感谢你的提示,这看起来非常接近。这可能有点多,但是你介意在你的机器上测试吗?只需将更新5中的代码粘贴到Processing中即可。 - George Profenza
显示剩余2条评论

0

在你的循环中:你需要添加一个对于lg规模的对数计算:

stroke(i,100,50);
line( i, height, i, height - fftSmooth[i]*8 );
stroke(i,100,100);
line( i, height, i, height - band*8 );

应该改为:

int l = map(log(map(i ,0 ,specSize,0,100),0,2,0,width).  // an estimation, may have to calibrate
stroke(i,100,50);
line( l, height, l, height - fftSmooth[i]*8 );
stroke(i,100,100);
line( l, height, l, height - band*8 );

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