Pbind Multichannel Expansion

i do this quite a bit. it is such a nice concise idiom for layering patterns

This solution is actually throwing an error for me.

ERROR: Pbind should have even number of args.

The clump(2) was in error I think. try:

SynthDef(\b, { |out, freq=440, sustain=0.2, amp=0.1| Out.ar(out, Blip.ar(freq, 15) * Line.kr(amp, 0, sustain, doneAction:2)) }).add;
SynthDef(\c, { |out, freq=440, sustain=0.2, amp=0.1| Out.ar(out, Saw.ar(freq) * Line.kr(amp * 0.5, 0, sustain, doneAction:2)) }).add;

~pppar = { |...args| Ppar(args.flop.collect(Pbind(*_))) };

(
~pppar.(
	\instrument, [\default, \b, \c], 
	\freq, [Pseq([200, 300, 340], inf), Pseq([220, 230], inf), 400], 
	\dur, [0.3, Pwhite(0.2, 0.4)]
).play
)

while we’re here: how about this?

+ Pbind {
	asPpar {
		 ^Ppar(patternpairs.flop.collect(Pbind(*_)))
	}
}

then we write

Pbind(
  \instrument, [\default, \b, \c], 
  \freq, [Pseq([200, 300, 340], inf), Pseq([220, 230], inf), 400], 
  \dur, [0.3, Pwhite(0.2, 0.4)]
).asPpar.play
+ Pbind {
	asPpar {
		 ^Ppar(patternpairs.flop.collect(Pbind(*_)))
	}
}

This should be added to the Patterns class?

Also, just to clarify, an array of notes will be sent to each instrument individually and not distributed over an array of instruments, correct? That’s the problem that Julian was talking about?

If you want all the parallel patterns to play the same values, and if there’s randomness involved, then you would need to Pseed them all with the same seed. In this example, the three instruments would play different rhythms AFAICS, which wasn’t the original requirement (unless I missed something).

hjh

I think the goal was to have the dur key multi-Channel expand (as it does here). so in the resulting array of Pbinds element 0 has dur: 0.2, element 1, dur: Pwhite(0.2,0.4) and element 2, dur: 0.2 also.

Both the dur and instrument keys muliti-channel expand

You point is well taken though. if the code were instead

Pbind(
  \instrument, [\default, \b, \c], 
  \freq, [Pseq([200, 300, 340], inf), Pseq([220, 230], inf), 400], 
  \dur, Pwhite(0.2, 0.4),
).asPpar.play

each voice would in fact have a different rhythm.

+ Pbind {
	asPpar {
		 ^Ppar(patternpairs.flop.collect({|i| Pseed(0,Pbind(*i)) }))
	}
}

seems to take care of that - good catch… of course user might want the rhythms different… hmmm

in any case I think it might be neat to hack Pbind’s embedInStream method to check if there is a List in either the instrument or dur field Field and if so to substitute Ppar’s embedInStream method using this expansion…

I think some important considerations for a new convenience syntax are:

  • Is it more clear than the original? (It’s easy to make a syntax that is more concise, but this may come at the expense of clarity.)

  • Does it smooth over significant syntactic or semantic hurdles? Or does it replace some hurdles with other hurdles?

In this case, I find myself asking whether a multichannel-expansion idiom for parallel patterns is an improvement over writing… well… Ppar as it exists now. I’m not immediately convinced that it improves clarity – I tend to think that it’s hiding more of the semantics and making the code harder to read.

As for syntactic hurdles, the only one that leaps to mind is, for instance, constant values that should be shared across all the parallel streams. The Ppar way would require them to be duplicated in every pattern; a multichannel approach would write them just once. I’m not sure this is significant enough…? Though that’s a matter of opinion. (And, going back to clarity, if you have a Pbind with \dur multichannel-expansion and, say, \amp not expanding, is \amp expected to be new for every event, or held consistent within some time window, or…? I think the solution to such ambiguities is to write what you mean, and that seeking out interesting ways to avoid writing it out can sometimes get in the way of writing what you mean.)

