# Passing and mapping multichannel control envelopes Pbind

hey, im writing a Gauss Curve to a Buffer to control the partials on an additive synth Strategy to control an additive synth. when you modulate the phase of LFGauss you can get a kind of Bandpass filter sweep through the partials. I was trying to modulate the phase of LFGauss with a Control Envelope ctrlEnv inside a Pbind and use ~bus[\ctrl][0] to map it to \phase of the source synth and was playing around with multichannel expansion and setting releaseNodes and loopNodes for the ctrlEnv and not sure how the multichannel expansion has to be done correctly.

\ctrlEnv, Pfunc{|e|
Env([0, 1, [2, 4], 1], [0.0001, [e.atk, 0.01], e.rel], \sin, releaseNode: 2, loopNode: 0);
},


What has to be done that ctrlEnv does multichannel expand or is this not possible at all with passing envelopes to patterns?
1.) Do I need more Channels for ~bufAmps and how many?
2.) Do I have to multichannel expand the namedControl \phase in the SynthDef with ! 2 for example? iphase: ( \phase.kr(0, 0.5) ! 2 ).mod(4pi)?

3.) Im also not sure if ctrlEnv needs DoneAction:Done.freeSelf when i set \dur, inf inside the Pbind and set a loopNode and releaseNode. Or if there is a better setting for the looping Envelope in general.

4.) I would like to have a flexible ctrlEnv SynthDef which could be used for several cases so i was wondering if its good to use methods like .linexp etc. inside the SynthDef which are already to hardcoded for different cases i think. is there a good way to have a flexible control Envelope SynthDef with a variable ModDepth which could be declared in the actual Pbind and not in the SynthDef?
any other ideas? thanks a lot

(
~numPartials = 50;
~bufAmps = Buffer.alloc(s, ~numPartials, 1);

var gainEnv = \gainEnv.ar(Env.newClear(8).asArray);

var numPartials = ~numPartials;
var bufAmps = ~bufAmps;
var sig;

gainEnv = EnvGen.kr(gainEnv, \gt.kr(1), doneAction: Done.freeSelf);

BufWr.ar(
LFGauss.ar(
duration: SampleDur.ir * numPartials * \factor.kr(1, 0.5).reciprocal,
width: \width.kr(0.2, 0.5),
iphase: \phase.kr(0, 0.5).mod(4pi),
),
bufnum: bufAmps,
phase: Phasor.ar(end: numPartials)
);

sig = Array.fill(numPartials, {|i|
var freqs, partials;
freqs = \freq.kr(150) * ((i * 2) + 1);
partials = SinOsc.ar(
freq: freqs,
mul: Index.ar(bufAmps, i)
) / numPartials;
}).sum;

sig = LeakDC.ar(sig);

sig = sig * gainEnv;

sig = Pan2.ar(sig, \pan.kr(0), \amp.kr(0.25));
Out.ar(\out.kr(0), sig);

SynthDef(\ctrlEnv, {
var sig, ctrlEnv;
ctrlEnv = \ctrlEnv.ar(Env.newClear(12).asArray);
sig = EnvGen.kr(ctrlEnv, \gt.kr(1), doneAction: Done.freeSelf);
Out.kr(\out.kr(0), sig)
)

(
Pbind(
\type, \hasEnv,

[...]

\phase, ~bus[\ctrl][0].asMap,

\atk, 1.5,
\sus, 4,
\rel, 2.5,

\gainEnv, Pfunc{|e|
Env.linen(e.atk, e.sus, e.rel, curve: \sine)
},

\sustain, Pfunc { |ev| ev[\gainEnv].duration },

[...]
)
);

Pdef(\ctrlEnv,
Pbind(
\type, \hasEnv,
\instrument, \ctrlEnv,

\dur, inf,

\atk, 0.3,
\rel, 0.3,

\ctrlEnv, Pfunc{|e|
Env([0, 1, [2, 4], 1], [0.0001, [e.atk, 0.01], e.rel], \sin, releaseNode: 2, loopNode: 0);
},
\sustain, Pfunc { |ev| ev[\ctrlEnv].duration },

\out, ~bus[\ctrl][0],
\group, ~ctrlGrp,
)
);
)

(
).play(t, quant:1);
)


