Penv vs Env asStream... or what does Thread.endBeat do?

The obvious difference between Env.embedInStream and Penv.embedInStream is that the latter does a conditional loop

		loop {
			inval = yield(this.at(thisThread.beats - startTime));
		}

vs

		while
			{ thisThread.beats < thisThread.endBeat }
			{ inval = yield(this.at(thisThread.beats - startTime))};

What does Thread.endBeat do though? (It’s not documented and neither is Penv.)

Grepping though the C++ code, it looks like there’s no special functionality for endBeat, so it seems it’s “just a slot” provided for the sclang code to use (the main thread has it nil)… So

		startTime = thisThread.endBeat ? thisThread.beats;
		thisThread.endBeat = this.times.sum + startTime;

is probably all there is to it.

EnvGen: When it reaches the end of the Env, it holds the ending value unless doneAction requires something else.

If Env is used as a stream, there’s no doneAction so it just holds.

Penv releases control at the end of the envelope and advances to the next thing (or causes the parent pattern to stop).

endBeat seems to be used to establish some continuity with a previous invocation of Penv (because Penv could appear within Pn or Pseq, e.g.). Interestingly, you quoted where endBeat is accessed, but it’s assigned just a line or two above – so the ending time of this envelope is set to be the end of the previous envelope (if available) + the total of the durations of the envelope segments.

It’s conceivable that the previous envelope’s end might not line up with the event deltas. The envelope tries to sync up its onset with the end of the last one. This means the time needs to be kept outside the pattern’s embedInStream scope, and patterns may be embedded into multiple different threads. The time may be different per thread.

It’s certainly not meant to be a user-facing variable, though. No need really to be concerned with it.

hjh

1 Like

Yeah, a trivial example to showcase the difference in the base case (no chaining):

~e1 = Env([0, 1, 0], [0.1, 0.1]);
~e2 = Penv([0, 1, 0], [0.1, 0.1]);

(fork { var s1 = ~e1.asStream, s2 = ~e2.asStream;
	7 do: { 0.04.wait; [s1.next, s2.next].postln; }
})

~e2 yields nil once it’s “done” whereas ~e1 outputs zeros forever thereafter.

I’m guessing Penv is not documented because Env.asPseg is almost the same thing, i.e.

(fork { var s1 = ~e1.asPseg.asStream, s2 = ~e2.asStream;
	7 do: { 0.04.wait; [s1.next, s2.next].postln; }
})

actually do the same thing at least in this simple case.

Somewhat useful hack perhaps: you can “sequence” a Penv to start right after another ends:

~e = Penv([0, 1, 0], [0.1, 0.1]);

(fork { var s1 = ~e.asStream, s2 = ~e.asStream; // two instances
	s1.endBeat.postln; // nil
	s1.next.postln;
	s1.endBeat.postln; // non-zero
	s2.endBeat = s1.endBeat; // delay 2nd Penv after the 1st
	10 do: { 0.04.wait; [s1.next, s2.next].postln; }
})

There’s actually a related comment in Pstep

// endBeat > beats only if Pfindur ended something early

Alas the endBeat sequencing hack doesn’t work if you use Env.asPseg instead of Penv, i.e. if you do

~e = Env([0, 1, 0], [0.1, 0.1]).asPseg;

instead in the above example. This is because and Pstep does

		thisThread.endBeat = thisThread.endBeat ? thisThread.beats min: thisThread.beats;
        // and only then    
        startTime = thisThread.endBeat;

unlike Env and Penv which (recall from my question) do

        startTime = thisThread.endBeat ? thisThread.beats;
		thisThread.endBeat = this.times.sum + startTime;

So by presetting endBeat, you can delay the start of an Env or Penv asStream, but not that of a Pseg (or Pstep for that matter).

This is probably unavoidable since Pseg pulls its “envelope” data from other streams, so it cannot precalculate its endBeat, but advances it at every next iteration as

    thisThread.endBeat = thisThread.endBeat + dur;

I have to admit that I’m not clear what the point is.

Is there something specific you’re trying to do, that you can’t do without mucking around with an internal variable?

you can “sequence” a Penv to start right after another ends

You can do that without messing around with internals.

~e = Penv([0, 1, 0], [0.1, 0.1]);

(
fork {
	var s1 = Pseq([~e, ~e], 1).asStream;
	10.do { 0.04.wait; s1.next.postln; }
}
)

