带非对称加窗的FFT吗?


17

常见的非矩形窗口函数似乎都是对称的。是否曾经有人希望在FFT之前使用非对称窗口函数?(假设是否认为FFT孔径一侧的数据比另一侧的数据更重要,或者噪声较小等)。

如果是这样,已经研究了哪种非对称窗函数,与(具有更大损耗的)偏移对称窗相比,它们将如何影响频率响应?


2
通常使用窗口,因为FFT在信号的小块上运行,试图使它在本地看起来像固定信号。因此,没有“侧面”可取,信号在整个过程中都是一致的。
endolith

4
在处理实时数据并且需要担心吞吐量延迟的音频分析算法中,有时可以设计一个非对称窗口,该窗口的有效延迟要小于相同长度的对称窗口。如果此非对称窗口的行为(预先已知)以已知方式影响此音频分析的输出参数,则可以补偿这些参数,并保留减少延迟的优点。
罗伯特·布里斯托

Answers:


9

我将速记窗口用于“窗口功能”。

对于音频,任何产生类似于预振铃或预回声的处理都会听起来像是低比特率的mp3。当瞬变或脉冲的局部能量在时间上向后扩展时,例如通过修改重叠变换(例如重叠修改的离散余弦变换(MDCT))中的频谱数据,就会发生这种情况。在这种处理中,音频通过重叠的分析窗口进行窗口化,变换,在频域中进行处理(例如将数据压缩到较小的比特率),再用合成窗口进行窗口化并加在一起。分析和合成窗口的乘积必须使重叠的窗口加和为一。

传统上,所使用的窗口函数是对称的,并且它们的宽度一直是频率选择性(长窗口)和时域伪影避免(短窗口)之间的折衷。窗口越宽,处理可以更及时地传播信号。最近的解决方案是使用非对称窗口。使用的两个窗口可以互为镜像。分析窗口从峰值快速下降到零,因此不会提前“检测”脉冲,而合成窗口从零快速上升到峰值,因此任何处理的效果都不会在时间上向后扩散。这样的另一个优点是低延迟。非对称窗口可以具有良好的频率选择性,并且可以像音频疗法一样替代音频压缩中大小可变的对称窗口。看到M. Schnell,M. Schmidt,M. Jander,T. Albert,R. Geiger,V. Ruoppila,P. Ekstrand,M. Lutzky,B. Grill,“ MPEG-4增强型低延迟 AAC-高音频的新标准质量通信”,第125 AES公约,美国加利福尼亚州旧金山,预印本7503,2008年10月,以及另一篇会议论文,其中还显示了窗的傅里叶变换的幅度: Schnell,M.等。2007。增强的MPEG-4低延迟AAC –低比特率高质量通信。在第122届AES公约中

使用不对称窗口的重叠分析-处理-合成插图
图1.重叠分析处理合成中使用非对称窗口的图示。分析窗口(蓝色)和合成窗口(黄色橙色)的乘积(黑色虚线)与上一帧的窗口(灰色虚线)求和。使用MDCT时,需要进一步的约束以确保完美的重建。

可以使用离散傅里叶变换(DFT,FFT)代替MDCT,但是在这种情况下会提供冗余频谱数据。与DFT相比,MDCT仅提供一半的光谱数据,如果选择合适的窗口,仍然可以实现完美的重建。

这是我自己的非对称窗口设计(图2),适用于使用DFT进行重叠分析-处理-合成的方法,但不适用于MDCT,因此无法完美地进行重构。该窗口尝试最小化均方时间和频率带宽的乘积(类似于受限的高斯窗口),同时保留一些潜在有用的时域属性:非负,单峰,在“时间零”处具有峰,围绕该峰进行分析和合成窗口是彼此的镜像,函数和一阶导数连续性为零,当窗口函数的平方被解释为未归一化的概率密度函数时,该平均值为零。该窗口是优化使用差异进化

不对称余弦窗
图2.左图:一个非对称分析窗口,适用于重叠的分析-处理-再合成,以及时间反向的对应合成窗口。右:余弦窗口,其延迟与不对称窗口相同

窗户的傅立叶变换
图3.图2的余弦窗口(蓝色)和不对称窗口(橙色)的傅立叶变换的幅度。不对称窗口显示出更好的频率选择性。

