11. Stepping Through The Ramp Tree
A big part of gest involves stepping through a ramp tree. A ramp tree produces a set of normalized ramp values from 0 to 1, whose period lengths are proportional to the external conductor signal. These values can then be used to interpolate between targets.
The ramp tree value is computed with ramptree_step
.
It requires an instance of gest g
, the estimated
increment amount for the sample inc
, a reset
flag to indicate if the conductor signal reset,
and the audio block buffer position blkpos
, used to
schedule events that want sample-accurate timing
precision.
static SKFLT ramptree_step(gest_d *g,
SKFLT inc,
int reset,
int blkpos);
static SKFLT ramptree_step(gest_d *g,
SKFLT inc,
int reset,
int blkpos)
{
SKFLT out;
SKFLT phs;
out = 0;
phs = g->phs;
if (g->phrase_selected == NULL) return phs;
<<check_spillover>>
<<beat_checkin>>
inc *= g->phrase_selected->mod * g->correction;
<<set_the_output>>
<<update_targets>>
<<update_phase>>
<<check_and_update>>
g->phs = phs;
g->t += inc;
return out;
}
11.1. Beat Check-in
In order to combat clock drift, a phrase "checks in" every time conductor signal resets, which is an indicator of the new beat. Every time a new beat occurs, the status of the phrase is figured out, and handled accordingly.
Clock drift naturally occurs within Gest because it resynthesizes a new timing signal based on the external conductor signal. Left unchecked, Gestures will eventually fall out of time with the conductor due to the fact that they are marching to the beat of their own drum.
The coarse way that clock drift is managed is by keeping drift localized within the phrase. A phrase is allocated to be a fixed number of beats. When the conductor goes on to the next beat, the current phrase, wherever it may be in its performance, is discarded, and the clock drift debt is reset.
Gestures are a game of constant imprecision. Their timing can either be late or early, but never quite on time. Early gestures occur when a phrase finishes before the conductor. When this happens, it is told to wait, returning 1 until the first downbeat of the next phrase. Late phrases don't quite make it to the end of their gesture. When this happens, the phrase bails and jumps to the next one. The hope is that they are close enough to prevent an audible glitch.
During the check-in, the ideal position, obtained from
the beat counter and known as the goal
,
can be compared with the actual position t
. These two
values can be used to create a course correction factor,
which allows the gesture to slow down or speed up to more
closely match the conductor signal.
if (reset && g->spillover == 0) {
int limit;
SKFLT goal;
limit = g->phrase_selected->beats;
g->beat++;
goal = (SKFLT) g->beat / limit;
if (g->t > 0) g->correction = goal / g->t;
if (g->beat >= limit) {
SKFLT amt;
amt = 1 - g->t;
if (amt > 0 && amt < g->tolerance) {
<<turn_on_spillover>>
} else {
if (g->squawk && amt > 0) {
fprintf(stderr, "Ramp undershot by %g\n", amt);
fprintf(stderr, "phase is %g\n", phs);
}
<<please_wait_in_undershoot>>
g->beat = 0;
g->t = 0;
g->phs = 0;
g->please_wait = 0;
g->correction = 1.0;
g->targetondeck = 1;
g->phrase_selected = g->nxtstate.phrase;
/* reset if next state was told to wait */
g->nxtstate.please_wait = 0;
return phs;
}
}
} else if (g->please_wait) {
return 1.0;
}
11.2. Setting the Output
The point of this function is to update the overall state of the ramp trees in gest and return a corresponding ramp value.
The returned value is the computation done on the previous
call to ramptree_step
.
out = g->phs;
11.3. Updating Targets
A set targetondeck
flag will not only be
used to change the target, but also the state information
that comes with the target, such as the node, the phrase,
and the conductor modifier associated with the target.
It is also here that the next node is found. Information for the next node needs to be immediately available, as certain tools using and extending gest (such as the vocal tract interpolation work) rely on knowing about the next target for it to work properly.
It is assumed that by the time targetondeck
is found,
the next node is already in place (presumably
from the last time targetondeck was set, or initialization),
and just needs to be swapped in.
please_wait
is cached as state, and gets turned on during
find_next_node
.
if (g->targetondeck) {
gest_state *s;
s = &g->nxtstate;
g->targetondeck = 0;
if (s->please_wait) {
g->please_wait = 1;
return 1.0;
}
g->num = s->num;
g->den = s->den;
s->target->curbehavior = s->behavior;
set_curtarget(g, s->target, blkpos);
set_curnode(g, s->node);
g->phrase_selected = s->phrase;
init_state(&g->nxtstate);
if (g->curnode != NULL) {
find_next_node(g, g->curnode, &g->nxtstate);
}
g->nxttarget = g->nxtstate.target;
if (g->nxttarget != NULL) {
g->nxtval = g->nxttarget->value;
} else {
g->nxtval = g->curval;
}
}
What triggers targetondeck
? Usually this happens when the
internal ramp reaches the end. This can also be triggered
during the beat check-in, when a conductor signal demands
to jump to the next phrase before fully finishing the
current one.
11.4. Updating the phase using modifier and friends
To begin, the modifier amount is calculated.
The modifier and increment amount are used together to update the existing phase.
{
SKFLT i;
SKFLT myinc;
if (g->phrase_selected->skew == NULL) {
myinc = inc;
} else {
myinc = g->phrase_selected->skew(g->t, inc);
}
i = myinc * ((SKFLT)g->num / g->den);
phs += i;
}
The phase is then checked to see if it has exceeded 1. If it has, a change in nodes is required.
if (phs > 1.0) {
<<goto_next_node>>
<<wraparound>>
}
11.5. Signalling to go to the next target
When the ramp reaches the end, it is time to go on to the
next node with a target. By the time it reaches this point,
this node has already been found, and just needs to be
signalled to switch with the variable targetondeck
.
g->targetondeck = 1;
11.6. Finding the next node in the Ramp Tree
When a new node begins, the next with a target in the ramp tree must be found immediately after. Many programs using Gest require knowing the next target.
The next node is found by traversing the Ramp Tree based on the position the current node is in.
The process of finding the next node is done in a function
called find_next_node
. It will find the next node, and
store the results in an instance of gest_state
. The
top
node is expected to be the currently selected node
curnode
.
static void find_next_node(gest_d *g,
gest_node *top,
gest_state *state);
The control flow of traversal starts large (checking between phrases), and gets smaller (parent nodes, sibling nodes).
To begin, check and see if the next node happens to be in the next phrase. That would mean the currently selected node is the right-most node (no nodes after it) in the top of the tree. If this is true, it is time to wait for the next phrase on the next down beat. If the next phrase is being found here, it has arrived a tad too early (which is actually better than being a tad too late, as it turns out. It's one or the other here).
/* next node is in next phrase */
phrase = next_phrase(g, phrase);
phrase = get_phrase(g, phrase);
if (phrase != NULL) next = phrase->top;
else next = NULL;
g->num = 1;
g->den = 1;
if (next != NULL) {
next = dive_to_target(g, next);
}
please_wait = 1;
Alternatively, if there are still nodes in the top-level node, go there instead.
TODO: do we need to handle edge case when next node is monoramp with a modifier greater than 1? We are doing that when finding sibling nodes.
next = top->next;
if (next->target == NULL) {
next = dive_to_target(g, next);
}
If it's not the top of the tree, there is a general check to see if the current node is the right-most node relative to the position in the tree. If so, the node reverts the global modification it did, and goes up one level to the parent node to check the next node there.
It may be a little bit confusing to see the parent node getting reverted and not the selected node. Parent nodes are the ones that apply their rhythmic influence, not the selected nodes. When the parent reverted, it reverts the global modifier to a state when the parent node was a child node to another node.
There is an edge case when selected nodes do actually influence things: if the selected node is a monoramp with a value greater than 1, it will actually be scaling the phasor, as it needs to "eat" up beats in the polyramp it is a child of. These kinds of nodes will need to be reverted.
/* if top is a monoramp >1, revert it */
if (top != last_reverted) {
if (top->type == NODE_MONORAMP && top->modifier > 1) {
/* revert the monoramp */
revert_modifier(g, top);
}
}
revert_modifier(g, top->parent);
last_reverted = top->parent;
top = top->parent;
Finally, the next node is found, and the program recursively dives into it to find the next target, applying modifiers along the way. Before that happens, the current node mayneed to revert the global modifier if it is a monoramp with a modifier greater than 1.
next = top->next;
/* if top is a monoramp >1, revert it */
if (top != last_reverted) {
if (top->type == NODE_MONORAMP && top->modifier > 1) {
/* revert the monoramp */
revert_modifier(g, top);
}
}
/* dive_to_target applies modifiers */
/* continguous nodes on the same level don't have these */
if (next->target == NULL) {
next = dive_to_target(g, next);
} else if (next->type == NODE_MONORAMP && next->modifier > 1) {
/* next node is on same level but is monoramp */
apply_modifier(g, next);
}
If the next node happens to be a monoramp with a modifier
greater than 1, it will also apply modifications. But should
be handled inside of dive_to_target
. If the node is already
a target with a monoramp >1, this gets applied here as a
special exception.
static void find_next_node(gest_d *g,
gest_node *top,
gest_state *state)
{
gest_node *next;
gest_node *last_reverted;
gest_phrase *phrase;
int num, den;
int please_wait;
next = NULL;
last_reverted = NULL;
num = g->num;
den = g->den;
phrase = g->phrase_selected;
please_wait = 0;
while (next == NULL) {
if (top == g->phrase_selected->top) {
/* are we at the end */
/* if so, go to next phrase */
if (top->next == NULL) {
<<reset_and_wait_for_next_phrase>>
break;
} else {
/* go to next child in top polyramp node */
<<goto_next_child_in_top_node>>
}
} else if (top->next == NULL) {
<<goto_parent_node>>
} else {
<<find_next_node_with_target>>
}
}
state->num = g->num;
state->den = g->den;
state->node = next;
state->target = node_target(g, next);
state->behavior = target_behavior(g, state->target);
state->phrase = phrase;
state->please_wait = please_wait;
/* restore old numerator and denominator */
g->num = num;
g->den = den;
}
In order for the metaphrase
to exist, phrases must be able
to be indirectly retrieved using an internally get
callback, which can be recursively called. This is is done
with the local function get_phrase
.
static gest_phrase* get_phrase(gest_d *g, gest_phrase *ph);
static gest_phrase* get_phrase(gest_d *g, gest_phrase *ph)
{
if (ph == NULL) return NULL;
while (ph->get != NULL) ph = ph->get(g, ph);
return ph;
}
The next_phrase
function is used to the next phrase after
the current phrase. This will either return the next
pointer directly, or a callback returning something else,
for more variable behavior. Abstracting the next phrase
getter like this allows for things like repeating phrases.
static gest_phrase* next_phrase(gest_d *g, gest_phrase *ph);
static gest_phrase* next_phrase(gest_d *g, gest_phrase *ph)
{
gest_phrase *next;
next = ph->next;
if (ph->nextf != NULL) next = ph->nextf(g, ph);
return next;
}
11.7. When a phrase is over, wraparound
A phrase is considered over when it reaches the end of the top-level polyramp. At this point, the next phrase is found and set to be the beginning of that node.
All ramps begin exactly at 0. When wraparound happens, the roundoff error is stored in the error variable (note: no, it isn't. we are truncating, and I believe this is done to prevent certain kinds of discontinuities. There are checks in place for this).
while (phs > 1) phs--;
phs = 0; /* just kidding, truncate */
11.8. Spillover
In situations where phrases do not quite
finish, but are mostly there (referred to as undershoot
),
spillover
can be applied. This is a special mode that
allows phrases to finish up in the next phrase.
Spillover is used to solve the discontinuities caused
by undershoot. However, too much spillover can cause timing
issues with other phrases. By default, spillover is
disabled.
The amount of spillover a gesture can have is determined by
a value called tolerance
. Tolerance is a value between 0
and 1, and should be a small value very close to or at zero.
The greater the value is, the more spillover can happen.
Spillover behavior is determined via the spillover flag.
The spillover flag gets turned on during the beat checkin when the undershoot is within the tolerance level of being completed. Instead of going to the next phrase, it will resume as-is until it reaches the end of the phrase.
if (g->squawk) {
fprintf(stderr, "spillover turned on\n");
}
g->spillover = 1;
<<please_wait_in_spillover>>
The spillover flag gest turned off when the phrase has
ended and is ready to go to the next phrase. At this
point, the please_wait
flag should be turned on. When
the spillover and wait flags have both been enabled, gest
will do what it would have done during the beat checkin and
go to the next phrase. It will do this immediately and not
wait for the next beat.
if (g->please_wait && g->spillover) {
/* Copied and pasted from beat-checkin code */
/* may want to wrap this in a function later */
g->beat = 0;
g->t = 0;
g->phs = 0;
g->please_wait = 0;
g->correction = 1.0;
g->targetondeck = 1;
g->phrase_selected = g->nxtstate.phrase;
g->spillover = 0;
/* reset if next state was told to wait */
/* TODO: is this needed here? */
g->nxtstate.please_wait = 0;
if (g->squawk) {
fprintf(stderr, "spillover turned off.\n");
}
return phs;
}
In Spillover mode, beat check-in is skipped entirely; there is hardly a need for it, so it is ignored.
11.9. edge case: please_wait flag in beat check-in
Here's a weird edge case: the phrase finishes and
the please_wait
flag is set one sample before the clock
reset. When this happens, a single sample discontinuity will
be returned. Why?
When computing the ramptree, beat-checkin happens before
the please_wait
flag is checked. In most scenarios,
please_wait
is unset by the time it reaches here. But,
because it happened exactly one sample before, the internal
phasor value has been wrapped back to 0 from the previous
call. This ends up returning the equivalent of target A
instead of target B, which is wrong.
To correct this, things must be configured so that the
returned value is 1 instead of 0, based on the state of
please_wait
in the beat check-in when the check-in
discovers it has started the next phrase. There are 2
places to check the flag.
The usual place is right about where gest would squawk
about undershoot. The local variable phs
gets set to be 1.
After some re-init for the next node, this value of gets
returned.
if (g->please_wait) {
if (g->squawk > 1) {
fprintf(stderr, "please_wait set in undershoot\n");
}
phs = 1.0;
}
When the spillover
flag gets turned on, please_wait
will
need to be checked too. Normally, the program is used to
waiting a few samples before please_wait
is set. But in
this edge case, it will only spill over into the next
sample.
In this situation, the internal phasor value is set to be 1.0 in addition to returning 1.0, which is needed for the next sample iteration.
if (g->please_wait) {
if (g->squawk) {
fprintf(stderr, "spillover: please_wait already on\n");
}
g->phs = 1.0;
return 1.0;
}
prev | home | next