"Nouveau Reich" — User-defined algorithms to generate and manipulate slippery-chicken data
+ Associated files
This demo piece, Nouveau Reich, and its accompanying
tutorial, will explore the use of user-defined algorithms for the
generation of slippery chicken data. The code for this piece
includes three functions which create lists that are then used as the
values for keyword arguments to the make-slippery-chicken
function.
The resulting composition is an 18-minute work in the style of Steve Reich, with the content being an amalgamation of the chords from his Music for 18 Musicians and the melodic contours and phasing techniques of his Piano Phase, using slippery chicken to implement variations on his principle of gradual process in 75 lines of code.
+ The code
The code is first presented on its own here, then explained point by point below.
NB: It is strongly recommended that the user not copy and paste code from the web browser into the Lisp listener, as this can occasionally lead to errors. The code below can be downloaded in complete form under the Associated files section above.
(defun move-first-to-end (list) (let (i) (setf i (pop list)) (setf list (append list (list i))))) (defun collect-n-rotations (num-rotations list-to-rotate) (loop repeat num-rotations collect list-to-rotate do (setf list-to-rotate (move-first-to-end list-to-rotate)))) (defun auto-curve-from-indices-and-items (indices items) (when (or (<= (apply #'min indices) 0) (> (apply #'max indices) (length items))) (error "all indices must be >0 and <= length of items")) (loop for pos in indices for x from 1 collect x collect (nth (1- pos) items))) (let* ((num-bars 646) (set-pal '((1 ((fs2 b2 d4 a4 d5 e5 a5 d6))) (2 ((b2 fs3 d4 e4 a4 d5 e5 a5 d6))) (3 ((cs3 fs3 e4 a4 e5 a5 e6))) (4 ((fs2 cs3 e4 a4 b4 e5 a5 b5 e6))) (5 ((d2 a2 e4 fs4 gs4 b4 e5 b5))) (6 ((a2 e3 e4 fs4 gs4 b4 cs5 e5 b5))) (7 ((cs3 fs3 fs4 gs4 a4 cs5 a5 cs6))) (8 ((fs2 cs3 fs4 gs4 a4 b4 cs5 fs5))) (9 ((e2 a2 cs4 fs4 gs4 a4 b4 e5 gs5 b5 e6))) (10 ((d2 a2 fs4 gs4 a4 e5 a5 e6))) (11 ((a2 d2 e4 fs4 a4 e5 a5))))) (fib-trans-ids (fibonacci-transitions num-bars (loop for i from 1 to (length set-pal) collect i))) (set-limits-lists (loop for pchs in '(((pno-one (a4 a4 e4 b4 fs4 gs4 gs4 gs4 b4 gs4 e4))) ((pno-two (d5 a4 e5 b4 gs4 gs4 a4 a4 gs4 a4 a4)))) collect (loop for inst in pchs collect (list (first inst) (auto-curve-from-indices-and-items fib-trans-ids (second inst)))))) (basic-bar '(((6 8) - s s s s s s - - s s s s s s -))) (ps-orig '(1 2 3 4 5 2 1 4 3 2 5 4)) (ps-list (fibonacci-transitions num-bars (collect-n-rotations 13 ps-orig))) (rsp (loop for rs from 1 to 2 for psp in (list (list ps-orig) ps-list) collect (list rs (list basic-bar :pitch-seq-palette psp)))) (rsm `((1 ,(loop for p in '(pno-one pno-two) for rs from 1 collect (list p (loop repeat num-bars collect rs)))))) (nouveau-reich (make-slippery-chicken '+nouveau-reich+ :title "Nouveau Reich" :ensemble '(((pno-one (piano :midi-channel 1)) (pno-two (piano :midi-channel 2)))) :staff-groupings '(1 1) :tempo-map '((1 (q. 72))) :set-palette set-pal :set-limits-low (first set-limits-lists) :set-limits-high (second set-limits-lists) :avoid-used-notes nil :avoid-melodic-octaves nil :set-map `((1 ,fib-trans-ids)) :rthm-seq-palette rsp :rthm-seq-map rsm))) (loop for p in '(pno-one pno-two) for n in '(("piano one" "pno i") ("piano two" "pno ii")) with e = (ensemble nouveau-reich) do (setf (staff-name (get-data-data p e)) (first n)) (setf (staff-short-name (get-data-data p e)) (second n))) (midi-play nouveau-reich :midi-file "/tmp/nouveau-reich.mid") (cmn-display nouveau-reich :file "/tmp/nouveau-reich") (write-lp-data-for-all nouveau-reich)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; EOF nouveau-reich.lsp
The code explained
The user-defined functions
move-first-to-end
The first of the four functions defined for this piece
is move-first-to-end
. This little function "pops" the
first item off the list that it is given and appends it again to the
end of that list. It is designed for use in the next
function, collect-n-rotations
.
(defun move-first-to-end (list) (let (i) (setf i (pop list)) (setf list (append list (list i))))) (move-first-to-end '(1 2 3 4 5)) => (2 3 4 5 1)
collect-n-rotations
The second function defined here collects a list of the new lists
created from consecutive calls to move-first-to-end
. The
user specifies an original list and the number of times that list is to
be rotated. This function is designed for automatic generation
of pitch-seq
curves that will reflect Reich's technique of
phasing.
(defun collect-n-rotations (num-rotations list-to-rotate) (loop repeat num-rotations collect list-to-rotate do (setf list-to-rotate (move-first-to-end list-to-rotate)))) (collect-n-rotations 11 '(1 2 3 4 5)) => ((1 2 3 4 5) (2 3 4 5 1) (3 4 5 1 2) (4 5 1 2 3) (5 1 2 3 4) (1 2 3 4 5) (2 3 4 5 1) (3 4 5 1 2) (4 5 1 2 3) (5 1 2 3 4) (1 2 3 4 5))
auto-curve-from-indices-and-items
The third user-defined function creates a list of break-point pairs with x values that increment by 1. It selects its y values from a list of specified items based on the pattern specified in a list of 1-based indices (integers) into that list. The indices must therefore all be between 1 and the length of the list of items, but the list of indices can be of any length, with its indices occurring in any order.
This function is designed for the automatic generation of envelopes
for the set-limits-high
and set-limits-low
arguments of the make-slippery-chicken
function. It will
ensure that limits are specified for each sequence in the piece, in
order to avoid the interpolation of pitches that otherwise occurs when
there are fewer break-point pairs than sequences.
(defun auto-curve-from-indices-and-items (indices items) (when (or (<= (apply #'min indices) 0) (> (apply #'max indices) (length items))) (error "all indices must be >0 and <= length of items")) (loop for pos in indices for x from 1 collect x collect (nth (1- pos) items))) (auto-curve-from-indices-and-items '(1 4 2 3 1 5 5 1 3 2 2 4) '(a b c d e)) => (1 A 2 D 3 B 4 C 5 A 6 E 7 E 8 A 9 C 10 B 11 B 12 D)
Predefining variables with values for make-slippery-chicken
num-bars
The first variable declared, num-bars
, actually
determines the number of sequences that will be make up the
piece. However, since the only rthm-seq
that will be
defined is exactly one bar long, the number of sequences is identical
to the number of bars. This variable will be used either directly or
indirectly to generate the set-limits-
values, as well as
the rthm-seq-map
and set-map
.
(let* ((num-bars 646)
set-pal
The next variable, set-pal
, simply contains the list that
will become the set-palette
of
the slippery-chicken
object to be made. It is declared
here, outside of the scope of that function, since the contents of
the fib-trans-ids
variable are dependent on the length of
the set-palette
for their generation.
The chords used in this set palette are the 11 chords used in Steve Reich's Music for 18 Musicians.
(set-pal '((1 ((fs2 b2 d4 a4 d5 e5 a5 d6))) (2 ((b2 fs3 d4 e4 a4 d5 e5 a5 d6))) (3 ((cs3 fs3 e4 a4 e5 a5 e6))) (4 ((fs2 cs3 e4 a4 b4 e5 a5 b5 e6))) (5 ((d2 a2 e4 fs4 gs4 b4 e5 b5))) (6 ((a2 e3 e4 fs4 gs4 b4 cs5 e5 b5))) (7 ((cs3 fs3 fs4 gs4 a4 cs5 a5 cs6))) (8 ((fs2 cs3 fs4 gs4 a4 b4 cs5 fs5))) (9 ((e2 a2 cs4 fs4 gs4 a4 b4 e5 gs5 b5 e6))) (10 ((d2 a2 fs4 gs4 a4 e5 a5 e6))) (11 ((a2 d2 e4 fs4 a4 e5 a5)))))
fib-trans-ids
The fib-trans-ids
variable uses
the fibonacci-transitions
function to generate a list
that is num-bars long from consecutive numbers starting
with 1
and ending with the length of
the set-pal
variable. The first purpose of this variable
is the automatic generation of a series of set IDs for
the set-map
argument of
the make-slippery-chicken
function. It allows the user
to change the number of sets in the set-palette
and have
the list of set IDs used for the set-map
automatically
adjusted accordingly.
The fib-trans-ids
variable will also be used to
generate the lists for set-limits-high
and set-limits-low
, as is described in more detail
below.
(fib-trans-ids (fibonacci-transitions num-bars (loop for i from 1 to (length set-pal) collect i)))
set-limits-lists
The set-limits- slots and interpolation
The set-limits-high
and set-limits-low
slots
of the slippery-chicken
object are designed to confine the
pitches available to players in the ensemble (or to the ensemble as a
whole) to subsets that gradually change over the course of the
piece. They take lists of break-point pairs that are scaled to the
number of sequences in the piece, and use them to change the pitch
limits for the specified player on a sequence-by-sequence basis. If the
list contains fewer break-point pairs than there are sequences in the
piece, the pitch limits for the remaining sequences are determined
through interpolation. Thus, if a piece has five sequences and the
break-point pairs passed to set-limits-high
for a player
consist of (0 f5 100 a5)
, the upper limit for that
player's five sequences would
be f5
, fs5
, g5
, gs5
,
and a5
.
Since the x values passed to the set-limits-
slots are scaled to the number of sequences in the piece, it can be
helpful to use actual sequence numbers for these values rather than an
arbitrary scale. If this approach were taken, the above list of
break-point pairs could be written as (1 f5 5 a5)
instead.
If the user would like to avoid the interpolation of pitches between
sequences, a break-point pair must be given for each individual
sequence. If, for example, the user would prefer to have the set limits
of the same five bars progress in whole-steps, skipping
the fs
and gs
, this could be done by
indicating (1 f5 2 f5 3 g5 4 g5 5 a5)
.
The latter is the approach taken for Nouveau Reich, but since there are 646 sequences in the piece, the decision was made to generate these lists of break-point pairs automatically, using a loop to retrieve each sequential sequence number and pair it with an item from a predefined list of pitches.
NB: In order to actually get the
pitches f5
, g5
etc., these must also be
present in the set being used for the current
sequence. The set-limits-
slots can consist of pitches
that are not part of the set (and indeed most often will if the
interpolating option is used.)
One list for both set-limits- slots
In order to avoid repeating code, a loop was defined for Nouveau
Reich to generate the lists for both set-limits-high
and set-limits-low
in one step.
The make-slippery-chicken
function will later access these
sublists by using (first set-limits-lists)
for the former
and and (second set-limits-lists)
for the latter.
Two five-pitch subsets for each set in the palette
Although it is not required that the pitches specified for the set
limits are part of the current set, the pitches chosen here for
Nouveau Reich are intentionally selected from each set in
order to provide a subset of five pitches for each player in each
section. This enables an exact mapping of the pitch-seq
objects defined below, all consisting only of numbers 1
to 5
, to specific consecutive pitches within each subset.
Each set in the piece is to be divided into the same two subsets each
time it occurs.
The five-pitch subsets are created by always giving
the pno-one
player the top five pitches of each set and
the pno-two
player the bottom five pitches of the same
set, which may or may not overlap with those of
the pno-one
. This means that set-limits-low
values are only required for pno-one
(always specifying
the fifth pitch from the top) and set-limits-high
values
are only required for pno-two
(always specifying the fifth
pitch from the bottom).
NB: The exact mapping of pitches to pitch-seqs
also requires use of the :avoid-used-notes
and :avoid-melodic-octaves
keywords of
the make-slippery-chicken
function, as described in more
detail below.
Outer loop pairs players with pitches and determines form
The outer loop in the set-limits-lists
therefore
specifies lists of 11 pitches for each player, corresponding to sets 1
to 11. These lists are each paired with the appropriate player
ID. Additional parentheses are added in order to ensure that the form
is complete and can be passed directly to the :set-limits-
keywords of the make-slippery-chicken
function.
(set-limits-lists (loop for pchs in '(((pno-one (a4 a4 e4 b4 fs4 gs4 gs4 gs4 b4 gs4 e4))) ((pno-two (d5 a4 e5 b4 gs4 gs4 a4 a4 gs4 a4 a4))))
Implementing the auto-curve-from-indices-and-items function
The inner loop uses the auto-curve-from-indices-and-items
function defined above to generate a list of break-point pairs whose
x values start at 1
and increment
by 1
for the number of items in the list of indices. For
its list of indices, it uses the same list generated for the
fib-trans-ids
variable, thereby ensuring that there are
the same number of pitch limits as there are sets in the set map, and
that the same pitch limits are always paired with the same sets from
the set-palette
.
collect (loop for inst in pchs collect (list (first inst) (auto-curve-from-indices-and-items fib-trans-ids (second inst))))))
basic-bar, ps-orig, ps-list, and rsp
The next four variables are used to algorithmically construct
the rthm-seq-palette
, using the
function collect-n-rotations
defined above.
basic-bar
Leaning on the phasing technique found in Steve
Reich's Piano Phase, the only rhythmic material for this piece
will consist of 12 sixteenth notes in a 6/8 bar. This bar is defined
once as basic-bar
.
(basic-bar '(((6 8) - s s s s s s - - s s s s s s -)))
Reich's principle of phasing
Reich's principal of phasing consists of having two players play the same rhythmic pattern and/or melodic contour, initially in unison, and gradually changing the second player's part by shifting the pattern in only that player's part by one unit at a time. The first unit in the pattern is taken off the front of the pattern and moved to the end, thus taking it more and more "out of phase" with each rotation. One of the fascinating by-products of the phasing technique are what Reich calls resulting patterns: new melodic patterns and sensations of changing meter that emerge at each shift.
Non-phasing and phasing pitch-seqs
Since the rhythmic pattern in this piece consists of consecutive
sixteenths, only a shift of the melodic contour will produce an audible
phasing effect. The code for this piece therefore defines an original
melodic contour, as ps-orig
, and then a collection of
gradually shifting permutations of that contour using
the collect-n-rotations
function defined above in
combination with fibonacci-transitions
.
The collect-n-rotations
function is first used to collect
a list of 13 rotations of the original pitch-seq
, meaning
the players will start and end with "unison" melodic contours, and the
second player will gradually pass through all possible rotations of the
original pitch-seq
in the course of the piece. This list
of 13 rotations is then passed to
the fibonacci-transitions
function with
the num-bars
argument to generate a list
of pitch-seq
curves that has one pitch-seq
for each sequence.
(ps-orig '(1 2 3 4 5 2 1 4 3 2 5 4)) (ps-list (fibonacci-transitions num-bars (collect-n-rotations 13 ps-orig)))
Generating the rthm-seq-palette
A short loop is then used to construct a rthm-seq-palette
consisting of only two rthm-seqs
. At each pass through the
loop, a rthm-seq
, with ID 1
or 2
, is created by combining the basic-bar
with a different pitch-seq-palette
.
The ps-orig
is used as the sole pitch-seq
in
the pitch-seq-palette
for the first rthm-seq
,
and the ps-list
is used for the second. (Also see the page
on using multiple curves in the same
pitch-seq-palette).
(rsp (loop for rs from 1 to 2 for psp in (list (list ps-orig) ps-list) collect (list rs (list basic-bar :pitch-seq-palette psp))))
Generating the rthm-seq-map
The final bit of content to be generated for
the make-slippery-chicken
function is
the rthm-seq-map
. This too is done using a small loop,
this time collecting a list of multiple repetitions of
either 1
, 2
, or nil
for the
players pno-one
, pno-two
,
and pno-two-lh
.
This variable is defined using Lisp's backquote and comma reader macros, allowing the code to be formulated more concisely. More on these macros can be found in the entry for backquote at the Common Lisp HyperSpec.
(rsm `((1 ,(loop for p in '(pno-one pno-two pno-two-lh) for rs in '(1 2 nil) collect (list p (loop repeat num-bars collect rs))))))
The call to make-slippery-chicken within a variable
The last variable to be declared within the let*
expression is nouveau-reich
. This variable will contain
the entire slippery-chicken
object generated by calling
the make-slippery-chicken
function, using the other
variables defined above as the values for its arguments. Compared to
the code for Primary Disposition and Second Law, this
call to make-slippery-chicken
looks very sleek.
This piece introduces two new keyword arguments of
the make-slippery-chicken
function,
namely avoid-used-notes
and avoid-melodic-octaves
, which are both required for the
exact mapping of the pitch-seqs
of ps-orig
and ps-list
to the 5-pitch subsets defined above.
(nouveau-reich (make-slippery-chicken '+nouveau-reich+ :ensemble '(((pno-one (piano :midi-channel 1)) (pno-two (piano :midi-channel 2)) (pno-two-lh (piano-lh :midi-channel 2)))) :staff-groupings '(1 2) :tempo-map '((1 (q. 72))) :set-palette set-pal :set-limits-low (first set-limits-lists) :set-limits-high (second set-limits-lists) :avoid-used-notes nil :avoid-melodic-octaves nil :set-map `((1 ,fib-trans-ids)) :rthm-seq-palette rsp :rthm-seq-map rsm)))
avoid-used-notes
By default, slippery chicken's automatic pitch-selection
algorithm endeavors to prevent the same pitch from being assigned to
more than one player in any given sequence. However, as some of the
subsets created by the set-limits-lists
loop overlap in
their pitch content, this would result in some of those subsets having
fewer than five pitches. In order to ensure that each of the subsets
has five pitches, even if some of those subsets contain the same
pitches for the same sequence, so that the pitch-seq
numbers can be exactly mapped to those pitches,
the avoid-used-notes
keyword can be set
to NIL
.
:avoid-used-notes nil
avoid-melodic-octaves
Another default feature of slippery chicken's pitch-selection
algorithm is the avoidance of melodic octaves. This feature will
prevent two non-unison pitches of the same pitch-class from occurring
in sequence in any player's part. This rule, too, will prevent the
exact mapping of pitch-seq
numbers to pitches of the
five-pitch subsets. For this piece, therefore, this feature is also
disabled by setting the avoid-melodic-octaves
keyword of
the make-slippery-chicken
function
to NIL
.
:avoid-melodic-octaves nil
Changing staff names in an existing sc object
Since both players in the Nouveau Reich ensemble use the
same instrument
object of
the +slippery-chicken-standard-instrument-palette+
, they
will, by default, each have the same full and abbreviated staff names
in the score. This can be remedied by changing the value of
the staff-name
and staff-short-name
slots of
the corresponding instrument
objects within
the ensemble
object stored in
the slippery-chicken
object's ensemble
slot. The data in this ensemble
object is copied from and
separate to that of the instrument-palette
, and can
therefore be changed independently. This is done here using a loop
after the slippery-chicken
object has been made.
(loop for p in '(pno-one pno-two) for n in '(("piano one" "pno i") ("piano two" "pno ii")) with e = (ensemble nouveau-reich) do (setf (staff-name (get-data-data p e)) (first n)) (setf (staff-short-name (get-data-data p e)) (second n)))
Since 28/5/14 Staff names can now also be set when initialising a slippery-chicken object, e.g.:
(make-slippery-chicken '+mini+ :ensemble '(((vn1 (violin :midi-channel 1 :staff-names "violin I")) (vn2 (violin :midi-channel 2 :staff-names "violin II")) (vc (cello :midi-channel 3)))) :set-palette '((1 ((gs3 as3 b3 cs4 ds4 e4 fs4 gs4 as4 b4 cs5)))) :set-map '((1 (1 1 1 1 1))) :rthm-seq-palette '((1 ((((2 4) q (e) s (32) 32)) :pitch-seq-palette ((1 2 3))))) :rthm-seq-map '((1 ((vn1 (1 1 1 1 1)) (vn2 (1 1 1 1 1)) (vc (1 1 1 1 1))))))))
However, do see this note for information about Lilypond and instrument names.
Note also that when a player doubles, it is possible to define names and short names for all the instruments used. In this case the strings are simply passed in a list, in the instrument order, as shown below:
(let ((mini (make-slippery-chicken '+mini+ :ensemble '(((solo ((marimba vibraphone) :midi-channel 1 :staff-names ("big wood" "big metal") :staff-short-names ("bw" "bm"))))) :instrument-change-map '((1 ((solo ((1 vibraphone) (3 marimba)))))) :set-palette '((1 ((c4)))) :set-map '((1 (1 1 1 1 1))) :rthm-seq-palette '((1 ((((4 4) (4 (1 1 1 1)))))) (2 ((((4 4) (4 (1 1 1 (1 (1 1)))))))) (3 ((((4 4) (4 (1 1 1 (2 (1 1)))))))) (4 ((((4 4) (4 (1 1 (2 (1 1 1)))))))) (5 ((((4 4) { 3 (te) { 3 (18) 36 } { 3 - 36 36 36 - } } (1 ((4 (1 (1) 1 1 1)) (5 (1 1 1 1)))) (1 (1 (3 (1 1 1 1)))) { 5 - fs x 5 - }))))) :rthm-seq-map '((1 ((solo (1 2 3 4 5)))))))) (lp-display mini))
The output
With all this done, the output for Nouveau Reich can be easily generated in the same manner as for Primary Disposition and Second Law. As this piece has 646 bars and several internal loops for generating data that is processed for each of those bars, the generation time can be quite long. The result is a 28-page LilyPond score, a 41-page CMN score, and an 18-minute MIDI file.
(midi-play nouveau-reich :midi-file "/tmp/nouveau-reich.mid") (cmn-display nouveau-reich :file "/tmp/nouveau-reich") (write-lp-data-for-all nouveau-reich))