Graphs are often used to describe behaviors including music. In mathematics and computer science, a graph is a set of nodes and a set of edges. We will consider directed graphs, which are graphs with directed edges.
The graph in Figure 13.3.1 has three nodes – C4, E4, and DS4. Each node represents a symbolic note name. The arrows connecting the nodes specify the direction in which the graph may be traversed. For example, from node E4, we can go to C4 or DS4. From node C4, we can only go to E4.
Figure 13.3.1: A directed graph
One way to model this graph is to represent each node with a pattern that selects the next node. Consider the program in Example
C4
E4 DS4
13.3 Graphs and Patterns 169
13.3.1. The variables c4-pattern, ds4-pattern, and e4-pattern
represent nodes and contain pattern generators that generate the next node to visit. For example, the ds4-pattern will alternately generate
c4-pattern and e4-pattern, corresponding to the edges from ds4 to
c4 and e4. The variable current-node keeps track of the current lo-
cation on the graph, and the function get-next-node traverses an
edge as follows: (1) the value of current-node is a symbol. The eval
function is applied to get the value of the symbol. (2) next is applied
to the pattern to get the next item. (3) The item is stored in current- node. Finally, a table is used to translate the value of current-node
(a symbol) into a pitch number.
The use of eval deserves further comment. We represent nodes with patterns that choose the next node. Thus, each pattern should return another pattern. However, when a pattern is an item of another pattern, the next function treats these as nested patterns, and it tries
to generate a period of items from the nested pattern. (See Chapter 6 for examples and further explanation.) To avoid this in Example 13.3.1, patterns return symbols. Each symbol is the name of a global variable containing the associated pattern. eval is used to look up the variable’s value. For example, current-node is initialized to the symbol c4-pattern. In line 8 (counting the blank line), eval is applied to the value of current-node, which is c4-pattern. The
value of c4-pattern in turn is the cycle pattern created and assigned in the first line. This pattern object is assigned to the variable
pattern and used to select the next value of current-node. Another
way to implement this graph traversal is described in Section 13.4.
Example 13.3.1: get-next-node.sal
set c4-pattern = make-cycle({e4-pattern})
set ds4-pattern = make-cycle({c4-pattern e4-pattern}) set e4-pattern = make-cycle({c4-pattern ds4-pattern}) set current-node = quote(c4-pattern)
define function get-next-node()
begin ;; note: current-node is a symbol with pattern = eval(current-node) set current-node = next(pattern) return second(assoc(current-node, {{c4-pattern 60} {ds4-pattern 63} {e4-pattern 64}})) end
Example 13.3.2 calls get-next-node in a loop and prints a se- quence of pitches that are generated. Note that the transitions from
E4 (64) alternate going to C4 (60) and DS4 (63), and DS4 (63) goes alternately to C4 (60) and E4 (64).
Example 13.3.3 incorporates get-next-node from Example
13.3.1 into a score generator. Listen to the output of this program. It is difficult to follow all the workings of the program, but you can easily hear, for example that the low note (C4) always makes a tran- sition to the high note (E4), as shown in the graph of Figure 15.3.1.
Example 13.3.2 : Calling get-next-node
SAL> loop
repeat 10
exec format(#t, "~A ", get-next-node()) end
64 60 64 63 60 64 60 64 63 64
Example 13.3.3: graph.sal
begin
with dur-pattern = make-random({0.2 0.4 0.6}), vel-pattern = make-cycle({60 75 90 105}) exec score-gen(save: quote(graph-score), score-len: 30,
pitch: get-next-node(), ioi: next(dur-pattern), vel: next(vel-pattern)) end
13.4 The markov Pattern Generator
A Markov process is a probability system where the likelihood that an event will be selected is based on one or more past events. A first- order Markov process is one where the next state depends only on
the current state. Note how a first-order Markov process can be rep- resented as a directed graph. A node in the graph represents an event, and the probability of making a transition to another node (event) is represented by edges labeled with probabilities. Typically, only edges with non-zero probabilities are included in the graph.
An alternative to this graphical representation is a transition table that shows the probability that a certain event will be selected based on one or more past events. A first-order transition table describes the probability that an event will be selected given one past event, and a second-order transition table describes the probability that an event will be selected given two past events. (Higher orders are also possible.) The succession from one event to the other is called a
13.4 The markov Pattern Generator 171
Table 13.4.1: A first-order transition table C4 D4 E4 C4 0.10 0.75 0.15 D4 0.25 0.10 0.65 E4 0.50 0.30 0.20
Table 13.4.1 is an example of a first-order transition table. The current events are listed in the zeroth column and the possible next
events are listed in the zeroth row. We interpret the first row of the
transition table as “if the current event is a C4, there is a 10% chance that another C4 will be selected, a 75% chance that D4 will be se- lected, and a 15% chance that E4 will be selected.” Figure 13.4.1 shows a graph that is equivalent to this table. Notice that some edges lead from a node back to the same node.
Figure 13.4.1: A first-order Markov process as a graph
Two frequent errors in constructing transition tables are loops and dead ends. The transition table in Table 13.4.2 will quickly fall into a loop that generates a chain of alternating D4s and E4s.
Table 13.4.2: A first-order transition table that loops C4 D4 E4
C4 — .5 .5
D4 — — 1.0
A dead end occurs in a transition table when an event is specified and there is no way to select another event from that event. The tran- sition table shown as Table 13.4.3 states that “if the current note is a C4, there is a 50% chance that D4 will be selected, a 25% chance that E4 will be selected, and a 25% chance that F4 is selected.” If F4 is selected, it has no next event since there is no row in the transition table that considers F4 as a current event. The Markov chain reaches a dead end.
Table 13.4.3: A first-order transition table that dead-ends C4 D4 E4 F4
C4 — .5 .25 .25
D4 — — 1.0 —
E4 — 1.0 — —
The function make-markov may be used to generate a pattern that constructs a Markov chain. To describe a Markov process using
make-markov, each row of the transition table is specified by a list
like this:
{current -> {next1weight1} {next2weight2} … {nextnweightn}}
where states are symbols. current is the current state and next1
through nextn are the possible next states, which are listed with their
associated transition weights. (Weights are relative and do not neces- sarily sum to 1.) The first input to make-markov is a list of transi-
tion rules.
Example 13.4.1 uses make-markov to produce a sequence of
pitches using the first-order transition table described in Table 13.4.1. Notice that two keyword parameters are passed to make-
markov in addition to the transition rules. The past: keyword gives
the starting state. If this were a second-order Markov process, the list would contain two states: the previous one and the current one. The
produces: keyword is used to convert the current state into a value.
In this example, the value keyword(eval) says to evaluate the state
name as a global variable to get the next value of the pattern genera- tor. Since our state names are C4, D4, E4, and these happen to be Lisp variables that evaluate to 60, 62, and 64, keyword(eval) offers a
convenient way to return pitch values rather than state names.
When markov.sal is evaluated, the note series shown in Figure 13.4.2 is generated. This series will be different each time the program is run.