【问题标题】:Frequency Shifter Using FFT使用 FFT 的移频器
【发布时间】:2017-06-16 06:25:17
【问题描述】:

我一直在使用 Rosetta Code 提供的原始 FFT 算法研究频移器。我知道要对样本信号进行频移,可以将 FFT 应用于原始音频,将每个生成的正弦波的频率乘以频移因子(用户定义),然后将正弦波加在一起。当我运行我的算法时,输出质量极低,就好像在算法中没有收集到足够的正弦波,无法在一开始就正确地再现信号。该算法在头文件中的类中实现,并在其他地方(正确)调用。

#include <complex>
#include <valarray>

typedef std::complex<double> Complex;
typedef std::valarray<Complex> CArray;

class FrequencyShifter {
float sampleRate;
public:
    FrequencyShifter() {

    }
    void setSampleRate(float inSampleRate) {
        sampleRate = inSampleRate;
    }
    double abs(double in0) {
        if (in0>=0) return in0;
        else return -in0;
    }
    void fft(CArray& x)
    {
        const size_t N = x.size();
        if (N <= 1) return;

        // divide
        CArray even = x[std::slice(0, N/2, 2)];
        CArray  odd = x[std::slice(1, N/2, 2)];

        // conquer
        fft(even);
        fft(odd);

        // combine
        for (size_t k = 0; k < N/2; ++k)
        {
            Complex t = std::polar(1.0, -2 * PI * k / N) * odd[k];
            x[k    ] = even[k] + t;
            x[k+N/2] = even[k] - t;
        }
    }
    double convertToReal(double im, double re) {
        return sqrt(abs(im*im - re*re));
    }
    void processBlock(float *inBlock, const int inFramesToProcess, float scale) {
        //inFramesToProcess is the amount of samples in inBlock
        Complex *copy = new Complex[inFramesToProcess];
        for (int frame = 0; frame<inFramesToProcess; frame++) {
            copy[frame] = Complex((double)inBlock[frame], 0.0);
        }
        CArray data(copy, inFramesToProcess);
        fft(data);
        const float freqoffsets = sampleRate/inFramesToProcess;
        for (float x = 0; x<data.size()/2; x++) {
            for (float frame = 0; frame<inFramesToProcess; frame++) {
                inBlock[(int)frame] = (float)(convertToReal(data[(int)x].imag(), data[(int)x].real())*sin(freqoffsets*x*frame*scale));
            }
        }
    }
};

我假设问题的一部分是我只包括sampleRate/inFramesToProcess 频率来覆盖正弦波。发送更大的音频文件(因此更大的*inBlocks 和inFramesToProcesss)会使音频的颗粒感减少吗?我将如何在不仅更改参数的值或长度的情况下完成此操作?

【问题讨论】:

  • “没有输出”是什么意思?
  • @1201ProgramAlarm 当我测试*inBlock 的输出时,没有电平(音频电平为 0 或遇到其他错误)。本质上,算法中存在一些我无法检测和修复的错误。
  • convertToReal 正确吗?很简单,如果inFramesToProcess 是 1,data 将有一个没有虚部的复数。 fft 不会对它做任何事情,所以当它被转换回来时,你会尝试取负数的 sqrt。重要的是,如果 x.size() 是奇数,fft 不会对 x 的最后一个元素做任何事情。
  • @1201ProgramAlarm 啊,感谢您的提醒!这两件事我都没有意识到。我会尽快修复它们并更新帖子。这可能是我正在寻找的解决方法。
  • @1201ProgramAlarm 我在试图解决这两个问题时走到了死胡同。如果您能将您的回复充实为答案,我将不胜感激。

标签: c++ audio fft


【解决方案1】:

这里是 processBlock 的更新版本,其中包含实现频移所需的一些调整,我将在下面描述:

void processBlock(float *inBlock, const int inFramesToProcess, float scale) {
    //inFramesToProcess is the amount of samples in inBlock
    Complex *copy = new Complex[inFramesToProcess];
    for (int frame = 0; frame<inFramesToProcess; frame++) {
        copy[frame] = Complex((double)inBlock[frame], 0.0);
    }
    CArray data(copy, inFramesToProcess);
    fft(data);
    const float freqoffsets = 2.0*PI/inFramesToProcess;
    const float normfactor  = 2.0/inFramesToProcess;
    for (int frame = 0; frame<inFramesToProcess; frame++) {
        inBlock[frame] = 0.5*data[0].real();
        for (int x = 1; x<data.size()/2; x++) {
            float arg = freqoffsets*x*frame*scale;
            inBlock[frame] += data[x].real()*cos(arg) - data[x].imag()*sin(arg);
        }
        inBlock[frame] *= normfactor;
    }
}

推导

您从 FFT 获得的频谱是复数值的,可以看作是用正弦波和余弦波表示您的信号。可以使用逆变换来重建时域波形,这将由以下关系给出:

利用频谱对称性,可以表示为:

或等效:

您可能已经注意到索引0N/2 处的术语是频域中纯实系数的特殊情况。为简单起见,假设频谱没有一直到达N/2,您可以放弃N/2 术语,但仍能得到一个合理的近似值。对于其他条款,您将获得可以实现为的贡献

normfactor = 2.0/inFramesToProcess;
normfactor*(data[x].real()*cos(arg) - data[x].imag()*sin(arg))

您当然需要将所有这些贡献添加到最终缓冲区 inBlock[frame] 中,而不是简单地覆盖以前的结果:

inBlock[frame] += normfactor*(data[x].real()*cos(arg) - data[x].imag()*sin(arg));
//             ^^ 

请注意,可以在循环之后对最终结果进行归一化,以减少乘法次数。这样做时,我们必须特别注意索引 0 处的 DC 项(其系数为 1/N 而不是 2/N):

inBlock[frame] = 0.5*data[0].real();
for (int x = 1; x<data.size()/2; x++) {
    float arg = freqoffsets*x*frame*scale;
    inBlock[frame] += data[x].real()*cos(arg) - data[x].imag()*sin(arg);
}
inBlock[frame] *= normfactor;

最后,在生成音调时,argsincos 的相位参数应该是2*pi*k*n/inFramesToProcess 的形式(在应用scale 因子之前),其中n 是时域样本索引,k 是频域索引。最终结果是计算出的频率增量freqoffsets 确实应该是2.0*PI/inFramesToProcess

备注

  • FFT 算法的工作假设是您的底层时域信号是周期性的,与您的块长度周期相同。因此,块之间可能会出现听得见的不连续性。
  • 未来的读者应该知道,这不会使频谱移动一个恒定的量,而是压缩或扩展频谱,因为频率是按乘法因子缩放的。例如,包含 100-200Hz 分量的信号可能会以 0.75 倍压缩到 75-150Hz。注意下限是如何下移 25Hz,而上限是下移 50Hz。

【讨论】:

  • 非常感谢您的回答!在我理解它之前,我将不得不重新阅读它。我已经实现了你的processBlock 版本,音频确实听起来好多了。我也非常感谢您花时间描述推导。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-14
  • 1970-01-01
相关资源
最近更新 更多