How to get constant slope instead of constant time envelope re-attacks?

I’m using Pbind with Event type \set and changing the \gate too to re-trigger an asr envelope before the release time finishes. The re-triggering works, but the slope of the re-attacks differs than that of the initial attack, because the time of the first envelope segment stays the same, but since the initial level differs for re-attacks, they get a different slope than the 1st attack.

(SynthDef(\beep, {|atk = 0.15, rls = 2, gate = 0, freq = 440, amp = 0.9|
	var env = EnvGen.kr(Env.asr(atk, 1, rls, 0), gate, doneAction:0);
	Out.ar(0, env * amp* SinOsc.ar(freq).dup);
}).add;)

x = Synth(\beep);

(p = Pbind(
    \type, \set,
    \id, x.nodeID,
    \args, #[\freq, \gate],
    \freq, Pwhite(200, 1000),
    \dur, 0.3,
	\gate, Pseq([1, 0], 3)
).play;
)

The length of the re-attack is obviously 0.15s here too, so the slope is much less steeper than that of the 1st attack.

So, is there a simple enough way to get “constant slope” envelopes, when re-attacking? I don’t want use different nodes, which would give me that, because the overlapping amplitudes would sum up to more than 1 in this case (and in general, even if you rescale them, the overlapping Pbinds will sound louder in the overlapping regions, unless you use exp envelopes, which for reasons I’m not gonna get into, I can’t use-- in fact my envelopes are log-like, I’ve used linear ones here for clarity of the picture)

In case one wonders “what’s wrong with PmonoArtic for this”… Here’s code+picture of the “obvious” solution…

(SynthDef(\beep2, {|atk = 0.15, rls = 0.5, slvl = 0.5, amp = 1, gate = 1, freq = 440|
	var sig = SinOsc.ar(freq).dup;
	var env = EnvGen.kr(Env.asr(atk, slvl, rls, 0), gate, doneAction:2);
	Out.ar(0, amp * env * sig);
}).add;
)

p = PmonoArtic(\beep2, \dur, Pseq([1], 5), \legato, Pseq([0.5, 0.7, 0.9, 1, 1]), \amp, 1).play;

The summing of the overlapping envelopes depends on the relative phase of the two signals, etc. It’s a “big mess” as \legato approaches 1 before it actually reaches it. This is due to the “discontinuous” approach of how PmonoArtic is implemented, i.e. different code path for legato > 1.

Look what a little \dur (phase) shift does:

p = PmonoArtic(\beep2, \dur, Pseq([1.001], 5), \legato, Pseq([0.5, 0.7, 0.9, 1, 1]), \amp, 1).play;

I see (now) I can probably use “separate” Pbinds (and/or PmonoArtic) but with ReplaceOut so they don’t add up. This is actually not entirely trivial because just doing ReplaceOut would restart the whole envelope; as seen below

(SynthDef(\beepR, {|atk = 0.15, rls = 0.5, slvl = 0.5, amp = 1, gate = 1, freq = 440|
	var sig = SinOsc.ar(freq).dup;
	var env = EnvGen.kr(Env.asr(atk, slvl, rls, 0), gate, doneAction:2);
	ReplaceOut.ar(0, amp * env * sig);
}).add;
)

p = PmonoArtic(\beepR, \addAction, 1, \dur, Pseq([1.001], 5), \legato, Pseq([0.5, 0.7, 0.9, 1, 1]), \amp, 1).play;

I need to max(in, env) and only until hit the first peak, so I’m going to post a solution once I clean up code a bit, in case someone else needs something similar. (I suppose once could come up with an XOut solution as well, but I’m not sure how to [pre]calibrate the cross-over time for that.)

This can probably be made a bit spiffier (I’d appreciate suggestions), but the basic idea:

(SynthDef(\beep, {
	var out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

c = Bus.control(s, 1);

(SynthDef(\menv, {
	var bus = \bus.ir(c), atk = \atk.kr(0.15), rls = \rls.kr(0.5), gate = \gate.kr(1);
	var insig = In.kr(bus), env = EnvGen.kr(Env.asr(atk, 1, rls, 0), gate, doneAction:2);
	var eSlopesDown = (HPZ1.kr(env) < 0), outsig = eSlopesDown.if(env, max(insig, env));
	ReplaceOut.kr(bus, outsig);
}).add;)

x = Synth(\beep); // perma running

x.map(\amp, c);

p = Pbind(\instrument, \menv, \addAction, 1, \dur, Pseq([1], 5), \legato, Pseq([0.5, 0.7, 0.9, 1, 1])).play;

The slopes are constant, as I desired. The “gap” between the \legato = 1 events is exactly the length of an “attack”, i.e. atk, although it’s part-release and part attack (due to taking the max between them).

To make Pmono style “full” legato with this code/approach, I need to set \sustain > \dur + atk, e.g.

p = Pbind(\instrument, \menv, \addAction, 1, \dur, Pseq([1], 4), \sustain, Pseq([1.10, 1.15, 1.2, 1])).play;

A couple more notes here:

  • If using an audio bus for the amplitude (no zipper noise), you can leave out the HPZ1-if part because audio busses (unlike control busses) auto-reset to 0 between cycles. Other effects are possible, e.g. “notches” in amplitude for “sidechain compression”, with an appropriate variation on menv (e.g. not taking the max but e.g. the min).

  • This idea can be encapsulated in a Pproto fairly easily, so a longer sequence of envelopes builds and tears down the main signal synth. When doing that, one needs to be a bit careful with the last envelope event so it has a enough dur, i.e. >= sustain + rls so that the Pproto cleanups don’t stop the synths prematurely.