Rephasor

Rephasor

Overview

This algorithm implements a rephasor, an operation that dynamically resynthesizes an input phasor signal (such as phasor) with a scaling value that allows the rate of the phasor to be changed proportionally to the input signal. This phasor also apply automatic correction to prevent clock drift accumulation.

While phasors are typical signals used for table-lookup oscilators (oscf is a phasor-driven oscillator), phasors being used with rephasors are intended to be used as timing signals, similar to that of metro.

What is Drift?

clock drift, or drift, refers to what happens when two independent clocks fall out of synchronization with eachother, despite being set to the same rate. Drift is most often a result of numerical errors accumulated over time.

Rephasors intentionally use truncation to produce phasors with clean resets to zero, rather than using wraparound. This makes for a much cleaner timing signal, but can quickly introduce drift.

Floating point operations can introduce some level of numerical error. It is not yet been formally tested how much this contributes to clock drift.

Clock drift build-up over time in a rephasor is mitigated by "checking in" with the external phasor signal, measure how much error there is, and compensating with "course correction".

Theory

To best understand how the rephasor works, some crude mathematical notation is adopted. This will serve as the basis of the C implementation, though some aspects will be reworked to be more optimized.

A phasor is a periodic rising ramp signal, normalized to be in the range 0 and 1.

A phasor \theta will be defined in terms of an increment value i :

\theta(i) = R(\theta_{n - 1} + i)

The function R is used to reset the phasor back to zero every time it exceeds 1, discarding any numerical values that exceed 1. For this reason, this phasor can be referred to as a truncated phasor.

The truncation method can be defined using the following C-style teritiary notation:

R(x) = (x > 1) \; ? \; 0 : x

NOTE: the sndkit phasor wraps around values exceeding 1. One could call the sndkit phasor a wraparound phasor.

The delta function \Delta will return the difference the difference between the previous sample and current sample of a given phasor. This returns the increment value. When a phasor resets, the last valid value is returned.

This function can be mathematically defined as a recursive function, though the C implementation is much more straight forward.

\Delta(\theta) = \cases{\theta_n - \theta_{n - 1},&if
$\theta_n \ge \theta_{n - 1}$;\cr
\Delta(\theta_{n - 1}),&otherwise.\cr}

A so-called rephasor can be defined as \theta_r :

\theta_r = \theta(sc\Delta(\theta_e))

This works by resynthesizes an external phasor signal \theta_e by obtaining the increment value via the delta function, and using that to generate a new phasor. This new increment value can also scaled using a scaling value s . A scaling value of 2 makes the phasor 2 times faster, a scaling value of 0.5 makes the phasor 2 times slower. Finally, there is a correction coefficient c that is used for course correction, and is the key component that makes the rephasor stay synchronized with the external phasor.

Correction works by measuring the amount of error that the rephasor has. To do this, a comparison phasor signal \theta_c is produced by putting the main rephasor signal into another rephasor that inverts the scaling value.

\theta_e = \theta(s^{-1}c\Delta(\theta_r))

This should return a reconstructed version of the external phasor. It is important to note a rephasor adds a unit-sample delay. Since two rephasors are used, a delay of 2 samples is introduced.

Using DSP z-notation to denote delay, the correction coefficient can be defined as ratio between the external signal and the correction signal:

c = {{\theta_e z^{-2}} \over {\theta_c z^{-2}}}

The correction coefficient tends to be a value close to 1. When the value is greater than 1, it tells the rephasor to speed up. When it is less than one, it tells the rephasor to slow down.

This mathematically defines everything required for the C implementation.

Tangled Files

Tangles to rephasor.c and rephasor.h.

SK_REPHASOR_PRIV exposes structs.

<<rephasor.h>>=
#ifndef SK_REPHASOR_H
#define SK_REPHASOR_H
#ifndef SKFLT
#define SKFLT float
#endif
<<typedefs>>
#ifdef SK_REPHASOR_PRIV
<<structs>>
#endif
<<funcdefs>>
#endif

<<rephasor.c>>=
#include <math.h>
#define SK_REPHASOR_PRIV
#include "rephasor.h"
<<funcs>>

Struct

Definition

State is managed in a struct called sk_rephasor.

<<typedefs>>=
typedef struct sk_rephasor sk_rephasor;

The struct will adopt constants based on the mathematical symbols defined above. It may be helpful to review that section.

pr, pc, and pe are cached phasor signals, representing , , and , respectively. pc and pe are arrays of 2 samples because of the 2-sample delay needed to compute the correction coefficient.

