不要将正弦计算为时间的函数,而是维护一个正弦/余弦对并通过复数乘法将其推进。这不需要任何三角函数或查找表;只有四次乘法和偶尔的重新归一化:
static const double a = 2 * M_PI * 280 * 30e-6;
static const double dx = cos(a);
static const double dy = sin(a);
double x = 1, y = 0; // complex x + iy
int counter = 0;
void control_loop() {
double xx = dx*x - dy*y;
double yy = dx*y + dy*x;
x = xx, y = yy;
// renormalize once in a while, based on
// https://www.gamedev.net/forums/topic.asp?topic_id=278849
if((counter++ & 0xff) == 0) {
double d = 1 - (x*x + y*y - 1)/2;
x *= d, y *= d;
}
double sine = y; // this is your sine
}
如果需要,可以通过重新计算 dx、dy 来调整频率。
此外,这里的所有操作都可以很容易地在定点完成。
理性
由于@user3386109 points out below (+1),280 * 30e-6 = 21 / 2500 是一个有理数,因此正弦应该在 2500 个样本后循环正好。我们可以通过每 2500 次迭代(或 5000 或 10000 等)重置我们的生成器 (x=1,y=0) 来将此方法与他们的方法结合使用。这将消除重整化的需要,并消除任何长期相位误差。
(从技术上讲,任何浮点数都是二元有理数。但是280 * 30e-6 没有二进制的精确表示。但是,通过按照建议重置生成器,我们将得到预期的精确周期性正弦。)
说明
有些人要求在 cmets 中解释为什么这样做。最简单的解释是使用angle sum trigonometric identities:
xx = cos((n+1)*a) = cos(n*a)*cos(a) - sin(n*a)*sin(a) = x*dx - y*dy
yy = sin((n+1)*a) = sin(n*a)*cos(a) + cos(n*a)*sin(a) = y*dx + x*dy
正确性通过归纳得出。
根据Euler's formula,如果我们将这些正弦/余弦对视为复数,这本质上就是De Moivre's formula。
一种更有洞察力的方法可能是从几何角度来看它。 exp(ia) 的复数乘法相当于a 弧度的旋转。因此,通过反复乘以dx + idy = exp(ia),我们沿着单位圆递增地旋转我们的起点1 + 0i。再次根据欧拉公式,y 坐标是当前相位的正弦。
标准化
虽然阶段随着每次迭代继续前进,但由于舍入误差,x + iy 的幅度(又名范数)会偏离1。然而,我们有兴趣生成幅度正弦1,因此我们需要对x + iy 进行归一化以补偿数值漂移。当然,直接的方法是将其除以自己的规范:
double d = 1/sqrt(x*x + y*y);
x *= d, y *= d;
这需要计算平方根的倒数。即使我们每 X 次迭代只标准化一次,避免它仍然很酷。幸运的是|x + iy| 已经接近1,因此我们只需要稍微修正一下就可以了。围绕1(一阶泰勒近似)扩展d 的表达式,我们得到代码中的公式:
d = 1 - (x*x + y*y - 1)/2
TODO:要完全理解这个近似值的有效性,需要证明它可以比累积误差更快地补偿舍入误差,从而确定需要应用它的频率。