Stopping an event with infinite duration

Is there a nice way to stop an event with infinite duration?
p = Pbind(\dur, inf).play; p.stop; obviously doesn’t work.

Also tried
p = Pbind( \callback, {|ev| i = ev[\id]}, \dur, inf ).play;

and then explicitly stopping the node with another event:
(type: \off, id: i).play;

This works but i was wondering if there might be a better way. Or is this the way?

1 Like
  1. Put the synths in a group that you created:

    g = Group.new;
    p = Pbind(..., \group, g, ...).play;
    
  2. When it’s time to stop them, do g.set(\gate, 0).

BTW inf scheduling ends up not scheduling anything. (The task will never wake up, so SC cleverly does not waste space in the queue for it.)

t = TempoClock.default;

t.dumpQueue;  // from ddwCommon quark, not in core
nil, priority 0
   nil  <<-- empty

t.sched(inf, { "hi".postln });
t.dumpQueue;
nil, priority 0
   nil  <<-- still empty

hjh

1 Like

Thanks! Yes, groups works in general of course. I this particular case i have several patterns sharing the same group where the idea was to have the possibility of “manually” starting and stopping synth nodes without leaving the pattern paradigm.

Usually it’s the Pattern’s job to start synths (though of course this could be circumvented). Could you maybe specify a bit, what the Pattern / EventStreamPlayer should do instead ? Do you mean a kind of unmuting / muting EvenStreamPlayers ?

Hm, do they have to share the same group?

Another option is a nested group structure.

~masterGroup = Group.new;
~groupA = Group.head(~masterGroup);
~groupB = Group.after(~groupA);
~groupC = Group.after(~groupB);
...

Then there is one group for everything, but you have access to all the nodes for one pattern under ~groupA, etc.

hjh

Well, “have to” is a strong expression. :wink: I could probably redo it using groups. The project i’m working on is based on Pbindefs and a live-coding framework using preProcessor. I thought: “what if i just want to play a long texture or sample?”, and kind of liked the idea of just replacing a \dur pattern with an inf to achieve this. Seemed simple and straightforward. Maybe this is a bad idea? I mean, it seems to work with the \callback and \off event solution i posted above, but it might have bad consequences yet to be discovered…

I don’t think there is much of a downside.

There would be a downside if the scheduler put entries into the queue that would never be removed by waking up. Then there would be a risk of filling up the queue with actions that will never happen. But SC prevents that by simply not scheduling things that will not wake up (no object leaks in the queue).

Then it’s up to you to take care of the nodes (topic of this thread :wink: ).

hjh

This is basically how Pmono does its thing. It wraps the original stream into a PmonoStream that adds some callbacks from the Event back into PmonoStream which mainly “steal” the node id. And of course, to actually be able to play anything, Event is extended with new event types: \monoNote for start + id “stealing” and \monoSet for “note expression” changes (to borrow the Cubase term). There’s also ‘\monoOff’ defined but it’s never used, instead a generic ‘\off’ is called/schedule.

1 Like

Here’s a wrapper-way to do almost this (i.e. group-based solution) for an existing Pbind without changing its source code… “Almost” because it kills the group rather than gate it out, I think:

(~gwrap = { |pat| Pproto(
    { ~newgroup = (type: \group).yield; },
    Pset(\group, Pkey(\newgroup), pat))})

p = ~gwrap.(Pbind(\dur, inf)).trace.play
p.stop // stops it!

This simplified version works too:

~gwrap2 = { |pat| Pproto({ ~group = (type: \group).yield; }, pat);  }
p = ~gwrap2.(Pbind(\dur, inf)).trace.play
p.stop // works too

It’s because the Pproto’s prototyped stuff gets passed to the event, but unlike the longer version it won’t override \group if there’s already one in pat. (I’m not sure in general if this is preferable or not.)

I can confirm that group is indeed killed, not released. There “magic” happens in the EventTypesWithCleanup class, which has group: \kill (Pproto does a lookup there to construct its default cleanup). Now I’m pondering if this can be changed to release…

Probably the selling point of this is that since it changes what stop does on the resulting ESP, it can be seamlessly combined with Pdefs further upstream.

Pdef(\heh, ~gwrap2.(Pbind(\dur, inf)))
Pdef(\heh).play
Pdef(\heh).stop //works

Surprisingly (for me), this works too

p = ~gwrap.(Pfunc({ |ev| ev[\dur] = inf })).play
p.stop // works somehow

I was expecting that to fail because Pproto does this conversion internally

stream = Pfpar(pattern.asArray).asStream;

I’m not sure exactly what the limitations stemming from that are (I guess I’d better ask separately.)

Another issue that’s perhaps worth mentioning here is that Pgroup is useless for this scenario, but at least it’s obvious why: it tries to schedule its group cleanup after all the events from the stream have finished so that clearly is a no-op with a dur: inf event.

To give some further alternatives, wrapping with a group-based solution can also be done with Pbindf or Pchain (<>):

p = Pbind(\dur, inf);
Pbindf(p, \group, g = Group()).play;

