HClock

HClock

Foreword

HClock is a synchronized clock generator with humanization.

Before we begin, a review of some terminology.

A trigger signal is a single-sample impulse signal. It's called a trigger signal because it is typically used to control event-like things in other nodes (starting an envelope, playing a sample, etc).

A clock signal is a trigger signal that repeats at a steady rate. Signals like these are used to control the timing of things like sequencers and drum machines.

humanization is the process of adding minor imperfections to an otherwise precisely created thing. In this case, the humanization here refers to adding slight variations in timing.

Clocks typically come from one source, and are used to make sure multiple devices are synchronized together.

Humanization is important because it makes things sound more natural. Without humanization, two scheduled events (say drum sounds) will always occur exactly at the same time, which creates a very brutal sound. Humanization adds temporal flutter to this sound, which smooths things out. Since humanization is randomized, the variation created also makes things sound less irritating.

One of the problems with adding humanization is phase. Error accumulation can build up between many clock instances using intentional timing jitter, and this can get to the point where things sound completely out of sync.

The method introduced here is a hybridized approach: a humanized clock source that occasionally checks in with a master clock.

How it works

The humanizing clock works by reconstructing triggers in a clock signal. The input signal expects a subdivided master signal that only triggers every N ticks. The hclock knows how many ticks are missing and how fast things are going, and is able to faithfully reproduce this signal while also adding in its own timing jitter.

Here is how hclock would typically work in practice:

A master clock signal is generated, presumably using something like clock. This clock is defined with a BPM and a beat subdivision: say 125 BPM with a subdivision of 4 (16th notes).

Before being fed into hclock, this signal is processed by an instance of tdiv, a clock divider, which makes the clock only trigger every N ticks, say 16 ticks, (once per measure in 4/4 time).

This new signal gets fed into hclock, along with to the following known values: BPM (125), beat subdivision (4), and ticks (16). These initial values are used to make the very first ticks without a delay.

In this example, every time a trigger happens, 15 subsequent new triggers are syntehsized, creating a total of 16 ticks. At the end of this last trigger, the program sits and waits for the next trigger from the master clock, where it will produce all of this again.

Generated File (hclock.c)

<<hclock.c>>=
#include <stdlib.h>
#include <math.h>
#include "patchwerk.h"
#include "runt.h"
#include "runt_patchwerk.h"
<<typedefs>>
<<structs>>
<<funcdefs>>
<<funcs>>

HClock struct and init

<<typedefs>>=
typedef struct hclock_d hclock_d;

<<structs>>=
struct hclock_d
{
    <<hclock>>
};

<<funcdefs>>=
static void hclock_init(hclock_d *h, int sr);

<<funcs>>=
static void hclock_init(hclock_d *h, int sr)
{
    <<hclock_init>>
}

Cable Parameters

Trigger in

the input clock signal, assumed to be pre-subdivided with something like tdiv.

<<hclock>>=
pw_cable *in;

<<hclock_init>>=
h->in = NULL;

<<bind_cables>>=
pw_node_get_cable(node, 0, &h->in);

Jitter Amount

jitter amount: in units of seconds. This will be the (+/-) amount to add.

<<hclock>>=
pw_cable *jitter;

<<hclock_init>>=
h->jitter = NULL;

<<bind_cables>>=
pw_node_get_cable(node, 1, &h->jitter);

NTicks

the number of ticks to produce, including the sync beat. this is parametric: it will be read every time a trigger happens

<<hclock>>=
pw_cable *nticks;

<<hclock_init>>=
h->nticks = NULL;

<<bind_cables>>=
pw_node_get_cable(node, 2, &h->nticks);

Output Cable

An output cable, where the trigger signal actually goes.

<<hclock>>=
pw_cable *out;

<<hclock_init>>=
h->out = NULL;

<<bind_cables>>=
pw_node_get_cable(node, 3, &h->out);

Counters

Like all good clocks, hclock is rooted in counters.

Main Counter

The main counter is used schedule new ticks. This counter ticks down to 0, where it will then output a tick and then update the parameters.

<<hclock>>=
unsigned long cnt;

<<hclock_init>>=
h->cnt = 0;

<<update_main_counter>>=
h->cnt--;

Target Duration

The target duration is the ideal spacing between each tick.

<<hclock>>=
int sr;
double target_dur;

<<hclock_init>>=
h->sr = sr;
h->target_dur = 0;