这是曲线图和非对称窗口的Octave源代码。绘图代码来自Wikimedia Commons。在Linux上,我建议先安装gnuplotepstoolpstoedittransfig然后librsvg2-bin使用进行查看display

pkg load signal

graphics_toolkit gnuplot
set (0, "defaultaxesfontname", "sans-serif")
set (0, "defaultaxesfontsize", 12) 
set (0, "defaultaxeslinewidth", 1)

function plotWindow (w, wname, wfilename = "", wspecifier = "", wfilespecifier = "")

  M = 32; % Fourier transform size as multiple of window length
  Q = 512; % Number of samples in time domain plot
  P = 40; % Maximum bin index drawn
  dr = 130; % Maximum attenuation (dB) drawn in frequency domain plot

  N = length(w);
  B = N*sum(w.^2)/sum(w)^2 % noise bandwidth (bins)

  k = [0 : 1/Q : 1];
  w2 = interp1 ([0 : 1/(N-1) : 1], w, k);

  if (M/N < Q)
    Q = M/N;
  endif

  figure('position', [1 1 1200 600])
  subplot(1,2,1)
  area(k,w2,'FaceColor', [0 0.4 0.6], 'edgecolor', [0 0 0], 'linewidth', 1)
  if (min(w) >= -0.01)
    ylim([0 1.05])
    set(gca,'YTick', [0 : 0.1 : 1])
  else
    ylim([-1 5])
    set(gca,'YTick', [-1 : 1 : 5])
  endif
  ylabel('amplitude')
  set(gca,'XTick', [0 : 1/8 : 1])
  set(gca,'XTickLabel',[' 0'; ' '; ' '; ' '; ' '; ' '; ' '; ' '; 'N-1'])
  grid('on')
  set(gca,'gridlinestyle','-')
  xlabel('samples')
  if (strcmp (wspecifier, ""))
    title(cstrcat(wname,' window'), 'interpreter', 'none')
  else
    title(cstrcat(wname,' window (', wspecifier, ')'), 'interpreter', 'none')
  endif
  set(gca,'Position',[0.094 0.17 0.38 0.71])

  H = abs(fft([w zeros(1,(M-1)*N)]));
  H = fftshift(H);
  H = H/max(H);
  H = 20*log10(H);
  H = max(-dr,H);
  k = ([1:M*N]-1-M*N/2)/M;
  k2 = [-P : 1/M : P];
  H2 = interp1 (k, H, k2);

  subplot(1,2,2)
  set(gca,'FontSize',28)
  h = stem(k2,H2,'-');
  set(h,'BaseValue',-dr)
  xlim([-P P])
  ylim([-dr 6])
  set(gca,'YTick', [0 : -10 : -dr])
  set(findobj('Type','line'),'Marker','none','Color',[0.8710 0.49 0])
  grid('on')
  set(findobj('Type','gridline'),'Color',[.871 .49 0])
  set(gca,'gridlinestyle','-')
  ylabel('decibels')
  xlabel('bins')
  title('Fourier transform')
  set(gca,'Position',[0.595 0.17 0.385 0.71])

  if (strcmp (wfilename, ""))
    wfilename = wname;
  endif
  if (strcmp (wfilespecifier, ""))
    wfilespecifier = wspecifier;
  endif
  if (strcmp (wfilespecifier, ""))
    savetoname = cstrcat('Window function and frequency response - ', wfilename, '.svg');
  else
    savetoname = cstrcat('Window function and frequency response - ', wfilename, ' (', wfilespecifier, ').svg');
  endif
  print(savetoname, '-dsvg', '-S1200,600')
  close

endfunction

N=2^17; % Window length, B is equal for Triangular and Bartlett from 2^17
k=0:N-1;

w = -cos(2*pi*k/(N-1));
w .*= w > 0;
plotWindow(w, "Cosine")

