17 -FM/AM Synthesis, Sequencer, Sample and Hold 

FM and AM synthesis or "Ring" Modulation 

In a previous chapter I used one oscillator in low frequency to control another in audio range, warning you not to exceed 20 Hz for the frequency. Hopefully you didn't take my suggestion and experimented with higher frequencies. Of course you can have control rates above 20 Hz, but something very different occurs. The sounds you get above LFO range are similar to 
tuning a radio. That’s because you are entering the realm of frequency and amplitude modulation; the same technology used by am and fm bands on your radio to transmit signals through the air. (It also sounds surprisingly similar to bird song. Some birds can produce two frequencies that interact in the same way.) 

17.1. From LFO to FM 

{SinOsc.ar(SinOsc.ar(MouseX.kr(1, 500), mul: 300, add: 800))}.play

What distinguishes am and fm from LFO are upper and lower sidebands. They are additional frequencies that appear as a product of the two modulated frequencies. 

In amplitude modulation there are two sidebands; the sum and difference of the carrier frequency (the audio frequency that is being modulated) and the modulator frequency (the frequency that is controlling the audio frequency). A carrier frequency of 500 and a modulating frequency of 112 could result in two sidebands: 612 and 388. If there are overtones in one of the waves (e.g. a saw wave being controlled by a sine wave), then there will be sidebands for each overtone. 

17.2. AM Synthesis (SinOsc, scope, mul, Saw) 

{SinOsc.ar(500, mul: SinOsc.ar(50, mul: 0.5))}.scope(1)

{Saw.ar(500, mul: SinOsc.ar(50,  mul: 0.5))}.scope(1)

In the example above the outer SinOsc is the carrier and the inner SinOsc is the modulator. The sidebands should be 550 and 450. Change the argument 50 to other values to see how it changes the sound. Replace the 50 with a MouseX.kr(1, 500). As you move the mouse from left slowly to the right you should hear three pitches; the carrier, which remains stationary, and the two sidebands, one ascending and the other descending. 

In frequency modulation a similar effect takes place. But with FM many more sidebands can be generated. The exact number depends on the modulation index. The modulation index is how far the modulating frequency deviates from the carrier (the amplitude of the modulating wave). In SC it is a little more difficult to recognize the components of FM synthesis because 
both the carrier and modulator frequencies can appear as arguments in a single SinOsc. The add of the modulator can represent the carrier frequency. For clarity in the example below I've used this form: 400 + SinOsc(etc.). The 400 is the carrier frequency, 124 is the modulator frequency and 100 is the index. (A higher index results in more sidebands.) 

17.3. FM Modulation (scope, SinOsc, mul) 

{SinOsc.ar(400 + SinOsc.ar(124, mul: 100), mul: 0.5)}.scope(1)

The code below illustrates the difference between a change in the modulating frequency and a change in the modulation index. The first example has a MouseX to control the modulating frequency. The second example has a MouseX to control the index. The third example has MouseX and MouseY to control both. 

17.4. MouseX and MouseY controlling FM frequency and index 

(
{SinOsc.ar(400 + 
	SinOsc.ar(
		MouseX.kr(100, 500), //control freq 
		mul: 100 //index
		), mul: 0.5)}.scope(1)
)

(
{SinOsc.ar(400 + 
	SinOsc.ar(
		111, //control freq
		mul: MouseX.kr(10, 1000) //index
	), mul: 0.5)}.scope(1)
)

(
{SinOsc.ar(400 + 
	SinOsc.ar(
		MouseX.kr(100, 500), //control freq
		mul: MouseY.kr(10, 1000) //index
	), mul: 0.5)}.scope(1)
)

The ratio of the carrier and control frequency determine the sidebands and hence the quality or timbre of the wave, the index controls the number of sidebands, i.e. the brightness of the wave. Different frequencies of the carrier (i.e. changing the carrier while the modulator frequency and index remain the same) will sound like one instrument with a changing pitch. Different frequencies of the modulator only will sound like different instruments playing the same pitch. Different indexes will sound like one instrument being filtered. Of course, you can change all three. 

17.5. Controls for carrier, modulator, and index 

(
{SinOsc.ar(LFNoise0.kr(5, 300, 700) + 
	SinOsc.ar(
		100,
		mul: 100
	), mul: 0.5)}.scope(1)
)

