5. DSP Kernel

The DSP kernel is the heartbeat of the signal processing chain. It is the DSP computation that happens inside of the audio render callback. It has a top-level compute function known as monolith_compute.

5.1. Top-level Kernel Compute

<<function_declarations>>=
void monolith_compute(monolith_d *m, int nframes, GFFLT **out);

When out is a null value, the compute function will compute but not not save any values.

<<functions>>=
<<static_compute_functions>>
<<cf_compute_function>>
void monolith_compute(monolith_d *m, int nframes, GFFLT **out)
{
    int s;
    GFFLT tmp;
    GFFLT *outL;
    GFFLT *outR;
    int store;
    int blksize;

    store = 1;
    if(out == NULL) {
        store = 0;
        outL = NULL;
        outR = NULL;
    } else {
        outL = out[0];
        outR = out[1];
    }

    m->nframes = nframes;
    blksize = gf_patch_blksize(m->patch);
    if(monolith_is_playing(m)) {
        for(s = 0; s < nframes; s++) {
            if(m->count == 0) {
                m->offset = s;
                compute_block(m);
            }
            tmp = get_sample(m);

<<cf_compute>>

            if(store) {
                outL[s] = tmp;
                outR[s] = tmp;
            }
            /* update counters */
            m->count = (m->count + 1) % blksize;
        }
    } else {
        for(s = 0; s < nframes; s++) {
            if(store) {
                outL[s] = 0;
                outR[s] = 0;
            }
        }

    }
}

Counter. This keeps track of when to render a new block of audio.

<<struct_contents>>=
int count;
<<init>>=
m->count = 0;

In order to hook up jack inputs to graforge blocks, frame offset must be kept track of.

<<struct_contents>>=
int offset;
<<init>>=
m->offset = 0;
<<struct_contents>>=
int nframes;
<<init>>=
m->nframes = 0;

5.1.1. Computing graforge blocks

<<static_compute_functions>>=
<<hotswap_function>>
static void compute_block(monolith_d *m)
{
<<cf_check_and_free>>
    if(m->please_swap == 1) {
        m->please_swap = 0;
        hotswap(m);
    }

    gf_subpatch_compute(&m->cab_read->subpatch);
<<cf_compute_write_block>>
}
<<static_compute_functions>>=
static GFFLT get_sample(monolith_d *m)
{
    return m->cab_read->blk[m->count];
}

5.1.2. Swapping in the Audio callback

When the swap flag is set, the audio callback will call the static function hotswap. This hotswap function will check how many userbuffers there are at the end, and print a warning if there are any held userbuffers left. This is done to prevent any weird side effects from happening.

<<hotswap_function>>=
static void hotswap(monolith_d *m)
{
    monolith_cable *tmp;
    int nuserbuf;
    tmp = m->cab_read;
    m->cab_read = m->cab_write;
    m->cab_write = tmp;

    nuserbuf = gf_bufferpool_nactive(gf_patch_pool(m->patch));

    if(nuserbuf != 0) {
        fprintf(stderr,
                "WARNING: there are %d userbuffers left, when there should be 0",
                nuserbuf);
        fprintf(stderr,
                "This will eventually cause graforge to crash on re-compilation.\n");
        fprintf(stderr,
                "To fix, ensure that you are unholding all held cables with bunhold.\n");
        fprintf(stderr,
                "If that fails, use bunholdall.\n");
    }

    gf_subpatch_save(m->patch, &m->cab_read->subpatch);
    gf_patch_reinit(m->patch);

    if(m->cf_state == CROSSFADE_NONE) {
        /* free and reinit old subpatch here */
        gf_subpatch_destroy(&m->cab_write->subpatch);
        gf_subpatch_free(&m->cab_write->subpatch);
    }

<<cf_begin>>
}

5.2. Hot Swapping

The term hot-swapping here refers to Monoliths ability to replace Graforge patches on the fly, which provides the user a means of live coding. Basic hot swapping in an audio patch isn't enough, as such an operation could cause an audible glitch or skip to occur. To mitigate this, a crossfade is used to smoothly transition between the old patch and the new patch.

