15 -Subtractive Synthesis, Noise, Message Chains 

In subtractive synthesis you begin with a sound or wave that has a rich spectrum and modify it with a filter. The results are often more natural because most natural sounds come about in the same way. The tone of a piano, violin, or guitar begins with a source of random excitation; the bowed string, the plucked or hammered string. It is periodic repetition of that chaotic event and the body of the instrument that shape the character of the sound. The same is true of the human voice. The vocal chords alone represent an unintelligible but rich fundamental sound source. The tongue, sinus cavities and shape of the mouth determine the rich tone qualities of speech. 

There are many rich sound sources available in SC and other synthesizers. If richness is defined by the number of sine waves that are represented in the sound, then arguably the richest is pure noise. In the previous chapter you may have noticed that the greater the number of enharmonic spectral elements there were the more the sound approached noise. Noise is often defined as all possible frequencies having equal representation. I like to think of it as any series I don't recognize. 

There are 15 or so different types of noise available in SC. Some of them are used primarily for audio signal, others for control. The common denominator is that they appear to be random, that is, we don't recognize any pattern in the wave or series of numbers. Control rate noise is not often heard as a signal but is used to generate random elements. Listen to each of these examples of noise. Notice the differences in how they look on the scope. 

15.1. noise (scope, WhiteNoise, PinkNoise, BrownNoise, GrayNoise, Dust) 

{WhiteNoise.ar}.scope(1)

{PinkNoise.ar}.scope(1)

{BrownNoise.ar}.scope(1)

{GrayNoise.ar}.scope(1)

{Dust.ar(12)}.scope(1)

Subtractive synthesis uses filters to remove frequencies from a rich wave. Three common filters are low pass, high pass, and band pass. The objects in SC that represent these devices are RLPF, RHPF, and BPF. The arguments for each are input, frequency cutoff, and the reciprocal of q (bandwidth/cutoff frequency). The input is the signal being filtered. The cutoff frequency is the point where frequencies begin to be filtered. Resonance effects how narrow the focus of the filter is. The practical result of a narrow focus (e.g. 0.05) is a pitch at the filter cutoff. 

In the examples below the X axis controls the frequency cutoff, the Y axis controls the resonance. The top of the screen is a narrow resonance (0.01) and the bottom is wide (2.0). 

15.2. Filtered Noise (scope, PinkNoise, MouseX and Y, RLPF, RHPF, BPF) 

(
{
var signal, filter, cutoff, resonance;

signal = PinkNoise.ar(mul: 0.7);
cutoff = MouseX.kr(40, 10000, 1);
resonance = MouseY.kr(0.01, 2.0);

RHPF.ar(signal, cutoff, resonance)}.scope(1)
)

(
{
var signal, filter, cutoff, resonance;

signal = PinkNoise.ar(mul: 0.7);
cutoff = MouseX.kr(40, 10000, 1);
resonance = MouseY.kr(0.01, 2.0);

RLPF.ar(signal, cutoff, resonance)}.scope(1)
)

(
{
var signal, filter, cutoff, resonance;

signal = PinkNoise.ar(mul: 0.7);
cutoff = MouseX.kr(40, 10000, 1);
resonance = MouseY.kr(0.01, 2.0);

BPF.ar(signal, cutoff, resonance)}.scope(1)
)

Here are similar examples using a saw wave (which is a rich sound by virtue of the upper harmonic content). When a low number is used for resonance on noise you hear a single continuous frequency that corresponds to the filter cutoff. With a saw wave each of the upper harmonics will resonate as the cutoff frequency passes near and over the harmonic. This familiar filter sweep was pretty cool back in the 70s. 

The volume is quite low when the resonance is narrow. Maybe you could add a variable that adjusts the volume as the resonance decreases? 

15.3. Saw with Filter (scope, LFSaw, MouseX and Y, RHPF) 

(
{
var signal, filter, cutoff, resonance;

signal = Saw.ar([50, 75], mul: 0.7);
cutoff = MouseX.kr(40, 10000, 1);
resonance = MouseY.kr(0.01, 2.0);

RLPF.ar(signal, cutoff, resonance)}.scope(2)
)

(
{
var signal, filter, cutoff, resonance;

signal = Saw.ar([50, 75], mul: 0.7);
cutoff = MouseX.kr(40, 10000, 1);
resonance = MouseY.kr(0.01, 2.0);

BPF.ar(signal, cutoff, resonance)}.scope(2)
)

{RLPF.ar(Saw.ar([100,250],0.1), XLine.kr(8000,400,5), 0.05)}.play;