(
{SinOsc.ar(700 + 
	SinOsc.ar(
		LFNoise0.kr(5, 500, 700),
		mul: 1000
	), mul: 0.5)}.scope(1)
)
	
(
{SinOsc.ar(700 + 
	SinOsc.ar(
		300,
		mul: LFNoise0.kr(5, 1000, 1010)  //index
	), mul: 0.5)}.scope(1)
)
	
(
{SinOsc.ar(LFNoise0.kr([9, 9], 300, 700) + 
	SinOsc.ar(
		LFNoise0.kr([9, 9], 500, 700),
		mul: LFNoise0.kr([9, 9], 500, 550)  //index
	), mul: 0.5)}.scope(1)
)

The sidebands are the sum and difference of the carrier and modulator wave. Given a carrier of 400 Hz and a modulator of 50 Hz, the sidebands would be 450, 500, 550, 600, 650, etc., depending on the modulation index, and 350, 300, 250, 200, etc., depending on the modulation index. 

A similar result could be achieved through additive synthesis; 12 sine waves each tuned to the frequencies listed above. But here is the value of FM: It is more efficient, using only two sine waves. This raises the issue of harmonic and enharmonic spectra from an earlier chapter. We hear the sidebands as partials of a single timbre. If they are lower multiples they will sound more harmonic. For this reason it is useful to calculate the modulating frequency as a ratio of the carrier frequency. In the patch below, change the ratio to whole numbers such as 1, 2, 3, 4, then try numbers such as 2.317, 1.576, etc. Since the modulating frequency ratio stays the same we hear a single timbre playing different pitches. The modulation index is still controlled by the MouseY. Changing the index with MouseY changes the brightness but not the timbre because it adds and removes sidebands (like a filter).

17.6. Carrier and modulator ratio 

(
{var freq, ratio;
freq = LFNoise0.kr(4, 20, 60).round(1).midicps;
ratio = 2;
SinOsc.ar(freq + 
	SinOsc.ar(
		freq*ratio, //control freq
		mul: MouseY.kr(10, 1000) //index
), mul: 0.5)}.play
)

The next example adds an envelope to control the amplitude of the SinOsc, but it also controls the modulation index (i.e. filter sweep). Notice also that the rate of the LFNoise, EnvGen gate, and the decay of the Env are linked with the variable rate. The LFNoise is generating values between 48 and 72, appropriate for MIDI. They are rounded to 1 producing an equal tempered scale (i.e. I'm not avoiding notes between the keys, just not what I wanted here), and converted to cps. Again, try unusual values for the ratio to change the character of the instrument. 

17.7. Envelope applied to amplitude and modulation index 

(
{var freq, ratio, env, rate;
rate = 7;
freq = LFNoise0.kr(rate, 12, 60).round(1).midicps;
ratio = 2;
env = EnvGen.kr(Env.perc(0, 1/rate), gate: Impulse.kr(rate));
SinOsc.ar(freq + 
	SinOsc.ar(
		freq*ratio, //control freq
		mul: env*1000 //index
), mul: env)}.play
)

FM synthesis was developed by John Chowning during the 60s and used extensively in the 80s, specifically in the Yamaha DX7 series. Its success comes from its efficiency; using only two waves to generate theoretically unlimited sidebands. 

Sequencer 

There are two more classic controls I would like to illustrate. The first is a sequencer. The sequencer moves through a set of values at a given rate or trigger. 

The sequence is defined by an "expansion protected" (with "`") array. The sequencer can be used to control any aspect of sound. In this example it is used to control MIDI pitch using midicps. In addition to a midi to cps conversion, arrays understand basic math operations, as shown in the last examples. 

17.8. Array math 

//run each example

[60, 62, 64, 65, 67, 69, 71].midicps;

([36, 48, 60].choose + [0, 2, 4, 5, 7, 9, 11]).midicps

300 * [1.345, 2.51, 3.8, 3.819, 8.291, 5.001]

You can use any set of frequencies (i.e. other tunings), or even random frequencies. You can also fill the array automatically using Array.rand, or modify the array in a number of ways (illustrated below). 

17.9. Sequencer (array, midicps, SinOsc, Sequencer, Impulse, kr) 

(
var pitchArray; //Declare a variable to hold the array
	//load the array with midi pitches
pitchArray = [60, 62, 64, 65, 67, 69, 71, 72]; 
pitchArray = pitchArray.midicps; //convert the midi pitches to cps
{
	SinOsc.ar(
		Select.kr(
			Stepper.kr(Impulse.kr(8), max: pitchArray.size-1),
			pitchArray),
		mul: 0.5)
}.play
)

The trigger can also be a random impulse, supplied by Dust.kr. 

17.10. Dust.ar (array, midicps, SinOsc, Sequencer, Dust, kr) 

(
var pitchArray; //Declare a variable to hold the array
	//load the array with midi pitches
pitchArray = [60, 62, 64, 65, 67, 69, 71, 72]; 
pitchArray = pitchArray.midicps; //convert the midi pitches to cps
{
	SinOsc.ar(
		Select.kr(
			Stepper.kr(Dust.kr(4), pitchArray.size-1),
			pitchArray),
		mul: 0.5)
}.play
)

You can do a lot of things besides math and midicps to an array; reverse, scramble, or fill using a function. Consider the lines below. First an array is filled with random numbers between 60 and 84. Array.rand takes three arguments; first the number of items in the array, then the low and high ends of the random range. Next we scramble the array and post it. Then reverse it and post it. Last we add 12 to the entire array and post it. 

17.11. scramble, reverse (Array.rand, postln, scramble, reverse) 

(
var pitchArray;
pitchArray = Array.rand(10, 60, 84);
pitchArray.postln;
pitchArray.scramble.postln;
pitchArray.reverse.postln;
(pitchArray + 12).postln
)

Here are examples of each process in an actual patch. Notice that each time you run the example the sequence is different because of the random choices. Add your own pitch arrays. 

17.12. sequencer variations (array, scramble, midicps, Sequencer, kr, Dust) 

(
var pitchArray; 
pitchArray = [60, 62, 64, 65, 67, 69, 71, 72]; 
pitchArray = [ //pick one of the following
	(pitchArray + rrand(1, 12)).midicps, //transpose
	pitchArray.reverse.midicps, //reverse
	pitchArray.scramble.midicps, //scramble
	Array.rand(12, 36, 72).midicps, //random midi
	Array.rand(12, 100, 1200) //random frequency
	].choose;
{
	SinOsc.ar(
		Select.kr(
			Stepper.kr(Impulse.kr(7), pitchArray.size-1),
			pitchArray),
		mul: 0.5)
}.play
)

Sample and Hold 

Sample and hold is another classic synthesis control source. It disappear with the modular synthesizer. But I think it provides a unique treatment of parameter choice. The SC equivalent to a sample and hold is Latch.kr. A wave is periodically sampled, returning the value sampled at that moment. (You might think of it as digital conversion at a very low sample rate. It is also similar to a strobe light, or the wagon wheel affect you see in movies where the frame rate corresponds in some way to the motion of the wheels.) The patterns that emerge are more varied than a single sequence, but more self-similar than an LFNoise control. If the sample rate is higher than the frequency of the wave being sampled you can actually hear the shape of the wave as it is mapped to some control (such as pitch). Try this example and listen for the shape of the Saw wave in the frequencies. I use a Line.kr to control the frequency of the sampled LFSaw. It begins at 0.1, or once every ten seconds and then moves to 20. As the frequency increases (think of the size of the wave shrinking in relation to the sample rate, which stays the same) you will hear less of the actual shape of the wave and more variety in the pattern as it falls on less predictable points in the saw wave. 

I use Blip in this patch. The second argument is the number of harmonics. It provides a quick way to add or subtract partials as you would with a filter (which we cover later). 

17.13. Latch (Blip, Latch, LFSaw, Impulse, mul) 

(
{	Blip.ar( //Audio Ugen
		Latch.kr( //Freq control Ugen
			LFSaw.kr(Line.kr(0.1, 10, 70), 0, 500, 600), //Input for Latch
			Impulse.kr(10)), //Sample trigger rate
		12, //Number of harmonics in Blip
		mul: 0.3 //Volume of Blip
	)
}.scope(1)
)

As the relationship between the frequency of the wave and the rate of sample changes you get different patterns, some obvious, others surprising. But any two values will result in some kind of pattern since both the trigger and the wave are periodic. Slowly move the mouse from left to right, changing the frequency of the sampled wave and the combined patterns. The Latch can be used to control amplitude, frequency cutoff, or in the second example, the number of harmonics in the Blip. I've given different maximum values for the mouse control so that as you move to the right different patterns for frequency and filter emerge. The third example is a stereo version. 

17.14. Latch (Blip, Latch, LFSaw, Impulse, mul) 

(
{	Blip.ar(
		Latch.kr( //Freq control Ugen
			LFSaw.kr(MouseX.kr(1.1, 30), 0, 1000, 1100), 
			Impulse.kr(10)), //Sample trigger rate
		7, //Number of harmonics in Blip
		mul: 0.3 //Volume of Blip
	)
}.scope(1)
)

(
{	Blip.ar(
		Latch.kr(
			LFSaw.kr(MouseX.kr(1.0, 30), 0, 1000, 1100), 
			Impulse.kr(10)), 
		Latch.kr(
			LFSaw.kr(MouseX.kr(1.0, 27), 0, 4, 5),
			Impulse.kr(10)), 
		mul: 0.3 
	)
}.scope(1)
)

(
{	Blip.ar(
		Latch.kr(
			LFSaw.kr(MouseX.kr(1.0, [11.1, 9.2]), 0, 500, 550), 
			Impulse.kr(10)), 
		Latch.kr(
			LFSaw.kr(MouseX.kr(1.0, [7.12, 15.7]), 0, 4, 5),
			Impulse.kr(10)), 
		mul: 0.3 
	)
}.scope(2)
)

In a previous chapter we saw that with intervals it is the ratio of the two frequencies that determines the relative complexity or simplicity of the destructive and constructive interference patterns. The same is true for a S&H. The ratio between the frequency of the sampled wave and the sample rate will determine the patterns. If we linked the two values the same pattern will persist when the rate is changed. In the patch below try replacing the MouseX with explicit ratios. I find values around 1.6 to 1.75 are pleasing (the golden mean, in case you forgot, is 1.61803399). 

The second example adds one more control so the ratio of the samples will gradually move through interesting patterns. 

17.15. Latch sample and speed ratio (Blip, Latch, LFSaw, Impulse, mul) 

(
{
var rate, speed, ratio;
rate = 12;
ratio = MouseX.kr(1.6, 1.75); //replace with static values
//
speed = rate*ratio;
Blip.ar(
	Latch.kr( //Freq control Ugen
		LFSaw.kr(speed, 0, 1000, 1100), 
		Impulse.kr(rate)), //Sample trigger rate
	7, //Number of harmonics in Blip
	mul: 0.3 //Volume of Blip
)
}.scope(1)
)

(
{
var rate, speed, ratio;
rate = 12;
ratio = SinOsc.ar(2/7, mul: 0.2, add: 1.75); 
speed = rate*ratio;
Blip.ar(
	Latch.kr( //Freq control Ugen
		LFSaw.kr(speed, 0, 1000, 1100), 
		Impulse.kr(rate)), //Sample trigger rate
	7, //Number of harmonics in Blip
	mul: 0.3 //Volume of Blip
)
}.scope(1)
)

It is also possible to use more complex waveforms as a source. It is a more complex period, but the patterns in the wave will be evident in the control values. Take the following wave as an example. Three waves are mixed together to create a more complex, but still periodic, wave. The first example below shows four waves at 100, 200, 300, and 550 Hz, mixed to one 
wave. The second part places the mixed wave into a Latch. 

17.16. Complex Wave as Sample Source (Mix, SinOsc, Blip, Latch, Mix, Impulse) 

//A complex wave
{Mix.ar(SinOsc.ar([100, 200, 300, 550], mul: 0.1))}.scope(1)

(
//Used in a sample and hold
{
	Blip.ar( //Audio Ugen
		Latch.kr( //Freq control Ugen
			Mix.ar(SinOsc.ar([100, 200, 300, 550], mul: 100, add: 110)), 
			Impulse.kr(10.2)), //Sample trigger rate
		3, //Number of harmonics in Blip
		mul: 0.3 //Volume of Blip
	)
}.play
)

Practice S&H FM 

In case you haven't guessed I really like the results of an S&H. We should use it more. Try an attack of 1/rate and a decay of 0. Try the alternate latchrates (my favorites are LFNoise0 for both latchrate and ratio), which move either gradually or stepwise through random values between 1.53 and 1.63. The patch is expanded to stereo with the [freq, freq * 1.5]. This creates a fifth in the right channel. Try other intervals such as 1.2, 1.7, 2, 1.25, etc. The key is to find that level of complexity, that sweet spot where you can still follow the emerging patterns, yet have enough variety to keep your attention for the 
long periods of time. Isn't that really what composition is all about? 

The second example does not use an S&H or sequencer (maybe you could add one?), but illustrates FM. It is a variation on one of James McCartney's original SC2 examples. James Harkins then ported it to SC3 and I'm just including his version with explanations. It generates FM events with complex ratios, therefore enharmonic spectra (the result is crotaleesque) with a collection of envelopes for the modulation index. 

Try both examples at once. If so you might want to adjust the overall volume of the first one. 

17.17. Practice, Sample and Hold, FM 

(
{var freq, latchrate, index, ratio, env, rate;
rate = 9;
latchrate = rate*1.61803399;
//Or try (uncomment) this:
//latchrate = rate*LFNoise1.kr(1/7, mul: 0.03, add: 1.6);
//Or this
//latchrate = rate*LFNoise0.kr(1/3, mul: 0.03, add: 1.6);
index = Latch.kr(
	LFSaw.kr(latchrate, mul: 1000, add: 1000),
	Impulse.kr(rate)
	);
freq = Latch.kr(
	LFSaw.kr(latchrate, mul: 24, add: 60),
	Impulse.kr(rate)
	).round(1).midicps;
//freq = 200; //uncomment this line to hear just the index
//For ratio try 1.01, 1.001, 2.1, 2.2, etc.
//or LFNoise1.kr(1, mul: 2.3, add: 3.0);
//or even better: 
// LFNoise0.kr(1, mul: 2.3, add: 3.0);
ratio = 2.01; 
//or
// ratio = LFNoise1.kr(1/5, mul: 2.0, add: 5.0);
env = EnvGen.kr(Env.perc(0, 1/rate), gate: Impulse.kr(rate));
SinOsc.ar([freq, freq * 1.5] + 
	SinOsc.ar(
		freq*ratio, //control freq
		mul: index //index
), mul: env*0.5 //overall volume
)}.play
)

//Crotale

  /* As in the previous chapter, the approach is to shift some of the calculations into the controlling Routine.
Since all synthdefs must be precompiled, and we have 5 envelopes to choose from, I must make 5 synthdefs in advance. Note the use of a loop to create them mechanically: I don't have to reproduce the synthdef code each time. */  

(
// Run this section first, then the Task below. 
// Make a separate synthdef for each envelope.
// Five possible envelopes.
// The total length of each Env is 1.0 sec; this will be 
// scaled by dur in the EnvGen.

[	Env.perc(0.99, 0.01), //long attack, sharp decay
	Env.perc(0.0001, 0.9999), //sharp attack, long decay
	Env.perc(0.01, 0.99), //softer attack, long decay
	Env.perc(0.1, 0.9), //soft attack, long decay
	Env.perc(0.2, 0.8) //soft attack, long decay
].do({ |env, i| // |env, i| is a shortcut syntax for "arg env, i;"
		// the defs will be named ch6-fun0, ch6-fun1, etc.
	SynthDef("ch6-fun" ++ i, {
		arg freq, 
			indexRange, dur;

		var ratioa, ratiob, ratioc, ratiod, factor, envs;
		var panCont, freqCont, index;
			
//Calculate ratios, carrier and modulator frequency multiples. I'm using
//the same calculations from one of McCartney's examples. Changes here
//won't lead to anything more interesting.

// It would be more efficient to do these calculations in the language-side
// routine as well, then pass them into the synthdef. But I'm not 
// doing that here.
		
// IRand calculates a new random integer each time the
// synth starts. If I used rrand, there would be one value
// used for every instantiation of this synthdef. The value
// would be different for each of the 5 synthdefs. Why?
ratioa = IRand(1, 12); 
ratiob = IRand(1, 12); factor = gcd(ratioa, ratiob);
ratioa = div(ratioa, factor); ratiob = div(ratiob, factor);

ratioc = IRand(1, 12); 
ratiod = IRand(1, 12); factor = gcd(ratioc, ratiod); 
ratioc = div(ratioc, factor); ratiod = div(ratiod, factor);
		
//use one of these freqCont lines (uncomment the others to use them)
freqCont = 1;
//Or
//freqCont = 
//	EnvGen.kr(env, timeScale: 2*dur, levelBias: 1, 
//	levelScale: Rand(100, 400));
//Or
//freqCont = 
//	EnvGen.kr(env, timeScale: 2*dur, levelBias: 1, 
//	levelScale: Rand(60, 1200));
//Or 
//freqCont = (EnvGen.kr(env, timeScale: 2*dur, levelBias: 1, 
//		levelScale: Rand(60, 1200)))
//		* (IRand(0, 1) * 2 - 1);
		
//use one of these panCont lines
// [1, -1].choose again will evaluate once only at compile time.
// to get it to evaluate every time the synth starts, 
// it has to be done like this:
//panCont = 0;
panCont = (EnvGen.kr(env, timeScale: 2*dur, levelBias: -1, levelScale: 2))
			* (IRand(0, 1) * 2 - 1);  // 0*2-1 = -1, 1*2-1 = 1
//panCont = LFNoise0.ar(20);
//panCont = LFNoise1.ar(2);
//panCont = 1.0.rand2;
		
o = PMOsc.ar(
	ratioa*freq, //or try ratioa*freqCont,
	ratiob*freq, //or try ratioa*freqCont,
	pmindex: EnvGen.kr(env, timeScale: 2*dur, 
		levelBias: 1, levelScale: indexRange), 
	mul: EnvGen.kr(env, timeScale: 2*dur, levelScale: 0.3));

p = PMOsc.ar(
	ratioc*freq, //or try ratioa*freqCont, 
	ratiod*freq, //or try ratioa*freqCont, 
	pmindex: EnvGen.kr(env, timeScale: 2*dur, 
		levelBias: 1, levelScale: indexRange), 
	mul: EnvGen.kr(env, timeScale: 2*dur, levelScale: 0.3));

o = Mix.ar([o, p]);
o = Pan2.ar(
	o, 
	panCont
);

o = o * EnvGen.kr(env, timeScale: 2*dur, levelScale: Rand(0.1, 0.5),
	doneAction:2);  // only 1 EnvGen in the synthdef needs to have
				// doneAction:2 for the synth to free itself
Out.ar(0, o);

}).send(s);
});
)

(
	// a Task is like a Routine. It's designed to be paused and resumed.
r = Task({
	var freq, indexDepth, indexRange, synthIndex, dur;
	
		// This is not a SynthDef, so rrand is appropriate here.
	freq = rrand(20, 4000);
	indexDepth = 1;
	
	inf.do({
		//Each new frequency is a ratio of the previous freq
		freq = (freq * 
			([3/2, 2/1, 4/3, 3/4, 2/3, 1/2].choose)).wrap(20, 600);
		//Index depth is how quickly the sound becomes richer
		indexDepth = indexDepth + 0.3; 
		indexRange = (indexDepth).round(1.0).wrap(1, 24)
			.rand2;
		synthIndex = 5.rand;
		dur = rrand(0.1, 2.0); //total duration of each event
		
			// place at head of server, so reverb effect will work below
		Synth.head(s, "ch6-fun" ++ synthIndex, [\freq, freq,
			\indexRange, indexRange, \dur, dur]);

	// linexp maps a linear range onto an exponential one
	// this puts more emphasis on shorter values,
	// making the texture more lively       
	// random time between triggers
	rrand(0.02, 2).linexp(0.02, 2, 0.02, 2).wait;   
	});
}).play(SystemClock);
)

17. Exercises 
17.1. What are the am sidebands of a carrier at 440 Hz and a modulator of 130? 
17.2. What are the fm sidebands of a carrier at 400 Hz and a modulator at 50? 
17.3. Create an fm patch where the modulator frequency is controlled by an LFNoise1 and the modulation index is controlled by an LFSaw ugen. 
17.4. Create an fm patch where the carrier frequency, modulator frequency, and modulation index are controlled by separate sequencers (with arrays of different lengths) but all using the same trigger. 
17.5. Create an fm patch where the carrier frequency is controlled by an LFNoise0, the modulator is controlled by a S&H, and the index a sequencer, all using the same trigger.