As an experiment, no harm to try it out. For the main class library, the bar should be set somewhat high, though, to avoid getting stuck with something where problems arise later.

hjh

…but Pbind already does multi-channel expansion!

User doesn’t run into a wall until they try to expand (in OPs case) the instrument key for example.

Expansion would happen here as elsewhere - using flop - so yes amp would be consistent.

Regading whiting “what you mean” - I think its clear enough what I might mean when I write

Pbind( *[ freq: Pwhite(300,400), instrument: [\default, \sawSynth] )

the only reason to have an expectation that Pbind will only expand certain keys is knowing that Event has this limitation. We expect Event to produce multiple Synths - perhaps this would be a more consistent place to put multi-instrument expansion.

In the case of dur you have a point - If we understand Pbind as producing only a continuous stream of Events whose deltas and durs align. (in fact we can already alter this relationship using legato>1 tediously creating multiple “lines” if we like…) That said I found @julian s example above inspiringly terse and readable vs the Ppar version…

I understand what you’re saying regarding the high bar and experimentation - yes absolutely. We are playing here. But let’s take care not to slow things down too much!

It doesn’t.

If it were true that Pbind were responsible for multichannel expansion, then you would be able to find the code in Pbind that performs the multichannel expansion. But you won’t find it, because it isn’t there.

Pbind is not responsible for multichannel expansion in any way.

A couple of counterexamples:

(
p = Pbindf(
	Pbind(
		\freq, Pexprand(200, 800, inf),
		\dur, [0.1, 0.11]
	),
	\amp, Pexprand(0.05, 0.2, inf)
);

// asPpar should apply to the source pattern,
// not the Pbindf
q = p.asPpar.play;
)

(
p = Pbindf(
	Pbind(
		\freq, Pexprand(200, 800, inf),
		\amp, Pexprand(0.05, 0.2, inf)
	),
	\dur, [0.1, 0.11]
);

// DoesNotUnderstand:
// Pbindf is a FilterPattern, not a Pbind subclass
q = p.asPpar.play;
)

(
p = Prout { |event|
	loop {
		event = event.putAll((
			freq: exprand(200, 800),
			amp: exprand(0.05, 0.2),
			dur: [0.1, 0.11]
		)).yield;
	}
};

// no way to implement asPpar at all here
q = p.asPpar.play;
)

The last is particularly troublesome for an asPpar proposal because there is no way to break up the user’s Prout function into separate elements to parallelize. That is, even if asPpar could be made perfectly clear and handle all the cases, there is still a brick wall – it’s just located somewhere else.

Where is the multichannel expansion code, then?

It’s in Event.

						if(strum == 0 and: { (sendGate and: { sustain.isArray })
							or: { offset.isArray } or: { lag.isArray } }) {
							bndl = flopTogether(
								bndl,
								[sustain, lag, offset]
							);
							#sustain, lag, offset = bndl[1].flop;
							bndl = bndl[0];
						} {
							bndl = bndl.flop
						};

						// produce a node id for each synth

						~id = ids = Array.fill(bndl.size, { server.nextNodeID });
						instrumentName = instrumentName.asArray;
						bndl = bndl.collect { | msg, i |
							msg[2] = ids[i];
							msg[1] = instrumentName.wrapAt(i);
							msg.asOSCArgArray
						};

So the solution to multichannel expansion of instrument properly belongs in Event, not in Pbind.

Here, it gets tricky, because there are a lot of places where the default event prototype assumes that the instrument name will be a single symbol. See synthDefName, which handles variant names. (So there’s another case to consider: what if you specify multiple instrument names, but only one variant name, and not all of the SynthDefs provide that variant name? Scsynth prints an error for undefined variants.)

But if you can get around that, then something like the following will collect a union of all the parameters for each instrument, which can then be flopped and performed using the existing logic.

				getSynthArgsFromCurEnvir: #{ |instrument|
					var msgFunc, coll, lib;
					if(instrument.size < 2) {
						// normal case, keep optimized version
						msgFunc = ~getMsgFunc.valueEnvir;
						msgFunc.valueEnvir
					} {
						coll = IdentityDictionary.new;
						lib = ~synthLib ?? { SynthDescLib.global };
						instrument.do { |name|
							msgFunc = lib[name].tryPerform(\msgFunc);
							if(msgFunc.isNil) {
								Error("msgFunc not available for " ++ name).throw;
							};
							msgFunc.valueEnvir.pairsDo { |key, value|
								coll.put(key, value);
							};
						};
						// now you have a union of all key-value pairs for all instruments
						coll.asPairs
					}
				}
