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.
ThreadLink
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", ":doc\0=Example port\0", [](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" (float) with one mapped property which defines the doc string "Example port" and a callback which is called at dispatch time. The concise way is to state that port maps float messages to "a_port" to the given function.
The justification for the use of std::function rather than just plain old function is due to the recent addition of C\++11 lambda functions. Let’s look at how this can be used with classes:
#include <rtosc/ports.h>
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).
One option is to use the included syntax sugar which simplifies the port definitions massively.
#include <rtosc/ports.h>
#include <rtosc/port-sugar.h>
class Foobar
{
float a,b,c,d;
};
#define rObject Foobar
Ports ports = {
rParamF(a, "doc str"),
rParamF(b, "doc str"),
rParamF(c, "doc str"),
rParamF(d, "doc str"),
};
There, that is a concise representation of those parameters. This can be further complicated by adding multiple layers to the process of dispatching an event.
#include <rtosc/ports.h>
#include <rtosc/port-sugar.h>
class Barfoo
{
float e;
static Ports ports;
};
class Foobar
{
float a,b,c,d;
Barfoo baz;
};
#define rObject Barfoo
Ports Barfoo::ports = {
rParamF(e, "doc str"),
};
#undef rObject
#define rObject Foobar
Ports Foobar::ports = {
rParamF(a, "doc str"),
rParamF(b, "doc str"),
rParamF(c, "doc str"),
rParamF(d, "doc str"),
rRecur(baz, "doc str"),
};
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).
While the default syntax sugar might not quite work out, it is possible to define any other method of generating ports. Notably some C++ templates might help with the definitions though the results will likely not be as concise as the provided macros.
Finally as this system is designed to facilitate static inspection of the parameter trees, it is very easy to add metadata to the various parameters. Consider defining a min and max value for a parameter with an associated midi mapping. This can be done with several levels of verbosity:
Ports Foobar::ports = { rParamF(a, ":scale\0=linear\0:min\0=1\0:max\0=15.2\0", "a verbose port"), rParamF(b, rMap(scale,linear), rMap(min, 0), rMap(max, 15.2), "a macro mapped port"), rParamF(c, rLinear(0,15.2), "a concise port"), };
Based upon this basic decomposition it should not be difficult to see how similar macros could be constructed to define port metadata which can be used within the callback or anything that might want to reflect on the ports.
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:
property_start := ':' value_start := '=' text := [^\0] entry := | ':' text '\0' | ':' text '\0' '=' text '\0' metadata := | entry | entry metadata
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 extensions 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.
MidiTable
As this system is designed to be fairly lightweight and the ports expose plenty of metadata about what arguments are accepted by each port, a logical next step is to enable some form of midi integration. The miditable is designed to allow for midi learning and general midi mapping of controllers. All this really amounts to is mapping a <controller id, channel id> to <path, type, conversion-function?>.
SubTree-Serialization
While loading new modules without interrupting the realtime thread is easy enough through techniques like pointer swaps, saving a running set of parameters can be trickier. Through some port reflection and a lot of c string manipulation it is possible to serialize arbitrary subtrees of the graph formed by the rtosc::Ports structures. This currently transforms a readable set of ports into a bundle which contains all of the values needed to restore the state of the underlying structures. As with most things with this library, this feature is still experimental.