clkphs

clkphs

Overview

The clkphs algorithm is a utility that takes in a clock signal (a periodic series of single-sample impulses), and produces a phasor (a period linear ramp signal).

There are a few caveats to this particular algorithm that one should be aware of. The conversion works by measuring the distance between ticks in a clock signal, and uses that to estimate the phasor for the next signal. When clkphs first starts, it will need to wait a beat before starting up. In this initial period, the module will return -1. clkphs also works best on clock signals that are mostly steady in tempo.

Tangled Files

This document tangles to a header and C file combo clkphs.c and clkphs.h.

Define SK_CLKPHS_PRIV will expose the header files.

<<clkphs.c>>=
#include <stdio.h>
#define SK_CLKPHS_PRIV
#include "clkphs.h"

<<funcs>>

<<clkphs.h>>=
#ifndef SK_CLKPHS_H
#define SK_CLKPHS_H

#ifndef SKFLT
#define SKFLT float
#endif

<<typedefs>>

#ifdef SK_CLKPHS_PRIV
<<structs>>
#endif

<<funcdefs>>

#endif

Initialization

Clkphs is intialized with sk_clkphs_init. No samplerate is needed because it only needs to work in units of samples.

<<funcdefs>>=
void sk_clkphs_init(sk_clkphs *c);

<<funcs>>=
void sk_clkphs_init(sk_clkphs *c)
{
    <<init>>
    c->correction = 1.0;
}

Struct Components

Typedef And Struct Declaration

State is managed in a struct called sk_clkphs.

<<typedefs>>=
typedef struct sk_clkphs sk_clkphs;

<<structs>>=
struct sk_clkphs {
    <<sk_clkphs>>
};

Counter

The counter variable is used to measure distances between ticks.

<<sk_clkphs>>=
unsigned long counter;

<<init>>=
c->counter = 0;

Increment

The increment variable inc is the amount the phasor will increment by every sample. It is computed based on the previously measured period between two clock ticks.

<<sk_clkphs>>=
SKFLT inc;

<<init>>=
c->inc = 0;

Internal Phase Position

The internal phase is kept track of as a floating point variable called phs.

<<sk_clkphs>>=
SKFLT phs;

<<init>>=
c->phs = 0;

Start Flag

The start flag is used to indicate if clkphs has just started yet. A value of 1 means it has just started.

A phasor can only be synthesized after the first distance between two ticks is measured. Before that point, it will have to wait return a negative value.

<<sk_clkphs>>=
int start;

There can also be a state where the DSP has started and is waiting for the first tick. This is set with a value of -1, which is what it gets initialized to.

<<init>>=
c->start = -1;

Wait Flag

If the clock is slowing down and the phasor doesn't yet know about it, it will need to wait for the next tick. This flag is set with the wait variable.

<<sk_clkphs>>=
int wait;

<<init>>=
c->wait = 0;

Spillover Flag

If the clock is speeding up and the phasor doesn't yet know about it, it will try to spill out over into the next tick's space. When this happens, the spilloverflag is set.

<<sk_clkphs>>=
int spillover;

<<init>>=
c->spillover = 0;

Correction Amount

When spillage happens, some course correction is added to wrap and move things along. This factor is stored in a variable called correction and dynamically adjusted based on how close to finishing the phasor is (closer values will result in less correction).

<<sk_clkphs>>=
SKFLT correction;

In normal circumstances, course correction has a factor of 1x, or no effect.

<<init>>=
c->correction = 1.0;

Computation

Computing a single sample of audio is done with sk_clkphs_tick. It expects an input clock signal clk, and returns a phasor.

<<funcdefs>>=
SKFLT sk_clkphs_tick(sk_clkphs *c, SKFLT clk);

<<funcs>>=
SKFLT sk_clkphs_tick(sk_clkphs *c, SKFLT clk)
{
    SKFLT out;
    SKFLT phs;

    out = 0;

    <<check_for_tick>>
    <<update_counter>>
    <<check_flags>>
    <<set_output>>
    <<phasor_computation>>


    return out;
}

Handling A Tick

At beginning, the algorithm will first check and respond to a tick that happens in the current sample. Depending on internal state, different things can occur.

<<check_for_tick>>=
if (clk != 0) {
    <<if_just_started>>
    <<if_first_period_completed>>
    <<typical_tick_handling>>
}

When clkphs as just started (aka start is -1), it is waiting for the first tick. This will begin the initial count measurement, and change the start flag to be 1.

<<if_just_started>>=
if (c->start == -1) {
    /* start initial count */
    c->start = 1;
    c->counter = 0;
    return -1;
}