g.release


p = Pbind(\dur, inf);
(p <> (group: g = Group())).play;

g.release

// also release time can be passed
// g.release(5)
1 Like

Looking through the Pdef and EventPatternProxy stuff I was hoping there would be something with built-in “group awareness” (somewhat like Pproto, but with an externally tacked group), but alas there doesn’t seem to be such a class ready made…

Here’s a variant of your code that does automatic group release on ESP stop, essentially using the same mechanism that Pproto uses (in this application) but without all the Pproto “extra baggage”:

p = Pbind(\dur, inf);
r = (p <> (group: g = Group(), addToCleanup: { g.release } )).play;
r.stop // works

Thankfully this doesn’t cause a plie-up of clean-up functions, even with more than one event in the stream, because r.cleanup.functions is implemented as an IdentitySet. It is slightly inefficient that the same clean-up gets “added over and over” with every event, but there’s no memory leaking from this.

It turns out Pfset allows a cleanup function too, so this works too for release-on-stop:

p = Pbind(\dur, inf);
r = Pfset({ ~group = g = Group() }, p, { g.release }).play;
r.stop // works

Pfset is basically a “Pproto-lite”. The documentation for Pfset is rather unclear whether the initial function gets evaluated before every event or just once. It turns out the latter is the case.

p = Pbind(\dur, Pseq([1, 1, inf]));
r = Pfset({ ~group = g = Group(); g.postln; }, p, { g.release }).trace.play;
r.stop 

outputs

Group(3599)
( 'group': Group(3599), 'dur': 1, 'addToCleanup': [ a Function ] )
( 'group': Group(3599), 'dur': 1 )
( 'group': Group(3599), 'dur': inf )

I.e. the group gets created only once, which is what we want for this scenario.

Alas the documentations of Pproto and Pfset don’t mention each other at all, even though they are far more equivalent than one might think based on their names…

There is actually one subtle problem with the above Pfset solution, that the one using Pproto doesn’t have. If you reset the stream r while it’s playing (the non-finite notes), then the init function gets called twice in a row. You still get two cleanups scheduled, but they execute in immediate succession on stop. Because the global variable g is overwritten by the 2nd init (on reset), you basically get two releases for the 2nd group created and none for the first. In contrast, Pproto manages to handle correctly this issue.

One can fix that by tracking the “activation records” from the outside, e.g.

p = Pbind(\dur, Pseq([1, 2, 7, inf]));
(var gl = List();
 r = Pfset({ gl.addFirst(~group = Group()); ("ini" + ~group).postln; }, p,
	{ var g = gl.pop.release; ("bye" + g).postln; fork { 3.wait; g.free } }).trace.play;)
r.reset // call before it reaches the infinite event
r.stop // seems to works properly

A nicer way would be if Pfset didn’t just discard what your init/make function returns but passed that valued to the cleanup function. That would allow you to capture g above in a closure, a different one for each initialization. This is basically how Pproto solves this issue internally.

Note that because reset doesn’t call the cleanup immediately but defers it to whenever you’ll actually call stop, you can’t terminate early the long, seven-second note above with the reset. It’s a design issue with cleanups not getting evaluated immediately on reset. One could even say this is a bug in EvenStreamPlayer… but it’s perhaps a (debatable) feature. I can see e.g. a fixed buffer not having any advantage being re-allocated on reset. I would suggest that reset could be improved in a backward-compatible manner with an optional boolean parameter (e.g. doCleanup, defaulting to false) that would invoke the cleanup (as it happens on stop) if this doCleanup flag were passed in as true to reset. Additionally the “stuff upstream” of ESP like Pdef would also have to be modified to support that flag by passing it downstream to ESPs created.

Basically

+ EventStreamPlayer {
	reset { arg doCleanup = false;
            if(doCleanup) {cleanup.terminate} {}; // this is the mod
            routine.reset; super.reset; // current "reset" impl does just these
    }
}

With this “mod” one can simply do

r = Pfset({ ~group = g = Group(); g.postln; }, p, { g.release.postln }).trace.play;
r.reset(doCleanup: true) // also calls cleanup now
r.stop

But deferred/forked “frees” in the cleanup can still mess up here if they read from the global g. This can however be easily fixed by capturing g in a closure in the cleanup function. I.e.

(r = Pfset({ ~group = g = Group(); g.postln; }, p,
           { g.release.postln; fork {3.wait; g.free} }).trace.play;)
r.reset(doCleanup: true); // still "surprising" results

// So instead do:
(r = Pfset({ ~group = g = Group(); g.postln; }, p,
           { var cg = g.release.postln; fork {3.wait; cg.free} }).trace.play;)
r.reset(doCleanup: true);

Also of note, Pproto cannot be currently used this way, i.e. with a custom cleanup, if you let it auto-create any clean-up at all, despite what its documentation says.

Hey these threads are useful - I’d encourage you to do a pull request to improve he docs if you have the time!

I’ve made a slightly better mouse trap, a Pfset with closure.