Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / HelpSource / Tutorials / A-Practical-Guide / PG_Cookbook07_Rhythmic_Variations.schelp
blob59349a161b1e6c094fa2b2172c775b077526006b
1 title:: PG_Cookbook07_Rhythmic_Variations
2 summary:: An ever-changing drumbeat
3 related:: Tutorials/A-Practical-Guide/PG_Cookbook06_Phrase_Network, Tutorials/A-Practical-Guide/PG_Ref01_Pattern_Internals
4 categories:: Streams-Patterns-Events>A-Practical-Guide
6 section::Creating variations on a base rhythmic pattern
8 Normally patterns are stateless objects. This would seem to rule out the possibility of making on-the-fly changes to the material that pattern is playing. Indeed, modifying an existing pattern object is tricky and not always appropriate (because that approach cannot confine its changes to the one stream making the changes).
10 link::Classes/Plazy:: offers an alternate approach: use a function to generate a new pattern object periodically, and play these patterns in succession, one by one. (Plazy embeds just one pattern; wrapping Plazy in link::Classes/Pn:: does it many times.)
12 The logic in this example is a bit more involved: for each measure, start with arrays containing the basic rhythmic pattern for each part (kick drum, snare and hi-hat) and insert ornamental notes with different amplitudes and durations. Arrays hold the rhythmic data because this type of rhythm generation calls for awareness of the entire bar (future), whereas patterns generally don't look ahead.
14 This suggests an object for data storage that will also encapsulate the unique logic for each part. We saw earlier that link::Classes/Penvir:: maintains a distinct environment for each stream made from the pattern. In other words, Penvir allows more complicated behavior to be modeled using an object that encapsulates both custom logic and the data on which it will operate.
16 The specific ornaments to be added are slightly different for the three parts, so there are three environments. Some functions are shared; rather than copy and paste them into each environment, we put them into a separate environment and use that as the parent of the environment for each drum part.
18 Most of the logic is in the drum parts' environments, and consist mostly of straightforward array manipulations. Let's unpack the pattern that uses the environments to generate notes:
20 code::
21 ~kik = Penvir(~kikEnvir, Pn(Plazy({
22         ~init.value;
23         ~addNotes.value;
24         Pbindf(
25                 Pbind(
26                         \instrument, \kik,
27                         \preamp, 0.4,
28                         \dur, 0.25,
29                         *(~pbindPairs.value(#[amp, decay2]))
30                 ),
31                 \freq, Pif(Pkey(\amp) > 0, 1, \rest)
32         )
33 }), inf)).play(quant: 4);
36 definitionList::
37 ## code::Penvir(~kikEnvir, ...):: || Tell the enclosed pattern to run inside the kick drum's environment.
39 ## code::Pn(..., inf):: || Repeat the enclosed pattern (Plazy) an infinite number of times.
41 ## code::Plazy({ ... }):: || The function can do anything it likes, as long as it returns some kind of pattern. The first two lines of the function do the hard work, especially code::~addNotes::.value, calling into the environment to use the rhythm generator code. This changes the data in the environment, which then get plugged into Pbind in the code::~pbindPairs.value():: line. That pattern will play through; when it ends, Plazy gives control back to its parent -- Pn, which repeats Plazy.
43 ## code::Pbindf(..., \freq, ...):: || Pbindf adds new values into events coming from a different pattern. This usage is to take advantage of a fact about the default event. If the code::\freq:: key is a symbol (rather than a number or array), the event represents a rest and nothing will play on the server. It doesn't matter whether or not the SynthDef has a code::freq:: control; a symbol in this space produces a rest. Here it's a simple conditional to produce a rest when code:: amp == 0 ::.
45 ## code::Pbind(...):: || The meat of the notes: SynthDef name, general parameters, and rhythmic values from the environment. (The code::*:: syntax explains the need for Pbindf. The code::\freq:: expression must follow the pbindPairs result, but it isn't possible to put additional arguments after code::*(...) ::. Pbindf allows the inner Pbind to be closed while still accepting additional values.)
48 strong::Third-party extension alert:: : This type of hybrid between pattern-style flow of control and object-oriented modeling is powerful but has some limitations, mainly difficulty with inheritance (subclassing). The strong::ddwChucklib:: quark (which depends on ddwPrototype) expands the object-oriented modeling possibilities while supporting patterns' ability to work with data external to a pattern itself.
50 subsection::Example
52 code::
54 // this kick drum doesn't sound so good on cheap speakers
55 // but if your monitors have decent bass, it's electro-licious
56 SynthDef(\kik, { |basefreq = 50, ratio = 7, sweeptime = 0.05, preamp = 1, amp = 1,
57                 decay1 = 0.3, decay1L = 0.8, decay2 = 0.15, out|
58         var     fcurve = EnvGen.kr(Env([basefreq * ratio, basefreq], [sweeptime], \exp)),
59                 env = EnvGen.kr(Env([1, decay1L, 0], [decay1, decay2], -4), doneAction: 2),
60                 sig = SinOsc.ar(fcurve, 0.5pi, preamp).distort * env * amp;
61         Out.ar(out, sig ! 2)
62 }).add;
64 SynthDef(\kraftySnr, { |amp = 1, freq = 2000, rq = 3, decay = 0.3, pan, out|
65         var     sig = PinkNoise.ar(amp),
66                 env = EnvGen.kr(Env.perc(0.01, decay), doneAction: 2);
67         sig = BPF.ar(sig, freq, rq, env);
68         Out.ar(out, Pan2.ar(sig, pan))
69 }).add;
71 ~commonFuncs = (
72                 // save starting time, to recognize the last bar of a 4-bar cycle
73         init: {
74                 if(~startTime.isNil) { ~startTime = thisThread.clock.beats };
75         },
76                 // convert the rhythm arrays into patterns
77         pbindPairs: { |keys|
78                 var     pairs = Array(keys.size * 2);
79                 keys.do({ |key|
80                         if(key.envirGet.notNil) { pairs.add(key).add(Pseq(key.envirGet, 1)) };
81                 });
82                 pairs
83         },
84                 // identify rests in the rhythm array
85                 // (to know where to stick notes in)
86         getRestIndices: { |array|
87                 var     result = Array(array.size);
88                 array.do({ |item, i|
89                         if(item == 0) { result.add(i) }
90                 });
91                 result
92         }
97 TempoClock.default.tempo = 104 / 60;
99 ~kikEnvir = (
100         parent: ~commonFuncs,
101                 // rhythm pattern that is constant in each bar
102         baseAmp: #[1, 0, 0, 0,  0, 0, 0.7, 0,  0, 1, 0, 0,  0, 0, 0, 0] * 0.5,
103         baseDecay: #[0.15, 0, 0, 0,  0, 0, 0.15, 0,  0, 0.15, 0, 0,  0, 0, 0, 0],
104         addNotes: {
105                 var     beat16pos = (thisThread.clock.beats - ~startTime) % 16,
106                         available = ~getRestIndices.(~baseAmp);
107                 ~amp = ~baseAmp.copy;
108                 ~decay2 = ~baseDecay.copy;
109                         // if last bar of 4beat cycle, do busier fills
110                 if(beat16pos.inclusivelyBetween(12, 16)) {
111                         available.scramble[..rrand(5, 10)].do({ |index|
112                                         // crescendo
113                                 ~amp[index] = index.linexp(0, 15, 0.2, 0.5);
114                                 ~decay2[index] = 0.15;
115                         });
116                 } {
117                         available.scramble[..rrand(0, 2)].do({ |index|
118                                 ~amp[index] = rrand(0.15, 0.3);
119                                 ~decay2[index] = rrand(0.05, 0.1);
120                         });
121                 }
122         }
125 ~snrEnvir = (
126         parent: ~commonFuncs,
127         baseAmp: #[0, 0, 0, 0,  1, 0, 0, 0,  0, 0, 0, 0,  1, 0, 0, 0] * 1.5,
128         baseDecay: #[0, 0, 0, 0,  0.7, 0, 0, 0,  0, 0, 0, 0,  0.4, 0, 0, 0],
129         addNotes: {
130                 var     beat16pos = (thisThread.clock.beats - ~startTime) % 16,
131                         available = ~getRestIndices.(~baseAmp),
132                         choice;
133                 ~amp = ~baseAmp.copy;
134                 ~decay = ~baseDecay.copy;
135                 if(beat16pos.inclusivelyBetween(12, 16)) {
136                         available.scramble[..rrand(5, 9)].do({ |index|
137                                 ~amp[index] = index.linexp(0, 15, 0.5, 1.8);
138                                 ~decay[index] = rrand(0.2, 0.4);
139                         });
140                 } {
141                         available.scramble[..rrand(1, 3)].do({ |index|
142                                 ~amp[index] = rrand(0.15, 0.3);
143                                 ~decay[index] = rrand(0.2, 0.4);
144                         });
145                 }
146         }
149 ~hhEnvir = (
150         parent: ~commonFuncs,
151         baseAmp: 15 ! 16,
152         baseDelta: 0.25 ! 16,
153         addNotes: {
154                 var     beat16pos = (thisThread.clock.beats - ~startTime) % 16,
155                         available = (0..15),
156                         toAdd;
157                         // if last bar of 4beat cycle, do busier fills
158                 ~amp = ~baseAmp.copy;
159                 ~dur = ~baseDelta.copy;
160                 if(beat16pos.inclusivelyBetween(12, 16)) {
161                         toAdd = available.scramble[..rrand(2, 5)]
162                 } {
163                         toAdd = available.scramble[..rrand(0, 1)]
164                 };
165                 toAdd.do({ |index|
166                         ~amp[index] = ~doubleTimeAmps;
167                         ~dur[index] = ~doubleTimeDurs;
168                 });
169         },
170         doubleTimeAmps: Pseq(#[15, 10], 1),
171         doubleTimeDurs: Pn(0.125, 2)
175 ~kik = Penvir(~kikEnvir, Pn(Plazy({
176         ~init.value;
177         ~addNotes.value;
178         Pbindf(
179                 Pbind(
180                         \instrument, \kik,
181                         \preamp, 0.4,
182                         \dur, 0.25,
183                         *(~pbindPairs.value(#[amp, decay2]))
184                 ),
185                         // default Event checks \freq --
186                         // if a symbol like \rest or even just \,
187                         // the event is a rest and no synth will be played
188                 \freq, Pif(Pkey(\amp) > 0, 1, \rest)
189         )
190 }), inf)).play(quant: 4);
192 ~snr = Penvir(~snrEnvir, Pn(Plazy({
193         ~init.value;
194         ~addNotes.value;
195         Pbindf(
196                 Pbind(
197                         \instrument, \kraftySnr,
198                         \dur, 0.25,
199                         *(~pbindPairs.value(#[amp, decay]))
200                 ),
201                 \freq, Pif(Pkey(\amp) > 0, 5000, \rest)
202         )
203 }), inf)).play(quant: 4);
205 ~hh = Penvir(~hhEnvir, Pn(Plazy({
206         ~init.value;
207         ~addNotes.value;
208         Pbindf(
209                 Pbind(
210                         \instrument, \kraftySnr,
211                         \rq, 0.06,
212                         \amp, 15,
213                         \decay, 0.04,
214                         *(~pbindPairs.value(#[amp, dur]))
215                 ),
216                 \freq, Pif(Pkey(\amp) > 0, 12000, \rest)
217         )
218 }), inf)).play(quant: 4);
221 // stop just before barline
222 t = TempoClock.default;
223 t.schedAbs(t.nextTimeOnGrid(4, -0.001), {
224         [~kik, ~snr, ~hh].do(_.stop);
228 Previous:       link::Tutorials/A-Practical-Guide/PG_Cookbook06_Phrase_Network::
230 Next:           link::Tutorials/A-Practical-Guide/PG_Ref01_Pattern_Internals::