Luckily, most of these hotswapping mechanics have been laid out in the previous incarnation of monolith, monomer. Much of this code will come from this.

5.2.1. The Monolith Audio Cable

The Monolith Audio Cable is a the data type that actually gets swapped out.

A monolith cable type is a struct called monolith_cable. Any patch in build in graforge will terminate at one of these monolith cable.

<<typedefs>>=
typedef struct monolith_cable monolith_cable;

A monolith cable is special kind of audio-rate graforge cable. It maintains a it's own internal buffer instead of relying on the buffer pool that graforge provides. This is important for live coding because it provides persistence between swaps.

A monolith cable is designed to be set inside of a graforge cable, so it also has an input signal which it saves to a gf_cable pointer in. This input signal then gets copied to the internal buffer.

<<structs>>=
struct monolith_cable {
    gf_cable *in;
    gf_subpatch subpatch;
    float blk[MONOLITH_BLKSIZE];
};

There are exactly 2 audio cables needed in the setup, one that is being written to, and the other that is being read. The read and write cables are set to be pointers, whose addresses get swapped.

<<struct_contents>>=
monolith_cable cab[2];
monolith_cable *cab_read; /* the one you hear */
monolith_cable *cab_write; /* the one on deck */

At init-time, the two internal cables are initialized, with cable 0 being assigned to cab_read and cable 1 being assigned to cab_write.

<<init>>=
gf_subpatch_init(&m->cab[0].subpatch);
gf_subpatch_init(&m->cab[1].subpatch);
monolith_cable_init(&m->cab[0]);
monolith_cable_init(&m->cab[1]);
m->cab_read = &m->cab[0];
m->cab_write = &m->cab[1];
<<cleanup>>=
gf_subpatch_destroy(&m->cab[0].subpatch);
gf_subpatch_destroy(&m->cab[1].subpatch);
gf_subpatch_free(&m->cab[0].subpatch);
gf_subpatch_free(&m->cab[1].subpatch);

Monolith cables are initialized with the function monolith_cable_init. This zeros out the buffer and nulls things out. Before to check for NULL before attempting to access data from in.

<<function_declarations>>=
void monolith_cable_init(monolith_cable *c);
<<functions>>=
void monolith_cable_init(monolith_cable *c)
{
    int n;
    for(n = 0; n < MONOLITH_BLKSIZE; n++) {
        c->blk[n] = 0;
    }
    c->in = NULL;
}

5.2.2. Cable Output Node (monout)

Graforge cable output is written to a monolith cable via a graforge node, which will be arbitrarily be called monout to distinguish itself from the default graforge out keyword.

<<function_declarations>>=
int node_monout(gf_node *node, monolith_d *d);
<<functions>>=
static void monout_compute(gf_node *node)
{
    monolith_cable *out;
    int s;
    int blksize;

    out = gf_node_get_data(node);
    blksize = gf_node_blksize(node);

    for(s = 0; s < blksize; s++) {
        out->blk[s] = gf_cable_get(out->in, s);
    }
}

static void monout_destroy(gf_node *node)
{
    monolith_cable *out;
    out = gf_node_get_data(node);
    monolith_cable_init(out);
    gf_node_cables_free(node);
}

int node_monout(gf_node *node, monolith_d *m)
{
    gf_patch *patch;
    int rc;
    monolith_cable *out;
    rc = gf_node_get_patch(node, &patch);
    if(rc != GF_OK) return rc;

    /* if(gf_patch_blksize(patch) != MONOLITH_BLKSIZE) { */
    /*     fprintf(stderr, */
    /*             "Block size mismatch between Graforge (%d) and Monolith (%d). Bye.\n", */
    /*             gf_patch_blksize(patch), MONOLITH_BLKSIZE); */
    /*     return GF_NOT_OK; */
    /* } */

    out = m->cab_write;

    gf_node_cables_alloc(node, 1);
    gf_node_get_cable(node, 0, &out->in);

    gf_node_set_destroy(node, monout_destroy);
    gf_node_set_compute(node, monout_compute);
    gf_node_set_data(node, out);

    return GF_OK;
}
<<monolith_runt_loader>>=
monolith_runt_define(m, "monout", 6, rproc_monout);
<<runt_functions>>=
static runt_int rproc_monout(runt_vm *vm, runt_ptr p)
{
    monolith_d *m;
    gf_patch *patch;
    gf_node *node;
    runt_int rc;
    rgf_param in;

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

    m = runt_to_cptr(p);
    patch = m->patch;

    rc = gf_patch_new_node(patch, &node);
    GF_RUNT_ERROR_CHECK(rc);
    rc = node_monout(node, m);
    GF_RUNT_ERROR_CHECK(rc);

    rgf_set_param(vm, node, &in, 0);
    return RUNT_OK;
}

