Skip to content

dimitry-ishenko-cpp/liboscpp

Repository files navigation

libosc++ – an OSC Library for C++

The libosc++ library allows one to easily create and parse OSC (Open Sound Control) packets.

OSC is an open, transport-independent, message-based protocol developed for communication between computers and other multimedia devices.

See the Usage section below if you are planning to develop applications with libosc++.

Installation

Binary

Debian/Ubuntu/etc:

$ p=libosc++ v=1.0
$ wget https://github.com/dimitry-ishenko-cpp/liboscpp/releases/download/v${v}/${p}_${v}_amd64.deb
$ sudo apt install ./${p}_${v}_amd64.deb

Install the development package, if you are planning to develop applications with libosc++:

$ p=libosc++-dev v=1.0
$ wget https://github.com/dimitry-ishenko-cpp/liboscpp/releases/download/v${v}/${p}_${v}_amd64.deb
$ sudo apt install ./${p}_${v}_amd64.deb

RaspberryPi:

$ p=libosc++ v=1.0
$ wget https://github.com/dimitry-ishenko-cpp/liboscpp/releases/download/v${v}/${p}_${v}_armhf.deb
$ sudo apt install ./${p}_${v}_armhf.deb

Install the development package, if you are planning to develop applications with libosc++:

$ p=libosc++-dev v=1.0
$ wget https://github.com/dimitry-ishenko-cpp/liboscpp/releases/download/v${v}/${p}_${v}_armhf.deb
$ sudo apt install ./${p}_${v}_armhf.deb

From source

Stable version (requires CMake >= 3.1):

$ p=libosc++ v=1.0
$ wget https://github.com/dimitry-ishenko-cpp/liboscpp/releases/download/v${v}/${p}-${v}.tar.bz2
$ tar xzf v${v}.tar.gz
$ mkdir ${p}-${v}/build
$ cd ${p}-${v}/build
$ cmake ..
$ make
$ sudo make install

Latest master (requires git and CMake >= 3.1):

$ p=liboscpp
$ git clone --recursive https://github.com/dimitry-ishenko-cpp/${p}.git
$ mkdir ${p}/build
$ cd ${p}/build
$ cmake ..
$ make
$ sudo make install

Usage

To use libosc++ in your application simply add:

#include <osc++.hpp>

to your file(s) and link with -losc++.

Description

libosc++ supports all standard OSC data types and most of the non-standard (extended) ones. The following table summarizes which types are supported:

OSC type Implemented as
int32 std::int32_t
float32 float
string std::string
blob std::vector<char>
int64 std::int64_t
time clock::time_point1
double double
char char
true bool
false bool
nil std::nullptr_t
inf inf_t

1 using clock = std::chrono::system_clock.

All supported data types are encapsulated in the osc::value variant class.

osc::value can be implicitly instantiated from any of the supported types, and has is_...() and to_...() series of functions to query and extract stored value. For example:

osc::value value{ 42 };
if(value.is_int32()) auto n{ value.to_int32() };
osc::value value{ "The ultimate answer" };
if(value.is_string()) accept(value.to_string());

OSC Packets

OSC data is transmitted in packets. An application transmitting the packets is called OSC client, and application receiving the packets is called OSC server.

An OSC packet is either an OSC message or an OSC bundle.

Within the library packets are represented by the osc::packet class, which is a storage container (not entirely unlike std::vector) and provides the following functions:

Function Description
packet{ } Construct empty packet.
packet{ size } Construct packet of size bytes.
packet{ data, size } Construct packet and copy size bytes from data.
packet{ begin, end } Construct packet and copy data from [begin, end).
data() Access packet data.
size() Get packet size.
resize(size) Resize packet.
clear() Discard packet data and resize to 0 bytes.

Example of sending a packet using the asio library:

asio::ip::udp::socket socket{ io };
asio::ip::udp::endpoint remote{ ... };
osc::packet packet{ ... };
...
socket.send_to(asio::buffer(packet.data(), packet.size()), remote);

Example of receiving a packet:

asio::ip::udp::socket socket{ io };
...
socket.async_wait(asio::ip::udp::socket::wait_read, [=](const asio::error_code& ec)
{
    osc::packet packet{ static_cast<osc::int32>(socket.available()) };
    asio::ip::udp::endpoint remote;

    socket.receive_from(asio::buffer(packet.data(), packet.size()), remote);
    ...
});

As mentioned earlier, a packet is either a message or a bundle. Data stored in the packet can be extracted with the parse() function, which returns an instance of osc::element.

osc::element is a variant class encapsulating osc::message and osc::bundle, and has is_message()/is_bundle() and to_message()/to_bundle() functions to query and extract stored data. For example:

auto element{ packet.parse() };
if(element.is_bundle())
{
    auto bundle = element.to_bundle();
    ...
}
else if(element.is_message())
{
    auto message = element.to_message();
    ...
}

OSC Message

OSC message -- represented by the osc::message class -- consists of an address pattern followed by zero or more arguments, which are instances of osc::value.

The address pattern starts with / (forward slash) and contains one or more nodes separated by /. The pattern can be thought of as a path in a tree-like structure with intermediate nodes called OSC containers and leaf nodes called OSC methods.

As such, OSC message can be considered to be a request to execute given method with certain arguments.

osc::message provides the following functions:

Function Description
message{ address } Construct message with address.
address() Get message address.
values() Access message values (returns std::deque<osc::value>).
value(n) Access n-th value.
operator<<(value) Add value to the message.
operator>>(T&) Extract value from the message.
to_packet() Construct packet from the message.

Examples of constructing a message:

osc::message message{ "/foo/bar/baz" };
message << "So long and thanks for all the fish." << 42;
message << osc::blob{ 100, 'X' };
using namespace osc::literals; // pull in osc::nil and osc::inf
osc::message message{ "/foo/bar/qux" };
message << true << false << nil << inf << osc::clock::now();

OSC Bundle

OSC bundle -- represented by the osc::bundle class -- consists of the #bundle keyword, followed by a time tag and zero or more elements.

The time tag indicates when the bundle is to be executed. If the time tag is in the past or the bundle was constructed with osc::immed time tag (the default), the bundle is to be executed immediately upon receipt.

Bundle elements are instances of osc::element and can themselves be bundles. In other words, bundles can contain other bundles (Inception-style).

osc::bundle provides the following functions:

Function Description
bundle{ time = immed } Construct bundle with tag time.
time() Get bundle time.
elements() Access bundle elements (returns std::deque<osc::element>).
element(n) Access n-th element.
operator<<(element) Add element to the bundle.
operator>>(message&) Extract message from the bundle.
operator>>(bundle&) Extract bundle from the bundle.
to_packet() Create packet from the bundle.

Examples of constructing a bundle:

osc::bundle bundle; // execute immediately
osc::message message{ "/path/to/method" };
message << 1 << 2 << 3.1415926535;
bundle << message;
osc::bundle bundle{ osc::clock::now() + 10min }; // execute 10 min from now
bundle << (osc::message{ "/foo/bar" } << 123 << 987);
bundle << (osc::message{ "/foo/baz" } << "Hello world");

Bundle within a bundle example:

osc::bundle bundle{ osc::clock::now() };
bundle << (osc::message{ "/do/it/now" } << 1 << 2 << 3)         // execute "now"
       << (osc::bundle{ bundle.time() + 1h }
           << (osc::message{ "/do/it/later" } << 4 << 5 << 6)); // execute in 1hr

OSC Client

OSC client -- the one transmitting the packets -- will usually:

  • create a message and/or bundle;
  • construct a packet from it;
  • send it out (by means of another library).

Transmitting example:

#include <asio.hpp>
#include <osc++.hpp>
using namespace asio::ip::udp;
...
asio::io_context io;
udp::endpoint remote{ ... };
udp::socket socket{ io };

socket.open(udp::v4());

osc::message message{ "/abc/def/ghi" };
message << 123 << 456 << 789;

auto packet = message.to_packet();
socket.send_to(asio::buffer(packet.data(), packet.size()), remote);

osc::bundle bundle;
bundle << (osc::message{ "/a/b/c" } << 1 << 2 << 3)
       << (osc::message{ "/1/2/3" } << 'a' << 'b' << 'c' );

auto packet_2 = bundle.to_packet();
socket.send_to(asio::buffer(packet_2.data(), packet_2.size()), remote);

OSC Server

OSC server -- the one receiving the packets -- usually will:

  • receive a packet;
  • parse it to get an instance of osc::element;
  • check if this instance is a message or a bundle;
  • if bundle, recurse into it and check each of its elements;
  • if message, "execute" it.

The parse() function may throw one of the following exceptions (derived from std::invalid_argument):

Exception
osc::invalid_value
osc::invalid_message
osc::invalid_element
osc::invalid_bundle
osc::invalid_packet

Receiving example:

#include <asio.hpp>
#include <osc++.hpp>
using namespace asio::ip::udp;

asio::io_context io;
udp::endpoint local{ ... };
udp::socket socket{ io };

socket.open(udp::v4());
socket.bind(local);

