So this post – https://github.com/supercollider/supercollider/pull/4736#issuecomment-596803787 – got me thinking overnight.
Cleanup is about side effects
Pure functional streams don’t need cleanup because they don’t make any messes.
With EventStreamPlayer, the whole point is the side effect of playing the events. And, you want to be able to stop the side effects on demand (allowing for differences of opinion whether “stop” means only to stop new side effects, or to stop the currently-active side effects immediately). Cleanup needs to work in both directions: bottom-up (the stream stopped, and needs to clean up its own stuff) and top-down (the user ordered the whole thing to stop).
The above github comment seems to be proposing that a pure data stream, e.g. Pseries(0, 1, 10)
, should be just as “clean-uppable” as an event stream. Which raises two questions: 1/ What is the side effect to clean up? 2/ What is the concept of stopping a pure data stream?
1/ In theory, you could have a Pfset({ set some state }, Pseries(...), { clear the state })
, so, OK. (It’s not pure anymore, but OK.)
2/ We “stop” a pure data stream passively, by simply not requesting any more values. You can call stop
on any routine at any time and it will yield nil
after that point, but in practice, we don’t bother.
#2 suggests that maybe it should be the stream itself handling the cleanup.
Data streams with side-effects
(
r = Routine {
var synth;
loop {
synth = Synth(\default, [freq: exprand(200, 800)]);
0.4.wait;
synth.release;
0.1.wait;
}
}.play;
)
r.stop;
Common usage pattern here. We can think of this as a data stream (0.4, 0.1, 0.4, 0.1…) with side effects.
There’s an 80% chance of hitting stop
between Synth()
and release
– leaving a hanging node. So that’s a second reason to consider that maybe the proper home of the cleanup is the stream itself.
What if you could write something like this?
(
r = Routine {
loop {
var synth;
var cleanup = { synth.release };
synth = Synth(\default, [freq: exprand(200, 800)]);
thisThread.addCleanup(cleanup);
0.4.wait;
thisThread.removeCleanup(cleanup);
synth.release;
0.1.wait;
}
}.play;
)
And imagine something like that throughout the pattern library.
Implementation
I’m afraid I don’t have time to build this myself at the moment, but some thoughts:
-
Don’t put this into Routine. There are other types of Streams (FuncStream, StreamCollect etc.). IMO a “clean-upabble Stream” should be a wrapper class.
-
If you wrap a Routine in some other class, thisThread
will be the Routine rather than the object handling cleanup. So you would need some other mechanism to get the current stream-with-cleanup. I don’t know what that should be right now.
-
For nested streams (call next
on A, and A calls next
on B, and B has a cleanup), I believe propagation upward would work something like:
+ StreamWithCleanup {
var stream;
next { |inval, cleanupDelta|
var childCleanupDelta = IdentityDictionary.new;
var result = stream.next(inval, childCleanupDelta);
... update my own cleanup according to delta...
... add child cleanups into passed-in cleanupDelta...
^result
}
}
… and returning the result to the caller would give any parent streams the opportunity to update, and so on. I’m not sure if that’s a good way to pass cleanup changes around, though.
Hope that helps. It may be difficult to implement but probably a design improvement. Comments (including “no, that’s a terrible idea”) welcome.