I looked at this some more.
‘f’ and ‘s’ both use math operators on the source Ndef:
Ndef(\o) is the second operand of a binary operator.
Ndef(\f) is the only operand of the unary operator
So these are handled by the
AbstractOpPlug hierarchy – any math operator applied to any subclass of BusPlug will end up here.
UnaryOpPlug takes its number of channels from the source BusPlug.
BinaryOpPlug determines its number of channels as the larger of the two operands (ignoring operands that haven’t been initialized yet –
a = NodeProxy.new; b = NodeProxy.new; (a + b).numChannels is nil).
And, when a
*OpPlug is resolved into something that can plug into a UGen (by
.value), this requires resolving into the multichannel array.
So far so good.
‘z’ does not apply any math operators. So the Ndef is a direct input – that is, you’re not passing in the channels – you’re passing in some other object that represents the channels. By
asAudioRateInput, this other object is expected to convert itself into its channels.
JITLib reshaping logic appears to be that a source BusPlug should adapt itself to the number of channels that the target expects –
for is the signal processor for which the source is resolving itself.
UGens assume that they are single-channel. So
Ndef(\s) is trying to resolve itself down to a single channel “for CombN.” (I confirmed using some debugging traces that AbstractFunction:asAudioRateInput is receiving
for == CombN and subsequently, BusPlug:ar is being called with arg numChannels = 1 – so this is where it’s going askew.)
CombN.ar(Ndef(\s).ar) bypasses that logic –
ar converts the proxy into the array of two channels, and then CombN must be stereo.
It’s still a bit mysterious – it looks like kr and ar inputs are treated differently:
Ndef(\f).midicps is kr and resolves to two channels, but if I try
CombN.ar(Ndef(\s) + 0) to force a math operation, CombN still squeezes this down to one channel.
So my conclusion for now is that to be safe, UGens require UGens (not proxies), or arrays of UGens, as inputs, and to get these from a NodeProxy, best practice is to call the rate method.