This document contains a full writeup of the expression syntax in Milkdrop presets. It documents the general syntax, available operators and functions and also any specific, non-obvious behaviours. Any differences between Milkdrop's original parser and projectM's are also documented where applicable.
This document does not cover the warp_
and comp_
shader code blocks. The shader code is written in DirectX HLSL,
which is better explained by Microsoft's documentation in the MSDN.
The document also doesn't cover the different code blocks, predefined variables and scope of the special t/q values. Please refer to the preset authoring guide for these.
The expression language only knows a few syntactic features:
- Numbers, with or without decimals. Can also contain a base 10 exponent using the
e[+-]n
notation. - Variables to hold a single number, both user-defined and predefined. May contain characters a-z, 0-9 and the
underscore (
_
), while 0-9 must not be the first letter. - Special constants beginning with
$
. - A single expression, separated from other expressions in the same list via semicolons (
;
), with the last semicolon being optional. - Calls to built-in functions in the form
func(arg1, arg2, ...)
. - Mathematical, assignment and other operators.
- Subscript access
[]
for megabuf/gmegabuf access. - Parentheses (
()
) to group statements and override precedence. - In-line comments starting with
//
until the end of the current line - Multi-line comments starting with
/*
and ended with*/
.
Expressions can return either a numeric value or a reference to a variable or memory location. What is actually returned is determined by the function or operator that is executed last. If the expression only consists or a single token, then the following applies:
- If the expression token is a constant number (
1.0;
), the number is returned by value. - If the expression token is a variable name (
x;
), a reference to the variable is returned.
For most functions, the return type doesn't matter. If it's a reference, the value pointed to by the reference is used.
References only behave differently when used on the left side of assignments or in the forst argument of the assign()
function. In this case, the variable or (g)megabuf location the reference points to is assigned a new value.
Every expression returns its outermost result, as a value or reference as described above. When writing multiple expressions (separated by semicolon) in a list, the result of the rightmost (or last) expression is returned. The results of previous expressions are discarded.
In general, each expression will be in the form var = expression;
, assigning a value to the given variable (or
megabuf index). Expressions will also always return a value, so when used as function arguments, this value will be
used. For example, all the following statements will call the sin()
function with the argument 4
:
x = sin(val = 2 * 2);
y = sin(2 + 2);
z = sin(4.0);
Variable and function names are generally case-insensitive, making variable
equivalent to vArIaBlE
.
Any text in expressions - starting with either a letter from a to z or an underscore, followed by more of these characters and also numbers from 0 to 9 - is first checked to be a built-in function. If it is not a function, the parser will interpret it as a variable.
Functions always need to be followed by parentheses (sin(expr)
), while variables must not.
Milkdrop imposes a 16-character limit on variable names. projectm-eval does not have a hard limit, but it's advisable to not use more than 16 characters for Milkdrop compatibility.
Milkdrop also has a limit of 64 user-defined variables, which also isn't applied by projectm-eval.
In addition to gmegabuf, there are an additional 100 variables which can be used to store global values. These variables
are named reg00
, reg01
etc. up to reg99
. Values stored in those variables are available in al execution contexts,
in the same way as gmegabuf.
Note the index must always be written with two digits. reg3
is not considered a global variable and will only have a
local scope as any other variable. The same is true for more digits like reg123
.
Same as with gmegabuf, global variables are not necessarily 0
when a preset is initialized, and they can change at any
time when two presets using the same global variables are blended during a transition.
All constants are converted into floating-point numbers internally, even if written as integers.
As with all other names, constant names (and hexadecimal values) are also case-insensitive.
Basic numerical/decimal constants can be defined in the code in the following formats:
- Integer values:
1
(or1.
) - Decimal values:
1.234
(values between 0 and 1 can be abbreviated with.234
, which equals0.234
) - Base 10 exponential form:
1.234e5
equals123400
and1.234e-5
equals0.00001234
A few named convenience constants can be used:
$PI
: Equals3.141592653589793
$E
: Equals2.71828183
$PHI
: Equals1.61803399
Hexadecimal constants start with $X
, immediately followed by the hexadecimal numbers.
Example: $XFF
converts to 255
.
Constants can also convert a single character into its ordinal (ASCII) value using the form $'c'
, where c
is the
character to convert.
Example: $'a'
converts to 97
.
Operator precedence is defined in a similar way as in C, except the ^
operator, which has a different
meaning in Milkdrop presets (pow() instead of binary XOR).
See the C precedence table on cppreference.com for details. The pow operator is applied just after the unary plus/minus operators, having a slightly lower precedence.
The subscript operator or "array access operator" has a slightly different meaning than in other programming languages. The Milkdrop expression syntax only knows a single data type, individual numbers, so there are no arrays.
If any index value is not an integer, it is rounded to the nearest integer after calculating the final index (e.g. adding index and offset first, then rounding).
The subscript operator is instead used to address memory locations in megabuf and gmegabuf. There are three possible syntax variants:
Accessing the gmegabuf is possible by using the special keyword gmem
, followed by an index in the subscript brackets.
The following statement sets gmegabuf location 10.000 to the current value of x
:
gmem[10000] = x;
Any memory index from 0 to 8.388.607 (= 128 * 65536) can be addressed.
Accessing the current context memory buffer (megabuf) can be done by writing the index before a set of empty brackets. The following example is analogous to the above, but it sets index 10.000 of the local memory instead:
10000[] = x;
An optional offset can be provided in the brackets, which is simply added to the index on the outside. The following
example will set memory index 10.123 to the value of x
:
10000[123] = x;
Both index and offset values can of course be calculated with expressions. So the following expression is valid:
if(x > 5,5000,1000)[(sin(y) + 1 * .5) * 1000] = z;
Parentheses can be used to specify a specific order in which operations are executed. Expressions inside parentheses are always evaluated, and the result of the evaluation is then used to evaluate any outside expression.
Inside parentheses, using expression lists is also valid. The result is, as described above, the value of the last
expression in the list. For example, the following expression will use 5
as the return value:
x = a * (b + c; d + e; 2 + 3);
The first two operations will do nothing, as they don't set a variable or memory value and the results are discarded.
For more complex calculations, it might be a good idea to leave some comments in the expressions to describe what's done in a specific line or block. Comments can also be useful to quickly disable a line or block of code to try out things when writing a preset.
Comments can be used as end-of-line or block variants:
x = sin(5); // This comment is only valid until the line ends
y = sin($PI * .5); /* This comment
spans
multiple
lines and the following expression is executed: */ z = 5;
Comments can appear inside any expression, but not within a variable, function name, number or other constant.
Writing x = var/*comment*/iable;
is invalid (but x = sin/*comment*/(variable);
is).
The following operators can be used in expressions.
These operators carry out basic mathematical operations.
+
: Adds both operands.-
: Subtracts the right operand from the left.*
: Multiplies both operands./
: Divides the left operand by the right. If the divisor is zero, the result of the operation is0
instead ofInf
.%
: Converts the operands to integers and returns the remainder of the left by the right operand.^
: Calculates the left operand to the power of the right operand.
These operators check for equality or inequality of the operands and return a boolean result:
==
: Equal. Returns1
if both operands are equal,0
if not.!=
: Not equal. Returns1
if both operands are not equal,0
if they are.<
: Below. Returns1
if the left operand is below the right operand,0
otherwise.>
: Above. Returns1
if the left operand is above the right operand,0
otherwise.<=
: Below or equal. Returns1
if the left operand is below or equal to the right operand,0
otherwise.>=
: Above or equal. Returns1
if the left operand is above or equal to the right operand,0
otherwise.
The following operators allow to carry out bitwise operations on integer representations of the operands:
&
: Converts both arguments to integers and calculates the bitwise AND between the resulting operands.|
: Converts both arguments to integers and calculates the bitwise OR between the resulting operands.
Boolean operators only check an operand to be zero or non-zero, then return the according result:
&&
: Returns1
if both operands are non-zero and0
if any operand is zero.||
: Returns1
if one or both operands are non-zero and0
if both operands are zero.!
: Unary operator. Returns1
if the operand is zero and0
if the operand is non-zero.a ? b : c
: The ternary operator. Evaluates thea
operand and evaluates and returns theb
operand if the condition is non-zero. If the condition evaluates to zero, only thec
operand is evaluated and returned.
Assignment operators can simply assign the result of the right-hand side to a variable or megabuf index, or in case of
compound operators a OP= b
also carry out an additional operation equivalent to a = a OP B
. All assignment operators
return a reference to the left operand.
=
: Assigns the value of the right operand to the left operand.+=
: Adds the value of the right operand to the left operand and assigns the result to the left operand.-=
: Subtracts the value of the right operand from the left operand and assigns the result to the left operand.*=
: Multiplies the value of the right operand with the left operand and assigns the result to the left operand./=
: Divides the left operand by the value of the right operand and assigns the result to the left operand. If the divisor is zero, the result of the operation is0
instead ofInf
.%=
: Calculates the remainder of the rounded integer values of the left operand divided by the right operand and assigns the result to the left operand. If the divisor is0
, the result will also be0
.^=
: Calculates the left operand to the power of the right operand and assigns the result to the left operand.&=
: Calculates the binary AND of the rounded integer values of the left and right operands and assigns the result to the left operand.|=
: Calculates the binary OR of the rounded integer values of the left and right operands and assigns the result to the left operand.
The subscript - or index - operator []
is a special notation to access megabuf and gmegabuf. If the inside of the
brackets is empty, it is interpreted as a 0
. For more details, see the "Subscript Access" section above.
The following functions are built into the expression parser and can be used in all Milkdrop 2.x presets.
Returns 1
if left
has a larger value than right
, otherwise 0
.
Same as the >
operator.
Returns the absolute (positive) value of val
.
Calculates and returns the arc cosine (inverse cosine) of val
. The angle is interpreted as radians.
Calculates and returns the arc sine (inverse sine) of val
. The angle is interpreted as radians.
Assign val
to the reference pointed by dest
and return dest
. If dest
is not a reference, the result is the
value of val
.
Same as the assignment operator =
.
Calculates and returns the arc tangent (inverse tangent) of val
. The angle is interpreted as radians.
Calculates and returns the polar coordinate angle associated with the coordinate represented by x,y
. The angle is
returned as radians.
See the Wikipedia definition of atan2 for details.
Evaluates both arguments and returns 1
if both arguments evaluate to a non-zero value and 0
if at least one argument
is zero.
Note: The boolean AND operator &&
returns the same result, but only evaluates the second argument if the first one is
not zero!
Returns 1
if left
has a smaller value than right
, otherwise 0
.
Same as the <
operator.
Boolean NOT operator. Returns 1
if val
is 0
, otherwise 0
.
Same as the !
operator.
Evaluates both arguments and returns 1
if at least one argument evaluates to a non-zero value and 0
if both
arguments are zero.
Note: The boolean OR operator ||
returns the same result, but only evaluates the second argument if the first one is
zero!
Rounds the value of val
to the next larger integer number in reference to 0
.
Calculates and returns the cosine of val
. The angle is interpreted as radians.
Compares both values and returns 1
of they are equal or 0
if not.
Same as the ==
operator.
Executes both expressions and returns the result of the second one.
Executes all three expressions in order and returns the result of the third one.
Exponential function with base e
to the power of val
.
Equivalent to the expression $E ^ val
.
Rounds the value of val
to the next smaller integer number in reference to 0
.
Requests freeing all memory blocks in megabuf
(NOT gmegabuf
) beginning with the block index
is in. This may result
in memory being freed before the given index.
Note: This function is currently a no-op in both Milkdrop and projectM. It does not actually free any memory or reset the memory contents.
Memory blocks are 65536 entries large, and there is a total of 128 blocks.
Returns a reference to the storage location of index
in the global memory buffer.
Any memory index from 0 to 8.388.607 (= 128 * 65536) can be addressed. Any index outside of this range will return the
fixed value 0
, and no memory is changed when assigning another value to it.
Evaluates cond
, and returns trueval
if the result is non-zero. Otherwise, falseval
is returned.
If either trueval
or falseval
evaluate to a reference, the reference is passed through. Thus, assigning a value to
the result of if()
is possible:
if(x > 5, a, b) = 10;
will assign 10
to a
if x > 5
and to b
if x <= 5
.
Truncates any decimals from val
, effectively converting it into an integer.
Calculates the fast inverse square root of val
.
Calculates and returns the ("natural") logarithm to base e
of val
.
Calculates and returns the logarithm to base 10 of val
.
Executes expr
exactly count
times.
As mentioned in the grammar section, expr
can also consist of multiple expressions separated by semicolon. this is
also true for count
, but expressions in this argument are only evaluated once before the first loop iteration starts.
The loop will always abort after 2^20 (~1 million) iterations.
Be careful when nesting loops, as the loop count and associated run time will grow exponentially with each nesting level, which can hang up the application.
Returns the larger of the two values of val1
and val2
.
Returns a reference to the storage location of index
in the local memory buffer.
Any memory index from 0 to 8.388.607 (= 128 * 65536) can be addressed. Any index outside of this range will return the
fixed value 0
, and no memory is changed when assigning another value to it.
Copies count
values starting at src
to dest
.
Memory areas are allowed to overlap.
Any source or destination memory falls outside the allowed indices of 0 to 8.388.607, the range is trimmed accordingly and only a data block is copied that fits into both areas.
Fills count
number of memory slots beginning at dest
with value
.
If the range falls outside the allowed indices of 0 to 8.388.607, the range is trimmed accordingly.
This function is way faster than setting individual values to 0 in a loop()
and should be preferred if a larger
memory block needs to be initialized or reset with a single number.
Returns the smaller of the two values of val1
and val2
.
Calculates base
to the power of exp
and returns the resulting value.
Calculates a pseudo-random number (using the Mersenne-Twister algorithm) between 0 and max
.
max
can be a floating-point value.
Calculates the logistic curve with an argument of (-a * b)
and returns the result.
This is equivalent to the expression tmp = 1 + pow($E, (-a + b)); if(tmp > 0.00001, 1 / tmp, 0);
.
Returns the signage of val
, which is -1
for negative values, 1
for positive values and 0
for 0
.
Calculates and returns the sine of val
. The angle is interpreted as radians.
Calculates and returns the square of val
.
This is equivalent to the expression val * val
.
Calculates and returns the square root of val
.
Calculates and returns the tangent of val
. The angle is interpreted as radians.
Executes expr
at least once and loops until it returns 0
.
expr
can be a semicolon-separated list of expressions. Only the return value of the last expression in the list is
used to determine the loop abort condition.
The loop will always abort after 2^20 (~1 million) iterations.
Be careful when nesting loops, as the loop count and associated run time will grow exponentially with each nesting level, which can hang up the application.
The following functions are defined internally, some being aliases to the above functions, others being used as actual implementations of operators. Depending on the operator being unary or binary, each function has one or two parameters.
- _aboeq => Operator
>=
- _above => Operator
>
- _add => Operator
+
- _addop => Operator
+=
- _and => Operator
&&
- _andop => Operator
&=
- _beleq => Operator
<=
- _below => Operator
<
- _div => Operator
/
- _divop => Operator
/=
- _equal => Operator
==
- _gmem => Alias for
gmegabuf
- _if => Operator
?:
, alias forif
- _mem => Alias for
megabuf
- _mod => Operator
%
- _modop => Operator
%=
- _mul => Operator
*
- _mulop => Operator
*=
- _neg => Negation operator
-
- _not => Operator
!
- _noteq => Operator
!=
- _or => Operator
||
- _orop => Operator
|=
- _powop => Operator
^=
- _set => Operator
=
, alias forassign
- _sub => Operator
-
- _subop => Operator
-=
Be aware that if this parser is used in other projects or non-Milkdrop-compatible presets, additional functions may be
defined which can't be used as variable names. One example are the additional getosc
and getspec
functions defined
in AVS.
Each execution context has two associated memory buffers, a local buffer and a global buffer. Each buffer can hold up to 2^23 (8.388.608) individual floating-point numbers which can be addressed via their zero-based index.
The local buffer is only valid within the same execution context. In classic Milkdrop, there are these contexts, each of
which having its own megabuf
:
- The global per-frame context (
per_frame_init_
andper_frame_
) - The per-vertex/per-pixel mesh context (
per_pixel_
) - The wave per-frame context, one per waveform (
wave_N_init
andwave_N_per_frame
) - The wave per-point context, one per waveform (
wave_N_per_point
) - The shape per-frame context, one per shape (
shape_N_init
andshape_N_per_frame
)
The global memory buffer, referred to as gmegabuf
, only exists once and is used for all execution contexts. It is
even shared between different Milkdrop presets. Keep this in mind when using the buffer, as the contents may be changed
by another preset during the transition phase!
Local buffers always start empty and return zero for all indices when being accessed the first time. The global buffer will also start this way, but is never reset. Code using the global megabuf should initialize the required indices with known values.
Memory is allocated in blocks of 64K (65.536) values each, with a total of 128 of such blocks per buffer. When using memory indices, ideally keep the indices close together to allocate only a minimum number of blocks and save the user's RAM resources.
If a larger memory region needs to be initialized with a single value, e.g. resetting a part of gmegabuf
, assigning
single values in a loop
is time-consuming. The memset
function can be used to achieve the same result in a very fast
operation. Calling the following function will reset values 10.000 to 19.999 to 5
:
memset(10000, 5, 10000);
Sometimes it's required to copy or move a continuous block of values, e.g. moving data from previous frames left and
adding a new set of data points at the end. This can be done in a loop
, but again is very time-consuming. The memcpy
function is able to perform this operation in a single call. It also supports moving memory in overlapping areas - the
original contents are first copied into a temporary buffer, then written to the new location. To move data forward in a
1000-value large block by 100 indices, with the block starting at index 5000 looks like this:
memcpy(5000, 5100, 900);
The last 100 values will appear twice in the block, while the first 100 got overwritten.
For most uses, the expression language is quite straightforward to use. Due to the implementation, users can take advantage of some features while they need to be aware of a few details that may cause problems.
As mentioned in the sections above, some functions may or may not return variable or memory references. It is, in any case, valid to assign a value to any other value, even a constant. So while the following statement doesn't do anything useful, it is valid:
assign(1, 2);
The function itself will return the value 2
as expected, but the actual assignment is discarded.
Other constructs are more helpful, for example to choose between two or more variables to assign a value to. The
following code will assign 10
to either a
, b
or c
depending on the value of x
:
assign(if(x < 0, a, if(x > 0), c, b), 10);
This works because if
will pass the result of the appropriate true/false expression, which can be a reference to a
variable. In the above example, it would also be valid to return 0
instead of b
is no assignment is wanted, but
the assign()
function should still return the value 10 in any case.
The last example can also be written in the assignment form, with if()
as the left operand:
if(x < 0, a, if(x > 0), c, b) = 10;
A few functions check a condition to execute a certain instruction. For example, if
decides whether to use the true or
false argument, and while
uses the value of the last expression to check if the loop should abort.
Due to how floating-point numbers work in computers, not all calculations may return exactly 0.0
, even when using
simple constants. The reason is that some numbers can't be exactly represented by the binary number used by the CPU and
needs to be slightly modified. Using such a number in math equations may even increase the error, and the result might
never be exactly zero.
To make the expression work as intended, one of the following approaches may be used, depending on what is required:
- If only the integer part of the value is of interest, use
int(val)
to truncate any decimals. - Round the result to the nearest integer with
round(val)
if the error can be larger than0.5
. - Compare the absolute value of the number to be smaller/larger than the allowed error, e.g.
abs(val) > 0.0001
The boolean comparison operators (<
, ==
etc.) will always return either 0.0
or 1.0
, which don't need to be
rounded and can be used directly for the condition check. Using those operators should always be preferred over directly
passing a calculated variable value as the conditional.
While execution of the compiled programs is generally quite fast, some functions and code constructs can heavily impact the performance, especially on low-end and embedded devices like the Raspberry Pi. To make presets available to a broad audience, keep the following considerations in mind:
- Keep code in the
per_pixel_
block small and fast. If possible, perform CPU-intensive work in theper_frame_
block and store the result in theq
variables (orgmegabuf
if required). - The same is true for instanced shape and waveform
per_point
code. - Do not overuse
loop
andwhile
, especially avoid nesting them. If they are required, make sure the abort condition on thewhile
loop is not affected by rounding errors. Try not to calculate heavy stuff inside a loop. - Some math functions, specifically trigonometrical ones like
sin
and alsosqrt
, are expensive. Try to cache and reuse values instead of recalculating them multiple times. - Try not to use
megabuf
andgmegabuf
for everything. Normal variables, including the globalregNN
, have less overhead, as they do not require the additional memory allocation checking and address calculation.