16 -Karplus/Strong, Synthdef, Server commands 

The Karplus/Strong pluck is another classic noise-based instrument. It is more subtractive than additive but closer to physical modeling, where you recreate the process of an instrument rather than build the resulting sound. In an earlier chapter we experimented with a millisecond of noise copied and pasted into a new file over and over and discovered that any periodic wave, no matter how complex, is heard as a pitch. Karplus-Strong achieves the same effect using a delay ugen. 

Karplus-Strong Pluck Instrument 

We begin with a very short burst of noise as in the chime. 

16.1. noise burst (scope, EnvGen, PinkNoise) 

(
{ 
	var burstEnv, att = 0, dec = 0.001; //Variable declarations
	burstEnv = EnvGen.kr(Env.perc(att, dec), gate: Impulse.kr(1)); //envelope
	PinkNoise.ar(burstEnv); //Noise, amp controlled by burstEnv
}.play 
)

The next step is to send the burst of noise through an echo chamber; CombL, which has these arguments in, maxdelaytime, delayTime, decayTime, mul, add. The input is going to be the burst of noise we just created. The delaytime and maxdelaytime are the same for this example. They represent the amount of time, in seconds, the signal is delayed (the echo). The decaytime is how long it takes for the echo to die away. The variable "out" is necessary to store the burst of noise since it is used two places in the CombL. Try changing the delayTime and the decayTime. Only one burst of noise is needed so the gate has been removed. 

16.2. burst and delay (PinkNoise, EnvGen, Env, perc, CombL) 

(
{ 
	var burstEnv, att = 0, dec = 0.001; 
	var out, delayTime = 0.5, delayDecay = 10;
	burstEnv = EnvGen.kr(Env.perc(att, dec)); 
	out = PinkNoise.ar(burstEnv); 
	CombL.ar(
		out, 
		delayTime, 
		delayTime, 
		delayDecay, 
		add: out); //Echo chamber
}.play //End Ugen function
)

Change the delay time to 0.1 (ten times per second, or 10 Hz), 0.01 (100 times per second, or 100 Hz), then 0.001 (1000 Hz), etc. The delay time is the reciprocal to the pitch we hear (1/100th of a second, 100 Hz). What delay time would we enter for A 440? 

SC can do the calculation with the reciprocal message, which can be combined with midicps. 

16.3. midi to cps to delay time 

69.midicps.reciprocal;

// Insert this section into the pluck instrument.

(	
{ 
	var burstEnv, att = 0, dec = 0.001;
	var burst, delayTime, delayDecay = 10;
	var midiPitch = 69; // A 440
	delayTime = midiPitch.midicps.reciprocal;
	burstEnv = EnvGen.kr(Env.perc(att, dec)); 
	burst = PinkNoise.ar(burstEnv); 
	CombL.ar(burst, delayTime, delayTime, 
		delayDecay, add: burst); 
}.play 
)

Insert postln messages to check values. 

Synth definitions 

In the post window you have probably noticed something like the following. 

Synth("-429504755" : 1008) 

Synth("-1768726205" : 1009) 

Synth("-2052671376" : 1010) 

Synth("-713843897" : 1011) 

Synth("2119841241" : 1012) 

They are synth definition names (e.g. 2119841241) followed by a node (e.g. 1012) that are created automatically each time an example is run. So far we've been using a short cut, allowing SC to create a new arbitrary synth definition each time a section of code is run. It creates a new synth even if you run the same example a second time. But this is inefficient. It would be better to replay a definition that already exists. Try these lines of code, replacing the number with what you see in your post window. (You can use copy and paste, or you can just remove the ":" and node number (e.g. : 1012) and run that statement in the post window.) 

16.4. playing a synthDef 

