【问题标题】:How to filter FFT data (for audio visualisation)?如何过滤 FFT 数据(用于音频可视化)?
【发布时间】:2023-03-16 05:50:01
【问题描述】:

我在看这个Web Audio API demothis nice book 的一部分

如果你看演示,fft 峰值平稳下降。我正在尝试使用 minim 库对 Java 模式下的处理做同样的事情。我已经查看了如何使用doFFTAnalysis() 方法中的网络音频 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;
}

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

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

我认为 Web Audio API 可能会考虑到中频和高频需要缩放以更接近我们的感知,但我不知道如何解决这个问题。

我试图阅读更多有关 RealtimeAnalyser 类如何为 WebAudioAPI 执行此操作的信息,但似乎 FFTFrame classdoFFT 方法可能会进行对数缩放。我还没有弄清楚 doFFT 是如何工作的。

如何使用对数刻度缩放原始 FFT 图以解释感知? 我的目标是做一个看起来不错的可视化,我猜我需要:

  • 平滑值,否则元素将动画快速/抽搐
  • 缩放 FFT bin/band 以获得更好的中/高频数据
  • 映射将 FFT 值处理为视觉元素(找到最大值/边界)

关于我如何实现这一点的任何提示?

更新 2

我猜这部分会完成我在 Web Audio API 中所追求的平滑和缩放: // 归一化,以便将 0dBfs 处的输入正弦波注册为 0dBfs(撤消 FFT 缩放因子)。 常量双幅值Scale = 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);
}

似乎缩放是通过取复数值的绝对值来完成的。 This post 指向同一个方向。我已经尝试使用 Minim 和各种窗口函数使用复数的 abs,但它看起来仍然不像我的目标(Web Audio API demo):

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++;
}

我不确定我是否按预期应用了提示。这是我的输出的样子:

但如果我将此与我的目标可视化进行比较,我认为我还没有到达那里:

windows 媒体播放器中的频谱

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++;
}

产生这个:

在绘制循环中,我从中心绘制,因为比例现在是负数。 如果我将值放大,结果开始看起来是随机的。

UPDATE6

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");
}

感觉如此接近:

这看起来比以前的版本好多了,但是光谱左下角的一些值看起来有点不对劲,而且形状似乎变化得很快。 (平滑值绘制零)

【问题讨论】:

    标签: java audio fft visualization processing


    【解决方案1】:

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

    缩放 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 加权的信息是on wikipedia

    窗口化

    执行窗口化主要是为了减少Gibbs phenomenon 的影响。我们永远无法完全摆脱它,但窗口确实有帮助。不幸的是,它还有其他影响:尖峰变宽并引入了“旁瓣”;峰值锐度和旁瓣高度之间总是存在折衷。除非您特别要求,否则我不会在这里详细介绍所有细节;关于开窗in this free online book有相当长的解释。

    单个频率区间的时域平滑

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

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

    其中0 &lt; alpha &lt; 1 是您的平滑因子,Y_(t-1)[k]Y[k]最后一个时间步的值 (t-1)。这实际上是一个简单的低通 IIR(无限脉冲响应)滤波器,希望基本上能满足您的需求(也许稍作调整)。 alpha 越接近于零,新的观察结果(输入 X[k])就会越快地影响结果。越接近1,结果衰减得越慢,但是输入对结果的影响也会越慢,因此可能会显得“迟钝”。如果新值高于当前值,您可能需要在其周围添加条件以立即获取新值。

    请再次注意,这应在转换为分贝之前执行。

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

    (编辑 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]);
    // ...
    

    把它们放在一起:一些代码

    在处理 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;
    }
    

    上面使用了一种稍微不同的方法 - 在一系列比总频谱大小更小的 bin 中平均频谱 - 产生的结果比原始的更接近 WMP。

    增强:现在使用 A 加权

    我有一个更新版本的代码,它在每个频带中应用 A 加权(尽管只有在 dB 模式打开时,因为我的表是以 dB 为单位的 :)。打开 A 加权以获得更接近 WMP 的结果,或关闭 A 加权以获得更接近 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;
    }
    

    【讨论】:

    • 非常感谢您的详尽解释。不幸的是,我仍然需要更多信息来实现您的笔记,尽管这感觉非常接近,正如您在我的帖子中的更新 3 中看到的那样。 为显示缩放 FFT 结果:我同意并且我已尝试应用上述内容。 感知权重:这很有趣。我将尝试实现这一点,尽管我不确定我现在是否需要它。 Windowing:在我的简单情况下,我将坚持使用 GAUSS 或 HANN 窗口。不过你推荐的这本书看起来很有用……
    • 单个频率区间的时域平滑:看起来像 Web Audio API 人们所做的,我正在尝试应用它,但我不确定我是否做对了:fftCurr 是当前 fft 值(10 * log10(real(x) ^ 2 + imag(x) ^ 2)),fftPrev 保存来自 fftCurr 的先前值。如果我使用之前显示的值(fftSmooth[i] 而不是 fftCurr[i]),我会得到零。 绝对值计算中的标准化:我的输出是基本的,不需要精确的数据库比例,如果我想轻松缩放图形,标准化是有意义的。我没有完全理解 abs 的工作原理......
    • ...将再试一次。如果 c++ 代码也进行了规范化,那看起来很方便。在我第一次尝试获得最大值时,我可能会丢失(abs(real),abs(imag))。我只需要根据声音在屏幕上绘制图形:使用对数意味着更高的频率会很明显,标准化数据意味着我可以轻松地缩放图形。我感觉很亲近,但仍有一些我不明白的东西,而且还没有
    • 再次感谢您的意见,我觉得我没有完全理解您的意思。但是,我已经测试了代码,现在情节看起来很混乱(如果您查看我在问题中发布的最新图片)。只是要仔细检查一下,您的计算机上是否有相同的情节,或者我的计算机上发生了一些奇怪的事情?谢谢
    • 我没有在我的电脑上试过这个 - 没有图书馆 dl'd。但是,我意识到 - 是不必要的...... 可能也没有必要进行 归一化 dB 转换。如果删除 - 不起作用,请尝试仅进行归一化且不进行 dB 转换(从您提供的样本来看,它们在您尝试重现的样本中是这样做的)
    【解决方案2】:

    在您的循环中:您需要为 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 );
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-13
      相关资源
      最近更新 更多