One Pattern Playing Multiple Synths

Hi,

you can try with Julian’s \pari event type he suggested a few days ago on the SC mailing list:

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

The synth combos can be defined in a separate pattern, e.g.

(
SynthDef(\synth_0, {|out = 0, freq = 440, amp = 0.1, pan = 0|
    Out.ar(out, Pan2.ar(SinOsc.ar(freq, mul: amp), pan) * EnvGate.new)
}).add;

SynthDef(\synth_1, {|out = 0, freq = 440, amp = 0.1, pan = 0|
    Out.ar(out, Pan2.ar(Saw.ar(freq, mul: amp), pan) * EnvGate.new)
}).add;


SynthDef(\synth_2, {|out = 0, freq = 440, amp = 0.1, pan = 0|
    Out.ar(out, Pan2.ar(Pulse.ar(freq, mul: amp), pan) * EnvGate.new)
}).add;

SynthDef(\synth_3, {|out = 0, freq = 440, amp = 0.1, pan = 0|
    Out.ar(out, Pan2.ar(Formant.ar(freq, mul: amp), pan) * EnvGate.new)
}).add;


Event.addEventType(\pari, {
    var instr = ~instrument;
    ~type = \note;
    if(instr.isArray and:  { instr.isString.not }) { // just to make sure
        instr.do { |each|
            ~instrument = each;
            currentEnvironment.play
        }
    } {
        currentEnvironment.play
    }
});
)


(
p = Pbind(
    \midinote, Pstutter(Pwhite(3, 8), Pwhite(60, 90)),
    \amp, Pstutter(Pwhite(3, 8), Pseq([0.05, 0.1], inf)),
    \dur, 0.1,
    \type, \pari
);

q = Pbind(
    \instrument, Pstutter(Pwhite(1, 3), Pseq([[0], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]], inf))
    .collect { |c| c.collect { |i| ("synth_" ++ i).asSymbol } }
) <> p;

x = q.trace.play
)

x.stop
1 Like

Yes! Thank you Daniel. In my case, the instrument key doesn’t need to be a pattern itself but it will be good to keep that in mind.

I often struggle with finding ways to do this. The most clear and flexible I’ve come up with is using Pchain (usually via the <> operator) with Ppar, Pbinds, captured in Pdef's. Ex:

(
SynthDef(\saw, {
	var sig, env;
	env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
	sig = LPF.ar(Saw.ar(\freq.kr, env), (1 + \filt.kr) * 2000);
	Out.ar(\out.kr, sig);
}).add;

SynthDef(\pulse, {
	var sig, env;
	env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
	sig = LPF.ar(Pulse.ar(\freq.kr, env, env), (1 + \filt.kr) * 600);
	Out.ar(\out.kr, sig);
}).add;

Pdef(\twoInstruments, Ppar([
	Pbind(\instrument, \saw, \octave, 4),
	Pbind(\instrument, \pulse, \octave, 5, \degree, Pkey(\degree) + Pseq([2, 4, 9], inf))
]));

Pdef(\rotate, Pbind(\filt, Penv([-0.5, 1.4, -0.5], [30, 30], \sin).repeat));

Pdef(\pattern, Pdef(\twoInstruments) <> Pbind(
	\scale, Scale.melodicMinor,
	\degree, Pstutter(16, Pseq([0, -2, -1.5, 4.05, 7], inf)),
	\dur, 0.25,
	\legato, 5
) <> Pdef(\rotate)).play;
)

4 Likes

Hi Scott

combining Pchain and Ppar is a nice idea - actually I think that one advantage of this approach doesn’t show up in your example: Ppar’s characteristic to turn several patterns into one stream – in other contexts often unwanted – here ironically turns into a strength: you can define a random sequence in your source template and it’s taken over!

Your example with a random sequence