freqData = [0.66697133904805994131, -0.20556692772918355727, 0.49267389481655493588, -0.25062332863369246594, -0.42388422228212319087, 0.42317609537724842905, -0.03930334287740060856, -0.11936153294075849129, 0.30201210285940127687, -0.15541616804857899536, -0.16208119255594669039, 0.12843871362286504723, -0.04470810646117385351, -0.00521885027256757845, 0.07185811583185619522, -0.02835116723496184862, -0.01393644785822748498, 0.00780746224568363342, -0.00748496824751256583, 0.00119325723511989282, 0.00194602547595042175];
freqData(1) /= 2;
scale = freqData(1) + sum(freqData.*not(mod(1:length(freqData), 2)));
freqData /= scale;
w = freqData(1)*ones(1, N);
for bin = 1:(length(freqData)/2)
  w += freqData(bin*2)*cos(2*pi*bin*((1:N)-1)/N);
  w += freqData(bin*2+1)*sin(2*pi*bin*((1:N)-1)/N);
endfor
w(N/4+1:N/2+1) = 0;
w(N/8+2:N/4) = (1 - w(N/8:-1:2).*w(7*N/8+2:N))./w(7*N/8:-1:6*N/8+2);
w = shift(w, -N/2);
plotWindow(w, "Asymmetrical");

您可能只想使用窗口的第二个样本,因为它的开始和结束都是零。以下C ++代码为您做到了这一点,因此,除了四分之一的窗口在任何地方都是零之外,您不会得到任何零样本。对于分析窗口,这是第一季度,对于综合窗口,这是最后一个季度。分析窗口的后半部分应与合成窗口的前半部分对齐,以计算其乘积。该代码还测试了窗口的均值(作为概率密度函数),并展示了重叠重建的平坦度。

#include <stdio.h>
#include <math.h>

int main() {
  const int windowSize = 400;
  double *analysisWindow = new double[windowSize];
  double *synthesisWindow = new double[windowSize];
  for (int k = 0; k < windowSize/4; k++) {
    analysisWindow[k] = 0;
  }
  for (int k = windowSize/4; k < windowSize*7/8; k++) {
    double x = 2 * M_PI * ((k+0.5)/windowSize - 1.75);
    analysisWindow[k] = 2.57392230162633461887-1.58661480271141974718*cos(x)+3.80257516644523141380*sin(x)
      -1.93437090055110760822*cos(2*x)-3.27163999159752183488*sin(2*x)+3.26617449847621266201*cos(3*x)
      -0.30335261753524439543*sin(3*x)-0.92126091064427817479*cos(4*x)+2.33100177294084742741*sin(4*x)
      -1.19953922321306438725*cos(5*x)-1.25098147932225423062*sin(5*x)+0.99132076607048635886*cos(6*x)
      -0.34506787787355830410*sin(6*x)-0.04028033685700077582*cos(7*x)+0.55461815542612269425*sin(7*x)
      -0.21882110175036428856*cos(8*x)-0.10756484378756643594*sin(8*x)+0.06025986430527170007*cos(9*x)
      -0.05777077835678736534*sin(9*x)+0.00920984524892982936*cos(10*x)+0.01501989089735343216*sin(10*x);
  }
  for (int k = 0; k < windowSize/8; k++) {
    analysisWindow[windowSize-1-k] = (1 - analysisWindow[windowSize*3/4-1-k]*analysisWindow[windowSize*3/4+k])/analysisWindow[windowSize/2+k];
  }
  printf("Analysis window:\n");
  for (int k = 0; k < windowSize; k++) {
    printf("%d\t%.10f\n", k, analysisWindow[k]);
  }
  double accu, accu2;
  for (int k = 0; k < windowSize; k++) {
    accu += k*analysisWindow[k]*analysisWindow[k];
    accu2 += analysisWindow[k]*analysisWindow[k];
  }
  for (int k = 0; k < windowSize; k++) {
    synthesisWindow[k] = analysisWindow[windowSize-1-k];
  }
  printf("\nSynthesis window:\n");
  for (int k = 0; k < windowSize; k++) {
    printf("%d\t%.10f\n", k, synthesisWindow[k]);
  }
  printf("Mean of square of analysis window as probability density function:\n%f", accu/accu2);
  printf("\nProduct of analysis and synthesis windows:\n");
  for (int k = 0; k < windowSize/2; k++) {
    printf("%d\t%.10f\n", k, analysisWindow[windowSize/2+k]*synthesisWindow[k]);
  }
  printf("\nSum of overlapping products of windows:\n");
  for (int k = 0; k < windowSize/4; k++) {
    printf("%d\t%.10f\n", k, analysisWindow[windowSize/2+k]*synthesisWindow[k]+analysisWindow[windowSize/2+k+windowSize/4]*synthesisWindow[k+windowSize/4]);
  }
  delete[] analysisWindow;
  delete[] synthesisWindow;
}

