Better parallel streams: a work in progress

Currently, to run multiple Event streams in parallel, you need to use one of the Ppar classes. The list of parallel patterns being run can ONLY be specified at construction time - you cannot add new parallel streams once you’re started playback of the Ppar.

This is an initial attempt at an alternate Ppar implementation, that allows adding new parallel streams dynamically. My hope here is to consolidate some of various implementations of “parallel-ish” pattern playback in one place. This overlaps somewhat with the Xspawn classes, but these have much more narrow and specific use-cases - I eventually hope that I can implement these as well with the PparStream.

For now, it provides two major pieces of functionality.

  1. An injectStream method that plays a new parallel stream starting at the current time (with a possible delta offset). A function wrapping this method is implicitly added to events passing through the stream in the ~injectStream key, so it can be accessed from your Event playback.
  2. A \fork parent Event type, that mirrors the behavior of the Pdef recursive phrasing functionality. Event’s of this type will take the contents of the \pattern key (assumed to be another event pattern), and play them in parallel starting at the current time, and stopping them according to \legato / \sustain as a normal Event would be.

This is still very beta, but I’ll post a few simple examples if anyone wants to explore or poke holes in it.

5 Likes

EXAMPLE 1.
Play an empty pattern, and then inject new Pbinds in from the outside…

(
~inject = List();

// Pb is a shortcut for Pbind, but using the new PparStream as a basis...
Pdef(\basic, ~parStream = Pb(
	\dur, 1/4,
	
	\streamsToInject, ~inject,
	\play, {
		~streamsToInject.do {
			|stream|
			~injectStream.(0, stream)
		};
		~streamsToInject.clear;
	}
).asStream).play
)

(
// Inject a stream from the outside....
~root = 4.rand;
~inject.add(Pbind(
	\scale, Scale.locrian,
	
	\dur, 1/8,
	\legato, 2,
	
	\octave, Pseq([3, 4, 3], 8),
	\degree, ~root + (
		Pstep(Pseq([1, 2, 4, -1], inf), 6/5)
		+ Pstep(Pseq([0, -3], inf), 4)
	),
))
)
1 Like

hey, this looks very nice :slight_smile:
would it be possible to access the keys of the imbeded patterns from outside the PparStream?
When putting Pdefs inside a Ppar one cannot access the keys of the individual Pdefs.
this would help me very much.

EXAMPLE 2.
Using the \fork method to play sub-patterns, similar to Pdef recursive phrasing.

(
SynthDef(\clicky, {
	var sustain = \sustain.kr;
	OffsetOut.ar(
		\out.kr,
		Env.perc(0, sustain).kr(doneAction:2) * LPF.ar(
			LFSaw.ar(
				\freq.kr(100) * Env.perc(0, sustain, curve:-12).kr
			),
			Env.perc(0, sustain).kr.exprange(40, 12000)
		)
	)
}).add;

// Pb is a shortcut for Pbind, but using the new PparStream as a basis...
Pdef(\fork, Pb(
	\type, \fork,
	
	// Pattern must be enclosed in a Function or Ref
	\pattern, {
		|minDur, maxDur|
		Pbind(
			\instrument, \clicky,
			\dur, Pseg([minDur, maxDur, minDur], [4, 4], 4*[1,-1]),
		)
	},
	
	\gatePattern, false, // don't cut the pattern off (it has a finite length)
	\dur, 4,
	\amp, 0.1,
	
	\octave, Pwhite(3, 12) + [0, 0.04], // octave key automatically is forwarded to child events
	
	\minDur, Pexprand(1/64, 1/8),
	\maxDur, Pkey(\minDur) + Pexprand(1/4, 0.25),
	
).asStream).play
)
1 Like

I guess the question is - what do you mean by “keys of the embedded pattern”? The patterns in the original pbind? Or, something like — the last value that was produced by a particular key? Or do you want to control individual key in a Pdef from the outside?

In any case, none of these things is made particularly easier using PparStream … once you inject a stream, you lose track of it unless you’ve stored it somewhere yourself.

thanks. i ment a Ppar class that does keep track of each event’s source, so that one could control individual keys from outside the Ppar. sorry for interupting the presentation of your initial work.

This all looks very similar to Ron Kuivila’s Spawner object (in the main class library).

// EXAMPLE 1 without needing any extensions
(
p = Pspawner({ |spawner|
	~spawner = spawner;  // external access
	spawner.par(Pbind(\type, \rest, \dur, 1/4));  // keep alive
}).play;
)

(
// Inject a stream from the outside....
~root = 4.rand;
~spawner.par(Pbind(
	\scale, Scale.locrian,
	\dur, 1/8,
	\legato, 2,
	\octave, Pseq([3, 4, 3], 8),
	\degree, ~root + (
		Pstep(Pseq([1, 2, 4, -1], inf), 6/5)
		+ Pstep(Pseq([0, -3], inf), 4)
	)
));
)