At init time, this is obtained from the initial BPM and subdivision.

<<set_counter_from_BPM>>=
h->target_dur = 60.0 / (tempo * subdiv);
h->target_dur *= sr;

After the first series of ticks, the target dur ation is obtained through another internal counter that measure the number of samples between ticks. This value, divided by the nticks variable, gets the target duration. The nice thing about this approach is that it can adjust to tempo ramps and fluctations in the master clock. To maintain precision, this target duration is stored as a floating point number so it can preserve fractional sample amounts.

<<set_target_dur>>=
if (h->timer > 0) {
    int nticks;

    nticks = floor(pw_cable_get(h->nticks, n));

    if (nticks > 0) {
        h->target_dur = (double)h->timer / nticks;
    }
}

Resetting the Counter

When a counter is reset, it uses the target duration plus some jitter amount. First, a random number generator is used to obtain a random value between -1 and 1, which is then applied to the jitter value. The jitter value is then converted from seconds to samples.

<<reset_counter>>=
{
    PWFLT rnd;
    PWFLT jit;
    long ijit;

    rnd = (PWFLT) rand() / RAND_MAX;
    rnd = (rnd * 2) - 1;

    jit = pw_cable_get(h->jitter, n);
    jit *= rnd;
    jit *= h->sr;
    ijit = floor(jit);

    h->cnt = h->target_dur + ijit;
}

Tick Position

An hclock is designed to produce a fixed number of ticks before waiting for the master clock. Another counter is used to measure progress.

<<hclock>>=
int tkpos;

<<hclock_init>>=
h->tkpos = 0;

The tick position is reset every time a master trigger occurs but reading the value from the nticks cable.

<<tkpos_reset>>=
{
    int nticks;
    nticks = floor(pw_cable_get(h->nticks, n));
    h->tkpos = nticks;
}

The tick position updates by counting down, like the other timers. This happens when a new tick starts.

<<tkpos_update>>=
h->tkpos--;
if (h->tkpos < 0) h->tkpos = 0;

The tkpos variable is used to prevent making too many ticks. hclock will not produce a tick if tkpos is 0. In an earlier check, HClock will also consider itself to be in wait mode with a zero position.

<<tick_guard>>=
if (h->tkpos < 0) smp = 0;

Timer

A timer counter is used to measure distance between master clock triggers, which is subsequently used to measure subdivisions. This value increments at ever sample, and is reset to be zero every time there is a new tick.

<<hclock>>=
unsigned long timer;

<<hclock_init>>=
h->timer = 0;

<<update_timer>>=
h->timer++;

<<reset_timer>>=
h->timer = 0;

Compute

The main compute function consists of checking the trigger signal and reacting, or just producing ticks internally.

<<funcdefs>>=
static void hclock_compute(hclock_d *h, int blksize);
<<funcs>>=
static void hclock_compute(hclock_d *h, int blksize)
{
    int n;
    for (n = 0; n < blksize; n++) {
        PWFLT smp;
        smp = 0;
        <<react>>
        if (h->tkpos > 0) {
            <<produce_ticks>>
        }
        <<update_counters>>
    }
}

<<react>>=
if (pw_cable_get(h->in, n) != 0) {
    <<reset>>
}

A trigger signal causes a reset button to happen. All the counters + timers are reset.

<<reset>>=
<<tkpos_reset>>
<<set_target_dur>>
<<reset_timer>>
<<trigger_delay>>

The reset trigger is delayed by an amount determined by the jitter. This is done in the hopes to mask the reset that occurs. In this case, it is done by setting the counter to be just the jitter amount.

<<trigger_delay>>=
{
    PWFLT rnd;
    PWFLT jit;
    jit = pw_cable_get(h->jitter, n);
    rnd = (PWFLT) rand() / RAND_MAX;
    h->cnt = floor(jit * rnd * h->sr);
}

The target duration is set first. If the timer is 0, it is most likely an indicator that this is the initial tick, and the default target duration is used. Otherwise, the target duration is derived by extracting the nticks cable and dividing it by the counter.

<<produce_ticks>>=
if (h->cnt == 0) {
    smp = 1;
    <<reset_counter>>
    <<tkpos_update>>
}
<<tick_guard>>
pw_cable_set(h->out, n, smp);

<<update_counters>>=
<<update_timer>>
<<update_main_counter>>

Patchwerk and Runt

