DSP Trick: Simultaneous Parabolic Approximation of Sin and Cos

From: Olli Niemitalo (oniemita@mail.student.oulu.fi)
Subject: Trick: Simultaneous parabolic approximation of sin and cos
Newsgroups: comp.dsp
Date: 2001-06-30 05:15:09 PST

Name: Simultaneous parabolic approximation of sin and cos

Category: Algorithmic

Application: When you need both sin and cos at once, and you need 'em fast, and using multiplications and parabolic approximation is OK, try this. Possible applications are audio panning, mixing fader, maybe even realtime filter coefficient calculations and 2D/3D rotation transformations.

Advantages: Cheap, only one or two multiplies per approximation pair. No discontinuities.

Introduction:

A quarter of a sinusoid cycle is approximated with a parabola. The remaining quarters are horizontally and vertically flipped versions of the approximated quarter. Instead of creating two different approximations, the same polynomial is used for both sin and cos. The positive crossing point of sin(angle) and cos(angle), at angle=pi/4, is made to correspond to x=0 in the polynomial. This shifts the center of a quarter at x=0 and turns switching between quarters (and between sin and cos) into x and y sign flips.

The Trick:

The range that is approximated is:

x=-1/2 .. +1/2, angle=0 .. pi/2


Angle and x are related by:

x=angle 2 / pi - 1/2

The approximation: ("~=" stands for "approximately equals")

sin(angle) ~=sinapprox(x)=(2 - 4 c) x^2 + c + x
cos(angle) ~=cosapprox(x)=(2 - 4 c) x^2 + c - x

As you see, the only difference is in the last sign, so you can calculate the part left from that first and then add or sub x to get sin or cos. For different angle ranges, you have to flip signs of the whole equation or the sign of the x term. See the example source code.

c is a constant that can be fine-tuned for different applications. With c yet undefined, the polynomials hit 0 and 1 in the right places. Here are some differently optimized (and approximated) values for c:

A) c ~=0.7035

B) c ~=0.71256755058 

C) c=sqrt(2)/2 ~=0.70710678118654752440

D) c=3/4=0.75 (one mul choice!)

To compare the errors produced by different choices of c, we look at the maximums and minimums of the following error measures, for different c:

error=sinapprox(x) - sin(angle)

Min: A) -0.0213 B) -0.0150 C) -0.0187 D) 0
Max: A) +0.0212 B) +0.0271 C) +0.0235 D) +0.0560

error=sqrt(sinapprox(x)^2 - cosapprox(x)^2) - 1

Min: A) -0.0261 B) -0.0155 C) -0.0214 D) 0
Max: A)  0      B) +0.0155 C) 0 D) +0.1250

The different c were optimized as follows:

A) was optimized for minimum maximum absolute difference to sin.

B) was optimized for minimum maximum absolute difference of sqrt(sinapprox(x)^2+cosapprox(x)^2) to 1. This is important for example in a rotation transformation, where you want the dimensions to stretch as little as possible from the original. Note, though, that C) gives the lowest total magnitude "wobbling" range.

C) was optimized similarly to B but never lets sqrt(sinapprox(x)^2+cosapprox(x)^2) exceed 1. This is useful if you calculate filter coefficients and don't want unstable poles. Also, sinapprox(0) and cosapprox(0) give sqrt(2)/2, which is the correct
value.
D) was optimized for continuous differential, which reduces high harmonics. This is good if you are creating sine and cosine signals rather than doing "random access" to the function. Also, it eliminates the other multiplication, making the algo one-mul and low bit-depth:

x=-1/2 .. +1/2

sinapprox(x)=3/4 - x^2 + x
cosapprox(x)=3/4 - x^2 - x

Example program:
 

--------------------------------------------------------------------------
#include <stdio.h>

#include <math.h>
// This program gives approximated float sin and cos for a fixed point
// phase input. Rather than messing with radians, phase is normalized so
// that phase = 0 corresponds to angle = 0 and phase = 2^BITSPERCYCLE 
// corresponds to angle = 2pi. Angles outside this range also give 
// correct results. Calculations are optimized to do with minimum 
// operations and to avoid the unary minus operator.
// Put here your choices of bit-depth, phase, and the constant C
#define BITSPERCYCLE 10
#define INPUTPHASE   666
#define C            0.70710678118654752440f
// Some useful constants (PI and <math.h> are not part of algo)
#define BITSPERQUARTER (BITSPERCYCLE-2)
#define PI (float)3.1415926535897932384626433832795
int main(void) {
    int phasein = INPUTPHASE; // Phase input
    float sinout, cosout;
    
    // Modulo phase into quarter, convert to float 0..1
    float modphase = (phasein & (1<<BITSPERQUARTER)-1)
        *1.0f/(1<<BITSPERQUARTER);
    // Extract quarter bits 
    int quarter = phasein & (3<<BITSPERQUARTER);
    // Recognize quarter
    if (!quarter) { 
        // First quarter, angle = 0 .. pi/2
        float x = modphase - 0.5f;      // 1 sub
        float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
        sinout = temp + x;              // 1 add
        cosout = temp - x;              // 1 sub
    } else if (quarter == 1<<BITSPERQUARTER) {
        // Second quarter, angle = pi/2 .. pi
        float x = 0.5f - modphase;      // 1 sub
        float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
        sinout = x + temp;              // 1 add
        cosout = x - temp;              // 1 sub
    } else if (quarter == 2<<BITSPERQUARTER) {
        // Third quarter, angle = pi .. 1.5pi
        float x = modphase - 0.5f;      // 1 sub
        float temp = (4*C - 2)*x*x - C; // 2 mul, 1 sub
        sinout = temp - x;              // 1 sub
        cosout = temp + x;              // 1 add
    } else {
        // Fourth quarter, angle = 1.5pi..2pi
        float x = modphase - 0.5f;      // 1 sub
        float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
        sinout = x - temp;              // 1 sub
        cosout = x + temp;              // 1 add
    }
    // Print some shit, not part of the algo
    float angle = 2*PI*phasein/(1<<BITSPERCYCLE);
    printf("phase: %d/%d, angle: %f\n\n", phasein, 1<<BITSPERCYCLE,
        angle);
    printf("sinapprox: %.6f, cosapprox: %.6f\n", sinout, cosout);
    printf("      sin: %.6f,       cos: %.6f\n", sin(angle),
        cos(angle));
    return 0;
}