Csound random Generators

Compiled from various sources 1998-2001, by rasmus ekman.

This article describes the workings of Csound's three random generator opcode families: rand, (bi)rnd() and the various-distributions (x-noise) set.

Please be warned that the author is not well versed in mathematics, nor a native English speaker.


CONTENTS


Randomness and Pseudo-Randomness

Randomness in nature, errors in the performance of a classical musician, arbitrariness of an improviser, and similarly most types of "noise" in physical events (such as thunder, background car traffic, an anthill or wheeze in a flute tone) cannot be represented directly in mathematics. It can at most be modelled by something more or less similar, to the best of our understanding of the phenomenon. The mathematical models will usually appear to us as microscopic components, or very wide generalizations of noise in the common sense. And since computer sound-making mostly relies on such mathematical models, this is what we use for building blocks.

For practical applications of mathematical models of systems with unpredictable parts, a limited notion of randomness is often used. This goes as follows.

To get a series of "random" numbers means:
You cannot predict what the next number will be.

This is enough to model measurements taken of a real world system that you can't predict, whether it is just not well understood, or it "really is truly random" (like eg decay times of radioactive material). The randomness functions used by Csound and most non-specialized programs can fulfill this limited randomness requirement, but as we'll see, they can not earn the title random in any other sense, so they are therefore often called "Pseudo-random number generators", or PRNGs for short.

