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 declares the variables too and it is not possible to assign the same variable twice


Custom Block definition

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

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.


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 above example 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

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
	}
}
phase is only visible within the first branch and gets updated only when then codition is met.

Compilation

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.


Further info

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.