Ciaramella

A declarative language for audio DSP.

  • Minimalistic
  • Data flow
  • Declarative
  • Modular
Try Ciaramella Online

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.