以及与Kiss FFT优化库一起使用的优化成本函数的源代码:

class WinProblem : public Opti::Problem {
private:
  int numParams;
  double *min;
  double *max;
  kiss_fft_scalar *timeData;
  kiss_fft_cpx *freqData;
  int smallSize;
  int bigSize;
  kiss_fftr_cfg smallFFTR;
  kiss_fftr_cfg smallIFFTR;
  kiss_fftr_cfg bigFFTR;
  kiss_fftr_cfg bigIFFTR;

public:
  // numParams must be odd
  WinProblem(int numParams, int smallSize, int bigSize, double* candidate = NULL) : numParams(numParams), smallSize(smallSize), bigSize(bigSize) {
    min = new double[numParams];
    max = new double[numParams];
    if (candidate != NULL) {
      for (int i = 0; i < numParams; i++) {
        min[i] = candidate[i]-fabs(candidate[i])*(1.0/65536);
        max[i] = candidate[i]+fabs(candidate[i])*(1.0/65536);
      }
    } else {
      for (int i = 0; i < numParams; i++) {
        min[i] = -1;
        max[i] = 1;
      }
    }
    timeData = new kiss_fft_scalar[bigSize];
    freqData = new kiss_fft_cpx[bigSize/2+1];
    smallFFTR = kiss_fftr_alloc(smallSize, 0, NULL, NULL);
    smallIFFTR = kiss_fftr_alloc(smallSize, 1, NULL, NULL);
    bigFFTR = kiss_fftr_alloc(bigSize, 0, NULL, NULL);
    bigIFFTR = kiss_fftr_alloc(bigSize, 1, NULL, NULL);
  }

  double *getMin() {
    return min;
  }

  double *getMax() {
    return max;
  }

// ___                                                            __ 1     
// |  \    |       |       |       |       |       |       |     / |       
// |   \   |       |       |       |       |       |       |    /  |       
// |    \_ |       |       |       |       |       |       |   /   |
// |      \|__     |       |       |       |       |       |  /|   |       
// |       |  -----|_______|___    |       |       |       | / |   |       
// |       |       |       |   ----|       |       |       |/  |   |       
// --------------------------------x-----------------------x---|---- 0
// 0      1/8     2/8     3/8     4/8     5/8     6/8     7/8 15/16 
// |-------------------------------|                       |-------|
//            zeroStarts                                   winStarts
//
// f(x) = 0 if 4/8 < x < 7/8
// f(-x)f(x) + f(-x+1/8)f(x-1/8) = 1 if 0 < x < 1/8