p = Pbind(
	\degree, Pn(Pseries(0, 1, 8), inf),
	\dur, [0.1, 0.2]
).play;

Before any progress could be made on “expanding” \dur, it’s necessary to decide on semantics. What does the above pattern mean? I can think of at least two, maybe three different meanings, which the syntax does not disambiguate. Until there is a concrete, unambiguous meaning for “multichannel-expanding dur,” then this problem is a “wouldn’t it be nice if…?” but not something that can be concretely addressed.

hjh

To be clear: I think multichannel expanding “instrument” is a fine idea, and do-able.

dur might not prove to be do-able.

hjh

1 Like

Thanks James I do understand that the expansion is done in Event - as I wrote in the post you were responding to:

…but thanks for the correction (emphatic though it was!) as I did absolutely misspeak! From the beginning user’s point of view though - I should have said that the keys in a Pbind (using default \note type at least) do multi-channel expand (excepting \instrument and \dur).

excellent - thanks for this

regarding semantics I would expect this:

 Pbind(
	\degree, Pn(Pseries(0, 1, 8), inf),
	\dur, [0.1, 0.2]
).play;

to expand (in effect) to:

Ppar([ 
  Pbind(
  	\degree, Pn(Pseries(0, 1, 8), inf),
  	\dur, 0.1
  ),
   Pbind(
  	\degree, Pn(Pseries(0, 1, 8), inf),  
	\dur, 0.2
  )
])

What are the other options you imagine? I think it would be nice for expansion to work everywhere the same (args are flopped before evaluation if that makes sense)

One issue that does spring to mind is values which are arrays - the convention elsewhere is to bubble them to prevent expansion and that would seem like a reasonable enough solution.

indeed

expanding “instrument” would be a win in any case and would solve OPs problem!

expanding dur is a seductive prospect though. would be nice to be able to write voices like