The second tick that happens (when start has been already set to be 1) completes the first counter. It is at this point that a phasor signal can be synthesized. The counter at this point will have measured the duration of two ticks in units of samples. The reciprocal of this will yield the phasor increment amount.

<<if_first_period_completed>>=
else if (c->start == 1) {
    /* first counter finished */
    c->start = 0;
    c->phs = 0;
    c->inc = 1.0 / c->counter;
    c->counter = 0;
}

Typical handling of a tick signals the re-initialization of the phasor signal, as well as resetting of the spillover and wait flags.

<<typical_tick_handling>>=
else {
    /* reset phasor and flags */
    c->inc = 1.0 / c->counter;
    c->counter = 0;
    c->correction = 1.0;
    c->wait = 0;

    <<too_much_spillage>>

    <<phasor_wraparound>>
}

It should be noted that if the spillover flag is still set by the time it reaches this point, it indicates that spillage couldn't fully recover in the previous period. When this happens, the algorithm will cut its losses, and reset the phasor entirely.

A hard reset of the phasor caused by too much spillover will result in a missing period, which can cause off-by-one rhythms to occur from things using this as a timing signal. Fortunately, it should be very unlikely that this will ever happen. Only extremly sudden and vast tempo jumps could cause a scenario like this to happen. If this is avoided, it should be non-issue.

<<too_much_spillage>>=
if (c->spillover) {
    /* too much spillage. abandon ship */
    c->spillover = 0;
    c->phs = 0;
}

Like a typical phasor algorithm, the internal phase is wrapped around itself. Both upper and lower bounds are checked, though it is typically assumed to just go above bounds.

<<phasor_wraparound>>=
if (c->phs >= 1.0) {
    c->phs -= 1.0;
} else if (c->phs < 0.0) {
    c->phs += 1.0;
} else {
    <<engage_spillover>>
}

If the internal phasor value is still within bounds, it means it hasn't fully reached the end of the phasor, and will be given some additional time in the next period to complete itself. This is known as spill-over, and the spillover flag will be set to change the algorithm behavior accordingly.

<<engage_spillover>>=
/* too slow! spill-over mode */
c->spillover = 1;

if (c->phs != 0) {
    <<compute_correction>>
} else {
    <<ignore_spillage>>
}

When spillover happens, some correction is factored into the increment signal. This factor is computed as the ideal place it is supposed to be (1.0), divided by the actual phase position. As the actual phase position approaches 1, the amount of correction gets smaller.

<<compute_correction>>=
c->correction = 1.0 / c->phs;

Divisions by exactly zero will cause things to crash, so spillover is ignored entirely when this happens. Other than the phase being explictely reset to be 0 when spillover happens, it is difficult to imagine a real-world scenario where this would happen.

<<ignore_spillage>>=
c->correction = 1.0;
c->spillover = 0;

Phasor Computation

After a tick is processed, actual phasor signal can be computed.

First, the counter updates itself by incrementing by 1.

<<update_counter>>=
c->counter++;

The wait and start flags are then checked. If either are enabled, the algorithm will return. Wait will return a value of 1. Start will return a value of -1.

<<check_flags>>=
if (c->start != 0) return -1;
if (c->wait) return 1;

The output of the phasor signal is the current state of the previous phasor.

<<set_output>>=
out = c->phs;

Phasor computation has 3 steps. First is incrementation, second is threshold check, third is an update.

<<phasor_computation>>=
phs = c->phs;

<<incrementation>>
<<threshold_check>>
<<update_phase>>

The internal phasor value is incremented using the current increment amount, multiplied by the current correction amount.

<<incrementation>>=
phs += c->inc*c->correction;

After it is updated, the phasor value will be checked to see if the phasor has exceeded 1.

<<threshold_check>>=
if (phs >= 1.0) {
    <<spillover_exception>>
    <<tell_it_to_wait>>
}

In a typical phasor algorithm, this is where the wraparound would happen. However, since it being externally synchronized with a clock signal, it is told to wait at 1 until the next tick via setting the wait flag.

<<tell_it_to_wait>>=
else {
    c->wait = 1;
}

The exception to this rule happens when the spillover flag is set, indicating that the phasor spilling over from the previous period has finally finished up, and it is time to work on synthesizing the phasor for the current period.

<<spillover_exception>>=
if (c->spillover) {
    /* now back to our regularly scheduled program */
    c->spillover = 0;
    phs -= 1.0;
}

Finally, the phase is updated in the struct.

<<update_phase>>=
c->phs = phs;