socket.async_wait(udp::socket::wait_read, [=](const asio::error_code& ec)
{
    if(!ec)
    {
        osc::packet packet{ static_cast<osc::int32>(socket.available()) };
        udp::endpoint remote;
        socket.receive_from(asio::buffer(packet.data(), packet.size()), remote);

        try
        {
            auto element{ packet.parse() };
            if(element.is_message())
            {
                auto message{ bundle.to_message() };
                execute(message);
            }
            else if(element.is_bundle())
            {
                auto bundle{ element.to_bundle() };
                recurse_into(bundle);
            }
        }
        catch(std::invalid_argument& e)
        {
            std::cerr << e.what() << std::endl;
        }
        ...
    }
});

To help with processing of received packets, osc::bundle implements the extraction operator (>>) as well as the elements().are<...>() function template to check types of its elements.

Let's say we want to implement an OSC server that receives bundles, which contain a message and another bundle with two more messages inside. This can be implemented in the following (boring) way:

osc::packet packet{ ... };
auto bundle{ packet.parse().to_bundle() };

auto message{ bundle.element(0).to_message() };
auto bundle_2{ bundle.element(1).to_bundle() };

auto message_2{ bundle_2.element(0).to_message() };
auto message_3{ bundle_2.element(1).to_message() };

The above example uses the element() function and relies on exceptions. We could instead use operator>>:

osc::packet packet{ ... };
auto element{ packet.parse() };

osc::bundle bundle, bundle_2;
osc::message message, message_2, message_3;

element >> bundle;
bundle >> message >> bundle_2;
bundle_2 >> message_2 >> message_3;

We can also use elements().are<...>() to check bundle signature and avoid exceptions:

osc::packet packet{ ... };
auto element{ packet.parse() }; // NB: may throw

osc::bundle bundle, bundle_2;
osc::message message, message_2, message_3;

if(element.is_bundle())
{
    element >> bundle;
    if(bundle.elements().are<osc::message, osc::bundle>())
    {
        bundle >> message >> bundle_2;
        if(bundle_2.elements().are<osc::message, osc::message>())
        {
            bundle_2 >> message_2 >> message_3;
            // look ma, no exceptions
            ...
        }
    }
}

Likewise, osc::message implements operator>> and values().are<...>() to help check and extract its values:

osc::message message{ "x/y/z" };
message << "The ultimate answer" << 42 << 2.71828 << osc::clock::now();

if(message.values().are<std::string, int, double, osc::time>())
{
    std::string v1; int v2; double v3; osc::time v4;
    message >> v1 >> v2 >> v3 >> v4;
}

Dispatching

libosc++ provides OSC message dispatching facility described in the OSC specification. Using this facility an OSC server simply needs to:

Contrary to the OSC specification though, libosc++ uses regex for pattern matching within the address space.

Example:

double x, y, z;

void set_coords(osc::message message) // callback
{
    if(message.values().are<double, double, double>()) message >> x >> y >> z;
}

void set_coord(const osc::message& message) // callback
{
    if(message.values().are<char, double>())
    {
        auto coord = message.value(1).to_double();
        switch(message.value(0).to_char())
        {
        case 'x': x = coord; break;
        case 'y': y = coord; break;
        case 'z': z = coord; break;
        }
    }
}

void print_state(const osc::message& message)
{
    std::cout << "state after " << message.address()
              << ": " << x << " " << y << " " << z << std::endl;
}

...

osc::address_space space;
space.add("/set/coords", &set_coords);
space.add("/set/coord", &set_coord);

#ifndef NDEBUG
space.add("/set/.*", &print_state);
#endif

space.dispatch(osc::message{ "/set/coords" } << 1.2 << 3.4 << 5.6);

space.dispatch(osc::bundle{ }
    << (osc::message{ "/set/coord" } << 'x' << 7.8)
    << (osc::message{ "/set/coord" } << 'y' << 9.0)
);

space.dispatch(osc::message{ "/set/speed" } << 69);

Output:

state after /set/coords: 1.2 3.4 5.6
state after /set/coord: 7.8 3.4 5.6
state after /set/coord: 7.8 9 5.6
state after /set/speed: 7.8 9 5.6

NB: Since the extraction operators (>>) modify the message, set_coords() above takes osc::message by value, which involves copying. On the other hand, set_coord() only needs const ref as it uses the value() function to access values without extracting them.

osc::address_space provides the following functions:

Function Description
address_space{ sched = call_immed{ } } Construct empty address space.2
add(pattern, callback) Add callback function matching pattern.
dispatch(element) Dispatch received element.

2 libosc++ relies on a user-provided sched function to schedule callbacks for future execution. The library itself includes default implementation osc::call_immed, which disregards the time tag and executes the callback immediately.

Authors

  • Dimitry Ishenko - dimitry (dot) ishenko (at) (gee) mail (dot) com

License

This project is distributed under the GNU GPL license. See the LICENSE.md file for details.