Web Log
▾ RtOSC
  »Home
  »User Guide
  »API Docs
  »Tests
  »Support
SFPV
RtOSC
Realtime Open Sound Control

RT OSC a library for simply using Open Sound Control messages in a realtime context.

The C Layer

For basic use of OSC messages, there is very little reason to complicate things more than needed. As such each message is simply stored in a buffer and assumed to be contiguous and stored properly. All manipulations of the OSC packets can be done with fixed sized buffers in a real time environment.

The simplest and most useful function is rtosc_message(), which allows for the generation of OSC messages.

char buffer[64];
int len = rtosc_message(buffer, sizeof(buffer), "hello", "s", "world");

In this example len now contains the length of the message and buffer contains a well formed OSC message (or at minimum one that is self consistent within this library). To see the message, we can print the buffer and the zeroth argument:

printf("%s %s!\n", buffer, rtosc_argument(buffer,0).s);
//hello world!

As the OSC message consists of null delimited path, argument string, and arguments, the path can be found at the start of the buffer and the argument can be fetched to get the fields.

Other properties of this message can be found with library calls on the message buffer.

rtosc_narguments(buffer);
//1
rtosc_argument_string(buffer);
//"s"
rtosc_type(buffer, 0);
//'s'
rtosc_message_length(buffer);
//same as len from above

While this is a fairly simple interface, which may appear restrictive, this library’s goal is to permit the simple use of basic RT OSC messages, nothing too much more, so for more complex construction of messages, I recommend liblo. This is not to say that all that all features are currently implemented.

For more complex messages there are also varargs and array versions of message

rtosc_vmessage(buffer, sizeof(buffer), address, args, va_arguments);

rtosc_arg_t args[] = {
  {.s = "foo"},
  {.i = 1234},
  {.f = 1024.2}
}
rtosc_amessage(buffer, sizeof(buffer), "/path", "sif", args);

The C++ Layer

Once you have an OSC message, that is nice, but not terribly useful. As the primary motivation for this project is to ease the use of OSC for RT audio, this layer provides several key features:

  • A Thread Link, which makes transmitting messages over jack ringbuffers simple.

  • An implementation of Ports for dispatching OSC messages to their destinations.

  • A trivial midi lookup table for use with the Ports implementation

As this library is based upon the concept of using fixed sized buffers to avoid memory allocation, and no size universally works, these classes are templates with respect to their size.

For simple usage, calls to write() replace any calls to rtosc_message(). One thread is intended to call write() when messages are sent and the other is expected to periodically read() all of the messages.

ThreadLink link(1024,128);
link.write("hello", "s", "world");
link.hasNext(); //true
link.read(); //yields the hello:s:world message

Ports

Defining all of the possible ways a message can be sent to various parts of an audio application is next to impossible, so the implementation of Ports result in a description of OSC methods handled by various patterns. With trivial programs, one might want to establish one table of ports to describe all of the possible connections, but this is not feasible for moderately sized to large sized applications. As such each set of ports defines one layer of a tree of ports statically. As all of the data presented via the Port interface can be statically specified, this means that the tree can easily be used as a read only data structure by both the frontend and RT backend.

The Port class defines one port which works on a OSC message and value. Below is a simple example port.

Port port("a_port:f", "::Example port", [](const char*, RtData){puts("port called")});

The verbose way to read this is that this defines a port named "a_port" which accepts messages of type "f" with metadata of "::Example port" and a function that acts on the message and some additional data. The concise way is to state that port maps float messages to "a_port" to the given function.

The justification for the templates and use of std::function rather than just void* and function pointers comes in when dealing with classes. Let’s look at how this can be used with classes:

class Foobar
{
    float a,b,c,d;
}

Ports ports = {
    {"a:f", "", NULL,
        [](const char *m, void *f){((Foobar*)f)->a = argument(m,0).f;}),
    {"b:f", "", NULL,
        [](const char *m, void *f){((Foobar*)f)->b = argument(m,0).f;}),
    {"c:f", "", NULL,
        [](const char *m, void *f){((Foobar*)f)->c = argument(m,0).f;}),
    {"d:f", "", NULL
        [](const char *m, void *f){((Foobar*)f)->d = argument(m,0).f;})
};

This is however quite verbose mainly due to the associated setter functions. As this field is a std::function and not just a simple function pointer it is possible to abstract this with a generated function (or a macro, though generated functions lead to more interesting possibilities).

class Foobar
{
    float a,b,c,d;
};

template<class T>
function<void(const char*,void*)> parameter(float T::*p)
{
    return [p](const char *, void*t){((T*)t)->*p) = argument(m,0).f};
}

Ports ports = {
    {"a:f", "", NULL, parameter(&Foobar::a)),
    {"b:f", "", NULL, parameter(&Foobar::b)),
    {"c:f", "", NULL, parameter(&Foobar::c)),
    {"d:f", "", NULL, parameter(&Foobar::d))
};

There, that is a concise representation of those parameters. It does however make use of a little used C++ feature, pointers to member data. This can be further complicated by adding multiple layers to the process of dispatching an event.

typedef const char *msg_t;

template<class T>
function<void(const char*,T*)> param(float T::*p)
{
    return [p](const char *, void*t){(((T*)t)->*p) = argument(m,0).f};
}

msg_t snip(msg_t m)
{
    while(*m && *m!='/')++m;
    return *m?m+1:m;
}

