Documentation Wiki rss-feed

Work In Progress

This article is still a work in progress. I'm working on it 'in public' so that people can give feedback, so please bear with the typos and rubbish diagrams!


The Event Model

In EigenD, performance information is propagated though Events. An Event is an ID associated with one or more queues of data. The Event has a timestamp, denoting when it starts, and each piece of data in each queue has a timestamp. During the lifetime of the event, more data will typically be fed into each queue by the Event originator.

An Event is not a concrete single thing. It represents a process which starts and ends at certain times, and has one or more streams of data associated with it during that time. These data streams might be routed through different paths and Agents, and modified or discarded. Events have different representations in different parts of EigenD.

Event IDs

Events are identified by dotted numeric notation. A single . is used to denote the empty Event ID. Empty ID's are distict from Null IDs and are perfectly valid.

An Event ID may also contain a single : which divides it into two parts. The leading part is denoted the channel and the trailing part the event. Mostly, this division is ignored, but is useful under certain circumstances.


An Event ID is not globally unique. It needs to be unique only within the context in which it is being used.

One fundamental Event is the key press. When a key is pressed, the keyboard agent generates an event, which ends when the key is released. Keyboard events typically have an ID which is simply the key number. Since you can't press a key more than once at the same time, it's unique enough for our purposes.

When a key is pressed, it generates the following data streams for each event:

  • Key Number -- A complex tuple which includes the sequential key number, and its row and column coordinates in physical and musical space. This stream doesn't change much during the course of the event.

  • Pressure, Roll and Yaw -- The key pressure values. Each of these is in its own data queue and typically change at 2khz while the Event is running.

These 4 signals appear separately on the agent and can be routed individually. However, they are conceptually related by the Event ID.

Here's the Key Number output from a keyboard with a key pressed. This was made using the bcat command line utility.

17.254.1  id=null
17.254.2  id=null
17.254.3  id=null
17.254.4  id=97    (97,(5.0,1.0),97,(1.0,97.0),2)
17.254.5  id=1     (1,(1.0,1.0),1,(1.0,1.0),2)
17.254.6  id=null
17.254.7  id=null
17.254.8  id=null
17.254.9  id=null
17.254.10 id=null

The first column is the index of the carrier nodes. The keyboard starts off with 10 nodes and adds more as needed. Then we have the Event ID and the last bit of data in the data queue.

I've pressed Key 1, which the keyboard is indicating is physically row 1 column 1, or musically key 1 in course 1. I'm also pressing key 97, which is row 5 column 1 physically, and key 07 in course 1 musically. The keyboard agent maps the keyboard as phyical rows and columns but 1 long musical course.

Notice that the Event ID is the same as the sequential key number. This is coincidental and just an implementation detail of the keyboard which is not true in all cases. To know which key is being pressed, you must read this Key Number data stream.

Here is a snapshot of some of the outputs of a keyboard with 2 keys being pressed. I've elided the empty carriers.

    2.254.9   id=1   0.667724609375
    2.254.10  id=97  0.817138671875

    3.254.4   id=97  -0.0078125
    3.254.5   id=1   -0.0107421875

    4.254.4   id=97  0.6015625
    4.254.5   id=1   0.1669921875

Key Number
    17.254.6  id=97  (97,(5.0,1.0),97,(1.0,97.0),2)
    17.254.7  id=1   (1,(1.0,1.0),1,(1.0,1.0),2)

Note that we have to use the Event ID to decide which values go together. There is no relationship between the carrier ID and the event.

Next we'll follow this signal through a key group:

Pressure  id=1   0.667724609375  id=97  0.817138671875

Roll  id=1   -0.0078125  id=97  -0.0107421875

Yaw  id=1   0.6015625  id=97  0.1669921875

Key Number  id=1  (1,(1.0,1.0),1,(1.0,1.0),2)  id=97 (89,(5.0,1.0),89,(5.0,1.0),2)

