What explains Pmono vs Pbind stop behavior?

I’ve noticed that if Pmono is playing a long \dur note and you sent a stop to its EventStreamPlayer, the note gets released right away, whereas Pbind doesn’t do this, i.e. it only “stops” after the current note has fully played its previously scheduled duration.

p = Pmono.new(\default, \dur, 5, \amp, 0.8, \degree, Pseq([1,2], inf)).play
p.stop // hit after a note just starts -> releases quickly

p = Pbind.new(\instrument, \default, \dur, 5, \amp, 0.8, \degree, Pseq([1,2], inf)).play
p.stop // doesn't release the current note!

Interestingly–or for my use case annoyingly–PmonoArtic has the Pmono stop behavior for notes with legato >=1 and the Pbind stop behavior otherwise. I’m trying to hack PmonoArtic so it will immediately release a note on stop regardless of the note’s legato. Any ideas how to do this?

I have a hack that works around the behavior by changing the clock tempo, i.e. something like

p.stop
p.clock.tempo = 100 // really fast stop now even for Pbind

but it’s not entirely satisfactory because the release is now extremely accelerated i.e. it’s still a different release time for the legato=1 and non-legato notes…

Looking at EventStreamPlayer internals, the difference in behavior seems to come from the call to cleanup.terminate. Pmono sets up cleanup while Pbind doesn’t. PmonoArticStream does something rather complicated, removing the cleanup in the sustain<delta (i.e. legato<1) notes. I’m honestly not sure why it is necessary for it do this, i.e. why can’t it leave the cleanup on for all notes…

Well, the termination function is basically moved from the cleaner to the clock’s queue, so a way to release early with release time preservation is to do something like

p.stop
p.clock.queue[2].value // this calls the release function for Pbind

This is pretty brittle though, depending on what other stuff there might be in the queue. (Also prints a failure in server when the clock tries to do the same action a bit later, when it was scheduled). I guess there might be a way to have the clock “jump” to next event, but I can’t figure out the canonical way to do that right now.

1 Like

it would be great to have comprehensive and consistent methods - kill, stop immediately/after current note. And then start from beginning, resume from paused position. If they were the same for Pmono, Pbind and Pdef that would be a plus…

I never know what’s going to happen when pausing restarting Patterns and just end up trying things even in my 3rd year w/sc…

1 Like

TL;DR the best solution to this is to create a group, put all of the pattern’s nodes in the group, and then when stopping, you can do theGroup.release or theGroup.set(\gate, 0).

This gets into one of my favorite topics.

In the help, usage patterns like p = Pbind(...).play appear all over the place.

This leads to an assumption that Pbind is a general-purpose note-manager. (The concern about release behavior here stems from this assumption.)

I think a better object model for this is that Pbind should be one component of a general-purpose note-manager.

Think about what you need for a note-player.

  • Mixing: a bus to isolate this pattern’s resulting signal, and a fader synth for volume/pan control.

  • Order of execution: a group, with a specific relationship to other nodes.

Neither of these is directly involved in pattern evaluation. The usual object-oriented concern about encapsulation means that we need an object that just evaluates pattern data. That is Pbind. We already have it. We shouldn’t change that concept because it’s an essential part of a sensible object model. Adding generalized note-manager functions onto patterns themselves is to create a “god object” (an anti-pattern).

I would suggest a “wrapper around” a pattern that includes these concepts.

In my chucklib quark, I do it like this:

(
BP(\x).free;  // start from scratch

Proto {
	~prep = {
		~group = Group.new;
	};
	~freeCleanup = {
		~group.free;
	};
	~asPattern = {
		Pbind(
			\degree, Pwhite(-7, 7, inf),
			\dur, 0.5,
			\sustain, 3,
			\group, ~group
		)
	};
	~stopCleanup = {
		~group.release;
	};
} => BP(\x);
)

BP(\x).play;

BP(\x).stop;

The BP is a wrapper object that manages the behavior pattern, along with any resources that it needs. ~stopCleanup is for stop-time behavior.

Pmono sets up cleanup while Pbind doesn’t.

The default \note event type schedules the release at a time in the future that can be calculated right now.

In Pmono, the release time of the event that is playing right now is unknown. You have no idea how many events, or how much time, will elapse between now and release. That’s why you need the cleanup.

PmonoArticStream does something rather complicated, removing the cleanup in the sustain<delta (i.e. legato<1) notes. I’m honestly not sure why it is necessary for it do this, i.e. why can’t it leave the cleanup on for all notes

If legato < 1, then the release time is known (you’re definitely going to release before the next note, and the time of the next note is known from theEvent.delta). So it’s appropriate to scheduled for release in the future. It doesn’t make sense to release the same note twice, so it should be removed from the cleanup at that time.

This leads to a problem with the solution that I proposed above: you can’t cancel the scheduled releases – but the note has been released by ~stopCleanup, so the server will post failure messages about that. They are harmless (the goal was to remove a node; removing a node that was already removed means you end up in the state that you wanted anyway), but unnerving. (There is a way to suppress the messages.)

I’d also note that your use case is to stop the notes immediately on .stop, but I very often play overlapping long-swell notes (slow attack and release). In a performance, it would sound silly if the texture did not fade naturally to silence. So I prefer Pbind’s behavior as it is.

hjh

I have yet to fully digest your post (the group idea seems good, also I see now that was discussed before), but yeah it struck me too that the auto-instantiation of the (not very parameterizable) EventStreamPlater player straight from patterns with .play is basically rather bad from a “separation of concerns” perspective…

