OSC postfix notation
The OSC postfix notation is a way to tell a device or program rules on how to dynamically construct a given OSC message on-the-fly. The most prominent usage case is where a given OSC sink demands specially constructed OSC messages. When the implementation of that sink cannot be changed, the OSC message construction at the sender needs to be changed instead.
For this to work, the OSC sender implements a stack based virtual machine that will be called each time a new OSC message needs to be created. The program logic that pushes and pops values to and from the stack is specified in OSC postfix notation - a mathematical postfix notation specially adapted for OSC output.
Although the notation is rather simple and may seem limited, it allows for an efficient evaluation of rather complex custom expressions.
Note: Make sure first to understand what postfix aka reverse-polish notation is before proceeding.
OSC postfix notation now just maps an arbitrary number of standard postfix expressions to standard OSC types. The notation i(...) evaluates the expression ... and creates an OSC integer argument, whereas the notation f(...) an OSC float argument and the notation m(...) an OSC MIDI argument. The expression delimited by the braces is given in pure postfix notation with support for variable referencing with a prefixed dollar sign $. This is best explained with some examples:
The following OSC postfix notation will append an OSC integer argument to an OSC message and set its value to 5 (the corresponding infix notation of the expression would be: 1*2 + 3.
/test1 i(1 2* 3+)
The next OSC postfix notatoin will append two OSC float arguments to an OSC message. The first float references a variable $x which is scaled and shifted by 0.5 each (infix: $x*0.5 + 0.5). The second float references an other variable $z which is subtracted from 1 (infix: 1 - z).
/test2 f($x 0.5* 0.5+) f(1 $z-)
The next example uses a conditional to choose between two values on the stack. Whereas both OSC integers and floats pop a single value only from the stack, the OSC MIDI argument pops four of them (channel number, system message, data 1 and data 2). If the variable $z is greater than 0.5, a NOTEON message is created for channel number 0 and a NOTEOFF message otherwise. The key is based on variable $x (infix: $x * 0x7f) and velocity is fixed to 0x7f.
/test3 m(0 0x90 0x80 $z 0.5>? $x 0x7f* 0x7f)
The last example is more complex and shows how the stack can be used to store values temporarily. The channel number is based on two variables $b and $g (infix: $b%8 + $g*2). The system message is set to 0xe0 (PITCHBEND). Pitch bend value is set to (infix: $x*0x3fff) by setting data 1 to its LSB value (infix: ($x*0x3fff) & 0x7f) and data 2 to its MSB value (infix: ($x*0x3fff) >> 7). When looking at the notation, you can see, that the multiplication is only done once for data 1, instead of redoing the multiplication, the result is just duplicated (@@) and later retrieved (#) when needed for data 2.
/test4 m($b 8% $g 2*+ 0xe0 $x 0x3fff* @@ 0x7f& # 7>>)
Note: Storing values on the stack temporarily also works across OSC type boundaries, e.g. the following is a valid notation and evaluates to i(1) i(2) i(2):
/test5 i(1 @@) i(1+ @@) i()
The following operators are know to the compiler and virtual machine:
|~||negation (unary op)|
|<=||less than or equal|
|>=||greater than or equal|
|!||not (unary op)|
|?||conditional (ternary op)|
|@||push value (unary op)|
|[||push to register|
|]||pop from register (unary op)|
The switch operator # simply switches the two topmost values on the stack. The conditional operator ? can be used as a poor-mans if-else statement. It pops three values from the stack and pushes either the deepest or the middle value onto the stack depending on whether the topmost evaluates to true or false. The @@ operator duplicates the value at the top of the stack, whereas the @ operator pops a position from the stack and duplicates the corresponding value in the stack. The virtual machine features a register with 8 slots to store temporary values across invocations. The [ operator takes two arguments (value and register position) and stores the value to the given register position. The ] operator takes position as an argument and pops the given value from the register.