  double costFunction(double *params, double compare, int print) {
    double penalty = 0;
    double accu = params[0]/2;
    for (int i = 1; i < numParams; i += 2) {
      accu += params[i];
    }
    if (print) {
      printf("%.20f", params[0]/2/accu);
      for (int i = 1; i < numParams; i += 2) {
        printf("+%.20fcos(%d pi x)", params[i]/accu, (i+1)/2);
        printf("+%.20fsin(%d pi x)", params[i+1]/accu, (i+1)/2);
      }
      printf("\n");
    }
    if (accu != 0) {
      for (int i = 0; i < numParams; i++) {
        params[i] /= accu;
      }
    }
    const int zeroStarts = 4; // Normally 4
    const int winStarts = 2; // Normally 1
    int i = 0;
    int j = 0;
    freqData[j].r = params[i++];
    freqData[j++].i = 0;
    for (; i < numParams;) {
      freqData[j].r = params[i++];
      freqData[j++].i = params[i++];
    }
    for (; j <= smallSize/2;) {
      freqData[j].r = 0;
      freqData[j++].i = 0;
    }
    kiss_fftri(smallIFFTR, freqData, timeData);
    double scale = 1.0/timeData[0];
    double tilt = 0;
    double tilt2 = 0;
    for (int i = 2; i < numParams; i += 2) {
      if ((i/2)%2) {
        tilt2 += (i/2)*params[i]*scale;
      } else {
        tilt2 -= (i/2)*params[i]*scale;
      }
      tilt += (i/2)*params[i]*scale;
    }
    penalty += fabs(tilt);
    penalty += fabs(tilt2);
    double accu2 = 0;
    for (int i = 0; i < smallSize; i++) {
      timeData[i] *= scale;
    }
    penalty += fabs(timeData[zeroStarts*smallSize/8]);
    penalty += fabs(timeData[winStarts*smallSize/16]*timeData[smallSize-winStarts*smallSize/16]-0.5);
    for (int i = 1; i < winStarts*smallSize/16; i++) {
      // Last 16th
      timeData[bigSize-winStarts*smallSize/16+i] = timeData[smallSize-winStarts*smallSize/16+i];
      accu2 += timeData[bigSize-winStarts*smallSize/16+i]*timeData[bigSize-winStarts*smallSize/16+i];
    }
    // f(-1/8+i)*f(1/8-i) + f(i)*f(-i) = 1
    // => f(-1/8+i) = (1 - f(i)*f(-i))/f(1/8-i)   
    // => f(-1/16) = (1 - f(1/16)*f(-1/16))/f(1/16)
    //             = 1/(2 f(1/16))
    for (int i = 1; i < winStarts*smallSize/16; i++) {
      // 2nd last 16th
      timeData[bigSize-winStarts*smallSize/8+i] = (1 - timeData[i]*timeData[bigSize-i])/timeData[winStarts*smallSize/8-i];
      accu2 += timeData[bigSize-winStarts*smallSize/8+i]*timeData[bigSize-winStarts*smallSize/8+i];
    }
    // Between 2nd last and last 16th
    timeData[bigSize-winStarts*smallSize/16] = 1/(2*timeData[winStarts*smallSize/16]);
    accu2 += timeData[bigSize-winStarts*smallSize/16]*timeData[bigSize-winStarts*smallSize/16];
    for (int i = zeroStarts*smallSize/8; i <= bigSize-winStarts*smallSize/8; i++) {
      timeData[i] = 0;
    }
    for (int i = 0; i < zeroStarts*smallSize/8; i++) {
      accu2 += timeData[i]*timeData[i];
    }
    if (print > 1) {
      printf("\n");
      for (int x = 0; x < bigSize; x++) {
        printf("%d,%f\n", x, timeData[x]);
      }
    }
    scale = 1/sqrt(accu2);
    if (print) {
      printf("sqrt(accu2) = %f\n", sqrt(accu2));
    }
    double tSpread = 0;
    timeData[0] *= scale;
    double tMean = 0;
    for (int i = 1; i <= zeroStarts*smallSize/8; i++) {
      timeData[i] *= scale;
      //      tSpread += ((double)i)*((double)i)*(timeData[i]*timeData[i]);
      double x_0 = timeData[i-1]*timeData[i-1];
      double x_1 = timeData[i]*timeData[i];
      tSpread += ((double)i)*((double)i)*(x_0 + x_1)*0.5 - ((double)i)*(2.0/3*x_0 + 1.0/3*x_1) + 0.25*x_0 + 1.0/12*x_1;
      double slope = timeData[i]-timeData[i-1];
      if (slope > 0) {
        penalty += slope+1;
      }
      tMean += x_1*i;
      if (timeData[i] < 0) {
        penalty -= timeData[i];
      }
    }
    double x_0 = timeData[0]*timeData[0];
    for (int i = 1; i <= winStarts*smallSize/8; i++) {
      timeData[bigSize-i] *= scale;
      double x_1 = timeData[bigSize-i]*timeData[bigSize-i];
      tSpread += ((double)i)*((double)i)*(x_0 + x_1)*0.5 - ((double)i)*(2.0/3*x_0 + 1.0/3*x_1) + 0.25*x_0 + 1.0/12*x_1;
      x_0 = x_1;        
      tMean += x_1*(-i);
    }
    tMean /= smallSize;
    penalty += fabs(tMean);
    if (tMean > 0) {
      penalty += 1;
    }
    tSpread /= ((double)smallSize)*((double)smallSize); 
    if (print) {
      printf("tSpread = %f\n", tSpread);
    }
    kiss_fftr(bigFFTR, timeData, freqData);
    double fSpread = 0;
    x_0 = freqData[0].r*freqData[0].r;
    for (int i = 1; i <= bigSize/2; i++) {
      double x_1 = freqData[i].r*freqData[i].r+freqData[i].i*freqData[i].i;
      fSpread += ((double)i)*((double)i)*(x_0 + x_1)*0.5 - ((double)i)*(2.0/3*x_0 + 1.0/3*x_1) + 0.25*x_0 + 1.0/12*x_1;
      x_0 = x_1;
    }
    if (print > 1) {
      for (int i = 0; i <= bigSize/2; i++) {
        printf("%d,%f,%f\n", i, freqData[i].r, freqData[i].i);
      }
    }
    fSpread /= bigSize; // Includes kiss_fft scaling
    if (print) {
      printf("fSpread = %f\n", fSpread);
      printf("%f,%f,%f\n", tSpread, fSpread, tSpread*fSpread);
    }
    return tSpread*fSpread + penalty;
  }

