Ciaramella
A declarative language for audio DSP.
- Minimalistic
- Data flow
- Declarative
- Modular
Getting Started
Ciaramella aims to be minimal and essential, therefore its syntax is extremely simple.
Assignments
Assignments are in the form:
variable = expression
or:
var1, var2 = expression
An assignment also declares the variables, so the same variable cannot be assigned twice.
Custom Block Definition
The user can define reusable custom composite blocks, similar to 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.
Blocks with multiple outputs are supported too:
y1, y2 = my_stereo_gain (x1, x2, vol1, vol2) {
y1 = x1 * vol1
y2 = x2 * vol2
}Expressions
Expressions follow a conventional syntax. You can use standard arithmetic operations and composite blocks defined by the user:
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 or my_stereo_gain is called block instantiation in Ciaramella terminology. Blocks with multiple outputs cannot be used directly inside other expressions.
Delay
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 example above implements a basic low-pass filter.
A Complete Program
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
}If-Then-Else
Conditional constructs are defined like blocks while preserving the same 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 useful feature is that state can be used inside branches:
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
}
}Here, phase is only visible inside the first branch and gets updated only when the condition is met.
Compilation
The compiler developed for Ciaramella is called Zampogna and it is available on GitHub. It ships with a command-line interface for Node.js. Alternatively, you can use the web playground to write code, compile it, and immediately hear the result.
To compile the previous example from a file called lp.crm:
zampogna -i lp3stereo -c volumeL,volumeR -t cpp lp.crm
The -i lp3stereo flag selects the initial block (similar to main in C). The -c volumeL,volumeR flag tells the compiler that volumeL and volumeR are user controls and not audio signals.
Further Info
For the history and motivation behind the project, you can read this blog post.
For questions, contact info@orastron.com.