Andreas Pfenning and Roger B. Dannenberg
This page describes some uses of probability distribution functions that are defined in lib/distributions.lsp, so be sure to load "distributions" before trying these examples. See the manual for more details.
A probability distribution generator outputs a single number based the particular distribution and the input parameters. These functions were created to supplement the uniform random generator real-random. There are two types of distributions: continuous and discrete. For either distribution, looking at the diagrams will show you the likely range of outputs. In a continuous distribution, the likelihood of returning a value between two points on the x-axis is equal to the area under the curve between those two points. Every continuous distribution has the property that the total area under the curve = 1. In a discrete distribution, only integers >= 0 are allowed as output. Each one of these numbers has a probability of being returned which is equal to the height of the bar in the diagram. For the user's convenience, many distributions have optional bounds that come in useful for certain applications.
One of the most common distributions found in algorithmic composition is known as the Poisson process. This consists of a sequence of notes or events separated by an amount of time determined by an exponential distribution. The resulting sequence of events has the following property: every point in time is equally likely to be that start of a note. Another property is that, the probability of an event happening in the next unit of time is completely independent of how long it has been since the previous event. These distributions are naturally occuring when events happen randomly without any coordination.
The following is just one example how to use the exponential
distribution. The first input parameter, .25, spreads the
distribution out so that higher values are more likely. The second
input is optional and is an upper bound on the output.
Note: all examples are in SAL syntax followed by Lisp syntax
in small type.
play seqrep(i, 20, pluck(c4, 0.5 * exponential-dist(.25, 2.0)))
(play (seqrep (i 20) (pluck c4 (* 0.5 (exponential-dist .25 2.0)))))
Discrete distribution generators are useful for changing pitch (to integer values). Constraining the output to a whole number means that every pitch is going to be in the chromatic scale. In the case of the binomial distribution, both parameters together determine the mean. One way to get feel for how the different parameters affect the distribution is to print values as you generate them. Distribution parameters may require some tweaking to produce a desired affect. In some cases, altering the parameters can yield unexpected and interesting sounds.
function try-binomial()
play seqrep(i, 20, pluck(binomial-dist(6, .5) + c4), 0.1)) (defun try-binomial () (play (seqrep (i 20) (pluck (+ (binomial-dist 6 .5) c4) 0.1))))
In granular synthesis, interesting sounds can be made
by having a pitch vary according to some distribution other than
the simple uniform distribution. In lib/dist-test.lsp,
there is a a granular synthesis function that can take in any
distribution
(within the bounds of granular synthesis) and vary the pitch of
the tone
based on that distribution. To do
this, you pass in a parameterless function as a parameter. Since
most
distribution functions take parameters, it is necessary to create
a closure as shown below using a lambda
expression. Strictly speaking, SAL does not support closures, but in this case, we can fool the compiler into creating a lambda expression by pretending lambda is a function. The SAL expression and the (more proper) Lisp expression are as follows:
function make-gamma(nu, high) return lambda(nil, gamma-dist(nu, high))
(defun make-gamma (nu high) (lambda () (gamma-dist nu high)))
Here, make-gamma takes two parameters and returns a function with no parameters. This no-parameter function remembers the values of nu and high and will call gamma-dist with these. For example, try
print apply(make-gamma(2, 5.0), nil)(The apply function applies a function to a list of arguments; in this case the list is nil, so there are no arguments.) This example just makes the function and uses it once, so there's not much point, but in the granular synthesis example, the function will be called many times. Unlike passing a number as a parameter, passing a function means that the granular synthesis function can call the function for every grain, resulting in a new value for each grain. (Be sure to load lib/dist-test.lsp before running this example.)
(apply (make-gamma 2 5.0) nil)
function try-pitch-dist() play simrep(i, 4, pitch-dist-granulate(
"demos/demo-snd.aiff", 0.04, 0.0, 0.02, 0.001, make-gamma(2, 5.0), 0, 0))) ~ 4 (defun try-pitch-dist () (play (stretch 4 (simrep (i 4) (pitch-dist-granulate "demos/demo-snd.aiff" 0.04 0.0 0.02 0.001 (make-gamma 2 5.0) 0 0)))))
The length of grains can also be made to vary by a distribution passed in as a continuation. Here is an example.
function make-gauss(xmu, sigma, low, high) return lambda(nil, gaussian-dist(xmu, sigma, low, high) function try-len-dist() play simrep(i, 2, len-dist-granulate("demos/demo-snd.aiff", make-gauss(0.0, 1.0, 0.1, .5),
0.02, 0.001, 2.0, 0, 0)) ~ 4 (defun make-gauss (xmu sigma low high) (lambda ()(gaussian-dist xmu sigma low high))) (defun try-len-dist () (play (stretch 4 (simrep (i 2) (len-dist-granulate "demos/demo-snd.aiff" (make-gauss 0.0 1.0 0.1 .5) 0.02 0.001 2.0 0 0)))))