Previous Section | Next Section | Table of Contents | Index | Title Page

Xmusic and Algorithmic Composition

Several Nyquist libraries offer support for algorithmic composition. Xmusic is a library for generating sequences and patterns of data. Included in Xmusic is the score-gen macro which helps to generate scores from patterns. Another important facility is the distributions.lsp library, containing many different random number generators.

Xmusic Basics

Xmusic is inspired by and based on Common Music by Rick Taube. Currently, Xmusic only implements patterns and some simple support for scores to be realized as sound by Nyquist. In contrast, Common Music supports MIDI and various other synthesis languages and includes a graphical interface, some visualization tools, and many other features. Common Music runs in Common Lisp and Scheme, but not XLISP, which is the base language for Nyquist.

Xmusic patterns are objects that generate data streams. For example, the cycle-class of objects generate cyclical patterns such as "1 2 3 1 2 3 1 2 3 ...", or "1 2 3 4 3 2 1 2 3 4 ...". Patterns can be used to specify pitch sequences, rhythm, loudness, and other parameters.

To use any of the Xmusic functions, you must manually load xm.lsp, that is, type (load "xm") to Nyquist. To use a pattern object, you first create the pattern, e.g.

(setf pitch-source (make-cycle (list c4 d4 e4 f4)))
After creating the pattern, you can access it repeatedly with next to generate data, e.g.
(play (seqrep (i 13) (pluck (next pitch-source) 0.2)))
This will create a sequence of notes with the following pitches: c, d, e, f, c, d, e, f, c, d, e, f, c. If you evaluate this again, the pitch sequence will continue, starting on "d".

It is very important not to confuse the creation of a sequence with its access. Consider this example:

(play (seqrep (i 13) 
       (pluck (next (make-cycle (list c4 d4 e4 f4))) 0.2)))
This looks very much like the previous example, but it only repeats notes on middle-C. The reason is that every time pluck is evaluated, make-cycle is called and creates a new pattern object. After the first item of the pattern is extracted with next, the cycle is not used again, and no other items are generated.

To summarize this important point, there are two steps to using a pattern. First, the pattern is created and stored in a variable using setf. Second, the pattern is accessed (multiple times) using next.

Patterns can be nested, that is, you can write patterns of patterns. In general, the next function does not return patterns. Instead, if the next item in a pattern is a (nested) pattern, next recursively gets the next item of the nested pattern.

While you might expect that each call to next would advance the top-level pattern to the next item, and descend recursively if necessary to the inner-most nesting level, this is not how next works. Instead, next remembers the last top-level item, and if it was a pattern, next continues to generate items from that same inner pattern until the end of the inner pattern's period is reached. The next paragraph explains the concept of the period.

The data returned by a pattern object is structured into logical groups called periods. You can get an entire period (as a list) by calling (next pattern t). For example:

(setf pitch-source (make-cycle (list c4 d4 e4 f4)))
(next pitch-source t)
This prints the list (60 62 64 65), which is one period of the cycle.

You can also get explicit markers that delineate periods by calling (send pattern :next). In this case, the value returned is either the next item of the pattern, or the symbol +eop+ if the end of a period has been reached. What determines a period? This is up to the specific pattern class, so see the documentation for specifics. You can override the "natural" period using the keyword :for, e.g.

(setf pitch-source (make-cycle (list c4 d4 e4 f4) :for 3))
(next pitch-source t)
(next pitch-source t)
This prints the lists (60 62 64) (65 60 62). Notice that these periods just restructure the stream of items into groups of 3.

Nested patterns are probably easier to understand by example than by specification. Here is a simple nested pattern of cycles:

(setf cycle-1 (make-cycle '(a b c)))
(setf cycle-2 (make-cycle '(x y z)))
(setf cycle-3 (make-cycle (list cycle-1 cycle-2)))
(dotimes (i 9) (format t "~A " (next cycle-3)))
This will print "A B C X Y Z A B C". Notice that the inner-most cycles cycle-1 and cycle-2 generate a period of items before the top-level cycle-3 advances to the next pattern.

Before describing specific pattern classes, there are several optional parameters that apply in the creating of any pattern object. These are:

:for
The length of a period. This overrides the default by providing a numerical length. The value of this optional parameter may be a pattern that generates a sequence of integers that determine the length of each successive period. A period length may not be negative, but it may be zero.

:name
A pattern object may be given a name. This is useful if the :trace option is used.

:trace
If non-null, this optional parameter causes information about the pattern to be printed each time an item is generated from the pattern.

The built-in pattern classes are described in the following section.

Pattern Classes

cycle

The cycle-class iterates repeatedly through a list of items. For example, two periods of (make-cycle '(a b c)) would be (A B C) (A B C).

(make-cycle items [:for for] [:name name] [:trace trace])
Make a cycle pattern that iterates over items. The default period length is the length of items. (See above for a description of the optional parameters.) If items is a pattern, a period of the pattern becomes the list from which items are generated. The list is replaced every period of the cycle.

line

The line-class is similar to the cycle class, but when it reaches the end of the list of items, it simply repeats the last item in the list. For example, two periods of (make-line '(a b c)) would be (A B C) (C C C).

(make-line items [:for for] [:name name] [:trace trace])
Make a line pattern that iterates over items. The default period length is the length of items. As with make-cycle, items may be a pattern. (See above for a description of the optional parameters.)

random

The random-class generates items at random from a list. The default selection is uniform random with replacement, but items may be further specified with a weight, a minimum repetition count, and a maximum repetition count. Weights give the relative probability of the selection of the item (with a default weight of one). The minimum count specifies how many times an item, once selected at random, will be repeated. The maximum count specifies the maximum number of times an item can be selected in a row. If an item has been generated n times in succession, and the maximum is equal to n, then the item is disqualified in the next random selection. Weights (but not currently minima and maxima) can be patterns. The patterns (thus the weights) are recomputed every period.

(make-random items [:for for] [:name name] [:trace trace])
Make a random pattern that selects from items. Any (or all) element(s) of items may be lists of the following form: (value [:weight weight] [:min mincount] [:max maxcount], where value is the item (or pattern) to be generated, weight is the relative probability of selecting this item, mincount is the minimum number of repetitions when this item is selected, and maxcount is the maximum number of repetitions allowed before selecting some other item. The default period length is the length of items. If items is a pattern, a period from that pattern becomes the list from which random selections are made, and a new list is generated every period.

palindrome

The palindrome-class repeatedly traverses a list forwards and then backwards. For example, two periods of (make-palindrome '(a b c)) would be (A B C C B A) (A B C C B A). The :elide keyword parameter controls whether the first and/or last elements are repeated:
(make-palindrome '(a b c) :elide nil) 
     ;; generates A B C C B A A B C C B A ...

(make-palindrome '(a b c) :elide t) ;; generates A B C B A B C B ...

(make-palindrome '(a b c) :elide :first) ;; generates A B C C B A B C C B ...

(make-palindrome '(a b c) :elide :last) ;; generates A B C B A A B C B A ...

(make-palindrome items [:elide elide] [:for for] [:name name] [:trace trace])
Generate items from list alternating in-order and reverse-order sequencing. The keyword parameter elide can have the values :first, :last, t, or nil to control repetition of the first and last elements. The elide parameter can also be a pattern, in which case it is evaluated every period. One period is one complete forward and backward traversal of the list. If items is a pattern, a period from that pattern becomes the list from which random selections are made, and a new list is generated every period.

heap

The heap-class selects items in random order from a list without replacement, which means that all items are generated once before any item is repeated. For example, two periods of (make-heap '(a b c)) might be (C A B) (B A C).

(make-heap items [:for for] [:name name] [:trace trace])
Generate items randomly from list without replacement. The period length is the length of items. If items is a pattern, a period from that pattern becomes the list from which random selections are made, and a new list is generated every period.

copier

The copier-class makes copies of periods from a sub-pattern. For example, three periods of (make-copier (make-cycle '(a b c) :for 1) :repeat 2 :merge t) would be (A A) (B B) (C C). Note that entire periods (not individual items) are repeated, so in this example the :for keyword was used to force periods to be of length one so that each item is repeated by the :repeat count.

(make-copier sub-pattern [:repeat repeat] [:merge merge] [:for for] [:name name] [:trace trace])
Generate a period from sub-pattern and repeat it repeat times. If merge is false (the default), each repetition of a period from sub-pattern results in a period by default. If merge is true (non-null), then all repeat repetitions of the period are merged into one result period by default. If the :for keyword is used, the same items are generated, but the items are grouped into periods determined by the :for parameter. If the :for parameter is a pattern, it is evaluated every result period. The repeat and merge values may be patterns that return a repeat count and a boolean value, respectively. If so, these patterns are evaluated initially and after each repeat copies are made (independent of the :for keyword parameter, if any). The repeat value returned by a pattern can also be negative. A negative number indicates how many periods of sub-pattern to skip. After skipping these patterns, new repeat and merge values are generated.

accumulate

The accumulate-class forms the sum of numbers returned by another pattern. For example, each period of (make-accumulate (make-cycle '(1 2 -3))) is (1 3 0). The default output period length is the length of the input period.

(make-accumulate sub-pattern [:for for] [:max maximum] [:min minimum] [:name name] [:trace trace])
Keep a running sum of numbers generated by sub-pattern. The default period lengths match the period lengths from sub-pattern. If maximum (a pattern or a number) is specified, and the running sum exceeds maximum, the running sum is reset to maximum. If minimum (a pattern or a number) is specified, and the running sum falls below minimum, the running sum is reset to minimum. If minimum is greater than maximum, the running sum will be set to one of the two values.

sum

The sum-class forms the sum of numbers, one from each of two other patterns. For example, each period of (make-sum (make-cycle '(1 2 3)) (make-cycle '(4 5 6))) is (5 7 9). The default output period length is the length of the input period of the first argument. Therefore, the first argument must be a pattern, but the second argument can be a pattern or a number.

(make-sum x y [:for for] [:name name] [:trace trace])
Form sums of items (which must be numbers) from pattern x and pattern or number y. The default period lengths match the period lengths from x.

product

The product-class forms the product of numbers, one from each of two other patterns. For example, each period of (make-product (make-cycle '(1 2 3)) (make-cycle '(4 5 6))) is (4 10 18). The default output period length is the length of the input period of the first argument. Therefore, the first argument must be a pattern, but the second argument can be a pattern or a number.

(make-product x y [:for for] [:name name] [:trace trace])
Form products of items (which must be numbers) from pattern x and pattern or number y. The default period lengths match the period lengths from x.

eval

The eval-class evaluates an expression to produce each output item. The default output period length is 1.

(make-eval expr [:for for] [:name name] [:trace trace])
Evaluate expr to generate each item. If expr is a pattern, each item is generated by getting the next item from expr and evaluating it.

length

The length-class generates periods of a specified length from another pattern. This is similar to using the :for keyword, but for many patterns, the :for parameter alters the points at which other patterns are generated. For example, if the palindrome pattern has an :elide pattern parameter, the value will be computed every period. If there is also a :for parameter with a value of 2, then :elide will be recomputed every 2 items. In contrast, if the palindrome (without a :for parameter) is embedded in a length pattern with a lenght of 2, then the periods will all be of length 2, but the items will come from default periods of the palindrome, and therefore the :elide values will be recomputed at the beginnings of default palindrome periods.

(make-length pattern length-pattern [:name name] [:trace trace])
Make a pattern of class length-class that regroups items generated by a pattern according to pattern lengths given by length-pattern. Note that length-pattern is not optional: There is no default pattern length and no :for keyword.

window

The window-class groups items from another pattern by using a sliding window. If the skip value is 1, each output period is formed by dropping the first item of the previous perioda and appending the next item from the pattern. The skip value and the output period length can change every period. For a simple example, if the period length is 3 and the skip value is 1, and the input pattern generates the sequence A, B, C, ..., then the output periods will be (A B C), (B C D), (C D E), (D E F), ....

(make-window pattern length-pattern skip-pattern [:name name] [:trace trace])
Make a pattern of class window-class that regroups items generated by a pattern according to pattern lengths given by length-pattern and where the period advances by the number of items given by skip-pattern. Note that length-pattern is not optional: There is no default pattern length and no :for keyword.

markov

The markov-class generates items from a Markov model. A Markov model generates a sequence of states according to rules which specify possible future states given the most recent states in the past. For example, states might be pitches, and each pitch might lead to a choice of pitches for the next state. In the markov-class, states can be either symbols or numbers, but not arbitrary values or patterns. This makes it easier to specify rules. However, symbols can be mapped to arbitrary values including pattern objects, and these become the actual generated items. By default, all future states are weighted equally, but weights may be associated with future states. A Markov model must be initialized with a sequence of past states using the :past keyword. The most common form of Markov model is a "first order Markov model" in which the future item depends only upon one past item. However, higher order models where the future items depend on two or more past items are possible. A "zero-order" Markov model, which depends on no past states, is essentially equivalent to the random pattern. As an example of a first-order Markov pattern, two periods of (make-markov '((a -> b c) (b -> c) (c -> a)) :past '(a)) might be (C A C) (A B C).

(make-markov rules [:past past] [:produces produces] [:for for] [:name name] [:trace trace])
Generate a sequence of items from a Markov process. The rules parameter has the form: (prev1 prev2 ... prevn -> next1 next2 ... nextn) where prev1 through prevn represent a sequence of most recent (past) states. The symbol * is treated specially: it matches any previous state. If prev1 through prevn (which may be just one state as in the example above) match the previously generated states, this rule applies. Note that every rule must specify the same number of previous states; this number is known as the order of the Markov model. The first rule in rules that applies is used to select the next state. If no rule applies, the next state is NIL (which is a valid state that can be used in rules). Assuming a rule applies, the list of possible next states is specified by next1 through nextn. Notice that these are alternative choices for the next state, not a sequence of future states, and each rule can have any number of choices. Each choice may be the state itself (a symbol or a number), or the choice may be a list consisting of the state and a weight. The weight may be given by a pattern, in which case the next item of the pattern is obtained every time the rule is applied. For example, this rules says that if the previous states were A and B, the next state can be A with a weight of 0.5 or C with an implied weight of 1: (A B -> (A 0.5) C). The default length of the period is the length of rules. The past parameter must be provided. It is a list of states whose length matches the order of the Markov model. The keyword parameter produces may be used to map from state symbols or numbers to other values or patterns. The parameter is a list of alternating symbols and values. For example, to map A to 69 and B to 71, use (list 'a 69 'b 71). You can also map symbols to patterns, for example (list 'a (make-cycle '(57 69)) 'b (make-random '(59 71))). The next item of the pattern is is generated each time the Markov model generates the corresponding state. Finally, the produces keyword can be :eval, which means to evaluate the Markov model state. This could be useful if states are Nyquist global variables such as C4, CS4, D4, ]..., which evaluate to numerical values (60, 61, 62, ....

(markov-create-rules sequence order [generalize])
Generate a set of rules suitable for the make-markov function. The sequence is a "typical" sequence of states, and order is the order of the Markov model. It is often the case that a sample sequence will not have a transition from the last state to any other state, so the generated Markov model can reach a "dead end" where no rule applies. This might lead to an infinite stream of NIL's. To avoid this, the optional parameter generalize can be set to t (true), indicating that there should be a fallback rule that matches any previous states and whose future states are weighted according to their frequency in sequence. For example, if sequence contains 5 A's, 5 B's and 10 G's, the default rule will be (* -> (A 5) (B 5) (G 10)). This rule will be appended to the end so it will only apply if no other rule does.

Random Number Generators

The distributions.lsp library implements random number generators that return random values with various probability distributions. Without this library, you can generate random numbers with uniform distributions. In a uniform distribution, all values are equally likely. To generate a random integer in some range, use random. To generate a real number (FLONUM) in some range, use real-random (or rrandom if the range is 0-1). But there are other interesting distributions. For example, the Gaussian distribution is often used to model real-world errors and fluctuations where values are clustered around some central value and large deviations are more unlikely than small ones. See Dennis Lorrain, "A Panoply of Stochastic 'Canons'," Computer Music Journal vol. 4, no. 1, 1980, pp. 53-81.

In most of the random number generators described below, there are optional parameters to indicate a maximum and/or minimum value. These can be used to truncate the distribution. For example, if you basically want a Gaussian distribution, but you never want a value greater than 5, you can specify 5 as the maximum value. The upper and lower bounds are implemented simply by drawing a random number from the full distribution repeatedly until a number falling into the desired range is obtained. Therefore, if you select an acceptable range that is unlikely, it may take Nyquist a long time to find each acceptable random number. The intended use of the upper and lower bounds is to weed out values that are already fairly unlikely.

(linear-dist g)
Return a FLONUM value from a linear distribution, where the probability of a value decreases linearly from zero to g which must be greater than zero. (See Figure 7.) The linear distribution is useful for generating for generating time and pitch intervals.




Figure 7: The Linear Distribution, g = 1.


(exponential-dist delta [high])
Return a FLONUM value from an exponential distribution. The initial downward slope is steeper with larger values of delta, which must be greater than zero. (See Figure 8. The optional high parameter puts an artificial upper bound on the return value. The exponential distribution generates values greater than 0, and can be used to generate time intervals. Natural random intervals such as the time intervals between the release of atomic particles or the passing of yellow volkswagons in traffic have exponential distributions. The exponential distribution is memory-less: knowing that a random number from this distribution is greater than some value (e.g. a note duration is at least 1 second) tells you nothing new about how soon the note will end. This is a continuous distribution, but geometric-dist (described below) implements the discrete form.




Figure 8: The Exponential Distribution, delta = 1.


(gamma-dist nu [high])
Return a FLONUM value from a Gamma distribution. The value is greater than zero, has a mean of nu (a FIXNUM greater than zero), and a mode (peak) of around nu - 1. The optional high parameter puts an artificial upper bound on the return value.




Figure 9: The Gamma Distribution, nu = 4.


(bilateral-exponential-dist xmu tau [low] [high])
Returns a FLONUM value from a bilateral exponential distribution, where xmu is the center of the double exponential and tau controls the spread of the distribution. A larger tau gives a wider distribution (greater variance), and tau must be greater than zero. The low and high parameters give optional artificial bounds on the minimum and maximum output values, respectively. This distribution is similar to the exponential, except it is centered at 0 and can output negative values as well. Like the exponential, it can be used to generate time intervals; however, it might be necessary to add a lower bound so as not to compute a negative time interval.




Figure 10: The Bilateral Exponential Distribution.


(cauchy-dist tau [low] [high])
Returns a FLONUM from the Cauchy distribution, a symetric distribution with a high peak at zero and a width (variance) that increases with parameter tau, which must be greater than zero. The low and high parameters give optional artificial bounds on the minimum and maximum output values, respectively.




Figure 11: The Cauchy Distribution, tau = 1.


(hyperbolic-cosine-dist [low] [high])
Returns a FLONUM value from the hyperbolic cosine distribution, a symetric distribution with its peak at zero. The low and high parameters give optional artificial bounds on the minimum and maximum output values, respectively.




Figure 12: The Hyperbolic Cosine Distribution.


(logistic-dist alpha beta [low] [high])
Returns a FLONUM value from the logistic distribution, which is symetric about the mean. The alpha parameter primarily affects dispersion (variance), with larger values resulting in values closer to the mean (less variance), and the beta parameter primarily influences the mean. The low and high parameters give optional artificial bounds on the minimum and maximum output values, respectively.




Figure 13: The Logistic Distribution, alpha = 1, beta = 2.


(arc-sine-dist)
Returns a FLONUM value from the arc sine distribution, which outputs values between 0 and 1. It is symetric about the mean of 1/2, but is more likely to generate values closer to 0 and 1.




Figure 14: The Arc Sine Distribution.


(gaussian-dist xmu sigma [low] [high])
Returns a FLONUM value from the Gaussian or Gauss-Laplace distribution, a linear function of the normal distribution. It is symetric about the mean of xmu, with a standard deviation of sigma, which must be greater than zero. The low and high parameters give optional artificial bounds on the minimum and maximum output values, respectively.




Figure 15: The Gauss-Laplace (Gaussian) Distribution, xmu = 0, sigma = 1.


(beta-dist a b)
Returns a FLONUM value from the Beta distribution. This distribution outputs values between 0 and 1, with outputs more likely to be close to 0 or 1. The parameter a controls the height (probability) of the right side of the distribution (at 1) and b controls the height of the left side (at 0). The distribution is symetric about 1/2 when a = b.




Figure 16: The Beta Distribution, alpha = .5, beta = .25.


(bernoulli-dist px1 [x1] [x2])
Returns either x1 (default value is 1) with probability px1 or x2 (default value is 0) with probability 1 - px1. The value of px1 should be between 0 and 1. By convention, a result of x1 is viewed as a success while x2 is viewed as a failure.




Figure 17: The Bernoulli Distribution, px1 = .75.


(binomial-dist n p
Returns a FIXNUM value from the binomial distribution, where n is the number of Bernoulli trials run (a FIXNUM) and p is the probability of success in the Bernoulli trial (a FLONUM from 0 to 1). The mean is the product of n and p.




Figure 18: The Binomial Distribution, n = 5, p = .5.


(geometric-dist p
Returns a FIXNUM value from the geometric distribution, which is defined as the number of failures before a success is achieved in a Bernoulli trial with probability of success p (a FLONUM from 0 to 1).




Figure 19: The Geometric Distribution, p = .4.


(poisson-dist delta)
Returns a FIXNUM value from the Poisson distribution with a mean of delta (a FIXNUM). The Poisson distribution is often used to generate a sequence of time intervals, resulting in random but often pleasing rhythms.




Figure 20: The Poisson Distribution, delta = 3.


Score Generation and Manipulation

A common application of pattern generators is to specify parameters for notes. (It should be understood that "notes" in this context means any Nyquist behavior, whether it represents a conventional note, an abstract sound object, or even some micro-sound event that is just a low-level component of a hierarchical sound organization. Similarly, "score" should be taken to mean a specification for a sequence of these "notes.") The score-gen macro (defined by loading xm.lsp) establishes a convention for representing scores and for generating them using patterns.

The timed-seq macro, described in Section "Combination and Time Structure", already provides a way to represent a "score" as a list of expressions. The Xmusic representation goes a bit further by specifying that all notes are specified by an alternation of keywords and values, where some keywords have specific meanings and interpretations.

The basic idea of score-gen is you provide a template for notes in a score as a set of keywords and values. For example,

(setf pitch-pattern (make-cycle (list c4 d4 e4 f4)))
(score-gen :dur 0.4 :name 'my-sound 
         :pitch (next pitch-pattern) :score-len 9)
generates a score of 9 notes as follows:
((0 0 (SCORE-BEGIN-END 0 3.6))
 (0 0.4 (MY-SOUND :PITCH 60))
 (0.4 0.4 (MY-SOUND :PITCH 62))
 (0.8 0.4 (MY-SOUND :PITCH 64))
 (1.2 0.4 (MY-SOUND :PITCH 65))
 (1.6 0.4 (MY-SOUND :PITCH 60))
 (2 0.4 (MY-SOUND :PITCH 62))
 (2.4 0.4 (MY-SOUND :PITCH 64))
 (2.8 0.4 (MY-SOUND :PITCH 65))
 (3.2 0.4 (MY-SOUND :PITCH 60)))
The use of keywords like :PITCH helps to make scores readable and easy to process without specific knowledge of about the functions called in the score. For example, one could write a transpose operation to transform all the :pitch parameters in a score without having to know that pitch is the first parameter of pluck and the second parameter of piano-note. Keyword parameters are also used to give flexibility to note specification with score-gen. Since this approach requires the use of keywords, the next section is a brief explanation of how to define functions that use keyword parameters.

Keyword Parameters

Keyword parameters are parameters whose presence is indicated by a special symbol, called a keyword, followed by the actual parameter. Keyword parameters may have default values that are used if no actual parameter is provided by the caller of the function.

To specify that a parameter is a keyword parameter, use &key to specify that the following parameters are keyword parameters. For example, here is a function that accepts keyword parameters and invokes the pluck function:

(defun k-pluck (&key pitch dur)
  (pluck pitch dur))
Now, we can call k-pluck with keyword parameters. The keywords are simply the formal parameter names with a prepended colon character (:pitch and :dur in this example), so a function call would look like:
(pluck :key c3 :dur 3)
Usually, it is best to give keyword parameters useful default values. That way, if a parameter such as :dur is missing, a reasonable default value (1) can be used automatically. If no default value is given, the NIL will be used. It is never an error to omit a keyword parameter, but the called function can check to see if a keyword parameter was supplied or not. Default values are specified by placing the parameter and the default value in parentheses:
(defun k-pluck (&key (pitch 60) (dur 1))
  (pluck pitch dur))
Now, we can call (k-pluck :pitch c3) with no duration, (k-pluck :dur 3) with only a duration, or even (k-pluck) with no parameters.

There is additional syntax to specify an alternate symbol to be used as the keyword and to allow the called function to determine whether or not a keyword parameter was supplied, but these features are little-used. See the XLISP manual for details.

Using score-gen

The score-gen macro computes a score based on keyword parameters. Some keywords have a special meaning, while others are not interpreted but merely placed in the score. The resulting score can be synthesized using timed-seq (see Section "Combination and Time Structure").

The form of a call to score-gen is simply (score-gen :k1 e1 :k2 e2 ... ), where the k's are keywords and the e's are expressions. A score is generated by evaluating the expressions once for each note and constructing a list of keyword-value pairs. A number of keywords have special interpretations. The rules for interpreting these parameters will be explained through a set of "How do I ..." questions:

How many notes will be generated? The keyword parameter :score-len specifies an upper bound on the number of notes. The keyword :score-dur specifies an upper bound on the starting time of the last note in the score. (To be more precise, the :score-dur bound is reached when the default starting time of the next note is greater than or equal to the :score-dur value. This definition is necessary because note times are not strictly increasing.) When either bound is reached, score generation ends. At least one of these two parameters must be specified or an error is raised. These keyword parameters are evaluated just once and are not copied into the parameter lists of generated notes.

What is the duration of generated notes? The keyword :dur defaults to 1 and specifies the nominal duration in seconds. Since the generated note list is compatible with timed-seq, the starting time and duration (to be precise, the stretch factor) are not passed as parameters to the notes. Instead, they control the Nyquist environment in which the note will be evaluated.

What is the start time of a note? The default start time of the first note is zero. Given a note, the default start time of the next note is the start time plus the inter-onset time, which is given by the :ioi parameter. If no :ioi parameter is specified, the inter-onset time defaults to the duration, given by :dur. In all cases, the default start time of a note can be overridden by the keyword parameter :time.

When does the score begin and end? The behavior SCORE-BEGIN-END contains the beginning and ending of the score (these are used for score manipulations, e.g. when scores are merged, their begin times can be aligned.) When timed-seq is used to synthesize a score, the SCORE-BEGIN-END marker is not evaluated. The score-gen macro inserts a "note" of the form (0 0 (SCORE-BEGIN-END begin-time end-time)) at the time given by the :begin keyword, with begin-time and end-time determined by the :begin and :end keyword parameters, respectively. If the :begin keyword is not provided, the score begins at zero. If the :end keyword is not provided, the score ends at the default start time of what would be the next note after the last note in the score (as described in the previous paragraph). Note: if :time is used to compute note starting times, and these times are not increasing, it is strongly advised to use :end to specify an end time for the score, because the default end time may be anywhere in the middle of the generated sequence.

What function is called to synthesize the note? The :name parameter names the function. Like other parameters, the value can be any expression, including something like (next fn-name-pattern), allowing function names to be recomputed for each note. The default value is note.

Can I make parameters depend upon the starting time or the duration of the note? Parameter expressions can use the variable sg:time to access the start time of the note, sg:ioi to access the inter-onset time, and sg:dur to access the duration (stretch factor) of the note. Also, sg:count counts how many notes have been computed so far, starting at 0. The order of computation is: sg:time first, then sg:ioi and sg:dur, so for example, an expression to compute sg:dur can depend on sg:ioi.

Can parameters depend on each other? The keyword :pre introduces an expression that is evaluated before each note, and :post provides an expression to be evaluated after each note. The :pre expression can assign one or more global variables which are then used in one or more expressions for parameters.

How do I debug score-gen expressions? You can set the :trace parameter to true (t) to enable a print statement for each generated note.

How can I save scores generated by score-gen that I like? If the keyword parameter :save is set to a symbol, the global variable named by the symbol is set to the value of the generated sequence. Of course, the value returned by score-gen is just an ordinary list that can be saved like any other value.

In summary, the following keywords have special interpretations in score-gen: :begin, :end, :time, :dur, :name, :ioi, :trace, :save, :score-len, :score-dur, :pre, :post. All other keyword parameters are expressions that are evaluated once for each note and become the parameters of the notes.

Score Manipulation

Nyquist encourages the representation of music as executable programs, or behaviors, and there are various ways to modify behaviors, including time stretching, transposition, etc. An alternative to composing executable programs is to manipulate scores as editable data. Each approach has its strengths and weaknesses. This section describes functions intended to manipulate Xmusic scores as generated by, or at least in the form generated by, score-gen. Recall that this means scores are lists of events (e.g. notes), where events are three-element lists of the form (time duration expression, and where expression is a standard lisp function call where all parameters are keyword parameters. In addition, the first "note" may be the special SCORE-BEGIN-END expression. If this is missing, the score begins at zero and ends at the end of the last note.

For convenience, a set of functions is offered to access properties of events (or notes) in scores. Although lisp functions such as car, cadr, and caddr can be used, code is more readable when more mnemonic functions are used to access events.

(event-time event)
Retrieve the time field from an event.

(event-set-time event time)
Construct a new event where the time of event is replaced by time.

(event-dur event)
Retrieve the duration (i.e. the stretch factor) field from an event.

(event-set-dur event dur)
Construct a new event where the duration (or stretch factor) of event is replaced by dur.

(event-expression event)
Retrieve the expression field from an event.

(event-set-expression event dur)
Construct a new event where the expression of event is replaced by expression.

(event-end event)
Retrieve the end time of event, its time plus its duration.

(expr-has-attr expression attribute)
Test whether a score event expression has the given attribute.

(expr-get-attr expression attribute [default])
Get the value of the given attribute from a score event expression. If attribute is not present, return default if specified, and otherwise nil.

(expr-set-attr expr attribute value)
Construct a new expression identical to expr except that the attribute has value.

(event-has-attr event attribute)
Test whether a given score event's expression has the given attribute.

(event-get-attr event attribute [default])
Get the value of the given attribute from a score event's expression. If attribute is not present, return default if specified, and otherwise nil.

(event-set-attr event attribute value)
Construct a new event identical to event except that the attribute has value.

Functions are provided to shift the starting times of notes, stretch times and durations, stretch only durations, add an offset to a keyword parameter, scale a keyword parameter, and other manipulations. Functions are also provided to extract ranges of notes, notes that match criteria, and to combine scores. Most of these functions (listed below in detail) share a set of keyword parameters that optionally limit the range over which the transformation operates. The :from-index and :to-index parameters specify the index of the first note and the index of the last note to be changed. If these numbers are negative, they are offsets from the end of the score, e.g. -1 denotes the last note of the score. The :from-time and :to-time indicate a range of starting times of notes that will be affected by the manipulation. Only notes whose time is greater than or equal to the from-time and strictly less than the to-time are modified. If both index and time ranges are specified, only notes that satisfy both constraints are selected.

(score-sorted score)
Test if score is sorted.

(score-sort score [copy-flag])
Sort the notes in a score into start-time order. If copy-flag is nil, this is a destructive operation which should only be performed if the top-level score list is a fresh copy that is not shared by any other variables. (The copy-flag is intended for internal system use only.) For the following operations, it is assumed that scores are sorted, and all operations return a sorted score.

(score-shift score offset [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Add a constant offset to the starting time of a set of notes in score. By default, all notes are modified, but the range of notes can be limited with the keyword parameters. The begin time of the score is not changed, but the end time is increased by offset. The original score is not modified, and a new score is returned.

(score-stretch score factor [:dur dur-flag] [:time time-flag] [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Stretch note times and durations by factor. The default dur-flag is non-null, but if dur-flag is null, the original durations are retained and only times are stretched. Similarly, the default time-flag is non-null, but if time-flag is null, the original times are retained and only durations are stretched. If both dur-flag and time-flag are null, the score is not changed. If a range of notes is specified, times are scaled within that range, and notes after the range are shifted so that the stretched region does not create a "hole" or overlap with notes that follow. If the range begins or ends with a time (via :from-time and :to-time), time stretching takes place over the indicated time interval independent of whether any notes are present or where they start. In other words, the "rests" are stretched along with the notes. The original score is not modified, and a new score is returned.

(score-transpose score keyword amount [:from-index i] [:to-index j] [:from-time x] [:to-time y])
For each note in the score and in any indicated range, if there is a keyword parameter matching keyword and the parameter value is a number, increment the parameter value by amount. For example, to tranpose up by a whole step, write (score-transpose 2 :pitch score). The original score is not modified, and a new score is returned.

(score-scale score keyword amount [:from-index i] [:to-index j] [:from-time x] [:to-time y])
For each note in the score and in any indicated range, if there is a keyword parameter matching keyword and the parameter value is a number, multiply the parameter value by amount. The original score is not modified, and a new score is returned.

(score-sustain score factor [:from-index i] [:to-index j] [:from-time x] [:to-time y])
For each note in the score and in any indicated range, multiply the duration (stretch factor) by amount. This can be used to make notes sound more legato or staccato, and does not change their starting times. The original score is not modified, and a new score is returned.

(score-voice score replacement-list [:from-index i] [:to-index j] [:from-time x] [:to-time y])
For each note in the score and in any indicated range, replace the behavior (function) name using replacement-list, which has the format: ((old1 new1) (old2 new2) ...), where oldi indicates a current behavior name and newi is the replacement. If oldi is *, it matches anything. For example, to replace my-note-1 by trombone and my-note-2 by horn, use (score-voice score '((my-note-1 trombone) (my-note-2 horn))). To replace all instruments with piano, use (score-voice score '((* piano))). The original score is not modified, and a new score is returned.

(score-merge score1 score2 ...)
Create a new score containing all the notes of the parameters, which are all scores. The resulting notes retain their original times and durations. The merged score begin time is the minimum of the begin times of the parameters and the merged score end time is the maximum of the end times of the parameters. The original scores are not modified, and a new score is returned.

(score-append score1 score2 ...)
Create a new score containing all the notes of the parameters, which are all scores. The begin time of the first score is unaltered. The begin time of each other score is aligned to the end time of the previous score; thus, scores are "spliced" in sequence. The original scores are not modified, and a new score is returned.

(score-select score predicate [:from-index i] [:to-index j] [:from-time x] [:to-time y] [:reject flag])
Select (or reject) notes to form a new score. Notes are selected if they fall into the given ranges of index and time and they satisfy predicate, a function of three parameters that is applied to the start time, duration, and the expression of the note. Alternatively, predicate may be t, indicating that all notes in range are to be selected. The selected notes along with the existing score begin and end markers, are combined to form a new score. Alternatively, if the :reject parameter is non-null, the notes not selected form the new score (in other words the selected notes are rejected or removed to form the new score). The original score is not modified, and a new score is returned.

(score-set-begin score time)
The begin time from the score's SCORE-BEGIN-END marker is set to time. The original score is not modified, and a new score is returned.

(score-get-begin score)
Return the begin time of the score.

(score-set-end score time)
The end time from the score's SCORE-BEGIN-END marker is set to time. The original score is not modified, and a new score is returned.

(score-get-end score)
Return the end time of the score.

(score-must-have-begin-end score)
If score does not have a begin and end time, construct a score with a SCORE-BEGIN-END expression and return it. If score already has a begin and end time, just return the score. The orignal score is not modified.

(score-filter-length score cutoff)
Remove notes that extend beyond the cutoff time. This is similar to score-select, but the here, events are removed when their nominal ending time (start time plus duration) exceeds the cutoff, whereas the :to-time parameter is compared to the note's start time. The original score is not modified, and a new score is returned.

(score-repeat score n)
Make a sequence of n copies of score. Each copy is shifted to that it's begin time aligns with the end time of the previous copy, as in score-append. The original score is not modified, and a new score is returned.

(score-stretch-to-length score length)
Stretch the score so that the end time of the score is the score's begin time plus length. The original score is not modified, and a new score is returned.

(score-filter-overlap score)
Remove overlapping notes (based on the note start time and duration), giving priority to the positional order within the note list (which is also time order). The original score is not modified, and a new score is returned.

(score-print score)
Print a score with one note per line. Returns nil.

(score-play score)
Play score using timed-seq to convert the score to a sound, and play to play the sound.

(score-adjacent-events score function [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Call (function A B C), where A, B, and C are consecutive notes in the score. The result replaces B. If the result is nil, B is deleted, and the next call will be (function A C D), etc. The first call is to (function nil A B) and the last is to (function Y Z nil). If there is just one note in the score, (function nil A nil) is called. Function calls are not made if the note is outside of the indicated range. This function allows notes and their parameters to be adjusted according to their immediate context. The original score is not modified, and a new score is returned.

(score-apply score function [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Replace each note in the score with the result of (function time dur expression), where time, dur, and expression are the time, duration, and expression of the note. If a range is indicated, only notes in the range are replaced. The original score is not modified, and a new score is returned.

(score-indexof score function [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Return the index (position) of the first score event (in range) for which applying function using (function time dur expression) returns true.

(score-last-indexof score function [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Return the index (position) of the last score event (in range) for which applying function using (function time dur expression) returns true.

(score-randomize-start score amt [:from-index i] [:to-index j] [:from-time x] [:to-time y])
Alter the start times of notes by a random amount up to plus or minus amt. The original score is not modified, and a new score is returned.

Xmusic and Standard MIDI Files

Nyquist has a general facility to read and write MIDI files. You can even translate to and from a text representation, as described in Chapter "MIDI, Adagio, and Sequences". It is also useful sometimes to read notes from Standard MIDI Files into Xmusic scores and vice versa. At present, Xmusic only translates notes, ignoring the various controls, program changes, pitch bends, and other messages.

MIDI notes are translated to Xmusic score events as follows:

(time dur (NOTE :chan channel
:pitch keynum :vel velocity))
,
where channel, keynum, and velocity come directly from the MIDI message (channels are numbered starting from zero). Note also that note-off messages are implied by the stretch factor dur which is duration in seconds.

(score-read-smf filename)
Read a standard MIDI file from filename. Return an Xmusic score, or nil if the file could not be opened. The start time is zero, and the end time is the maximum end time of all notes. A very limited interface is offered to extract MIDI program numbers from the file: The global variable *rslt* is set to a list of MIDI program numbers for each channel. E.g. if *rslt* is (0 20 77), then program for channel 0 is 0, for channel 1 is 20, and for channel 2 is 77. Program changes were not found on other channels. The default program number is 0, so in this example, it is not known whether the program 0 on channel 0 is the result of a real MIDI program change command or just a default value. If more than one program change exists on a channel, the last program number is recorded and returned, so this information will only be completely correct when the MIDI file sends single program change per channel before any notes are played. This, however, is a fairly common practice. Note that the list returned as *rslt* can be passed to score-write-smf, described below.

(score-write-smf score filename [programs])
Write a standard MIDI file to filename with notes in score. In this function, every event in the score with a :pitch attribute, regardless of the "instrument" (or function name), generates a MIDI note, using the :chan attribute for the channel (default 0) and the :vel attribute for velocity (default 100). There is no facility (in the current implementation) to issue control changes, but to allow different instruments, MIDI programs may be set in two ways. The simplest is to associate programs with channels using the optional programs parameter, which is simply a list of up to 16 MIDI program numbers. Corresponding program change commands are added to the beginning of the MIDI file. If programs has less than 16 elements, program change commands are only sent on the first n channels. The second way to issue MIDI program changes is to add a :program keyword parameter to a note in the score. Typically, the note will have a :pitch of nil so that no actual MIDI note-on message is generated. If program changes and notes have the same starting times, their relative playback order is undefined, and the note may be cut off by an immediately following program change. Therefore, program changes should occur slightly, e.g. 1 ms, before any notes. Program numbers and channels are numbered starting at zero, matching the internal MIDI representation. This may be one less than displayed on MIDI hardware, sequencers, etc.

Workspaces

When working with scores, you may find it necessary to save them in files between work sessions. This is not an issue with functions because they are normally edited in files and loaded from them. In contrast, scores are created as Lisp data, and unless you take care to save them, they will be destroyed when you exit the Nyquist program.

A simple mechanism called a workspace has been created to manage scores (and any other Lisp data, for that matter). A workspace is just a set of lisp global variables. These variables are stored in the file workspace.lsp. For simplicity, there is only one workspace, and no backups or versions are maintained, but the user is free to make backups and copies of workspace.lsp. To help remember what each variable is for, you can also associate and retrieve a text string with each variable. The following functions manage workspaces.

In addition, when a workspace is loaded, you can request that functions be called. For example, the workspace might store descriptions of a graphical interface. When the workspace is loaded, a function might run to convert saved data into a graphical interface. (This is how sliders are saved by the IDE.)

(add-to-workspace symbol)
Adds a global variable to the workspace. The symbol should be a (quoted) symbol.

(save-workspace)
All global variables in the workspace are saved to workspace.lsp (in the current directory), overwriting the previous file.

(describe symbol [description])
If description, a text string, is present, associate description with the variable named by the symbol. If symbol is not already in the workspace, it is added. If description is omitted, the function returns the current description (from a previous call) for symbol.

(add-action-to-workspace symbol)
Requests that the function named by symbol be called when the workspace is loaded (if the function is defined).

To restore a workspace, use (load "workspace"). This restores the values of the workspace variables to the values they had when save-workspace was last called. It also restores the documentation strings, if set, by describe. If you load two or more workspace.lsp files, the variables will be merged into a single workspace. The current set of workspace variables are saved in the list *workspace*. To clear the workspace, set *workspace* to nil. This does not delete any variables, but means that no variables will be saved by save-workspace until variables are added again.

Functions to be called are saved in the list *workspace-actions*. to clear the functions, set *workspace-actions* to nil. Restore functions to the list with add-action-to-workspace.

Utility Functions

This chapter concludes with details of various utility functions for score manipulation.

(patternp expression)
Test if expression is an Xmusic pattern.

(params-transpose params keyword amount)
Add a transposition amount to a score event parameter. The params parameter is a list of keyword/value pairs (not preceded by a function name). The keyword is the keyword of the value to be altered, and amount is a number to be added to the value. If no matching keyword is present in params, then params is returned. Otherwise, a new parameter list is constructed and returned. The original params is not changed.

(params-scale params keyword amount)
Scale a score event parameter by some factor. This is like params-transpose, only using multiplication. The params list is a list of keyword/value pairs, keyword is the parameter keyword, and amount is the scale factor.

(interpolate x x1 y1 x2 y2)
Linearly interpolate (or extrapolate) between points (x1, y1) and (x2, y2) to compute the y value corresponding to x.

(intersection a b)
Compute the set intersection of lists a and b.

(union a b)
Compute the set union of lists a and b.

(set-difference a b)
Compute the set of all elements that are in a but not in b.

(subsetp a b)\
Returns true iff a is a subset of b, that is, each element of a is a member of b.


Previous Section | Next Section | Table of Contents | Index | Title Page