Pbind( *[
  degree: [ Pseq([0, -1, 0),     Pseq([2, 3, 2)],
  dur:    [ Pseq([1.5, 0.5, 1]), Pseq([1, 1, 1)],
  pan:    [-0.2,                 0.2],
])

One way to look at Pbind with arrayed values is: when a key has an array, the array values are distributed over multiple synths, while other single-value keys replicate the same value over multiple synths. One could read that pattern as saying that at any given moment, there is one \degree that is distributed over multiple voices.

In practice, this couldn’t be written in that way because there’s one \degree but multiple timing specifications. Which one should \degree follow? This can’t be resolved with the given syntax, so this isn’t a “proposal” as such. But, one thing that the SC and Pd forums have taught me that everything that is possible to misunderstand will be misunderstood, at some point. If there were an auto-parallelizing Pbind, I would predict that, eventually, somebody would argue “I wrote one \degree pattern and I expect not to hear different degrees playing at the same time” and it takes some thought to grasp – it isn’t immediately obvious – why it doesn’t work that way.

(
p = Pchain(
	Ppar([
		Pbind(\dur, 0.2, \pan, -0.8),
		Pbind(\dur, 0.4, \pan, 0.8)
	]),
	Pbind(
		\degree, Pseg(
			Pn(Pseries(0, 1, 8), inf),
			// oh just for fun, randomize it lol
			Prand([0.2, 0.4], inf),
			\step
		),
		\absTime, Ptime(),
		\amp, 0.5
	)
).play;
)

p.stop;

The other way to look at it is: Each key in Pbind gets one stream. If you parallelize, maybe the stream should be distributed.

(
var degree = Pn(Pseries(0, 1, 8), inf).asStream;

p = Ppar([
	Pbind(
		\degree, degree,
		\dur, 0.2
	),
	Pbind(
		\degree, degree,
		\dur, 0.4
	)
]).play;
)

p.stop;

To sum up 3 viewpoints:

  1. “Parallel” view: A \dur array means that the entire Pbind structure is replicated n times, with different timing.
  2. “One pattern → one concurrent value” view: For keys with fewer than n values, some of the values will be copied into other concurrent voices.
  3. “One pattern → one stream” view: For keys with some m values less than n, you would have only m streams and some of them would evaluate in multiple voices.

I think 3 is unlikely for people to imagine, 2 is somewhat likely but I could see someone guessing this, and 1 is most likely. The point is, though, that stating the semantics in terms of the relationship between patterns and output values, it isn’t so obvious which one is ideal (and I can imagine users at times not wanting auto-parallelizing behavior)… which is where MC-expanding \dur might be a neat trick, but I’m not certain that it’s an improvement over just spelling it out in user code. It might be an improvement, but assuming that “obviously everyone will want #1” may end up not being an improvement.

hjh

Just to propose a different opinion…

Having large monolithic pbinds is incredibly difficult to read and maintain. When there are multiple voices this becomes even worse as you can’t just see one voice at a time, but have to sort through all the subarrays - this is a draw back of multichannel expansion and it occurs in synth contexts too. Having multiple instruments, with potentially different keys will exasperate this and should not be encouraged.

Instead, I think there should be a large section in the documentation detailing and encouraging people to write more modular patterns and use pattern composition to solve there issues. In pbind, there could be a section about the instrument and dur keys that show these issue can be solved with functions and composed from smaller more manageable chunks.

I think there’s one legit use for multiple instruments – “I’ve got these 3 SynthDefs and I want them all to play the same material with the same rhythm together” (kinda like those MIDI piano solos in the Chick Corea Elektric Band albums from the 80s). Currently takes some plumbing to do; I don’t see a problem with making that easier.

I’m still skeptical of folding multiple dur streams into one Pbind, for the reasons you mentioned, and also thinking of the coming efforts to trim some of the fat in the interfaces and reduce the “too many ways to do things.” Would be ok if someone makes an ParallelizingPbind as a quark to try it out (then it’s optional).

hjh

Made a PR that checks there is only one instrument and throws an error telling the user how to fix it.

If some one proposes a better one, I will remove it - this is just a default fix that doesn’t change anything but still provides some useful information to the user.

I wouldn’t mind if, at minimum, someone had a look at First attempt at multiple instruments in the default event prototype · jamshark70/supercollider@3aa6de8 · GitHub before making a decision on that pull request.

(I suppose reductio ad absurdum could lead to someone down the road requesting support for multiple \type values… now this I would be quite comfortable rejecting summarily :laughing: but to play the same material in multiple SynthDefs at the same time strikes me as a reasonable idea.)

hjh

1 Like

Déjà-vu …

Also see this old thread on the list:

https://listarc.cal.bham.ac.uk/lists/sc-users-2018/msg62864.html

@julian made an event type suggestion then:

https://listarc.cal.bham.ac.uk/lists/sc-users-2018/msg62866.html

Actually I spoke too quickly – this is a question that has come up before. It’s easier than multiple instruments, though: change the current “play” function (Event.sc line 480) to “playType” and:

				play: #{
					if(~type.size >= 2) {
						~type.do { |type|
							currentEnvironment.copy.put(\type, type)
							.use { ~playType.() }
						};
					} {
						~playType.();
					}
				},

Of course it’s the user’s responsibility to make sure the shared parameters make sense for all the types.

hjh