(
SynthDef(\saw, {
    var sig, env;
    env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
    sig = LPF.ar(Saw.ar(\freq.kr, env), (1 + \filt.kr) * 2000);
    Out.ar(\out.kr, sig);
}).add;

SynthDef(\pulse, {
    var sig, env;
    env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
    sig = LPF.ar(Pulse.ar(\freq.kr, env, env), (1 + \filt.kr) * 600);
    Out.ar(\out.kr, sig);
}).add;

Pdef(\twoInstruments, Ppar([
    Pbind(\instrument, \saw, \octave, 4),
    Pbind(\instrument, \pulse, \octave, 5, \note, Pkey(\note) + Pseq([-5, 0, 4], inf))
]));

Pdef(\rotate, Pbind(\filt, Penv([-0.5, 1.4, -0.5] + 0.5, [30, 30], \sin).repeat));

// always 8 repeats, random base note taken for both voices
// took note instead of degrees because with microtones note gives more control 

Pdef(\pattern, Pdef(\twoInstruments) <> Pbind(
    \note, Pstutter(16, Pwhite(-10.0, 5.0)),
    \dur, 0.25,
    \legato, 1
) <> Pdef(\rotate)).trace.play;
)

This works fine as long as the number of voices is constant, if not, then other measure would have to be taken, e.g. fiddling with rests, but I suppose in this case Julian’s event type would be more comfortable.

(
SynthDef(\saw, {
    var sig, env;
    env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
    sig = LPF.ar(Saw.ar(\freq.kr, env), (1 + \filt.kr) * 2000);
    Out.ar(\out.kr, sig);
}).add;

SynthDef(\pulse, {
    var sig, env;
    env = Env.adsr().kr(gate:\gate.kr(1), doneAction:2);
    sig = LPF.ar(Pulse.ar(\freq.kr, env, env), (1 + \filt.kr) * 600);
    Out.ar(\out.kr, sig);
}).add;

Pdef(\rotate, Pbind(\filt, Penv([-0.5, 1.4, -0.5] + 0.5, [30, 30], \sin).repeat));

Pdef(\pattern, Pdef(\polyInst) <> Pbind(
	\instrument, Pseq([ \saw, [\pulse, \saw], \pulse], inf),
    \noteBase, Pstutter(16, Pseq([0, -2, -1.5, 4.05, 7], inf)),
	\noteOffset, Pseq([2, 4, 9], inf),
	\note, Ptuple([Pkey(\noteBase), Pkey(\noteBase) + Pkey(\noteOffset)]),
	\dur, 0.25,
	\legato, 2
) <> Pdef(\rotate)).play;
)

The question is, what is Pdef(\polyInst)? In other words, can you have a mix-in Pbind that enables polyphonic instrument parameters “naturally” without using Ppar or creating a new event type from scratch (with the requisite code duplication, potential for changed behavior, etc)…

Considering the amount of time I’ve spent trying to crack this one, I’m pretty happy that this thread rattled loose whatever final neuron was required.

the answer below.....
Pdef(\polyInst, Pbind(
	\getMsgFunc, {
		|instrument|
		var funcs;

		if (instrument.isKindOf(Array)) {
			funcs = instrument.collect({
				|i|
				Event.parentEvents[\default][\getMsgFunc].value(i)
			});
			~instrument = instrument;
			{
				funcs.collect({
					|f|
					f.valueEnvir
				}).flop
			}
		} {
			Event.parentEvents[\default][\getMsgFunc].value(instrument)
		};
	},
	\synthDefName, {
		if (~instrument.isArray) {
			var collectedInstruments, instrument = ~instrument;
			collectedInstruments = instrument.collect({ 
				|i|
				~instrument = i;
				Event.parentEvents[\default][\synthDefName].valueEnvir;
			});
			~instrument = instrument;
			collectedInstruments;
		} {
			Event.parentEvents[\default][\synthDefName].valueEnvir
		}
	}
));

2 Likes

Surprisingly, this seems to work just fine even when you e.g. use \strum and \lag, which deal with the bundle structure directly - there may still be some other places that expect a monophonic \instrument, but they’d probably be easy to fix.

gonna bookmark this one

1 Like

Actually you are transfering an “event-type-like definition” to a dedicated helper pattern.
Not tested yet, but I can imagine the merits to do so, you don’t block the option to use other event types that way.