The c variable is the correction coefficient .

The variables s and si implement the scaling variables and the inverse s^{-1} , which is stored to shave off a 1/x division operation.

Rephasors need to cache increment values, which are stored in ir and ic, for the main rephasor and the comparison phasor .

<<structs>>=
struct sk_rephasor {
    SKFLT pr;
    SKFLT pc[2];
    SKFLT pe[2];
    SKFLT c;
    SKFLT s;
    SKFLT si;

    SKFLT ir;
    SKFLT ic;
};

Initialization

Initialized with sk_rephasor_init.

<<funcdefs>>=
void sk_rephasor_init(sk_rephasor *rp);

Generally speaking, most things are zeroed out. The scalar values and correction coefficient are used as scalars, set to be 1 in order to be netural.

<<funcs>>=
void sk_rephasor_init(sk_rephasor *rp)
{
    rp->pr = 0;
    rp->pc[0] = 0;
    rp->pc[1] = 0;
    rp->pe[0] = 0;
    rp->pe[1] = 0;
    rp->c = 1.0;
    rp->s = 1.0;
    rp->si = 1.0;

    rp->ir = 0.0;
    rp->ic = 0.0;
}

Setting the scaling value

Called sk_rephasor_scale.

<<funcdefs>>=
void sk_rephasor_scale(sk_rephasor *rp, SKFLT scale);

If this is a new scaling value, set the s and the siparameters.

<<funcs>>=
void sk_rephasor_scale(sk_rephasor *rp, SKFLT scale)
{
    if (scale != rp->s) {
        rp->s = scale;
        rp->si = 1.0 / scale;
    }
}

Compute

Main Compute Function

A sample of audio is computed with sk_rephasor_tick, where ext is an external phasor signal.

<<funcdefs>>=
SKFLT sk_rephasor_tick(sk_rephasor *rp, SKFLT ext);

The code below is an implementation based on the mathematical definition defined previously. Some code comments have been made in an attempt to connect the points.

<<funcs>>=
/* implementation of a truncated phasor */

static SKFLT phasor(SKFLT phs, SKFLT inc)
{
    phs += inc;

    if (phs > 1.0) return 0;

    return phs;
}

SKFLT sk_rephasor_tick(sk_rephasor *rp, SKFLT ext)
{
    SKFLT pr, pc;
    SKFLT out;


    /* delta function of \theta_e */
    if (ext > rp->pe[0]) {
        rp->ir = ext - rp->pe[0];
    }

    /* compute main rephasor \theta_r */
    pr = phasor(rp->pr, rp->s * rp->ir * rp->c);

    /* delta function of \theta_r */
    if (pr > rp->pr) {
        rp->ic = pr - rp->pr;
    }

    /* compute rephasor \theta_c */
    pc = phasor(rp->pc[0], rp->si * rp->ic);

    /* compute correction coefficient */
    if (rp->pc[1] != 0) {
        rp->c = rp->pe[1] / rp->pc[1];
    }

    <<bounds_checking>>

    out = pr;

    /* update state */

    rp->pr = pr;

    rp->pc[1] = rp->pc[0];
    rp->pc[0] = pc;

    rp->pe[1] = rp->pe[0];
    rp->pe[0] = ext;

    return out;
}

Bounds Checking

As it turns out, the correction mechanism is rather brittle in practice. For example, it was very easy to break the rephasor with a steady input signal at around 73 BPM and a scaling value of 0.25 (4x slower).


To circumvent this, the rephasor will reject very high or very low values computed. Anything out of these bounds is probably wrong.

<<bounds_checking>>=
if (rp->c > 2.0 || rp->c < 0.5) rp->c = 1.0;

No-sync computation

sk_rephasor_tick_nosync will compute a rephasor without any of the corrections done for synchronization. This is included as a way to compare implementations. By itself, it's probably not all that useful.

<<funcdefs>>=
SKFLT sk_rephasor_tick_nosync(sk_rephasor *rp, SKFLT ext);

NOTE: flip-flopping between sk_rephasor_tick_nosyncand sk_rephasor_tick in a single instance of rephasor is probably not a good idea.

<<funcs>>=
SKFLT sk_rephasor_tick_nosync(sk_rephasor *rp, SKFLT ext)
{
    SKFLT out;

    if (ext > rp->pe[0]) {
        rp->ir = ext - rp->pe[0];
    }

    rp->pr = phasor(rp->pr, rp->s * rp->ir);
    rp->pe[0] = ext;

    out = rp->pr;

    return out;
}