These filters are rather simple. Klank is a filter that allows you to specify banks of frequencies, amplitudes, and decay rates. This is a physical model of spaces or bodies (such as a room or instrument) that resonate on a collection of frequencies rather than a single low or high cutoff. 

The first argument for Klank is an array of arrays (two dimensional array). Each item in the array is also an array (like a box filled with boxes). The outer array begins with a "`" to protect against multi-channel expansion. (If this were left off SC would interpret the array as different channels. Go ahead and try it.) The first inner array is the set of frequencies that resonate, the second is the amplitude of each frequency (default is 1), and the last is the decay rates of each frequency. With steady state sounds decay has little affect, so it's left off. Since we are adding 10 resonant frequencies the noise source has to be very quiet: 0.01 to 0.1. The second example fills the arrays using Array.series. Look at the help file for an explanation. The third example loads an array with random frequencies. Run it several times to see how it is different every time. 

15.4. Resonant array (scope, Klank, ClipNoise, Array.series, BrownNoise) 

(
{
Klank.ar(
	`[
	[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], //freq array
	[0.05, 0.2, 0.04, 0.06, 0.11, 0.01, 0.15, 0.03, 0.15, 0.2]
	
	],
	PinkNoise.ar(MouseX.kr(0.01, 0.1)))
}.scope(1)
)

(
{
Klank.ar(
	`[Array.series(10, 50, 50), 
	  Array.series(10, 1.0, -0.1)],
	ClipNoise.ar(0.01)
)
}.scope(1)
)
	
