4. Examples
The following section outlines a curated set of examples using Gest to control patches written sndkit. Programming and configuration is done using LIL, a tiny TCL-like scripting language included with sndkit.
4.1. How To Render Examples
First, tangle this document using worgle. This will produce all the code snippets mentioned below.
worgle guide.org
Make sure gest
and the lilgest
program has been
compiled, then use it to compile the files below.
./lilgest p0.lil
./lilgest p1.lil
./lilgest p2.lil
./lilgest p3.lil
./lilgest p4.lil
./lilgest p5.lil
./lilgest p6.lil
Or just use the generated shell script above:
sh render_examples.sh
4.2. Part 0: Targets
To start things off, a basic gesture using one looped phrase and three targets will be used.
The entirety of the program can be found below, using named codeblocks to chunk out different sections of the program (a feature of literate programming).
<<generate_gesture>>
<<generate_conductor>>
<<oscillator>>
<<write_to_wav>>
<<unhold_conductor>>
<<compute_audio>>
4.2.1. Overview of the Modular Patch
Before sequencing the gesture, a few words on the underlying patch, which will be used in subsequent examples after this initial one.
The conductor signal is generated using the phasor
algorithm in sndkit. Set at a rate of 1.5Hz, this is
equivalent to 90BPM. To make it easier to access, this
signal is stored in a register. hold
and unhold
are
low-level things that allow the cable to safely be
stored for later.
phasor 1.5 0
hold zz
regset zz 0
regget 0
unhold zz
The main patch is a subtractive sawtooth oscillator patch.
A bandlimited saw oscillator blsaw
is fed into a 1-pole
virtual-analog lowpass filter valp1
. Gest will be used
to manipulate the frequency of oscillator. The Gesture
produces a sequence seq
in units of MIDI note numbers,
which must be converted to frequency using mtof
.
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
The output of the oscillator is written to a WAV file
p0.wav
using wavout
.
wavout zz "p0.wav"
At the end, 10 seconds of audio is computed.
computes 10
4.2.2. The Gesture
This gesture, encapsulated in a function
called seq
, will produce a signal that controls the pitch
of the oscillator in units of MIDI note numbers.
func seq {} {
<<create_gesture>>
<<begin_gesture>>
<<add_targets>>
<<finish_gesture>>
<<synthesize_gesture>>
}
A new instance of gest is made with gest_new
and pushed
onto the underlying stack, and then duplicated
(the reference to the instance) with dup
. Under
the hood, there's some
implicit stack behavior happening that makes this code
easier to read, but enough about that!
gest_new
dup
A new phrase is created with gest_begin
. This phrase
will allocate a chunk of time 3 beats long (first argument),
and divide it into 3 equal parts. Because they are the same
value, this makes the internal clock of this phrase match
the conductor.
gest_begin 3 3
These 3 parts (often referred to here as "ramps") will be
capped with 3 targets using gest_target
, a command
taking the value of the target as its argument.
gest_target 64
gest_target 67
gest_target 60
The phrase is ended with gest_end
. This will be the only
phrase created for the gesture, which will be set to loop
back on itself using gest_loopit
. The gesture is completed
with gest_finish
.
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
The gesture is synthesized using the command gesticulate
.
The conductor signal is retrieved from register 0 using
regget
.
4.2.3. Output Results
Because discrete notes were used as targets, one could expect to hear discrete notes in the output. Instead, they are all glissando'd together like some LFO. This is because the default behvaior of a target is linear. These targets are acting like breakpoints in a line generator!
4.3. Part 1: Behaviors
The next example build off the previous example by
explicitly defining target behaviors. After a target
is created with gest_target
, it is explicitly defined
to have step behavior with gest_step
. This command
works with the last created target.
The step behavior will not do any form of interpolation between itself and the next target, creating the kind of signal one would find in a classic sequencer.
func seq {} {
gest_new
dup
gest_begin 3 3
gest_target 64
gest_step
gest_target 67
gest_step
gest_target 62
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
phasor 1.5 0
hold zz
regset zz 0
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
wavout zz "p1.wav"
regget 0
unhold zz
computes 10
4.4. Part 2: Polyramps
Rhythmic subdivisions in gestures are done using polyramps, which get their name because they divide up a larger ramp into smaller ones.
When the phrase is first instantiated, it produces a ramp tree with 3 nodes which produce 3 ramps.
The first polyramp that gets created divides the leftmost ramp into two smaller ramps, and targets are bound to these with step behavior.
When the next target gets created, there are no available ramps left in the polyramp, so it moves leftwards to the next available ramp, which happens to be the second ramp found in the top of phrase.
The second polyramp divides the last ramp into 2 parts like the first. The very last target is left to have the default linear behavior so it glisses back on itself.
func seq {} {
gest_new
dup
gest_begin 3 3
# first polyramp
gest_polyramp 2
gest_target 64
gest_step
gest_target 66
gest_step
gest_target 67
gest_step
# second polyramp
gest_polyramp 2
gest_target 69
gest_step
gest_target 62
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
phasor 1.5 0
hold zz
regset zz 0
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
wavout zz "p2.wav"
regget 0
unhold zz
computes 10
4.5. Part 3: Monoramps
The monoramp can be thought of as the reverse of a polyramp. It takes two or more consecutive ramps at the same level of tbe underlying ramp tree, and merges them into one continuous ramp. From there, they can be optionally subdivided further using polyramps (this will come later).
Like the previous examples, this gesture uses a single
looped phrase that is 3 beats long divided into 3 ramps.
A monoramp, created using gest_monoramp
is used to take
the first 2 ramps to produce
a note 2 beats long, leaving the second note to be one beat
long.
func seq {} {
gest_new
dup
gest_begin 3 3
gest_monoramp 2
gest_target 64
gest_step
gest_target 62
gest_step
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
phasor 1.5 0
hold zz
regset zz 0
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
wavout zz "p3.wav"
regget 0
unhold zz
computes 10
4.6. Part 4: Nested Polyramps
Polyramps can be populated with more polyramps to do more rhythmic subdivisions.
This phrase in this gesture consists of two nested polyramps. The first nested polyramp divides the ramp into 2, then one of the parts into 2 again. The second nested polyramp creates a triplet rhythm, then subdivides the last triplet beat into 2 parts.
func seq {} {
gest_new
dup
gest_begin 3 3
gest_polyramp 2
gest_target 64
gest_step
gest_polyramp 2
gest_target 66
gest_step
gest_target 67
gest_step
gest_target 69
gest_step
gest_polyramp 3
gest_target 72
gest_step
gest_target 71
gest_step
gest_polyramp 2
gest_target 62
gest_step
gest_target 63
gest_step
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
phasor 1.5 0
hold zz
regset zz 0
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
wavout zz "p4.wav"
regget 0
unhold zz
computes 10
4.7. Part 5: Complex Rhythms
Combining monoramps and polyramps can be used to produce more complex rhythms. In this example, a monoramp is used to take up the first 2 beats, and then this resulting ramp is divided up into a quintuplet rhythm (5 parts). The last beat is divided up into to parts to create an eigth note rhythm.
func seq {} {
gest_new
dup
gest_begin 3 3
gest_monoramp 2
gest_polyramp 5
gest_target 64
gest_step
gest_target 66
gest_step
gest_target 67
gest_step
gest_target 69
gest_step
gest_target 62
gest_step
gest_polyramp 2
gest_target 71
gest_step
gest_target 72
gest_step
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
phasor 1.5 0
hold zz
regset zz 0
blsaw [mtof [seq]]
mul zz 0.8
valp1 zz 300
wavout zz "p5.wav"
regget 0
unhold zz
computes 10
4.8. Part 6: Temporal Weight and Multiple Gestures
This guide will conclude by garnishing the previous example with temporal weight and more gestures to emphasize musical phrasing.
Temporal weight can be used as a mechanism to dynamically change tempo based on context, rather than relying on a tempo automation curve to do the work. When certain targets play, they change the global inertia and mass of the gesture. Ab increase in mass makes things faster. An increase in inertia reaction time to tempo changes slower.
In this particular example, temporal weight is used to shape the the phrasing of the quintuplets. The mass is reduced here so that it gently eases up on the tempo before reaching the peak high note. This is used to build up anticipation.
A second gesture, called brightness
, adds some rudimentary
timbral expression by manipulating the filter cutoff amount
during the phrase.
func seq {} {
regget 1
dup
gest_begin 3 3
gest_monoramp 2
gest_polyramp 5
gest_target 64
gest_step
gest_target 66
gest_step
gest_target 67
gest_step
# decrease mass and increase inertia
gest_inertia 0.5
gest_mass -90
gest_target 69
gest_step
gest_target 62
gest_step
# reset inertia
gest_inertia 0
gest_mass 0
gest_polyramp 2
gest_target 71
gest_step
gest_target 72
gest_step
gest_end
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
func expression {} {
gest_new
dup
gest_begin 3 2
gest_target 0
gest_target 1
gest_loopit
gest_finish
regget 0
gesticulate zz zz
}
gest_new
regset zz 1
regget 1
gestweight zz
mul zz 0.7
add zz 1.5
phasor zz 0
hold zz
regset zz 0
expression
hold zz
regset zz 2
blsaw [mtof [add [seq] [sine [param 6] [mul [regget 2] 0.1]]]]
mul zz [scale [regget 2] 0.5 0.8]
valp1 zz [scale [regget 2] 300 800]
wavout zz "p6.wav"
regget 0
unhold zz
regget 2
unhold zz
computes 10
prev | home | next