This section will mainly focus on writing custom patterns and pattern transformations, which should enable you, with a bit of time and programming knowledge to create any type of generative music system your heart desires in gibberwocky.
A sequence consists of a pattern for output and a pattern for scheduling (“values” and “timings”). Each pattern is just a function that returns a value.
When you pass arrays or special objects to any call to .seq, this objects are wrapped in functions. For example, consider the following sequence:
namespace('test').seq( [0,1,2,3], 1 )
The first array of numbers (0,1,2,3) is converted to a function that outputs each value, one at a time, progressing linearly and looping when it reaches the end of the arrray. The second argument to .seq is converted to a function that always outputs the same value, in this case 1.
The important insight is that we can easily write our own pattern generators to exhibit any behavior we choose. For example, the following codeblock is functionally identical to the single line of code above:
let idx = 0
const values = [0,1,2,3]
const valuesFunc = ()=> values[ idx++ % values.length ]
namespace('test').seq( valuesFunc, ()=> 1 )Note that we can also chain these types of functions using an array. For example, here’s an additional function that skips through the array two at a time; we’ll add that to our previous test sequence:
let idx = 0
const values = [0,1,2,3]
const valuesFunc = ()=> values[ ++idx % values.length ]
const skipper = ()=> values[ ( idx += 2 ) % values.length ]
namespace('test').seq( [ valuesFunc, skipper ), ()=> 1/2 )If you’ve gone through the patterns and pattern transformations tutorial, you might know that there are a lot of simple serialist techniques that you’re able to apply to patterns, for example .reverse, .rotate, .transpose etc. In gibberwocky we can also use custom functions (or filters)to create these pattern transformations.
The function signature for a pattern filter is as follows:
filter( args, pattern )
Both the argsarray and the pattern object will automatically be passed to the function once it has been added as a filter to a pattern in a running sequence. While the pattern variable simply refers to the pattern that contains the filter, the args array contains three values. The first is the output of the pattern, which we can modify before returning it from our function. The second is a modifier for the phase of the pattern, which will normally be 1 if the pattern is advancing linearly through a sequence, but could also be zero if we want to repeat a value or a higher number if we want to skip values like we did earlier. The final value is the current index.
Let’s say we wanted to create a pattern filter that would repeat every value it outputs twice. We could do that as follows:
const repeatTwice = ( args, pattern ) => { args[1] = .5; return args }
namespace('test').seq( [0,1,2,3], 1/4 )
namespace('test')[0].values.filters.push( repeatTwice )Note that we are modifying the args array that the filter is passed and then returning it. For example, if we wanted to scale a pattern by value, we could do so as follows:
const scale = ( args, pattern ) => { args[0] *= 2; return args }
const repeatTwice = ( args, pattern ) => { args[1] = .5; return args }
namespace('test').seq( [0,1,2,3], 1/4 )
namespace('test')[0].values.filters.push( repeatTwice, scale )You can also write this a bit more tersely, which is useful for long chains:
namespace('test').seq( [0,1,2,3], 1/4 )
namespace('test')[0].values.filters.push(
( args, pattern ) => { args[0] *= 2; return args },
( args, pattern ) => { args[1] = .5; return args }
)- Rotate two Euclidean rhythms against each other. I do this all over the place, for melodies, bass lines, and beats.
devices['drums'].midinote.seq( 36, Euclid(5,8) )
devices['drums'].midinote.seq( 36, Euclid(5,16), 1 )
devices['drums'].midinote[1].timings.rotate.seq( 1,1 )- Use two oscillators with differing frequencies / amplitudes to drive mono synths.
devices['bass'].note.seq( sine(1,0,3), Euclid(3,8) )
devices['bass'].note.seq( sine(2,0,4), Euclid(4,9), 1 )A video of using oscillators to drive sample-and-hold patterns: https://www.youtube.com/watch?v=dieGOIrp2L4&feature=youtu.be
- Make sure to remember to sequence velocity (assuming your synth is velocity enabled). You can either sequence velocity for all sequences targeting a particular synth, or for individual sequences:
// control all velocities on 'velocitysynth'. note that if you don't pass
// a scheduling pattern, gibberwocky will call this sequence every time
// a note is triggered. here we use a sine oscillator to gradually
// move velocities from 2-62.
devices['velocitysynth'].velocity.seq( sine( 2, 32, 30 ) )
// tell our note sequence number 2 to use a different sine osc for
// velocities; all other sequnces will use the previous one we assigned.
devices['velocitysynth'].note[2].velocity.seq( sine( 1, 15, 10 ) )- Transitions are tricky! One function that should really be documented In the tutorials is the
fade()function. It accepts three parameters: a duration, a starting value, and an ending value. I often find that about a fifth of my code is fading different instruments in and out, fading in and out FX etc.
Simple example of a fade:
devices['bass'].note.seq( sine(2,0,3), Euclid(5,16) )
devices['bass'].mod_wave( fade(16,0,127) )NOTE: ACK! Fades are apparently broken for Max/MSP. I will fix this ASAP.
-
I love
HexStepsfor defining beats. However,Stepsis also great when you have a velocity-sensitive drum synth to work with. If you’re doing drumbeats definitely check both of those objects out; there’s tutorials for both included in gibberwocky. -
Sequences that have an odd number of values vs. an even number of timings (or vice-versa) wind up creating irregular patterns that eventually “loop”.