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
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.
Individual elements of variable values can be accessed using the notation
<variable>[<index>]
. The index specifies the vector element, and
obviously must be >=0
. Expressions must match the vector lengths of the
source and destination signals, and can be used to translate between
signals with different vector lengths.
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.
There are several special functions that operate across all elements of the vector:
any(x)
— output1
if any of the elements of vectorx
are non-zero, otherwise output0
all(x)
— output1
if all of the elements of vectorx
are non-zero, otherwise output0
sum(x)
– output the sum of the elements in vectorx
mean(x)
– output the average (mean) of the elements in vectorx
max(x)
– output the maximum element in vectorx
(overloaded)min(x)
– output the minimum element in vectorx
(overloaded)
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};
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 per-instance with the map context 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);
# 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. |
Signal 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 Accessing 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
By default, convergent maps will trigger expression evaluation when any of the source signals are updated. For example, the convergent map y=x0+x1
will output a new value whenever x0
or x1
are updated. Evaluation can be disabled for a source signal by inserting an underscore _
symbol before the source name, e.g. y=x0+_x1
will be evaluated only when the source x0
is updated, while updates to source x1
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});
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.
alive = (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 out 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 incoming samples are averaged instead of discarding them:
alive = (t_x - t_y{-1}) > 0.1; y = B / C; B = !alive * B + x; C = alive ? 1 : C + 1;
Explanation:
order | step | expression clause | description |
---|---|---|---|
1 | check elapsed time | alive = (tx - ty{-1}) > 0.1 |
Set alive to 1 (true) if more than 0.1 seconds have elapsed since the last output; or 0 otherwise. |
2 | conditional output | y = B / C |
Output the average B/C (if alive is true) |
3 | update accumulator | B = !alive * B + x |
reset accumulator B to 0 if alive is true, add x |
4 | update count | C = alive ? 1 : C + 1 |
increment C , reset if alive is true |