(
{
Klank.ar(
	`[{exprand(60, 10000)}.dup(15)],
	PinkNoise.ar(0.005)
)
}.scope(1);
)

Chimes, Message Chains 

Earlier we created a bell using additive synthesis; building the characteristic ring by adding together random frequencies. In this patch we approach a bell from the opposite direction; removing or filtering all but a set of random frequencies. 

The clapper striking the side of a bell is a burst of noise. (Try holding a bell so it doesn't resonate and strike it with the clapper. You just hear a short tick.) The physical construction of the bell or chime resonates on certain frequencies. Those frequencies are sustained and all others die away quickly. The remaining frequencies are what make up the character of the bell or chime. For this patch we begin with an extremely short burst of noise; .0001 seconds. 

15.5. chime burst (Env, perc, PinkNoise, EnvGen, Spawn, scope) 

(
{
var burstEnv, att = 0, burstLength = 0.0001, signal; //Variables
burstEnv = Env.perc(0, burstLength); //envelope times
signal = PinkNoise.ar(EnvGen.kr(burstEnv, gate: Impulse.kr(1))); //Noise burst
signal;
}.play
)

Try different noise sources such as WhiteNoise, BrownNoise, ClipNoise and try longer attacks and decay. 

For a resonator we use Klank.ar, generating the arrays with the dup message. The input argument for Klank is the noise burst from above. 

15.6. chimes (normalizeSum, round, Klank, EnvGen, MouseY) 

(
{
var chime, freqSpecs, burst, totalHarm = 10;
var burstEnv, att = 0, burstLength = 0.0001;

freqSpecs = `[
	{rrand(100, 1200)}.dup(totalHarm), //freq array
	{rrand(0.3, 1.0)}.dup(totalHarm).normalizeSum.round(0.01), //amp array
	{rrand(2.0, 4.0)}.dup(totalHarm)]; //decay rate array

burstEnv = Env.perc(0, burstLength); //envelope times
burst = PinkNoise.ar(EnvGen.kr(burstEnv, gate: Impulse.kr(1))); //Noise burst

Klank.ar(freqSpecs, burst)*MouseX.kr(0.1, 0.8)
}.scope(1)
)

This patch uses a few new conventions. One is variable assignment in the declaration line using the syntax "var totalHarm = 20, freqSpecs, att = 0;" etc. The second is message chains. It is possible to link several messages together as illustrated below. 

15.7. message chains 

//separate messages

[30, 23, 87].choose; //choose one member of the array

20.rand; //choose a number between 0 and 20

71.midicps; //convert the midi number to cycles per second (Hz)

//Now all of them combined in several lines of code. 
(
a = [30, 56, 32]; //store array in variable a
a = a.choose; //choose a value from a and store it in b
a = a.rand; //choose a random number from 0 to b
a = a.midicps; //calculate the cps given c as a midi number
a.postln; //post the results
)

//The same thing using a message chain

[30, 23, 87].choose.rand.midicps.postln

The number of harmonics, amplitudes, and decay rates are set with totalHarm. Each array in the freqSpecs array is filled automatically with values generated by the function and the dup message. The variable burst is inserted as the second argument to Klank.ar, along with the freqSpecs array, and that result is played. 

You may have noticed there are several places where you can adjust the amplitude of the signal in a patch. The same is true with most electro-acoustic music studios. In this patch you can adjust the volume in the EnvGen, or at the PinkNoise, at the Klank or at the final EnvGen. 

Try adjusting the total number of harmonics and burstLength. Try adding a variable for attack. A softer attack may represent a softer strike. Change the range of frequencies in the freq array. Change the range of choices for decay rates. Finally, try replacing any of the freqSpecs functions with your own array of frequencies. (You just have to make sure you enter the same number of frequencies as stipulated with totalHarm, since those arrays will be of that size.) Try even harmonics, odd harmonics, slightly detuned harmonics. Could you write a function that would automatically fill the array with even or odd harmonics? 

Debugging, commenting out, balancing enclosures, postln 

How can you be sure the frequency collection is what you wanted (random, odd, even, detuned, etc.)? Likewise, how do you go about isolating problems in a patch that isn't doing what you want? 

The first thing I do when something is wrong is to isolate and evaluate small pieces of code. If you are clever you can select and run a single line or even a section of a line. You have to be careful about commas and variables. For example, if you select and run " {rrand(100, 1200)}.dup(totalHarm)," you will get an error because of the comma and missing variable declaration. But if you replace the totalHarm with 10 and select everything but the comma, it will run and SC will print the results in the post window. 

You can also isolate problems by "commenting out" a section or line of code. Again, you have to be careful about semicolons and commas. Comments are identified by "//" to the end of the line or "/*" to begin the comment and "*/" to end the comment. If you suspect a problem with the freqSpecs arrays you could insert a single "safe" value in place of the random array: {0.2 /*rrand(0.3, 1.0)*/}.dup(totalHarm). It will fill the entire array with 0.2 rather than the rrand(0.3, 1.0). 

I sometimes use a sort of A/B switch, as shown below. The comment and uncomment command keys are useful in switching between the two sections of code. The "safe" section is something that I know works. If, for example, I have four pages of complex code in a function that makes no sound and I want to just check to see if the server is working I can place a simple SinOsc.ar as the last line of the function and that line will be played while everything above it is ignored. 

15.8. commenting out 

(
{
var chime, freqSpecs, burst, totalHarm = 10;
var burstEnv, att = 0, burstLength = 0.0001;

freqSpecs = `[ //"experimental" collection
	{rrand(100, 1200)}.dup(totalHarm), //freq array
	{rrand(0.3, 1.0)}.dup(totalHarm).normalizeSum.round(0.01), //amp array
	{rrand(2.0, 4.0)}.dup(totalHarm)]; //decay rate array

//freqSpecs = `[ //"safe" collection
//	[100, 200] //freq array
//	[0.5, 0.5] //amp array
//	[0.1, 0.1]]; //decay rate array

When the second freqSpecs is commented it is not included in the patch. If you remove the comments (opt-com-/) then the safe section will replace the experimental section. (You don't have to comment out the experimental section, it is just reassigned.) If the patch then runs correctly, there is something wrong with the experimental part. 

Another common error is mismatched enclosures. Com-shift-b "balances" enclosures by highlighting everything between the nearest outer enclosure. When it cannot find a match it gives an error flash. To locate the problem place the cursor inside an obviously correct enclosure (in the example above, perhaps the rrand(2.0, 4.0) and repeatedly press com-shift

b. It will balance as far as it can and flash when there is a missing enclosure. The last debugging strategy uses post or postln to print the results of an operation to the post window. There are intentional errors in the patch below. Can you spot them by examining the posted data? 

15.9. debugging using postln 

(
{
var chime, freqSpecs, burst, totalHarm = 10;
var burstEnv, att = 0, burstLength = 0.0001;

freqSpecs = `[
	rrand(100, 1200).dup(totalHarm).postln, 
	{rrand(0.3, 1.0)}.dup(totalHarm).normalizeSum.round(0.1).postln,
	{rrand(2.0, 4.0)}.dup(totalHarm).round(0.1).postln]; 

burstEnv = Env.perc(0, burstLength); //envelope times
burst = PinkNoise.ar(EnvGen.kr(burstEnv, gate: Impulse.kr(1))); 

Klank.ar(freqSpecs, burst)*MouseX.kr(0.1, 0.8)
}.scope(1)
)

The postln message can be added within a message chain, as shown in this example. You can 
remove them with find and replace. 

15.10. debugging using postln 

[[60, 62], [65, 67], [61, 63]].choose.postln.midicps.postln.reciprocal.postln

Practice, Chimes and Cavern 

The patch below uses noise in a steady state as well as a burst. Both are resonated within a Klank. The chimes and cavern are mixed by simply adding them together. This patch uses lots of ugens and may max out your cpu. If so, reduce the number of harmonics, instruments, etc. 

15.11. Subtractive Synthesis (Mix, Pan2, Klank, Decay, Dust, PinkNoise, RLPF, LFSaw)
 
(
{
var totalInst, totalPartials, baseFreq, ampControl, chimes, cavern;
totalInst = 5; //Total number of chimes
totalPartials = 12; //Number of partials in each chime
baseFreq = rrand(200, 1000); //Base frequency for chimes

chimes = 
	Mix.ar(
		{
		Pan2.ar(
			Klank.ar(`[
				{baseFreq*rrand(1.0, 12.0)}.dup(totalPartials),
				Array.rand(totalPartials, 0.3, 0.9),
				Array.rand(totalPartials, 0.5, 6.0)], 
				Decay.ar(
					Dust.ar(0.2, 0.02), //Times per second, amp
					0.001, //decay rate
					PinkNoise.ar //Noise
				)), 1.0.rand2) //Pan position
		}.dup(totalInst)
	);

cavern = 
	Mix.ar(
		{
		var base;
		base = exprand(50, 500);
			Klank.ar(
				`[ //frequency, amplitudes, and decays
					{rrand(1, 24) * base * 
						rrand(1.0, 1.1)}.dup(totalPartials),
					Array.rand(10, 1.0, 5.0).normalizeSum
				],
				GrayNoise.ar( [rrand(0.03, 0.1), rrand(0.03, 0.1)])
			)*max(0, LFNoise1.kr(3/rrand(5, 20), mul: 0.005))
		}.dup(5));
cavern + chimes
}.play
)

(
//Variation: filtered chimes
{
var totalInst, totalPartials, baseFreq, ampControl, chimes, cavern;
totalInst = 7; //Total number of chimes
totalPartials = 12; //Number of partials in each chime
baseFreq = rrand(100, 900); //Base frequency for chimes

chimes = 
	Mix.ar(
		{
		Pan2.ar(
			RLPF.ar(Klank.ar(`[
				{baseFreq*rrand(1.0, 12.0)}.dup(totalPartials),
				Array.rand(totalPartials, 0.3, 0.9),
				Array.rand(totalPartials, 0.5, 6.0)], 
				Decay.ar(
					Dust.ar(0.2, 0.02), //Times per second, amp
					0.001, //decay rate
					PinkNoise.ar //Noise
				)), LFNoise1.kr(5/7, 2000, 2100), 0.1), 1.0.rand2) //Pan position
		}.dup(totalInst)
	);

cavern = 
	Mix.ar(
		{
		var base;
		base = exprand(50, 500);
			Klank.ar(
				`[ //frequency, amplitudes, and decays
					{rrand(1, 24) * base * rrand(1.0, 1.1)}.dup(totalPartials),
					Array.rand(10, 1.0, 5.0).normalizeSum
				],
				GrayNoise.ar( [rrand(0.03, 0.1), rrand(0.03, 0.1)])
			)*max(0, LFNoise1.kr(3/rrand(5, 20), mul: 0.005))
		}.dup(5));
cavern + chimes
}.play
)	

15. Exercises 

15.1. Write a patch with an RLPF and Saw as signal source. Insert an LFNoise0 to control the frequency cutoff with a range of 200, 2000. 

15.2. Write a patch using Klank and PinkNoise as a filter source. Fill the frequency array with harmonic partials but randomly detuned by +/-1.1. 

15.3. Without deleting any existing code, rewrite the patch below with "safe" values to replace trigger and fund. 

15.4. Monitor the frequencies of all the SinOsc by only adding "(", ")", and a postln. 

({var trigger, fund; 
	trigger = Dust.kr(3/7); fund = rrand(100, 400); 
	Mix.ar( 
	Array.fill(16,
	{arg counter; var partial;
	partial = counter + 1;
	Pan2.ar(
		SinOsc.ar(fund*partial) * 
		EnvGen.kr(Env.adsr(0, 0, 1.0, 5.0), trigger, 1/partial) * max(0, 		LFNoise1.kr(rrand(5.0, 12.0))), 1.0.rand2) 
}))*0.5}.play) 

15.5. Again, using only the arrow keys to place the cursor and only typing "//", "comment out" the Pan2. 

15.6. Without deleting or commenting out, replace the entire Mix portion with a "safe" SinOsc.ar(400).