5. Trigger Loops and Banks

5.1. Defining a Loop (data)

Each input trigger event is managed separately in what is referred to as a loop. A TDelay instance pre-allocates a fixed set of these at init time. This is known as a bank.

5.1.1. Struct Declaration

<<typedefs>>=
typedef struct tdelay_loop tdelay_loop;
<<structs>>=
struct tdelay_loop {
<<tdelay_loop>>
};

5.1.2. Init

A loop is initialized with tdelay_loop_init.

<<funcdefs>>=
void tdelay_loop_init(tdelay_loop *lp);
<<funcs>>=
void tdelay_loop_init(tdelay_loop *lp)
{
<<tdelay_loop_init>>
}

5.1.3. Components

A loop has 4 major components.

5.1.3.1. State

The state holds one of three main states: SLEEP, WAIT, and FIRE

<<tdelay_loop>>=
int state;
<<tdelay_loop_init>>=
lp->state = SLEEP;
<<tdelay_loop_states>>=
enum {
    BEGIN,
    SLEEP,
    WAIT,
    FIRE,
    RELOAD
};
5.1.3.2. Energy

The energy level is used to determine whether or not a trigger should fire.

<<tdelay_loop>>=
GFFLT energy;
<<tdelay_loop_init>>=
lp->energy = 0;
5.1.3.3. Block Position

When a FIRE event does happen, the position in the block is noted with a variable blockpos.

<<tdelay_loop>>=
int blockpos;
<<tdelay_loop_init>>=
lp->blockpos = 0;
5.1.3.4. Counter

While in WAIT mode, the loop will burn down a counter. When it goes to zero, it will fire.

<<tdelay_loop>>=
int counter;
<<tdelay_loop_init>>=
lp->counter = 0;

5.2. Trigger Loop

When an event trigger happens, the next available trigger bank will be assigned to the event. If there isn't an available loop, it will go through and try to find the next available loop. If the slots are all filled, it will not even bother to look.

---

I believe the counter may need to be incremented by 1 here for the first hit in order for the time to be correct.

<<funcdefs>>=
void tdelay_trigger_loop(tdelay_d *td, int pos);
<<funcs>>=
void tdelay_trigger_loop(tdelay_d *td, int pos)
{
    int loop_id;
    if (td->nactive >= td->nloops) {
        return; /* full! */
    }

    loop_id = -1;

    if (td->last_free >= 0) {
        loop_id = td->last_free;
    } else {
        int n;
        tdelay_loop *bank;

        bank = td->bank;

        for (n = 0; n < td->nloops; n++) {
            if (bank[n].state == SLEEP) {
                loop_id = n;
                break;
            }
        }
    }

    if (loop_id >= 0) {
        tdelay_loop *lp;

        lp = &td->bank[loop_id];

        lp->state = BEGIN;
        lp->counter = 0;
        lp->energy = 1;

        td->nactive++;
        td->last_free = -1;
    }
}

5.3. Updating The Loop Bank

When a clock trigger happens, the program will tick through the trigger bank and update states/times. Feedback + delay values will be cached as well.

<<funcdefs>>=
void tdelay_update(tdelay_d *td, int pos);
<<funcs>>=
void tdelay_update(tdelay_d *td, int pos)
{
    int n;

    for (n = 0; n < td->nloops; n++) {
        tdelay_update_loop(td, n, pos);
    }
}

5.4. Updating Loop State

<<funcdefs>>=
void tdelay_update_loop(tdelay_d *td,
                        int loop,
                        int pos);

A trigger delay loop goes through different states. For now, only one state change can happen per render block. To simplify the code, state changes for any given loop can only happen once per render block. In most practical cases, this should be enough. There are three main states: SLEEP, WAIT, and FIRE.

As edge cases + bugs have been uncovered during development, extra states have also been added (RELOAD, BEGIN).

A loop begins in SLEEP mode, until it is activated and used to delay a particular event, where it then will alternate between FIRE and WAIT. FIRE mode means that the loop will output a trigger in this render block at a given sample position in the block. WAIT means it is waiting to be fired. When a loop has decayed to nothing, it gets put back into SLEEP mode to be used again.

A trigger loop can mostly be thought of as a counter that counts down. When it reaches zero, it fires and will subsequently be reset. (Note: this actually may mean that two sequential fires cannot happen between blocks. Given the block sizes, this is still okay).

By the time a particular loop is being computed, no state needs to be modified at all. All the loop needs to do is check the block position and output a trigger if it is in FIRE mode for the block.

When a clock triggers, each trigger loop in the bank gets updated. Depending on the mode, certain things can happen:

When a event is first added, it is set to BEGIN. This is the chance for the loop to be properly intialized.

For this to work, the event trigger needs to be processed before the loop states are processed.

<<state_fire>>=
case BEGIN:
    lp->energy = 1;

    lp->counter = floor(gf_cable_get(td->dly, pos)) - 1;

    if (lp->counter < 0) {
        lp->counter = 0;
    }

    lp->state = WAIT;
    break;


When in WAIT mode, a countdown happens. If the counter is zero, it gets set to FIRE mode. The block position is noted.

<<state_wait>>=
case WAIT:
    if (lp->counter <= 0) {
        lp->state = FIRE;
        lp->blockpos = pos;
        td->ticked_last_block = 1;
    } else {
        lp->counter--;
    }
    break;

A loop in FIRE mode means there was a trigger in the last render block. The counter is reset, and the energy is reduced by the feedback amount. If the energy level is about zero, then the loop mode is set to SLEEP. This loop will be on deck to be the next available slot. This will not be available until the next block starts. To make sure there is only one tick per block, there will be some sort of tick-lock to disable double event triggers. Because of this logic, you cannot have an two event triggers occur right next to eachother between render blocks. When working with small 64-sample blocks, this a non-issue.

<<state_fire>>=
case FIRE:
case RELOAD:
    lp->energy *= gf_cable_get(td->fdbk, pos);
    if (lp->energy < td->eps) {
        lp->state = SLEEP;
        td->last_free = loop;
        td->nactive--;
    } else {
        lp->counter = floor(gf_cable_get(td->dly, pos)) - 1;

        if (lp->counter <= 0) {
            lp->counter = 0;
            lp->state = FIRE;
            lp->blockpos = pos;
            td->ticked_last_block = 1;
        } else {
            lp->counter--;
            lp->state = WAIT;
        }
    }
    break;

A loop in SLEEP mode will just be skipped. There is nothing that needs to be done here.

<<state_sleep>>=
case SLEEP:
default:
    break;

After a TDelay node is instantiated (called the Mother Node), triggers for each loop in the bank must be accessed via other nodes with access to the TDelay state data for that loop. The loop node treats the state as read-only, so it can be called many times.

<<funcs>>=
void tdelay_update_loop(tdelay_d *td,
                        int loop,
                        int pos)
{
    tdelay_loop *lp;
    lp = &td->bank[loop];

    switch (lp->state) {
<<state_wait>>
<<state_fire>>
<<state_sleep>>
    }
}



prev | home | next