如何在一段时间内准确计算正弦波

时间:2016-06-08 05:22:12

标签: c algorithm audio sine sound-synthesis

用例是为数字合成生成正弦波,因此,我们需要计算sin(d t)的所有值,其中:

t 是一个整数,表示样本编号。这是可变的。对于CD质量的一小时声音,范围从0到158,760,000。

d 是double,表示角度的增量。这是不变的。范围是:大于0,小于pi。

目标是使用传统的 int double 数据类型实现高准确度。表现并不重要。

天真的实施是:

double next()
{
    t++;
    return sin( ((double) t) * (d) );
}

但是,问题是当 t 增加时,准确性会降低,因为大数字提供给“罪”功能。

改进版本如下:

double next()
{
    d_sum += d;
    if (d_sum >= (M_PI*2)) d_sum -= (M_PI*2);

    return sin(d_sum);
}

在这里,我确保提供从0到2 * pi到“罪”功能的数字。

但是,现在问题是当 d 很小时,会有很多小的添加,每次都会降低准确性。

这里的问题是如何提高准确性。

附录1

“准确度降低,因为大数字提供给”罪“功能”:

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

#define TEST      (300000006.7846112)
#define TEST_MOD  (0.0463259891528704262050786960234519968548937998410258872449766)
#define SIN_TEST  (0.0463094209176730795999323058165987662490610492247070175523420)

int main()
{
    double a = sin(TEST);
    double b = sin(TEST_MOD);

    printf("a=%0.20f \n" , a);
    printf("diff=%0.20f \n" , a - SIN_TEST);
    printf("b=%0.20f \n" , b);
    printf("diff=%0.20f \n" , b - SIN_TEST);
    return 0;
}

输出:

a=0.04630944601888796475
diff=0.00000002510121488442
b=0.04630942091767308033
diff=0.00000000000000000000

5 个答案:

答案 0 :(得分:2)

您可以尝试使用的方法是快速傅立叶变换的一些实现。三角函数的值基于先前的值和Δ来计算。

Sin(A + d) = Sin(A) * Cos(d) + Cos(A) * Sin(d)

这里我们必须存储和更新余弦值并存储常数(对于给定的delta)因子Cos(d)和Sin(d)。

现在关于精度:小d的余弦(d)非常接近1,因此存在精度损失的风险(数字中只有少数有效数字,如0.99999987)。为了解决这个问题,我们可以将常数因子存储为

dc = Cos(d) - 1 =  - 2 * Sin(d/2)^2
ds = Sin(d) 

使用其他公式更新当前值
(此处sa = Sin(A)表示当前值,ca = Cos(A)表示当前值)

ts = sa //remember last values
tc = ca
sa = sa * dc + ca * ds
ca = ca * dc - ts * ds
sa = sa + ts
ca = ca + tc

P.S。一些FFT实施周期性地(每K步)通过trig更新saca值。功能,以避免错误累积。

示例结果。计算双打。

d=0.000125
800000000 iterations
finish angle 100000 radians

                             cos               sin
described method       -0.99936080743598  0.03574879796994 
Cos,Sin(100000)         -0.99936080743821  0.03574879797202
windows Calc           -0.9993608074382124518911354141448 
                            0.03574879797201650931647050069581           

答案 1 :(得分:1)

sin(x)= sin(x + 2N∙π) ,所以问题可以归结为准确找到一个等于大的小数数字 x 模2π。

例如,-1.61059759≅256 mod 2π,您可以计算sin(-1.61059759)的精度高于sin(256)

所以让我们选择一些整数来处理,256。首先找到等于256的幂的小数,模2π:

// to be calculated once for a given frequency
// approximate hard-coded numbers for d = 1 below:
double modB = -1.61059759;  // = 256  mod (2π / d)
double modC =  2.37724612;  // = 256² mod (2π / d)
double modD = -0.89396887;  // = 256³ mod (2π / d)

然后将索引拆分为基数256的数字:

// split into a base 256 representation
int a = i         & 0xff;
int b = (i >> 8)  & 0xff;
int c = (i >> 16) & 0xff;
int d = (i >> 24) & 0xff;

您现在可以找到一个小得多的x,它等于i模2π/ d

// use our smaller constants instead of the powers of 256
double x = a + modB * b + modC * c + modD * d;
double the_answer = sin(d * x);