Notice that the Event ID has remained the same. However, the key has been remapped physically and musically. Our original key 97 is now the 89th key, and is key 1 in course 5. The other data streams are untouched.

If we look at the data stream after its been through a rig we see the following:

Key Number  id=1:97 (89,(5.0,1.0),89,(5.0,1.0),2)  id=1:1  (1,(1.0,1.0),1,(1.0,1.0),2)

I'm pressing the same keys. Generally, a rig will be connected to more than one key group. Since we need to keep the keypress events from each key group distinct, we make the connections from key group to rig with a special parameter. You can see this with the connection editor in Workbench.


This keygroup is connected 'into channel' 1, which simply means that all events coming in on this connection are prefixed with 1. Other key groups will be connected with channels 2, 3 and so on.

If I switch to a split which allows me to play notes from 2 different key groups at once, I see this coming out of the rig:  id=2:97  (17,(5.0,1.0),17,(5.0,1.0),2)  id=3:102 (69,(5.0,1.0),69,(5.0,1.0),2)

I'm actually playing the same note (course 5 key 1) twice. But the Events remain distinct.

If we look at the output of a recorder, while holding down two keys and also playing back a take, we see something more complicated. Here I'm just showing the Key Number and Pressure signals:

Key Number  id=2.2.1:1   (1,(1.0,1.0),1,(1.0,1.0),2)  id=2.2.1:97  (89,(5.0,1.0),89,(5.0,1.0),2)  id=1.1:1     (1,(1.0,1.0),1,(1.0,1.0),2) id=1.1:97    (89,(5.0,1.0),89,(5.0,1.0),2)

Pressure  id=2.2.1:1    0.82958984375  id=2.2.1:97   0.744873046875  id=1.1:97     0.62255859375  id=1.1:1      0.46435546875

We are seeing 4 key presses. The recorder adds another prefix to the channel part of the incoming event. It prefixes the 'live' data with 1, and takes being played back with 2.N where N is different for each take.

The channel part is important because it allows an Agent to determine which key presses are related. Agents like fingerer, which use key combinations, need to look at this. All the incoming events on the same channel come from the same source.

If we follow the signal still further, to a scaler inside the rig, we see something like this on the frequency:

Frequency  id=1.2:97  329.6275634765625  id=1.3:102 329.6275634765625

The scaler is just creating another signal (frequency) with the same event ID as the incoming key signal. We can use this signal in conjunction with other signals from the same key press, but only in the same context, ie, behind our recorder.

Event-Centric vs Signal-Centric


Signal Centric - Ports

When connecting Agents, we make connections between Ports. This is the Signal-Centric view we've seen above. Each Port may carry lots of events. Associated with each Event ID on that Port is a single buffer. Each event on that Port carries one data queue which contains only the data pertaining to that Port. We call these Port Events.

The Event ID's are used to match up the corresponding parts. All the queue's corresponding to a particular press will have the same ID regardless of the Port.

You could route one of these signals via a filtering Agent. As long as it leaves the Event ID's alone, outputting the filtered signal with the same ID as the incoming signal, the new filtered data can be used in place of the original signal.

Ports expose multiple carrier channels. Each channel can carry a single Event at a time. The number of carrier channels is dynamic, and they are added as required.

Event Centric - Bundles

When we process data inside an Agent, we typically want to gather together all the different data queues for a particular event.

For example, a Scaler Agent might have input Ports for Key Number and Pitch Bend. You might connect the Key Number and Yaw signals from a keyboard into those ports. The Scaler is effectively a filter which turns Key Number into Frequency. The Pitch Bend provided optional, additional data which changes this conversion.

The process of matching up incoming events by ID is called Correlation. It gathers the incoming Events on multiple signals and synthesises an aggregate structure with all the relevant data queues under a single ID. We call this a Bundle Event.

EigenD contains a framework for creating processing graphs that deal with these aggregated events. We call these graphs Bundles and the processing nodes in them Bundle Components. Nearly all performance data is processed in this form.

