Make your own soft synths with open source audio programming languages

In this tutorial we will see how to make our own MIDI controlled soft synths using some of the various audio programming languages available in the opensourcesphere: Csound, SuperCollider, and Chuck.

For each of these languages, I'll show you a basic template (1 sine oscillator + 1 ADSR) that you can use as a starting point to make more interesting instruments.

Csound

Here's the template for a basic polyphonic synthesizer for Csound:

<CsoundSynthesizer>
<CsOptions>
;Edit the line below for your configuration(see 'csound --help').
;Note that Csound is not seen by the ALSA MIDI:
;you'll need to load the snd_seq_virmidi module and connect your
;MIDI ouput to one of the virtual midi devices
-+rtaudio=JACK --sched -+rtmidi=ALSA --midi-device=hw:4,0 -o dac:alsa_pcm:playback_
</CsOptions>
<CsInstruments>  

sr      =      	48000 ;the sampling rate of your audio card 
ksmps  	=     	1
nchnls	=	2 ;stereo 

	instr 1
midinoteoncps p4, p5 
;when a note is triggered, p4 is filled with the frequency of the midi note
;and p5 with the velocity (1-127) 
iattack = .001
idecay = .005
isustain = .8
irelease = .5
ift = 1 ;sine table index 

kamp = p5*50
kenv linsegr 0, iattack, 1, idecay, isustain, irelease, 0 ; ADSR 
kcps = p4

aout	poscil kenv, kcps, ift ;sine oscillator 
	outs	aout*kamp,aout*kamp
	endin

</CsInstruments>
<CsScore>
f 1 0 65536 10 1 ;sine table
i1 10000 1 ;we need this to keep csound alive
e
</CsScore>
</CsoundSynthesizer>

To make things a little more interesting we can add a bandlimited saw oscillator connected to a low pass filter with MIDI controllable frequency and resonance:

...
kmoogfrq	init 0
kmoogres	init 0

	instr 1
kmoogfrq midictrl 74, 1, 16000
kmoogres midictrl 75, 0, 1
; 74 and 75 are the control message numbers for two knobs on my keyboard
...

...
aout		poscil kenv, kcps, ift ;sine oscillator 
aout_saw	vco2 kenv, kcps, 0 ;bandlimited saw oscillator 
aout_saw	moogvcf aout_saw, kmoogfrq, kmoogres ;moog low pass filter 
aout		sum aout, aout_saw
...

Csound handles polyphony by default: every time you play a note and all the slots are full it creates another instance of the instrument. When the release stage of a note is finished the slot gets freed. Automagically. The number of allocated voices never decreases, though. They're reused through the session.

You can also create a whole bank of synths and play them by sending notes on different channels. You can use the massign opcode to assign instruments to channels. So, if I want the synth above to listen to channel 2, instead of the default 1, I'll add:

massign 2, 1 ; instrument 1 on MIDI channel 2

before the instruments definitions.

SuperCollider

Sclang, the SuperCollider programming language is a SmallTalk dialect. It takes some time to grasp it if you've never encountered anything similar before. But it's such a powerful tool!

Here's the code for our bare bones polyphonic synth:

s = Server.local;

s.waitForBoot({
var notes, synth;

MIDIIn.connect;
notes = Array.newClear(128); //array to hold the synth instances

SynthDef("synth1", { arg freq=440, gate=0.0, amp=0.5;
var sine, env;
// sine oscillator - !2 doubles the channels
sine = SinOsc.ar( freq, 0, amp)!2;
// envelope with the same values of the Csound example
env = EnvGen.kr(Env.adsr(0.001, 0.005, 0.8, 0.5), gate,
                Latch.kr(gate, gate), doneAction:2);
Out.ar(0, sine*env);}).send(s);

// function to handle NOTEONs
MIDIIn.noteOn = { arg src, chan, num, vel;
synth = Synth("synth1");
notes.put(num, synth);
synth.set(\freq, num.midicps);
synth.set(\gate, vel/127);
};

// function to handle NOTEOFFs
MIDIIn.noteOff = { arg src,chan,num,vel;
notes[num].set(\gate, 0.0);
};

});

To create a bank of synths, like we've done in Csound, we can modify the template above adding a notes array for each channel:

...
notes = Array.newClear(128);
notes2 = Array.newClear(128);
notes3 = Array.newClear(128);
...

and then modify the MIDIIn.noteOn like this:

