"Nouveau Reich" — User-defined algorithms to generate and manipulate slippery-chicken data

+ Associated files

close

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

close

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))