CCRMA

grani: a granular synthesis instrument

by Fernando Lopez-Lezcano

grani.ins is a quite complete granular synthesis instrument designed to process input soundfiles. Almost all parameters can be either numbers or envelopes so that a note generated with grani can have very complex behavioral changes over its duration. Parameters can control grain density, grain duration, grain envelopes, sampling rate conversion factor, spatial location of grains, etc. Almost all the important parameters have a companion xxx-spread parameter that defines a random spread around the central value defined by the xxx base parameter (and both can be envelopes).

All examples are transformations of one basic soundfile, a small gong (mp3 84K sound file). All examples are also mp3 compressed soundfiles (at 128kbits/second).

Parameters

Parameters are specified as follows. On the left, the name, type and default value. On the right an explanation of the parameter with examples of use. Allowed types include:

[num] a number
[env] an envelope (a list of x y pairs)
[array] an array of floating point numbers

All default values are asigned to special variables named grani-xxx, where xxx stands for the name of the parameter. This enables calls to the instrument to be surrounded by let statements so that local bindings can be established for default parameters.

Required parameters
start-time, duration, amplitude, file
Key parameters
grains, amp-envelope, grain-envelope, grain-envelope-end, grain-envelope-transition, grain-envelope-array-size, grain-duration, grain-duration-spread, grain-duration-limit, grain-start, grain-start-spread, grain-density, grain-density-spread, srate, srate-spread, srate-linear, srate-base, srate-error, reverse, where-to, where-bins, grain-distance, grain-distance-spread, grain-degree, grain-degree-spread, reverb-amount

Required parameters

start-time
[num]
Start time of the note in the output soundfile.

duration
[num]
Duration of the note. Actually this is not going to be the real duration of the note. This is merely a stop value for the internal grain generator. When the start of a new grain falls outside of the specified duration the grain generation stops. But of course the previous grains could very well extend beyond the specified time...

An alternative way of stopping grain generation is by specifying a discrete number of grains to be generated (see the grains key parameter below). In that case the value of the duration parameter is only used to define the duration of all note long envelopes.

amplitude
[num]
Amplitude scaler for the generated samples.

file
[path]
Path name of the input soundfile.

Here's a very simple call to grani with all the default parameters:

(with-sound()(grani 0 4 1 "small-gong.snd"))

Key parameters

grains
[num]
0
Number of grains to be generated. Normally this is "0" and the grain generator is controlled by the duration required parameter. If grains is not cero grain generation is stopped after grains grains have been generated. Note that duration is still used to calculate all note envelopes.

The following grani call generates only 10 grains. Note that the output soundfile is only 1.0 seconds long (as the default grain-density is 10 grains per second):


(with-sound(:statistics t)
  (grani 0 4 1 "small-gong.snd" :grains 10))
; grains: 10, sample ratio: 0.250000
/zap/test.snd: 
  Duration: 1.0000, Last begin time: 0.0000
  Compute time: 0.679, Compute ratio: 0.68
  OutA max amp: 0.045 (near 0.649 secs)
"/zap/test.snd"

Amplitude envelopes
amp-envelope
[env]
'(0 0 0.3 1 0.7 1 1 0)
Not much to say about the amp-envelope parameter. It is a regular clm envelope that controls the overall amplitude envelope of the note.

grain-envelope
[env|array]
'(0 0 0.3 1 0.7 1 1 0)
Defines the amplitude envelope for each individual grain. Grain envelopes are internally lookup tables so that the duration of the grain (and its envelope) can be changed on the fly in the run loop. Grain envelopes can be specified as regular clm envelopes or as an array of points. If the supplied parameter is a list an equivalent array of default size 512 is created and passed on to the make-table-lookup unit generator (the default size can be changed with the grain-envelope-array-size parameter). If the supplied parameter is an array it is directly passed to make-table-lookup.

grain-envelope-end
[env|array]
nil
Defines an alternate amplitude envelope for grains. The transition between grain-envelope and grain-envelope-end is controlled through grain-envelope-transition.

grain-envelope-transition
[env|number]
'(0 0 1 1)
If the optional grain amplitude envelope is defined (grain-envelope-end) this parameter controls the interpolation between envelopes over the duration of the note. When y=0 grain-envelope is used as the envelope for the grain; when y=1 grain-envelope-end is used as the envelope. For y between 0 and 1 a linear interpolation is calculated.

Here's a grani call in which the grain envelope goes from a triangular shape to a square shape:


(with-sound(:statistics t)
  (grani 0 4 1 "small-gong.snd" 
         :grain-start 0.11 
         :amp-envelope '(0 1 1 1) :grain-density 8
         :grain-envelope '(0 0 0.2 0.2 0.5 1 0.8 0.2 1 0)
         :grain-envelope-end '(0 0 0.01 1 0.99 1 1 0)
         :grain-envelope-transition '(0 0 0.4 1 0.8 0 1 0)))
; grains: 33, sample ratio: 0.825000
/zap/test.snd: 
  Duration: 4.0996, Last begin time: 0.0000
  Compute time: 0.597, Compute ratio: 0.15
  OutA max amp: 0.097 (near 2.036 secs)
"/zap/test.snd"
(mp3 66K)

grain-start is used to process the same input samples in each grain, the grain-density is selected so that the grains don't overlap with the default grain-duration of 0.1 seconds. Note that grain-envelope-transition goes back to "0" at x=0.8 instead of one so that the last grains do get a "0" value. The interpolation value is looked up at the beginning of each grain and is constant through the duration of the grain.

grain-envelope-array-size
[num]
512
Size of the array used to create the grain amplitude envelope table lookup unit generators. You might need higher sizes if your grain envelopes have a lot of intrincate features.

Grain duration
grain-duration
[env|num]
0.1
Defines the duration of a grain in seconds. Can be a constant value or an envelope.

grain-duration-spread
[env|num]
0.0
Defines a random duration value that is added to the duration of a grain. It is specified in seconds and can be a constant value or an envelope. The random spread is around the value defined by grain-duration, that is a grain duration spread of 1 second will result on random values between -0.5 and +0.5 being added to the current grain duration.

grain-duration-limit
[num]
0.002
Defines the minimum duration of a grain in seconds. This is to avoid cero or negative durations when high values of grain-duration-spread are used.

Here's an example of grains that grow in duration from 3ms to 300ms:


(with-sound()
  (grani 0 3 1 "small-gong.snd" 
         :grain-start 0.1 
         :amp-envelope '(0 1 1 1) :grain-density 20
         :grain-duration '(0 0.003 0.2 0.01 1 0.3)))
(mp3 52K)

And here's grain-duration-limit making the shortest grains at least 20ms long. You should hear the grain duration not changing at all during the beginning of the note:


(with-sound()
  (grani 0 3 1 "small-gong.snd" 
         :grain-start 0.1 
         :amp-envelope '(0 1 1 1) :grain-density 20
         :grain-duration '(0 0.003 0.2 0.01 1 0.3)
         :grain-duration-limit 0.02))
(mp3 52K)

Grain start in input
grain-start
[env|num]
'(0 0 1 1)
Defines the location in the input soundfile where the samples for the grain are going to be extracted from. It is a constant or an envelope that maps output soundfile time into fractions of the input soundfile duration. When y=0 the starting sample for the grain will come from the beginning of the input soundfile. When y=1 the starting sample will come from the end of the soundfile.

grain-start-spread
[env|num]
0.0
Defines a random deviation around the current value of grain-start. It is specified in fractions of the input soundfile duration. If the end of the grain falls outside of the input soundfile the grain starting time is corrected to align the last sample of the grain with the last sample of the input soundfile. If the start of the grain falls outside of the input soundfile the start of the grain is changed to start at the beginning of the input soundfile.

Here we start scanning the input soundfile at 0.1 of its duration and then at x=0.3 we keep going till at the end of the note the input samples are scanned from 0.6 of the input file duration. Note that the static part at the beginning of the note where all grains scan the same part of the input soundfile sounds pretty bad:


(with-sound()
  (grani 0 2 1 "small-gong.snd" 
         :amp-envelope '(0 1 1 1) :grain-density 40
         :grain-start '(0 0.1 0.3 0.1 1 0.6)))
(mp3 34K)

We can spice things up by adding just a tiny amount of random deviation to the grain-start parameter (of course we can use an envelope to change the deviation during the note):


(with-sound()
  (grani 0 2 1 "small-gong.snd" 
         :amp-envelope '(0 1 1 1) :grain-density 40
         :grain-start '(0 0.1 0.3 0.1 1 0.6)
         :grain-start-spread 0.01))
(mp3 34K)

Grain density
grain-density
[env|num]
10
Defines the density of the stream of grains in grains per second. Can be a constant or an envelope.

grain-density-spread
[env|num]
0
Defines a random deviation around the current value of grain-density in grains per second.

Sample rate conversion
srate
[env|num]
0.0
Defines the sample rate conversion factor to be applied to the input samples. Can be a constant value or an envelope. By default it is specified as a number of semitones to transpose the samples, positive values transpose up and negative values transpose down. The sample rate conversion factor is normalized with respect to the output sampling rate, meaning that different output sampling rates with the same input soundfile and srate will produce the same transposition.

srate-spread
[env|num]
0.0
Defines a random sample rate conversion factor deviation around the current value of srate. By default it is specified in semitones and can be a constant value or an envelope. The random spread is around the value defined by srate, that is a sample rate spread of 1 semitone will result on random values between -0.5 and +0.5 semitones being added to the current sample rate conversion factor.

srate-linear
[bool]
nil
Set to "t" if you want to define srate and srate-spread as linear envelopes (by default they are defined as semitone envelopes).

srate-base
[num]
(expt 2 (/ 12))
When srate-linear is "nil" defines the default base for the srate and srate-spread envelopes. The default value of (expt 2 (/ 12)) causes envelopes to be interpreted as semitone envelopes.

In the following example the pitch of the sound goes up from no transposition (0) to 5 semitones up during the note:


(with-sound()
  (grani 0 2.6 1 "small-gong.snd" 
         :grain-start 0.1 :grain-start-spread 0.01
         :amp-envelope '(0 1 1 1) :grain-density 40
         :srate '(0 0 0.2 0 0.6 5 1 5)))
(mp3 43K)

We can also change the scale of the envelope (ie: the measurement units we are using). Let's transpose down by an octave with the envelope expressed in octave units (:srate-base 2):


(with-sound()
  (grani 0 2.6 1 "small-gong.snd" 
         :grain-start 0.1 :grain-start-spread 0.01
         :amp-envelope '(0 1 1 1) :grain-density 40
         :srate-base 2
         :srate '(0 0 0.2 0 0.6 -1 1 -1)))
(mp3 43K)

Or we can select a linear scale for the srate envelope and transpose from 1.0 (no change) down to 0.5 (one octave down):


(with-sound()
  (grani 0 2.6 1 "small-gong.snd" 
         :grain-start 0.1 :grain-start-spread 0.01
         :amp-envelope '(0 1 1 1) :grain-density 40
         :srate-linear t
         :srate `(0 1 0.2 1 0.6 ,(expt 2 (/ 5 12)) 1 ,(expt 2 (/ 5 12)))))
(mp3 43K)

srate-error
[num]
0.01
When srate-linear is "nil" exponential envelopes are approximated by a linear segment representation. srate-error defines the error bound used during the rendering process.

reverse
[bool]
nil
If set to "t" causes the input soundfile to be read backwards.

Spatial processing
where-to
[const]
grani-to-locsig
A first cut at spliting the grain stream into different output channels according to a preselected criteria. This feature has been used to independently localize is space different parts of the same grain stream. The stream can be currently split according to the following criteria:

grani-to-locsig
sends the grains to the normal "locsig" unit generator.
grani-to-grain-duration
duration of the grain is used as the controlling parameter.
grani-to-grain-start
start time in fractions of the input soundfile duration is used as the controlling parameter.
grani-to-grain-sample-rate
sampling rate conversion factor is used as the controlling parameter. Note: the sample rate used is the resulting linear sample rate even when exponential srates are chosen (ie: "1" means no change in sample rate).
grani-to-grain-random
a random number is used as the controlling parameter.
where-bins
[list]
'()
A list that defines bins that correspond to each output channel. Each element in the list is a delimiter between two output bins or channels. If the controlling parameter falls within the limits of one bin it will be sent to the corresponding output channel, otherwise it is not sent. The enclosing with-sound has to have a corresponding :channels parameter that defines the desired number of channels.

In the following example we change the duration of the grains over the duration of the note (with a random deviation that peaks in the middle of the note) and use grain duration as the criteria for spatial location of the samples:


(with-sound(:channels 2)
  (grani 0 2 1 "small-gong.snd" 
         :grain-start 0.1 :grain-start-spread 0.01
         :amp-envelope '(0 1 1 1) :grain-density 40
         :grain-duration '(0 0.02 1 0.1) 
         :grain-duration-spread '(0 0 0.5 0.1 1 0)
         :where-to grani-to-grain-duration
         :where-bins '(0 0.05 1)))
(mp3 34K) stereo

:where-bins '(0 0.05 1) means that if a grain has a duration that is between 0 and 0.05 seconds it will be routed to the first channel of the output soundfile. If the duration is greater than 0.05 it will be sent to the second channel. As the durations have a random factor added during the transition the grains will jump back and forth between the two channels during the transition. We could also use the regular locsig panning:


(with-sound(:channels 2)
  (grani 0 2 1 "small-gong.snd" 
         :grain-start 0.1 :grain-start-spread 0.01
         :amp-envelope '(0 1 1 1) :grain-density 40
         :grain-degree '(0 0 1 90)
         :grain-degree-spread 10))
(mp3 34K) stereo

grain-distance
[env|num]
1
Defines the distance of the sound source as in the regular clm locsig unit generator.

grain-distance-spread
[env|num]
0
Defines a random deviation around the current value of grain-distance. Can be a constant or an envelope.

grain-degree
[env|num]
45
Defines the angular location of the sound source as in the regular clm locsig unit generator. For stereo files 0 represents the left channel and 90 the right channel.

grain-degree-spread
[env|num]
0
Defines a random deviation around the current value of grain-degree. Can be a constant or an envelope.

reverb-amount
[num]
0.01
Amount of signal that is sent to the reverberator.

Changes

"grani" started its life as an example for the 1996 edition of the 220a: Fundamentals of Computer Generated Sound" course. It was originally developed by me (Fernando Lopez-Lezcano) and Juan Pampin as an example of the use of granular synthesis (you can still find the original code in the Lecture 6 web page). I latter expanded it to include dynamic parameters for almost all the key parameters and tons of processing options. The new version was also used while teaching the 1998 of the 220a Course

Feb 19 1999: warning triggered by "(where)" on some lisps
added "nil" as default value of "where" to avoid warning (found by Juan Reyes, fixed by Bill)

©1998 Fernando Lopez-Lezcano. All Rights Reserved.
Created and mantained by Fernando Lopez-Lezcano, nando@ccrma.stanford.edu