MIDIIn.noteOn = { arg src, chan, num, vel;
switch( chan,
0, {
synth = Synth("synth1");
notes.put(num, synth);},
1, {
synth = Synth("synth2"); // the name of the SynthDef to be played on ch 2
notes2.put(num, synth);},
2, {
synth = Synth("synth3"); // the name of the SynthDef to be played on ch 3
notes3.put(num, synth);},
synth.set(\freq, num.midicps);
synth.set(\gate, vel/127);
};

and the MIDIIn.noteOff like this:

MIDIIn.noteOff = { arg src,chan,num,vel;
switch( chan,
0, {notes[num].set(\gate, 0.0);},
1, {notes2[num].set(\gate, 0.0);},
2, {notes3[num].set(\gate, 0.0);});
};

Chuck

Chuck is quite a peculiar language. Its emphasis is on a strongly-timed programming model. To make things happen concurrently you have to wrap them in functions and spork them. Its syntax is Java-like but it uses some custom operators (like => to connect audio modules and to fill variables) and keywords (like spork~).

The '=>' operator has different meanings in different contexts. If it's used between unit generators it connects them. It is used to store a value in a variable too (like in 4 => int device;). When a time value is sent to the keyword now, the execution will wait until the specified time has passed. When an event variable is sent to now (like in: 'min => now' in the code below) it will wait until the event has happened.

We'll make a monophonic synth, first:

4 => int device; // my midi device number (find your with: 'chuck --probe')

MidiIn min;

// we declare and connect our chain of unit generators
// the sine oscillator, connected to the ADSR,
// connected to the audio output
SinOsc s => ADSR e => dac;
e.set( 1::ms, 5::ms, .8, 500::ms );
e.keyOff();

// we spork the midi event handler funcion (defined below)
spork ~ midiEvent( min );

// loop
while( true ) 1::second => now;

// here's the function to handle MIDI notes messages
fun void midiEvent( MidiIn min ){
    MidiMsg msg;
    while( true )
    {
        min => now;

        while( min.recv( msg ) )
        {
	    if (msg.data3 != 0){
                msg.data2 => Std.mtof => s.freq;
		e.keyOn();}
	    else
		e.keyOff();
        }
    }
}

To make it polyphonic we have to decide the number of voice beforehand, and spork an instance of the note handler function for each voice.

4 => int device; // my midi device number (find your with: 'chuck --probe')

MidiIn min;
if ( !min.open( device ) ) me.exit();

// we extend the Event class to make our note event class
class NoteEvent extends Event
{
    int note;
    int velocity;
}

NoteEvent on;
Event @ notes[128]; // array to store the notes status
MidiMsg msg;

// This the function that will be executed for a note.
// We create the units and connect them on the fly.
// When the note is finished we have to remove the 
// event instance from the array.
fun void handlenote()
{
	SinOsc s => ADSR e;
	Event off;
	int note;
	
	e.set( 1::ms, 5::ms, .8, 500::ms );
	e.keyOff();
	
	while( true ) 
	{
		on => now; // note on happened
		on.note => note;
		e => dac;
		e.keyOn();
		Std.mtof( note) => s.freq;
		on.velocity / 127.0 => s.gain;
		off @=> notes[note];
		off => now; // note off happened
		null @=> notes[note];
		e.keyOff();
		500::ms => now; // wait till the release stage is finished.
		e =< dac;
	}
}

// 12 notes of polyphony.
for( 0 => int i; i < 12; i++) spork ~ handlenote();

while( true ) 
{
	min => now;
	
	while( min.recv( msg ) )
	{
		if (msg.data3 != 0){
			msg.data2 => on.note;
			msg.data3 => on.velocity;
			on.signal();
			me.yield();
		}
		else
		{
			notes[msg.data2].signal();
		}
	}
}

Lisp

For anyone who likes lisp, here's how to do the exact thing with Snd (http://www.notam02.no/arkiv/doc/snd-rt/):

(definstrument (osc-synth :key 
			  (volume 0.2)
			  (attack 0.01)
			  (decay 0.05)
			  (release 0.5))

  (for-each (lambda (my-note)
	      (let* ((attack (make-env (append (list 0 0)
					       (list attack (* 1.9 volume))
					       (list (+ attack decay) 1))
				       :duration (+ attack decay)))
		     (release (make-env (append (list 0 1)
						(list decay 0))
					:duration decay))
		     (is-attacking #f)
		     (is-releasing #f)
		     (is-playing #f)
		     (osc (make-oscil :frequency (midi-to-freq my-note))))
		( (lambda ()
			     (receive-midi (lambda (control data1 data2)
					     (if (= data1 my-note)
						 (begin
						   (set! control (logand #xf0 control))
						   (cond ((and (= control #x90)
							       (> data2 0)
							       (not is-playing))
							  (set! is-playing #t)
							  (set! is-attacking #t))
							 ((and is-playing
							       (or (and (= control #x90)
									(= data2 0))
								   (= control #x80)))
							  (set! is-releasing #t)))))))
			     (cond (is-attacking
				    (out (* volume (env attack) (oscil osc)))
				    (if (>= (mus-location attack) (mus-length attack))
					    (set! is-attacking #f)))
				   (is-releasing
				    (out (* volume (env release) (oscil osc)))
				    (if (>= (mus-location release) (mus-length release))
					(begin
					  (set! is-releasing #f)
					  (set! is-playing #f)
					  (mus-reset osc)
					  (mus-reset attack)
					  (mus-reset release))))
				   (is-playing
				    (out (* volume (oscil osc)))))))))
	    (iota 128)))

(osc-synth)