Envelopes are used to define how a value evolves over time. In early synthesizers, envelopes were used to define the changes in amplitude in a sound across its duration thereby imbuing sounds characteristics such as 'percussive', or 'sustaining'. Envelopes are also commonly used to modulate filter cutoff frequencies and the frequencies of oscillators but in reality we are only limited by our imaginations in regard to what they can be used for.
Csound offers a wide array of opcodes for generating envelopes including ones which emulate the classic ADSR (attack-decay-sustain-release) envelopes found on hardware and commercial software synthesizers. A selection of these opcodes types shall be introduced here.
The simplest opcode for defining an envelope is line. line describes a single envelope segment as a straight line between a start value and an end value which has a given duration.
ares line ia, idur, ib kres line ia, idur, ib
In the following example line is used to create a simple envelope which is then used as the amplitude control of a poscil oscillator. This envelope starts with a value of 0.5 then over the course of 2 seconds descends in linear fashion to zero.
<CsoundSynthesizer> <CsOptions> -odac ; activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 aEnv line 0.5, 2, 0 ; amplitude envelope aSig poscil aEnv, 500, giSine ; audio oscillator out aSig ; audio sent to output endin </CsInstruments> <CsScore> i 1 0 2 ; instrument 1 plays a note for 2 seconds e </CsScore> </CsoundSynthesizer>
The envelope in the above example assumes that all notes played by this instrument will be 2 seconds long. In practice it is often beneficial to relate the duration of the envelope to the duration of the note (p3) in some way. In the next example the duration of the envelope is replaced with the value of p3 retrieved from the score, whatever that may be. The envelope will be stretched or contracted accordingly.
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 ; A single segment envelope. Time value defined by note duration. aEnv line 0.5, p3, 0 aSig poscil aEnv, 500, giSine ; an audio oscillator out aSig ; audio sent to output endin </CsInstruments> <CsScore> ; p1 p2 p3 i 1 0 1 i 1 2 0.2 i 1 3 4 e </CsScore> </CsoundSynthesizer>
It may not be disastrous if a envelope's duration does not match p3 and indeed there are many occasions when we want an envelope duration to be independent of p3 but we need to remain aware that if p3 is shorter than an envelope's duration then that envelope will be truncated before it is allowed to complete and if p3 is longer than an envelope's duration then the envelope will complete before the note ends (the consequences of this latter situation will be looked at in more detail later on in this section).
line (and most of Csound's envelope generators) can output either k or a-rate variables. k-rate envelopes are computationally cheaper than a-rate envelopes but in envelopes with fast moving segments quantisation can occur if they output a k-rate variable, particularly when the control rate is low, which in the case of amplitude envelopes can lead to clicking artefacts or distortion.
linseg is an elaboration of line and allows us to add an arbitrary number of segments by adding further pairs of time durations followed envelope values. Provided we always end with a value and not a duration we can make this envelope as long as we like.
In the next example a more complex amplitude envelope is employed by using the linseg opcode. This envelope is also note duration (p3) dependent but in a more elaborate way. An attack-decay stage is defined using explicitly declared time durations. A release stage is also defined with an explicitly declared duration. The sustain stage is the p3 dependent stage but to ensure that the duration of the entire envelope still adds up to p3, the explicitly defined durations of the attack, decay and release stages are subtracted from the p3 dependent sustain stage duration. For this envelope to function correctly it is important that p3 is not less than the sum of all explicitly defined envelope segment durations. If necessary, additional code could be employed to circumvent this from happening.
<CsoundSynthesizer> <CsOptions> -odac ; activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 ; a more complex amplitude envelope: ; |-attack-|-decay--|---sustain---|-release-| aEnv linseg 0, 0.01, 1, 0.1, 0.1, p3-0.21, 0.1, 0.1, 0 aSig poscil aEnv, 500, giSine out aSig endin </CsInstruments> <CsScore> i 1 0 1 i 1 2 5 e </CsScore> </CsoundSynthesizer>
The next example illustrates an approach that can be taken whenever it is required that more than one envelope segment duration be p3 dependent. This time each segment is a fraction of p3. The sum of all segments still adds up to p3 so the envelope will complete across the duration of each each note regardless of duration.
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 aEnv linseg 0, p3*0.5, 1, p3*0.5, 0 ; rising then falling envelope aSig poscil aEnv, 500, giSine out aSig endin </CsInstruments> <CsScore> ; 3 notes of different durations are played i 1 0 1 i 1 2 0.1 i 1 3 5 e </CsScore> </CsoundSynthesizer>
The next example highlights an important difference in the behaviours of line and linseg when p3 exceeds the duration of an envelope.
When a note continues beyond the end of the final value of a linseg defined envelope the final value of that envelope is held. A line defined envelope behaves differently in that instead of holding its final value it continues in the trajectory defined by its one and only segment.
This difference is illustrated in the following example. The linseg and line envelopes of instruments 1 and 2 appear to be the same but the difference in their behaviour as described above when they continue beyond the end of their final segment is clear when listening to the example.
<CsoundSynthesizer> <CsOptions> -odac ; activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 ; linseg envelope aCps linseg 300, 1, 600 ; linseg holds its last value aSig poscil 0.2, aCps, giSine out aSig endin instr 2 ; line envelope aCps line 300, 1, 600 ; line continues its trajectory aSig poscil 0.2, aCps, giSine out aSig endin </CsInstruments> <CsScore> i 1 0 5 ; linseg envelope i 2 6 5 ; line envelope e </CsScore> </CsoundSynthesizer>
expon and expseg are versions of line and linseg that instead produce envelope segments with concave exponential shapes rather than linear shapes. expon and expseg can often be more musically useful for envelopes that define amplitude or frequency as they will reflect the logarithmic nature of how these parameters are perceived. On account of the mathematics that are used to define these curves, we cannot define a value of zero at any node in the envelope and an envelope cannot cross the zero axis. If we require a value of zero we can instead provide a value very close to zero. If we still really need zero we can always subtract the offset value from the entire envelope in a subsequent line of code.
The following example illustrates the difference between line and expon when applied as amplitude envelopes.
<CsoundSynthesizer> <CsOptions> -odac ; activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 ; line envelope aEnv line 1, p3, 0 aSig poscil aEnv, 500, giSine out aSig endin instr 2 ; expon envelope aEnv expon 1, p3, 0.0001 aSig poscil aEnv, 500, giSine out aSig endin </CsInstruments> <CsScore> i 1 0 2 ; line envelope i 2 2 1 ; expon envelope e </CsScore> </CsoundSynthesizer>
The nearer our 'near-zero' values are to zero the quicker the curve will appear to reach 'zero'. In the next example smaller and smaller envelope end values are passed to the expon opcode using p4 values in the score. The percussive 'ping' sounds are perceived to be increasingly short.
<CsoundSynthesizer> <CsOptions> -odac ; activates real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1; expon envelope iEndVal = p4 ; variable 'iEndVal' retrieved from score aEnv expon 1, p3, iEndVal aSig poscil aEnv, 500, giSine out aSig endin </CsInstruments> <CsScore> ;p1 p2 p3 p4 i 1 0 1 0.001 i 1 1 1 0.000001 i 1 2 1 0.000000000000001 e </CsScore> </CsoundSynthesizer>
Note that expseg does not behave like linseg in that it will not hold its last final value if p3 exceeds its entire duration, instead it continues its curving trajectory in a manner similar to line (and expon). This could have dangerous results if used as an amplitude envelope.
When dealing with notes with an indefinite duration at the time of initiation (such as midi activated notes or score activated notes with a negative p3 value), we do not have the option of using p3 in a meaningful way. Instead we can use one of Csound's envelopes that sense the ending of a note when it arrives and adjust their behaviour according to this. The opcodes in question are linenr, linsegr, expsegr, madsr, mxadsr and envlpxr. These opcodes wait until a held note is turned off before executing their final envelope segment. To facilitate this mechanism they extend the duration of the note so that this final envelope segment can complete.
The following example uses midi input (either hardware or virtual) to activate notes. The use of the linsegr envelope means that after the short attack stage lasting 0.1 seconds, the penultimate value of 1 will be held as long as the note is sustained but as soon as the note is released the note will be extended by 0.5 seconds in order to allow the final envelope segment to decay to zero.
<CsoundSynthesizer> <CsOptions> -odac -+rtmidi=virtual -M0 ; activate real time audio and MIDI (virtual midi device) </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave instr 1 icps cpsmidi ; attack-|sustain-|-release aEnv linsegr 0, 0.01, 0.1, 0.5,0 ; envelope that senses note releases aSig poscil aEnv, icps, giSine ; audio oscillator out aSig ; audio sent to output endin </CsInstruments> <CsScore> f 0 240 ; csound performance for 4 minutes e </CsScore> </CsoundSynthesizer>
Sometimes designing our envelope shape in a function table can provide us with shapes that are not possible using Csound's envelope generating opcodes. In this case the envelope can be read from the function table using an oscillator. If the oscillator is given a frequency of 1/p3 then it will read though the envelope just once across the duration of the note.
The following example generates an amplitude envelope which uses the shape of the first half of a sine wave.
<CsoundSynthesizer> <CsOptions> -odac ; activate real time sound output </CsOptions> <CsInstruments> sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1 ; a sine wave giEnv ftgen 0, 0, 2^12, 9, 0.5, 1, 0 ; envelope shape: a half sine instr 1 ; read the envelope once during the note's duration: aEnv poscil 1, 1/p3, giEnv aSig poscil aEnv, 500, giSine ; audio oscillator out aSig ; audio sent to output endin </CsInstruments> <CsScore> ; 7 notes, increasingly short i 1 0 2 i 1 2 1 i 1 3 0.5 i 1 4 0.25 i 1 5 0.125 i 1 6 0.0625 i 1 7 0.03125 f 0 7.1 e </CsScore> </CsoundSynthesizer>
lpshold, loopseg and looptseg - A Csound TB303
These opcodes generate envelopes which are looped at a rate corresponding to a defined frequency. What they each do could also be accomplished using the 'envelope from table' technique outlined in an earlier example but these opcodes provide the added convenience of encapsulating all the required code in one line without the need for phasors, tables and ftgens. Furthermore all of the input arguments for these opcodes can be modulated at k-rate.
lpshold generates an envelope with in which each break point is held constant until a new break point is encountered. The resulting envelope will contain horizontal line segments. In our example this opcode will be used to generate the notes (as MIDI note numbers) for a looping bassline in the fashion of a Roland TB303. Because the duration of the entire envelope is wholly dependent upon the frequency with which the envelope repeats - in fact it is the reciprocal of the frequency – values for the durations of individual envelope segments are not defining times in seconds but instead represent proportions of the entire envelope duration. The values given for all these segments do not need to add up to any specific value as Csound rescales the proportionality according to the sum of all segment durations. You might find it convenient to contrive to have them all add up to 1, or to 100 – either is equally valid. The other looping envelope opcodes discussed here use the same method for defining segment durations.
loopseg allows us to define a looping envelope with linear segments. In this example it is used to define the amplitude envelope for each individual note. Take note that whereas the lpshold envelope used to define the pitches of the melody repeats once per phrase, the amplitude envelope repeats once for each note of the melody therefore its frequency is 16 times that of the melody envelope (there are 16 notes in our melodic phrase).
looptseg is an elaboration of loopseg in that is allows us to define the shape of each segment individually, whether that be convex, linear or concave. This aspect is defined using the 'type' parameters. A 'type' value of 0 denotes a linear segement, a positive value denotes a convex segment with higher positive values resulting in increasingly convex curves. Negative values denote concave segments with increasing negative values resulting in increasingly concave curves. In this example looptseg is used to define a filter envelope which, like the amplitude envelope, repeats for every note. The addition of the 'type' parameter allows us to modulate the sharpness of the decay of the filter envelope. This is a crucial element of the TB303 design.
Other crucial features of this instrument such as 'note on/off' and 'hold' for each step are also implemented using lpshold.
A number of the input parameters of this example are modulated automatically using the randomi opcodes in order to keep it interesting. It is suggested that these modulations could be replaced by linkages to other controls such as CsoundQt widgets, FLTK widgets or MIDI controllers. Suggested ranges for each of these values are given in the .csd.EXAMPLE 05A10_lpshold_loopseg.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ; Example by Iain McCurdy sr = 44100 ksmps = 4 nchnls = 1 0dbfs = 1 seed 0; seed random number generators from system clock instr 1; Bassline instrument kTempo = 90 ; tempo in beats per minute kCfBase randomi 1,4, 0.2 ; base filter frequency (oct format) kCfEnv randomi 0,4,0.2 ; filter envelope depth kRes randomi 0.5,0.9,0.2 ; filter resonance kVol = 0.5 ; volume control kDecay randomi -10,10,0.2 ; decay shape of the filter. kWaveform = 0 ; oscillator waveform. 0=sawtooth 2=square kDist randomi 0,1,0.1 ; amount of distortion kPhFreq = kTempo/240 ; freq. to repeat the entire phrase kBtFreq = (kTempo)/15 ; frequency of each 1/16th note ; -- Envelopes with held segments -- ; The first value of each pair defines the relative duration of that segment, ; the second, the value itself. ; Note numbers (kNum) are defined as MIDI note numbers. ; Note On/Off (kOn) and hold (kHold) are defined as on/off switches, 1 or zero ; note:1 2 3 4 5 6 7 8 ; 9 10 11 12 13 14 15 16 0 kNum lpshold kPhFreq, 0, 0,40, 1,42, 1,50, 1,49, 1,60, 1,54, 1,39, 1,40, \ 1,46, 1,36, 1,40, 1,46, 1,50, 1,56, 1,44, 1,47,1 kOn lpshold kPhFreq, 0, 0,1, 1,1, 1,1, 1,1, 1,1, 1,1, 1,0, 1,1, \ 1,1, 1,1, 1,1, 1,1, 1,1, 1,1, 1,0, 1,1, 1 kHold lpshold kPhFreq, 0, 0,0, 1,1, 1,1, 1,0, 1,0, 1,0, 1,0, 1,1, \ 1,0, 1,0, 1,1, 1,1, 1,1, 1,1, 1,0, 1,0, 1 kHold vdel_k kHold, 1/kBtFreq, 1 ; offset hold by 1/2 note duration kNum portk kNum, (0.01*kHold) ; apply portamento to pitch changes ; if note is not held: no portamento kCps = cpsmidinn(kNum) ; convert note number to cps kOct = octcps(kCps) ; convert cps to oct format ; amplitude envelope attack sustain decay gap kAmpEnv loopseg kBtFreq, 0, 0, 0,0.1, 1, 55/kTempo, 1, 0.1,0, 5/kTempo,0,0 kAmpEnv = (kHold=0?kAmpEnv:1) ; if a held note, ignore envelope kAmpEnv port kAmpEnv,0.001 ; filter envelope kCfOct looptseg kBtFreq,0,0,kCfBase+kCfEnv+kOct,kDecay,1,kCfBase+kOct ; if hold is off, use filter envelope, otherwise use steady state value: kCfOct = (kHold=0?kCfOct:kCfBase+kOct) kCfOct limit kCfOct, 4, 14 ; limit the cutoff frequency (oct format) aSig vco2 0.4, kCps, i(kWaveform)*2, 0.5 ; VCO-style oscillator aFilt lpf18 aSig, cpsoct(kCfOct), kRes, (kDist^2)*10 ; filter audio aSig balance aFilt,aSig ; balance levels kOn port kOn, 0.006 ; smooth on/off switching ; audio sent to output, apply amp. envelope, ; volume control and note On/Off status aAmpEnv interp kAmpEnv*kOn*kVol out aSig * aAmpEnv endin </CsInstruments> <CsScore> i 1 0 3600 ; instr 1 plays for 1 hour e </CsScore> </CsoundSynthesizer>
Hopefully this final example has provided some idea as to the extend of parameters that can be controlled using envelopes and also an allusion to their importance in the generation of musical 'gesture'.