5.2.3. Cable Input Node (monin)

Lets in a mono input signal. For now, this input is a wrapper around a JACK audio cable.

<<function_declarations>>=
int node_monin(gf_node *node, monolith_d *d);
<<functions>>=
static void monin_compute(gf_node *node)
{
#ifndef MONOLITH_SIMPLE
    monolith_d *m;
    int blksize;
    int s;
    jack_default_audio_sample_t *in;
    gf_cable *out;

    m = gf_node_get_data(node);
    blksize = gf_node_blksize(node);

    in = (jack_default_audio_sample_t*)
        jack_port_get_buffer(m->in[0], m->nframes);

    if ((m->nframes - m->offset) < blksize) {
        blksize = m->nframes - m->offset;
    }

    gf_node_get_cable(node, 0, &out);

    for(s = 0; s < blksize; s++) {
        gf_cable_set(out, s, in[m->offset + s]);
    }
#endif
}

int node_monin(gf_node *node, monolith_d *m)
{
    gf_node_cables_alloc(node, 1);
    gf_node_set_block(node, 0);

    gf_node_set_compute(node, monin_compute);
    gf_node_set_data(node, m);

    return GF_OK;
}
<<monolith_runt_loader>>=
monolith_runt_define(m, "monin", 5, rproc_monin);
<<runt_functions>>=
static runt_int rproc_monin(runt_vm *vm, runt_ptr p)
{
    monolith_d *m;
    gf_patch *patch;
    gf_node *node;
    runt_int rc;
    runt_stacklet *out;

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

    m = runt_to_cptr(p);
    patch = m->patch;

    rc = gf_patch_new_node(patch, &node);
    GF_RUNT_ERROR_CHECK(rc);
    rc = node_monin(node, m);
    GF_RUNT_ERROR_CHECK(rc);

    rgf_push_output(vm, node, out, 0);
    return RUNT_OK;
}

5.2.4. Swap Flag

When a patch is done being populated, it sends a message to the audio DSP kernel to swap the patch by setting the flag please_swap.

<<struct_contents>>=
int please_swap;
<<init>>=
m->please_swap = 0;

The hotswap flag can be set using the C function monolith_please_swap.

<<function_declarations>>=
void monolith_please_swap(monolith_d *m);
<<functions>>=
void monolith_please_swap(monolith_d *m)
{
    m->please_swap = 1;
}

The flag can also be set using the runt word ps.

<<monolith_runt_loader>>=
monolith_runt_define(m, "ps", 2, rproc_ps);
<<runt_functions>>=
static runt_int rproc_ps(runt_vm *vm, runt_ptr p)
{
    monolith_d *m;
    m = (monolith_d *)runt_to_cptr(p);
    monolith_please_swap(m);
    return RUNT_OK;
}

5.3. Play/Pause

The DSP kernel can pause and resume computation. This is controlled via a state flag. By default, it is set to be 1, which is on.

5.3.1. Play/Pause Flags

<<struct_contents>>=
int playstate;
<<init>>=
m->playstate = 1;

5.3.2. Check if Monolith is Playing

The function monolith_is_playing will check the state flag to determine if it is playing.

<<function_declarations>>=
int monolith_is_playing(monolith_d *m);
<<functions>>=
int monolith_is_playing(monolith_d *m)
{
    return m->playstate;
}

The playback state can be read from scheme via monolith:is-playing.

5.3.3. Play/Pause functions

The function monolith_play will explicitely set the playback state to be on. The function monolith_pause will explicitely pause playback.

