[
Cottle examples]
6. Envelopes, Reciprocals
Envelopes
//6 Assignment
s = Server.internal;
s.boot;
(
{ var env, trig;
trig = Impulse.kr(1);
env = Env.linen(0.1, 0.2, 0.3, 0.5);
SinOsc.ar(400, mul: EnvGen.kr(env, gate: trig))
}.scope;
)
(
var pitchCollection, env;
pitchCollection = [60, 45, 67, 82, 55, 66, 62].midicps;
env = Env.linen(0.01, 0.1, 0.3, 0.5);
{ var trig;
trig = Impulse.kr(2);
SinOsc.ar(
Select.kr( // pitch selector
Stepper.kr( // steadily increasing index
trig, // trigger
0, // reset trigger (not needed)
0, // min value
pitchCollection.size-1, // max value
1, // step size
pitchCollection.size-1 // reset value
),
pitchCollection
),
mul: EnvGen.kr(env, gate: trig)
)
}.scope;
)
//6.1 Envelopes plotted
//6.2 Complex Envelope
// Plot examples will not work in sc3.
//6.3 Envelope and Envelope Generator controlling amplitude
(
{ var env, trig;
trig = Impulse.kr(1);
env = Env.linen(0.1, 0.2, 0.3, 0.5);
SinOsc.ar(400, mul: EnvGen.kr(env, gate: trig))
}.scope;
)
// A note about doneAction:
// In sc2, the Synth is supposed to stick around until the piece is done.
// In sc3, often you want a synth to die when its envelope is over.
// doneAction == 0 means that the synth persists. This is the default.
// doneAction == 2 means that the synth will free itself.
// The EnvGen helpfile lists all 13 possible doneActions.
// Watch the number of Synths in the server window. At first, it's 2
// (one for the player and one for the scope). After 0.6 seconds
// (total length of Env.linen), it drops to 1 (scope only).
(
{ var env, trig;
trig = Impulse.kr(1);
env = Env.linen(0.1, 0.2, 0.3, 0.5);
SinOsc.ar(400, mul: EnvGen.kr(env, gate: trig, doneAction:2))
}.scope;
)
//6.4 Scaled envelope for frequency
// EnvGen no longer has mul and add arguments. Use levelScale and levelBias instead.
EnvGen.kr(Env.linen(1.5, 2.5, 1.2, 0.3), levelScale: 1000, levelBias: 500)
//6.5 Duration, attack, decay
var dur, att, dec;
dur = 10;
att = dur*0.1;
dec = dur*0.9;
//6.6 Frequency and duration
{Impulse.kr(4)}.scope; //freq = 4, dur = 1/4 or 0.25
{Impulse.kr(3)}.scope; //freq = 3, dur = 1/3 or 0.33
{Impulse.kr(0.2)}.scope; //freq = 0.2, dur = 1/0.2 or 10/2 or 5
sec
{Impulse.kr(0.125)}.scope; //freq = 4, dur = 1/0.125 or 1000/125
or 8
//6.7 Frequency expressed as a ratio
{Impulse.kr(4)}.scope; //freq = 4, dur = 1/4 or 0.25
{Impulse.kr(3)}.scope; //freq = 3, dur = 1/3 or 0.33
{Impulse.kr(0.2)}.scope; //freq = 0.2, dur = 1/0.2 or 10/2 or 5
sec
{Impulse.kr(0.125)}.scope; //freq = 4, dur = 1/0.125 or 1000/125
or 8
//6.8 Duration, attack, decay
var dur, att, dec, trigFreq;
dur = 10;
att = dur*0.1;
dec = dur*0.9;
trigFreq = 1/dur;
//Just For Fun
//
//6.9 Just For Fun: Crotales
// 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.
// This one's kind of cool. Very tape-studio.
(
// 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, // Cottle has the synth in the TSpawn calculate the freq.
// I will pass it in from the controlling Routine.
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 on of McCartney's example. 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
freqCont = 1;
// freqCont = EnvGen.kr(env, timeScale: 2*dur, levelBias: 1, levelScale: Rand(100, 400));
//freqCont = EnvGen.kr(env, timeScale: 2*dur, levelBias: 1, levelScale: Rand(60, 1200));
//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
rrand(0.02, 2).linexp(0.02, 2, 0.02, 2).wait; // random time between triggers
});
}).play(SystemClock);
)
// Cottle sez: try adding this reverb, but I like it without
// I say: Note that the reverb is removed from the code inside the synthdefs
// that are being spawned. The reverb can be applied to the whole signal,
// once. This is a lot more efficient than putting it inside.
(
SynthDef("rvb", {
var o;
o = In.ar(0, 2); // get the output signal
4.do({ o = AllpassN.ar(o, 0.05, [0.05.rand,0.05.rand], 1) });
Out.ar(0, o);
}).send(s);
)
// the reverb must go after the playing synths
Synth.tail(s, "rvb");
r.stop;
s.scope;