快速等效于STK中引用的DSP的sin()

时间:2012-02-14 23:52:28

标签: c++ algorithm math signal-processing

我正在使用Perry Cook的Synthesis Toolkit(STK)来生成锯齿和方波。 STK包括这个基于BLIT的锯齿振荡器:

inline STKFloat BlitSaw::tick( void ) {
  StkFloat tmp, denominator = sin( phase_ );
  if ( fabs(denominator) <= std::numeric_limits<StkFloat>::epsilon() )
      tmp = a_;
  else {
      tmp = sin( m_ * phase_ );
      tmp /= p_ * denominator;
  }

  tmp += state_ - C2_;
  state_ = tmp * 0.995;

  phase_ += rate_;
  if ( phase_ >= PI ) 
     phase_ -= PI;

  lastFrame_[0] = tmp;
     return lastFrame_[0];
}

方波振荡器大致相似。在顶部,有这样的评论:

// A fully  optimized version of this code would replace the two sin 
// calls with a pair of fast sin oscillators, for which stable fast 
// two-multiply algorithms are well known.

我不知道从哪里开始寻找这些“快速双倍算法”,我很欣赏一些指针。我可以使用查找表,但我很想知道这些'快速振荡器'是什么。我也可以使用缩写的泰勒系列,但这可能超过两倍。尽管我确实发现了这种近似值,但搜索并没有发生任何变化:

#define AD_SIN(n) (n*(2.f- fabs(n))) 

绘制出来表明它并不是-1到1范围之外的近似值,所以当phase_在-pi到pi的范围内时我不认为我可以使用它:

Plot of Sine vs. approximation

这里,正弦是蓝线,紫线是近似值。

对我的代码进行分析后发现,对sin()的调用是最耗时的调用,所以我真的很想优化这篇文章。

由于

编辑感谢您提供详细而多样的答案。我将在周末探索这些并接受一个。

编辑2 匿名关闭选民请在评论中解释他们的投票吗?谢谢。

7 个答案:

答案 0 :(得分:6)

基本上,正弦振荡器是一个(或多个)变量,随着每个DSP步骤而变化,而不是从头开始重新计算。