I’m afraid I can’t quite follow the structure of what you’re trying to do – I can at least explain what the multichannel envelope is doing, and then you can figure out how to work with that.

When you’re talking about parameters in a Pbind being mapped to synth(s), this is the job of Event. So the real question is, what does Event do with a multichannel envelope?

Event first uses information from the SynthDef (SynthDesc.global.at(~instrument).msgFunc) to make an array of argument pairs: [name, value, name, value ...].

Some of the values could be arrays. These need to be expanded out into multiple argument lists, by .flop. If one of the values is a two-element array, .flop will produce an array of two argument lists, distributing those two values between them:

[a: [1, 2], b: 3].flop
-> [ [ a, 1, b, 3 ], [ a, 2, b, 3 ] ]


So then you would get two synths: one with the first argument list, the other with the second.

Then each one of these gets processed by .asOSCArgArray, which expands Env objects and handles subarrays.

So let’s test that. Because the Env() is Pfunc-ed, the event will contain the Env object directly, so the argument list will just hold the Env.

// simple env
e = Env([0, 1, 2, 1], [0.0001, 3, 7]);

[ctrlEnv: e].flop.collect(_.asOSCArgArray).asCompileString
-> [ [ 'ctrlEnv', $[, 0, 3, -99, -99, 1, 0.0001, 1, 0, 2, 3, 1, 0, 1, 7, 1, 0,$] ] ]

// two-channel env
e = Env([0, 1, [2, 4], 1], [0.0001, [3, 5], 7], \sin, releaseNode: 2, loopNode: 0);

[ctrlEnv: e].flop.collect(_.asOSCArgArray)
-> [ [ 'ctrlEnv', $[,$[, 0, 3, 2, 0, 1, 0.0001, 3, 0, 2, 3, 3, 0, 1, 7, 3, 0, $],$[, 0, 3, 2, 0, 1, 0.0001, 3, 0, 4, 5, 3, 0, 1, 7, 3, 0, $],$] ] ]


The $[ and $] characters are just tokens to tell the server that the data in between should be treated as an array.

So the simple case is an outer array containing one argument list, containing an arrayed value for ctrlEnv.

The two channel case is an outer array containing one argument list, containing an array of arrays for ctrlEnv.

The server does not support multidimensional arrayed arguments. The only way to get multidimensional arrays into a Synth is that the SynthDef should define a flat array, and unpack the array within the SynthDef structure, and the Synth argument list needs to collapse the multidimensional array: serialize for transmission, and de-serialize on the other side.

TL;DR You can’t use multichannel Envs directly with Events. You’ll have to write extra code to manipulate the structure.

hjh

hey, thanks for your reply I have to read it more carefully to have an idea what has to be changed in my approach.
my intial post is a bit confusing ideed

in general i would like to have all the control Structures outside the actual Synthdef, thats the reason for passing the envelopes with \type, \hasEnv and i was also trying to have Control Envelopes or LFOs which are seperated from the actual Pbind which plays the SynthDef. so i can either have a continous LFO type control or for example a looping Envelope on a different timeline.

The reason for sharing the actual \additive SynthDef and not a \test SynthDef was, that its using BufWr to write LFGauss to a global Buffer ~bufAmps and indexes this buffer to control the amplitudes of the different partials of the SinOsc inside the \additive SynthDef.

The parameter ive been trying to control with a seperate, looping multichannel Envelope ctrlEnv is \phase of LFGauss (to get a kind of bandpass filter sweep), which is written to ~bufAmps. so i was wondering if ctrlEnv is a multichannel envelope ~bufAmps needs also more channels than only 1.

its already working nicely without multichannel expansion (was fooling around with multichannel expansion for the LFTri though, not sure if its working correctly of the same reasons)
two sound examples: one uses a LFO and the other one the ctrlEnv

LFO

(
Pbind(
\type, \hasEnv,

[...]

\phase, ~bus[\ctrl][0].asMap,

[...]

)
);
)

(
x = { Out.kr(~bus[\ctrl][0], LFTri.ar(0.5 * [1.000, 1.003]).linexp(-1, 1, 1, [2.1, 2])) }.play;
)


looping Control Envelope ctrlEnv

