.
Ciaramella aims to be minimal and essential, therefore its syntax is extremely simple.
Assignments are in the form
variable = expression
or
var1, var2 = expression
An assignment declares the variables too and it is not possible to assign the same variable twice
The user can define reusable custom composite blocks, like defining functions in C:
y = my_mixer (x1, x2) { tmp = x1 + x2 y = tmp * 0.5 }
It defines a block called "my_mixer" which takes 2 inputs and outputs 1 value. Within its body it is possible to create new temporary variables.
An assignment declares the variables too and it is not possible to assign the same variable twice.
It is possible to define blocks with multiple outputs too:
y1, y2 = my_stereo_gain (x1, x2, vol1, vol2) { y1 = x1 * vol1 y2 = x2 * vol2 }
Expressions follow a conventional syntax. It is possible to use standard arithmetic operations and the composite blocks defined by the user. For example:
sum = a + b + (d * e) / 2 mix = 0.5 * my_mixer(a, b) t1, t2 = my_stereo_gain (a, b, 0.2, 0.7)
Using a composite block (like my_mixer and my_stereo_gain) is called block instantiation in the Ciaramella terminology.
Intuitively blocks with multiple outputs cannot be used within other expressions.
Ciaramella comes with the delay1 (unitary delay) operator to access the previous value of a variable. It is necessary to create loops:
y = lp (x) { y = delay1(y) + 0.1 * (x - delay1(y)) @y = 0 }
The @ operator sets the initial value of the variable. It is needed for the first iteration.
The above example implements a basic low pass filter
The following example shows the implementation of some trivial multiple-pole low pass filters.
b = 0.1 y = lp (x) { y_z1 = delay1(y) y = y_z1 + b * (x - y_z1) @y = 0 } y = lp3 (x) { y = lp (lp(lp(x))) } yL, yR = lp3stereo (xL, xR, volumeL, volumeR) { yL = lp3 (xL) * volumeL yR = lp3 (xR) * volumeR }
We lately introduced if-then-else constructs. They are defined like blocks in the usual fully declarative style
y = decimator(x) { y, s = if (delay1(s)) { y = x s = 0 } else { y = delay1(t) s = 1 } t = y @s = 1 @y = 0 }
A cool feature is the possibility to use state (delay) within branches. For example:
y = saw_generator(enable, frequency) { y = if (enable > 0.5) { phaseInc = mapFreq(frequency) / fs phase = frac(delay1(phase) + phaseInc) @phase = 0 y = 2 * phase - 1 } else { y = 0 } } y = mapFreq (fr) { y = fr * fr * fr * 10000 + 20 } # Only for fs >= 10020 y = frac (x) { y = if (x >= 1) { y = x - 1 } else { y = x } }
The compiler we developed for Ciaramella is called Zampogna and it is downloadble here. It comes with the zampogna-cli.js command line interface which is easily usable via nodejs. Alternatively you can try the web version of zampogna online which comes with a text editor and an execution section to directly listen to the effect of you code!
Zampogna-cli is easily usable. For example, to compile the previous example, assuming you saved it in lp.crm, you can use:
zampogna.js -i lp3stereo -c volumeL,volumeR -t cpp lp.crm
the "-i lp3stereo" selects the lp3stereo block as the initial one (something like the main in C). The "-c volumeR,volumeR" tells the compiler that volumeL and volumeR are user control inputs and not audio signals.
For the history and motivation behind the project, you can check this blog post.
In any case, feel free to contact us at info@orastron.com.