Sculpting Gestural Space with Envelopes Part 1
Introduction
Use of voltage control amplitude envelopes became standard in the 1960s after they were implemented by Moog on the evolving Moog Synthesizer (1964-1965). In the digital domain the envelope realizes its true potential as a variable function of change over time more akin to the use of lines (stochastic calculus, probability curves, ruled surface) by Xenakis where they were used to generate notes, control the density of events, and more. (See here for an example of using lines to inform frequency change.) This post is part 1 of (at least) 2 parts where we will explore the use of envelopes to generate compelling musical gestures. Below, I will first introduce the Envelope, what it is, and how it is implemented in SuperCollider (what is SuperCollider? — How do I use it? — Is there a PDF Introduction to the language?).
The basic idea of the envelope is this: each segment or node of an envelope is a value-time pair. In the times indicated for each node, you interpolate between the values you supply. A simple example is a ramp from 0 to 1 and back again over one second (0.5 seconds between 0 and 1, and 1 and 0.)
// Ex 1a -- Linear Ramp (up-down) plot
Env([0,1,0], [0.5,0.5]).plot
Digital Envelopes
Envelopes existed in the digital domain at least as far back as MUSIC IV. A LINEN Ugen was described in the Music IV Programmer’s Manual (1963) that “produces linear attack and decay envelopes for notes where the attack and decay times are independent of the note duration.” The MUSIC N series of languages are the progenitors of almost all modern audio programming languages and the envelope specifications in MUSIC IV and V were adopted by those later languages. In later languages including Csound and SuperCollider, arbitrary numbers of values and times may be used to specify an envelope of any shape.
Envelopes in SuperCollider
As in other languages SuperCollider provides many envelope types ranging from Line (a two value, one-time linear ramp) to Env which can have an arbitrary number of value-time nodes. You can browse the many classes available using the online documentation (available from within the IDE as well).
http://doc.sccode.org/Browse.html#Envelopes
http://doc.sccode.org/Browse.html#Control
http://doc.sccode.org/Browse.html#UGens>Envelopes
Env
Env is an object that can be used both client and server side. That means you can use it directly in the language to inform the shape of events over time using Env.at(time) to get the interpolated value at that time. You can also use it in Synths with Env.kr or by using in within the EnvGen class.
The arguments to Env are: Env.new(levels, times, curve)
Levels and times can be any floating point values. For curve we select the shape of the interpolated values between the value nodes proper. Here’s a quick illustration with four envelopes each with the same value-time, but with a different curves.
[ Env([0,1], 1, 'lin'), Env([0,1], 1, 'sine'), Env([0,1], 1, 4), Env([0,1], 1, -4) ].plot;
Graphical Editing — Edit arrays using Plotter
Being able to specify your envelope value and time arrays in code gives you the ability to generate data algorithmically. However, there are times when it’s advantageous to be able to draw data graphically. There are objects in SuperCollider that allow this. Below are examples for Plotter and EnvelopeView.
Plotter is simpler to edit, so I’ll start with it.
a = (0..40).plot; // create a Plotter object
a.editMode = true; // now edit the data by clicking into the plot..
a.value; // print the value
b = a.value.normalize(0.0, 1.0); // assign the array to a new variable with amplitude ranges of 0.0-1.0
b.plot;
play { Mix(SinOsc.ar(Array.fill(b.size, {|i| i = i + 1; 75 * i}), mul: b * 0.05)) }; // b supplies amplitudes for the partials here
Running the above code line by line.
- Line 1: Creates an instance of Plotter with values linearly distributed from 0 to 40.
- Line 2: Makes the Plotter object editable.
- Edit the plotter!
- Line 3: prints the array to the post window.
- Line 4: assigns the array value to b and changes the range to 0.0 to 1.0, suitable for amplitudes in SC3.
- Line 5: Plots b, should match a with the range changed.
- Line 6: creates a temporary synth object using the amplitudes for a 40-partial additive synthesis instrument.
If you wanted to use Plotter to create envelope data, you would need to create two Plotter objects, one each for values and times. Then you would use these in your envelope.
a = (0..10).plot("values").editMode = true; // values
b = (0..9).plot("times").editMode = true; // times -- one less than values
c = Env(a.(), b.()).plot(name: "Resulting Envelope"); // use the results in your Env
You will also probably want to save your altered array data in plain text as your changes are lost once the plotter is closed. Simply call a.value or b.value and copy the text from the post window into the text editor.
Graphical Editing — Edit arrays using EnvelopeView
EnvelopeView provides another, if fiddly, way of editing array data. With EnvelopeView you are editing x/y coordinate nodes. This is the primary advantage – that you can see (and set) the values relative to each other. There are some limitations, however, that require some extra work:
1) The arrays can only contain floats between 0.0 and 1.0
2) There’s no direct way to convert an EnvelopeView to an Env object.
Both of these are annoying, but not fatal. For 1) above, you merely work within your envelope as a sort of universal space, moving nodes as you see fit with the understanding that the values and times can be converted to any ranges you want later on. For 2) you just need a line of code to convert the x array to times for your Env object. (See below)
(
w = Window("EnvelopeView", Rect(150 , Window.screenBounds.height - 250, 1000, 500)).front;
w.view.decorator = FlowLayout(w.view.bounds);
b = EnvelopeView(w, Rect(0, 0, 995, 490))
.value_([Array.fill(10, {|i| i = i + rrand(0.01, 0.1)}).normalize,Array.fill(10, {rrand(0.001, 1.0)}).normalize ])
.grid_(Point(0.1, 0.1))
.gridOn_(true)
.drawLines_(true)
.selectionColor_(Color.red)
.drawRects_(true)
.step_(0.005)
.action_({arg b; [b.index, b.value].postln})
.thumbSize_(10)
.keepHorizontalOrder_(true)
;
w.front;
)
The above will generate a View something like this:
Yours will be different because the above code generates random points. Once the view is open, however, you can grab any point and move it around. I moved a couple points to make the shapes more severe:
Now that’s done, I can use this as an envelope object, converting the ranges as-needed. The d = Array… line below converts the x coordinates to times. The way it works is to simply subtract Tn (time now) from Tn+1 (next time value). This results in an array that is one index smaller than the values array, which is what we want.
// Convert the array from X coordinates to times:
c = b.();
d = Array.fill(c[0].size -1, {|i| c[0][i + 1] - c[0][i] });
Env(c[1], d).plot;
The results are indistinguishable after the conversion:
You can now convert the 0.0-1.0 values (and times) to whatever you like:
// convert to desired ranges (values, times) as needed
play { SinOsc.ar(Env(c[1].linlin(0.0, 1.0, 400, 800), d.linlin(0.0, 1.0, 0.0, 5.0)).kr) }
Here, I’m just using a simple linear map to convert 0.0-1.0 ranges to 400-800 for the frequency and to change the duration of the event from 1 second to 5 seconds. Here’s how it sounds:
As noted above, using EnvelopeView is definitely more labor intensive (and less flexible) than simply using Plotter, but with the benefit of being able to see and adjust your values and times in the same view. Regardless of how you want to create your envelopes, you now have a couple options for graphical editing in addition to coding your envelope shapes.
In Sculpting Gestural Space with Envelopes Part 2 we will explore how to put some of this knowledge to work and create larger, more complex and interesting sounds and textures. Please use the Subscribe page and enter your email address to be alerted when that post (and any new content) is created.