template<class T, class TT>
std::function<void(msg_t,T*)> recur(TT T::*p)
{
    return [p](msg_t m, void*t){TT::ports.dispatch(snip(m),(T*)(t->*p));};
}

class Barfoo
{
    float e;
    static Ports ports;
};

class Foobar
{
    float a,b,c,d;
    Barfoo baz;
};

Ports Barfoo::ports = {
    {"e:f", "", NULL, param(&Barfoo::e)}
};

Ports Foobar::ports = {
    {"a:f",  "", NULL,           param(&Foobar::a)},
    {"b:f",  "", NULL,           param(&Foobar::b)},
    {"c:f",  "", NULL,           param(&Foobar::c)},
    {"d:f",  "", NULL,           param(&Foobar::d)},
    {"baz/", "", &Barfoo::ports, recur(&Foobar::baz)}
};

Now a recursive dispatching hierarchy has been formed with only a few more lines. While this may appear somewhat complicated, all it does is define another function that handles any object that can dispatch messages. The snip() in this context trims off the earlier part of the OSC message, which (assuming alignment is known) causes no issues whatsoever when decoding it. Depending upon the topology of the system being described, this can simplify things immensely.

Lastly, it should be noted that &Barfoo::ports is stored in the port table. This permits traversal of the tree of ports with this hierarchy. As this tree does not reveal any actual data, the real data can easily be a subset of the ports listed in these structures. Speaking of the metadata, it should be noted that it is empty in all of the above examples, but it is a great space to provide information on the parameters themselves in some ad-hoc manner. It is a great place to put parameter scaling/range information, which is used by the MidiTable in one example and it can contain other properties of the ports, such as descriptions, units, visibility, and so forth.

For more detail on the exact methods you should see the doxygen output, or for the moment the source itself (it doesn’t bite much).

For those who’s eyes glazed over after looking at this templated C++ code, it is also possible to do the same thing with macros in a very concise manner:

#define PARAM(Klass, var) \
{ #var ":f", "", NULL,    \
  [](const char *m, void *v){((Klass*)v)->var = rtosc_argument(m,0).f;}}

#define RECUR(src, dest, var) \
{ #var "/", "", dest::ports,  \
  [](const char *m, void *v){dest::ports.dispatch(snip(m),((src*)v)->var);}}

Ports Barfoo::ports = {
    PARAM(Barfoo,e)
};

Ports Foobar::ports = {
    PARAM(Foobar, a),
    PARAM(Foobar, b),
    PARAM(Foobar, c),
    PARAM(Foobar, d),
    RECUR(Foobar, Barfoo, baz)
};

You can of course combine these approaches to get the desired input as you like, though as only constant character pointers are stored for the port pattern and metadata, generating those requires either static strings or external memory management. While placing closures over pointers-to-member data in a templated tree is not trivial nor strait forward, it is a (somewhat) concise, performant, and safe way of describing the dispatching hierarchy. While this could be done without the used C++11 magic, the application domain is C++ on recent systems and the generated dynamic library is not tainted by any C++ code so this layer as previously mentioned is entirely optional.

Path Specifiers

The rough specification for the grammar of the path specifiers is:

argument_specializer_delimiter := ':'
range_specifier   := '#'
subport_specifier := '/'

path      := location subport
           | location subport arguments

subport   :=
           | '/'

location  := text
           | text '#' number

arguments :=
           | ':' types arguments

types     :=
           | type types

A brief justification of this grammar can be summarized in a few points which echo the sentiment make 99% of code simple and the 1% possible:

  • Array fields are common in signal processing and the # specifier is a simple means of explaining the ranges

  • While paths could be denoted outside of this string, it is concise to label them with / which is disallowed from the subpaths from the OSC 1.0 spec

  • Type checking arguments is a PITA that can be reduced by formally specifying all possible argument types. This also provides information when the tree is statically traversed

  • All other edge cases can be either explained in the metadata or via the behavior of the handling function

Metadata

Looking at all of this you may notice that the metadata field of the port is left blank every time. This field is not shown as it would complicate the above examples and it is only really useful when attaching other code to this idiom of a series of ports. The roughly established grammar for this specifier is:

metadata_delimiter := ':'
field_delimiter    := ','

scaling_fn  :=
             | 'log' ',' number ',' number
             | 'lin' ',' number ',' number
             | text

fields      := text
             | text ',' fields

property    := `'` text `'`

description := text

properties  :=
             | property
             | property ',' properties

metadata    :=
             | scaling_fn ':' properties ':' description

Most of this data has been structured s.t. it is easy to allow some UI to hook onto the data to eliminate some data redundancy and to permit documenting parameters where they are defined rather than in the mist of some horribly convoluted UI code. This field does not necessarily need to conform to the above structure, though port based extentionds will expect the strings to roughly conform and all examples should conform.

For an example of code using this format, see the Fl_Osc_* collection of widgets and meta-widgets in the complex example.

Midi

The midi class just provides simple mapping of midi controls to Ports. All this does at the moment is provide a path and conversion string for registered ports.

While this functionality is currently still under development, it is intended that this object will serve as the mechanism for all midi connections, midi learning, and midi translation. By this, I mean that when midi events are received (at least CC events), they should be sent directly to the midi table which will translate them into OSC events if they are known events and store them if they are unknown events. With a small amount of external code, it should be possible to also perform midi learning by pairing unknown messages and unknown MIDI CC events.