Serial OSC C++ Example
Getting a monome device to communicate with a custom-written native C++ application or VST plugin is actually not as hard as it might sound. This tutorial will show you one way to do it, using only the C++ standard library and some additional open source code. I'm assuming a moderate level of C++ knowledge, as well as a basic understanding of Open Sound Control (OSC) and the Monome serialosc service/daemon application. The tutorial was written on Windows in Visual C++ 2012, but it should be easy to compile on any other major compiler / IDE (XCode, GCC, Clang, etc). Please note that I'm using a lot of C++11 goodies, so make sure your compiler is up to date.
First Steps
First things first, go browse the code at serialosc_example repository.
serialosc_example.cpp houses our main function entry point, so we should start there. First thing you will notice is a class named MonomeDemo, but let's skip over that and check out the actual main function. Most of the code here is actually just used to set up an infinite loop (until 'q' is entered). The main parts of interest are:
SerialOsc osc("test", 13000);
MonomeDemo device(&osc);
The first line here sets up a new SerialOsc object, passing in "test" as the device prefix and 130000 as the starting default port. If you peek into SerialOsc.h, you'll notice the constructor actually takes 3 arguments:
SerialOsc(std::string devicePrefix, int defaultPort, int maxPortsToScan = 1000);
with the third argument being the maximum number of ports to try and use, defaulting to 1000. This just means that if the default port you select is not available, the SerialOsc object will attempt to bind to the next maxPortsToScan ports until one works.
The next line creates our MonomeDemo object, and passes it a pointer to our SerialOsc object in the constructor.
MonomeDemo Class
Let's take a look at our MonomeDemo class, which also lives in serialosc_example.cpp.
This class represents our application itself, which is using the underlying SerialOsc object pointer to communicate with the serialosc server (which itself communicates with our Monome devices). Notice that our class inherits from SerialOsc::Listener. This is a callback class type, and is used for SerialOsc to communicate that some device-specific event has occurred. In this example, that would include the deviceFound(), deviceRemoved() and buttonPressMessageReceived() methods. If you look in these methods, you will see that a MonomeDevice object pointer is passed to the callback method, so that the application code can know which device the event occurred for.
MonomeDevice Class
Speaking of the MonomeDevice class, let's take a peek at that now. If you look at MonomeDevice.h, you will notice it's actually just a POD (Plain Old Data) struct.
struct MonomeDevice
{
std::string id;
std::string type;
std::string prefix;
int port;
int width;
int height;
int rotation;
};
The values stored inside are returned from serialosc when querying for device information, so we should dive into that next.
SerialOsc Class
OK, finally we are at the meat of our little demo, the SerialOsc class. This is the class that actually communicates with the serialosc server and allow us to access our Monome devices.
serialosc uses OSC to for all communications. OSC is an application-level protocol typically built on top of UDP. Think of UDP as TCP's simpler sibling. UDP is a network protocol, and network programming requires us to use Berkley Sockets or WinSock libraries, which is an awful lot of work. Luckily, there are few C and C++ OSC libraries we can use to make our lives simpler. For this example (and all of my own personal C++ application projects) I chose to use oscpack, written by Ross Bencina and distributed under the MIT open source license. If you look in the repository, you'll notice I included the oscpack source code, under the oscpack folder.
In order for us to start listening for incoming OSC messages, we need to create a new UdpListeningReceiveSocket object. The main problem we have here is that this object will block our thread while it waits for incoming messages, so we need to start this guy on it's own thread. Luckily for us, C++ finally has a platform-independent threading library, defined under the <thread>
header. To take advantage of this, we just add the header as well as a std::thread member variable to our class. Then, we set this up in our start() method. This method takes a SerialOsc::Listener object pointer as it's parameter, which you will remember is implemented by our MonomeDemo class in serialosc_example.cpp. Our SerialOsc class can then use this pointer to call back into the main application code to notify it of any device events (NOTE: remember we are listening on another thread, so this callback will happen on that thread. This example code is NOT thread safe by any means, and a proper GUI application will probably need to then post this device message into some sort of queue for the GUI thread to read from and handle).
The next bit of code is our port scanning, as mentioned earlier. We basically just loop from zero to portsToScan, add our default listenPort and hope we can bind to that port (if we can't a std::runtime_error is thrown that we can catch).
One we have our socket, we initialize our thread object with our SerialOsc class' runThread() method, which will automatically start a new thread and call the specified method on that thread.
thread = std::thread(&SerialOsc::runThread, this);
runThread() itself is pretty simple, it just calls Run on our socket.
if (listenSocket != nullptr) {
listenSocket->Run();
}
Once our thread is running, we send 2 commands to SerialOsc to begin our device discovery process. These are sendDeviceQueryMessage() and sendDeviceNotifyMessage().
Sending commands to serialosc
To send messages to serialosc, we can use the oscpack class UdpTransmitSocket. This class represents an outgoing UDP socket connection to some destination server. To use the class, we need to create a byte array buffer, as well as an osc::OutboundPacketStream object. OutboundPacketStream works like most C++ output stream objects, using the left shift operator to push data into the stream.
Once our stream is set up with our message address and data, we can call Send() on our UdpTransmitSocket to send the message out to the server.
Receiving Device Commands From SerialOsc
The real meat of our SerialOsc class is the ProcessMessage() method, which handles any incoming messages from serialosc. Note that this method is BIG! It would be much cleaner to break out the handling of each message type into a separate method, or even another class for message parsing, but I kept it simple here for the sake of readability.
Since we called sendDeviceQueryMessage(), we are expecting serialosc to respond with a message '/serialosc/device' (note that sendDeviceNotifyMessage() will respond with '/serialosc/add' if a new device is added after, and the handling of this is near identical to that of '/serialosc/device'). In our message handler for this address type, our first order of business is to extract the OSC data and see what we get back. That is handled by code that looks like this:
std::string id = (iter++)->AsString();
std::string type = (iter++)->AsString();
int port = (iter++)->AsInt32();
which gives us our device id, device type and the port serialosc assigned to the device.
After that, we check our devices vector member variable to see if we already have this device registered. In this example we are storing the devices in a vector, but another design might store them in a map or hash table with the device id as a key.
Lambda Functions
To search the vector, we use the std::find_if function, which lives in the <algorithm>
header. This function lets us pass in a custom function to filter out the device we are searching for by some criteria. What's nice about C++11 is that we finally have something called lambda functions. Lambda functions are inline unnamed functions that are perfect for things like find_if, since we can specify the search code function in the same place as the code doing the search. In older versions of C++ we would have to create a new function somewhere else to handle this search predicate. The search code in question looks like this:
auto deviceIterator = std::find_if(devices.begin(), devices.end(), [id] (const MonomeDevice* x) { return x->id == id; });
First, note that we are using the 'auto' keyword to tell the compiler "hey, you figure out what the type is on the right of the equals sign". This is nice since C++ STL types can be awfully ugly to type out.
The next part that might look strange is:
[id] (const MonomeDevice* x) {
This is where our inline lambda function is defined. The brackets [] tells the compiler what variables to "capture", to be allowed to be used inside the function. Here we tell it we only care about the id variable, which we want to capture by value (the default in C++). We could also specify [=] to capture EVERYTHING by value, or [&] to capture everything by reference (by "everything", I mean all variables in scope, which are local variables and class member variables in this case).
Back to serialosc Message Parsing
Now that we have our device iterator, we can see if we already know about this device, or if we should add it to our vector. After that, we call into our listener pointer to notify our application that we found a new device. After that, we send a few messages back to serialosc: sendDeviceInfoMessage(port), sendDevicePrefixMessage(port) and sendDevicePortMessage(port). sendDeviceInfoMessage() sends a request to serialosc for additional device information, sendDevicePrefixMessage(port) sends a request to serialosc to update our device prefix to the one defined in our SerialOsc constructor, and sendDevicePortMessage(port) tells serialosc to please start sending device button/tilt/knob/etc messages to our listen port, which is listening on the thread we set up earlier.
When we called sendDeviceInfoMessage(), we are now expecting a series of messages to come back from serialosc. The first will be '/sys/id'. In our handler for this message, we just extract the ID from the message and store it in our currentDeviceID member variable. Next we will receive '/sys/size', '/sys/rotation' and '/sys/prefix' messages (the last one we are ignoring in our demo). We then use the ID we stored in currentDeviceID to know which device these messages are meant for (personally I wish serialosc just included the device id in these messages, but that's not how it works so we have to make due with the currentDeviceID variable instead).
Another interesting message we are handling is '/serialosc/remove'. We receive this message when we unplug a device (and '/serialosc/add' when we plug a new device in). We can use this to update our devices vector. Note that after an add or remove message is sent, we must immediately re-register our application with serialosc to receive more notifications by calling sendDeviceNotifyMessage().
Our final message type we are looking at is our device button press messages. Note the following else block:
else if (address == ("/" + devicePrefix + "/grid/key")) {
Since serialosc will send device messages using the prefix we specified earlier, we need to look for messages coming in on that prefix in the incoming address pattern. Note that we are doing a string concatenation here, for performance reasons it would be better if we calculated this string as soon as we know what the device prefix is.
Sending Device LED Commands
This should be pretty simple to understand now, just take a look at sendDeviceLedCommand() and sendDeviceLedAllCommand(). Our MonomeDemo class does this to echo back key press messages to LED commands, so that pressing a button lights up the LED for that button.
Final Thoughts
This concludes our tutorial. Hopefully by now you can try and compile the code, set some breakpoints and play with the application to see how it all works.
Some things that are missing from this example code that you might want to account for in real production code:
- Multiple prefixes per device
- Cleaner code to parse incoming messages
- Thread-safe device message callbacks
- Additional device LED commands
- Additional device message types (tilt, arc, etc).
Feel free to contact me with questions, comments or to just say 'hi!' at daniels.bytes at gmail dot com. Thanks!
Check out my main blog page at daniel-bytes.github.io