Concise workaround for `if` failing on an array of UGens?

I know what if actually does server-side, but since it does that and not work like the client-side boolean-if, I don’t see why it fails to apply to an array.

Basically

SynthDef(\meh, { var out = \out.ir(0), sig = In.ar(out); sig.if(0.1, 0.2); }) // ok 

SynthDef(\meh, { var out = \out.ir(0), sig = In.ar(out, 2); sig.if(0.1, 0.2); }) // error

// The preceding error dump is for ERROR: Message 'if' not understood.
// RECEIVER: [ an OutputProxy, an OutputProxy ]

Having to manually break out the ifs over an array can be really annoying e.g.

var outsig = [eSlopesDown[0].if(env2[0], max(insig[0], env2[0])), eSlopesDown[1].if(env2[1], max(insig[1], env2[1]))];

Is there a concise workaround for this issue with if not spanning an array of ugens?

sig.collect(if(_, 0.1, 0.2))?
Also, I don’t see why defining a local +SequenceableCollection { if {} } would disrupt anything else, though it would have the same limitations as SynthDef if.

1 Like

Unfortunately for the more complex expression which doesn’t involve just constants being returned by the if, it’s a little more hairy, but at least writeable as one expression (no copypasta):

var outsig = eSlopesDown collect: {|t,i| t.if(env2[i], max(insig[i], env2[i]))};

if for a UGen is really just linear interpolation: (true - false) * notReallyABoolean + false. (Tbh we should remove UGen:if because it’s totally misleading in this way.)

So maybe blend would do it: blend(false, true, notReallyABoolean). Haven’t tested that but this blend expression in principle should do the same thing as the arithmetic in the first paragraph.

hjh

1 Like

The funny thing is that the implementation expression (of if) actually vectorizes without further artifice, although there is a repeated sub-expression in it:

var msig = max(insig, env2), outsig = eSlopesDown * (env2 - msig) + msig;

And yeah, blend does the same thing (and vectorizes)

var outsig = blend(max(insig, env2), env2, eSlopesDown);

I’m not sure if blend vectorization doesn’t depend on the Jitlib quark being installed though; it uses it for its way of doing preset blending. (Aside: the Jitlib quark has a pretty odd implementation for that preset business: it overrides Dictionary.blend but never actually uses it, but blends arrays.)

I wasn’t sure the blendFrac (last arg vectorizes properly–jitlib only ever uses a scalar there) but it does actually work as expected

blend([0, 0], [1, 2], 0.5) // -> [ 0.5, 1.0 ]
blend([0, 0], [1, 2], [0.5, 0.25]) // -> [ 0.5, 0.5 ]

And the blend from SequenceableCollection is done via multiChannelPerform and doesn’t depend on any quark being installed (thankfully).

It would be wrong if it did depend on the quark: blend is listed in the documentation as an n-ary math operator, and math operators are in general intended to work on arrays as well. (if is not an n-ary operator so it isn’t subject to that requirement – which is why I think its use should be discouraged for UGens.)

blend is defined for Object – which probably wouldn’t pass code review now, but it’s the way James McCartney originally did it. (Also, SequenceableCollection:multiChannelPerform seems to exist only to prevent infinite recursion if the receiver is empty.)

hjh

I’ve actually done this now

+ SequenceableCollection {
	if { arg ... args; ^this.multiChannelPerform('if', *args) }
}

And it seems to work both client- and server-side:

[false, true].if("yay", "nay") // -> [ nay, yay ]

// not error anymore
SynthDef(\meh, { var out = \out.ir(0), sig = In.ar(out, 2); sig.if(0.1, 0.2); })

And

var outsig = eSlopesDown.if(env2, max(insig, env2))

now works (for multi-channel) but in hindsight the blend is more explicit as to the server-side if semantics…

I switched to Select / SelectX for this very reason, it does multi-channel expansion.
Note: no (0) before yes (1) !

1 Like

Actually UGen.blend overrides that, so I think multiChannelPerform is also redirecting into that and never calling Object’s blend when called from server-side/ugen. I could be wrong though.

Ugen.blend seems to (ultimately) call LinXFade2 for control signals and XFade2 (power based) for audio, which are a bit more limited (only 2 channel/gens are mixed). I have no idea if they’re faster than SelectX.

Exactly. I think this is one of those cases where users get more benefit from understanding how if resolves to a signal flow rather than a branching control… which may be best achieved by avoiding this use of if.

A fixed graph of continuous signal processors literally can’t express the idea of conditional evaluation. (It’s conceivable to switch off parts of a graph conditionally but then it isn’t a fixed graph anymore.)

It is possible, in a fixed graph, to express interpolation between two signals (blend) or conditionally using one or the other signal while ignoring the other (Select). I tend to think in this case: avoid cleverness and write what is really happening.

UGen:if lies to users. I really don’t like it and I think it would be a mistake to extend its use to arrays. Sorry if I’m beating a dead horse but IMO this is one of the big programming interface blunders in SC. (Though strongly held, it’s still just an opinion.)

hjh

It might actually happen to be correct in the sense of fuzzy logic, but I’m not knowledgeable enough in that area to say more.

I think the actual reason why if is iffy on ther sever is that someone might try to fix (borrowing this from an old tutorial)

(SynthDef(\kablooie, { |x = 0|
	var signal;
	if(x > 0, { signal = SinOsc.ar }, { signal = Saw.ar });
})); //err

with

(SynthDef(\kablooie, { |x = 0|
	var signal;
	if(x > 0, signal = SinOsc.ar, signal = Saw.ar);
})); // ok sans functions, but what will signal var be?

Which of course will always assign the same value to the signal variable (I think Saw.ar, i.e. rightmost one, but I don’t recall exactly if the order of function parameter evaluations is promised/specified in SC.)

The “old tutorial” is uncredited but it’s mine :wink:

hjh