(
Pbind(
\type, \hasEnv,

[...]

\phase, ~bus[\ctrl][0].asMap,

[...]

)
);
)
(
Pdef(\ctrlEnv,
Pbind(
\type, \hasEnv,
\instrument, \ctrlEnv,

\dur, inf,

\atk, 0.5,
\rel, 0.3,

\ctrlEnv, Pfunc{|e|
Env([0, 1, 2, 1], [0.0001, e.atk, e.rel], \sin, releaseNode: 2, loopNode: 0);
},
\sustain, Pfunc { |ev| ev[\ctrlEnv].duration },

\out, ~bus[\ctrl][0],
\group, ~ctrlGrp,
)
);
)

(
).play(t, quant:1);
)


Basically, split them into separate Envs. Or, get the asArray result and parcel out the subarrays under your own control.

I haven’t tested this, but it may work to do Pfunc { Env(... multichannel ...).asArray } – then each envelope will get its own synth. You’d have to supply multiple \out buses (otherwise they’ll be summed) but this might get you most of the way there. – That’s assuming you’re using separate synths for the control envelopes.

hjh

okay it works with the LFOs but not with the ctrlEnv.

(
~numPartials = 50;
~bufAmps = Buffer.alloc(s, ~numPartials);

[...]

BufWr.ar(
LFGauss.ar(
duration: SampleDur.ir * numPartials * \factor.kr(1, 0.5).reciprocal,
width: \width.kr(0.2, 0.5),
iphase: \phase.kr(0, 0.5).mod(4pi).poll,
),
bufnum: bufAmps,
phase: Phasor.ar(end: numPartials)
);

[...]

Out.ar(\out.kr(0), sig);

SynthDef(\ctrlEnv, {
var sig, ctrlEnv;
ctrlEnv = \ctrlEnv.ar(Env.newClear(12).asArray);
sig = EnvGen.kr(ctrlEnv, \gt.kr(1), doneAction: Done.freeSelf);
Out.kr(\out.kr(0), sig)
)

(
Pbind(
\type, \hasEnv,

[...]

\phase, [~bus[\ctrl][0], ~bus[\ctrl][1]].collect(_.asMap),

[...]
)
);

Pdef(\ctrlEnv,
Pbind(
\type, \hasEnv,
\instrument, \ctrlEnv,

\dur, inf,

\ctrlEnv, Pfunc{|e|
Env([0, 1, [2, 4], 1], [0.0001, [0.5, 0.01], 0.3], \sin, releaseNode: 2, loopNode: 0).asArray
},
\sustain, Pfunc { |ev| ev[\ctrlEnv].duration },

\out, [~bus[\ctrl][0], ~bus[\ctrl][1]],
\group, ~ctrlGrp,
)
);
)

// LFOS
(
x = { Out.kr(~bus[\ctrl][0], LFTri.ar(0.5).linexp(-1, 1, 1, 2)) }.play;
y = { Out.kr(~bus[\ctrl][1], LFTri.ar(2).linexp(-1, 1, 3, 4)) }.play;
)

// ctrlEnv
(
).play(t, quant:1);
)


might be related
https:/scsynth.org/t/maximum-number-of-synth-controls/3323/6

Im not sure but i think it works when additionally multichannel expanding the ctrlEnv in the ctrlEnv SynthDef. does this make any sense? at least it gives no error and i hear a correlation of the two envs.

(
SynthDef(\ctrlEnv, {
var numChannels = 2;
var sig, ctrlEnv;
ctrlEnv = \ctrlEnv.kr(Env.newClear(12).asArray) ! numChannels;
sig = EnvGen.kr(ctrlEnv, \gt.kr(1), timeScale: \time.kr(1), doneAction: Done.freeSelf);
Out.kr(\out.kr(0), sig)
)

(
Pdef(\ctrlEnv,
Pbind(
\type, \hasEnv,
\instrument, \ctrlEnv,

\dur, inf,

\ctrlEnv, Pfunc{|e|
Env([0, [1, 3], [2, 4], [1, 3]], [0.0001, [0.8, 0.6], [0.3, 0.4]], [\sin, \lin], releaseNode: 2, loopNode: 0)
},
\sustain, Pfunc { |ev| ev[\ctrlEnv].duration },
\time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo },

\out, [~bus[\ctrl][0], ~bus[\ctrl][1]],
\group, ~ctrlGrp,
)
);
)