So by presetting endBeat, you can delay the start of an Env or Penv asStream, but not that of a Pseg (or Pstep for that matter).

Eh, that makes me nervous.

endBeat is really for internal use. We should not be encouraging users to mess around with it. Admitting that I’m not clear what you’re trying to do, at this point I’d have to advise finding another way.

hjh

Obviously you can (sequence them externally). I was just pondering why Penv and Env set that endBeat… especially since they way they set it seems “incompatible” with what Pseg does with the same slot…

Penv obviously needs to know “when to quit”, but there was no need for a Thread level slot to do that. A private variable would have been enough.

Hm, now I wonder what happens if you have a Pseq alternating between a Penv and Pseg. If they use the variable differently, maybe something would go wrong there.

Suppose a Pbind is polling a sequence of Penv patterns every 0.25 beat, but the Penvs are arbitrary durations (not quantized to quarter beats). Then the handoff point is between polling points. How would the next Penv know when the previous one ended, without a thread level variable?

hjh

I see, so the intention of endBeat in Penv is to make

e = Penv([0, 1, 0], [0.1, 0.1]);
e = Pseq([e, e]);

behave as if the whole 2nd Penv “comes after” the first, i.e. make it similar to what Pseq-inside-Pseq does, e.g. like

Pseq([Pseq([1, 1]), Pseq([2, 2])])

Interesting enough this Psequencing also ultimately works with Pseg although it’s a bit less obvious why (the durations get added more incrementally to endBeat), i.e.

(
~teste = { var s1 = e.asStream, start = thisThread.beats;
	12 do: { 0.04.wait; [s1.next, s1.endBeat-start].postln }};
fork {
	"Penv:".postln;
	e = Penv([0, 1, 0], [0.1, 0.1]);
	e = Pseq([e, e], 1);
	~teste.();
};
fork {
	1.wait;
	"Pseg:".postln;
	e = Pseg([0, 1, 0], [0.1, 0.1]);
	e = Pseq([e, e], 1);
	~teste.();
};
)

posts something like

Penv:
[ 0.0, 0.24000000001979 ]
[ 0.40000000008149, 0.24000000001979 ]
[ 0.80000000016298, 0.24000000001979 ]
[ 0.79999999975553, 0.24000000001979 ]
[ 0.39999999967404, 0.24000000001979 ]
[ 2.9103830456734e-10, 0.44000000003143 ]
[ 0.40000000037253, 0.44000000003143 ]
[ 0.80000000045402, 0.44000000003143 ]
[ 0.79999999946449, 0.44000000003143 ]
[ 0.399999999383, 0.44000000003143 ]
[ nil, 0.44000000003143 ]
[ nil, 0.44000000003143 ]
Pseg:
[ 0.0, 0.14000000001397 ]
[ 0.40000000008149, 0.14000000001397 ]
[ 0.80000000016298, 0.14000000001397 ]
[ 0.79999999981374, 0.24000000001979 ]
[ 0.39999999973224, 0.24000000001979 ]
[ 2.9103830456734e-10, 0.34000000002561 ]
[ 0.40000000037253, 0.34000000002561 ]
[ 0.80000000045402, 0.34000000002561 ]
[ 0.7999999995227, 0.44000000003143 ]
[ 0.39999999944121, 0.44000000003143 ]
[ nil, 0.44000000003143 ]
[ nil, 0.44000000003143 ]

So Pseq-d Penv and Pseg are indistinguishable inside Pbind

e = Pseg([0, 1, 0], [0.1, 0.1]); // same with e = Penv([0, 1, 0], [0.1, 0.1])
p = Pbind(\amp, Pseq([e, e]), \dur, 0.02);
p.trace.play

Unfortunately all the Ptime-like patterns get desynchronized from the underlying stream if you pause the Stream[Player]. So the analogy with embedding Pseqs in each other is somewhat imperfect even with Penv. (The workaround isn’t too complicated for Ptime, but it gets a bit duplicative to do it for all other similar classes.)

I believe the intention here was to treat it as if the envelope were constantly running behind the scenes, and you just poll it at the moment an event is being calculated. It isn’t what you wanted/expected, but it’s also a justifiable design.

It might be interesting to implement some sort of flag to tell envelope or other time-related patterns to follow clock time or integrated durations.

hjh