9. WIP Trig Graforge Nodes
9.1. Handling sample-accuracy
Trig interfaces with audio-rate signals using wires. Wires read/write at clock-rate (every time the clock triggers), while the signals run at audio rate.
To preserve the sample-accuracy of these signals, a fixed-size event stack is used for each wire. Any time a clock trigger occurs inside of a render block, the signal is stored for it to be retrieved again at the right time later.
In an event stack, nitems
refers to the total of events
the event stack is holding. These are held in the vals
array.
The input
variable is used to mark whether or
not the wire is being used as an input signal, and is
used for processing events in trigex
.
The ref
variable holds a pointer reference to the
corresponding wire in the VM, and is used by trigwget
.
In order to process events in the right sample accurate
position, a reference of the the clock event stack is
supplied as the variable ces
. This gets set when the
page is first initialized.
last
caches the last value from the previous compute call.
typedef struct wire_evstack wire_evstack;
struct wire_evstack {
int nitems;
GFFLT vals[4];
int input;
float *ref;
clock_evstack *ces;
float last;
};
Block positions are stored in an array for the clock signal, which correspond to each event stack.
typedef struct clock_evstack clock_evstack;
struct clock_evstack {
int nitems;
int vals[4];
};
A fixed event size of 4 is decent enough size. Given that the default block size of 64 is used, this means the clock can be a maximum of around 2kHz, way faster than any clock that will be practically used.
9.2. Trig Clock (trigclk)
This sets the clock signal for Trig. It stores any triggers as block events in an event stack.
9.2.1. Trigclk Node Function
static int node_trigclk(gf_node *node, page_trig_d *trig);
<<trig_nodeclk_functions>>
static int node_trigclk(gf_node *node, page_trig_d *trig)
{
gf_node_cables_alloc(node, 1);
gf_node_set_data(node, trig);
gf_node_set_destroy(node, trigclk_destroy);
gf_node_set_compute(node, trigclk_compute);
return GF_OK;
}
9.2.2. Trigclk Compute
static void trigclk_compute(gf_node *n)
{
int blksize;
int s;
gf_cable *in;
page_trig_d *trig;
clock_evstack *ces;
trig = gf_node_get_data(n);
blksize = gf_node_blksize(n);
ces = &trig->ces;
gf_node_get_cable(n, 0, &in);
ces->nitems = 0;
update_and_clear(trig);
for(s = 0; s < blksize; s++) {
GFFLT smp;
smp = gf_cable_get(in, s);
if (smp != 0) {
if (ces->nitems < 4) {
ces->vals[ces->nitems] = s;
ces->nitems++;
}
}
}
}
9.2.3. Trigclk Destroy
static void trigclk_destroy(gf_node *node)
{
gf_node_cables_free(node);
}
9.3. Trig Execute (trigex)
This will step through the trigger VM at the clock rate. Should be called just once per patch.
9.3.1. Trigex Node Function
static int node_trigex(gf_node *node, page_trig_d *trig);
<<node_trigex_functions>>
static int node_trigex(gf_node *node, page_trig_d *trig)
{
gf_node_set_data(node, trig);
gf_node_set_destroy(node, trigex_destroy);
gf_node_set_compute(node, trigex_compute);
return GF_OK;
}
9.3.2. Trigex Compute
9.3.2.1. Overview
There are many moving parts that occur in trigex
.
Some explanation is warranted.
Events from trigclk
get processed here. If there are no
events to process, the function exits.
In the block loop, the next event is searched for. When the event is found, the node prepares to step through the VM.
Before the VM can step, input signals are processed. While the trig VM doesn't make a distinction between inputs and outputs, graforge does. The function will iterate through all 8 wire event stacks. If the wire event stack is marked as an input cable are there are events to process, it will copy the event over to the cable.
After input signals are copied over, trig_vm_step
can be
called.
9.3.2.2. Setting wires at audio rate
Wires used for output signals are taken out of the VM using a special callback interface. Any time a write command happens in the VM, it triggers a user-defined callback. In this case, this callback pops a value onto the event stack.
static void wire_cb(trig_vm *vm, void *ud, int w, float x);
static void wire_cb(trig_vm *vm, void *ud, int w, float x)
{
/* TODO: prevent double-writing */
page_trig_d *trig;
wire_evstack *wes;
trig = ud;
wes = &trig->wes[w];
if (wes->nitems >= 4) return;
wes->vals[wes->nitems] = x;
wes->nitems++;
}
trig_vm_setter(&trig->tvm, wire_cb, trig);
9.3.2.3. What happens if the VM writes to cable more than once?
What happens if the VM writes to a cable more than once? Without some kind of protection, the VM is at risk of overpopulating the event stack for that wire! By the time output cables are being written to, the clock event stack has been populated containing a list of ordered events and buffer offsets. The current buffer position can also be kept track. With this information, it can be determined if the callback has been called at this sample.
Input cables are a bit more forgiving: the only potential risk here is if a two signals write to the same cable. The solution here is a straight forward one: just reset the event stack at the beginning of every block, making only the last signal the valid one.
9.3.2.4. The Callback Itself
static void trigex_compute(gf_node *n)
{
int blksize;
int s;
page_trig_d *trig;
clock_evstack *ces;
int nitems;
int pos;
trig_vm *tvm;
trig = gf_node_get_data(n);
blksize = gf_node_blksize(n);
ces = &trig->ces;
nitems = ces->nitems;
tvm = &trig->tvm;
if (trig->please_reset) {
trig_state_reset(&tvm->istate);
trig->please_reset = 0;
}
if (nitems == 0) return;
pos = 0;
/* clear output wires */
for (s = 0; s < 8; s++) {
if (!trig->wes[s].input) {
trig->wes[s].nitems = 0;
}
}
for(s = 0; s < blksize; s++) {
if (pos >= nitems) break;
if (s == ces->vals[pos]) {
wire_evstack *wes;
int w;
wes = trig->wes;
for (w = 0; w < 8; w++) {
if (wes[w].input && wes[w].nitems > 0) {
float val;
val = wes[w].vals[pos];
trig_vm_wire_set(tvm, w, val);
}
}
trig_vm_step(tvm);
pos++;
}
}
draw_position(trig, tvm->istate.pos);
check_and_draw(trig);
}
9.3.3. Trigex Destroy
static void trigex_destroy(gf_node *node)
{
gf_node_cables_free(node);
}
9.4. WIP Wire Get (trigwget)
This returns a wire. Should be called after trigex
. The
wire number is specified as an init-time constant, and
should be a number between 1 and 8 (inclusive).
9.4.1. Trigwget Node Function
At init-time the wire used is used to specify which wire to choose from. Internally, the wires are addressed using zero indexed indices (0, 1, 2, 3, etc). However, the grid interface makes them 1-indexed (1, 2, 3, 4) because it works out better visually. To keep the end-user interface consistent, the wire id will be 1-indexed, then converted internally.
static int node_trigwget(gf_node *node,
page_trig_d *trig,
int wire);
<<node_trigwget_functions>>
static int node_trigwget(gf_node *node,
page_trig_d *trig,
int wire)
{
if (wire < 1 || wire > 8) {
fprintf(stderr, "Invalid wire %d\n", wire);
return GF_NOT_OK;
}
gf_node_cables_alloc(node, 1);
gf_node_set_block(node, 0);
gf_node_set_data(node, &trig->wes[wire - 1]);
gf_node_set_destroy(node, trigwget_destroy);
gf_node_set_compute(node, trigwget_compute);
return GF_OK;
}
9.4.2. Trigwget Compute
Reading from a wire is a matter of parsing the event stack for that wire. If the event stack is empty, then the value is read from the VM wire directly.
static void trigwget_compute(gf_node *n)
{
int blksize;
int s;
clock_evstack *ces;
wire_evstack *wes;
int nitems;
int pos;
GFFLT val;
gf_cable *out;
wes = gf_node_get_data(n);
blksize = gf_node_blksize(n);
ces = wes->ces;
nitems = ces->nitems;
gf_node_get_cable(n, 0, &out);
if (nitems == 0) {
val = *wes->ref;
} else {
val = wes->last;
}
pos = 0;
for(s = 0; s < blksize; s++) {
if (pos < nitems) {
if (s == ces->vals[pos]) {
val = wes->vals[pos];
pos++;
}
}
gf_cable_set(out, s, val);
}
wes->last = val;
}
9.4.3. Trigwget Destroy
static void trigwget_destroy(gf_node *node)
{
gf_node_cables_free(node);
}
9.5. TODO Wire Set (trigwset)
Copies a signal to a wire. Copies the signal to event
stacks, so should be called after trigclk
and before
trigex
.
When wires are set, the event stack that corresponds to it is reset at the top of the block. If multiple signals write to the same wire, only the last signal gets chosen.
When a trigwset
node is created, it makes a note in that
wire's event stack that it is being used at an input signal.
That way, the trigwset
node is able to differentiate
between output cables in the midst of being written to
and input cables ready to be parsed.
When this node is destroyed, it is unmarked.
9.6. WIP Trig Re-Execute (trigrex)
trigrex
creates another program reader that can work
concurrently with the main reader, but with a different
starting position. Use this to have trig play multiple
patterns simultaneously.
The trigrex
node expects to be sometime called after the
main trigex
function.
9.6.1. Trigrex Node Function
The node function will allocate a new instance trig_state
,
and then store the starting position and trig_vm
data.
static int node_trigrex(gf_node *node,
page_trig_d *trig,
int pos);
<<node_trigrex_functions>>
static int node_trigrex(gf_node *node,
page_trig_d *trig,
int pos)
{
trig_state *ts;
int rc;
void *ud;
gf_patch *patch;
rc = gf_node_get_patch(node, &patch);
if (pos > 32 || pos < 1) {
printf("trigrex: pos %d must be in range 1-32\n",
pos);
return GF_NOT_OK;
}
if (rc != GF_OK) return rc;
rc = gf_memory_alloc(patch, sizeof(trig_state), &ud);
ts = ud;
trig_state_init(ts);
trig_state_ud_set(ts, trig);
trig_state_ipos(ts, pos);
trig_state_reset(ts);
gf_node_set_data(node, ts);
gf_node_set_destroy(node, trigrex_destroy);
gf_node_set_compute(node, trigrex_compute);
return GF_OK;
}
9.6.2. Trigrex Compute
This behaves very similarly to trigex
. It iterates through
the blocks event stack, but instead of calling
trig_vm_step
, it calls trig_state_step
.
static void trigrex_compute(gf_node *n)
{
int blksize;
int s;
page_trig_d *trig;
clock_evstack *ces;
int nitems;
int pos;
trig_vm *tvm;
trig_state *ts;
ts = gf_node_get_data(n);
blksize = gf_node_blksize(n);
trig = trig_state_ud_get(ts);
ces = &trig->ces;
nitems = ces->nitems;
tvm = &trig->tvm;
if (trig->please_reset) {
trig_state_reset(ts);
}
if (nitems == 0) return;
pos = 0;
for(s = 0; s < blksize; s++) {
if (pos >= nitems) break;
if (s == ces->vals[pos]) {
wire_evstack *wes;
int w;
wes = trig->wes;
for (w = 0; w < 8; w++) {
if (wes[w].input && wes[w].nitems > 0) {
float val;
val = wes[w].vals[pos];
trig_vm_wire_set(tvm, w, val);
}
}
trig_vm_step_state(tvm, ts);
pos++;
}
}
draw_position(trig, ts->pos);
}
9.6.3. Trigrex Destroy
static void trigrex_destroy(gf_node *node)
{
void *ud;
gf_patch *patch;
int rc;
rc = gf_node_get_patch(node, &patch);
if (rc != GF_OK) return;
ud = gf_node_get_data(node);
gf_memory_free(patch, &ud);
gf_node_cables_free(node);
}
prev | home | next