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:
21 ~kik = Penvir(~kikEnvir, Pn(Plazy({
29 *(~pbindPairs.value(#[amp, decay2]))
31 \freq, Pif(Pkey(\amp) > 0, 1, \rest)
33 }), inf)).play(quant: 4);
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.
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;
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))
72 // save starting time, to recognize the last bar of a 4-bar cycle
74 if(~startTime.isNil) { ~startTime = thisThread.clock.beats };
76 // convert the rhythm arrays into patterns
78 var pairs = Array(keys.size * 2);
80 if(key.envirGet.notNil) { pairs.add(key).add(Pseq(key.envirGet, 1)) };
84 // identify rests in the rhythm array
85 // (to know where to stick notes in)
86 getRestIndices: { |array|
87 var result = Array(array.size);
89 if(item == 0) { result.add(i) }
97 TempoClock.default.tempo = 104 / 60;
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],
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|
113 ~amp[index] = index.linexp(0, 15, 0.2, 0.5);
114 ~decay2[index] = 0.15;
117 available.scramble[..rrand(0, 2)].do({ |index|
118 ~amp[index] = rrand(0.15, 0.3);
119 ~decay2[index] = rrand(0.05, 0.1);
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],
130 var beat16pos = (thisThread.clock.beats - ~startTime) % 16,
131 available = ~getRestIndices.(~baseAmp),
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);
141 available.scramble[..rrand(1, 3)].do({ |index|
142 ~amp[index] = rrand(0.15, 0.3);
143 ~decay[index] = rrand(0.2, 0.4);
150 parent: ~commonFuncs,
152 baseDelta: 0.25 ! 16,
154 var beat16pos = (thisThread.clock.beats - ~startTime) % 16,
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)]
163 toAdd = available.scramble[..rrand(0, 1)]
166 ~amp[index] = ~doubleTimeAmps;
167 ~dur[index] = ~doubleTimeDurs;
170 doubleTimeAmps: Pseq(#[15, 10], 1),
171 doubleTimeDurs: Pn(0.125, 2)
175 ~kik = Penvir(~kikEnvir, Pn(Plazy({
183 *(~pbindPairs.value(#[amp, decay2]))
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)
190 }), inf)).play(quant: 4);
192 ~snr = Penvir(~snrEnvir, Pn(Plazy({
197 \instrument, \kraftySnr,
199 *(~pbindPairs.value(#[amp, decay]))
201 \freq, Pif(Pkey(\amp) > 0, 5000, \rest)
203 }), inf)).play(quant: 4);
205 ~hh = Penvir(~hhEnvir, Pn(Plazy({
210 \instrument, \kraftySnr,
214 *(~pbindPairs.value(#[amp, dur]))
216 \freq, Pif(Pkey(\amp) > 0, 12000, \rest)
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::