I discovered, that one pattern playing multiple synths works out of the box when using the \set event type.

This pattern

Pbind(
    \type, \set,
    \id, Pfunc({ [synth1.nodeID, synth2.nodeID] }),
    \degree, Pwhite(0, 4)
)

works just as you would expect and want. Perhaps the only reason it isn’t supported out of the box with the \note event type has to do with the complication of looking up the synth by name in the SynthLib simply not being implemented - as opposed to some intrinsic limitation of Patterns. It seems like it would be nice to have this capability built-in.

The problem is the case with multiple instruments, with one instrument it’s straight:

// can hardly be be heard here, but see number of running synths

Pbind(
	\amp, 0.1!2, // you need at least one array to indicate expansion
	\degree, Pwhite(0, 4)
).play


// audible here

Pbind(
	\degree, Pwhite(0, 4),
	\ctranspose, [0, 0.5]
).play


// 3 synths, usual mapping convention: 
// L gets ctranspose 0
// Mid gets ctranspose 0.5
// R gets ctranspose 0

Pbind(
	\degree, Pwhite(0, 4),
	\pan, [-0.9, 0, 0.9],
	\ctranspose, [0, 0.5] 
).play

Again, if the problem is a limitation in the Event, you can address it in the Event:

(
Event.addEventType(\polySynth, { |server|
	var original = currentEnvironment.copy
	// note, don't omit this! or you get infinite recursion
	.put(\type, \note);
	~instrument.do { |instrument|
		original.copy.put(\instrument, instrument).play;
	};
});

// for demo purposes, two SynthDefs with different controls
SynthDef(\a, { |out, gate = 1, freq = 440, amp = 0.1|
	Out.ar(out, DC.ar(0));  // doesn't matter
}).add;

SynthDef(\b, { |out, gate = 1, freq = 440, pan = 0|
	Out.ar(out, DC.ar(0));  // doesn't matter
}).add;
)

(type: \polySynth, instrument: [\a, \b]).asOSC.do(_.postln);

[ 0.0, [ 9, a, 1000, 0, 1, out, 0, freq, 261.6255653006, amp, 0.1 ] ]  <<-- 'a' finds 'amp'
[ 0.0, [ 9, b, 1001, 0, 1, out, 0, freq, 261.6255653006, pan, 0.0 ] ]  <<-- 'b' finds 'pan'
[ 0.8, [ 15, 1000, gate, 0 ] ]
[ 0.8, [ 15, 1001, gate, 0 ] ]

hjh

Right, that’s what Julian suggested with the \pari event type.
BTW, a problem with old links to the mailing list is, that you’d have to update them with the year, so

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

must become

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

What I’m wondering is if it just works with the \set event type why doesn’t it just work with the \note event type? From a high level aren’t they doing exactly the same thing?

What I remember from old discussions especially with Julian - and I have to agree - is that multi-synth expansion becomes either ambivalent or super-complicated if you have arrays of different lengths which should be mapped to different instruments.

But indeed, maybe it’s worth rethinking it … late here, so just some loose thoughts …

I think the case with type ‘set’ and multiple ids is a less problematic one: the ids for the event are given, it’s easier then to find a convention to map all other data - arrays of possibly different lengths - to those.
But in the case of type ‘note’ and a given array of instruments the ids are not yet there, the decision what and how many synths should be created has to be done, and will depend on the other data too: again arrays of possibly different lengths. So here freedom for a new convention is a greater and more difficult one. I don’t say that it’s impossible to cope with that, but nobody has done it so far.

I’ll try to sketch the problems than would occur. Suppose you have an event like this, when played, expansion works according to the current convention, 4 synths with instrument \a, freqs 100 to 400 and pannings -1, 0, 1, -1.

// (0)

(
	\instrument, \a,
	\freq, [100, 200, 300, 400],
	\pan, [-1, 0, 1]
)

Now, what should be done with this ?

(
	\instrument, [\a, \b],
	\freq, [100, 200, 300, 400],
	\pan, [-1, 0, 1]
)