Pseudo-Random Number Generators (PRNG's)

We'll briefly explain the inner workings of what could be called "the basic PRNG".
This operates by taking any number N, multiplying it by a (large) number M, adding another (small or large) number A to it, and finally taking this modulus a (large) number X:

To generate the next number, the output of the previous number is used (indicated above by the subscripting i for the input number, and i+1 for the result). Provided that the numbers M and A are selected with extreme care, this will fulfil the limited pseudo-randomness requirement. There may be only one combination in many billion that actually pass various tests on randomness sported by careful scientists. X is given implicitly by the bit-size of the digital numbers involved, but there are several variations using another modulus operation on the output.

The basic PRNG is primarily designed to be very simple and fast to compute. Quality has been completely subordinated to speed here. All randomness generators in Csound use variants of the basic PRNG, so this is not irrelevant to Csounders. We'll see how the limitations of this algorithm pops up in several disguises.

Features of Common PRNG Variants

Now we turn to listing a few consequences of the just-related arrangement.

All these limitations are relevant to Csound, since the noise generators use four variants of the basic PRNG. For instance,

Shared and Separate Sequences

As explained above, a PRNG will step through a fixed sequence of numbers. The sequence can actually be considered as a fixed list of values, and the seeding can be viewed as selecting an entry point. This may be seem not very random at all, but it is usually of no consequence to mathematical systems.

Now, every PRNG needs to store its state: the last value it generated. This is used to generate the next number. The seed sets the initial state. A certain PRNG together with its state is called an instance of that PRNG. There can be several instances of the same PRNG: This is like having several people (or robots) reading from different places in the same list of numbers. Or there can be a single PRNG instance serving every opcode instance in the orchestra; each opcode in the orch takes turns getting numbers from the central generator.

Each instance of the rand opcodes has its own private PRNG (we speak here similarly of opcode instances). The opcode pair rnd()/birnd() uses a single central PRNG instance, while the whole x-noise family share a single instance of another PRNG.

This can lead to surprises:

We turn now to describing the behaviour of Csound's random generators.


Csound's Random Opcodes

Csound has three families of random opcodes; the ancient opcodes rand/ randi/randh, the inline operators rnd() and birnd(), and the so-called x-noise or variable noise family. Each family of opcodes uses a variant of the basic PRNG, each variant with slightly different characteristics.

Comparison of rand, (bi)rnd() and x-noise ugens

There are three sets of ugens for generating pseudo-random numbers in Csound. In the course of time they have been changed, and they all work slightly differently internally, so we will try to list the differences here. First a tabulated overview, details follow.

Family rand/i/h bi/rnd() x-distribution
Cycle length 216 or 231 ??? (very long) sys dep [1]
Precision [2] 216 or 231 253 215
Share sequence no yes yes
Seedable yes no globally
Distribution uniform uniform various
Source code file Ugens4.c/h Aops.c/h Cmath.c/h

Note 1: Dependent on C library used to compile your particular Csound version. Usually dependent on compiler. 231 length versions should be typical (or at least common) today, but do not rely on it - it could be an ancient 16-bit model!

Note 2: The precision concerns the internal generator. For standard Csound versions, which use 32-bit float data, no more than float precision (24 bits) is representable in orchestra variables. 64-bit versions of Csound will however get maximum precision from rnd() and birnd().

The rand family

The original rand opcodes use a not-very-good 16-bit PRNG. Some have worried that this has too short cycle length for audio-rate noise, so a newer 31-bit PRNG has been inserted, and is selectable by setting the iseed flag. The newer version should be adequate for most uses, but there are issues with the seeding.
See Usage section for more about this.

rnd() and birnd()

The inline operators rnd() and birnd() use a less common PRNG about which I haven't found any online references.

The basic PRNGs discussed up to now use integer numbers of 2 or 4 byte size internally. This was a good decision for speed until very recently. This PRNG variant uses so-called "double" data types. This uses 64 bits (or sometimes 80), ie twice as many bits as common 32-bit integers. Since bit size increase the maximum representable values exponentially, cycle length and precision of these operators are by far the best among Csound's random generators.

From inspecting the source code, barring any misunderstandings, they seem to have much higher precision, and much longer cycle than the basic PRNGs discussed above. In a very primitive raw cycle length test the code was run for 235 iterations without getting back to the seed number (that is only 16 times the length of the 31-bit versions of rand , but it seems reasonable that the cycle is much longer).

Note that these operators are NOT seedable in any way.
Note also that the quality of the pseudo-random series output is unknown (to me...). It is probably (hopefully) not worse than many C-library versions around.

Various Noise Distributions (or "x-noise") Family

    xr    uniform   krange
    xr    gauss     krange
    xr    linrand   krange
    xr    poisson   klambda
    xr    weibull   ksigma, ktau
    xr    betarand  krange, kalpha, kbeta

(See docs for full list)

The x-noise opcodes use the PRNG that happens to be supplied by the vendor of the compiler used to create a certain platform version of Csound. Thus different ones are used for the standard Linux, Mac, Windows versions, etc. If somebody builds Csound from source code at home, it may be different from the public version too.

The definition of this PRNG for ANSI C (the programming language Csound is written in) doesn't say what cycle length it should have, it is only is specified that the output should have 15-bit precision. The cycle length and other qualities of the PRNG varies between vendors, but historically many versions have apparently been rather bad. This perhaps needn't cause a lot of worry today: Modern compilers would be likely to use reasonable cycle length (eg 231).

The x-noise opcodes are intended for uses which would most often (in the imagination of this writer) not involve a-rate data, rather for high-level musical events like melodies and rhythms. Perhaps there are synthesis models where noise distribution is significant.Distribution is explained in the usage section.

Other Opcodes Using Random Numbers

Apart from the opcodes designed to generate random values, there are a couple of recent ones for pink noise, and some opcodes that use random values internally.

noise This uses the same internal generator as the x-noise opcodes (the one supplied by compiler vendor). The noise is variably filtered to something like pink noise, so the resulting output does not necessarily cycle.

pinkish The generator version of this opcode uses a 32-bit integer PRNG with cycle length and precision 232 internally. It is configurable by the user, and the random values are shifted to use 24 bits, then added up, so output precision normally goes up to 30 bits. Since random values are accumulated and added to the output the cycle length variation is not clear to this author, but it can be much longer than that of the internal PRNG. pinkish is optionally seedable from system clock, or by a user value. Each opcode instance has its own PRNG sequence.

Some opcodes, including grain and granule use random values from the same compiler vendor PRNG as the x-noise ugens (and thus consume values from its internal cycle).


Usage Tips

There are several common types of randomness and noise usage in computer music. This writer would recommend different Csound opcodes for different tasks:

When an event is "repeatable" above, this means that the exact same sound (random sequence) is output every time the instrument performs a sound. As explained above, this is only practically possible when using the rand opcode.

Seeding Problems in rand Family

Each instance of the rand opcode (and siblings) use its own instance of the PRNG. The PRNG is seeded just before the instrument starts performing in the instrument initialization pass. This awakens a problem mentioned above:

If the rand is intended to create some unexpected variation in the instrument, which should be different each time the instrument is run (or you use several instances of rand in the same instrument), a direct number cannot be used for seed - this starts off the same pseudo-random sequence every time.

For this reason, the seed was specialized so that numbers >= 1.0 use the current system clock for seeding. This is a simple micro- or milli-second tick count, completely unrelated to Greenwich clock time. Let's call this a clock seed anyway.

This has two potentially problematic consequences:

Seeding Problem Workaround

For those cases when you want repeatable performances of several rand units, the seeding gets slightly complicated. To avoid getting the same sequence you would have to type all seeds by hand, which is tedious. This can be simplified by using a single seed value and adding a small amount to it:

iRandSeed  init  [provide seed value <0.9] ; init up to 100 rand opcodes if iRandSeed = 0.9
kval0	rand  0.5, iRandSeed
kval1	rand  0.5, iRandSeed+.01
kval2	rand  0.5, iRandSeed+.02

...etc

If you need repeatable different sequences in several instances of the same instrument, the seed has to be made global:

giRandSeed  init  [provide seed value <0.9] instr 1 ; init up to 1000 Rand opcodes if iRandSeed <= 0.9
kval0	rand  0.5, 
giRandSeed
,  31
kval1	rand  0.5, 
giRandSeed + .001
,  31
kval2	rand  0.5, 
giRandSeed + .002
,  31

; Change giRandSeed every time the instrument inits so we get fresh seeds
giRandSeed = giRandSeed + 0.1
if giRandSeed <0.9 goto instrcode
; The seed is shifted slightly to avoid returning to first seed immediately
giRandSeed = giRandSeed - 0.89991

instrcode:
; instrument performance code...
	endin

What is Distribution?

As stated earlier, with random numbers you cannot say what the next number will be, or the next thousandth, even if you record each number and try to find some pattern. But there are lots of things that can be said with statistics about (large) collections of random values.

Imagine some natural phenomenon, being measured to a heap of seemingly random values.
One of the simplest things is to add up all measured values, and divide this value by the number of values recorded. This yields the average value. Another trivial operation is to sort all the random values in the collection, then examine the middlemost one and so say what the median value is.

We get a more detailed view of the data by checking small ranges of values to see how often they come up: Whether there are more small values than large, or vice versa. This is exactly what distribution is about:

The x-noise ugens are designed to model different kinds of distributions.

Now uniform noise means that every value within the range is equally common. If the range is 0.0 up to 1.0, and you generate a huge number of values, you will get roughly as many values in the most extreme range – above 0.995 – as the number of values near 0.5, or below 0.005 .

With linearly distributed randoms, the frequency of generated values fall off linearly towards the extreme end. linrand will thus output half as many values near 0.5 as values around 0.0, and even fewer values around 0.7 and still fewer at 0.995 and up. But still any value can pop up at any time; there is no way of guessing what comes next.

So, if you generate a soundfile with samples taken from rand or any of the x-noise opcodes, the spectrum of the sound (which is the time-varying histogram of pitches in the sound, not the distribution of sample values), and equally the percieved output will be the same old white noise in every case.

Noise, Melodies and Rhythms

Distribution of random values can be relevant when trying to create "random"-sounding rhythms and melodies. The various distributions are used to model different phenomena. For example, poisson is specifically designed to model random variations to an average waiting time (eg of decaying nuclear material). This could perhaps be explored for rhythms.

For melodies, a different approach could be tried. Placing tones by plain uniform noise does not create any kind of melodic cohesion except accidentally. Brown noise (the term is based on molecular Brownian motion) might be interesting. This uses a random value to pick the distance to the next value, rather than the next value itself. This leads to it gradually and tenatively exploring the entire range, without jumping about too violently There is no Csound opcode to simulate this, one simply adds the next random value (not necessarily uniform noise) to the current value.

There are also two units with variable distribution; weibull and betarand. These might be the most interesting since they allow dynamic choice of distribution. weibull can be used to generate exponential, gaussian and poisson-like distributions, while betarand can do gaussian, uniform, or distribution favouring extreme values.


REFERENCES

Numerical Recipes, chapter 12
- this gives a mathematical overview of PRNG's and distributions.

The Csound Book, ch 16: A Look at random Numbers, Noise and Chaos with Csound by John ffitch
- this article explains the mathematical concepts of noise, and discusses random distributions. It also gives graphs for the various distributions.

Colors of Noise Pseudo FAQ
- discusses pink and other noises.

Robin Whittle's page following a particular discussion about implementing pink noise.
The pinkish opcode was designed based on material from this page.


Document date: Juli 3, 2001