<<primitive_entries>>=
{"monolith:is-playing", pp_is_playing, 0, 0, {___,___,___}},
<<scheme_functions>>=
static cell pp_is_playing(cell p) {
    monolith_d *m;
    m = monolith_data_get();
    if(monolith_is_playing(m)) return TRUE;
    else return FALSE;
}
<<function_declarations>>=
void monolith_play(monolith_d *m);
void monolith_pause(monolith_d *m);
<<functions>>=
void monolith_play(monolith_d *m)
{
    m->playstate = 1;
}
void monolith_pause(monolith_d *m)
{
    m->playstate = 0;
}

The playback can be controlled with monolith:play and monolith:pause.

<<primitive_entries>>=
{"monolith:play", pp_play, 0, 0, {___,___,___}},
{"monolith:pause", pp_pause, 0, 0, {___,___,___}},
<<scheme_functions>>=
static cell pp_play(cell p) {
    monolith_d *m;
    m = monolith_data_get();
    monolith_play(m);
    return UNSPECIFIC;
}
static cell pp_pause(cell p) {
    monolith_d *m;
    m = monolith_data_get();
    monolith_pause(m);
    return UNSPECIFIC;
}

5.4. Crossfade

Normal hotswapping will often cause a discontintunity in the sound between the signal of the new patch and the signal of the old patch. If this discontinuity is large enough, it will cause an unwanted audible click to be heard. This can be smoothed somewhat using a linear crossfade. This section will describe the underlying principles of how crossfading works on top of the hotswapping system.

5.4.1. Enabling/Disabling Crossfade

Crossfades can be enabled with the function monolith_crossfade_enable, and disabled with the function monolith_crossfade_disable. On success, they will return true (1).

5.4.1.1. Enabling/Disabling Crossfade in C

<<function_declarations>>=
int monolith_crossfade_enable(monolith_d *m);
int monolith_crossfade_disable(monolith_d *m);

Enabling the crossfade will set the crossfade state variable to CROSSFADE_STANDBY, assuming the state is currently set to be CROSSFADE_NONE.

<<functions>>=
int monolith_crossfade_enable(monolith_d *m)
{
    if(m->cf_state == CROSSFADE_STANDBY) return 1; /* already enabled */
    if(m->cf_state == CROSSFADE_NONE) {
        m->cf_state = CROSSFADE_STANDBY;
        return 1;
    }
    return 0;
}

Crossfades can be disabled by setting the crossfade state to CROSSFADE_NONE. The state will need to be in CROSSFADE_STANDBY, in order to safely disable it.

<<functions>>=
int monolith_crossfade_disable(monolith_d *m)
{
    if(m->cf_state == CROSSFADE_NONE) return 1; /* already disabled */
    if(m->cf_state == CROSSFADE_STANDBY) {
        m->cf_state = CROSSFADE_NONE;
        return 1;
    }
    return 0;
}
5.4.1.2. Enabling/Disabling Crossfade in Scheme

Crossfades can be enabled/disabled using the functions monolith:crossfade-enable and monolith:crossfade-disable.

<<primitive_entries>>=
{"monolith:crossfade-enable", pp_crossfade_enable, 0, 0, {CHR,___,___}},
{"monolith:crossfade-disable", pp_crossfade_disable, 0, 0, {CHR,___,___}},
<<scheme_functions>>=
static cell pp_crossfade_enable(cell x)
{
    int rc;
    monolith_d *m;

    m = monolith_data_get();
    rc = monolith_crossfade_enable(m);

    if(!rc) prints("Could not enable crossfade\n");

    return UNSPECIFIC;
}
static cell pp_crossfade_disable(cell x)
{
    int rc;
    monolith_d *m;

    m = monolith_data_get();
    rc = monolith_crossfade_disable(m);

    if(!rc) prints("Could not disable crossfade\n");

    return UNSPECIFIC;
}

5.4.2. Setting Crossfade size

The crossfade time is set in samples.

5.4.2.1. Setting the Crossfade Size in C

The crossfade size can be set in C with the function monolith_crossfade_size_set.