hclock is wrapped inside of a patchwerk node, which is then wrapped inside of a runt word + loader. The runt word is then fit to be exported as a runt plugin, should the HCLOCK_PLUGIN macro be defined.

Node

Create

A new node instance of hclock is created with node_hclock. It takes in initial tempo and subdivision as init-time variables.

<<funcdefs>>=
int node_hclock(pw_node *node, PWFLT tempo, PWFLT subdiv);


TODO: build me.

<<funcs>>=
<<nodefuncs>>
int node_hclock(pw_node *node, PWFLT tempo, PWFLT subdiv)
{
    void *ptr;
    hclock_d *h;
    pw_patch *patch;
    int rc;
    int sr;

    rc = pw_node_get_patch(node, &patch);
    pw_node_cables_alloc(node, 4);

    pw_node_set_block(node, 3);

    sr = pw_patch_srate_get(patch);

    if (rc != PW_OK) return rc;

    rc = pw_memory_alloc(patch, sizeof(hclock_d), &ptr);

    if (rc != PW_OK) return rc;

    h = ptr;

    hclock_init(h, sr);
    <<set_counter_from_BPM>>

    <<bind_cables>>

    pw_node_set_data(node, h);
    pw_node_set_compute(node, compute);
    pw_node_set_destroy(node, destroy);

    return PW_OK;
}

Compute

<<nodefuncs>>=
static void compute(pw_node *node)
{
    int blksize;
    hclock_d *h;

    h = pw_node_get_data(node);
    blksize = pw_node_blksize(node);

    hclock_compute(h, blksize);
}

Destroy

<<nodefuncs>>=
static void destroy(pw_node *node)
{
    void *ptr;
    int rc;
    pw_patch *patch;

    pw_node_cables_free(node);

    rc = pw_node_get_patch(node, &patch);

    if (rc != PW_OK) return;

    ptr = pw_node_get_data(node);
    pw_memory_free(patch, &ptr);
}

Runt

Runt Word

<<word>>=
static runt_int rproc_hclock(runt_vm *vm, runt_ptr p)
{
    int rc;
    rpw_param in;
    rpw_param jit;
    rpw_param nticks;
    rpw_param tempo;
    rpw_param subdiv;
    runt_stacklet *out;
    pw_patch *patch;
    pw_node *node;

    rc = rpw_get_param(vm, &subdiv);
    RUNT_ERROR_CHECK(rc);

    if (!rpw_param_is_constant(&subdiv)) {
        runt_print(vm, "subdiv should be constant\n");
        return RUNT_NOT_OK;
    }

    rc = rpw_get_param(vm, &tempo);
    RUNT_ERROR_CHECK(rc);

    if (!rpw_param_is_constant(&tempo)) {
        runt_print(vm, "tempo should be constant\n");
        return RUNT_NOT_OK;
    }

    rc = rpw_get_param(vm, &nticks);
    RUNT_ERROR_CHECK(rc);

    rc = rpw_get_param(vm, &jit);
    RUNT_ERROR_CHECK(rc);

    rc = rpw_get_param(vm, &in);
    RUNT_ERROR_CHECK(rc);

    runt_ppush(vm, &out);
    RUNT_ERROR_CHECK(rc);

    patch = rpw_get_patch(p);
    rc = pw_patch_new_node(patch, &node);

    node_hclock(node,
                rpw_param_get_constant(&tempo),
                rpw_param_get_constant(&subdiv));

    rpw_set_param(vm, node, &in, 0);
    rpw_set_param(vm, node, &jit, 1);
    rpw_set_param(vm, node, &nticks, 2);
    rpw_push_output(vm, node, out, 3);

    return RUNT_OK;
}

Runt Loader

<<funcdefs>>=
void load_hclock(runt_vm *vm, runt_ptr pw);
<<funcs>>=
<<word>>
void load_hclock(runt_vm *vm, runt_ptr pw)
{
    runt_cell *c;
    runt_keyword_define(vm, "hclock", 6, rproc_hclock, &c);
    runt_cell_data(vm, c, pw);
}

Plugin Entry

<<funcs>>=
#ifdef HCLOCK_PLUGIN
runt_int rplug_hclock(runt_vm *vm)
{
    runt_int rc;
    runt_ptr pw;
    rc = rpw_plugin_data(vm, &pw);

    if(rc != RUNT_OK) return rc;

    load_hclock(vm, pw);

    return RUNT_OK;
}
#endif

home | index