Trace: » start » bundles » reorganisation
libIntegra reorganisation
This page includes suggestions for the reorganisation in future versions of libIntegra.
Pluggable architecture
It has been suggested in various email discussions that the current fixed design for the serialization layer and instance host 'server' interface should be replaced by a pluggable architecture. This will mean that the 'backend' and 'frontend' will be selectable at runtime in the same way that the module host is currently interchangeable via the integra bridge architecture. The practical advantages of this are:
- It reduces dependencies for the library (no more libintegra dependency on liblo or libxml2)
- It increases portability
- It simplifies the code by creating common APIs for a range of front/back ends
- It allows calling applications to use the storage and communication protocols of their choice rather than being forced into OSC/XML
Code cleanup
- There are currently a lot of (very useful) helper function in helper.c, which should probably go somewhere more specific
- Separation of 'public' and 'private' APIs. There are currently a lot of functions in the public API that should be private
- Clearer delineation of 'low level' and 'high level' functions
Scripting support
There has been talk for some time about adding some kind of scripting support to the library. This could enable things like:
- Modules implemented 'inside' the library. E.g. Connections could be treated as modules and do dynamic processing between ports at runtime
- Dynamic attribute values in module definitions, e.g. maxima: samplerate / 2.0, minima = foo
Data conditioning
1. It should be able to connect up two ports and have an automatic conditioning (scaling, unit conversion, type conversion etc) between them 2. This should be optional
Where does the 'conditioning' flag get set, and what does the conditioning? I.e. how do we tell the `conditioning agent' that the data between two ports needs to be conditioned?
Idea 1
- We have a Connection class
- 'conditioning' is a boolean attribute of the Connection class determines whether or not data gets conditioned
- The actual conditioning gets done by the library 'inside' the connection
Pros:
- Conceptually connections go between ports so having the conditioning inside the connection makes intuitive sense
- Conditioning happens inside the library making it easier to implement and keep consistent between module hosts
Cons:
- It increases the dependency on libIntegra (although we're already pretty dependent on it – current module implementations are fairly useless without it)
- It begs the further question of how we address the connection, e.g. how do we set the flag?
Idea 2
- 'conditioning' is a boolean attribute of the IntegraModule class
- The flag gets set on the receiving side of the connection, and simply: /sinus1/conditioning 1
- The actual conditioning takes place in libIntegra 'somewhere'!
Pros:
- No need for a separate connection class
- Simple and obvious addressing semantics
Cons:
- conditioning is set per module rather than per port
- Still got the libIntegra dependency (but surely we don't expect people to implement this in a separate module (cf. AddressMap)!)
Integra Collections
An Integra Collection is a container for storing module instances and their inter-connections: A Collection may be thought of as the Integra representation of a Pd patch. When a user loads modules and connect them a collection is created behind the scenes and this collection may be saved hence representing the status of the system, including instance data.
libIntegra Collection representation
libIntegra holds collection data according to the following:
- The collection itself is a struct ntg_instance_ of type struct ntg_collection_
- A collection holds an arbitrary number of instances in ntg_collection::instances
- ntg_collections::instances may point to another collection (referred to as an encapsulated collection) and so forth ad infinitum.
- All instances, collections as well as modules, may be accessed through struct ntg_instances_::next
- For the topmost Collection, that wich is not enclosed by any other collection, ntg_instances::next is NULL.
So, in the graph below we have two collections {A, B} out of which A is the Top Level Collection (TLC) and B is an Encapsulated Collection. Apart from the two collections we also have four modules {a, b, c, d}. Contained in collection A are the module instances a and b and the collection instance B. Contained in collection B are modules c and d.
Data representation
Following is the definition of the struct ntg_collection_ in Integra/integra_collection.h:
typedef struct ntg_collection_ { /** Determines whether or not this is an encapsulated collection */ int encapsulated; /** A descriptive general name for the instance's class, e.g. MultiSampler. */ char *class_name; /** A description of the class. */ char *description; /** Optional documentation */ ntg_uri *documentation; /** Optional list of tags */ int *tag_codes; /** Pointer to node in linked list of collection or module * instances. */ ntg_instance *instances; /** Array of arrays of exposed ports. */ int **ports; int n_ports; } ntg_collection;
And following is the definition of the struct ntg_instance_ in Integra/integra_instance.h:
typedef struct ntg_instance_ { /** The instance id */ ntg_id id; /** The unique id of the class to which this instance belongs. */ ntg_id class_id; /** Unique instance name */ char *name; /** An array of references to instance tags. */ int *tag_codes; /** Points to the next instance at the same level */ struct ntg_instance_ *next; /** Points back to the previous instance. */ struct ntg_instance_ *prev; /** Has the instance been changed since last * save/load */ int dirty; /** Used to determine the type of the instance union 'i' */ int type; union i_ { ntg_class_instance *class_; ntg_module_instance *module; struct ntg_collection_ *collection; } i; /** Array of arrays of destination ports. */ struct ntg_port_ **connections; /** Array index gives source port, each array element gives the number of * connections that port has */ int *n_connections; } ntg_instance;
Then if we go back to our example above, we have a total of six instances that may be adressed in the following way (Please note that the following code is provided as an example, and is not intended as real world, runnable code.) If:
ntg_instance A; ntg_instance B;
Then, to get the class-name of the TLC:
char *A-name = A->i.collection->class_name;
To get all module instances contained in A:
ntg_instance instance = A->i.collection->instances; while(instance != NULL) { char *instance_name = instance->name; instance = instance->next; }
And finally, to get to module c of collection B assuming its the first instance of A:
ntg_instance d = A->i.collection->instances->i.collection->instances;
Collection data correspondance
The above is how the data is represented in run time in the library libIntegra. A collection such as the one above, and any kind of data used by the library, can be serialized to XML following the IXD file format specified in the Integra XML Schema: CoreSchema.xsd. The XML may then be stored onto the Integra database for persistent storage and re-usability. When stored in the database the data adopts to the IntegraClassSchema. In other words, there are several transformations of the data as it migrates through the different parts of the Integra system. This section will explain the correspondance between the elements of the IXD and the fields of the ntg_instance and ntg_collection structs.
Below is a sample of a serialized collection with one Sine wave oscillator and one slider connected up to each other.
<name>Test</name>
<description>A short description of the collection</description>
<documentation title="Collection documentation"
href="collection_doc.xhtml"
actuate="onLoad" />
<tag>tag 1</tag>
<object id="obj.0">
<definition id="cd.0"/>
<name>sine</name>
<tag>oscillator</tag>
<state>
<value>220</value>
<value>0</value>
</state>
</object>
<object id="obj.1">
<definition id="cd.1" />
<name>slider</name>
<state>
<value>0</value>
</state>
</object>
<connection id="con.0">
<name>connection1</name>
<source>1</source>
<destination>0</destination>
<sourcePort>1</sourcePort>
<destinationPort>2</destinationPort>
<rate>4410</rate>
<typeConditioning>false</typeConditioning>
<unitConditioning>false</unitConditioning>
<tag></tag>
</connection>
The first four tags, <name>, <description>, <documentation> and <tag> correspond to ntg_collection::class_name, ntg_collection::description, ntg_collection::documentation and ntg_collection::tag_codes.
Each <object> correspond to a node in the linked list of instances pointed to by ntg_collection::instances. The id attribute of the <object> element corresponds to the ntg_instance:id which is a unique identifier for this instance. The id attribute of the <definition> element corresponds to the ntg_instance:class_id and identifies the module definition for this particular instance. The <name> tag corresponds to the ntg_instance::name field and is a unique string identifier. The optional <tag> element corresponds to ntg_instance::tag_codes. The <state> element holds the current status of the instance at the time the collection was saved. The number of <value> elements enclosed by the <state> tag depends on the number of attributes of the current module. In the library this may be gathered from
ntg_instance instance; instance->i.class->n_attributes;
And the name of the first attribute:
ntg_instance instance; instance->i.class->attributes->name;
The names of the attributes are not serialized, only the value. This is because attribute names and other details are stored in the module's definition (IXD) file. The contents of a given <value> tag corresponds to ntg_attribute::value and the order in which the <value>s are serialized always correspond to ntg_attribute::index for a given attribute. The ntg_attribute::index is equal to the port number.
Each <connection> element wraps information about one connection between two ports of two instances. The nodes within this element corresponds to ntg_instance::connections and ntg_instance::n_connections is equal to the number of <connection> elements in a given collection.
Within the <connection> tag we have <source> and <destination>. These perhaps self explanatory elements holds the ntg_instance::id of the source and destination instances respectively. Though there will always only be one <source> element there may be many <destination> elements. (Each connection may be one-to-one, one-to-many or many-to-one.) In the current version of libIntegra these elements are serialized from the ntg_instance::connections array of arrays. In this 2D array, each element corresponds to a source port. This can be used to access a corresponding array of destination ports. For example, the following code shows the 3rd source port's 2nd destination port may be accessed:
instance->connections[3][2]->module_id; instance->connections[3][2]->port_id;
This gives the instance and port id respectively of the second destination port to which the third port of instance is connected. These are the values that gets serialized to
<destinations>'module_id'</destinations>
<destinationPort>'port_id'</destinationPort>
</connection>
However, in the upcoming version of libIntegra a new ntg_connection_ data struct has been created in order to more fully comply with the CollectionSchema:
typedef struct ntg_connection_ { ntg_id id; /**< Connection id */ char * name; /**< Connection name */ ntg_port *source; /**< Source port */ ntg_port *destinations; /**< Array of destination ports */ int rate; /**< The rate for this connection */ char *script; /**< String holding the name of a Lua script to be executed inthe connection */ bool *unit_conditioning; /**< Array of flags to determine whether or not conditioning is to be applied. */ bool *type_conditioning; /**< Array of flags to determine whether or not conditioning is to be applied. */ int *tag_codes; } ntg_connection;
In this new version the mapping is as follows (con is of type struct ntg_connection_):
| CollectionSchema | ntg_connection |
|---|---|
<connection id= /> | con→id |
<name/> | con→name |
<source/> | con→source→module_id |
<destination/> | con→destinations[n]→module_id |
<sourcePort/> | con→source→port_id |
<destinationPort/> | con→destinations[n]→port_id |
<rate/> | con→rate |
<typeConditioning/> | con→type_conditioning |
<unitConditioning/> | con→unit_conditioning |
<tags/> | con→tag_codes |
There will be as many <destination/> and <destinationPort/> elements as there are subscribed receiver ports on this source. Likewise is the array of tag_codes in ntg_connection::tag_codes expanded in to as many <tag> elements as there are tags for this connection.
Collection XML Schema to DataBase Schema correspondance
To be continued…