  double costFunction(double *params, double compare) {
    return costFunction(params, compare, false);
  }

  int getNumDimensions() {
    return numParams;
  }

  ~WinProblem() {
    delete[] min;
    delete[] max;
    delete[] timeData;
    delete[] freqData;
    KISS_FFT_FREE(smallFFTR);
    KISS_FFT_FREE(smallIFFTR);
    KISS_FFT_FREE(bigFFTR);
    KISS_FFT_FREE(bigIFFTR);
  }
};

3

这取决于窗口的上下文。正如传统上开发的那样,开窗用于功率谱密度估计的Blackman-Tukey方法。这是相关图法的一般形式,其中利用了离散时间Wiener-Khinchin定理。回想一下,这通过离散时间傅立叶变换将自相关序列与功率谱密度相关联。

因此,在设计窗口时要牢记几个标准。首先,他们必须在原点获得团结。这是为了保留信号自相关序列中的功率,因为​​rxx [0]可以被认为是采样功率。接下来,窗口应从原点开始逐渐变细。这是出于多种原因。首先,为了成为有效的自相关序列,所有其他滞后必须小于或等于原点。其次,这允许使用较低的滞后权重进行较高的加权(已使用大多数样本进行了高信度计算),并且对较高的滞后项进行了较小的加权或零加权,由于可用的数据样本量减少,方差增加了计算。最终导致主瓣变宽,PSD分辨率随之降低,

最后,如果窗口具有非负光谱,则也非常需要。这是因为使用Blackman-Tukey方法,您可以将最终估计的偏差视为真实功率谱密度与窗谱卷积。如果此窗口频谱具有负区域,则功率谱密度估计中可能具有负区域。这显然是不希望的,因为在这种情况下它几乎没有物理意义。此外,您会注意到Blackman-Tukey方法中没有大小平方运算。这是因为,用实数和偶数自相关序列乘以实数和偶数窗口,离散傅里叶变换也将是实数和偶数。在实践中,您会发现通常被量化的很小的负分量。

由于这些原因,窗口的长度也是奇数,因为所有有效的自相关序列也是如此。现在,在周期图方法的上下文中,仍然可以完成(并且已经完成)窗口化。即,将数据窗口化,然后取窗口数据的大小平方。这不等同于Blackman-Tukey方法。通过一些统计推导,您可以发现它们的平均表现类似,但总体却不一样。例如,在韦尔奇(Welch)或巴特利特(Bartlett)方法中为每个段使用开窗以减小估计的方差是很常见的。因此,从本质上讲,使用这些方法,动机部分相同,但有所不同。在这些方法中,通过划分窗口能量(而不是对窗口延迟进行仔细加权)来对功率进行归一化。

因此,希望这可以将窗口及其来源以及它们为什么对称的内容与背景相关联。如果您对为什么选择非对称窗口感到好奇,请考虑傅立叶变换的对偶属性的含义,以及卷积功率谱密度估计对您的应用意味着什么。干杯。


1

开窗的最初目的是确保(由DFT假定为周期性的)信号与开始时相比在开始时没有急剧的瞬变。代价是,朝着(对称)窗口中心的频率将得到更多加权,并在随后的DFT中表示出来。

有了所有这些背景知识,我可以想象一个人想要使用非对称窗口来强调通过DFT分析的信号中的局部时间特征。但是,如果开窗后信号的端点幅度大致不相同,则在DFT期间可能会以更宽的波瓣宽度为代价。

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.