This section assumes that you have already installed and setup:
- Trackmate Tracker, or the LusidOSC Simulator
- Processing
- LusidOSC Processing Bundle, with the .tar file uncompressed
1. Understanding how LusidOSC works
Before you start coding, it's important to understands the basics of how data is sent from Trackmate to your application.
Instead of hard-coding specific function calls into the Tracker's code to interact with an application, data is simply streamed over a network connection (which can be on the same machine, or across the internet to somewhere remote) to any application listening for the the incoming information. All data, such as object position, rotation, ID, type, color, etc. is wrapped in a protocol (an agreed upon ordering and structure for the data), LusidOSC.
LusidOSC connects spatial interfaces with user-level applications via a simple, extendable protocol over a local or remote socket connection. With the help of a library, a few lines of code can be used to identify objects, examine relationships between them, and build up interesting interaction techniques. The details of the protocol (which will not be discussed here) can be found in the LusidOSC Specification v1.0 document.
Make sure to check out all of the examples in the LusidOSC Processing Bundle (and the application explanations/screenshots here) for help with how to explore the wide range of possibilities using processing.
Okay, now let's dive into the code!
2. Using the LusidOSC Processing library
In Processing, received LusidOSC data can be easily accessed and manipulated via a library using the following steps:
- include the library files, lusidOSC.jar and javaosc.jar, in your sketch's code folder (create the folder if it doesn't already exist). The .jar files can be found in any of the example applications' code folders.
- include import lusidOSC.*; to the top of your sketch.
- initialize the library by creating a new instance of LusidClient in your project's setup method.
- include callback methods to be called when data is received.
// we need to import the LusidOSC library and declare a LusidClient variable
import lusidOSC.*;
LusidClient lusidClient;
// setup: this gets called once when the application starts.
void setup() {
// setup the processing display window size and type
size(320,240);
// Create an instance of the LusidClient. The LusidClient expects
// an implementation of the 3 LusidOSC callback methods (see below).
lusidClient = new LusidClient(this);
}
// -------------------------------------------------------------------
// these methods are called whenever a LusidOSC event occurs.
// -------------------------------------------------------------------
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
}
// called when an object is removed from the scene void removeLusidObject(LusidObject lObj) {
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
}
import lusidOSC.*;
LusidClient lusidClient;
// setup: this gets called once when the application starts.
void setup() {
// setup the processing display window size and type
size(320,240);
// Create an instance of the LusidClient. The LusidClient expects
// an implementation of the 3 LusidOSC callback methods (see below).
lusidClient = new LusidClient(this);
}
// -------------------------------------------------------------------
// these methods are called whenever a LusidOSC event occurs.
// -------------------------------------------------------------------
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
}
// called when an object is removed from the scene void removeLusidObject(LusidObject lObj) {
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
}
3. Ways to get Trackate tag data in Processing
There are two basic ways to get LusidOSC object data via the Processing library: push and pull.
Push
One way to use incoming object data is to be pushed, or notified via a callback, when information is received. This strategy is convenient when you are waiting for particular information; instead of constantly checking for new data (see the Pull method below), your application will remain idle until LusidOSC data is received. However, pushing is not always better as your application may get flooded if, for example, you perform an action on each tag and 50 new tags are added all at once - It really depends on what you want to do with the data.
The callbacks are automatically called each time appropriate data is received. To use them, simply add code to the appropriate methods.
// -------------------------------------------------------------------
// these methods are called whenever a LusidOSC event occurs.
// -------------------------------------------------------------------
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
// Print out lots of info when an object is added.
println("add object: "+lObj.getUniqueID());
println(" location = ("+lObj.getX()+","+lObj.getY()+","+lObj.getZ()+")");
println(" rotation = ("+lObj.getRotX()+","+lObj.getRotY()+","+lObj.getRotZ()+")");
println(" data = ("+lObj.getEncoding()+","+lObj.getData()+")");
}
// called when an object is removed from the scene void removeLusidObject(LusidObject lObj) {
// Print out a notification that object has been removed.
println("remove object: "+lObj.getUniqueID());
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
// Print out the ID of each updated object (lots of them!).
println("update object: "+lObj.getUniqueID());
}
// these methods are called whenever a LusidOSC event occurs.
// -------------------------------------------------------------------
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
// Print out lots of info when an object is added.
println("add object: "+lObj.getUniqueID());
println(" location = ("+lObj.getX()+","+lObj.getY()+","+lObj.getZ()+")");
println(" rotation = ("+lObj.getRotX()+","+lObj.getRotY()+","+lObj.getRotZ()+")");
println(" data = ("+lObj.getEncoding()+","+lObj.getData()+")");
}
// called when an object is removed from the scene void removeLusidObject(LusidObject lObj) {
// Print out a notification that object has been removed.
println("remove object: "+lObj.getUniqueID());
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
// Print out the ID of each updated object (lots of them!).
println("update object: "+lObj.getUniqueID());
}
Pull
Sometimes you don't want to be bothered by large streams of incoming data, and instead, just want to have access to object information when requested. Choosing to pull data (often implemented as polling, or sending requests) gives you the flexibility to see a list of the objects currently present, along with their corresponding properties, and act accordingly.
A simple example is a program that displays dots on the screen corresponding to the location of each object. By requesting data via getLusidObjects() in the draw() method (a special method in Processing that is called every frame; usually 15-30 times a second, but you can set it to whatever you like), a list of the current objects can be accessed and used.
// draw: gets called every frame (~30 frames per second default)
// here, we retrieve an array of LusidObject from the LusidClient and
// then loop over both lists to draw the graphical feedback.
void draw() {
// clear the background to white.
background(255, 255, 255);
// set the fill color
fill(0, 0, 0);
// get the list of all objects that are currently present
LusidObject[] lusidObjectList = lusidClient.getLusidObjects();
for (int i=0;i
LusidObject lObj = lusidObjectList[i];
// shift the X and Y so they are centered on the screen.
int x = width/2 + lObj.getX();
int y = height/2 - lObj.getY();
// now draw each object to the screen as a rectangle!
rect(x, y, 10, 10);
}
}
// here, we retrieve an array of LusidObject from the LusidClient and
// then loop over both lists to draw the graphical feedback.
void draw() {
// clear the background to white.
background(255, 255, 255);
// set the fill color
fill(0, 0, 0);
// get the list of all objects that are currently present
LusidObject[] lusidObjectList = lusidClient.getLusidObjects();
for (int i=0;i
// shift the X and Y so they are centered on the screen.
int x = width/2 + lObj.getX();
int y = height/2 - lObj.getY();
// now draw each object to the screen as a rectangle!
rect(x, y, 10, 10);
}
}
Another way to pull is to request a particular object by ID using the getLusidObject(uniqueID) method. Be careful, though, as it may return null if the object is not present.
// set the background to white if our specific object is present,
// otherwise make it black to indicate that we don't see it.
void draw(){
// try to get the object if it is present...
LusidObject lObj = lusidClient.getLusidObject("0x08D03B05BA29");
if(lObj != null){
println("Object is present! -- x:"+lObj.getX()+", y:"+lObj.getY());
background(255,255,255);
}else{
println("Object was not found.");
background(0,0,0);
}
}
// otherwise make it black to indicate that we don't see it.
void draw(){
// try to get the object if it is present...
LusidObject lObj = lusidClient.getLusidObject("0x08D03B05BA29");
if(lObj != null){
println("Object is present! -- x:"+lObj.getX()+", y:"+lObj.getY());
background(255,255,255);
}else{
println("Object was not found.");
background(0,0,0);
}
}
4. Quick interaction examples
Here are just a few of the ways you can map object properties to user interaction in an application.
Do something when any object is added
Use the addLusidObject() callback method.
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
// Do something here...
}
void addLusidObject(LusidObject lObj) {
// Do something here...
}
Do something when a specific object is added
Use the addLusidObject() callback method, and check for the object's uniqueID.
// called when an object is added to the scene
void addLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// Do something here...
}
}
void addLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// Do something here...
}
}
Use object rotation to directly set a variable
Use a specific object's rotation to set a variable directly, such as a volume knob. This example uses the updateLusidObject() callback method to make changes anytime the specified object is moved.
// called when an object is moved/updated
void updateLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// Now update the desired variable...
volume = lObj.getRotZ() / TWO_PI; }
}
void updateLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// Now update the desired variable...
volume = lObj.getRotZ() / TWO_PI; }
}
Use object rotation to incrementally adjust a variable
Create a knob by mapping an object's rotation to a variable. The variable will be incrementally adjusted, such as a jog wheel or scroller. Be careful to catch the discontinuity (the angle goes between 0 and TWO_PI, and then immediately back to 0), and bound the variable with min/max to keep it in your desired range. This example primarily uses the updateLusidObject() callback method to make changes anytime the specified object is moved, as well as the addLusidObject() method to initialize the rotation.
// The jog wheel variables...
// here, we use an object's rotation to incrementally
// adjust a variable, tempo.
int tempo = 120;
int MIN_TEMPO = 60;
int MAX_TEMPO = 240;
float rotationThreshold = PI/16;
float lastUsedRotation = 0;
// called when an object is added
void addLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// object just added, set its initial rotation.
lastUsedRotation = lObj.getRotZ();
}
}
// called when an object is removed
void removeLusidObject(LusidObject lObj) {
// don't need to do anything when object is removed.
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
float rotDiff = lObj.getRotZ() - lastUsedRotation;
if(abs(rotDiff) > PI){
// handle discontinuity; edge case.
rotDiff = rotDiff-(abs(rotDiff)/rotDiff)*TWO_PI;
}
if(abs(rotDiff) > PI/2){
// rotation is too much, seems noisy and improbable.
lastUsedRotation = lObj.getRotZ();
}
else{
if(abs(rotDiff) > rotationThreshold){
// rotation looks good! do something with it...
int rotationTicks = (int)(rotDiff / rotationThreshold);
tempo = max(MIN_TEMPO, min(MAX_TEMPO, tempo + rotationTicks));
println("Tempo is now: " + tempo);
lastUsedRotation = lObj.getRotZ();
}
}
}
}
// here, we use an object's rotation to incrementally
// adjust a variable, tempo.
int tempo = 120;
int MIN_TEMPO = 60;
int MAX_TEMPO = 240;
float rotationThreshold = PI/16;
float lastUsedRotation = 0;
// called when an object is added
void addLusidObject(LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
// object just added, set its initial rotation.
lastUsedRotation = lObj.getRotZ();
}
}
// called when an object is removed
void removeLusidObject(LusidObject lObj) {
// don't need to do anything when object is removed.
}
// called when an object is moved/updated
void updateLusidObject (LusidObject lObj) {
if(lObj.getUniqueID().equals("0x08D03B05BA29")){
float rotDiff = lObj.getRotZ() - lastUsedRotation;
if(abs(rotDiff) > PI){
// handle discontinuity; edge case.
rotDiff = rotDiff-(abs(rotDiff)/rotDiff)*TWO_PI;
}
if(abs(rotDiff) > PI/2){
// rotation is too much, seems noisy and improbable.
lastUsedRotation = lObj.getRotZ();
}
else{
if(abs(rotDiff) > rotationThreshold){
// rotation looks good! do something with it...
int rotationTicks = (int)(rotDiff / rotationThreshold);
tempo = max(MIN_TEMPO, min(MAX_TEMPO, tempo + rotationTicks));
println("Tempo is now: " + tempo);
lastUsedRotation = lObj.getRotZ();
}
}
}
}
Use the distance between two objects
The distance between two objects can be calculated and used as an additional parameter. This example finds the distance between two specific objects, and then uses that distance to change the background color in the draw() method, called each frame.
void draw() {
// get the objects, if they exist.
LusidObject objA = lusidClient.getLusidObject("0x08D03B05BA29");
LusidObject objB = lusidClient.getLusidObject("0x3F28629CA473");
if(objA != null && objB != null){
// now calculate the distance between the objects.
float d = dist(objA.getX(), objA.getY(), objB.getX(), objB.getY());
// update the background color using the distance.
background(d,d,d);
println("Distance between objects: " + d);
}
}
// get the objects, if they exist.
LusidObject objA = lusidClient.getLusidObject("0x08D03B05BA29");
LusidObject objB = lusidClient.getLusidObject("0x3F28629CA473");
if(objA != null && objB != null){
// now calculate the distance between the objects.
float d = dist(objA.getX(), objA.getY(), objB.getX(), objB.getY());
// update the background color using the distance.
background(d,d,d);
println("Distance between objects: " + d);
}
}
More complex forms of interaction...
For more interaction techniques and working examples, take a look at the code in the LusidOSC Processing Bundle. There you will find a set of simple applications that grow in complexity,SimpleApp1-7, as well as six other applications designed to spotlight various functionality. See the bundle screenshot and descriptions for more information.
5. Extensions and other languages
Processing is just one of the many ways you can build application with Trackmate and LusidOSC. Here are some other ways you can program to help you work outside of Processing if you prefer.
- LusidOSC Java/Eclipse Bundle
If you want to build large-scale applications and know your way around Java, the Java/Eclipse Bundle is a good place to start. Only one simple application is currently included, but more will be added to the bundle as they are developed. Eclipse is an extremely powerful integrated development environment for coding in Java (as well as many other languages via plugins). - MaxMSP Example Patcher
MaxMSP allows for developers to quickly patch together different devices with wires between graphical blocks. Each block can contain inputs, outputs, and internal scripts, allowing for fast prototyping of signal processing and interactive systems. This example patcher is pretty basic; giving a handle to the incoming stream of LusidOSC messages which can be further abstracted if desired. - Other langauages
Since LusidOSC is just a protocol layer on top of Open Sound Control and UDP, creating your own library is pretty straight forward for most languages. Use the other libraries and patchers as starting points, and reference the LusidOSC v1.0 Specification for low-level details.