Pbind Multichannel Expansion

Hi -

I am trying to understand a particular use case of multichannel expansion with Pbind.

Pbind(
	\instrument, Pseq([[\default, \sine, \sine]], inf),
	\freq, Pn([400, 900, 300], inf),
	\dur, 0.9).play;

Here I have a cluster of notes being played. I would like each note to play on a different instrument. I suspect this might not be possible, as is. I was hoping someone could help explain what is happening here.

Thanks a lot.

nope the instrument key won’t expand sadly (the default EventType is \note IIRC which only supports a single instrument - you might hack together a multiple-instrument EventType I suppose…)

but how about

(
Ppar(
	[
		\instrument,[\sawSynth, \default, \wash],
		\freq,[400,900,300],
		\dur:0.900
	].flop.collect{|a|
		Pbind(
			*a
		)
	}
).play
)

Here you do the multi-channel-expansion outside the Pbind using flop

you could omit the Ppar and use do instead of collect for brevity but timing might suffer

1 Like

Thank you - this definitely works.
I am curious, though, to learn about how to “hack together a multiple-instrument EventType”, though.

I know how to make a new event type… but I am not sure how to specific that it would expand to multiple instruments…

Event.addEventType(\multinstrument, {
		~instrument = ~instrument.do{|x| }
});

Maybe something where it iterates through a list? I am definitely lost here.

Close!!

maybe something like…

Event.addEventType(\multipleInstruments,{
    ~instrument.collect{|i| 
        (type:\note,instrument:i, parent:currentEnvironment).play
    }
})

The problem of this approach is that the other keys don’t expand together with the instrument. When I made more of the event arguments expandable, I intended to make instrument to expand too, of course. But the problem is that the arguments themselves (which should be expanded) depend on the instrument. Thus, you will often end up with different lists of arguments that need to be expanded together. This makes the implementation complicated and potentially hard to maintain, so I left this one as an exception (apart from dur, which is an exception for other reasons).

1 Like

thanks for this explanation @julian - that makes total sense!

… I can imagine a solution in which we look at the synthdescs and expand args appropriately. Folly?

1 Like

Another thing you might consider is pattern composition and functions…

~base = Pbind (
    // put whatever is shared here
	\dur, 0.9,
	\a_bunch_of_keys, 1
);

~mkInstFreqPar = {|...instrFreqArray|
	Ppar(instrFreqArray.collect{|a|
		Pbind(\instrument, a[0], \freq, a[1])
	}, inf)
};

~pat = ~base <> ~mkInstFreqPar.(
	[\default, 400],  // these values can be patterns
	[\sine, 900],
	[\sine, 300]
)

~pat.trace.play

Alternatively you could do it like this, which is similar to Michael’s original

~mkParPbind = {|...bindInfos|
	Ppar(bindInfos.collect{|a| Pbind(*a) }, inf)
};
~pat = ~base <> ~mkInstFreqPar.(
	[\instrument, \default, \freq, Pwhite(200, 300)],  
	[\instrument, \sine, \freq, 900, \somethingElse, 3]
);
2 Likes

Ah that is a good direction to go. Like this you can expand all the parameters, even dur!



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.clump(2).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
)
3 Likes

should Pbind.play check if the instrument (or dur!) key is a List and return ~ppar.(patternpairs).play if so ?

or add a playMulti method? (toPpar.play expandAndPlay)

It would be nice not to answer this question every 6 weeks!

1 Like

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