Each bundle connection contains a number of carrier channels. We call these Wires. Each wire is used for processing one event at a time, and logically contains the resources for processing one event. The framework adds wires as required.

In each component the framework creates a processing node for each wire coming into the component. This node will receive one event at a time, and each event will have all the related data queues associated with it. The component is free to create as many output wires as it needs. For each output wire it creates, downstream components will have to create corresponding processing nodes.

Many components are filters which generate one output event for each incoming event, where the output event has the same ID as the incoming event. There are particular tools in the framework to simplify this common case.


Bundle connections are a little more complicated than Port connections. A Wire can be in one of three states:

  • Idle

    There is no event on the wire. The wire is ready to accept a new event.

  • Processing

    An event is in progress on the wire.

  • Lingering

    An event has ended on the wire, but components are still processing. They may be envelope generators in their release phase, or a physical model which is still shedding residual energy.

    Wires in this phase will only accept a new event with the same ID as the last event.



Correlation gathers events from multiple sources and aggregates all the related data queue's together for simple processing.

This is not always a straightforward process. Events on different inputs do not necessarily start and stop at the same time. Multiple consecutive Events with the same ID may be received on an input.

For this reason, much of the correlation process is policy driven to tune it to the requirements of a given Agent.

Primary and Secondary Inputs

In our scaler example, it can do nothing without an incoming Key Number. Pitch Bend by itself doesn't mean anything. Moreover, Pitch Bend isn't needed for the scaler to function.

Key Number is a primary input for the scaler. Each incoming Port Event on a primary input (aka, a primary event) creates a Bundle Event. Pitch Bend is a secondary input. Secondary events are only used with a corresponding primary event.

The basic correlation algorithm is as follows:

  • When a primary event is received, check for existing bundle events with the same ID. If one exists, join the data queue for this primary event into the bundle event. Note that this check includes lingering events with the same ID.

    If no bundle event exists, create one. Check for any secondary events that have been received with compatible IDs. Join their data queues into the bundle event.

  • When a secondary event is received, check for existing bundle events with compatible IDs. If any exist, join the data queue from the secondary event into those bundle events.

  • When a secondary event ends, remove its data queue from any bundle events of which it is a part.

  • When a primary event ends, remove its data queue from its bundle event. If the bundle event has no queues from primary events, end the bundle event. This might cause the event to go into lingering state, or the event might terminate immediately.

Compatible IDs

The check by which a secondary event is deemed compatible with a bundle event isn't a straight comparison of Event ID. A secondary event matches with the bundle event if its ID is a leading subset of the bundle event ID.

This means that a secondary event might end up contributing to more than one bundle event.

As an example, think about the scaler again. Our breath signals and strip controller signals are generated with the empty . event ID. This makes them compatible with all other event IDs. (Each strip can generate only one event at a time, so the empty event ID is OK.)

If we connect the strip controller into the scalers Pitch Bend input, it will act as a global pitch bend, bending notes from all keys. If we connect key yaw instead, each key will be bent only by its own yaw signal.

The way that recorders and rigs treat their inputs preserves this locality. After the strip has been through the rig gateway, it will be modified in the same way as above.

                              Strip IDs              Key IDs
Event ID from keyboard:       .                      1
After Rig:                    1:. or 2:.             1:1 or 2:1
After Recorder:               1.1:. or 1.2:.         1.1:1 or 1.2:1

So in each instrument, the strip will affect only the notes being generated in the same context. The recorded strip controller will affect only the notes being played back in the same take. The 'live' strip controller won't affect recorded takes.


Anisochronous Signals

Isochronous Signals

Clock Domains

The Clocking Graph

Bundle Inputs

Port input policies








Stream Converters

Data Domains and Bundles

Fundamental Classes


data_t and data_nb_t




Event transmission




Event Data










Plumbing it all together

The interface for a typical bundle component

The Python Wrapper

The Python Agent

Command line tools


brpc identify