p.stop;


// (almost) EXAMPLE 2 without extensions
(
SynthDef(\clicky, {
	var sustain = \sustain.kr;
	OffsetOut.ar(
		\out.kr,
		(Env.perc(0, sustain, \amp.kr).kr(doneAction:2) * LPF.ar(
			LFSaw.ar(
				\freq.kr(100) * Env.perc(0, sustain, curve:-12).kr
			),
			Env.perc(0, sustain).kr.exprange(40, 12000)
		)).dup
	)
}).add;

p = Pspawn(Pbind(
	\method, \par,
	
	// Pattern must be enclosed in a Function or Ref
	\pattern, { |ev|
		Pbind(
			\instrument, \clicky,
			\dur, Pseg([ev[\minDur], ev[\maxDur], ev[\minDur]], [4, 4], 4*[1,-1]),
			// other keys are not automatically forwarded currently
			\amp, ev[\amp],
			\octave, ev[\octave]
		)
	},

	// currently no equivalent in Pspawn, worth adding
	\gatePattern, false, // don't cut the pattern off (it has a finite length)
	
	\dur, 4,
	
	\minDur, Pexprand(1/64, 1/8),
	\maxDur, Pkey(\minDur) + Pexprand(1/4, 0.25),
	\amp, 0.1,
	\octave, Pwhite(3, 12) + [0, 0.04]
)).play
)

Pspawn never became very popular, but it looks very similar to your Pb. You did a couple of nice things in Pb which Pspawn doesn’t currently implement – 1/ valueEnvir for a function supplying the pattern, 2/ forwarding parent-pattern keys into child patterns, 3/ gating the child pattern (that’s nice! missing feature in the Spawner hierarchy) – #2 and #3 could be added without much trouble; #1 would break the current interface of passing the parent event into the pattern-generating function.

Since Spawner and its related classes already do most of what you’re proposing here, it would probably be better to tidy up the interfaces and add missing features to the existing class, rather than add something new that is essentially a duplicate.

The ~inject interface in your example looks rather brittle to me – IMO it would be better to inject a stream by calling a method directly on the object responsible for running streams in parallel – which it turns out we can already do with Spawner.

hjh

PparStream uses more or less the same design details as Spawner as well (which is in turn the same as Ppar…), with some minor cleanup.

Unfortunately, Pspawner can’t straightforwardly be controlled by event streams - you’re stuck writing functions with wait's, which doesn’t compose well with Pbinds or other kinds of event streams.
Pspawn seems to try to remedy this, but it ignores the pattern system entirely and uses it’s own particular interpretation of Events, it’s own keys etc. For me, this feels like a recipe for disaster…

This are mostly taken from the \phrase event type. The functional difference is: \phrase simply .play's each forked pattern, rather than merging them into a unified stream as Ppar and friends do. This means, you effectively lose track of these events streams in an important way once they are forked. You can never do something like Pbind(\octave, 3) <> Pbind(\type, \phrase) because none of the phrase notes ever makes it to the pattern where you set the octave. But in any case, none of this is new.

I very much agree - this isn’t ideal. I’m open to other suggestions - here’s the set of problems that lead me there:

  1. Pspawner doesn’t compose with other streams, because it passes the Spawner object as the inval to your function. This inval is the path through which you can get upstream values. For example:
(
Routine({
	|inval|
	inval.postln;
	inval = 1.wait;
	inval.postln
}) <> Pseq([10, 20])
).play

In fact: if you EVER want a Routine to be composable, you have to use inval to pass through upstream values and nothing else, otherwise you’ll be dropping values. This is the one and only way to pass values into a Routine, which means we lose any obvious way to pass in our “stream controller” object (like Spawner) as an argument. (There are some ways of working around this, but they end up breaking composability in subtle ways and in generally being very code-smelly).

  1. I most cases in SC, Events are only “final” when Event:play is called - up until that point, they are still subject to transformation as they pass through a chain of streams to whatever is playing them. Pspawn is a bit of an exception here, in that it interprets and removes events from the stream at the point where it sees them. This isn’t WRONG, but it can create problems. I wanted to see if I could create a similar mechanism where the stream itself wasn’t opinionated about Event’s - in other words, it didn’t care about what events it was passing through, what they looked like, what was in them. Instead, it only does a minor additive transformation (adding a key) and then lets Event:play decide how to interpret the Event.

Given these two constraints, I decided on:

  1. Add the most bare-bones possible interface into Events (\injectPattern).
  2. Use custom event types to provide e.g. parallel behavior by making use of \injectPattern. \type, \fork is only one such example of this.