最简单的是基于以下三角形标识:(其中d是常量,因此cos(d)sin(d)

sin(x+d) = sin(x) cos(d) + cos(x) sin(d)
cos(x+d) = cos(x) cos(d) - sin(x) sin(d)

然而,这需要两个变量(一个用于sin,一个用于cos)和4个乘法来更新。然而,这仍然比在每一步计算完整正弦要快得多。

Oli Charlesworth的解决方案基于这个通用方程式的解决方案

A_{n+1} = a A_{n} + A_{n-1}

在寻找A_n = k e^(i theta n)形式的解决方案时,会给出theta的等式。

e^(i theta (n+1) ) = a e^(i theta n ) + b e^(i theta (n-1) )

简化为

e^(i theta) - e^(-i theta ) = a
2 cos(theta) = a

给予

A_{n+1} = 2 cos(theta) A_{n} + A_{n-1}

无论您使用哪种方法,您都需要为每个频率使用一个或两个振荡器,或者使用另一个触发标识来获得更高或更低的频率。

答案 1 :(得分:3)

您需要这么准确吗?

这个函数f(x)= 0.398x *(3.1076- | x |),对于-pi和pi之间的x做了相当不错的工作。

修改

更好的近似值是f(x)= 0.38981969947653056 *(pi- | x |),它在-pi和pi之间保持绝对误差为0.038158444604或更小。

最小二乘最小化将产生稍微不同的函数。

答案 2 :(得分:2)

只用两次乘法就不可能产生一次性的sin调用(好吧,无论如何都不是有用的近似值)。但是有可能生成一个低复杂度的振荡器,即每个值都是根据前面的值计算的。

例如,考虑以下差分方程将给出正弦曲线:

y[n] = 2*cos(phi)*y[n-1] - y[n-2]

(其中cos(phi)是常数)

答案 3 :(得分:1)

(来自VST BLT代码的原作者)。

事实上,我正在将VST BLT振荡器移植到C#,因此我正在使用谷歌搜索好的振荡器。这就是我想出来的。翻译成C ++很简单。请参阅最后关于累积的四舍五入错误的说明。

public class FastOscillator
{

    private double b1;
    private double y1, y2;

    private double fScale;

    public void Initialize(int sampleRate)
    {
        fScale = AudioMath.TwoPi / sampleRate;
    }
     // frequency in Hz. phase in radians.
    public void Start(float frequency, double phase)
    {
        double w = frequency * fScale;
        b1 = 2.0 * Math.Cos(w);
        y1 = Math.Sin(phase - w);
        y2 = Math.Sin(phase - w * 2);
    }
    public double Tick()
    {
        double y0 = b1 * y1 - y2;
        y2 = y1;
        y1 = y0;
        return y0;
    }
}

请注意,此特定振荡器实现会随时间漂移,因此需要定期重新初始化。在该特定实现中,sin波的幅度随时间衰减。 STK代码中的原始注释表明双倍振荡器。事实上,随着时间的推移,双倍振荡器相当稳定。但回想起来,保持罪(相位)和sin(m *相)振荡器紧密同步的需要可能意味着它们无论如何都必须重新同步。相位和m *相位之间的舍入误差意味着即使振荡器稳定,它们也会最终漂移,从而在BLT函数的零点附近产生大量峰值的重大风险。也可以使用单倍振荡器。

这些特定的振荡器应该每30到100个周期(左右)重新初始化一次。我的C#实现是基于帧的(即它在void Tick(int count,float [] result)方法中计算float []结果数组。振荡器在每个Tick调用结束时重新同步。这样的事情:

   void Tick(int count, float[] result)   
   {
        for (int i = 0; i < count; ++i)
        {
                ...
            result[i] = bltResult;
        }
        // re-initialize the oscillators to avoid accumulated drift.
        this.phase = (this.phase + this.dPhase*count) % AudioMath.TwoPi;
        this.sinOsc.Initialize(frequency,this.phase);
        this.mSinOsc.Initialize(frequency*m,this.phase*m);
   }

可能缺少STK代码。您可能想要对此进行调查。提供给STK的原始代码就是这样做的。 Gary Scavone调整了一下代码,我认为优化失败了。我知道STK实现受到DC漂移的影响,正确实现后几乎可以完全消除。

即使在扫描振荡器的频率时,也存在一种特殊的黑客攻击,可以防止振荡器的直流漂移。诀窍是振荡器应该以dPhase / 2的初始相位调整开始。正好恰好以零直流漂移启动振荡器,而不必为每个BLT振荡器中的各种积分器计算出正确的初始状态。

奇怪的是,如果每当振荡器的频率改变时重新调整调整,那么当扫描振荡器的频率时,这也可以防止输出的疯狂DC漂移。每当频率改变时,从前一个相位值中减去dPhase / 2,重新计算新频率的dPhase,然后添加dPhase / 2.I而不是怀疑这可以被正式证明;但我没能这样做。我所知道的只是它的工作原理。

对于块实现,实际上应该按如下方式初始化振荡器,而不是在当前的this.phase值中进行相位调整。

        this.sinOsc.Initialize(frequency,phase+dPhase*0.5);
        this.mSinOsc.Initialize(frequency*m,(phase+dPhase*0.5)*m);

答案 4 :(得分:1)

你可能想看看这里:

http://devmaster.net/forums/topic/4648-fast-and-accurate-sinecosine/

有一些示例代码只使用乘法,加法和abs()函数来计算sin / cos的非常好的应用。非常快。评论也很好。

它基本上归结为:

float sine(float x)
{
    const float B = 4/pi;
    const float C = -4/(pi*pi);
    const float P = 0.225;    

    float y = B * x + C * x * abs(x);

    return P * (y * abs(y) - y) + y;
}

适用于-PI到PI的范围

答案 5 :(得分:0)

如果可以的话,你应该考虑基于记忆的技巧。基本上存储束值的sin(x)和cos(x)值。为了计算sin(y),找到a和b,其中存在预先计算的值,使得&lt; = y&lt; = b。现在使用sin(a),sin(b),cos(a),cos(b),y-a和y-b近似计算sin(y)。

答案 6 :(得分:0)

从正弦或余弦函数获得周期性采样结果的一般想法是使用触发递归或初始化(几乎)稳定的IIR滤波器(最终可能是几乎相同的计算)。 DSP文献中有许多这些,具有不同的准确性和稳定性。请仔细选择。