//This first
{SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play

//You will see something like this in the post window
//448
//Synth("1967540257" : 1012)

//Then this, replacing 1967540257 with the number in your post window
Synth("1967540257");

We have also been stopping each example using command period, which stops all processes. If the Synth is assigned to a variable you can stop it explicitly with the free message. Run these two lines separately; again replace the number with what you see in the post window. 

16.5. stopping a synthDef 

a = Synth("1967540257");

a.free;
 
We can bypass finding the synth name by assigning the original patch to a variable. We can also run several examples and stop them separately or one at a time. 

16.6. playing a synthDef 

//Run these one at a time
a = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play

b = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play

c = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play

d = {SinOsc.ar(rrand(700, 1400), mul: 0.1)}.play

//Then either of these

a.free; b.free; c.free; d.free

//or one at a time

a.free;
b.free;
c.free;
d.free

The SynthDef object allows you to give the patch a name and add arguments. An argument is similar to a variable and is used to pass values to the function. You can set arguments when a synth is first created or after it is running. The first example below is just the patch in the style we have been using so far. The next couches it in a synth definition with a name and arguments. 

16.7. SynthDef 

(//Original patch
{
var rate = 12, att = 0, decay = 5.0, offset = 400;
var env, out, pan; 
pan = LFNoise1.kr(1/3);
env = EnvGen.kr(Env.perc(att, decay));
out = Pan2.ar(
			Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), 
				(env)*12 + 1, 0.3),
			pan)*env;
out
}.play
)

//SynthDef (naming it) and arguments

(
SynthDef("SH", 
{
arg rate = 12, att = 0, decay = 5.0, offset = 400;
var env, out, pan; 
pan = LFNoise1.kr(1/3);
env = EnvGen.kr(Env.perc(att, decay), doneAction: 2);
out = Pan2.ar(
			Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), 
				(env)*12 + 1, 0.3),
			pan)*env;
Out.ar(0, out)
}).play
)

Watch the post window when you run this example. You will see that the long number is replaced by the name "SH." 

With SynthDef a few things that were previously done automatically need to be explicit. The first is assigning an output bus using Out.ar. (More on busses later.) The next is stopping the patch when it is done playing. We use the envelope to generate the event, but there are a number of possible results when the envelope terminates. The default is to do nothing. Changing it to doneAction: 2 signals the server to deallocate and free up the processing power used for that ugen when it is done. (To see its importance try commenting out this line of code when running the examples below. Watch the peak CPU in the synth server add up and eventually explode.) 

Now that the code is loaded into the server we can run several copies of the instrument with control values (for the arguments) using set. The arguments are placed in an array with the syntax symbol (A symbol is a word preceded by a backslash: \rate. ), value: [\rate, 10, \offset, 200]. Run these lines singly but one right after the other. (SC makes this easy because when you run one line the cursor moves to the next line. So you can just keep pressing enter.) 

16.8. Multiple nodes of SH 

//Three separate nodes of "SH" with different arugments. 
//Run these three lines in sequence then stop.
a = Synth("SH", [\rate, 10, \offset, 200]);
b = Synth("SH", [\offset, 400, \att, 3.0, \decay, 0]);
c = Synth("SH", [\rate, 30, \offset, 2000]);

//Let them die out or run these lines to stop them.
a.free; 
b.free; 
c.free;

//Changing a parameter of an existing node. Run these lines in sequence. 
a = Synth("SH", [\rate, 23, \offset, 30, \decay, 20]);
a.set(\offset, 1000)
a.set(\offset, 300)
a.set(\offset, 800)

a.free;

//Two nodes with arguments
a = Synth("SH", [\rate, 7, \offset, 200, \decay, 20]);
b = Synth("SH", [\rate, 23, \offset, 1200, \decay, 20]);
a.set(\offset, 40)
b.set(\offset, 1000)
a.set(\offset, 800)
b.set(\offset, 600)
a.set(\offset, 1200)
b.set(\offset, 50)

a.free; b.free

You can also set arguments using the argument number followed by value, or string, value, as shown below. 

16.9. Syntax for passing arguments 

