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
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.
<<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.
int count;
m->count = 0;
In order to hook up jack inputs to graforge blocks, frame offset must be kept track of.
int offset;
m->offset = 0;
int nframes;
m->nframes = 0;
5.1.1. Computing graforge blocks
<<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 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.
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.
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.
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.
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
.
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];
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
.
void monolith_cable_init(monolith_cable *c);
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.
int node_monout(gf_node *node, monolith_d *d);
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_define(m, "monout", 6, rproc_monout);
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.
int node_monin(gf_node *node, monolith_d *d);
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_define(m, "monin", 5, rproc_monin);
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
.
int please_swap;
m->please_swap = 0;
The hotswap flag can be set using the C function monolith_please_swap
.
void monolith_please_swap(monolith_d *m);
void monolith_please_swap(monolith_d *m)
{
m->please_swap = 1;
}
The flag can also be set using the runt word ps
.
monolith_runt_define(m, "ps", 2, rproc_ps);
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
int playstate;
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.
int monolith_is_playing(monolith_d *m);
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.
{"monolith:is-playing", pp_is_playing, 0, 0, {___,___,___}},
static cell pp_is_playing(cell p) {
monolith_d *m;
m = monolith_data_get();
if(monolith_is_playing(m)) return TRUE;
else return FALSE;
}
void monolith_play(monolith_d *m);
void monolith_pause(monolith_d *m);
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
.
{"monolith:play", pp_play, 0, 0, {___,___,___}},
{"monolith:pause", pp_pause, 0, 0, {___,___,___}},
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
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
.
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.
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
.
{"monolith:crossfade-enable", pp_crossfade_enable, 0, 0, {CHR,___,___}},
{"monolith:crossfade-disable", pp_crossfade_disable, 0, 0, {CHR,___,___}},
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
.
void monolith_crossfade_size_set(monolith_d *m, int size);
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,___,___}},
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.
int cf_counter;
int cf_size;
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
.
int cf_state;
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.
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.
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_block
function.
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
.
if(m->cf_state == CROSSFADE_COMPUTE) {
tmp = crossfade_samp(m, tmp);
<<cf_increment_counter>>
}
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
).
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
.
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