Envelopes are used to define the change in a value 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'. Of course envelopes can be applied to any parameter and not just amplitude.
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, which represent the basic 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.
EXAMPLE 05A01.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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.
EXAMPLE 05A02.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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, p3, 0; single segment envelope. time value defined by note duration 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 quantization 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. A 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.
EXAMPLE 05A03.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy sr = 44100 ksmps = 32 nchnls = 1 0dbfs = 1 giSine ftgen 0, 0, 2^12, 10, 1; a sine wave instr 1 ; |-attack-|-decay--|---sustain---|-release-| aEnv linseg 0, 0.01, 1, 0.1, 0.1, p3-0.21, 0.1, 0.1, 0; a more complex amplitude envelope 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.
EXAMPLE 05A04.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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 a trajectory defined by the last 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.
Note that information given in the Csound Manual in regard to this matter is incorrect at the time of writing.
EXAMPLE 05A05.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdysr = 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 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 is 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.
EXAMPLE 05A06.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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 more concave the segment curve will be. 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.
EXAMPLE 05A07.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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.
EXAMPLE 05A08.csd
<CsoundSynthesizer> <CsOptions> -odac -+rtmidi=virtual -M0; activates real time sound output and virtual midi device </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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, 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; extend 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 and 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 is the shape of the first half of a sine wave.
EXAMPLE 05A09.csd
<CsoundSynthesizer> <CsOptions> -odac ;activates real time sound output </CsOptions> <CsInstruments> ;Example by Iain McCurdy 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; the envelope shape: a half sine instr 1 aEnv poscil 1, 1/p3, giEnv; read the envelope once during the note 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>
The next example introduces three of Csound's looping opcodes, lpshold, loopseg and looptseg.
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 provides the added convenience of encapsulating all the required code in one line without the need of any function tables. 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 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 – values for the durations of individual envelope segments are defining times in seconds but 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 segements. In this example it is used to define the amplitude envelope of 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 of 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. Note that looptseg is only available in Csound 5.12 or later.
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 QuteCsound widgets, FLTK widgets or MIDI controllers. Suggested ranges for each of these values are given in the .csd.
The filter used in this example is moogladder, an excellent implementation of the classic moog filter. This filter is however rather computationally expensive, if you encounter problems running this example in realtime you might like to swap it for the moogvcf opcode which is provided as an alternative in a commented out line.
EXAMPLE 05A10.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 second kCfBase randomi 1,4,0.2; base filter cutoff frequency described in octaves above the current pitch. Values should be greater than 0 up to about 8 kCfEnv randomi 0,4,0.2; filter envelope depth. Values probably in the range 0 - 4 although negative numbers could be used for special effects kRes randomi 0.5,0.9,0.2; filter resonance. Suggested range 0 - 0.99 kVol = 0.5; volume control. Suggested range 0 - 1 kDecay randomi -10,10,0.2; decay shape of the filter. Suggested range -10 to +10. Zero=linear, negative=increasingly_concave, positive=increasingly_convex kWaveform = 0;waveform of the audio oscillator. 0=sawtooth 2=square kDist randomi 0,0.8,0.1; amount of distortion. Suggested range 0 - 1 ;read in phrase event widgets - use a macro to save typing kPhFreq = kTempo/240; frequency with which to repeat the entire phrase kBtFreq = (kTempo)/15; frequency of each 1/16th note ; the first value of each pair defines the relative duration of that segment (just leave these as they are unless you want to create quirky rhythmic variations) ; 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 ;envelopes with held segments note:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 DUMMY 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,45; need an extra 'dummy' value 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,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,0; need an extra 'dummy' value 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 will be applied kCps = cpsmidinn(kNum) kOct = octcps(kCps) ; amplitude envelope ; attack sustain decay gap kAmpEnv loopseg kBtFreq, 0, 0, 0,0.1, 1, 55/kTempo, 1, 0.1,0, 5/kTempo,0 ; sustain segment duration (and therefore attack and decay segment durations) are dependent upon tempo kAmpEnv = (kHold=0?kAmpEnv:1); if hold is off, use amplitude envelope, otherwise use constant value ; filter envelope kCfOct looptseg kBtFreq, 0, 0, kCfBase+kCfEnv+kOct, kDecay, 1, kCfBase+kOct kCfOct = (kHold=0?kCfOct:kCfBase+kOct); if hold is off, use filter envelope, otherwise use steady state value kCfOct limit kCfOct, 4, 14; limit the cutoff frequency to be within sensible limits ;kCfOct port kCfOct, 0.05; smooth the cutoff frequency envelope with portamento kWavTrig changed kWaveform; generate a 'bang' if waveform selector changes if kWavTrig=1 then; if a 'bang' has been generated... reinit REINIT_VCO; begin a reinitialization pass from the label 'REINIT_VCO' endif REINIT_VCO:; a label aSig vco2 0.4, kCps, i(kWaveform)*2, 0.5; generate audio using VCO oscillator rireturn; return from initialization pass to performance passes aSig moogladder aSig, cpsoct(kCfOct), kRes; filter audio ;aSig moogvcf aSig, cpsoct(kCfOct), kRes ;use moogvcf is CPU is struggling with moogladder ; distortion iSclLimit ftgentmp 0, 0, 1024, -16, 1, 1024, -8, 0.01; rescaling curve for clip 'limit' parameter iSclGain ftgentmp 0, 0, 1024, -16, 1, 1024, 4, 10; rescaling curve for gain compensation kLimit table kDist, iSclLimit, 1; read Limit value from rescaling curve kGain table kDist, iSclGain, 1; read Gain value from rescaling curve kTrigDist changed kLimit; if limit value changes generate a 'bang' if kTrigDist=1 then; if a 'bang' has been generated... reinit REINIT_CLIP; begin a reinitialization pass from label 'REINIT_CLIP' endif REINIT_CLIP: aSig clip aSig, 0, i(kLimit); clip distort audio signal rireturn aSig = aSig * kGain; compensate for gain loss from 'clip' processing kOn port kOn, 0.006 out aSig * kAmpEnv * kVol * kOn; audio sent to output, apply amp. envelope, volume control and note On/Off status endin </CsInstruments> <CsScore> i 1 0 3600 e </CsScore> </CsoundSynthesizer>