How does Env.shapeNumber work?

I expected these two lines to produce similar envelopes, but they differ…significantly. Perhaps someone can help me understand why?

Env([0,1,0],[0.01,1],Env.shapeNumber(\cubed)).plot;

Env([0,1,0],[0.01,1],\cubed).plot;

I’d like to be able to pass an integer to a Synth arg in order to get the curves normally accessed by key (\sine, \welch, etc.)…if possible!

Hello!
They cannot be identical because
Env.shapeNumber(\cubed)
returns a number 7 as an integer. The third argument of Env.new is the curve which accepts a Symbol, Float, or an Array of those. Since the number 7 is an integer, it will internally be interpreted as a float, i.e. as a curvature value for all segments. 0 means linear, positive and negative numbers curve the segment up and down.
Thus, the first code

Env([0,1,0],[0.01,1],Env.shapeNumber(\cubed)).plot;

is same as the following code:

Env([0, 1, 0], [0.01, 1], 7).plot;

In the help document of VarLag, there are some examples with Env.shapeNumber.
https://doc.sccode.org/Classes/VarLag.html

prko is correct that you can’t use the shape number in an Env definition directly, because it will be interpreted as a curvature value.

The shape number is embedded into the envelope’s array representation (to which it’s converted when it’s used in an EnvGen).

To pass in the shape number, then, AFAIK you have to hack the array representation. The array starts with four values: initial level, number of segments, release node, loop node. You don’t want to touch these. Starting with index 4, then you have four values for each segment: target level, time, shape number, curvature. Overwrite these with synth controls.

This example assumes the same shape for all segments.

(
SynthDef(\envtest, { |out, shape = 0, curve = 0, time = 1, amp = 0.1|
	// \lin is a dummy curvature -- it will be replaced
	var env = Env([0, 1, 0], [0.5, 0.5], \lin);
	var eg;
	
	var envArray = env.asArray;
	envArray[6, 10 ..] = shape;
	envArray[7, 11 ..] = curve;
	
	eg = EnvGen.kr(envArray, timeScale: time, doneAction: 2);
	
	Out.ar(out, (SinOsc.ar(440) * (eg * amp)).dup);
}).add;
)

Synth(\envtest, [shape: Env.shapeNumber(\lin)]);
Synth(\envtest, [shape: Env.shapeNumber(\sin)]);
Synth(\envtest, [shape: Env.shapeNumber(-4), curve: -4]);

hjh

3 Likes

Thanks @prko and @jamshark70 for your replies - accessing the envArray should work for my purposes.

Just to clarify, what’s the distinction between shape and curvature?

1 Like

Yes, the distinction between shape and curvature is difficult to grasp.

Here’s some of the code from Env.sc:

prAsArray {
		var contents, size;
		var levelArray = levels.asUGenInput;
		var timeArray = times.asUGenInput;
		var curvesArray = curves.asArray.asUGenInput;

		size = times.size;
		contents = Array.new((size + 1) * 4);
		contents.add(levelArray.at(0));
		contents.add(size);
		contents.add(releaseNode.asUGenInput ? -99);
		contents.add(loopNode.asUGenInput ? -99);

		size.do { arg i;
			contents.add(levelArray.at(i+1));
			contents.add(timeArray.at(i));
			contents.add(this.class.shapeNumber(curvesArray.wrapAt(i)));
			contents.add(this.curveValue(curvesArray.wrapAt(i)));
		};

		^contents.flop;
	}

The last two lines make the distinction, though rather vaguely.

curveValue was written for & only used in .asArray above:

curveValue { arg curve;
		^if(curve.isSequenceableCollection) {
			curve.collect { |x|
				if(x.isValidUGenInput) { x } { 0 }
			}
		} {
			if(curve.isValidUGenInput) { curve } { 0 }
		}
	}

It’s only purpose is to replace invalid Ugen arguments with zero values.

It’s unclear how .curveValue fits into the bigger picture of the envelope, and how it differs from typical .curves (shapes) such as \linear, \exponential, \welch, etc.

Shape is \lin, \exp, \sin etc. (or one other shape which isn’t named, for a curved line).

Curvature is a parameter of that unnamed shape, which specifies how far away from a straight line to bend.

hjh

1 Like

Can you send an example?

Different segment shapes and curve values are plotted here – see the last 3 in the first section for curvature.

http://doc.sccode.org/Classes/Env.html#examples

hjh

So, a major element of the distinction is when float or integer values (or an array of them) are used instead of the traditional values found in Env.shapeNames?

\sine would be a shape, and -4 would be a curve

Thank you as always for answering where no one else can.

I’ve come to the understanding that either a shape value such as \sine or \welch can be used, or a curve value which is an instance of magnitude, and they can’t be used simultaneously in the same envelope segment, though they can be used in the same envelope.

Rather perplexing from the start (for myself included) was this:

Also rather strange, on my end running Env.shapeNumber with any number returns an integer 5.

For .asArray, when the contents are being made, the last two lines are still a source of slight confusion.

