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)
#include <stdlib.h>
#include <math.h>
#include "patchwerk.h"
#include "runt.h"
#include "runt_patchwerk.h"
<<typedefs>>
<<structs>>
<<funcdefs>>
<<funcs>>
HClock struct and init
typedef struct hclock_d hclock_d;
struct hclock_d
{
<<hclock>>
};
static void hclock_init(hclock_d *h, int sr);
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
.
pw_cable *in;
h->in = NULL;
pw_node_get_cable(node, 0, &h->in);
Jitter Amount
jitter amount: in units of seconds. This will be the (+/-) amount to add.
pw_cable *jitter;
h->jitter = NULL;
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
pw_cable *nticks;
h->nticks = NULL;
pw_node_get_cable(node, 2, &h->nticks);
Output Cable
An output cable, where the trigger signal actually goes.
pw_cable *out;
h->out = NULL;
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.
unsigned long cnt;
h->cnt = 0;
h->cnt--;
Target Duration
The target duration is the ideal spacing between each tick.
int sr;
double target_dur;
h->sr = sr;
h->target_dur = 0;
At init time, this is obtained from the initial BPM and subdivision.
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.
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.
{
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.
int tkpos;
h->tkpos = 0;
The tick position is reset every time a master trigger
occurs but reading the value from the nticks
cable.
{
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.
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.
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.
unsigned long timer;
h->timer = 0;
h->timer++;
h->timer = 0;
Compute
The main compute function consists of checking the trigger signal and reacting, or just producing ticks internally.
static void hclock_compute(hclock_d *h, int blksize);
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>>
}
}
if (pw_cable_get(h->in, n) != 0) {
<<reset>>
}
A trigger signal causes a reset button to happen. All the counters + timers are 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.
{
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.
if (h->cnt == 0) {
smp = 1;
<<reset_counter>>
<<tkpos_update>>
}
<<tick_guard>>
pw_cable_set(h->out, n, smp);
<<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.
int node_hclock(pw_node *node, PWFLT tempo, PWFLT subdiv);
TODO: build me.
<<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
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
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
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
void load_hclock(runt_vm *vm, runt_ptr pw);
<<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
#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