<<function_declarations>>=
void monolith_crossfade_size_set(monolith_d *m, int size);
<<functions>>=
void monolith_crossfade_size_set(monolith_d *m, int size)
{
    m->cf_size = size;
}
5.4.2.2. Setting the Crossfade Size in Scheme

The crossfade size can be set from scheme using the function monolith:crossfade-size-set#+NAME: primitive_entries

{"monolith:crossfade-size-set", pp_crossfade_size_set, 1, 1, {INT,___,___}},
<<scheme_functions>>=
static cell pp_crossfade_size_set(cell x)
{
    int size;
    monolith_d *m;
    char name[] = "monolith:crossfade-size-set";

    size = integer_value(name, car(x));
    m = monolith_data_get();
    monolith_crossfade_size_set(m, size);

    return UNSPECIFIC;
}

5.4.3. Crossfade Struct Contents

Crossfade centers around a counter and a arbitrary crossfade size in samples. This is used to calculate a normalized alpha value used for a linear crossfade.

<<struct_contents>>=
int cf_counter;
int cf_size;
<<init>>=
m->cf_counter = 0;
m->cf_size = 64; /* default block size */

5.4.4. Crossfade State

The behavior of the crossfade is stored in a single state variable. By default, crossfading is disabled, so the mode is set to be CROSSFADE_NONE. When crossfading is enabled, it is initially set to be CROSSFADE_STANDBY.

<<struct_contents>>=
int cf_state;
<<init>>=
m->cf_state = CROSSFADE_STANDBY;

The total crossfade states are described below:

- CROSSFADE_NONE disables all crossfade functionality. - CROSSFADE_STANDBY means crossfade is on and ready to happen - CROSSFADE_COMPUTE means crossfade is currently being computed - CROSSFADE_DONE means crossfade is done, and the write cable needs to be freed.

<<macros>>=
enum {
CROSSFADE_NONE,
CROSSFADE_STANDBY,
CROSSFADE_COMPUTE,
CROSSFADE_DONE
};

5.4.5. Starting crossfades

A crossfade begins when a cable is hotswapped, which means a crossfade will always happen at the start of the block. The state flag is set to begin crossfading (CROSSFADE_COMPUTE), and the crossfade counter is set to be 0.

<<cf_begin>>=
if(m->cf_state == CROSSFADE_STANDBY) {
    m->cf_state = CROSSFADE_COMPUTE;
    m->cf_counter = 0;
}

5.4.6. Computing both blocks

When CROSSFADE_COMPUTE mode is set, both old (write) and new (read) cable blocks are computed. This is operation is handled in the compute_blockfunction.

<<cf_compute_write_block>>=
if(m->cf_state == CROSSFADE_COMPUTE) {
    gf_subpatch_compute(&m->cab_write->subpatch);
}

5.4.7. Crossfade computation

The crossfade itself is computed by taking a sample from both patches and linearly interpolating between the samples.

When a crossfade is active, it will take the sample from the read patch, stored in a variable called tmp, and pass it into a function called crossfade_samp.

<<cf_compute>>=
if(m->cf_state == CROSSFADE_COMPUTE) {
    tmp = crossfade_samp(m, tmp);
<<cf_increment_counter>>
}
<<cf_compute_function>>=
static GFFLT crossfade_samp(monolith_d *m, GFFLT read)
{
    GFFLT write;
    GFFLT a;

    a = (GFFLT)m->cf_counter / m->cf_size;
    write = m->cab_write->blk[m->count];

    return a*read + (1 - a)*write;
}

5.4.8. Crossfade Cleanup

When the counter exceeds the crossfade duration, the state will be set to free the cable (CROSSFADE_DONE).

<<cf_increment_counter>>=
m->cf_counter++;
if(m->cf_counter >= m->cf_size) {
    m->cf_state = CROSSFADE_DONE;
}

The next time the block computes, the write cable patch will be freed, and the patch will be ready to be populated again. The state will then be set back to CROSSFADE_STANDBY.

<<cf_check_and_free>>=
if(m->cf_state == CROSSFADE_DONE) {
    gf_subpatch_destroy(&m->cab_write->subpatch);
    gf_subpatch_free(&m->cab_write->subpatch);
    m->cf_state = CROSSFADE_STANDBY;
}



prev | home | next