5. Variables, Comments, Triggers
Variables and Comments
//5 Assignment:
s = Server.internal; s.boot; // start the server // See the explanation of the rewrite under ex. 5.3. // What happens if you replace Impulse.kr(8) with Dust.kr(3)? ( var pitchCollection; pitchCollection = [60, 45, 67, 82, 55, 66, 62].midicps; { Out.ar([0, 1], // [0, 1] duplicates the mono signal // between the left and right channels SinOsc.ar( Select.kr( // pitch selector Stepper.kr( // steadily increasing index Impulse.kr(8), //Trigger frequency 0, // reset trigger (not needed) 0, // min value pitchCollection.size-1, // max value 1, // step size pitchCollection.size-1 // reset value ), pitchCollection ), mul: 0.3 ) ) }.play(s) ) //5.1 Variable declaration, assignment, and comments //First patch { SinOsc.ar(LFNoise0.ar(10, 400, 800), 0, 0.3) }.play(s); //First patch with variables ( { var freqRate, freqRange, lowValue; freqRate = 10; //rate at which new values are chosen freqRange = 1200; //the range of frequencies chosen lowValue = 60; //the lowest frequency SinOsc.ar( LFNoise0.ar(freqRate, freqRange/2, freqRange + lowValue), 0, 0.3) }.play(s); ) //Triggers, Gates //5.2 Trigger // Synth.plot does not exist, so those examples are omitted. // There is an ascii-based plotting mechanism, but writing the results // of a ugenfunc into it is a bit more complicated than we should get // into at this point. The examples with scope should illustrate the // behavior of triggers well enough. // Periodically, you will see single-sample spikes in the scope window. // You won't see every impulse because of the way scope works. { Impulse.ar(4, mul: 0.5) }.scope; { Dust.ar(5) }.scope; //5.3 Sequencer // SC3 does not support Sequencer.kr. // The same thing can be done with Select.kr, which addresses // an array, and using a Stepper as the index. // In Stepper, reset value should be set to the max value so that // when the first trigger fires, the value wraps back around to the // minimum value. ( var pitchCollection; pitchCollection = [60, 45, 67, 82, 55, 66, 62].midicps; { Out.ar([0, 1], // [0, 1] duplicates the mono signal // between the left and right channels SinOsc.ar( Select.kr( // pitch selector Stepper.kr( // steadily increasing index Impulse.kr(8), //Trigger frequency 0, // reset trigger (not needed) 0, // min value pitchCollection.size-1, // max value 1, // step size pitchCollection.size-1 // reset value ), pitchCollection ), mul: 0.3 ) ) }.play(s) ) //Just For Fun // //5.4 Just for Fun // This is a great illustration of one of the main architectural differences // between sc2 and sc3. // Here we have a couple of levels of synth activity: // rate, delayFunction, frequency, and foldFunc are overall control processes // that should apply to each event created by OverlapTexture in the original. // The function in the OverlapTexture creates a new event. // We can modularize this by creating separate synths for the control processes, // playing their output onto **control buses**. Then, the synth that makes the events // can use the values on the control buses to determine its behavior. /* The original patch is written in a way that's very idiomatic for sc2. Here are two translations: first, more "literal" so you can compare more easily to the original. This first translation is less modularized (and, as we'll see in a moment, less flexible). */ // pass 1: make all the required synthdefs. ( SynthDef("ch5-controls", { /* My very strong preference is never to hardwire bus numbers into a synthdef. By passing in the bus numbers as arguments, I can make two copies of this entire control-value complex, running on separate buses, without rewriting the synthdef. */ arg ratebus, dlyFuncbus, freqbus, foldFuncbus; var frequency, rate, seqTrigRate, foldFunc, foldFuncRate, foldFuncWidth, foldFuncFocus, delayFunction, filterCutoff, delayMax, delayMin, echoDecay, minFreq, maxFreq, freqRate; //line 5 minFreq = 20; maxFreq = 40; freqRate = 1/3; foldFuncWidth = 0.05; //line 10 foldFuncFocus = 0.001; foldFuncRate = 0.5; delayMax = 0.4; delayMin = 0.01; //line 15 // As in the original, all these control values can be calculated in one UGenFunc. seqTrigRate = Dust.kr(1/3); //rate trigger; one in three seconds rate = TChoose.kr( seqTrigRate, [1/4, 3, 6, 3/4, 14, 20] ); delayFunction = LFNoise1.kr( rate, mul: delayMax/2, add: delayMax/2 + delayMin); frequency = LFNoise1.kr( freq: freqRate, mul: (maxFreq - minFreq)/2, add: ((maxFreq - minFreq)/2) + minFreq); //line 30 foldFunc = LFNoise1.kr( freq: foldFuncRate, mul: foldFuncWidth, add: foldFuncWidth + foldFuncFocus ); //line 35 /*They must be output separately on to their respective buses. There's a shortcut for this, which we'll see in later chapters. Out.kr makes sure that control-rate buses are used. */ Out.kr(ratebus, rate); Out.kr(dlyFuncbus, delayFunction); Out.kr(freqbus, frequency); Out.kr(foldFuncbus, foldFunc); }).send(s); SynthDef("ch5-synth", { // The sound-making synth also needs to know the bus numbers, so they get passed in again. arg ratebus, dlyFuncbus, freqbus, foldFuncbus; var outMix, frequency, rate, foldFunc, delayFunction, filterCutoff, delayMax, echoDecay; //line 5 filterCutoff = 1000; delayMax = 0.4; echoDecay = 4; rate = In.kr(ratebus, 1); delayFunction = In.kr(dlyFuncbus, 1); frequency = In.kr(freqbus, 1); foldFunc = In.kr(foldFuncbus, 1); outMix = SinOsc.ar(frequency, mul: 0.8).fold2(foldFunc); outMix = CombN.ar(outMix, delayMax, delayFunction, echoDecay, add: outMix); //line 40 outMix = RLPF.ar(outMix, filterCutoff); outMix = Pan2.ar(outMix, LFNoise1.kr(rate)); //Again, a fixed-length envelope is used to handle the overlap. outMix = outMix * EnvGen.kr(Env.linen(1, 3, 1), doneAction:2); Out.ar(0, outMix); }).send(s); ) /*This block of code assigns bus numbers, instantiates the control processes, and periodically triggers a new audio synth. Here I am manually assigning bus numbers. There is a bus object which manages allocating the numbers for you. We'll use that in later chapters. */ ( var ratebus = 0, delayFuncbus = 1, freqbus = 2, foldFuncbus = 3; // Start controls running. // Observe the syntax to pass values into a synth at creation time. // Arguments are given as pairs: the argument name from the function // may be given as a \symbol or a "string" -- the next value in the array // is the value the argument should receive. Synth("ch5-controls", [\ratebus, ratebus, \delayFuncbus, delayFuncbus, \freqbus, freqbus, \foldFuncbus, foldFuncbus], target:s); r = Routine({ inf.do({ Synth("ch5-synth", [\ratebus, ratebus, \delayFuncbus, delayFuncbus, \freqbus, freqbus, \foldFuncbus, foldFuncbus], target:s); 2.5.wait; }); }).play(SystemClock); ) /* So far, it looks like more code and more trouble to do what sc2 did in a very straightforward way. But here's something sc2 can't do so easily. After starting the previous processes, say you want to hear another copy of the same processes playing at the same time. In sc2, you would have to stop the synth, do some measure of surgery, and restart. In sc3, you can simply execute the following. Note that the bus numbers are different. */ ( var ratebus = 4, delayFuncbus = 5, freqbus = 6, foldFuncbus = 7; // Start controls running. Synth("ch5-controls", [\ratebus, ratebus, \delayFuncbus, delayFuncbus, \freqbus, freqbus, \foldFuncbus, foldFuncbus], target:s); t = Routine({ inf.do({ Synth("ch5-synth", [\ratebus, ratebus, \delayFuncbus, delayFuncbus, \freqbus, freqbus, \foldFuncbus, foldFuncbus], target:s); 2.5.wait; }); }).play(SystemClock); ) r.stop; // to prove it, stop the first. The second will still play. t.stop; // stop the second. // Clean up: Press cmd-. to stop the control synths. //end patch /* Let's carry the modularization further. This version places all the control processes into separate synthdefs. Doing it this way makes the code more idiomatic to sc3. Although initially the code looks fussier, we'll see the advantage momentarily. */ ( var frequency, rate, seqTrigRate, foldFunc, foldFuncRate, foldFuncWidth, foldFuncFocus, delayFunction, filterCutoff, delayMax, delayMin, echoDecay, minFreq, maxFreq, freqRate; //line 5 minFreq = 20; maxFreq = 40; freqRate = 1/3; foldFuncWidth = 0.05; //line 10 foldFuncFocus = 0.001; foldFuncRate = 0.5; delayMax = 0.4; delayMin = 0.01; //line 15 SynthDef("ch5-trig", { arg trigbus; Out.kr(trigbus, Dust.kr(1/3)); //rate trigger; one in three seconds }).send(s); /*The rate function depends on the trigger, and the delay function depends on the rate. Therefore each must have an argument for the bus where the input value resides, in addition to the output bus number. The input value is retrieved by In.kr(bus_number, 1) (one channel) */ SynthDef("ch5-rate", { arg ratebus, trigbus; Out.kr(ratebus, TChoose.kr( In.kr(trigbus, 1), [1/4, 3, 6, 3/4, 14, 20])) }).send(s); SynthDef("ch5-dlyFunc", { arg dlybus, ratebus; Out.kr(dlybus, LFNoise1.kr(In.kr(ratebus, 1), mul: delayMax/2, add: delayMax/2 + delayMin)); }).send(s); SynthDef("ch5-freq", { arg freqbus; Out.kr(freqbus, LFNoise1.kr( freq: freqRate, mul: (maxFreq - minFreq)/2, add: ((maxFreq - minFreq)/2) + minFreq)); }).send(s); SynthDef("ch5-fold", { arg foldbus; Out.kr(foldbus, LFNoise1.kr( freq: foldFuncRate, mul: foldFuncWidth, add: foldFuncWidth + foldFuncFocus)); }).send(s); //The audio synth does not need be changed from the previous version. SynthDef("ch5-synth", { arg ratebus, dlyFuncbus, freqbus, foldFuncbus; var outMix, frequency, rate, foldFunc, delayFunction, filterCutoff, delayMax, echoDecay; //line 5 filterCutoff = 1000; delayMax = 0.4; echoDecay = 4; rate = In.kr(ratebus, 1); delayFunction = In.kr(dlyFuncbus, 1); frequency = In.kr(freqbus, 1); foldFunc = In.kr(foldFuncbus, 1); outMix = SinOsc.ar(frequency, mul: 0.8).fold2(foldFunc); outMix = CombN.ar(outMix, delayMax, delayFunction, echoDecay, add: outMix); //line 40 outMix = RLPF.ar(outMix, filterCutoff); outMix = Pan2.ar(outMix, LFNoise1.kr(rate)); outMix = outMix * EnvGen.kr(Env.linen(1, 3, 1), doneAction:2); Out.ar(0, outMix); }).send(s); ) // pass 2: flow of control, playback ( var ratebus = 0, delayFuncbus = 1, freqbus = 2, foldFuncbus = 3, trigbus = 4; //Now we need to make separate synths for each of the control processes. a = Synth("ch5-trig", [\trigbus, trigbus], target:s); b = Synth("ch5-rate", [\ratebus, ratebus], target:s); c = Synth("ch5-dlyFunc", [\dlybus, delayFuncbus], target:s); d = Synth("ch5-freq", [\freqbus, freqbus], target:s); e = Synth("ch5-fold", [\foldbus, foldFuncbus], target:s); r = Routine({ inf.do({ Synth("ch5-synth", [\ratebus, ratebus, \delayFuncbus, delayFuncbus, \freqbus, freqbus, \foldFuncbus, foldFuncbus], target:s); 2.5.wait; }); }).play(SystemClock); ) //OK. Now we get to the real strength of sc3. Let's play what if. /*What if the trigger occurs regularly every half second, instead of randomly distributed? Note that I'm first stopping the original trigger synth, then substituting a new one. */ a.free; a = { Out.kr(4, Impulse.kr(2)) }.play(s); //Not so much difference, let's switch back to the original trigger. a.free; a = Synth("ch5-trig".postln, [\trigbus, 4], target:s); //What if the frequency rises, then jumps to the minimum value and rises again? d.free; d = { Out.kr(2, LFSaw.kr(0.4, 0, 10, 30)) }.play(s); //What if it oscillates rapidly around a base frequency that rises and falls slowly? d.free; d = { Out.kr(2, LFPulse.kr(7, 0, 0.5, 10, SinOsc.kr(0.2, 0, 30, 50))) }.play(s); /*What if the base frequency moves from plateau to plateau, sliding at varying rates of speed, and the oscillation rate changes as well? */ ( d.free; d = { var base; base = Lag.kr( LFNoise0.kr( LFNoise1.kr(0.1, 0.4, 0.5), 30, 50 ), LFNoise1.kr(0.1, 0.3, 0.31) ); Out.kr(2, LFPulse.kr(SinOsc.kr(0.05, 0, 5, 7), 0, 0.5, 10, base)); }.play(s); ) //What if the delay time follows a sawtooth pattern as well? c.free; c = { Out.kr(1, LFSaw.kr(0.07, 0, 0.2, 0.21)) }.play(s); // Now stop the audio (let it fadeout), and then free all the control synths at once. r.stop; [a, b, c, d, e].do({ arg synth; synth.free }); /* In sc2, to make these changes, you would have to stop the synth completely, do some rewriting, then play it from the beginning again. Or, you would have to plan for all the possible alternatives you would want and include a mechanism to switch between them in real time. (Or use Julian Rohrhuber's just-in-time library, but let's stick to out of the box functionality.) In sc3, you can mix and match parts of the processes at will, depending on the degree to which you have modularized your code. Since all the processes are independent, you can stop and start them independently. I think this makes it easier to experiment, and makes the creative process more fluid. */