size.do { arg i;
			contents.add(levelArray.at(i+1));
			contents.add(timeArray.at(i));
			contents.add(this.class.shapeNumber(curvesArray.wrapAt(i)));
			contents.add(this.curveValue(curvesArray.wrapAt(i)));
		};

If these values are treated so distinctly, then why is .shapeNumber & curveValue used on every value in the curvesArray?

And why are both added to contents for each envelope segment, when they can never be used simultaneously?

To use one of the examples from the documentation:

Env.new([0, 1, 0.3, 0.8, 0], [2, 3, 1, 4], 2).asArray

Returns:

// [ 0, 4, -99, -99, 1, 2, 5, 2, 0.3, 3, 5, 2, 0.8, 1, 5, 2, 0, 4, 5, 2 ]

The values can be followed just as seen in the source code:

contents.add(levelArray.at(0));
contents.add(size);
contents.add(releaseNode.asUGenInput ? -99);
contents.add(loopNode.asUGenInput ? -99);

size.do { arg i;
	contents.add(levelArray.at(i+1));
	contents.add(timeArray.at(i));
	contents.add(this.class.shapeNumber(curvesArray.wrapAt(i)));
	contents.add(this.curveValue(curvesArray.wrapAt(i)));
};

The values 5 and 2 can be seen representing each segment for the curvesArray

Env.shapeNumber will return 5 when given any number, and the segments have a true curve value of 2.

-5 to 5 is the full range of effective curve values, with the exception of 0, which has the same effect on curve as the default value, \lin.

@jamshark70’s original example works because he’s passing the envelope’s array representation to the EnvGen's envelope argument.

This can be seen in the source for EnvGen:

EnvGen : UGen { // envelope generator
	*ar { arg envelope, gate = 1.0, levelScale = 1.0, levelBias = 0.0, timeScale = 1.0, doneAction = 0;
		envelope = this.convertEnv(envelope);
		^this.multiNewList(['audio', gate, levelScale, levelBias, timeScale, doneAction, envelope])
	}

EnvGen first calls convertEnv on it’s envelope:

	*convertEnv { arg env;
		if(env.isSequenceableCollection) {
			if (env.shape.size == 1) {
				^env.reference
			} {
				// multi-channel envelope
				^env.collect(_.reference)
			};
		};
		^env.asMultichannelArray.collect(_.reference).unbubble
	}

The following both return the same result:

EnvGen.convertEnv([ 0, 2, -99, -99, 1.0, 0.01, 5, -4.0, 0, 1.0, 5, -4.0 ]);

EnvGen.convertEnv(Env.perc);

As a final note:

Shape and curve CAN NOT be used to affect each other in the same envelope segment.
A shapeNumber of 5 is the true missing or unnamed shape.
(step -> 0)
(linear -> 1)
(exponential -> 2)
(sine -> 3)
(wel -> 4)
(sqr -> 6)
(cubed -> 7)
(hold -> 8) 
In the array passed to EnvGen, the shape number must be 5 for the curve # to have an effect.
AppClock.sched(0, r{
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 5, -4, 0.3, 3, 5, -1, 0.8, 1, 5, 1, 0, 4, 5, 4 ])}.plot(10);
	//                                 ~   #          ~   #          ~  #        ~  #
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 0, -4, 0.3, 3, 0, -1, 0.8, 1, 0, 1, 0, 4, 0, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 1, -4, 0.3, 3, 1, -1, 0.8, 1, 1, 1, 0, 4, 1, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 2, -4, 0.3, 3, 2, -1, 0.8, 1, 2, 1, 0, 4, 2, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 3, -4, 0.3, 3, 3, -1, 0.8, 1, 3, 1, 0, 4, 3, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 4, -4, 0.3, 3, 4, -1, 0.8, 1, 4, 1, 0, 4, 4, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 6, -4, 0.3, 3, 6, -1, 0.8, 1, 6, 1, 0, 4, 6, 4 ])}.plot(10);
	{EnvGen.ar([ 0, 4, -99, -99, 1, 2, 7, -4, 0.3, 3, 7, -1, 0.8, 1, 7, 1, 0, 4, 7, 4 ])}.plot(10);
})

The following lines are useful for printing Env.shapeNames:

Env.shapeNames.keys(SortedList).printAll
cub
cubed
exp
exponential
hold
lin
linear
sin
sine
sqr
squared
step
wel
welch

Env.shapeNames.keys(SortedList).collect{ |n| n -> shapeNumber(Env, n) }.printAll
(cub -> 7)
(cubed -> 7)
(exp -> 2)
(exponential -> 2)
(hold -> 8)
(lin -> 1)
(linear -> 1)
(sin -> 3)
(sine -> 3)
(sqr -> 6)
(squared -> 6)
(step -> 0)
(wel -> 4)
(welch -> 4)

Env.shapeNames.invert.keys(SortedList).collect{ |n| Env.shapeNames.findKeyForValue(n) -> n }.printAll
(step -> 0)
(linear -> 1)
(exponential -> 2)
(sine -> 3)
(wel -> 4)
(sqr -> 6)
(cubed -> 7)
(hold -> 8)