对于 d 的不同值,您必须计算不同的值modBmodCmodD等于256的幂,但模数(2π/ d )。您可以使用高精度库进行这些计算。

答案 2 :(得分:1)

将周期缩放到2 ^ 64,并使用整数运算进行乘法运算:

// constants:
double uint64Max = pow(2.0, 64.0);
double sinFactor = 2 * M_PI / (uint64Max);

// scale the period of the waveform up to 2^64
uint64_t multiplier = (uint64_t) floor(0.5 + uint64Max * d / (2.0 * M_PI));

// multiplication with index (implicitly modulo 2^64)
uint64_t x = i * multiplier;

// scale 2^64 down to 2π
double value = sin((double)x * sinFactor);

只要您的期间不是数十亿的样本,multiplier的精确度就足够了。

答案 3 :(得分:0)

以下代码将sin()函数的输入保持在一个小范围内,同时稍微减少了由于可能非常微小的相位增量引起的小的加法或减法的数量。

double next() {
    t0 += 1.0;
    d_sum = t0 * d;
    if ( d_sum > 2.0 * M_PI ) {
        t0 -= (( 2.0 * M_PI ) / d );
    }
    return (sin(d_sum));
}

答案 4 :(得分:0)

对于超精确度,OP有两个问题:

  1. d乘以n并保持比double更高的精度。这在第一部分回答。

  2. 执行mod期间。简单的解决方案是使用度数然后mod 360,这很容易做到。要做2*π大角度是很棘手的,因为它需要2*π的值,其精度比(double) 2.0 * M_PI

  3. 大约多27位

    使用2 double代表d

    我们假设32位intbinary64 double。因此double具有53位的准确度。

    0 <= n <= 158,760,000约为2 27.2 。由于double可以连续且准确地处理53位无符号整数,因此53-28 - &gt; 25,任何只有25位有效位的double都可以乘以n,但仍然是准确的。

    d细分为2个double s dmsb,dlsb,25位最高位数和28位数。

    int exp;
    double dmsb = frexp(d, &exp);  // exact result
    dmsb = floor(dmsb * POW2_25);  // exact result
    dmsb /= POW2_25;               // exact result
    dmsb *= pow(2, exp);           // exact result
    double dlsb = d - dmsb;        // exact result
    

    然后dmsb*n的每次乘法(或连续加法)都是精确的。 (这是重要的部分。)dlsb*n只会在最少的位中出错。

    double next()
    {
        d_sum_msb += dmsb;  // exact
        d_sum_lsb += dlsb;
        double angle = fmod(d_sum_msb, M_PI*2);  // exact
        angle += fmod(d_sum_lsb, M_PI*2);
        return sin(angle);
    }
    

    注意:fmod(x,y)结果应该精确到达x,y

    #include <stdio.h>
    #include <math.h>
    
    #define AS_n 158760000
    double AS_d = 300000006.7846112 / AS_n;
    double AS_d_sum_msb = 0.0;
    double AS_d_sum_lsb = 0.0;
    double AS_dmsb = 0.0;
    double AS_dlsb = 0.0;
    
    double next() {
      AS_d_sum_msb += AS_dmsb;  // exact
      AS_d_sum_lsb += AS_dlsb;
      double angle = fmod(AS_d_sum_msb, M_PI * 2);  // exact
      angle += fmod(AS_d_sum_lsb, M_PI * 2);
      return sin(angle);
    }
    
    #define POW2_25 (1U << 25)
    
    int main(void) {
      int exp;
      AS_dmsb = frexp(AS_d, &exp);         // exact result
      AS_dmsb = floor(AS_dmsb * POW2_25);  // exact result
      AS_dmsb /= POW2_25;                  // exact result
      AS_dmsb *= pow(2, exp);              // exact result
      AS_dlsb = AS_d - AS_dmsb;            // exact result
    
      double y;
      for (long i = 0; i < AS_n; i++)
        y = next();
      printf("%.20f\n", y);
    }
    

    输出

    0.04630942695385031893
    

    使用度

    建议将度数用作360度是精确时段,M_PI*2弧度是近似值。 C不能完全代表π

    如果OP仍然想要使用弧度,为了进一步了解执行π的mod,请参阅Good to the Last Bit