spobooks bbv9810.0001.001 in

    Chapter 10: Sets and Tables

    A set is a collection of objects. In Common LISP, we can think of a set as a list. Each object in a set is called an element or a member . In algorithmic composition, sets may be manipulated to achieve a compositional result.

    10.1 Introduction to Set Theory

    The analysis of atonal music using set theory mathematics was codified by Allen Forte in his landmark book, "The Structure of Atonal Music." [Forte, 1973] His theory of atonal music develops a comprehensive framework for the organization of collection of pitches referred to as pitch class sets . A pitch class is an integer in the range 0-11 that represents a symbolic note name. Pitch class assignment in twelve-tone equal temperament is C (or its enharmonic equivalent) is 0, C-sharp (or its enharmonic equivalent) is 1, D-flat (or its enharmonic equivalent) is 2. . . and so on. A pitch class set, or pc set, is a collection of integers representing pitch classes.

    Forte grouped pc sets by cardinality . The cardinality of a set is the number of elements in that set. Within each cardinality, pc sets are assigned a unique integer identifier for the prime form of a set. The prime form of a set is the arrangement of pitch classes such that the smallest intervals are at the beginning of the set, the interval between the first and last members of the set is smaller than the interval between the last and first members of the set, and the first pitch class is 0. For example, set 4-1 is comprised of pitch classes (0 1 2 3). The 4 in the pc set name represents the cardinality of the set. The 1 is the unique integer identifier associated with that set. Only pc set 4-1 is comprised of a succession of three minor seconds.

    10.2 Set Operations

    Common LISP provides a number of functions that perform operations on lists or sets. These primitives are helpful in analyzing or composing music that is based on sets.APPEND, which was first discussed in Chapter 3, is very helpful in manipulating sets.

    APPEND may take two or more lists as input and return a list of all of the elements of the first list followed by all of the elements of the second. When appending lists, the template for APPEND is:
    (APPEND LIST LIST)

    In the following example, we assign pitch class sets to two global variables 6-1 and 5-2. We use APPEND to create a list of the two pc sets in succession.

    Example 10.2.1
    ? (setf 6-1 '(0 1 2 3 4 5))
    (0 1 2 3 4 5)
    ? (setf 5-2 '(0 1 2 3 5))
    (0 1 2 3 5)
    ? (append 6-1 5-2)
    (0 1 2 3 4 5 0 1 2 3 5)

    In Example 10.2.2, we use major triads on C, F and G as sets. A major triad corresponds to set 3-11 (0 3 7). You may look at the pitch classes and see a c-minor triad or C E-flat G. In set theory, the major and minor triads are equivalent because they reduce to the same prime form. The Common Music declaration vars assigns the lists of symbolic note names to variables that are local to the container. The variables that represent the lists are appended and converted into a cyclic item stream using make-item-stream.

    audio file sets.mp3

    Example 10.2.2: append.lisp

    (generator append midi-note (length 10 channel 0)
    #|
    We use vars to declare and assign variables.
    The variable-value pairs in vars are evaluated once when the
    container is scheduled to run.
    |#
    (vars (c-major '(c4 e4 g4))
    (f-major '(f4 a4 c5))
    (g-major '(g4 b4 d5))
    (the-list (make-item-stream 'notes 'cycle
    (append c-major f-major g-major '(c5)))))
    #|
    We use append to create a big list comprised of the symbolic
    notes names assigned to the variables c-major, f-major and
    g-major and the list '(c5). We use make-item-stream to create
    an item stream of notes using the cycle pattern type
    |#
    (setf note (item the-list))
    #|
    We use the item-stream-accessor item to assign one element of
    the item stream to the note slot.
    |#
    (setf rhythm (item (rhythms s s. s.. in heap)))
    (setf duration (+ (* rhythm .5)))
    (setf amplitude (item (crescendo from pp to ff in 10))))
    The Common LISP primitive REVERSE makes a retrograde of its input. The template for REVERSE is:
    (REVERSE LIST)
    Example 10.2.3
    ? (reverse '(0 1 2 3 4 5)
    (5 4 3 2 1 0)

    What's the difference between the Common LISP primitive REVERSE and the Common Music macro retrograde ?REVERSE expects a list as input whereas retrograde expects an item stream.

    In Example 10.2.4, we use the Common Music function make-item stream to create an item stream of the pitch classes in set 6-1. The Common Music macro retrograde reverses the order of elements in the item stream. The Common Music function read-items shows the result of the retrograded item stream.

    Example 10.2.4

    Stella [Top-Level]: (read-items (retrograde (make-item-stream 'items 'cycle '(0 1 2 3 4 5))))
    (5 4 3 2 1 0)

    Example 10.2.5 uses REVERSE and retrograde in a Common Music generator.

    Example 10.2.5: reverse.lisp
    (generator reverse midi-note (length 9 channel 0)
    (vars (c-major (reverse '(c4 e4 g4)))
    ;;; c-major is '(g4 e4 c4)
    (f-major (reverse '(f4 a4 c5)))
    ;;; f-major is '(c5 a4 f4)
    (g-major (reverse '(g4 b4 d5)))
    ;;; g-major is d5 b4 g4
    (the-list (make-item-stream 'notes 'cycle
    (append c-major f-major g-major))))
    ;;; the-list is g4 e4 c4 f4 a4 c5 d5 b4 g4
    (format t "~% c-major ~a f-major ~a g-major ~a" c-major f-major g-major)
    (setf note (item (retrograde the-list)))
    #|
    The retrograde of the list is g4 b4 d5 c5 a4 f4 c4 e4 g4
    Why do we have to use the Common Music macro retrograde on
    the the-list rather than reverse?
    Because reverse does not expect an item stream as input.
    |#
    (setf rhythm (item (rhythms s s. s.. in heap)))
    (setf duration (+ (* rhythm .5)))

    (setf amplitude (item (crescendo from pp to ff in 9))))

    The Common LISP primitive NTH returns a specified element of a list- the nth element of a list. The template for NTH is:
    (NTH INDEX LIST)
    where INDEX is the zero-based index that accesses elements in the list.

    audio file reverse.mp3

    Example 10.2.6
    ? (nth 4 '(0 1 2 3 5))
    5

    In Example 10.2.7, we use NTH to randomly access elements in a set.LET* is used to assign the local variables INDEX, 6-Z36, and OCTAVE. These variables are used to determine the value of the note slot.

    Example 10.2.7: nth.lisp
    (generator nth midi-note (length 25 channel 0)
    (let* ((index (random 6))
    ;;; return a random integer in the range 0-5
    (6-Z36 '(0 1 2 3 4 7))
    ;;; 0 1 2 3 4 7 is set 6-Z36 according to
    ;;; Allen Forte's "Structure of Atonal Music"
    (octave (nth (random 2) '(36 48))))
    ;;; use nth to return a random octave designation
    (setf note (+ octave (nth index 6-Z36))))
    ;;; nth returns a random index into the set 6-Z36 and it is
    ;;; randomly transposed up either 3 or 4 octaves
    (setf amplitude (item (amplitudes ppp pp p mp ff in heap)))
    (setf rhythm .2)
    (setf duration .3))

    audio file nth.mp3

    The Common LISP predicate MEMBER checks to see if an element is included in a list. If the element is not found in the list, MEMBER returns NIL. If the element is found, MEMBER returns a subset beginning with the found member.

    Example 10.2.8
    ? (member 6 '(0 1 2 3 5))
    NIL
    ? (member 3 '(0 1 2 3 5))
    (3 5)

    Why is MEMBER considered a predicate if it returns a sublist? Recall that in Common LISP, any non-NIL value evaluates to T.

    Example 10.2.9 creates a merge container of generators melody, accompaniment, and final-chord.

    audio file member.mp3

    Example 10.2.9: member.lisp
    (merge member ()
    ;;; the musical material is based on 4 sets:
    ;;; 6-Z3, 6-Z36, 5-Z38 and 7-Z38
    (generator melody midi-note (length 101 channel 0)
    (vars (6-Z36 '(0 1 2 3 4 7))
    (6-Z3 '(0 1 2 3 5 6))
    (5-Z38 '(0 1 2 5 8))
    (7-Z38 '(0 1 2 4 5 7 8))
    (notes1 (make-item-stream 'items 'heap
    (member 2 6-Z36)))
    (notes2 (make-item-stream 'items 'heap
    (member 2 6-Z3)))
    (notes3 (make-item-stream 'items 'heap
    (member 2 5-Z38)))
    (notes4 (make-item-stream 'items 'heap
    (member 2 7-Z38))))
    #|
    Declare and assign the variables. Make an item stream from a subset of each set from pitch class 2: the largest pitch class that all sets have in common.
    |#
    (if (< time 5) (setf note (+ 24 (item notes1)))
    (if (and (>= time 5) (< time 10))
    (setf note (+ 36 (item notes2)))
    (if (and (>= time 10) (< time 15))
    (setf note (+ 48 (item notes3)))
    (setf note (+ 60 (item notes4))))))
    #|
    Use a nested if that monitors time to select which set is played.
    |#
    (setf amplitude (* (item (amplitudes pp p p mp ff fff in heap)) (/ count 101)))
    ;;; Scale the amplitudes over the length of the generator
    (setf rhythm .2)
    (setf duration rhythm))
    (generator accompaniment midi-note (end 20 channel 0)
    (if (< time 5) (setf note (item (items (chord 50 51 52 53 54 57))))
    ;;; Chord based on 6-Z36
    (if (and (>= time 5) (< time 10)) (setf note (item
    (items (chord 60 61 62 63 65 66))))
    ;;; Chord based on 6-Z3
    (if (and (>= time 10) (< time 15)) (setf note
    (item (items (chord 70 71 72 75 78))))
    ;;; Chord based on 5-Z38
    (setf note (item (items
    (chord 80 81 82 84 85 87 88)))))))
    ;;; Chord based on 7-Z38
    (setf amplitude (item (items .01 .02 .03 .04 .05 .06 .07 .08 .09 .01 .02 .03 .04 .05 .06 .07 .08 .09 .1 .2 .3 .4 .5 .1 .2 .3 .4 .5 .1 .2 .3 .4 .5 .1 .2 .3 .4 .5 .1 .2 .3 .4 .5 .6 .7 in sequence)))
    (setf rhythm (item (rhythms q q. h h. in heap)))
    (setf duration .1))
    (generator final-chord midi-note (start 21 length 1)
    (setf note (item (items (chord 12 25 38 51 64 77 90 103 116))))
    #|
    Final chord is based on the union of pitch classes from all four sets (0 1 2 3 4 5 6 7 8) or set 9-1.
    Each pitch class is transposed up a successive octave (12 24 36 48 60 72 84 96 108)
    |#
    (setf amplitude .9)
    (setf rhythm 1)
    (setf duration 2)))

    The Common LISP primitive INTERSECTION takes two lists as input and returns the intersection of the two sets, that is, a list of the elements common to both sets.

    In Example 10.2.10, we take the INTERSECTION of pc sets 6-Z3 and 6-Z36.

    Example 10.2.10

    ? (intersection '(0 1 2 3 5 6) '(0 1 2 3 4 7))
    (3 2 1 0)

    The Common LISP primitive UNION takes two lists as input and returns the elements that are found in either set.

    Example 10.2.11
    ? (union '(0 1 2 3 5 6) '(0 1 2 3 4 7))
    (6 5 0 1 2 3 4 7)

    Example 10.2.12 demonstrates how the Common LISP set operations INTERSECTION and UNION may be used in algorithmic composition. We use vars to assign pc sets to their set names. Within the vars, we take the INTERSECTION and UNION of set combinations, convert the list to an item stream using make-item-stream, and assign the result to a variable. We use the item stream accessor item to select an item from the item streams and assign that item to a slot.

    Example 10.2.12 sets.lisp
    (generator sets midi-note (length 15 channel 0)
    (vars (6-Z36 '(0 1 2 3 4 7))
    (6-Z3 '(0 1 2 3 5 6))
    (5-Z38 '(0 1 2 5 8))
    (7-Z38 '(0 1 2 4 5 7 8))
    (common-set1 (make-item-stream 'items 'heap
    (intersection 6-Z36 6-Z3)))
    (common-set2 (make-item-stream 'items 'heap
    (intersection 5-Z38 7-Z38)))
    (inclusive-set1 (make-item-stream 'items 'cycle
    (union 6-Z36 6-Z3)))
    (inclusive-set2 (make-item-stream 'items 'cycle
    (union 5-Z38 7-Z38))))
    (setf note (+ (item common-set1) 60))
    (setf amplitude (* (item inclusive-set1) .1))
    (if (= (item common-set2) 0) (setf rhythm (+ (* (item common-set2) .5) .01))
    (setf rhythm (* (item common-set2) .5)))
    (if (= (item inclusive-set2) 0) (setf duration (+ (* (item inclusive-set2) .5) .01))
    (setf duration (* (item inclusive-set2) .5))))

    The Common LISP primitive SET-DIFFERENCE performs set subtraction.SET-DIFFERENCE takes two lists as input and subtracts all of the elements in the first list from those that are in common with the second list.SET-DIFFERENCE returns a list.

    Example 10.2.13
    ? (set-difference '(0 1 2 3 5 6) '(0 1 2 3 4 7))
    (6 5)
    ? (set-difference '(0 1 2 3 4 7) '(0 1 2 3 5 6))
    (7 4)
    ? (set-difference '(0 1 2 3) '(4 5 6 7))
    (3 2 1 0)
    ? (set-difference '(0 1 2 3) '(0 1 2 3))
    NIL

    The Common LISP predicate SUBSETP accepts two lists a input and returns T if the first list is a subset of the second and NIL if not.

    Example 10.2.14

    ? (subsetp '(0 1 2 3 4 7) '(0 1 2 3 4 5 6 7 8))
    T
    ? (subsetp '(0 1 2 3 4 7) '(0 1 2 5 8))
    NIL

    10.3 Tables

    Tables are built in Common LISP by making lists of lists. In fact, a table can be thought of as a nested indexed list. In Common LISP, sometimes tables are referred to as association lists or simply a-lists. Consider the table in Figure 10.3.1 that makes a correspondence between pitch class and note name.

    Figure 10.3.1: A Simple Table

    Pitch Class Note Name

    0

    C

    1

    C-sharp

    2

    D

    3

    D-sharp

    4

    E

    In Figure 10.3.1, we refer to the pitch class as the key to the table and the note name as its value. For example, key 1 has a value of C-sharp.

    The way we represent tables or a-lists in Common LISP are as nested lists. Example 10.3.1 converts the table representation of Figure 10.3.1 to a nested list and assigns it to the global variable *SIMPLE-TABLE*.

    Example 10.3.1
    ? (setf *simple-table*
    '((0 C)
    (1 C-sharp)
    (2 D)
    (3 D-sharp)
    (4 E)))

    Example 10.3.2 demonstrates that Common LISP has stored our table as a nested list.

    Example 10.3.2

    ? *simple-table*
    ((0 C) (1 C-SHARP) (2 D) (3 D-SHARP) (4 E))

    Now that we have a table stored in memory, we can look-up things in the table. Common LISP performs table look-up using the function ASSOC. When ASSOC is given a key in a table, it returns a list comprised of the key and its corresponding value(s). It may be helpful to think of ASSOC as returning a specified row in a table as a list.

    Example 10.3.2
    ? (assoc 2 *simple-table*)
    (2 D)

    If we want to find the value associated with a particular key, we simply use list functions to point to the element of interest.

    Example 10.3.3
    ? (second (assoc 2 *simple-table*))
    D

    Performing table look-up is such a common occurrence, we define a Common LISP function TABLE-LOOK-UP in Example 10.3.4 to simplify the procedure.

    Example 10.3.4
    ? (defun table-look-up (key table-name)
    "a generic function to access the second element in a table given its key"
    (second (assoc key table-name)))

    We call the function to perform the look-up.

    Example 10.3.5

    ? (table-look-up 2 *simple-table*)
    D

    Example 10.3.6 demonstrates how you can use tables into Common Music. We assign a table named table using vars. Notice the syntactical difference between assigning a table using SETF and vars. Like LET and LET*,vars requires the variable value pairs be enclosed in parentheses. After table is assigned using vars, we enter a LET* that randomly generates a pitch class in the range 0-11. The randomly-generated pitch class serves as the key for table look-up. We assign the result of the table look-up to the note slot in the body of the LET* .

    audio file table.mp3

    Example 10.3.6: table.lisp
    (generator table midi-note (length 15 channel 0)
    (vars (table
    '((0 C4)
    (1 CS4)
    (2 D4)
    (3 DS4)
    (4 E4)
    (5 F4)
    (6 FS4)
    (7 G4)
    (8 GS4 )
    (9 A4)
    (10 AS4)
    (11 B4))))
    (let* ((random-pitch-class (random 12))
    (note-name (table-look-up random-pitch-class table)))
    (setf note note-name))
    (setf rhythm (item (rhythms s s. e e. in heap)))
    (setf duration rhythm)
    (setf amplitude (item (diminuendo from ff to pp in 15))))

    Example 10.3.7 integrates many of the concepts we have learned in this chapter and applies them to Common Music.

    Example 10.3.7: table-with-sets.lisp
    (defun table-look-up (key table-name)
    "a generic function to access the second element in a table given its key"
    (second (assoc key table-name)))
    #|
    Generator table-with-sets creates a table of the first four pitch-class sets of cardinality 6. A random number selects one of four keys in the range 1-4 that corresponds to sets 6-1, 6-2, 6-Z3 and 6-Z4. We use the function table-look-up to return the pitch classes associated with the key. We assign a pitch-class to the note slot by randomly generating an index to the list which we access using nth.
    |#
    (generator table-with-sets midi-note (length 60 channel 0)
    (vars (table-cardinal6
    '((1 (0 1 2 3 4 5) 6-1)
    (2 (0 1 2 3 4 6) 6-2)
    (3 (0 1 2 3 5 6) 6-Z3)
    (4 (0 1 2 4 5 6) 6-Z4)))
    (key1 (+ (random 4) 1))
    (key2 (+ (random 4) 1))
    (key3 (+ (random 4) 1))
    (key4 (+ (random 4) 1))
    (set1 (table-look-up key1 table-cardinal6))
    (set2 (table-look-up key2 table-cardinal6))
    (set3 (table-look-up key3 table-cardinal6))
    (set4 (table-look-up key4 table-cardinal6)))
    (let ((index (random 6)))
    #|
    generate a random number to access one pitch class in the set
    |#
    (cond ((< count 15) (setf note (+ (nth index set1) 40)))
    ((and (>= count 15) (< count 30))
    (setf note (+ (nth index set2) 50)))
    ((and (>= count 30) (< count 45))
    (setf note (+ (nth index set3) 60)))
    (t (setf note (+ (nth index set4) 70)))))
    (setf rhythm (item (items .2 .4 .5 in heap)))
    (setf duration .1)
    (setf amplitude (item (amplitudes pp mp mf ff in heap))))

    audio file table-with-sets.mp3

    10.4 Suggested Listening

    Late August by Paul Lansky is one in a series of his pieces that explores the conversations of everyday life. In this composition, Paul Lansky recorded a conversation between two Chinese students sometime in late August, 1989. Their processed speech, in conjunction with pitch material motivated by set theory, became the foundation for this composition. [Lansky, 1990], [Simoni, 1999]