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/):