(A) According to the current convention we could produce 4 synths with frequencies 100 to 400 and instruments \a, \b, \a, \b.

(B) On the other hand one might think, no, with this writing I want the expansion of type (0), but on two instruments.

Both thoughts are legitimate, but which one should be preferred ? Ok, we could decide for two types, say ‘noteX1’ and ‘noteX2’ but next, if we invent the opportunity to have multiple instruments we would also want to differentiate the inputs for both instruments, which could again be numbers or arrays. This forces to cope with the handling of unpleasant nested lists and their possible ambiguities, the option of array arguments in either instrument makes it yet worse! All in all this looks like a nightmare to implement and I’m not surprised that nobody wants to touch it …

I kinda think by default it should just let flop figure it out (I believe this is what the \set event type is doing). If your arrays are symmetrical (or you only specify 1 instrument) then you’re good. If not, you end up with this:

[ [ a, 100, -1 ], [ b, 200, 0 ], [ a, 300, 1 ], [ b, 400, -1 ] ]

which would result in valid messages sent to the server but may not produce the sounds you want. That’s the point when a custom event type or other technique could come into the picture.

For the instrument case, you unfortunately can’t let the flop figure it ALL out :slight_smile:
The problem is — as it is, the flop happens once you have produced an argument list from your event, e.g.:

(
\freq, [100, 200]
)

… becomes

[\freq, [100, 200]]

…which flops to:

[[\freq, 100], [\freq, 200]]

The tricky thing is, that second form is produced by the value in \instrument (via ~msgFunc): you need to get the instrument in order to produce the arg array.

I think the totally-correct answer here would require: for arrayed instrument cases, you have to (for each instrument) grab event values from each key using something like ~event[key].wrapAt((n * instrumentCount) + instrumentIndex), where your n iterates until you’re at the MAX size of the largest array in your event. THEN you flop the arrays for each instrument. So, for something like:

(
\instrument, [\foo, bar],
\freq, [100, 200, 300],
\amp, [1, 2, 3, 4]
)

…would give you roughly:

[
[\foo, [\freq, [100, 300], \amp, [1, 3]]],
[\bar, [\freq, [200, 100], \amp, [2, 4]]]
]

… which flops out to:

[
   [\foo, [\freq, 100, \amp, 1]],
   [\foo, [\freq, 300, \amp, 3]],
   [\bar, [\freq, 200, \amp, 2]],
   [\bar, [\freq, 100, \amp, 4]],
]

There may be a combinations of flops and array ops in SC already that can deal with this more elegantly… I’d love to hear if someone can figure out how to get from my initial Event example to something like that final array, which is about what we need to send a message to the server.

(This is even hiding some complexity - remember that in order to get to that second representation, e.g. one array per instrument - you have to call ~msgFunc on the Event. So, you really need to produce something like [\foo, (\freq, [100, 300], \amp, [1, 3])] in that second step - where the event needs to be passed to the ~msgFunc)

Thought of another way of achieving this using the \phrase event type. You can pass the phrase pattern values to the instrument pattern where a Ppar can play multiple instruments using those passed-in values.

(
Pdef(\inner, {|degree, foo|
	"--".postln;
	Ppar([
		Pbind(
			\instrument, {|freq| SinOsc.ar(freq) * Env.perc.ar(doneAction:Done.freeSelf) }.asSynthDef.add,
			\degree, degree.debug(\degree_a), 
			\octave, 6, 
			\foo, foo.debug(\foo_a)
		),
		Pbind(
			\instrument, {|freq| Saw.ar(freq) * Env.perc.ar(doneAction:Done.freeSelf) }.asSynthDef.add, 
			\degree, degree.debug(\degree_b), 
			\octave, 3, 
			\foo, foo.debug(\foo_b)
		)
	])
});
Pdef(\outer, 
	Pbind(
		\type, \phrase,
		\instrument, \inner,
		\degree, Pbrown(0, 4, 1),
		\dur, 0.25,
		\foo, Pwhite(0, 4)
	)
).play;
)
Pdef(\outer).stop;

I hadn’t seen this technique before.

1 Like

…So did I :wink: