General design

introduction

Rubyk is a tool to process signals. It is made of two different agents:

  1. the planets process the signals
  2. the satellites observe and alter the planets

The planets handle all the heavy computation and works in real time. Their worlds are made of many nodes connected together through links coming in and out of the nodes through slots. These slots are called outlets when they correspond to an outgoing signal and inlets for incoming signals. There is also a special outgoing slot called notifier that is used to notify observers of the node’s state changes.

The satellites correspond to user interfaces used to observe the data flow and alter the nodes and connections. All planets and satellites can live on different machines, networks and hardware.

There can be many satellites observing and altering the same planets.

intro

communication

This is a draft… It will need a rewrite after zeroconf implementation.

For the moment, the basic idea is that a satellite registers through

/reply_to host,port

And the answers to all queries are sent to all satellites (possibly through multicast).

function calls (node ⇒ node)

Communication between nodes on the same planet is done through direct function calls by slots, either using the virtual method bang for first inlet or a function pointer (functor) for other inlets. Signals are passed by reference.

trigger osc (node ⇒ /planet/node)

Communication between nodes from different planets is done by sending open sound control messages through UDP packets. These packets land into the osc port of the server with the same priority as observers (low).

debugging osc (node ⇒ /satellite)

Signal spying is implemented in special nodes that connect to outlets and send all the data through UDP directly to the observer. With every packet, the planet sends the current TTL (time to live) of the spy node. If the app does not update the TTL by sending the observe command before it reaches zero, the spy node is deleted.

planets

minimal planets

Planets contain nodes connected together to process signal. They must be able to respond to node queries (node listing, node method listing) :

A very simple planet could abstract all it’s processing through a unique node:

pluto
/pluto/mic/amp
/pluto/mic/amp 0.88
/pluto/mic/out/audio/link /venus/amp/in/sig1
/pluto/mic/notify /me

If a planet can instantiate new nodes, it should contain the “rubyk” meta node:

/venus/rubyk                               -- get rubyk capabilities
/venus/rubyk/class/Metro/new 'met'         -- create Metro instance
/venus/rubyk/free 'met'                    -- delete node

links

Links between nodes are created/destroyed by the nodes themselves:

/venus/met/out                              -- get all outlets
/venus/met/out/tic                          -- get all links
/venus/met/out/tic/link /venus/met/bang     -- link
/venus/met/out/tic/unlink *                 -- remove all links

functionality discovery

Discovery of planets is done via ZeroConf (see avahi).

In order to discover what a planet contains the satellite sends messages ending with a / (slash). For example, this could be the queries used to discover a planet called “venus” containing a metronome called “metro” and a counter called “c” :

1. What does planet venus contain ?

query : /#list
answer: rubyk/,metro/,c/

Since the responses end with ”/”, they are all osc containers.

2. What does object “metro” do ?

query : /venus/metro/#list
answer: class,bang,tempo,start,stop,in/,out/

It contains methods “class”, “bang”, “tempo”, “start” and “stop” and the sub-objects “in/” and “out/”. See how the absence of a trailing slash indicates a method.

3. What is the current tempo ?

query : /venus/metro/tempo
answer: 115

4. Set tempo to 90

query : /venus/metro/tempo 90
answer: 90

5. set a wrong value

query : /venus/metro/tempo -100
answer: 90

The reply sends the value of the parameter after the action (unchanged in case of error).

6. get information on the ‘tempo’ value

query : /venus/metro/tempo/#info
answer: "Tempo value in beats per minute. A value of '0' halts the metronome."

classes

All nodes on a planet are subclasses of Node. Even the planet itself is a subclass of Node.

class_hierarchy

See Group for information on how to group objects on a planet.

Any object loaded from the library is instanciated as a Class. This object contains information on the number of outlets/inlets, the class name, the super class.

The Object stores information like:

object

  • object/method name (unique in group scope)
  • list of sub-objects (can be methods)
  • global dictionary of url.hash => object
  • what to do on “trigger” (default implementation = list sub-objects)

Moving an object around is done through “set_parent” or “set_name”.

values

Objects send Values to each other, through outlets and inlets but also by direct calls to accessors (/met/tempo returns a Value for example).

The Value can be either a direct value (double) or a smart pointer to a more complex data container.

Here is a graphical definition of Value:

value

rules

  1. never store data in something that is not a Value.
  2. Value type is an anonymous holder for Matrix,Midi,String, etc.
    Objects of this type cannot be “dereferenced”, you must convert them to a specific type first using “set”.
  3. the type of a value is actually stored in “Data” through a virtual method “data_type()”.

data structure

value_uml

reference counting example

value_abc

Usage example:

  const Value v1(new NumberData(1)); // v1.refCount = 1
  Value v2(new Data());              // v2.refCount = 1
  
  Value v3 = v1;                     // v1.refCount = 2

  v2 = v1;                           // v2 destroyed, v1.refCount = 3
  
  Number n;                          // n.refCount = 1, n is a NilValue

  if (!v3.set(&n)) {                 // v1.refCount = 4, n is now a NumberValue
    return -1;
  }
  
  printf("%i\n",n->value());         // v1.refCount = 4: const accessor "->"
  
  n.mutable_data()->increment();     // v1.refCount = 3, n.refCount = 1 (clone)
  
  if (v1.set(&d)) {                  // type conversion to native types
    printf("%f   ... ok\n", d);      // v1.refCount = 1
  }

signal processing

Life starts in the bang method where all the processing is triggered. During processing, the node can send signals through it’s outlets. It should first send signals with the highest outlet id (right to left output).

Once the processing is done, if the state changes the node should call it’s changed method which registers this node as wanting observer notifications.

At the end of the planet’s processing loop, notifications are sent, eventually from a slow thread. Going through each of the nodes registered as needing observer notification, the notify slot is called. This slot calls the node’s state method to get a dictionary with the node’s attributes and sends the attributes to the observers.

observer connection

When an observer wants to be notified about a node’s state, these are the steps involved:

1. get the node’s class

/venus/met/class     --> Metro

2. get the interface for this node (views/Metro)

3. instantiate the widget with the node’s url

/venus/met

4. interface sends notification need to it’s node depending on its visibility status (it sends a list of attribute keys needed for its state update):

/venus/met/notify /satellite/met "frequency, osc_type, ..."

error handling

When an exceptions is raised, the node becomes freezed (locked). It will no longer process signals. The only way to unlock a node is to initialize it again (send it new parameters).

Exceptions should never occur when receiving signals. Bad/incompatible signals should be simply rejected.