Another thing that I find a little dubious from the same perspective is that the ESP doesn’t really play anything. Nor do the Pmono etc. actually instantiate a different player, even though one could conceptualize exactly the same stream being played differently by a “mono player” and a “chordy/poly player”. Instead Pmono wraps the stream (via PmonoStream) by adding some callbacks from the Events back into the PmonoStream (to find/store the node id) and everything actually gets played (i.e. sent to server) via the real player(s) that are in Event. The fact that Event is hardly a generic capsule but instead knows how to play every possible event type (extensible via addEventType) is quite “Pdef-ish” and strikes me as being basically the “god object” you criticize…

yeah it struck me too that the auto-instantiation of the (not very parameterizable)

What other parameters would you like to see?

EventStreamPlayer player straight from patterns with .play is basically rather bad from a “separation of concerns” perspective…

What design would you prefer?

Another thing that I find a little dubious from the same perspective is that the ESP doesn’t really play anything.

It’s an analogue to Routine { ... stuff ... }.play(aClock, quant).

play in SC is a highly polymorphic keyword – which you could regard as a design flaw, or you could see it as part of the fun and joy in the language.

Nor do the Pmono etc. actually instantiate a different player, even though one could conceptualize exactly the same stream being played differently by a “mono player” and a “chordy/poly player”.

p = Pbind(
	... whatever data I want...
);

q = Pchain(
	Pmono(\defname, \dummy, 0),
	p
).play;

There, ‘p’ has now been converted to slurs.

The fact that Event is hardly a generic capsule but instead knows how to play every possible event type (extensible via addEventType) is quite “Pdef-ish” and strikes me as being basically the “god object” you criticize.

I disagree – and again, if you see the current design as inadequate, it would be helpful to suggest a better one.

hjh

I rushed to conclusion here. It turns out that using dependants one can customize the stop behavior of the ESP, but this does require separate instantiation, i.e.

(~clkffwd = { |clo, bts=10, spd=100|
	var ot = clo.tempo;
	clo.sched(bts, { clo.tempo = ot; nil });
	clo.tempo = spd * ot;
})

(~myplay = { |pat, clk=(TempoClock())| 
	var p = EventStreamPlayer(pat.asStream);
	p.addDependant ({ |n, x| switch (x, \userStopped, { ~clkffwd.(n.clock) }) }); 
	p.play(c);
})

p = ~myplay.(Pbind(\dur, 5, \degree, Pseq([2,3], inf)));
p.stop // releases note early

But this was quite a bit of hackery to figure out for a relatively simple change in behavior…

If one is willing to overlook a potential race condition, the stop-modding dependant can be added to any playing ESP, thankfully…

p = Pbind(\dur, 5, \degree, Pseq([2,3], inf)).play
p.addDependant({ |n, x| switch (x, \userStopped, { ~clkffwd.(n.clock) }) }) 
p.stop

Ok, that’s a good way.

One thing to watch out for is to remove the dependants when you don’t need them anymore. They stay in a global collection otherwise; if not removed, then they never get garbage collected.

Actually you’re already adding the dependant every time in ~myplay, so you can remove the dependant in the dependant itself:

p.addDependant({ |n, x|
    switch (x,
        \userStopped, {
            n.removeDependant(thisFunction); 
            ~clkffwd.(n.clock)
        }
    )
});

(Untested.) So the dependant exists only when actually playing.

It’s not actually that simple a requirement. To stop them, something has to track them. (The group-release is as close as we’re going to get to a MIDI all-notes-off.) Perhaps the event system just sidesteps tracking them by dumping the releases onto the scheduler, and perhaps this is insufficient. OTOH tracking also implies watching for n_end replies (if you start tracking a node, you have to stop tracking it at some point), which wouldn’t work for NRT rendering because there are no n_ends coming back.

If stop behavior seems under-developed, I’d have to note that in 17 years of using SuperCollider, I can’t recall anyone requiring all nodes to stop immediately when stopping the pattern player. It simply hasn’t come up. Not to say that it should or shouldn’t be raised, just that historically for SC, it hasn’t been a common assumption that .stop = silence right now.

hjh

1 Like

Well, the older thread here that I already mentioned (in post further above) is discussing something even more difficult, i.e. stopping a dur: inf event with stop… My clock-acceleration solution won’t work for that, only the/your group-based solution works for that. By the way, I’ve posted a fairly simple Pproto wrapper for that over there, but it has the caveat of not gating out but killing the group (I think).

One way to fix/enhance this would be for the Clock to expose more its scheduler, i.e. have a “lookup and reschedule” function. In some priority queue implementations (not SC’s as far as I can tell), there’s a primitive to change the priory of an event/item already queued. (With the usual min-heap-based implementation of priority queue, only moving events closer to “now” is allowed.)

Sure, in that case, users would expect to be responsible for the eventual release.

What I was trying to say is analogous to estimating the number of cases of the virus that’s in the news. Some percentage will always be undetected, but if there are enough cases, then inevitably some will show up in the medical system. Similarly, you can estimate the demand for a feature based on how often it’s requested. If “I want all the notes coming from a pattern player to stop at the moment when I stop the player” were a commonly desired behavior, it would have come up repeatedly on the mailing list over a decade and a half. It might have come up a couple of times, but a couple of times over 17 years isn’t much. If it came up a couple times a year, every year, over the last decade, I’d remember.

I do see the validity of what you’re asking for (even though I don’t want that behavior in my own work) but it hasn’t been commonly requested – which would explain why the functionality isn’t already there for you to use.

Not a bad idea. The catch is that you need to be able to find the specific object that was scheduled for a specific note release, or scheduled items need to be tagged in some way. Event internals are generally not accessible. That might be good for an RFC (GitHub - supercollider/rfcs), as it’s not a simple feature.

hjh