Equivalent to the Max <line> object?

Hello,

Another newbie question… I’m looking for a simple way to have an equivalent of the “line” object in Max. You don’t need to specify the start, just the end point and ramp time. For example, I’d like to send a transition time to a new frequency or amplitude using .set in the following little program:

(
SynthDef(\noisepulse,{
	arg freq = 2, amp = 0.5;
	var sig;
	sig = LFNoise0.ar(freq: freq ! 2, mul: amp);
	Out.ar(0, sig)}).add)

x = Synth(\noisepulse, [\freq, 5, \amp, 0.5]);

x.set(\freq, (15, 5)); //Example (\freq, (newvalue, time to reach it))

I did read about a similar subject, but I couldn’t make it work…

I think I’d do it like this:

SynthDef(\krLine, { |out, value = 0, time = 1|
    var trig = Changed.kr(value);
    var eg = EnvGen.kr(Env([value, value], [time], \lin), trig);
    Out.kr(out, eg);
}).add;

// then, if x is a Synth(\krLine) ...

x.set(\value, 1, \time, 0.5);  // half second ramp to 1

I didn’t test but it should work.

(Well… that’s [line~] actually, not [line].)

hjh

I also always wanted this feature, so I looked into it but it seems this is not really possible without modifying the code of the SynthDef, see No way to see value of control mapped to a bus · Issue #1280 · supercollider/supercollider · GitHub

But by making use of the ProxySpace “dialect” (ProxySpace | SuperCollider 3.13.0 Help), this can become really easy

// get into a new proxy space
p = ProxySpace.push;

// definy a variable which will store our freq
~freq = 200.0;

// start a synth which uses ~freq
~mySynth = {SinOsc.ar(~freq).dup * 0.2 * SinOscFB.kr(1.3, 1.3)};
~mySynth.play(fadeTime: 10.0);

// now set glide time for freq
~freq.fadeTime = 10;
// sky is the limit
~freq = 10000;

// change time again
~freq.fadeTime = 4;
~freq = 400;

// fade out
~mySynth.stop(fadeTime: 10.0);

// out of proxy space
p.pop;

I thought my suggestion was pretty easy. I think it’s highly overstated to say that it isn’t possible, when it’s actually quite straightforward if EnvGen is being used properly.

hjh

Thank you for your suggestion. Sorry, I’m not yet comfortable with the basics of programming in SC. How do you connect your proposed SynthDef to the parameter of another SynthDef, for example the frequency or amplitude in my example?

If I try to integrate your idea into a SynthDef, it won’t work…

(
SynthDef(\noisepulse_ramp,{
	arg freq = 2, amp = 0.5, time = 1;
	var sig, trig;
	trig = Changed.kr(freq);
	freq = EnvGen.kr(Env([freq, freq], [time], \lin), trig);
	sig = LFNoise0.ar(freq: freq ! 2, mul: amp);
	Out.ar(0, sig)}).add);

x = Synth(\noisepulse, [\freq, 3,  \amp, 0.5]);
x.set(\freq, 10, \time, 10);

Thank you dscheiba. I still don’t understand what it means to go through a proxy, especially when you multiply SynthDef containing numerous parameters that would be subject to line, while being controlled by Patterns or Routines… For the moment, this is beyond my understanding… I’ll look into it.

Sorry, Typo, it actually works well!


(
SynthDef(\noisepulse_ramp,{
	arg freq = 2, amp = 0.5, time = 1;
	var sig, trig;
	trig = Changed.kr(freq);
	freq = EnvGen.kr(Env([freq, freq], [time], \lin), trig);
	sig = LFNoise0.ar(freq: freq ! 2, mul: amp);
	Out.ar(0, sig)}).add);

x = Synth(\noisepulse_ramp, [\freq, 10,  \amp, 0.5]);

x.set(\freq, 1, \time, 10)

Just about to write the same but you were quicker.

Maybe I’ve misunderstood your intention but there are some other ugens that could simplify the code a bit: Lag, Ramp or VarLag:

(
SynthDef(\noisepulse_ramp,{
	arg freq = 2, amp = 0.5, time = 3;
	var sig = LFNoise0.ar(Lag.kr(freq ! 2, time), mul: amp);
	Out.ar(0, sig)}).add
);

x = Synth(\noisepulse_ramp, [\freq, 10,  \amp, 0.5]);
x.set(\freq, 1, \time, 10);

If you are working with patterns maybe Pseg | SuperCollider 3.13.0 Help can be a help?

Pdef(\test, Pbind(
	\freq, Pseg([1000, 4000], durs: 10, curves: \exp),
	\dur, 0.15,
)).play;

Note that this is more like a “discrete glissando” - once the Synth started it will stay on this frequency.


If re-writing/adjusting the SynthDef is an option, there is also the UGen VarLag which moves to a certain value with a given time, see VarLag | SuperCollider 3.13.0 Help

(
SynthDef(\foo, {
	var sig = SinOsc.ar(
		freq: VarLag.kr(
			in: \freq.kr(200.0),
			time: \time.kr(4.0),
			// frequency domain works in an exp manner
			warp: \exp
		)
	// mulitply a SinOscFB to get some sense of time in seconds
	) * SinOscFB.kr(freq: 1.0, feedback: [1.3, 1.4]);
	Out.ar(\out.kr(0), sig * \amp.kr(0.2));
}).add;
)

x = Synth(\foo);

// jump to 400 hz
x.set(\freq, 400);

// change fade time
(
// move to 20 hz in 10 secs
x.set(\time, 10.0);
x.set(\freq, 20.0);
)

Sorry, I wasn’t saying that your solution is not valid, I just said that it is not possible to add “moving to a certain value in n seconds” on arbitrary parameters of an existing Node without changing the SynthDef because it is not always possible to get the current state of a parameter, but knowing the state is mandatory because I need to say “move from x to y” in some way.

edit: I first thought that you made a mistake in your snippet by saying “go from freq to freq in time x” - can you explain why this still morphs in this case? It seems like some updating procedures of Env are really counter-intuitive :confused:

Because I thought it doesn’t work I adapted it to use a TrigDelay with a Latch to update its value internally.

(
SynthDef(\freqWrap, { |freq=400, time = 0.1|
	// add an init spike
	var change = Changed.kr(freq) + \init.tr(1.0);
	var freqEnv = EnvGen.kr(Env(
		// prepend a 0 stage so it can be re-triggered
		// we delay the pick up of the target value as init value
		// by theamount of time the envelope takes
		levels: [0.0, Latch.kr(freq, trig: TDelay.kr(change, dur: time)), freq],
		times: [0.0, time],
		curve: \exp,
		// debug value by uisng poll
	), gate: change).poll(trig: 10, label: "freq");
	var sig = SinOsc.ar(freqEnv) * SinOscFB.kr(freq: 1.0, feedback: [1.3, 1.4]);
	Out.ar(\out.kr(0), sig * \amp.kr(0.2));
}).add;
)

// this takes
x = Synth(\freqWrap)
// synth needs to run at least 
x.set(\freq, 100, \time, 4.5);
x.set(\freq, 400, \time, 2.5);
x.set(\freq, 200, \time, 2.5);

// this makes mistakes if the envelope has not been executed fully

Sure. An envelope segment is always defined as movement toward a target value from the envelope generator’s current value, in some amount of time, according to a curve setting. Notice that this definition is not “from a start value toward a target value” – an envelope segment always starts with the envelope generator’s current value. This is documented but frequently overlooked, and it’s also the only logical way to do it. You don’t want the envelope generator to jump to a different value just because a segment’s “start value” differs from the current value.

The problem, then, is that the first segment needs a “current value” to start with. This is supplied by the first item in the levels array. The first “levels” item is used at EnvGen initialization time and then never touched after that. So in this context, it means that the EnvGen will start with the first “freq” value that was given, and then, when the freq is changed, it will start with the EnvGen’s value (old freq) and ramp toward the new freq.

hjh

1 Like

Yes, VarLag seems to work in a similar way to the use proposed by jamshark70 with EnvGen (thanks for these explanations!), both work well, the first is just a little more concise.
Lag (mysterious Lag Help: “lagTime: 60 dB lag time in seconds”) and Ramp work less linearly.
For a little comparison, if it’s of any use to anyone else…

//----VarLag
(
SynthDef(\ramp1,{
	arg freq = 200, amp = 0.5, time = 1;
	var sig, rampe;
	rampe = VarLag.kr(freq, time);
	sig = SinOsc.ar(rampe.midicps ! 2, mul: amp);
	Out.ar(0, sig)}).add
);
x = Synth(\ramp1, [\freq, 60,  \amp, 0.5, \time, 5]);
x.set(\freq, 48, \time, 10);
x.set(\freq, 72, \time, 1);

//----EnvGen
(
SynthDef(\ramp2,{
	arg freq = 200, amp = 0.5, time = 1;
	var sig, trig, rampe;	
	trig = Changed.kr(freq);
	rampe = EnvGen.kr(Env([freq, freq], [time], \lin), trig);	
	sig = SinOsc.ar(rampe.midicps ! 2, mul: amp);
	Out.ar(0, sig)}).add
);
y = Synth(\ramp2, [\freq, 60,  \amp, 0.5, \time, 5]);
y.set(\freq, 48, \time, 10);
y.set(\freq, 72, \time, 1);

//----Lag
(
SynthDef(\ramp3,{
	arg freq = 200, amp = 0.5, time = 1;
	var sig, rampe;
	rampe = Lag.kr(freq, time);
	sig = SinOsc.ar(rampe.midicps ! 2, mul: amp);
	Out.ar(0, sig)}).add
);
m = Synth(\ramp3, [\freq, 60,  \amp, 0.5, \time, 5]);
m.set(\freq, 48, \time, 10);
m.set(\freq, 72, \time, 1);

//----Ramp
(
SynthDef(\ramp4,{
	arg freq = 200, amp = 0.5, time = 1;
	var sig, rampe;
	rampe = Ramp.kr(freq, time);
	sig = SinOsc.ar(rampe.midicps ! 2, mul: amp);
	Out.ar(0, sig)}).add
);
w = Synth(\ramp4, [\freq, 60,  \amp, 0.5, \time, 5]);
w.set(\freq, 48, \time, 10);
w.set(\freq, 72, \time, 1);

Yes, thank you, I’m also working on Patterns, but for Pseg you have to give the start point and the end point, I don’t know if it has the same properties as EnvGen.

Thank you for the explanation! I indeed thought of Env in a different manner, but it indeed makes more sense this way.

That’s true – if the node was created in a way that isn’t prepared to accept this kind of modulation, then it can get tricky.

Here, I think there is a difference between experimenting with ideas (where you might not have prepared the synthesis structure for additional modulation) and production (where you probably already have some idea what you want to do). What you’re saying is that it’s not possible without preparation (true) but it came across a bit like it’s not possible in general.

One of the purposes of Teaser: ddwPlug quark (work-in-progress, not public yet) (which is public now btw) was to reduce the distance between Synth() style and NodeProxy-style control mapping. That is: Currently, the first thing we teach new users is Synth(). Then they want to plug a signal into some input and… “well… OK, get ready to learn about control buses and asMap, and manage these resources by yourself.” They might find their way to JITLib, but then give up the easy polyphony that Synth() provides.

I wanted a structure that is superficially like Synth() but easily mappable, e.g.:

(
x = Syn(\default, [
	freq: Plug({ |value = 440, time = 1|
		VarLag.kr(value, time, warp: Env.shapeNumber(\lin))
	})
]);
)

x.set("freq/value", 880, "freq/time", 2);

x.release;

The \default SynthDef has not been changed, but you get the glissando. Patterns…? Sure. This interface might look a little funky, but I wanted to preserve events’ pitch calculations, which would be overridden if a Plug were provided for \freq.

(
p = Pbind(
	\type, \syn,
	\instrument, \default,  // no change to SynthDef, again
	\dur, Pexprand(0.2, 0.8, inf),
	\legato, Pwhite(2.0, 5.0, inf),
	\degree, Pwhite(0, 10, inf),
	\freqPlug, { |freq|
		Plug({ |freq, time|
			freq * EnvGen.kr(Env([0.5, 1], [time], -3))
		}, [freq: freq, time: ~sustain.value * 0.25])
	}
).play;
)

p.stop;

Note that all of these overlap (very hard to do with proxy mapping!) and there’s no bus leakage.

One thing that I want to do, but haven’t done yet, is to allow Plugs to be added in .set – then you wouldn’t even need to prepare, just x = Syn(\default) and x.set(\freq, Plug(...)) later.

If this were to enter the SC lexicon in general, then it wouldn’t be “oh that’s hard,” but rather just a few keystrokes away. But it’s difficult for ideas like this to gain traction, because Synth() and its limitations are deeply ingrained.

hjh

2 Likes