I would prefer to NOT modify Event’s with \injectPattern (you’re right, it feels brittle), but there’s not an obvious other way to ensure a very-far-away-stream function is accessible inside a \play function.

We may have to agree to disagree here: I think that mixing parent and child streams into one output stream is more likely to be disastrous. It will create considerable challenges if you want to apply filtering to the composite output stream that depends on the format of the output events, because the parent events have a totally different purpose and design. It’s my opinion that these should be kept separate.

hjh

I think both use-cases are 100% valid for different situations. Having a composite stream of very different kinds of events, with different behaviors and structures, is extremely common even without any parallel stuff or spawning - so I don’t think this is a particularly esoteric behavior. Any stream filtering that reaches into Events is necessarily fragile, since it can break depending on the structure of the events (I see these problems very often).

You can provide the same behavior as Pspawn (e.g. hard-filtering of Events from the stream) using something as simple as:

Pfunc({ |e| if (e[\type] == \fork) { Event.silent(e.playAndDelta()) } { e } }) <> Pb(...) // everything downstream of this sees only the "spawned" 

I’d probably like best a version of Pspawn that uses the implicit injectPattern key rather than Spawner, so that you can compose normally with other Patterns. This gets the explicit replacement you’re talking about, with better syntax - imagining something like:

PpatternSpawner({ Pbind(\dur, Pseq([1/4], 4), \degree, Pseries()) }) <> Pbind(\dur, 4, \octave, Pseq([3, 4, 5, 6], inf))

Great feedback - again, I wrote this only to solve some specific use-cases and explore whether I could unify some of the different parallel stream implementations, so it’s not currently intended to be a replacement for the spawn classes (though it would be nice if we didn’t have three or four copy-pasted version of the priority queue stuff all over the place…).

Sure (up to a point – I’ll come back to that), and I agree that none of these situations is especially well-handled currently.

True, but Pspawn also makes the Spawner available:

// EXAMPLE 1, no extensions, with composability
(
p = (Pspawn(Pbind(
	\method, \par,
	\publishSp, Pfunc { |ev|
		~spawner = ev[\spawner];
	},
	\dur, 0.25,
	\pattern, `Pbind(\type, Pn(\rest, 1))
)) <> Pbind(\pan, Pseq([-1, 1], inf))).play;
)

(
// Inject a stream from the outside....
~root = 4.rand;
~spawner.par(Pbind(
	\scale, Scale.locrian,
	\dur, 1/8,
	\legato, 2,
	\octave, Pseq([3, 4, 3], 8) + 2,  // higher, to make panning clearer
	\degree, ~root + (
		Pstep(Pseq([1, 2, 4, -1], inf), 6/5)
		+ Pstep(Pseq([0, -3], inf), 4)
	)
));
)

… or Prout { |ev| ~spawner = ev[\spawner]; loop { 0.yield } } if you don’t want to reassign ~spawner repeatedly.

That’s a bit of a misunderstanding. Pspawn deliberately “lifts” the controlling pattern out of the output stream. It’s true that this is different from (most of) the rest of the pattern system, where every component in a pattern contributes to one output stream. But, in this case, it’s not quite right to think of the controlling events as though they were always part of the main output stream, and Pspawn is violating the contract by removing them. That’s not the design at all. The control events simply don’t (and IMO shouldn’t) belong to the main output stream.

To your point that both ways are 100% valid – I agree in principle but to be honest, I don’t see why you would want to mix these two completely different functionalities into the same output stream. I fully realize that we are looking at the same problem from different angles but, to have a pattern 1/ receiving pre-populated values from some other pattern; 2/ distributing these among control events and note events alike (this part is already not quite making sense to me); 3/ post-filtering all of the control and note events by the same filter pattern (which now requires “programming to the implementation” by writing a conditional to distinguish them) – I simply can’t imagine myself wanting or needing this. (This is possibly a lapse in my own imagination.)

For example, in my code block above, if Pspawn did treat the control events as regular events, then every control event would consume one \pan value, breaking the pattern’s intent to sound notes alternately left or right. The only way to prevent this would be to apply \pan as a post-filter (but I could also imagine cases where you’d want other note-event parameters to depend on pre-populated pan values, then you’re stuck). ----- well, the flaw in this logic is that Ppar’s Rest events will also consume pre-values :laughing: === parallelism is just hard, period.

It is a valid criticism of Pspawn that it doesn’t allow custom behavior for the controller events. That wouldn’t be hard to address, I think.

I wonder if there’s a way to unify your PparStream with Spawner (try to keep code duplication to some sort of a minimum), and then have a couple of client classes (Pspawn more or less as it is now, and your other approach)… because it’s really up to the user to decide which approach they need for a given situation, not up to my bias (I completely admit I’m biased here :wink: ) or yours.

hjh