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.