there’s a few issues with the multichannel handling here. do look at overview helpfile called Multichannel Expansion. also recommend helpfile for Control.
-
.dup makes a new array with two copies of its operand, use this if you want to send a mono signal to L/R in an Out.
-
the .slice is returning some array of channels, then you duplicate that array, and send it to stereo output, dunno what will happen but you won’t hear everything in the original array.
i think the array manipulations are over-complicated for what you want. and it can get confusing when working with OutputProxys - these are under-hood placeholders for ugens that can have variable channel counts / rates. (at synthdef compilation time, we don’t know what will be passed into the Controls that bring synth arguments in - could be floats or signals.)
anyways, for mono output, this works for me (de-commented for brevity and emphasis) - i am just being a caveman and multiplying the carriers by the “on/off” arguments
SynthDef.new(\polyFM7, {
arg out, amp=0.2, amplag=0.02, gate=1, hz,
hz1=1, hz2=2, hz3=3, hz4=4, hz5=5, hz6=6,
amp1=1,amp2=0.5,amp3=0.3,amp4=1,amp5=1,amp6=1,
phase1=0,phase2=0,phase3=0,phase4=0,phase5=0,phase6=0,
ampAtk=0.05, ampDec=0.1, ampSus=1.0, ampRel=1.0, ampCurve= -1.0,
hz1_to_hz1=0, hz1_to_hz2=0, hz1_to_hz3=0, hz1_to_hz4=0, hz1_to_hz5=0, hz1_to_hz6=0,
hz2_to_hz1=0, hz2_to_hz2=0, hz2_to_hz3=0, hz2_to_hz4=0, hz2_to_hz5=0, hz2_to_hz6=0,
hz3_to_hz1=0, hz3_to_hz2=0, hz3_to_hz3=0, hz3_to_hz4=0, hz3_to_hz5=0, hz3_to_hz6=0,
hz4_to_hz1=0, hz4_to_hz2=0, hz4_to_hz3=0, hz4_to_hz4=0, hz4_to_hz5=0, hz4_to_hz6=0,
hz5_to_hz1=0, hz5_to_hz2=0, hz5_to_hz3=0, hz5_to_hz4=0, hz5_to_hz5=0, hz5_to_hz6=0,
hz6_to_hz1=0, hz6_to_hz2=0, hz6_to_hz3=0, hz6_to_hz4=0, hz6_to_hz5=0, hz6_to_hz6=0,
carrier1=1,carrier2=1,carrier3=1,carrier4=1,carrier5=1,carrier6=1;
var ctrls, mods, osc, osc_mix, aenv, chans, chan_vec;
ctrls = [[ Lag.kr(hz * hz1, 0.01), phase1, Lag.kr(amp1,0.01) ],
[ Lag.kr(hz * hz2, 0.01), phase2, Lag.kr(amp2,0.01) ],
[ Lag.kr(hz * hz3, 0.01), phase3, Lag.kr(amp3,0.01) ],
[ Lag.kr(hz * hz4, 0.01), phase4, Lag.kr(amp4,0.01) ],
[ Lag.kr(hz * hz5, 0.01), phase5, Lag.kr(amp5,0.01) ],
[ Lag.kr(hz * hz6, 0.01), phase6, Lag.kr(amp6,0.01) ]];
mods = [[hz1_to_hz1, hz1_to_hz2, hz1_to_hz3, hz1_to_hz4, hz1_to_hz5, hz1_to_hz6],
[hz2_to_hz1, hz2_to_hz2, hz2_to_hz3, hz2_to_hz4, hz2_to_hz5, hz2_to_hz6],
[hz3_to_hz1, hz3_to_hz2, hz3_to_hz3, hz3_to_hz4, hz3_to_hz5, hz3_to_hz6],
[hz4_to_hz1, hz4_to_hz2, hz4_to_hz3, hz4_to_hz4, hz4_to_hz5, hz4_to_hz6],
[hz5_to_hz1, hz5_to_hz2, hz5_to_hz3, hz5_to_hz4, hz5_to_hz5, hz5_to_hz6],
[hz6_to_hz1, hz6_to_hz2, hz6_to_hz3, hz6_to_hz4, hz6_to_hz5, hz6_to_hz6]];
osc = FM7.ar(ctrls, mods);
/////////////////////
chan_vec = [carrier1, carrier2, carrier3, carrier4, carrier5, carrier6];
osc_mix = Mix.new(chan_vec.collect({ |v,i| osc[i]*v }) );
/////////////////////
amp = Lag.ar(K2A.ar(amp), amplag);
aenv = EnvGen.ar(
Env.adsr( ampAtk, ampDec, ampSus, ampRel, 1.0, ampCurve),
gate, doneAction:2);
Out.ar(out, (osc_mix * aenv * amp).dup);
}).send(s);
you say
mix them into stereo
so then you need to add a Pan2 for each output or something. Mix will flatten only the outer array, correctly mixing an array of 2-ch arrays of ugens down to a single stereo array.
with panning per carrier:
SynthDef.new(\polyFM7_pan, {
arg out, amp=0.2, amplag=0.02, gate=1, hz,
hz1=1, hz2=2, hz3=3, hz4=4, hz5=5, hz6=6,
amp1=1,amp2=0.5,amp3=0.3,amp4=1,amp5=1,amp6=1,
phase1=0,phase2=0,phase3=0,phase4=0,phase5=0,phase6=0,
ampAtk=0.05, ampDec=0.1, ampSus=1.0, ampRel=1.0, ampCurve= -1.0,
hz1_to_hz1=0, hz1_to_hz2=0, hz1_to_hz3=0, hz1_to_hz4=0, hz1_to_hz5=0, hz1_to_hz6=0,
hz2_to_hz1=0, hz2_to_hz2=0, hz2_to_hz3=0, hz2_to_hz4=0, hz2_to_hz5=0, hz2_to_hz6=0,
hz3_to_hz1=0, hz3_to_hz2=0, hz3_to_hz3=0, hz3_to_hz4=0, hz3_to_hz5=0, hz3_to_hz6=0,
hz4_to_hz1=0, hz4_to_hz2=0, hz4_to_hz3=0, hz4_to_hz4=0, hz4_to_hz5=0, hz4_to_hz6=0,
hz5_to_hz1=0, hz5_to_hz2=0, hz5_to_hz3=0, hz5_to_hz4=0, hz5_to_hz5=0, hz5_to_hz6=0,
hz6_to_hz1=0, hz6_to_hz2=0, hz6_to_hz3=0, hz6_to_hz4=0, hz6_to_hz5=0, hz6_to_hz6=0,
carrier1, carrier2, carrier3, carrier4, carrier5, carrier6,
pan1=0,pan2=0,pan3=0,pan4=0,pan5=0,pan6=0;
var ctrls, mods, osc, osc_mix, aenv, chans, chan_vec, pan_vec;
ctrls = [[ Lag.kr(hz * hz1, 0.01), phase1, Lag.kr(amp1,0.01) ],
[ Lag.kr(hz * hz2, 0.01), phase2, Lag.kr(amp2,0.01) ],
[ Lag.kr(hz * hz3, 0.01), phase3, Lag.kr(amp3,0.01) ],
[ Lag.kr(hz * hz4, 0.01), phase4, Lag.kr(amp4,0.01) ],
[ Lag.kr(hz * hz5, 0.01), phase5, Lag.kr(amp5,0.01) ],
[ Lag.kr(hz * hz6, 0.01), phase6, Lag.kr(amp6,0.01) ]];
mods = [[hz1_to_hz1, hz1_to_hz2, hz1_to_hz3, hz1_to_hz4, hz1_to_hz5, hz1_to_hz6],
[hz2_to_hz1, hz2_to_hz2, hz2_to_hz3, hz2_to_hz4, hz2_to_hz5, hz2_to_hz6],
[hz3_to_hz1, hz3_to_hz2, hz3_to_hz3, hz3_to_hz4, hz3_to_hz5, hz3_to_hz6],
[hz4_to_hz1, hz4_to_hz2, hz4_to_hz3, hz4_to_hz4, hz4_to_hz5, hz4_to_hz6],
[hz5_to_hz1, hz5_to_hz2, hz5_to_hz3, hz5_to_hz4, hz5_to_hz5, hz5_to_hz6],
[hz6_to_hz1, hz6_to_hz2, hz6_to_hz3, hz6_to_hz4, hz6_to_hz5, hz6_to_hz6]];
osc = FM7.ar(ctrls, mods);
chan_vec = [carrier1, carrier2, carrier3, carrier4, carrier5, carrier6];
pan_vec = [pan1, pan2, pan3, pan4, pan5, pan6];
osc_mix = Mix.new(chan_vec.collect({ |v,i| Pan2.ar(osc[i]*v, pan_vec[i]) }) );
amp = Lag.ar(K2A.ar(amp), amplag);
aenv = EnvGen.ar(
Env.adsr( ampAtk, ampDec, ampSus, ampRel, 1.0, ampCurve),
gate, doneAction:2);
//// notice we removed `.dup` because `osc_mix` is now stereo
Out.ar(out, (osc_mix * aenv * amp));
}).send(s);
here’s a test:
x = Synth.new(\polyFM7_pan, [\gate, 1, \hz, 110]);
x.set(\hz2, 1.25);
x.set(\hz3, 2.0);
x.set(\hz4, 2.25);
x.set(\hz3, 3.0);
x.set(\hz4, 3.5);
x.set(\hz5, 4.5);
x.set(\hz6, 5.25);
Routine {
10.do {
// randomly change carrier amps a few times
6.do({|i| x.set(("carrier"++(i+1)).asSymbol, 0.2 + 0.5.rand) });
0.2.wait;
};
1.0.wait;
// randomly change carrier panning a few times.
10.do {
6.do({|i| x.set(("pan"++(i+1)).asSymbol, 1.0.rand2); });
0.2.wait;
};
}.play;
you could indeed make the synthdef less verbose with array arguments. but i dunno, it’s a tradeoff - maybe more confusing esp. when using ctl busses. in other words, dubious benefit unless you can actually send arrays as arguments at runtime. (and i bet your editor has a macro function.)
responding to code comments:
// set all the defaults. Why aren't these values the same as the the values for the SynthDef args?
// DRY it up?
the extra dict is there in polysub because 1) wanted to programmatically make commands, didn’t want to make command for every synthdef arg, 2) couldn’t figure out a way to pull the arg defaults out of the synthdef. you’re totally correct that this makes the synthdef defaults redundant, they could be removed
// the output bus, is this multiplication the right way to do this?
// oscilator times envelope times vca.
works for me
// set the amplitude to 0.2. Didn't we already set this somewhere else?
yes, looks like dbg cruft, my bad
// NodeWatcher informs the client of the server state, so we get free voice information from there?
yes. it is more convenient than setting up a dedicated trigger or something when the env stops. we want to wait til env really stops before updating voice allocation stuff