Xmusic and Algorithmic Composition (original) (raw)
Previous Section | Next Section | Table of Contents | Index | Title Page
Several Nyquist libraries offer support for algorithmic composition. Xmusic is a library for generating sequences and patterns of data. Included in Xmusic are:
- pattern objects, used to generate interesting sequences of parameter values,
- random number generators, used to create random sequences from different distributions (contained in the
distributions.lsp
library) - a standard representation for "note lists" called scores and functions to render them as sounds,
- the
score-gen
macro which helps to generate scores from patterns, - score manipulation functions to select, transform, shift, and perform other operations on scores.
Xmusic Basics
Xmusic is inspired by and based on Common Music by Rick Taube. 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 (the base language for Nyquist).
The Xmusic libraries in Nyquist offer an interesting subset of the tools in Common Music. One important feature of Xmusic is that it is integrated with all of the Nyquist synthesis functions, so you can use Xmusic patterns and scores to control fine details of sound synthesis.
Xmusic Patterns
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.
Xmusic functions are automatically loaded when you start Nyquist. To use a pattern object, you first create the pattern, e.g.
set pitch-source = make-cycle(list(c4, d4, e4, f4))
In this example, pitch-source
is an object of classcycle-class
which inherits from pattern-class
.
Because SAL is not an object-oriented language, these classes and their methods are not directly accessible from SAL. Instead, Xmusic defines a functional interface, e.g. make-cycle
creates an instance of cycle-class
, and the next
function, introduced below, retrieves the next value from any instance of pattern-class
. Using LISP syntax, you can have full access to the methods of all objects (see the source code in xm.lsp
, but the functional interface described here should be sufficient and it is recommended that you limit your access to this interface.
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. Second, the pattern is accessed (multiple times) using next
.
next(_pattern-object_ [, #t])
[SAL](next _pattern-object_ [t])
[LISP]
Returns the next element from a pattern generator object. If the optional second argument is true (default value is false), then an entire period is returned as a list.
Nested Patterns
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.
Periods
The data returned by a pattern object is structured into logical groups called periods. You can get an entire period (as a list) by callingnext(_pattern_, t)
. For example:
set pitch-source = make-cycle(list(c4, d4, e4, f4)) print 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.
set pitch-source = make-cycle(list(c4, d4, e4, f4), for: 3) print next(pitch-source, t) print next(pitch-source, t)
This prints the lists (60 62 64) (60 62 64)
. Notice that the cycle starts from the beginning after only 3 items and the fourth is never reached in this case. The for:
parameter_could itself be a pattern_, in which case the a new cycle length would be computed at the beginning of every cycle.
A variation on this restructures the stream of items into groups of 3:
set pitch-source = make-length(make-cycle(list(c4, d4, e4 f4)), 3) print next(pitch-source, t) print next(pitch-source, t)
This prints the lists (60 62 64) (65 60 62)
. Note that in this case the cycle pattern is producing default cycles of length 4 because there is no for:
specification, and these are regrouped by the length pattern.
Nested patterns are probably easier to understand by example than by specification. Here is a simple nested pattern of cycles:
set cycle-1 = make-cycle({a b c}) set cycle-2 = make-cycle({x y z}) set cycle-3 = make-cycle(list(cycle-1, cycle-2)) loop repeat 9 exec format(t, "~A ", next(cycle-3)) end
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.
General Parameters for Creating Pattern objects
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.
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_)
[SAL](make-cycle _items_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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.) The list is replaced and restarted every period of the cycle, which defaults to the whole list, but may be specified by the for:
keyword. If items is a pattern, a new period of _items_becomes the list from which items are generated for each cycle pattern. Note that the items list may be truncated by the use of for:
. Different groupings and repetitions can be obtained by nesting make-cycle
within make-length
and/or make-copier
patterns.
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_)
[SAL](make-line _items_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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_)
[SAL](make-random _items_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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 (optional) relative probability of selecting this item, mincount is the (optional) minimum number of repetitions when this item is selected, and maxcount is the (optional) 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_)
[SAL](make-palindrome _items_ :elide _elide_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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)
. Normally, repetitions can occur even if all list elements are distinct. This happens when the last element of a period is chosen first in the next period. To avoid repetitions, themax:
keyword argument can be set to 1. The max:
keyword only controls repetitions from the end of one period to the beginning of the next. If the list contains more than one copy of the same value, it may be repeated within a period regardless of the value of max:
.
make-heap(_items_, for: _for_, max: _max_, name: _name_, trace: _trace_)
[SAL](make-heap _items_ :for _for_ :max _max_ :name _name_ :trace _trace_)
[LISP]
Generate items randomly from list without replacement. If max is 1, the first element of a new period will not be the same as the last element of the previous period, avoiding repetition. The default value of max is 2, meaning repetition is allowed. 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.
accumulation
The accumulation-class
takes a list of values and returns the first, followed by the first two, followed by the first three, etc. In other words, for each list item, return all items from the first through the item. For example, if the list is (A B C), each generated period is (A A B A B C).
make-accumulation(_items_, name: _name_, trace: _trace_)
[SAL](make-accumulation _items_ :name _name_ :trace _trace_)
[LISP]
Return the prefixes of items, e.g. the first element followed by the first and second elements, then the first three, until the entire list is returned. The period length is (_n_2 + n) / 2 where n is the length of items. If items is a pattern, a period from that pattern becomes the list from which items are generated, and a new list is generated every period. Note that this is similar in name but different from make-accumulate
.
copier
The copier-class
makes copies of periods from a sub-pattern. For example, three periods of make-copier(make-length(make-cycle({a b c}), 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, make-length
is used to re-group the cycle into periods 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_)
[SAL](make-copier _sub-pattern_ :repeat _repeat_ :merge _merge_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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_)
[SAL](make-accumulate _sub-pattern_ :for _for_ :max _maximum_ :min _minimum_ :name _name_ :trace _trace_)
[LISP]
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. Minimum and maximum patterns are reevaluated every cycle. Note that this is similar in name but not in function to make-accumulation
.
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_)
[SAL](make-sum _x_ _y_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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_)
[SAL](make-product _x_ _y_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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 if the expression is an expression; otherwise, the expression must be a pattern that returns expressions to be evaluated, and the output periods match the periods of the expression pattern.
make-eval(_expr_, for: _for_, name: _name_, trace: _trace_)
[SAL](make-eval _expr_ :for _for_ :name _name_ :trace _trace_)
[LISP]
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. Note that expr is evaluated as a Lisp expression, which is a list consisting of the function name followed by parameter expressions (which may also be lists). In the simplest case (recommended), create a SAL function with no parameters that performs the computation. For example, to create a pattern in SAL that calls the parameterless function rrandom
, writemake-eval({rrandom})
. In SAL, {rrandom}
creates a list with the (unevaluated) symbol rrandom
, equivalent to list(quote(rrandom))
.
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, thenelide:
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_)
[SAL](make-length _pattern_ _length-pattern_ :name _name_ :trace _trace_)
[LISP]
Make a pattern of classlength-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_)
[SAL](make-window _pattern_ _length-pattern_ _skip-pattern_ :name _name_ :trace _trace_)
[LISP]
Make a pattern of classwindow-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_)
[SAL](make-markov _rules_ _past_ :produces _produces_ :for _for_ :name _name_ :trace _trace_)
[LISP]
Generate a sequence of items from a Markov process. The rules parameter is a list of rules where each rule 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(quote(a), 69, quote(b), 71)
. You can also map symbols to patterns, for examplelist(quote(a), make-cycle({57 69}), quote(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_])
[SAL](markov-create-rules _sequence_ _order_ [_generalize_])
[LISP]
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. Further discussion and examples can be found in nyquist/demos/probability_distributions.htm
.
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_)
[SAL](linear-dist _g_)
[LISP]
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_])
[SAL](exponential-dist _delta_ [_high_])
[LISP]
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_])
[SAL](gamma-dist _nu_ [_high_])
[LISP]
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_])
[SAL](bilateral-exponential-dist _xmu_ _tau_ [_low_ _high_])
[LISP]
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_])
[SAL](cauchy-dist _tau_ [_low_ _high_])
[LISP]
Returns a FLONUM
from the Cauchy distribution, a symmetric 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_])
[SAL](hyperbolic-cosine-dist [_low_ _high_])
[LISP]
Returns a FLONUM
value from the hyperbolic cosine distribution, a symmetric 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_])
[SAL](logistic-dist _alpha_ _beta_ [_low_ _high_])
[LISP]
Returns a FLONUM
value from the logistic distribution, which is symmetric 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()
[SAL](arc-sine-dist)
[LISP]
Returns a FLONUM
value from the arc sine distribution, which outputs values between 0 and 1. It is symmetric 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_])
[SAL](gaussian-dist _xmu_ _sigma_ [_low_ _high_])
[LISP]
Returns a FLONUM
value from the Gaussian or Gauss-Laplace distribution, a linear function of the normal distribution. It is symmetric 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_)
[SAL](beta-dist _a_ _b_)
[LISP]
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 symmetric about 1/2 when a = b.
Figure 16: The Beta Distribution, alpha = .5, beta = .25.
bernoulli-dist(_px1_ [, _x1_, _x2_])
[SAL](bernoulli-dist _px1_ [_x1_ _x2_])
[LISP]
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_)
[SAL](binomial-dist _n_ _p_)
[LISP]
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_)
[SAL](geometric-dist _p_)
[LISP]
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_)
[SAL](poisson-dist _delta_)
[LISP]
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,
set pitch-pattern = make-cycle(list(c4, d4, e4, f4)) score-gen(dur: 0.4, name: quote(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 withscore-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 in SAL have default values that are used if no actual parameter is provided by the caller of the function. (See AppendixAppendix 3: XLISP: An Object-oriented Lisp to learn about keywords in XLISP.)
To specify that a parameter is a keyword parameter, use a keyword symbol (one that ends in a colon) followed by a default value. For example, here is a function that accepts keyword parameters and invokes the pluck
function:
define function k-pluck(pitch: 60, dur: 1) return 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:
k-pluck(pitch: 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. 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. Because of default values, 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.
In XLISP, 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_, ...)
[SAL](score-gen _:k1_ _e1_ _:k2_ _e2_ ...)
[LISP]
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 questions and answers below.
How many notes will be generated? The keyword parameter score-len:
specifies an upper bound on the number of notes. (Note: in LISP syntax, keywords are always preceded by colons, so you would write:score-len
instead.) 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:start
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:start
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_)
[SAL](event-time _event_)
[LISP]
Retrieve the time field from an event.
event-set-time(_event_, _time_)
[SAL](event-set-time _event_ _time_)
[LISP]
Construct a new event where the time of event is replaced by time.
event-dur(_event_)
[SAL](event-dur _event_)
[LISP]
Retrieve the duration (i.e. the stretch factor) field from an event.
event-set-dur(_event_, _dur_)
[SAL](event-set-dur _event_ _dur_)
[LISP]
Construct a new event where the duration (or stretch factor) of event is replaced by dur.
event-expression(_event_)
[SAL](event-expression _event_)
[LISP]
Retrieve the expression field from an event.
event-set-expression(_event_, _dur_)
[SAL](event-set-expression _event_ _dur_)
[LISP]
Construct a new event where the expression of event is replaced by expression.
event-end(_event_)
[SAL](event-end _event_)
[LISP]
Retrieve the end time of event, its time plus its duration.
expr-has-attr(_expression_, _attribute_)
[SAL](expr-has-attr _expression_ _attribute_)
[LISP]
Test whether a score event expression has the given attribute.
expr-get-attr(_expression_, _attribute_ [, _default_])
[SAL](expr-get-attr _expression_ _attribute_ [_default_])
[LISP]
Get the value of the given attribute from a score event_expression_. If attribute is not present, return default if specified, and otherwise nil
. (See the example after score-apply
below for an example using expr-get-attr
.)
expr-set-attr(_expr_, _attribute_, _value_)
[SAL](expr-set-attr _expr_ _attribute_ _value_)
[LISP]
Construct a new expression identical to expr except that the attribute has value. (See the example after score-apply
below for an example using expr-set-attr
.)
event-has-attr(_event_, _attribute_)
[SAL](event-has-attr _event_ _attribute_)
[LISP]
Test whether a given score event's expression has the given attribute.
event-get-attr(_event_, _attribute_, [_default_])
[SAL](event-get-attr _event_ _attribute_ [_default_])
[LISP]
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_)
[SAL](event-set-attr _event_ _attribute_ _value_)
[LISP]
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, where 1 (not zero) denotes the first note. If indices are in range, the number of note selected is to-index
-
from-index
+
1
). Out-of-range indices are ignored. If these numbers are negative, they are offsets from the end of the score, e.g. -1 denotes the last note of the score. Thefrom-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. (Note: in LISP syntax, colons precede the keyword, so use:from-index
, :to-index
, :from-time
, and :to-time
.)
score-sorted(_score_)
[SAL](score-sorted _score_)
[LISP]
Test if score is sorted.
score-sort(_score_ [, _copy-flag_])
[SAL](score-sort _score_ [_copy-flag_])
[LISP]
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_)
[SAL](score-shift _score_ _offset_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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 decreased if necessary to the minimum time of any event that is moved to an earlier time (by a negative_offset_), and the end time of the score is increased if necessary to the maximum end time of any event that is moved to a later time. If all shifted events remain within the score's begin-to-end range, the begin and end times are not changed. 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_)
[SAL](score-stretch _score_ _factor_ :dur _dur-flag_ :time _time-flag_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-transpose _score_ _keyword_ _amount_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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. If keyword is :pitch
and a corresponding parameter value is a list, each element of the list is incremented by amount. This special case is in keeping with the convention of timed-seq
in which score events with lists for the :pitch
attribute are expanded into "chords" by instantiating an event for each element (pitch) in the list (chord).
score-scale(_score_, _keyword_, _amount_, from-index: _i_, to-index: _j_, from-time: _x_, to-time: _y_)
[SAL](score-scale _score_ _keyword_ _amount_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-sustain _score_ _factor_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-voice _score_ _replacement-list_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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
byhorn
, 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_, ...)
[SAL](score-merge _score1_ _score2_ ...)
[LISP]
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_, ...)
[SAL](score-append _score1_ _score2_ ...)
[LISP]
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_, extract: _ex_
[SAL](score-select _score_ _predicate_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_ :reject _flag_ :extract _ex_)
[LISP]
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. Note that from-index:
i and to-index:
j are 1-based: A value of 1 refers to the first note, etc., because the zeroth element of the score is a SCORE-BEGIN-END
event. For consistency, if a SCORE-BEGIN-END
event is missing from score, one is inserted before any further processing. The selected notes are combined to form a new score. By default, ex, the value of the extract:
keyword, is false, and the begin and end markers are retained from_score_. Conceptually, the score and its timing are retained as an object, but only some of its constituent sound events are retained. Otherwise, if ex is true (non-nil), the begin and end times are computed based on the selected (extracted) events: The begin time is from-time
x, if present, and otherwise the time of the first event (if any), and otherwise the begin time of score. Similarly, the end time is theto-time
y, if present, and otherwise the end time of the last event (if any), and otherwise the new start time. 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 begin and end times of_score are retained and the extract
parameter (ex) is ignored. In all cases, the original score is not modified, and a new score is returned.
score-set-begin(_score_, _time_)
[SAL](score-set-begin _score_ _time_)
[LISP]
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_)
[SAL](score-get-begin _score_)
[LISP]
Return the begin time of the score.
score-set-end(_score_, _time_)
[SAL](score-set-end _score_ _time_)
[LISP]
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_)
[SAL](score-get-end _score_)
[LISP]
Return the end time of the score.
score-must-have-begin-end(_score_)
[SAL](score-must-have-begin-end _score_)
[LISP]
If score does not have a begin and end time, construct a score with aSCORE-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_)
[SAL](score-filter-length _score_ _cutoff_)
[LISP]
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_)
[SAL](score-repeat _score_ _n_)
[LISP]
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_)
[SAL](score-stretch-to-length _score_ _length_)
[LISP]
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_)
[SAL](score-filter-overlap _score_)
[LISP]
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_, [_lines_])
[SAL](score-print _score_ [_lines_])
[LISP]
Print a score with one note per line. Returns nil
. If lines (optional FIXNUM) is given, print a maximum of that many lines (but the minimum is at least 3). The format is first _lines_-2 score events, the line "...
", and the last score event.
score-play(_score_)
[SAL](score-play _score_)
[LISP]
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_)
[SAL](score-adjacent-events _score_ _function_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-apply _score_ _function_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
Replace each note in the score with the result of(_function time dur expression_)
(in Lisp) or_function_(_time_, _dur_, _expression_)
(in SAL), 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.
For example, the following uses score-apply
to insert anaccent:
attribute with value 100 on every sound event (note) where pitch:
is greater than 70. Notice thatadd-accents
must be quoted to pass the name of the function to score-apply
(without the quote
, add-accents
denotes the value of a variable, but as a variable,add-accents
is undefined). Also, note that add-accents
must construct and return a valid score event, hence the expressionlist(time, dur, sound))
.
set my-score = {{0 1 {note pitch: 60}} {1 1 {note pitch: 72}}}
function add-accents(time, dur, sound) begin if expr-get-attr(sound, :pitch, 70) > 70 then set sound = expr-set-attr(sound, :accent, 100) return list(time, dur, sound) end
exec score-print(score-apply(my-score, quote(add-accents)))
The output will be:
((0 0 (SCORE-BEGIN-END 0 2)) (0 1 (NOTE :PITCH 60)) (1 1 (NOTE :PITCH 72 :ACCENT 100)) )
score-indexof(_score_, _function_, from-index: _i_, to-index: _j_, from-time: _x_, to-time: _y_)
[SAL](score-indexof _score_ _function_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-last-indexof _score_ _function_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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_)
[SAL](score-randomize-start _score_ _amt_ :from-index _i_ :to-index _j_ :from-time _x_ :to-time _y_)
[LISP]
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.
Using score-read-smf
and score-read
, 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.
The score-from-seq
function allows you to obtain control changes and other MIDI messages.
score-read-smf(_filename_)
[SAL](score-read-smf _filename_)
[LISP]
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-read(_filename_)
[SAL](score-read _filename_)
[LISP]
Read an Adagio file from filename. Return an Xmusic score, or nil
if the file could not be opened. See Chapter MIDI, Adagio, and Sequences for details on Adagio, a text-based score language. See score-read-smf
for details on handling program changes.
score-from-seq(_seq_, prog: _pflag_, synths: _synths_, bend: _bend_, cpress: _cpress_, ctrl: _ctrls_)
[SAL](score-from-seq _seq_ :prog _pflag_ :synths _synths_ :bend _bend_ :cpress _cpress_ :ctrl _ctrls_)
[LISP]
Produce a score from a sequence (see Section The SEQ Type) type_seq_. The optional pflag, if non-null, will insert program changes as event :prog
attributes in score events. The_bend_ and cpress (channel pressure) values may be:onset
to introduce :bend
or :cpress
attributes into score events, or :contin
to encode these MIDI messages as SOUNDs available through event :contin
attributes. The_ctrl_ parameter is a list where the first element is either:onset
or :contin
and the remaining elements are controller numbers to be encoded. In all cases :contin
values appear in score events as an object. You can access the SOUNDs that encode control changes using the functionsctrlfn-bend(_contin_)
,ctrlfn-cpress(_contin_)
orctrlfn-ctrl(_contin_, _number_)
, which will return const(0)
if no corresponding MIDI messages were found. (As a special case the default for controller 7 (volume pedal) is const(1)
). See lib/midi/midi_tutorial.htm
for more details and code examples. To test for the presence of MIDI messages and avoid the construction of const(0)
, use ctrlfn-bend?(_contin_)
,ctrlfn-cpress?(_contin_)
orctrlfn-ctrl?(_contin_, _number_)
.
score-write-smf(_score_, _filename_, [_programs_ _as-adagio_])
[SAL](score-write-smf _score_ _filename_ [_programs_ _as-adagio_])
[LISP]
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. The as-adagio optional parameter should normally be omitted. If non-nil, the file is written in Adagio format, but if you want to do that, call score-write
instead. Xmusic scores do not specify tempo, so the MIDI file is written with a fixed tempo of 100bpm. If you create scores or stretch scores so that each beat is exactly 0.6s (100bpm), sequencers and score editors will quantize your scores correctly. Otherwise, the _timing_will be correct, but for example a score with one note every second will be notated as 1 note every 1 2/3 beats.
score-write(_score_, _filename_, [_programs_, _absolute_])
[SAL](score-write _score_ _filename_ [_programs_ _absolute_)]
[LISP]
Write an Adagio format file to filename with notes in score, using absolute times if absolute is true, otherwise write relative times (the default). See Chapter MIDI, Adagio, and Sequences for details on Adagio, a text-based score language. See score-write-smf
for details on MIDI program changes.
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_)
[SAL](add-to-workspace _symbol_)
[LISP]
Adds a global variable to the workspace. The symbol should be a (quoted) symbol.
save-workspace()
[SAL](save-workspace)
[LISP]
All global variables in the workspace are saved to workspace.lsp
(in the current directory), overwriting the previous file.
describe(_symbol_ [, _description_])
[SAL](describe _symbol_ [_description_])
[LISP]
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_)
[SAL](add-action-to-workspace _symbol_)
[LISP]
Requests that the function named by symbol be called when the workspace is loaded (if the function is defined).
To restore a workspace, use the command load "workspace"
. This restores the values of the workspace variables to the values they had whensave-workspace
was last called. It also restores the documentation strings, if set, by describe
. If you load two or moreworkspace.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_)
[SAL](patternp _expression_)
[LISP]
Test if expression is an Xmusic pattern.
params-transpose(_params_, _keyword_, _amount_)
[SAL](params-transpose _params_ _keyword_ _amount_)
[LISP]
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_)
[SAL](params-scale _params_ _keyword_ _amount_)
[LISP]
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_)
[SAL](interpolate _x_ _x1_ _y1_ _x2_ _y2_)
[LISP]
Linearly interpolate (or extrapolate) between points (x1, y1) and (x2, y2) to compute the y value corresponding to x.
intersection(_a_,_b_)
[SAL](intersection _a_ _b_)
[LISP]
Compute the set intersection of lists a and b.
union(_a_, _b_)
[SAL](union _a_ _b_)
[LISP]
Compute the set union of lists a and b.
set-difference(_a_,_b_)
[SAL](set-difference _a_ _b_)
[LISP]
Compute the set of all elements that are in a but not in b.
subsetp(_a_, _b_)
[SAL](subsetp _a_ _b_)
[LISP]
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