Supercollider simple piano sampler

In this post I will run through the process of creating a simple sampler using the SuperCollider platform. If you don’t know what SuperCollider is, I highly recommend you to visit the official SuperCollider website.

I’ve also found this really well-made series of video tutorial by Eli Fieldsteel a great way to start learning it.

Premise

Some time ago, I found this beautiful collection of piano samples from freesound.org (by the way, freesound is one of my favourite websites but this is actually another topic). These are very good samples recorded from a Steinway & Sons piano.

I wanted to create a virtual instrument in SuperCollider with these requisites:

The Problem

If I download the sample collection from freesound.org and take a look at the samples file names, I will see that they force the entire collection to appear completely unorganized as shown in the image below.

unorganized list

If I play the audio files in order, from the first one onward, I will hear sounds from different octaves unevenly mixed up.

The problem remains even after I import them inside buffer objects in SuperCollider.

The Solution

The solution stands in renaming all the audio files in a convenient way in order to load them in ascending order, from the lower tone to the higher one.

Even if the easiest solution remains that of renaming the files manually, I wanted the job done in a quicker and automated way, so I started thinking about a Python script that could do the job. At first I started looking the file name structure, then tryed to figure out what operations could accomplish my goal.

Renamer Script!

Let’s take a look at the first audio file from the collection:

148270__neatonk__piano-med-db4.wav

As you see the file name starts with something that means nothing to us. I would like only to preserve informations about the note name (db), the octave (4) and the stroke pressure (med or loud).

At the same time I wanted to order the files starting from lower the sound to the higher one.

Eventually I came out with this python script (maybe it is not the best declared one but is seemed to work pretty well):

#!/usr/bin/python

import os

for filename in os.listdir("."):
    extension = filename[-4:]

    if extension == ".wav":
        parts = filename.rsplit("-")

        dynamic = parts[1] # es. med, loud
        therest = parts[2] # es. db4.wav
        octave =  int( therest[-5:-4] ) # es. 4
        note = therest[:-5] # es. db

        index = (octave+1) * 12

        if note == "c":
            index = index + 0
        elif note == "db":
            index = index + 1
        elif note == "d":
            index = index + 2
        elif note == "eb":
            index = index + 3
        elif note == "e":
            index = index + 4
        elif note == "f":
            index = index + 5
        elif note == "gb":
            index = index + 6
        elif note == "g":
            index = index + 7
        elif note == "ab":
            index = index + 8
        elif note == "a":
            index = index + 9
        elif note == "bb":
            index = index + 10
        elif note == "b":
            index = index + 11

        if index <100:
            // if the index has only 2 digit, we place a 0 in front of them
            newname = dynamic + "-0"+str(index) + "-" + note + str(octave) + extension
        else:
            newname = dynamic + "-"+str(index) + "-" + note + str(octave) + extension

        os.rename(filename, newname)           

I decided to use the standard MIDI note numbering for indexing the files so that a e7, for example, would have corresponded to the index 100, or a c4 to the 60:

Here’s an image from the folder containing all the med renamed files:

well organized list

SuperCollider

Time to move to SuperCollider. First I loaded the sound files inside buffers using the following code:

~samples = Array.new;
~folder = PathName.new("path/to/your/samples/folder/");

(
~folder.entries.do({
	arg path;
	~samples = ~samples.add(Buffer.read(s, path.fullPath));
});
)

Then I created a SynthDef. I declared some argument in order to be able to modify them from the outside:

(
SynthDef.new(\piano, {
	arg buf, vel=64, gate=0, rate=1;
	var sig, env, amp;
	env = EnvGen.kr(Env.asr(), gate, doneAction:2);
	sig = PlayBuf.ar(2, buf, rate*BufRateScale.ir( buf ));
	amp = LinExp.kr(vel, 1, 127, 0.01, 1);
	sig = sig * env * amp;
	Out.ar(0, sig);
}).add;
)

Next I made an array to store all of the synth instances I’d create every time a new key is pressed:

~keys = Array.newClear(128);

Now I needed MIDI methods and definitions to manage the incoming MIDI messages from the external MIDI keyboard. In particular I needed a noteOn method to create and store the synth inside the ~keys array and to send it thebuf, vel and gate values:

MIDIClient.init;
MIDIIn.connectAll;

(
MIDIdef.noteOn(\noteOnDef, {
	arg vel, note, ch, src;

	~keys[note] = Synth.new(\piano, [
		// MIDI note 23 corresponds to
    // the 0 index in the sample  array
		\buf, ~samples[note-23].bufnum,
		\vel, vel,
		\gate, 1
	]
	);
});
)

Then I needed a method to deal with the noteOff MIDI messages. This method, called noteOff, simply looks inside the ~keys array for the correct synth to refer to.

Then it sets its gate value to zero in order to start the releasing phase and eventually free the synth from the server (see the doneAction argument for the EnvGen method inside the SynthDef).

)
MIDIdef.noteOff(\noteOffDef, {
	arg vel, note, ch, src;
	~keys[note].set(\gate, 0);
});
)

If you find this article useful and you like it, please leave a comment below: let us know what do you think about it, we'd really appreciate it. Thank you very much and, as always, stay tuned for more to come!