//Same thing
a = Synth("SH", [\rate, 10, \offset, 200]);
a = Synth("SH", [0, 10, 3, 200]);
a = Synth("SH", ["rate", 10, "offset", 200]);
a = Synth("SH", ["rate", 10, "offset", 200]); 

Finally you can indicate a lag time between control changes for a smooth transition. 

16.10. Transition time between control changes 

//SynthDef, arguments, transition

(
SynthDef("SH", 
{
arg rate = 12, att = 0, decay = 5.0, offset = 400;
var env, out, pan; 
pan = LFNoise1.kr(1/3);
env = EnvGen.kr(Env.perc(att, decay), doneAction: 2);
out = Pan2.ar(
			Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), 
				(env)*12 + 1, 0.3),
			pan)*env;
Out.ar(0, out)
},
[0.5, 0.1, 0, 4] //transition for each argument above
).play
)

a = Synth("SH", [\rate, 6, \decay, 20, \offset, 200]);
a.set(\rate, 18);
a.set(\offset, 1000);

We could have changed the parameters in the original patch and run it again. But that would be less flexible. Using a SynthDef with arguments now allows us to create copies of instruments with different control values automatically. 

The loop message repeats a function. Task allows you to pause the loop. (Don't try a loop without the pause, or at least save your work before you do.) With each loop a new SH is created with random values as arguments, or controls. 

16.11. Multiple nodes of SH 

(
r = Task({
	{      
		Synth("SH", [
		\rate, exprand(3.0, 22.0),
		\decay, rrand(0.5, 15.0),
		\att, [0, rrand(0, 3.0)].choose,
		\offset, rrand(100, 2000)], target:s);
		rrand(1.0, 5.0).wait; //wait time between repetitions
	}.loop; //repeat this function
}).play
)

r.stop;

Try changing any of the values in these examples. Also try inserting postln messages to monitor values (e.g. rrand(1.0, 5.0).wait.postln). 

Finally, once you have honed the instrument down to where it is worth saving, you can write a copy to disk (synthdefs folder of the SC folder). The next time you launch SC you can just run the synth by name. (It is important to send it to the correct server, whichever is running. If you click on the default button of the running server it will go to that server. Or you can use the target argument to send it to one or the other. I've included the boot expression below to illustrate the second method.) 

16.12. Multiple nodes of SH 

(//Save to file and load in server "s"
SynthDef("SH", 
{
arg rate = 12, att = 0, decay = 5.0, offset = 400;
var env, out, pan; 
pan = LFNoise1.kr(1/3);
env = EnvGen.kr(Env.perc(att, decay), doneAction: 2);
out = Pan2.ar(
			Blip.ar(LFNoise0.ar(rate, min(100, offset), offset), 
				(env)*12 + 1, 0.3),
			pan)*env;
Out.ar(0, out)
}).load(s)
)

//Now quit SC, look in the synthdefs folder for "SH.scsyndef"

//Launch SC and run these lines

s = Server.internal; s.boot;

a = Synth("SH", [\rate, 10, \offset, 200], target: s);

After you've compiled a nice library of instruments like the KSpluck you will probably forget what control busses, output busses, and arguments they had. You can look up all that information using these lines. 

16.13. SynthDef Browser 

(
// a synthdef browswer
SynthDescLib.global.read;
SynthDescLib.global.browse;
)

Below is the KSpluck in a SynthDef and then a repeating routine. 

16.14. KSpluck SynthDef (EnvGen, Env, perc, PinkNoise, CombL, choose) 

(
//First load the synth and save to disk
SynthDef("KSpluck", 
	{
		arg midiPitch = 69, delayDecay = 1.0;
		var burstEnv, att = 0, dec = 0.001;
		var signalOut, delayTime;
		delayTime = [midiPitch, midiPitch + 12].midicps.reciprocal;
		burstEnv = EnvGen.kr(Env.perc(att, dec)); 
		signalOut = PinkNoise.ar(burstEnv); 
		signalOut = CombL.ar(signalOut, delayTime, delayTime, 
			delayDecay, add: signalOut); 
		DetectSilence.ar(signalOut, doneAction:2);
		Out.ar(0, signalOut)
	}
).load(s);
)

(
//Then run this playback task
r = Task({
	{Synth("KSpluck", 
		[
		\midiPitch, rrand(30, 90), //Choose a pitch
		\delayDecay, rrand(0.1, 1.0) //Choose duration
		]);
		//Choose a wait time before next event
		[0.125, 0.125, 0.25].choose.wait;
	}.loop;
}).play(SystemClock)
)

//Stop it

r.stop;

The right channel is doubled at the octave (+ 12 in MIDI). Try adding an argument allowing you to double at other intervals (fifth, fourth, third, etc.) Change the choices of wait time. Why in this patch did I put two values of 0.125 and one of 0.25? Add postln messages to check variables. Add a statement that links MIDI pitch with decay time such that high notes have short decays and low notes long decays. 

Listen carefully to each attack and the character of each pitch. It is clearly the same instrument, yet each note has a slightly different timbre. The character changes because each time the function "pluckInst" is run a new burst of noise is used, which has a different wave shape. Traditional presets on synthesizers lack this complexity, which is inherent in natural instruments. 

Practice: Karplus-Strong Patch 

In the practice K-S patch the argument midiPitch is set from an array of "legal" pitch choices. It is then added to an array of octave choices. The variable art (articulation) has replaced delayDecay because it is used to shorten or lengthen each attack. Notice that the burst envelope supplies a stereo (array) signal to out. Even though you hear one pitch, the left and right channel should be slightly different. Also the delayTime is set using a stereo array; the right an octave higher (MIDI numbers). The entire patch is passed through an RLPF with LFNoise1 control for the filter sweep. The wait array uses a quick method for biasing choices: load the dice. Since there are 5 instances of 0.125 and one of 1, then 0.125 will be chosen about 86% of the time. 

Try changing the midiPitch array to various scales: whole tone, diatonic, chromatic, octatonic, quarter tone, etc. Try adding stereo arrays to other aspects of the patch, e.g. LFNoise rate or filter cutoff. 

16.15. Practice: K S pluck (EnvGen, PinkNoise, LFNoise1, Out, DetectSilence) 

//Load this definition
(
SynthDef.new("KSpluck3", 
	{ //Beginning of Ugen function
	arg midiPitch, art;
	var burstEnv, att = 0, dec = 0.01, legalPitches; //Variable declarations
	var out, delayTime;
	delayTime = [midiPitch, midiPitch + 12].midicps.reciprocal;
	burstEnv = EnvGen.kr(Env.perc(att, dec)); 
	out = PinkNoise.ar([burstEnv, burstEnv]); //Noise burst
	out = CombL.ar(out, delayTime, delayTime, 
		art, add: out); //Echo chamber
	out = RLPF.ar(out, LFNoise1.kr(2, 2000, 2100), 0.1); //Filter
	DetectSilence.ar(out, doneAction:2);
	Out.ar(0, out*0.8)
	}
).load(s);
)

//Then run this routine

(
r = Task({
	{Synth("KSpluck3",
	[
	\midiPitch, [0, 2, 4, 6, 8, 10].choose + [24, 36, 48, 60].choose,
	\art, [0.125, 0.25, 0.5, 1.0, 2.0].choose
	]);
	//Choose a wait time before next event
	[0.125, 0.125, 0.125, 0.125, 0.125, 1].choose.wait;
	}.loop;
}).play(SystemClock)
)

16. Exercise 

16.1. In a KS patch, what delay time would you use to produce these frequencies: 660, 271, 1000, 30 Hz? 

16.2. Rewrite the chimes and cavern patch from the past chapter as two synth definitions written to the hard disk with arguments replacing the variables for base frequency. Add a Synth to launch each instrument. 

16.3. Create a Task that generates multiple layered chimes and caverns.