### 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;