Lazily populate fields when handling multichannel or multivoice Events

Here’s simple trick I just worked out.

Problem:

I’m generating Events with arrayed parameters. These will get multi-channel expanded by the Event system into multiple voices. For example, something like:

Pbind(
  \degree, [0,2]
)

…produces two Synths, one for each specified \degree value.

How MANY voices I ultimately get is dependent on the array sizes of my Event values (specifically: the maximum size). This voice count may change over time in my pattern, and may not be simple to determine a priori (e.g. maybe I’m generating these events based on complex combinations of Pbinds/patterns, or playing them live via a MIDI device).

So, what if I want each voice to have a unique value - for example, I want each one to output to a different Bus? I can’t simply add a list of buses like (bus: [b1, b2, b3, b4...]) - as per above, it’s not easy to determine my voice count, so I can’t tell how many buses to add. I want something that will lazily supply values for any number of voices, but a value that itself doesn’t cause a multi-channel expansion.

Solution:

Set your Event value to a Routine that incrementally yields new values. When the Event system is preparing values for the Synth it’s creating, it calls .asControlInput on each item. .asControlInput, by default, forwards to Object:value - and for Routine, .value forwards to .next, which fetches a new value. So, every time your Routine is embedded in a new OSC message, a new value is requested - this gets us our desired behavior: lazy supply different values for each voice without forcing multichannel expansion.

~buses = 12.collect { Bus.audio(s, 2) };
Pbind(
    \degree, [0, 2],
    \out, Pfunc({
       Routine({
         ~buses.do {
           |bus|
           bus.asControlInput.yield
         }
       })
    })
);

You can see in my resulting OSC messages:

[ "#bundle", 16697629428674429394, 
  [ 9, "default", 1060, 0, 1, "velocity", 88.9, "freq", 261.626, "pan", 0, "amp", 0.1, "out", 76 ],
  [ 9, "default", 1061, 0, 1, "velocity", 88.9, "freq", 329.628, "pan", 0, "amp", 0.1, "out", 78 ]
]

This works well for regular \note events, and for any Event key that shadows a Synth parameter (since these are the ones that are unpacked with a asControlInput call). Outside of these cases, it may not work as anticipated.

This works for implementing something like voice groups as well:

~voiceGroups = 12.collect { Group() };

Pbind(
	\degree, [0, 2, 4],
	\group, Pfunc({
		Routine({
			~voiceGroups.do {
				|b|
				b.asControlInput.yield
			}
		})
	}) 
).play;

Another example: I have one overall “chord” - I want to lazily pull notes from this depending on how many events I’m producing.

Pbind(
	\chord, [0, 2, 4, 5, 9],
	\degree, Pfunc({
		|e|
		Routine({
			e[\chord].do(_.yield)
		})
	}),
	\amp, Pclump(
		Prand([2, 3, 4, 5], inf),
		Pseq([0.2], inf)
	)
).play

In this example, the \amp key is determining how many voices we have, and the degree values are pulled lazily from \chord - if amp returns 2 values, then we get the first two notes in the chord.

We can easily make a utility Pdefn that will transform arrayed inputs into “lazy” arrays:

(
Pdefn(\lazyExpand, Pfunc({
	|array|
	Routine({
		array.do(_.yield)
	})
}));

Pbind(
	\amp, Pseq([
		0.5 ! 1,
		0.5 ! 2,
		0.5 ! 3,
	], inf),
	\degree, Pdefn(\lazyExpand) <> Pseq([ [0, 1, 2] ], inf)
).trace.play
)
5 Likes

This is great! Could this be added to the ide help files?

A good idea - but I’d want to give it some more time and testing in my code to make sure it behaves well in a wider variety of cases.