Connections between signals that are maintained by libmapper can be configured with optional signal processing described in the form of an expression.
Expressions in libmapper must always be presented in the form y = x
, where x
refers to the updated source value and y
is the computed value to be forwarded
to the destination. Sub-expressions can be used if separated by a semicolon (;
). Spaces may be freely used within the expression, they will have no effect on the
generated output.
Arithmetic operators | Bitwise operators | |||
---|---|---|---|---|
+ | addition | << | left bitshift | |
- | subtraction | >> | right bitshift | |
* | multiplication | & | bitwise AND | |
/ | division | | | bitwise OR | |
% | modulo | ^ | bitwise XOR (exclusive OR) | |
Comparison operators | Logical operators | |||
> | greater than | ! | logical NOT | |
>= | greater than or equal | && | logical AND | |
< | less than | || | logical OR | |
<= | less than or equal | Conditional operator | ||
== | equal | ?: | if / then / else (ternary operation) used in the form a?b:c . If the second operand is omitted (e.g. a?:c ) the first operand will be used in its place. |
|
!= | not equal | |||
abs(x)
— absolute value
exp(x)
— returns e raised to the given powerexp2(x)
— returns 2 raised to the given powerlog(x)
— computes natural ( base e ) logarithmlog10(x)
— computes common ( base 10 ) logarithmlog2(x)
– computes the binary ( base 2 ) logarithmlogb(x)
— extracts exponent of the number
sqrt(x)
— square rootcbrt(x)
— cubic roothypot(x, n)
— square root of the sum of the squares of two given numberspow(x, n)
— raise a to the power b
sin(x)
— sinecos(x)
— cosinetan(x)
— tangentasin(x)
— arc sineacos(x)
— arc cosineatan(x)
— arc tangentatan2(x, n)
— arc tangent, using signs to determine quadrants
sinh(x)
— hyperbolic sinecosh(x)
— hyperbolic cosinetanh(x)
— hyperbolic tangent
floor(x)
— nearest integer not greater than the given valueround(x)
— nearest integer, rounding away from zero in halfway casesceil(x)
— nearest integer not less than the given valuetrunc(x)
— nearest integer not greater in magnitude than the given value
min(x,y)
– smaller of two values (overloaded)max(x,y)
– greater of two values (overloaded)schmitt(x,a,b)
– a comparator with hysteresis (Schmitt trigger) with inputx
, low thresholda
and high thresholdb
. Output is binary state of the trigger (0
or1
)
uniform(x)
— uniform random distribution between 0 and the given value
midiToHz(x)
— convert MIDI note value to HzhzToMidi(x)
— convert Hz value to MIDI note
ema(x, w)
– a cheap low-pass filter: calculate a running exponential moving average with inputx
and a weightw
applied to the current sample.
pi
– the ratio of a circle's circumference to its diameter, approximately equal to 3.14159e
– Euler's number or Napier's constant, approximately equal to 2.71828
Individual elements of variable values can be accessed using the notation
<variable>[<index>]
in which index
specifies the vector element starting from zero. Overruns and underruns are not possible as the modulus will be used if the index is outside the range <0,length-1>
. This means that negative indices may be used for indexing from the end of the vector.
When assigning values to a vector, if the source is shorter than the assignee it will be repeated as necessary, e.g. if myvar
has length 5 and the expression assigns myvar=[1,2,3]
its value will now be [1,2,3,1,2]
.
Variables and expressions may also be used as indexes. A fractional index will result in linear interpolation between the neighbouring elements. Since we use the index modulus it is possible to interpolate between the last and first elements of the vector.
y = x[0]
— simple vector indexingy = x[1:2]
— specify a range within the vectory = [x[1], x[2], x[0]]
— rearranging vector elementsy[1] = x
— apply update to a specific element of the outputy[0:2] = x
— apply update to elements0-2
of the output vector[y[0], y[2]] = x
— apply update to output vector elementsy[0]
andy[2]
but leavey[1]
unchanged.y = x[i]; i = i + 1;
– use the user-defined variablei
as an index.y = x[sin(x) * 5]
– use an expression to calculate an index.
It is possible to call any of the scalar functions listed above on a vector and the result will be calculated for each element. There are also a number of special functions that operate across all elements of the vector and output a scalar value:
x.length()
– output the dimensionality (number of elements) of the vectorx
x.any()
— output1
if any of the elements of vectorx
are non-zero, otherwise output0
x.all()
— output1
if all of the elements of vectorx
are non-zero, otherwise output0
x.sum()
– output the sum of the elements in vectorx
x.mean()
– output the average (mean) of the elements in vectorx
x.max()
– output the maximum element in vectorx
x.min()
– output the minimum element in vectorx
x.center()
– output the midpoint betweenx.min()
andx.max()
x.norm()
– output the magnitude of the vectorx
x.index(a)
– output the index of elementa
in the vectorx
, or-1
if the element is not found
angle(a, b)
– output the angle between vectorsa
andb
dot(a, b)
– output the dot product of vectorsa
andb
sort(x, d)
orx.sort(d)
– output a sorted version of the vector. The output will be sorted in ascending order ifd
is positive or descending order ifd
is negative.
Past samples of expression input and output can be accessed using the notation
<variable>{<index>}
. The index specifies the history index in samples, and must be <=0
for the input (with 0
representing the present input sample) and <0
for the expression output ( i.e. it cannot be a value that has not been provided or computed yet ).
Using only past samples of the expression input x
we can create Finite
Impulse Response ( FIR ) filters - here are some simple examples:
y = x - x{-1}
— 2-sample derivativey = x + x{-1}
— 2-sample integral
Using past samples of the expression output y
we can create Infinite
Impulse Response ( IIR ) filters - here are some simple examples:
y = y{-1} * 0.9 + x * 0.1
— exponential moving average with current-sample-weight of0.1
y = y{-1} + x - 1
— leaky integrator with a constant leak of1
Note that y{-n}
does not refer to the expression output, but rather to the actual
value of the destination signal which may have been set locally or by another map
since the last time the expression was evaluated. If you wish to reference past samples
of the expression output you will need to cache the output using a user-defined
variable, e.g.:
output = output + x - 1; y = output;
Of course the filter can contain references to past samples of both x
and y
-
currently libmapper will reject expressions referring to sample delays > 100
.
Past values of the filter output y{-n}
can be set using additional sub-expressions, separated using semicolons:
y = y{-1} + x; y{-1} = 100;
Filter initialization takes place the first time the expression evaluator is called
for a given signal instance; after this point the initialization sub-expressions will
not be evaluated. This means the filter could be initialized with the first sample of
x
for example:
y = y{-1} + x; y{-1} = x * 2;
A function could also be used for initialization, for example we could initialize y{-1}
to a random value:
y = y{-1} + x; y{-1} = uniform(1000);
Any past values that are not explicitly initialized are given the value 0
.
It is possible to define a variable delay argument instead of using a constant. In this case it is necessary to add a second maximum delay size argument to let libmapper know how much signal memory to allocate.
y = y{x, 100};
Using a fractional delay argument causes linear interpolation between samples:
y = x + y{-1.5};
Up to 8 additional variables can be declared as-needed in the expression. The variable
names can be any string except for the reserved variable names x
and y
. The values
of these variables are stored with the map context, either per-instance (if assigned from an instanced signal) or per-signal (if assigned from a singleton signal), and can be accessed in subsequent calls to the evaluator. In the following example, the user-defined variable
ema
is used to keep track of the exponential moving average
of the input signal
value x
, independent of the output value y
which is set to give the difference
between the current sample and the moving average:
ema = ema{-1} * 0.9 + x * 0.1; y = x - ema;
Just like the output variable y
we can initialize past values of user-defined variables before expression evaluation. Initialization will always be performed first, after which sub-expressions are evaluated in the order they are written. For example, the expression string y=ema*2; ema=ema{-1}*0.9+x*0.1; ema{-1}=90
will be evaluated in the following order:
ema{-1} = 90
— initialize the past value of variableema
to90
y = ema * 2
— set output variabley
to equal the current value ofema
multiplied by2
. The current value ofema
is0
since it has not yet been set.ema = ema{-1} * 0.9 + x * 0.1
— set the current value ofema
using current value ofx
and the past value ofema
.
User-declared variables will also be reported as map metadata, prefixed by the string var@
. The variable ema
from the example above would be reported as the map property var@ema
. These metadata may be modified at runtime by editing the map property using a GUI or through the libmapper properties API:
/* C API */
/* establish a map between previously-declared signals 'src' and 'dst' */
mpr_map map = mpr_map_new(1, &src, 1, &dst);
mpr_obj_set_prop((mpr_obj)map, MPR_PROP_EXPR, NULL, 1, MPR_STR,
"mix=0.1;y=y{-1}*mix+x*(1-mix);", 1);
mpr_obj_push((mpr_obj)map);
...
/* modify the variable "mix" */
float mix = 0.2;
mpr_obj_set_prop((mpr_obj)map, MPR_PROP_EXTRA, "var@mix", 1, MPR_FLT, &mix, 1);
mpr_obj_push((mpr_obj)map);
// C++ API
// establish a map between previously-declared signals 'src' and 'dst'
mpr.Map map(src, dst);
map[mpr.Property.EXPRESSION] = "mix=0.1;y=y{-1}*mix+x*(1-mix);";
map.push();
...
// modify the variable "mix"
map["var@mix"] = 0.2;
map.push();
//map.set_property("mix", 0.2).push();
# Python API
# establish a map between previously-declared signals 'src' and 'dst'
map = mpr.map(src, dst)
map['expr'] = 'mix=0.1;y=y{-1}*mix+x*(1-mix);'
map.push()
...
# modify the variable "mix"
map['var@mix'] = 0.2
map.push()
Note that modifying variables in this way is not intended for automatic (i.e. high-rate) control. If you wish to include a high-rate variable you should declare it as a signal and use convergent maps as explained below.
Convergent mapping—in which multiple source signals update a single destination signal–are supported by libmapper in five different ways:
Method | Example |
---|---|
interleaved updates (naïve convergent maps): if multiple source signals are connected to the same destination, new updates will simply overwrite the previous value. This is the default for singleton (i.e. non-instanced) signals. | |
partial vector updates: if the destination signal has a vector value (i.e. a value with a length > 1), individual sources may address different elements of the destination. | |
shared instance pools: instanced destination signals will automatically assign different instances to different sources. | |
destination value references: including the destination signal value in the expression enables simple "mixing" of multiple sources in an IIR filter. Within the mapping expression, y{-N} represents the Nth past value of the destination signal (rather than the expression output) and will thus reflect updates to this signal caused by other maps or local control. If you wish to use past samples of the expression output instead you will need to cache this output explicitly as explained above in the section FIR and IIR Filters. |
|
convergent maps: arbitrary combining functions can be defined by creating a single map with multiple sources. Libmapper will automatically reorder the sources alphabetically by name, and source values are referred to in the map expression by the string x$ +<source index> as shown in the example to the right. When editing the expression it is crucial to use the correct signal indices which may have been reordered from the array provided to the map constructor; they can be retrieved using the function mpr_map_get_sig_idx() or you can use mpr_map_new_from_str() to have libmapper handle signal index lookup automatically. When a map is selected in the Webmapper UI the individual sources are labeled with their index. The most recently updated source can be referenced using x.signal.newest()() or the shorthand x$$ |
By default, convergent maps will trigger expression evaluation when any of the source signals are updated. For example, the convergent map y=x$0+x$1
will output a new value whenever x$0
or x$1
are updated. Evaluation can be disabled for a source signal by inserting an underscore _
symbol before the source name, e.g. y=x$0+_x$1
will be evaluated only when the source x$0
is updated, while updates to source x$1
will be stored but will not trigger evaluation or propagation to the destination signal.
If desired, the entire expression can be evaluated "silently" so that updates do not propagate to the destination. This is accomplished by manipulating a special variable named muted
. For maps with singleton destination signals this has an identical effect to manipulating the alive
variable, but for instanced destinations it enables filtering updates without releasing the associated instance.
The example below implements a "change" filter in which only updates with different input values are sent to the destination:
muted = (x == x{-1}); y = x;
Note that (as above) the value of the muted
variable must be true (non-zero) when y is assigned in order to mute the update; the arbitrary example below will instead mute the next update following the condition (x==x{-1})
:
y = x; muted = (x == x{-1});
Input and output signals addressed by libmapper may be instanced meaning that there are multiple independent copies of the object or phenomenon represented by the signal. For example, a signal representing /touch/position
on a multitouch display would have an instance corresponding to each active touch. This means that a signal value x
can have up to four dimensions:
dimension | syntax | application |
---|---|---|
vector elements | x[n] |
representation of signals that are naturally multidimensional, e.g. 3D position |
input signals | x$n |
convergent maps |
signal history | x{-n} |
DSP (e.g. smoothing filters); live looping |
signal instances | TBA | representation of signals that are naturally multiplex and/or ephemeral, e.g. multitouch |
As mentioned in the section on vectors above, the index n
can be a literal, a variable, or an expression. In the case of expression-type input signal indices parentheses must be used to indicate the scope of the index, e.g. y=x$(sin(x)>0);
.
Typically instancing is handled at the signal
level and can be ignored while designing map expressions, however if desired instancing can also be managed from within the map expression by manipulating a special variable named alive
that represents the instance state. The use cases for in-map instancing can be complex, but here are some simple examples:
Singleton Destination | Instanced Destination | |
---|---|---|
Singleton Source | conditional output | conditional serial instancing |
Instanced Source | conditional output | modified instancing |
In the case of a map with a singleton (non-instanced) destination, in-map
instance management can be used for conditional updates. For example,
imagine we want to map x -> y
but only propagate updates when x > 10
– we could use the expression:
alive = x > 10; y = x;
Since in this case the destination signal is not instanced it will not be "released" when alive
evaluates to False, however any assignments to the output y
while alive
is False will not take effect. The statement alive = x > 10
is evaluated first, and the update y = x
is only propagated to the destination if x > 10
evaluates to True (non-zero) at the time of assignment. The entire expression is evaluated however, so counters can be incremented etc. even while alive
is False. There is a more complex example in the section below on Variable Timetags.
When mapping a singleton source signal to an instanced destination signal there are several possible desired behaviours:
- The source signal controls one of the available destination signal instances. The destination instance is activated upon receiving the first update and a release event is triggered when the map is destroyed so the lifetime of the map controls the lifetime of the destination signal instance. This configuration is the default for maps from singleton->instanced signals, and is achieved by setting the map property
use_inst
to True. - The source signal controls all of the available active destination signal instances in parallel. This is accomplished by setting the
use_inst
property of the map to False (0). Note that in this case a source update will not activate new instances, so this configuration should probably only be used with destination signals that manage their own instances or that are persistent (non-ephemeral).- Example 1: a destination signal named polyPressure belongs to a software shim device for interfacing with MIDI. The singleton signal mouse/position/x is mapped to polyPressure, and the map's
use_inst
property is set to False to enable controlling the poly pressure parameter of all active notes in parallel.
- Example 1: a destination signal named polyPressure belongs to a software shim device for interfacing with MIDI. The singleton signal mouse/position/x is mapped to polyPressure, and the map's
- The source signal controls available destination signal instances serially. This is accomplished by manipulating the
alive
variable as described above. On each rising edge (transition from 0 to non-zero) of thealive
variable a new instance id map will be generated
currently undocumented
There are several special functions that operate across all elements of a signal dimension. Use the table below and simply replace <dim>
with the dimension name: instance, history, signal, or vector. In the case of vector reduce the output will be a scalar, otherwise the output will have the same vector length as the expression being reduced.
x.<dim>.any()
– output1
if any element ofx
is non-zero, otherwise output0
x.<dim>.all()
– output1
if all elements ofx
are non-zero, otherwise output0
x.<dim>.count()
— output the number of elements ofx
, e.g.x.instance.count()
to get the number of active instances.x.<dim>.sum()
– output the sum of the values of all elements ofx
x.<dim>.mean()
– output the mean of the values of all elements ofx
x.<dim>.max()
– output the maximum value of all elements ofx
x.<dim>.min()
– output the minimum value of all elements ofx
x.<dim>.size()
– output the difference between the maximum and minimum values of all elements, i.e.x.<dim>.max()-x.<dim>.min()
x.<dim>.center()
– output the N-dimensional point located at the center of the element ranges, i.e.(x.<dim>.max()+x.<dim>.min())*0.5
Note that the history
type of reduce function requires an integer argument after .history
specifying the number of samples to reduce, e.g. x.history(5).mean()
. The instance
dimension reduce functions operate over all currently active instances of the signal.
These functions accept subexpressions as arguments. For example, we can calculate the linear displacement of input x
averaged across all of its active instances with the expression y=(x-x{-1}).instance.mean()
. Similarly, we can calculate the average angular displacement around the center of a bounding box including all active instances:
c0{-1}=x.instance.center(); c1=x.instance.center(); y=(angle(x{-1}-c0, x-c1)).instance.mean(); c0=c1
In a scenario where x
represents the touch coordinates on a multitouch surface, this value gives mean rotation of all touches around their mutual center.
In addition to the specialised reducing functions mentioned above, map expressions can also include the function reduce()
with a user-defined arrow function that operates over any of the four signal dimensions:
dimension | syntax |
---|---|
vector elements | x.vector.reduce(a, b [= initialValue] -> ...) |
input signals | x.signal.reduce(a, b [= initialValue] -> ...) |
signal instances | x.instance.reduce(a, b [= initialValue] -> ...) |
signal history | x.history(len).reduce(a, b [= initialValue] -> ...) |
Note that the history
type of reduce function requires an integer argument after .history
specifying the number of samples to reduce. The reduce()
function requires arguments specifying the input
and accumulator
variable names, followed by the arrow symbol ->
and an expression describing how to process each element. The input
and accumulator
variable names are user-defined, and are lexically scoped, i.e. if reduce functions are nested using the same variable names no variable collisions will occur. The accumulator
variable can be initialised is needed; otherwise it will default to zero.
For example, the following expression will calculate the mean signal value over the last 10 samples:
y = x.history(10).reduce(a, b -> a + b) / 10;
Note: this expression could also be implemented as:
y = x.history(10).mean();
As mentioned above, reduce functions can be nested to reduce across multiple dimensions within a single expression. Obviously the nested reduce functions must operate across different dimensions in order for the expression to work. The expression below calculates the sum of all vector elements of all input signals:
y = x.signal.reduce(a, b -> a.vector.reduce(c, d -> c + d) + b);
In some scenarios it may be useful to convert the active instances, historic values, or multiple signal values into a vector before further processing. This can be accomplished using the concat()
function applied to one of these dimensions, e.g.:
x.history(5).concat(5)
– concatenate the last 5 historical values to form a vector.x.instance.concat(10)
– concatenate active instance values to form a vector with a maximum length of 10.x.signal.concat(4)
– concatenate input signal values (in a convergent map) to form a vector.
Note that the concat()
function requires an integer argument specifying the maximum length of the output vector. This is needed to preallocate memory needed by the expression evaluator.
The precise time at which a signal or variable is updated is always tracked by libmapper and communicated with the data value. In the future we plan to use this information in the background for discarding out-of-order packets and jitter mitigation, but it may also be useful in your expressions.
The timetag associated with a variable can be accessed using the syntax t_<variable_name>
– for example the time associated with the current sample of signal x
is t_x
, and the timetag associated with the last update of a hypothetical user-defined variable foo
would be t_foo
. This syntax can be used anywhere in your expressions:
y = t_x
— output the timetag of the input instead of its valuey = t_x - t_x{-1}
— output the time interval between subsequent updates
This functionality can be used along with in-map signal instancing to limit the output rate. The following example only outputs if more than 0.5 seconds has elapsed since the last output, otherwise discarding the input sample.
muted = (t_x - t_y{-1}) <= 0.5; y = x;
Also we can calculate a moving average of the sample period:
y = y{-1} * 0.9 + (t_x - t_y{-1}) * 0.1;
Of course the first value for (t_x-t_y{-1})
will be very large since the first value for t_y{-1}
will be 0
. We can easily fix this by initializing the first value for t_y{-1}
– remember from above that this part of the expression will only be called once so it will not adversely affect the efficiency of our expression:
t_y{-1} = t_x; y = y{-1} * 0.9 + (t_x - t_y{-1}) * 0.1;
Here's a more complex example with 4 sub-expressions in which the rate is limited but skipped samples are averaged instead of discarding them:
count{-1} = 1; muted = (t_x - t_y{-1}) <= 0.1; y = (accum + x) / count; accum = muted * accum + x; count = muted ? count + 1 : 1;
Explanation:
order | step | expression clause | description |
---|---|---|---|
0 | initialization | count{-1}=1 |
set the initial value of count to 1 to avoid divide by zero. |
1 | check elapsed time | muted=(tx-ty{-1})<0.1 |
Set muted to 1 (true) if less than 0.1 seconds have elapsed since the last output; or 0 otherwise. |
2 | conditional output | y=(accum+x)/count |
output the average of collected samples (if muted is false) |
3 | update accumulator | accum=muted*accum+x |
reset accumulator to 0 if muted is false; add x |
4 | update count | count=muted?count+1:1 |
increment count , reset if muted is false |