On Thu, Feb 21, 2019 at 11:16 PM robert bristow-johnson
<r...@audioimagination.com> wrote:

> But Martin, if you let this thing run for days on end, would not eventually 
> the amplitude of the output change a bit?

Short answer: yes, sometimes significantly for audio purposes when
using 32-bit float state variables, but not significantly when using
64-bit doubles.

Even as poles lie exactly on the unit circle, limited precision in the
state variables leads to rounding errors. Because of the recursive
nature of the algorithm, rounding errors accumulate in manner of a
(pseudo)random walk. Almost always a past state will be encountered
again, and the system enters a limit cycle, locking the error into a
range of error values close to what was accumulated thus far. With
32-bit floating point state variables, sometimes significant numerical
error is accumulated before a limit cycle is entered, depending on
pseudo-luck with the sinusoid frequency. With 64-bit floating point
state variables the problem seems to be almost never significant even
though entering a limit cycle becomes a rare occasion.

The error accumulation and limit cycles can be visualized by plotting
the magnitude of the quadrature pair output of Martin's algorithm
(red), and the same for a Goertzel algorithm based quadrature
oscillator (blue) for comparison, as function of output sample number:

  https://imgur.com/a/cE5R50d

For this plot, 32-bit float variables were used, and rounding mode was
set to round-to-nearest, and angular frequency arbitrarily to (float)
sqrt(2.0) radians to get visible and somewhat lengthy limit cycles.
For the Goertzel algorithm, about 4 periods of a limit cycle, and for
Martin's algorithm, about 2 periods of a limit cycle are visible, for
the first one million output samples.

Based on testing with random frequencies and single (32-bit) and
double (64-bit) precision floating point numbers, compared to
Goertzel, Martin's algorithm tends to have faster error accumulation
(due to a longer cascade of rounded operations per iteration), giving
a larger error, measured as absolute log magnitude, for about 62 % of
random frequencies, both for floats and doubles (tested over 10^4
replications, measured at 10^4 samples generated).

At one million samples generated, the root mean square magnitude error
was roughly 0.0002 for Goertzel and 0.002 for Martin's algorithm using
single precision state variables, and roughly 7*10^-13 for Goertzel
and 3*10^-12 for Martin's algorithm using double precision floating
point state variables (tested over 1000 replications).

C++ test code below.

-olli

// Compile with: g++ -std=c++0x -O3 -ffloat-store -fno-unsafe-math-optimizations
#define _USE_MATH_DEFINES
typedef float FLOAT_TYPE; // float or double
const bool printStats = true; // Print statistics
const bool printData = false; // Print (last) data
#include <math.h>
#include <stdio.h>
#include <fenv.h>
#include <random>
#include <chrono>

main() {
  long int numReps = 1000;
  long int numSamples = 1000;
  long int decimate = 1; // Decimate results by this ratio (useful if
huge numReps)
  unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
  std::mt19937 rng(seed);
  std::uniform_real_distribution<long double> uniform(0.0, 1.0);
  int numDecimated = numSamples/decimate + 1;
  long double *magGoertzel = new long double[numDecimated];
  long double *magMartin = new long double[numDecimated];
  long double *sseGoertzel = new long double[numDecimated];
  long double *sseMartin = new long double[numDecimated];
  for (long int count = 0; count < numDecimated; count++) {
    sseGoertzel[count] = 0;
    sseMartin[count] = 0;
  }
  fesetround(FE_TONEAREST); // FE_TONEAREST, FE_UPWARD, FE_DOWNWARD,
or FE_TOWARDZERO
  long int numGoertzelWorse = 0;
  long int numMartinWorse = 0;
  for (long int rep = 0; rep < numReps; rep++) {
    FLOAT_TYPE w = M_PI*uniform(rng);
    // Goertzel
    FLOAT_TYPE coef = 2*cos(w);
    FLOAT_TYPE outHistory1 = 0, outHistory2 = 0;
    FLOAT_TYPE input = 1; // First input value
    FLOAT_TYPE cosw = cos(w);
    FLOAT_TYPE sinw = sin(w);
    // Martin
    FLOAT_TYPE k1 = tan(w/2);
    FLOAT_TYPE k2 = 2*k1/(1 + k1*k1);
    FLOAT_TYPE u = 1;
    FLOAT_TYPE v = 0;
    numDecimated = 0;
    for (long int count = 0; count < numSamples; count++) {
      // Goertzel
      FLOAT_TYPE out = input + coef*outHistory1 - outHistory2;
      outHistory2 = outHistory1;
      outHistory1 = out;
      input = 0;
      FLOAT_TYPE re = outHistory1 - outHistory2*cosw;
      FLOAT_TYPE im = outHistory2*sinw;
      // Martin
      FLOAT_TYPE tmp = u - k1*v;
      v = v + k2*tmp;
      u = tmp - k1*v;
      // Results
      if (count % decimate == 0) {
        magGoertzel[numDecimated] = sqrt(re*(long double)re + im*(long
double)im);
        magMartin[numDecimated] = (FLOAT_TYPE)sqrt(u*(long double)u +
v*(long double)v);
        sseGoertzel[numDecimated] += pow(magGoertzel[numDecimated] - 1, 2);
        sseMartin[numDecimated] += pow(magMartin[numDecimated] - 1, 2);
        numDecimated++;
      }
    }
    if (fabs(log(magGoertzel[numDecimated - 1])) >
fabs(log(magMartin[numDecimated - 1]))) {
      numGoertzelWorse++;
    }
    if (fabs(log(magGoertzel[numDecimated - 1])) <
fabs(log(magMartin[numDecimated - 1]))) {
      numMartinWorse++;
    }
  }
  if (printStats) {
    printf("Goertzel worse ratio = %f\n", numGoertzelWorse/(FLOAT_TYPE)numReps);
    printf("Martin's worse ratio = %f\n", numMartinWorse/(FLOAT_TYPE)numReps);
    printf("Goertzel RMSE = %.20Lf\n",
sqrtl(sseGoertzel[numDecimated-1]/numReps));
    printf("Martin's RMSE = %.20Lf\n",
sqrtl(sseMartin[numDecimated-1]/numReps));
  }
  if (printData) {
    for (int count = 0; count < numDecimated; count++) {
      printf("%d,%.20Lf,%.20Lf,%.20Lf,%.20Lf\n", count,
magGoertzel[count]-1, magMartin[count]-1,
sqrtl(sseGoertzel[count]/numReps), sqrtl(sseMartin[count]/numReps));
    }
  }
}
_______________________________________________
dupswapdrop: music-dsp mailing list
music-dsp@music.columbia.edu
https://lists.columbia.edu/mailman/listinfo/music-dsp

Reply via email to